Spring Boot源码分析-启动过程中我们进行了启动源码的分析,大致了解了整个Spring Boot的启动过程,具体细节这里不再赘述,感兴趣的同学可以自行阅读。今天让我们继续阅读源码,了解配置文件加载原理。

基于Spring Boot 2.1.0.RELEASE

在开始阅读源码之前,首先准备三个问题。

  1. 什么时候开始加载配置文件?
  2. 如何读取相关配置文件内容?
  3. 如何区分不同环境的配置?

下面用Spring代替Spring Boot

接下来进入主题,首先关注第一个问题。

一、什么时候开始加载配置文件?

Spring Boot源码分析-启动过程中我们可以得知,Spring在启动的过程中发布了ApplicationEnvironmentPreparedEvent事件,ConfigFileApplicationListener监听到这个消息的时候,开始实例化并调用(META-INF/spring.factories中定义)EnvironmentPostProcessorpostProcessEnvironment方法。而ConfigFileApplicationListener本身也实现了EnvironmentPostProcessor接口,且将自身加入到EnvironmentPostProcessor集合中,故也会调用自身的方法。

跟踪ConfigFileApplicationListenerpostProcessEnvironment方法源码

public void postProcessEnvironmen(ConfigurableEnvironment environment,
        SpringApplication application) {
    addPropertySources(environment,application.getResourceLoader());
}

继续跟踪addPropertySources方法

/**
 * Add config file property sources to the specified environment.
 * @param environment the environment to add source to
 * @param resourceLoader the resource loader
 * @see #addPostProcessors(ConfigurableApplicationContext)
 */
protected void addPropertySources(ConfigurableEnvironmentenvironment,
        ResourceLoader resourceLoader) {
    RandomValuePropertySource.addToEnvironmen(environment);
    new Loader(environment, resourceLoader).load();
}

从注释中我们可以看出,这个方法是将配置文件内容添加到指定的Environment中。到此为止,我们已经明白了Spring是在发布ApplicationEnvironmentPreparedEvent事件之后,才开始加载配置文件的。接下来开始关注第二个问题。

二、如何读取相关配置文件内容?

继续跟踪Loader源码,LoaderConfigFileApplicationListener的一个内部类,用来读取配置文件并配置相关环境。

首先跟踪Loader构造方法(注意load存在多个方法重载)

Loader(ConfigurableEnvironment environmentResourceLoader resourceLoader) {
    this.environment = environment;
    this.placeholdersResolver = nePropertySourcesPlaceholdersResolver(
            this.environment);
    this.resourceLoader = (resourceLoader != null) resourceLoader
            : new DefaultResourceLoader();
    // 实例化配置文件读取工具
    this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
            PropertySourceLoader.class, getClass.getClassLoader());
}

SpringFactoriesLoader.loadFactories获取META-INF/spring.factories中预定义的类

org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

从类名中可以看出这两个类主要是用来读取.properties.yml文件

继续跟踪load方法

