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. SpringJunitTest

    1.用MockBean和assert,而不是输出 import org.springframework.boot.test.mock.mockito.MockBean;MockBean import ...

  2. Beta冲刺(1/4)

    队名:福大帮 组长博客链接: https://www.cnblogs.com/mhq-mhq/p/11990568.html 作业博客 : https://edu.cnblogs.com/campus ...

  3. python——装饰器(不定长参数,闭包,装饰器)示例

    def func(functionName): print("正在装饰") def func_in(*args, **kargs): print("------func_ ...

  4. 自定义vue-cli生成项目模板配置(1)

    最近在读<变量>,目前得到的认知之一:慢变量才是决定事物长期发展的因素. 打算自定义vue-cli的脚手架或者根据自己的需要设置项目模板的相关参数,很大程度与慢变量这个概念相关. 当然,我 ...

  5. RDS数据库全量恢复方案

    一.全量恢复 恢复最近的快照,将快找之前的数据全量恢复 二.增量恢复 下载对应的binlog日志导入到数据库 三.还没有备份的binlog日志获取方法 首先连接 RDS for MySQL 后查看当前 ...

  6. DML语句

    DML 操作是指对数据库中表记录的操作,主要包括表记录的插入(insert).更新(update).删除(delete)和查询(select),是开发人员日常使用最频繁的操作. 插入记录 表创建好后, ...

  7. 基于Python使用scrapy-redis框架实现分布式爬虫

    1.首先介绍一下:scrapy-redis框架 scrapy-redis:一个三方的基于redis的分布式爬虫框架,配合scrapy使用,让爬虫具有了分布式爬取的功能.github地址: https: ...

  8. 如何快速通过json构建javabean对象(Intellij IDEA-->GsonFormat使用教程)

    和第三方对接的时候,返回给我们的json时参数字段多是很常见的现象,所以我们手动去创建javabean肯定是要花费不少时间,博主在网上找到了很多种,可用通过json自动生成javabean的工具,这里 ...

  9. HDFS的基础与操作

    一 HDFS概念 1.1 概念 HDFS,它是一个文件系统,全称:Hadoop Distributed File System,用于存储文件通过目录树来定位文件:其次,它是分布式的,由很多服务器联合起 ...

  10. vue项目报错:Unexpected tab character (no-tabs)

    eslint意思是检查规范代码 第一种方法: 新建项目的时候 第二种方法: 首先在项目的根目录下.eslintrc.js中加入一行代码:"no-tabs":"off&qu ...