public void load() {
    this.profiles = new LinkedList<>();
    this.processedProfiles = new LinkedList<>();
    this.activatedProfiles = false;
    this.loaded = new LinkedHashMap<>();
    initializeProfiles();
    while (!this.profiles.isEmpty()) {
        Profile profile = this.profiles.poll();
        if (profile != null &!profile.isDefaultProfile()) {
            addProfileToEnvironment(profile.getName(;
        }
        load(profile, this::getPositiveProfileFilter,
                addToLoad(MutablePropertySources::addLastfalse));
        this.processedProfiles.add(profile);
    }
    resetEnvironmentProfiles(this.processedProfiles);
    load(null, this::getNegativeProfileFilter,
            addToLoad(MutablePropertySources::addFirst, true));
    addLoadedPropertySources();
}

继续跟踪initializeProfiles方法

/**
 * Initialize profile information from both the {@link Environment} active
 * profiles and any {@code spring.profiles.active{@code spring.profiles.include}
 * properties that are already set.
 */
private void initializeProfiles() {
    // The default profile for these purposes irepresented as null. We add it
    // first so that it is processed first and halowest priority.
    this.profiles.add(null);
    Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
    this.profiles.addAll(getOtherActiveProfil(activatedViaProperty));
    // Any pre-existing active profiles set viproperty sources (e.g.
    // System properties) take precedence over thosadded in config files.
    addActiveProfiles(activatedViaProperty);
    if (this.profiles.size() == 1) { // only has nulprofile
        for (String defaultProfileName this.environment.getDefaultProfiles()) {
            Profile defaultProfile = new Profi(defaultProfileName, true);
            this.profiles.add(defaultProfile);
        }
    }
}

从注释中我们可以了解到这个方法用来初始化profile。继续往下看Spring如何初始化profile。接着跟踪getProfilesActivatedViaProperty方法。

private Set<Profile> getProfilesActivatedViaProperty {
    if (!this.environment.containsProper(ACTIVE_PROFILES_PROPERTY)
            && !this.environment.containsProper(INCLUDE_PROFILES_PROPERTY)) {
        return Collections.emptySet();
    }
    Binder binder = Binder.get(this.environment);
    Set<Profile> activeProfiles = new LinkedHashSet();
    activeProfiles.addAll(getProfiles(binderINCLUDE_PROFILES_PROPERTY));
    activeProfiles.addAll(getProfiles(binderACTIVE_PROFILES_PROPERTY));
    return activeProfiles;
}

Environment目前没有读取配置文件,故这里返回一个空集合。继续回到上面的方法,跟踪addActiveProfiles方法

void addActiveProfiles(Set<Profile> profiles) {
    if (profiles.isEmpty()) {
        return;
    }
    if (this.activatedProfiles) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Profiles alreadactivated, '" + profiles
                    + "' will not be applied");
        }
        return;
    }
    this.profiles.addAll(profiles);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Activated activeProfiles "
                StringUtils.collectionToCommaDelimitString(profiles));
    }
    this.activatedProfiles = true;
    removeUnprocessedDefaultProfiles();
}

上面分析得知profiles是一个空集合,所以这里不会继续往下执行。再回到上面方法。

private void initializeProfiles() {
    this.profiles.add(null);
    Set<Profile> activatedViaProperty getProfilesActivatedViaProperty();
    this.profiles.addAll(getOtherActiveProfil(activatedViaProperty));
    addActiveProfiles(activatedViaProperty);
    if (this.profiles.size() == 1) {
        for (String defaultProfileName : this.environment.getDefaultProfiles()) {
            Profile defaultProfile = new Profi(defaultProfileName, true);
            this.profiles.add(defaultProfile);
        }
    }
}

因为profiles添加了一个null,所以if条件成立,遍历environment中默认的profile,默认的profile是什么呢?

通过查看AbstractEnvironment源码得知,默认profiledefault

protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";

private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles());

protected Set<String> getReservedDefaultProfiles() {
    return Collections.singleto(RESERVED_DEFAULT_PROFILE_NAME);
}

继续回到上面方法,往profiles添加了一个default profile,这时候profiles里面已经有了两个元素,nulldefault

接下来回到load方法,关注while循环

public void load() {
    this.profiles = new LinkedList<>();
    this.processedProfiles = new LinkedList<>();
    this.activatedProfiles = false;
    this.loaded = new LinkedHashMap<>();
    initializeProfiles();
    while (!this.profiles.isEmpty()) {
        Profile profile = this.profiles.poll();
        if (profile != null &!profile.isDefaultProfile()) {
            addProfileToEnvironment(profile.getName(;
        }
        load(profile, this::getPositiveProfileFilter,
                addToLoad(MutablePropertySources::addLastfalse));
        this.processedProfiles.add(profile);
    }
    resetEnvironmentProfiles(this.processedProfiles);
    load(null, this::getNegativeProfileFilter,
            addToLoad(MutablePropertySources::addFirst, true));
    addLoadedPropertySources();
}

从上面的分析已经可以知道profiles中的第一个元素实际上是null,所以直接进入load方法

private void load(Profile profileDocumentFilterFactory filterFactory,
        DocumentConsumer consumer) {
    getSearchLocations().forEach((location) -> {
        boolean isFolder = location.endsWith("/");
        Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
        names.forEach(
                (name) -> load(location, name, profile, filterFactory, consumer));
    });
}

先看看getSearchLocations返回的内容

public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";

public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";

private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

private Set<String> getSearchLocations() {
    if (this.environment.containsProper(CONFIG_LOCATION_PROPERTY)) {
        return getSearchLocatio(CONFIG_LOCATION_PROPERTY);
    }
    Set<String> locations = getSearchLocations(
            CONFIG_ADDITIONAL_LOCATION_PROPERTY);
    locations.addAll(
            asResolvedSet(ConfigFileApplicationListener.thisearchLocations,
                    DEFAULT_SEARCH_LOCATIONS));
    return locations;
}

从上面可知,environment目前还没有读取到配置文件内容,所以不会进入if条件,同理可知Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY)实际上也是一个空集合。

asResolvedSet返回的是DEFAULT_SEARCH_LOCATIONS对应的四个配置文件位置。

回到load方法

private void load(Profile profileDocumentFilterFactory filterFactory,
        DocumentConsumer consumer) {
    getSearchLocations().forEach((location) -> {
        boolean isFolder = location.endsWith("/");
        Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
        names.forEach(
                (name) -> load(location, name, profile, filterFactory, consumer));
    });
}

这里的isFolder都是true,跟踪getSearchNames方法

private static final String DEFAULT_NAMES = "application";

private Set<String> getSearchNames() {
    if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
        String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
        return asResolvedSet(property, null);
    }
    return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}

从这里可以看出来getSearchNames返回的集合只包含一个application。继续跟踪load方法

private void load(String location, String nameProfile profile,
        DocumentFilterFactory filterFactoryDocumentConsumer consumer) {
    if (!StringUtils.hasText(name)) {
        for (PropertySourceLoader loader this.propertySourceLoaders) {
            if (canLoadFileExtension(loader, locatio) {
                load(loader, location, profile,
                        filterFactorgetDocumentFilter(profile)consumer);
                return;
            }
        }
    }
    Set<String> processed = new HashSet<>();
    for (PropertySourceLoader loader this.propertySourceLoaders) {
        for (String fileExtension loader.getFileExtensions()) {
            if (processed.add(fileExtension)) {
                loadForFileExtension(loader, locatio+ name, "." + fileExtension,
                        profile, filterFactoryconsumer);
            }
        }
    }
}

从上面可以得知,nameProfile的值实际上是application,所以直接跟踪下面的for循环。

Loader的构造方法可知,propertySourceLoaders

org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

从类名可知PropertiesPropertySourceLoader解析properties文件,YamlPropertySourceLoader解析yml文件,但是PropertiesPropertySourceLoader还可以解析xml文件。

public String[] getFileExtensions() {
    return new String[] { "properties", "xml" };
}

继续跟踪loadForFileExtension

private void loadForFileExtensi(PropertySourceLoader loader, String prefix,
        String fileExtension, Profile profile,
        DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
    DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
    if (profile != null) {
        // Try profile-specific file & profile section in profile file (gh-340)
        String profileSpecificFile = prefix + "-" + profile + fileExtension;
        load(loader, profileSpecificFile, profile, defaultFilter, consumer);
        load(loader, profileSpecificFile, profile, profileFilter, consumer);
        // Try profile specific sections in files we've already processed
        for (Profile processedProfile : this.processedProfiles) {
            if (processedProfile != null) {
                String previouslyLoaded = prefix + "-" + processedProfile
                        + fileExtension;
                load(loader, previouslyLoaded, profile, profileFilter, consumer);
            }
        }
    }
    // Also try the profile-specific section (if any) of the normal file
    load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

从上面的分析可知,当前profilenull,所以继续跟踪load方法

private void load(PropertySourceLoader loader, String location, Profile profile,
        DocumentFilter filter, DocumentConsumer consumer) {
    try {
        Resource resource = this.resourceLoader.getResource(location);
        if (resource == null || !resource.exists()) {
            if (this.logger.isTraceEnabled()) {
                StringBuilder description = getDescription(
                        "Skipped missing config ", location, resource, profile);
                this.logger.trace(description);
            }
            return;
        }
        if (!StringUtils.hasText(
                StringUtils.getFilenameExtension(resource.getFilename()))) {
            if (this.logger.isTraceEnabled()) {
                StringBuilder description = getDescription(
                        "Skipped empty config extension ", location, resource,
                        profile);
                this.logger.trace(description);
            }
            return;
        }
        String name = "applicationConfig: [" + location + "]";
        // 开始读取文件内容
        List<Document> documents = loadDocuments(loader, name, resource);
        if (CollectionUtils.isEmpty(documents)) {
            if (this.logger.isTraceEnabled()) {
                StringBuilder description = getDescription(
                        "Skipped unloaded config ", location, resource, profile);
                this.logger.trace(description);
            }
            return;
        }
        List<Document> loaded = new ArrayList<>();
        for (Document document : documents) {
            if (filter.match(document)) {
                addActiveProfiles(document.getActiveProfiles());
                addIncludedProfiles(document.getIncludeProfiles());
                loaded.add(document);
            }
        }
        Collections.reverse(loaded);
        if (!loaded.isEmpty()) {
            loaded.forEach((document) -> consumer.accept(profile, document));
            if (this.logger.isDebugEnabled()) {
                StringBuilder description = getDescription("Loaded config file ",
                        location, resource, profile);
                this.logger.debug(description);
            }
        }
    }
    catch (Exception ex) {
        throw new IllegalStateException("Failed to load property "
                + "source from location '" + location + "'", ex);
    }
}

继续跟踪loadDocuments方法

private List<Document> loadDocuments(PropertySourceLoader loader, String name,
        Resource resource) throws IOException {
    DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
    List<Document> documents = this.loadDocumentsCache.get(cacheKey);
    if (documents == null) {
        // PropertySource 用来存储配置项
        List<PropertySource<?>> loaded = loader.load(name, resource);
        documents = asDocuments(loaded);
        this.loadDocumentsCache.put(cacheKey, documents);
    }
    return documents;
}

读取配置的时候首先看是否存在缓存,如果不存在,则调用loader.load方法。通过上面的分析可知loader对象实际上是PropertiesPropertySourceLoaderYamlPropertySourceLoader,我们这里的配置文件是properties文件,所以我们选择跟踪PropertiesPropertySourceLoaderload方法。

public List<PropertySource<?>> load(String name, Resource resource)
        throws IOException {
    // 调用loadProperties方法读取配置文件
    Map<String, ?> properties = loadProperties(resource);
    if (properties.isEmpty()) {
        return Collections.emptyList();
    }
    return Collections
            .singletonList(new OriginTrackedMapPropertySource(name, properties));
}

private Map<String, ?> loadProperties(Resource resource) throws IOException {
    String filename = resource.getFilename();
    if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
        // 读取配置文件
        return (Map) PropertiesLoaderUtils.loadProperties(resource);
    }
    return new OriginTrackedPropertiesLoader(resource).load();
}

可以看出PropertiesPropertySourceLoader是通过PropertiesLoaderUtils.loadProperties读取配置文件,继续跟踪loadProperties

/**
 * Load properties from the given resource (in ISO-8859-1 encoding).
 * @param resource the resource to load from
 * @return the populated Properties instance
 * @throws IOException if loading failed
 * @see #fillProperties(java.util.Properties, Resource)
 */
public static Properties loadProperties(Resource resource) throws IOException {
    Properties props = new Properties();
    fillProperties(props, resource);
    return props;
}

private Map<String, ?> loadProperties(Resource resource) throws IOException {
    String filename = resource.getFilename();
    if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
        // 读取XML格式文件
        return (Map) PropertiesLoaderUtils.loadProperties(resource);
    }
    return new OriginTrackedPropertiesLoader(resource).load();
}

首先从注释中,得知Spring是以ISO-8859-1编码读取配置文件内容的,所以当我们在application.properties中写入中文,会发现在读取的时候中文都变成了乱码。这里只是通过注释得知的,如何寻找确凿的证据呢?继续跟踪OriginTrackedPropertiesLoaderload方法

public Map<String, OriginTrackedValue> load(boolean expandLists) throws IOException {
    // 创建字符读取Reader
    try (CharacterReader reader = new CharacterReader(this.resource)) {
        Map<String, OriginTrackedValue> result = new LinkedHashMap<>();
        StringBuilder buffer = new StringBuilder();
        while (reader.read()) {
            String key = loadKey(buffer, reader).trim();
            if (expandLists && key.endsWith("[]")) {
                key = key.substring(0, key.length() - 2);
                int index = 0;
                do {
                    OriginTrackedValue value = loadValue(buffer, reader, true);
                    put(result, key + "[" + (index++) + "]", value);
                    if (!reader.isEndOfLine()) {
                        reader.read();
                    }
                }
                while (!reader.isEndOfLine());
            }
            else {
                OriginTrackedValue value = loadValue(buffer, reader, false);
                put(result, key, value);
            }
        }
        return result;
    }
}

为了寻找乱码的原因,我们继续跟踪CharacterReader构造方法

CharacterReader(Resource resource) throws IOException {
    // InputStreamReader以ISO-8859-1读取内容
    this.reader = new LineNumberReader(new InputStreamReader(
            resource.getInputStream(), StandardCharsets.ISO_8859_1));
}

看到这里我们终于明白了,原来是CharacterReader在读取文件内容的时候采用了ISO-8859-1编码,所以才导致中文乱码的原因。

明白了乱码原因之后,在回到上面的方法观察loadKey方法读取=前面的内容作为配置项名称,并且支持数组(配置项名称以[]结尾)。loadKey如何读取到key的呢?

private String loadKey(StringBuilder buffer, CharacterReader reader)
        throws IOException {
    // 有效char的数量,设置成0,相当于清空buffer,但实际字符还是存在StringBuilder中,只不过生成String的时候过滤了 >count 的字符
    buffer.setLength(0);
    boolean previousWhitespace = false;
    while (!reader.isEndOfLine()) {
        // 是否是分隔符
        if (reader.isPropertyDelimiter()) {
            reader.read();
            return buffer.toString();
        }
        // 是否是空格
        if (!reader.isWhiteSpace() && previousWhitespace) {
            return buffer.toString();
        }
        previousWhitespace = reader.isWhiteSpace();
        // 添加当前字符到buffer
        buffer.append(reader.getCharacter());
        reader.read();
    }
    return buffer.toString();
}

reader.isPropertyDelimiter用来判断当前字符是否是key/value分隔符,如果是则说明已经读取到完整的key,继续读取下一个字符,直到读取到完整的key

接下来就要读取value的值了(数组配置项的值是什么格式呢?)

继续跟踪loadValue方法

private OriginTrackedValue loadValue(StringBuilder buffer, CharacterReader reader,
        boolean splitLists) throws IOException {
    buffer.setLength(0);
    while (reader.isWhiteSpace() && !reader.isEndOfLine()) {
        reader.read();
    }
    Location location = reader.getLocation();
    while (!reader.isEndOfLine() && !(splitLists && reader.isListDelimiter())) {
        buffer.append(reader.getCharacter());
        reader.read();
    }
    Origin origin = new TextResourceOrigin(this.resource, location);
    return OriginTrackedValue.of(buffer.toString(), origin);
}

public boolean isListDelimiter() {
    // 数组配置分隔符
    return !this.escaped && this.character == ',';
}

这里的location是什么意思呢?继续跟踪Location类的定义

public static final class Location {
    private final int line;
    private final int column;
    // 其余内容省略
}

从这里可以看出Location实际记录了当前reader读取到的行和列的值。

继续回到上面的方法,可以发现读取value的方式实际和读取key相似,这里不再赘述,相信大家都能够看明白。

PropertiesPropertySourceLoader就基本完成了properties文件的读取。YamlPropertySourceLoader配置文件的加载逻辑类似,大家可以自行阅读相关源码。到此为止,我们也明白了第二个问题“如何读取相关配置文件内容?”。接下来关注第三个问题。

三、如何区分不同环境的配置?

假设我们在项目中存在两个多个配置文件

  • application.properties
spring.profiles.active=dev
  • application-dev.properties
a=dev
  • application-test.properties
a=test

通过之前的代码分析,我们可以知道初始状态下profiles存在两个值nulldefault,首先默认加载的是application.properties文件,从该文件中可以读取到spring.profiles.active配置项,然后将读取到的profile设置为当前激活的profile

for (Document document : documents) {
    if (filter.match(document)) {
        // 获取配置文件中设置的profile
        addActiveProfiles(document.getActiveProfiles());
        addIncludedProfiles(document.getIncludeProfiles());
        loaded.add(document);
    }
}

void addActiveProfiles(Set<Profile> profiles) {
    if (profiles.isEmpty()) {
        return;
    }
    if (this.activatedProfiles) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Profiles already activated, '" + profiles
                    + "' will not be applied");
        }
        return;
    }
    this.profiles.addAll(profiles);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Activated activeProfiles "
                + StringUtils.collectionToCommaDelimitedString(profiles));
    }
    this.activatedProfiles = true;
    // 移除默认的default
    removeUnprocessedDefaultProfiles();
}

private void removeUnprocessedDefaultProfiles() {
    this.profiles.removeIf(
            (profile) -> (profile != null && profile.isDefaultProfile()));
}

从上面的代码中可以看出来,读取完默认的配置文件之后,将原有的default移除,添加读取到的profileprofiles,接着回到开始的load方法

public void load() {
    this.profiles = new LinkedList<>();
    this.processedProfiles = new LinkedList<>();
    this.activatedProfiles = false;
    this.loaded = new LinkedHashMap<>();
    initializeProfiles();
    while (!this.profiles.isEmpty()) {
        // 第一次循环的时候,profile的值为null
        // 第二次循环的时候,profile的值为application.properties中配置的值
        Profile profile = this.profiles.poll();
        if (profile != null && !profile.isDefaultProfile()) {
            addProfileToEnvironment(profile.getName());
        }
        load(profile, this::getPositiveProfileFilter,
                addToLoaded(MutablePropertySources::addLast, false));
        this.processedProfiles.add(profile);
    }
    resetEnvironmentProfiles(this.processedProfiles);
    load(null, this::getNegativeProfileFilter,
            addToLoaded(MutablePropertySources::addFirst, true));
    addLoadedPropertySources();
}

所以,当默认配置文件中设置了激活的profile,接下来就会去读取该文件内容。在本例中,第二次循环读取的就是application-dev.properties文件,而application-test.properties不会被读取。这样就实现了根据profile读取不同环境的配置文件。

这时候我们再考虑一个问题,如果在application.propertiesapplication-dev.properties同时添加相同的key,但value不同的配置,哪一个配置会生效呢?基于目前的分析来看,两个配置都已经被读取了,怎么决定优先级呢?

实际上application-dev.properties中的配置会生效,为了搞清楚这个问题,我们继续往下跟踪addLoadedPropertySources方法

/**
 * 已经读取到的配置
 */
private Map<Profile, MutablePropertySources> loaded;

private void addLoadedPropertySources() {
    MutablePropertySources destination = this.environment.getPropertySources();
    List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
    // 反转集合
    Collections.reverse(loaded);
    String lastAdded = null;
    Set<String> added = new HashSet<>();
    for (MutablePropertySources sources : loaded) {
        for (PropertySource<?> source : sources) {
            if (added.add(source.getName())) {
                addLoadedPropertySource(destination, lastAdded, source);
                lastAdded = source.getName();
            }
        }
    }
}

private void addLoadedPropertySource(MutablePropertySources destination,
        String lastAdded, PropertySource<?> source) {
    if (lastAdded == null) {
        if (destination.contains(DEFAULT_PROPERTIES)) {
            destination.addBefore(DEFAULT_PROPERTIES, source);
        }
        else {
            // 从尾部添加
            destination.addLast(source);
        }
    }
    else {
        // 从指定位置之后添加
        destination.addAfter(lastAdded, source);
    }
}

loaded对象保存了之前读取到的配置。从这里可以看出是将loaded中读取到的配置文件添加到environment中,并且都是从尾部添加。首先我们要明白一点,PropertySourceMutablePropertySources中的顺序决定了它的优先级,也就是说越靠前优先级越高。那么我们会想,loaded中的元素顺序应该是application.properties -> application-dev.properties,所以application.properties优先级更高,这显然不合符实际情况。

再回到上面的代码中可以看到Collections.reverse(loaded),到这里我们就明白了,添加的顺序和读取的顺序正好是相反的,所以后读取到的application-dev.properties反而先添加到destination中,所以applicaiton-dev.properties的优先级比application.properties高。

到此我们已经完全明白了这三个问题,顺便还搞清楚了为什么properties里面的中文会乱码的原因。

  1. 什么时候开始加载配置文件?
  2. 如何读取相关配置文件内容?
  3. 如何区分不同环境的配置?

中间涉及的源码非常多,而且方法名称相似,很容易让人迷惑,所以需要大家仔细多读,才能完全理解整个的流程。

本文由博客一文多发平台 OpenWrite 发布!

Spring Boot源码分析-配置文件加载原理的更多相关文章

  1. 精尽Spring Boot源码分析 - 配置加载

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  2. Spring Boot源码分析-启动过程

    Spring Boot作为目前最流行的Java开发框架,秉承"约定优于配置"原则,大大简化了Spring MVC繁琐的XML文件配置,基本实现零配置启动项目. 本文基于Spring ...

  3. 精尽Spring Boot源码分析 - 文章导读

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  4. 精尽Spring Boot源码分析 - Jar 包的启动实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  5. 精尽Spring Boot源码分析 - SpringApplication 启动类的启动过程

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  6. 精尽Spring Boot源码分析 - 日志系统

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  7. 精尽Spring Boot源码分析 - @ConfigurationProperties 注解的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  8. 精尽Spring Boot源码分析 - 序言

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  9. 精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

随机推荐

  1. 8.6 JavaScript之HTML的DOM(三)

    8.5 HTML和JavaScript的DOM(三) 一.DOM 是Document Object Model( 文档对象模型 )的缩写. DOM把所有的html都转换为节点 .整个文档 是一个节点 ...

  2. python性能测试值timeit的使用示例

    from timeit import Timer def t1(): li = [] for i in range(10000): li.append(i) def t2(): li = [] for ...

  3. 关于Math.random()

    关于 Math.random() ,以前经常搞混淆,这次写个笔记专门记录下: Math.random()  : 返回的是 0~1 之间的一个随机小数0<=r<1,即[0,1); 注意:这里 ...

  4. Download google drive public shared file in terminal

    http://unix.stackexchange.com/questions/136371/how-to-download-a-folder-from-google-drive-using-term ...

  5. matlab gui界面设计记录

    我们要进行的程序是彩色图像处理试验示例,用这个程序来练习我们的gui前台设计. 程序功能介绍:具有彩色图像处理及保存和音乐播放功能效果如下图 2 在MATLAB的命令窗口中输入guide命令,打开gu ...

  6. mariadb数据库(1)

    一.什么是数据库? 简单的说,数据库就是一个存放数据的仓库,这个仓库是按照一定的数据结构(数据结构是指数据的组织形式或数据之间的联系)来组织,存储的,我们可以通过数据库提供的多种方法来管理数据库里的数 ...

  7. python 学习记录1

    存储 序号   分类    技术      用途 01      存储     Number  数字(不可变) String     字符串(不可变) List          列表 Tuple   ...

  8. Android MVC MVP MVVM (二)

    MVP模型 View主要是Activity,Fragment MVP和MVC的差别 1.Model和View不再直接通信,通过中间层Presenter来实现. 2.Activity的功能被简化,不再充 ...

  9. 一次JDBC支持表情存储的配置过程

    公司的一个项目,一开始没有考虑到内容字段支持表情,有一个接入方的内容含有表情要支持下 项目是基于Springboot的. 方案1先尝试直接配置数据库连接 shardingsphere: datasou ...

  10. Linux下查看文件编码及批量修改编码

    查看文件编码在Linux中查看文件编码可以通过以下几种方式:1.在Vim中可以直接查看文件编码:set fileencoding即可显示文件编码格式.如果你只是想查看其它编码格式的文件或者想解决用Vi ...