精盡Spring Boot源碼分析 – 配置加載

該系列文章是筆者在學習 Spring Boot 過程中總結下來的,裏面涉及到相關源碼,可能對讀者不太友好,請結合我的源碼注釋 Spring Boot 源碼分析 GitHub 地址 進行閱讀

Spring Boot 版本:2.2.x

最好對 Spring 源碼有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 – 文章導讀》 系列文章

如果該篇內容對您有幫助,麻煩點擊一下「推薦」,也可以關注博主,感激不盡~

該系列其他文章請查看:《精盡 Spring Boot 源碼分析 – 文章導讀》

概述

在我們的 Spring Boot 應用中,可以很方便的在 application.ymlapplication.properties 文件中添加需要的配置信息,並應用於當前應用。那麼,對於 Spring Boot 是如何加載配置文件,如何按需使指定環境的配置生效的呢?接下來,我們一起來看看 Spring Boot 是如何加載配置文件的。

提示:Spring Boot 加載配置文件的過程有點繞,本篇文章有點長,可選擇性的跳過 Loader 這一小節

回顧

回到前面的 《SpringApplication 啟動類的啟動過程》 這篇文章,Spring Boot 啟動應用的入口和主流程都是在 SpringApplication#run(String.. args) 方法中。

在這篇文章的 6. prepareEnvironment 方法 小節中可以講到,會對所有的 SpringApplicationRunListener 廣播 應用環境已準備好 的事件,如下:

// SpringApplicationRunListeners.java
void environmentPrepared(ConfigurableEnvironment environment) {
    // 只有一個 EventPublishingRunListener 對象
    for (SpringApplicationRunListener listener : this.listeners) {
        listener.environmentPrepared(environment);
    }
}

只有一個 EventPublishingRunListener 事件發佈器,裏面有一個事件廣播器,封裝了幾個 ApplicationListener 事件監聽器,如下:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

其中有一個 ConfigFileApplicationListener 對象,監聽到上面這個事件,會去解析 application.yml 等應用配置文件的配置信息

在 Spring Cloud 還會配置一個 BootstrapApplicationListener 對象,監聽到上面的這個事件會創建一個 ApplicationContext 作為當前 Spring 應用上下文的父容器,同時會讀取 bootstrap.yml 文件的信息

ConfigFileApplicationListener

org.springframework.boot.context.config.ConfigFileApplicationListener,Spring Boot 的事件監聽器,主要用於加載配置文件到 Spring 應用中

相關屬性

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {

	/** 默認值的 PropertySource 在 Environment 中的 key */
	private static final String DEFAULT_PROPERTIES = "defaultProperties";

	// Note the order is from least to most specific (last one wins)
	/** 支持的配置文件的路徑 */
	private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

	/** 配置文件名稱(不包含後綴) */
	private static final String DEFAULT_NAMES = "application";

	private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);

	private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);

	private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);

	/** 需要過濾的配置項 */
	private static final Set<String> LOAD_FILTERED_PROPERTY;

	static {
		Set<String> filteredProperties = new HashSet<>();
		filteredProperties.add("spring.profiles.active");
		filteredProperties.add("spring.profiles.include");
		LOAD_FILTERED_PROPERTY = Collections.unmodifiableSet(filteredProperties);
	}

	/**
	 * The "active profiles" property name.
	 * 可通過該屬性指定配置需要激活的環境配置
	 */
	public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";

	/**
	 * The "includes profiles" property name.
	 */
	public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";

	/**
	 * The "config name" property name.
	 * 可通過該屬性指定配置文件的名稱
	 */
	public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

	/**
	 * The "config location" property name.
	 */
	public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";

	/**
	 * The "config additional location" property name.
	 */
	public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";

	/**
	 * The default order for the processor.
	 */
	public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;

	private final DeferredLog logger = new DeferredLog();

	private String searchLocations;

	private String names;

	private int order = DEFAULT_ORDER;
    
    @Override
	public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
		return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
				|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);
	}
}

屬性不多,幾個關鍵的屬性都有注釋,同時支持處理的事件有 ApplicationEnvironmentPreparedEvent 和 ApplicationPreparedEvent

我們看到它實現了 EnvironmentPostProcessor 這個接口,用於對 Environment 進行後置處理,在刷新 Spring 應用上下文之前

1. onApplicationEvent 方法

onApplicationEvent(ApplicationEvent) 方法,ApplicationListener 處理事件的方法,如下:

@Override
public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    }
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent(event);
    }
}

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    // <1> 通過類加載器從 `META-INF/spring.factories` 文件中獲取 EnvironmentPostProcessor 類型的類名稱,並進行實例化
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    // <2> 當前對象也是 EnvironmentPostProcessor 實現類,添加進去
    postProcessors.add(this);
    // <3> 將這些 EnvironmentPostProcessor 進行排序
    AnnotationAwareOrderComparator.sort(postProcessors);
    // <4> 遍歷這些 EnvironmentPostProcessor 依次對 Environment 進行處理
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        // <4.1> 依次對當前 Environment 進行處理,上面第 `2` 步添加了當前對象,我們直接看到當前類的這個方法
        postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
    }
}

我們在前面 回顧 中講到,會廣播一個 應用環境已準備好 的事件,也就是 ApplicationEnvironmentPreparedEvent 事件

處理該事件的過程如下:

  1. 通過類加載器從 META-INF/spring.factories 文件中獲取 EnvironmentPostProcessor 類型的類名稱,並進行實例化

    List<EnvironmentPostProcessor> loadPostProcessors() {
        return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
    }
    
  2. 當前對象也是 EnvironmentPostProcessor 實現類,添加進去

  3. 將這些 EnvironmentPostProcessor 進行排序

  4. 遍歷這些 EnvironmentPostProcessor 依次對 Environment 進行處理

    1. 依次對當前 Environment 進行處理,上面第 2 步添加了當前對象,我們直接看到當前類的這個方法

2. postProcessEnvironment 方法

postProcessEnvironment(ConfigurableEnvironment, SpringApplication) 方法,實現 EnvironmentPostProcessor 接口的方法,對 Environment 進行後置處理

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    // 為 Spring 應用的 Environment 環境對象添加屬性(包括 `application.yml`)
    addPropertySources(environment, application.getResourceLoader());
}

直接調用 addPropertySources(..) 方法,為當前 Spring 應用的 Environment 環境對象添加屬性(包括 application.yml 配置文件的解析)

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
   // <1> 往 Spring 應用的 Environment 環境對象添加隨機值的 RandomValuePropertySource 屬性源
   // 這樣就可直接通過 `@Value(random.uuid)` 隨機獲取一個 UUID
   RandomValuePropertySource.addToEnvironment(environment);
   // <2> 創建一個 Loader 對象,設置佔位符處理器,資源加載器,PropertySourceLoader 配置文件加載器
   // <3> 加載配置信息,並放入 Environment 環境對象中
   // 整個處理過程有點繞,嵌套有點深,你可以理解為會將你的 Spring Boot 或者 Spring Cloud 的配置文件加載到 Environment 中,並激活對應的環境
   new Loader(environment, resourceLoader).load();
}

過程如下:

  1. 往 Spring 應用的 Environment 環境對象添加隨機值的 RandomValuePropertySource 屬性源,這樣就可直接通過 @Value(random.uuid) 隨機獲取一個 UUID

    // RandomValuePropertySource.java
    public static void addToEnvironment(ConfigurableEnvironment environment) {
        environment.getPropertySources().addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
                new RandomValuePropertySource(RANDOM_PROPERTY_SOURCE_NAME));
        logger.trace("RandomValuePropertySource add to Environment");
    }
    

    邏輯很簡單,感興趣的可以去看看

  2. 創建一個 Loader 對象,設置佔位符處理器,資源加載器,PropertySourceLoader 配置文件加載器

  3. 調用這個 Loader 的 load() 方法,加載配置信息,並放入 Environment 環境對象中

加載配置信息的過程有點繞嵌套有點深,你可以先理解為,將你的 Spring Boot 或者 Spring Cloud 的配置文件加載到 Environment 中,並激活對應的環境

Loader

org.springframework.boot.context.config.ConfigFileApplicationListener.Loader,私有內部類,配置文件的加載器

構造方法

private class Loader {
    
    /** 環境對象 */
    private final ConfigurableEnvironment environment;
    
    /** 佔位符處理器 */
    private final PropertySourcesPlaceholdersResolver placeholdersResolver;
    
    /** 資源加載器 */
    private final ResourceLoader resourceLoader;
    
    /** 屬性的資源加載器 */
    private final List<PropertySourceLoader> propertySourceLoaders;
    
    /** 待加載的 Profile 隊列 */
    private Deque<Profile> profiles;
    
    /** 已加載的 Profile 隊列 */
    private List<Profile> processedProfiles;
    
    /** 是否有激活的 Profile */
    private boolean activatedProfiles;
    
    /** 保存每個 Profile 對應的屬性信息 */
    private Map<Profile, MutablePropertySources> loaded;

    private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();

    Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        this.environment = environment;
        // 佔位符處理器
        this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
        // 設置默認的資源加載器
        this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
        /**
         * 通過 ClassLoader 從所有的 `META-INF/spring.factories` 文件中加載出 PropertySourceLoader
         * Spring Boot 配置了兩個屬性資源加載器:
         * {@link PropertiesPropertySourceLoader} 加載 `properties` 和 `xml` 文件
         * {@link YamlPropertySourceLoader} 加載 `yml` 和 `yaml` 文件
         */
        this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
                getClass().getClassLoader());
    }
}

屬性不多,上面都已經注釋了,在構造器中會通過 ClassLoader 從所有的 META-INF/spring.factories 文件中加載出 PropertySourceLoader,如下:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

PropertiesPropertySourceLoader:加載 propertiesxml 文件

YamlPropertySourceLoader:加載 ymlyaml 文件

3. load 方法

load() 方法,加載配置信息,並放入 Environment 環境對象中,如下:

void load() {
    // 藉助 FilteredPropertySource 執行入參中的這個 Consumer 函數
    // 目的就是獲取 `defaultProperties` 默認值的 PropertySource,通常我們沒有設置,所以為空對象
    FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
        (defaultProperties) -> {
            this.profiles = new LinkedList<>();
            this.processedProfiles = new LinkedList<>();
            this.activatedProfiles = false;
            this.loaded = new LinkedHashMap<>();
            // <1> 初始化 Profile 對象,也就是我們需要加載的 Spring 配置,例如配置的 JVM 變量:`dev`、`sit`、`uat`、`prod`
            // 1. `java -jar xxx.jar --spring.profiles.active=dev` or `java -jar -Dspring.profiles.active=dev xxx.jar`,那麼這裡的 `profiles` 就會有一個 `null` 和一個 `dev`
            // 2. `java -jar xxx.jar`,那麼這裡的 `profiles` 就會有一個 `null` 和一個 `default`
            initializeProfiles();
            // <2> 依次加載 `profiles` 對應的配置信息
            // 這裡先解析 `null` 對應的配置信息,也就是公共配置
            // 針對上面第 `2` 種情況,如果公共配置指定了 `spring.profiles.active`,那麼添加至 `profiles` 中,並移除 `default` 默認 Profile
            // 所以後續和上面第 `1` 種情況一樣的處理
            while (!this.profiles.isEmpty()) {
                // <2.1> 將接下來的準備加載的 Profile 從隊列中移除
                Profile profile = this.profiles.poll();
                // <2.2> 如果不為 `null` 且不是默認的 Profile,這個方法名不試試取錯了??
                if (isDefaultProfile(profile)) {
                    // 則將其添加至 Environment 的 `activeProfiles`(有效的配置)中,已存在不會添加
                    addProfileToEnvironment(profile.getName());
                }
                /**
                 * <2.3> 嘗試加載配置文件,並解析出配置信息,會根據 Profile 歸類,最終保存至 {@link this#loaded} 集合
                 * 例如會去加載 `classpath:/application.yml` 或者 `classpath:/application-dev.yml` 文件,並解析
                 * 如果 `profile` 為 `null`,則會解析出 `classpath:/application.yml` 中的公共配置
                 * 因為這裡是第一次去加載,所以不需要檢查 `profile` 對應的配置信息是否存在
                 */
                load(profile, this::getPositiveProfileFilter,
                        addToLoaded(MutablePropertySources::addLast, false));
                // <2.4> 將已加載的 Profile 保存
                this.processedProfiles.add(profile);
            }
            /**
             * <3> 如果沒有指定 `profile`,那麼這裡嘗試解析所有需要的環境的配置信息,也會根據 Profile 歸類,最終保存至 {@link this#loaded} 集合
             * 例如會去加載 `classpath:/application.yml` 文件並解析出各個 Profile 的配置信息
             * 因為上面可能嘗試加載過,所以這裡需要檢查 `profile` 對應的配置信息是否存在,已存在則不再添加
             * 至於這一步的用途暫時還沒搞懂~
             */
            load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
            /** <4> 將上面加載出來的所有配置信息從 {@link this#loaded} 集合添加至 Environment 中 */
            addLoadedPropertySources();
            // <5> 設置被激活的 Profile 環境
            applyActiveProfiles(defaultProperties);
        });
}

方法內部藉助 FilteredPropertySource 執行入參中的這個 Consumer 函數,目的就是獲取 defaultProperties 默認值的 PropertySource,通常我們沒有設置,所以為空對象,如下:

static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties,
        Consumer<PropertySource<?>> operation) {
    MutablePropertySources propertySources = environment.getPropertySources();
    // 先獲取當前環境中 `defaultProperties` 的 PropertySource 對象,默認沒有,通常我們也不會配置
    PropertySource<?> original = propertySources.get(propertySourceName);
    if (original == null) {
        // 直接調用 `operation` 函數
        operation.accept(null);
        return;
    }
    // 將這個當前環境中 `defaultProperties` 的 PropertySource 對象進行替換
    // 也就是封裝成一個 FilteredPropertySource 對象,設置了幾個需要過濾的屬性
    propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));
    try {
        // 調用 `operation` 函數,入參是默認值的 PropertySource
        operation.accept(original);
    }
    finally {
        // 將當前環境中 `defaultProperties` 的 PropertySource 對象還原
        propertySources.replace(propertySourceName, original);
    }
}

所以我們直接來看到 load() 方法中的 Consumer 函數,整個處理過程如下:

  1. 調用 initializeProfiles() 方法,初始化 Profile 對象,也就是我們需要加載的 Spring 配置,例如配置的 JVM 變量:devsituatprod

    1. java -jar xxx.jar --spring.profiles.active=dev or java -jar -Dspring.profiles.active=dev xxx.jar,那麼這裡的 profiles 就會有一個 null 和一個 dev

    2. java -jar xxx.jar,那麼這裡的 profiles 就會有一個 null 和一個 default

  2. 依次加載上一步得到的 profiles 對應的配置信息,這裡先解析 null 對應的配置信息,也就是公共配置

    針對上面第 1.2 種情況,如果公共配置指定了 spring.profiles.active,那麼添加至 profiles 中,並移除 default 默認 Profile,所以後續和上面第 1.1 種情況一樣的處理,後面會講到

    1. 將接下來的準備加載的 Profile 從隊列中移除

    2. 如果不為 null 且不是默認的 Profile,這個方法名不試試取錯了??則將其添加至 Environment 的 activeProfiles(有效的配置)中,已存在不會添加

      也就是保存激活的 Profile 環境

    3. 調用 load(..) 重載方法,嘗試加載配置文件,並解析出配置信息,會根據 Profile 歸類,最終保存至 this#loaded 集合

      例如會去加載 classpath:/application.yml 或者 classpath:/application-dev.yml 文件,並解析;如果 profilenull,則會解析出 classpath:/application.yml 中的公共配置,因為這裡是第一次去加載,所以不需要檢查 profile 對應的配置信息是否存在

    4. 將已加載的 Profile 保存

  3. 繼續調用 load(..) 重載方法,如果沒有指定 profile,那麼這裡嘗試解析所有需要的環境的配置信息,也會根據 Profile 歸類,最終保存至 this#loaded 集合

    例如會去加載 classpath:/application.yml 文件並解析出各個 Profile 的配置信息;因為上面可能嘗試加載過,所以這裡需要檢查 profile 對應的配置信息是否存在,已存在則不再添加,至於這一步的用途暫時還沒搞懂~

  4. 調用 addLoadedPropertySources() 方法,將上面加載出來的所有配置信息從 this#loaded 集合添加至 Environment 中

上面的的 load(..) 重載方法中有一個 Consumer 函數,它的入參又有一個 Consumer 函數,第 2.33 步的入參不同,注意一下⏩

上面的整個過程有點繞,有點難懂,建議各位小夥伴自己調試代碼⏩

3.1 initializeProfiles 方法

initializeProfiles() 方法,初始化 Profile 對象,也就是我們需要加載的 Spring 配置,如下:

private void initializeProfiles() {
    // The default profile for these purposes is represented as null. We add it
    // first so that it is processed first and has lowest priority.
    // <1> 先添加一個空的 Profile
    this.profiles.add(null);
    // <2> 從 Environment 中獲取 `spring.profiles.active` 配置
    // 此時還沒有加載配置文件,所以這裡獲取到的就是你啟動 `jar` 包時設置的 JVM 變量,例如 `-Dspring.profiles.active`
    // 或者啟動 `jar` 包時添加的啟動參數,例如 `--spring.profiles.active=dev`
    Set<Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);
    // <3> 從 Environment 中獲取 `spring.profiles.include` 配置
    Set<Profile> includedViaProperty = getProfilesFromProperty(INCLUDE_PROFILES_PROPERTY);
    // <4> 從 Environment 配置的需要激活的 Profile 們,不在上面兩個範圍內則屬於其他
    List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
    // <5> 將上面找到的所有 Profile 都添加至 `profiles` 中(通常我們只在上面的第 `2` 步可能有返回結果)
    this.profiles.addAll(otherActiveProfiles);
    // Any pre-existing active profiles set via property sources (e.g.
    // System properties) take precedence over those added in config files.
    this.profiles.addAll(includedViaProperty);
    // 這裡主要設置 `activatedProfiles`,表示已有需要激活的 Profile 環境
    addActiveProfiles(activatedViaProperty);
    // <6> 如果只有一個 Profile,也就是第 `1` 步添加的一個空對象,那麼這裡再創建一個默認的
    if (this.profiles.size() == 1) { // only has null profile
        for (String defaultProfileName : this.environment.getDefaultProfiles()) {
            Profile defaultProfile = new Profile(defaultProfileName, true);
            this.profiles.add(defaultProfile);
        }
    }
}

過程如下:

  1. 先往 profile 集合添加一個空的 Profile

  2. 從 Environment 中獲取 spring.profiles.active 配置,此時還沒有加載配置文件,所以這裡獲取到的就是你啟動 jar 包時設置的 JVM 變量,例如 -Dspring.profiles.active,或者啟動 jar 包時添加的啟動參數,例如 --spring.profiles.active=dev

    在前面的 《SpringApplication 啟動類的啟動過程》 這篇文章的 6. prepareEnvironment 方法 小節的第 2 步講過

  3. 從 Environment 中獲取 spring.profiles.include 配置

  4. 從 Environment 配置的需要激活的 Profile 們,不在上面兩個範圍內則屬於其他

  5. 將上面找到的所有 Profile 都添加至 profiles 中(通常我們只在上面的第 2 步可能有返回結果)

  6. 如果只有一個 Profile,也就是第 1 步添加的一個空對象,那麼這裡再創建一個默認的

3.2 load 重載方法1

load(Profile, DocumentFilterFactory, DocumentConsumer) 方法,加載指定 Profile 的配置信息,如果為空則解析出公共的配置

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    // <1> 先獲取 `classpath:/`、`classpath:/config/`、`file:./`、`file:./config/` 四個路徑
    // <2> 然後依次遍歷,從該路徑下找到對應的配置文件,找到了則通過 `consumer` 進行解析,並添加至 `loaded` 中
    getSearchLocations().forEach((location) -> {
        // <2.1> 判斷是否是文件夾,這裡好像都是
        boolean isFolder = location.endsWith("/");
        // <2.2> 是文件夾的話找到應用配置文件的名稱,可以通過 `spring.config.name` 配置進行設置
        // Spring Cloud 中默認為 `bootstrap`,Spring Boot 中默認為 `application`
        Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
        // <2.3> 那麼這裡開始解析 `application` 配置文件了
        names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
    });
}

過程如下:

  1. 調用 getSearchLocations() 方法,獲取 classpath:/classpath:/config/file:./file:./config/ 四個路徑

    private Set<String> getSearchLocations() {
        Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
        if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
            locations.addAll(getSearchLocations(CONFIG_LOCATION_PROPERTY));
        }
        else {
            // 這裡會得到 `classpath:/`、`classpath:/config/`、`file:./`、`file:./config/` 四個路徑
            locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
        }
        return locations;
    }
    
  2. 然後依次遍歷,從該路徑下找到對應的配置文件,找到了則通過 consumer 進行解析,並添加至 loaded

    1. 判斷是否是文件夾,這裡好像都是

    2. 是文件夾的話找到應用配置文件的名稱,默認就是 application 名稱

      private Set<String> getSearchNames() {
          // 如果通過 `spring.config.name` 指定了配置文件名稱
          if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
              String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
              // 進行佔位符處理,並返回設置的配置文件名稱
              return asResolvedSet(property, null);
          }
          // 如果指定了 `names` 配置文件的名稱,則對其進行處理(佔位符)
          // 沒有指定的話則去 `application` 默認名稱
          return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
      }
      
    3. 遍歷上一步獲取到 names,默認只有一個application,那麼這裡開始解析 application 配置文件了,調用的還是一個 load(..) 重載方法

總結下來就是這裡會嘗試從 classpath:/classpath:/config/file:./file:./config/ 四個文件夾下面解析 application 名稱的配置文件

3.3 load 重載方法2

load(String, String, Profile, DocumentFilterFactory, DocumentConsumer) 方法,加載 application 配置文件,加載指定 Profile 的配置信息,如果為空則解析出公共的配置

private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
        DocumentConsumer consumer) {
    // <1> 如果沒有應用的配置文件名稱,則嘗試根據 `location` 進行解析,暫時忽略
    if (!StringUtils.hasText(name)) {
        for (PropertySourceLoader loader : this.propertySourceLoaders) {
            if (canLoadFileExtension(loader, location)) {
                load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
                return;
            }
        }
        // 拋出異常
    }
    Set<String> processed = new HashSet<>();
    /**
     * <2> 遍歷 PropertySourceLoader 對配置文件進行加載,這裡有以下兩個:
     * {@link PropertiesPropertySourceLoader} 加載 `properties` 和 `xml` 文件
     * {@link YamlPropertySourceLoader} 加載 `yml` 和 `yaml` 文件
     */
    for (PropertySourceLoader loader : this.propertySourceLoaders) {
        // 先獲取 `loader` 的後綴,也就是說這裡會總共會遍歷 4 次,分別處理不同後綴的文件
        // 加上前面 4 種 `location`(文件夾),這裡會進行 16 次加載
        for (String fileExtension : loader.getFileExtensions()) {
            // 避免重複加載
            if (processed.add(fileExtension)) {
                // 例如嘗試加載 `classpath:/application.yml` 文件
                loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
                        consumer);
            }
        }
    }
}

過程如下:

  1. 如果沒有應用的配置文件名稱,則嘗試根據 location 進行解析,暫時忽略

  2. 遍歷 PropertySourceLoader 對配置文件進行加載,回到 Loader 的構造方法中,會有 PropertiesPropertySourceLoaderYamlPropertySourceLoader 兩個對象,前者支持 propertiesxml 後綴,後者支持 ymlyaml

    1. 獲取 PropertySourceLoader 支持的後綴,然後依次加載對應的配置文件

      也就是說四種後綴,加上前面四個文件夾,那麼接下來每次 3.load 方法 都會調用十六次 loadForFileExtension(..) 方法

3.4 loadForFileExtension 方法

loadForFileExtension(PropertySourceLoader, String, String, Profile, DocumentFilterFactory, DocumentConsumer) 方法,嘗試加載 classpath:/application.yml 配置文件,加載指定 Profile 的配置信息,如果為空則解析出公共的配置

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
        Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    // <1> 創建一個默認的 DocumentFilter 過濾器 `defaultFilter`
    DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
    // <2> 創建一個指定 Profile 的 DocumentFilter 過濾器 `profileFilter`
    DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
    // <3> 如果傳入了 `profile`,那麼嘗試加載 `application-${profile}.yml`對應的配置文件
    if (profile != null) {
        // Try profile-specific file & profile section in profile file (gh-340)
        // <3.1> 獲取 `profile` 對應的名稱,例如 `application-dev.yml`
        String profileSpecificFile = prefix + "-" + profile + fileExtension;
        // <3.2> 嘗試對該文件進行加載,公共配置
        load(loader, profileSpecificFile, profile, defaultFilter, consumer);
        // <3.3> 嘗試對該文件進行加載,環境對應的配置
        load(loader, profileSpecificFile, profile, profileFilter, consumer);
        // Try profile specific sections in files we've already processed
        // <3.4> 也嘗試從該文件中加載已經加載過的環境所對應的配置
        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
    // <4> 正常邏輯,這裡嘗試加載 `application.yml` 文件中對應 Profile 環境的配置
    // 當然,如果 Profile 為空也就加載公共配置
    load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

過程如下:

  1. 創建一個默認的 DocumentFilter 過濾器 defaultFilter

    private DocumentFilter getPositiveProfileFilter(Profile profile) {
        return (Document document) -> {
            // 如果沒有指定 Profile,那麼 Document 中的 `profiles` 也得為空
            // 也就是不能有 `spring.profiles` 配置,就是公共配置咯
            if (profile == null) {
                return ObjectUtils.isEmpty(document.getProfiles());
            }
            // 如果指定了 Profile,那麼 Document 中的 `profiles` 需要包含這個 Profile
            // 同時,Environment 中也要接受這個 Document 中的 `profiles`
            return ObjectUtils.containsElement(document.getProfiles(), profile.getName())
                    && this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
        };
    }
    
  2. 創建一個指定 Profile 的 DocumentFilter 過濾器 profileFilter

  3. 如果傳入了 profile,那麼嘗試加載 application-${profile}.yml對應的配置文件

    1. 獲取 profile 對應的名稱,例如 application-dev.yml
    2. 又調用 load(..) 重載方法加載 3.1 步的配置文件,這裡使用 defaultFilter 過濾器,找到公共的配置信息
    3. 又調用 load(..) 重載方法加載 3.1 步的配置文件,這裡使用 profileFilter 過濾器,找到指定 profile 的配置信息
    4. 也嘗試從該文件中加載已經加載過的環境所對應的配置,也就是說 dev 的配置信息,也能在其他的 application-prod.yml 中讀取
  4. 正常邏輯,繼續調用 load(..) 重載方法,嘗試加載 application.yml 文件中對應 Profile 環境的配置,當然,如果 Profile 為空也就加載公共配置

沒有什麼複雜的邏輯,繼續調用重載方法

3.5 load 重載方法3

load(PropertySourceLoader, String, Profile, DocumentFilter,DocumentConsumer) 方法,嘗試加載配置文件,加載指定 Profile 的配置信息,如果為空則解析出公共的配置

private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
        DocumentConsumer consumer) {
    try {
        // <1> 通過資源加載器獲取這個文件資源
        Resource resource = this.resourceLoader.getResource(location);
        // <2> 如果文件資源不存在,那直接返回了
        if (resource == null || !resource.exists()) {
            return;
        }
        // <3> 否則,如果文件資源的後綴為空,跳過,直接返回
        if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
            return;
        }
        String name = "applicationConfig: [" + location + "]";
        // <4> 使用 PropertySourceLoader 加載器加載出該文件資源中的所有屬性,並將其封裝成 Document 對象
        // Document 對象中包含了配置文件的 `spring.profiles` 和 `spring.profiles.active` 屬性
        // 一個文件不是對應一個 Document,因為在一個 `yml` 文件可以通過 `---` 來配置多個環境的配置,這裡也就會有多個 Document
        List<Document> documents = loadDocuments(loader, name, resource);
        // <5> 如果沒有解析出 Document,表明該文件資源無效,跳過,直接返回
        if (CollectionUtils.isEmpty(documents)) {
            return;
        }
        List<Document> loaded = new ArrayList<>();
        // <6> 通過 DocumentFilter 對 `document` 進行過濾,過濾出想要的 Profile 對應的 Document
        // 例如入參的 Profile 為 `dev` 那麼這裡只要 `dev` 對應 Document
        // 如果 Profile 為空,那麼找到沒有 `spring.profiles` 配置 Document,也就是我們的公共配置
        for (Document document : documents) {
            if (filter.match(document)) {
                // 如果前面還沒有激活的 Profile
                // 那麼這裡嘗試將 Document 中的 `spring.profiles.active` 添加至 `profiles` 中,同時刪除 `default` 默認的 Profile
                addActiveProfiles(document.getActiveProfiles());
                addIncludedProfiles(document.getIncludeProfiles());
                loaded.add(document);
            }
        }
        // <7> 將需要的 Document 們進行倒序,因為配置在後面優先級越高,所以需要反轉一下
        Collections.reverse(loaded);
        // <8> 如果有需要的 Document
        if (!loaded.isEmpty()) {
            /**
             * 藉助 Lambda 表達式調用 {@link #addToLoaded} 方法
             * 將這些 Document 轉換成 MutablePropertySources 保存至 {@link this#loaded} 集合中
             */
            loaded.forEach((document) -> consumer.accept(profile, document));
        }
    } catch (Exception ex) {
        throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
    }
}

過程如下:

  1. 通過資源加載器獲取這個文件資源,例如 classpath:/application.yml

  2. 如果文件資源不存在,那直接返回了

  3. 否則,如果文件資源的後綴為空,跳過,直接返回

  4. 調用 loadDocuments(..) 方法,使用 PropertySourceLoader 加載器加載出該文件資源中的所有屬性,並將其封裝成 Document 對象

    Document 對象中包含了配置文件的 spring.profilesspring.profiles.active 屬性,一個文件不是對應一個 Document,因為在一個 yml 文件可以通過 --- 來配置多個環境的配置,這裡也就會有多個 Document

  5. 如果沒有解析出 Document,表明該文件資源無效,跳過,直接返回

  6. 通過 DocumentFilter 對 document 進行過濾,過濾出想要的 Profile 對應的 Document

    例如入參的 Profile 為 dev 那麼這裡只要 dev 對應 Document,如果 Profile 為空,那麼找到沒有 spring.profiles 配置 Document,也就是我們的公共配置

    1. 如果前面還沒有激活的 Profile,那麼這裡嘗試將 Document 中的 spring.profiles.active 添加至 profiles 中,同時刪除 default 默認的 Profile
  7. 將需要的 Document 們進行倒序,因為配置在後面優先級越高,所以需要反轉一下

  8. 如果有需要的 Document,藉助 Lambda 表達式調用 addToLoaded(..) 方法,將這些 Document 轉換成 MutablePropertySources 保存至 this#loaded 集合中

邏輯沒有很複雜,找到對應的 application.yml 文件資源,解析出所有的配置,找到指定 Profile 對應的配置信息,然後添加到集合中

你要知道的是上面第 4 步得到的 Document 對象,例如 application.yml 中設置 dev 環境激活,有兩個 devprod 不同的配置,那麼這裡會得到三個 Document 對象

3.6 loadDocuments 方法

loadDocuments(PropertySourceLoader, String, Resource) 方法,從文件資源中加載出 Document 們,如下:

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) {
        // 使用 PropertySourceLoader 加載器進行加載
        List<PropertySource<?>> loaded = loader.load(name, resource);
        // 將 PropertySource 轉換成 Document
        documents = asDocuments(loaded);
        // 放入緩存
        this.loadDocumentsCache.put(cacheKey, documents);
    }
    return documents;
}

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) {
        // 使用 PropertySourceLoader 加載器進行加載
        List<PropertySource<?>> loaded = loader.load(name, resource);
        // 將 PropertySource 轉換成 Document
        documents = asDocuments(loaded);
        // 放入緩存
        this.loadDocumentsCache.put(cacheKey, documents);
    }
    return documents;
}

邏輯比較簡單,先通過 PropertySourceLoader 加載配置文件,例如 YamlPropertySourceLoader 加載 application.yml 配置文件

然後將加載出來的 PropertySource 屬性源對象們一一封裝成 Document 對象,同時放入緩存中

YamlPropertySourceLoader

org.springframework.boot.env.YamlPropertySourceLoaderymlyaml 配置文件的加載器

public class YamlPropertySourceLoader implements PropertySourceLoader {

	@Override
	public String[] getFileExtensions() {
		return new String[] { "yml", "yaml" };
	}

	@Override
	public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
		// 如果不存在 `org.yaml.snakeyaml.Yaml` 這個 Class 對象,則拋出異常
		if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
			throw new IllegalStateException(
					"Attempted to load " + name + " but snakeyaml was not found on the classpath");
		}
		// 通過 Yaml 解析該文件資源
		List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();
		if (loaded.isEmpty()) {
			return Collections.emptyList();
		}
		// 將上面獲取到的 Map 集合們一一封裝成 OriginTrackedMapPropertySource 對象
		List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());
		for (int i = 0; i < loaded.size(); i++) {
			String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";
			propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,
					Collections.unmodifiableMap(loaded.get(i)), true));
		}
		return propertySources;
	}

}

可以看到,主要就是通過 org.yaml.snakeyaml.Yaml 解析配置文件

3.7 addToLoaded 方法

addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>>, boolean) 方法,將加載出來的配置信息保存起來,如下:

private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
        boolean checkForExisting) {
    return (profile, document) -> {
        // 如果需要檢查是否存在,存在的話直接返回
        if (checkForExisting) {
            for (MutablePropertySources merged : this.loaded.values()) {
                if (merged.contains(document.getPropertySource().getName())) {
                    return;
                }
            }
        }
        // 獲取 `loaded` 中該 Profile 對應的 MutablePropertySources 對象
        MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
                (k) -> new MutablePropertySources());
        // 往這個 MutablePropertySources 對象中添加 Document 對應的 PropertySource
        addMethod.accept(merged, document.getPropertySource());
    };
}

loaded 中添加該 Profile 對應的 PropertySource 屬性源們

4. addLoadedPropertySources 方法

addLoadedPropertySources() 方法,將前面加載出來的所有 PropertySource 配置信息們添加到 Environment 環境中

private void addLoadedPropertySources() {
    // 獲取當前 Spring 應用的 Environment 環境中的配置信息
    MutablePropertySources destination = this.environment.getPropertySources();
    // 將上面已加載的每個 Profile 對應的屬性信息放入一個 List 集合中 `loaded`
    List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
    // 將 `loaded` 進行翻轉,因為寫在後面的環境優先級更好
    Collections.reverse(loaded);
    String lastAdded = null;
    Set<String> added = new HashSet<>();
    // 遍歷 `loaded`,將每個 Profile 對應的屬性信息按序添加到 Environment 環境中
    for (MutablePropertySources sources : loaded) {
        for (PropertySource<?> source : sources) {
            if (added.add(source.getName())) {
                // 放入上一個 PropertySource 的後面,優先默認配置
                addLoadedPropertySource(destination, lastAdded, source);
                lastAdded = source.getName();
            }
        }
    }
}

過程如下:

  1. 獲取當前 Spring 應用的 Environment 環境中的配置信息 destination
  2. 將上面已加載的每個 Profile 對應的屬性信息放入一個 List 集合中 loaded
  3. loaded 進行翻轉,因為寫在後面的環境優先級更好❓❓❓前面不是翻轉過一次嗎?好吧,暫時忽略
  4. 遍歷 loaded,將每個 Profile 對應的 PropertySources 屬性信息按序添加到 Environment 環境中

5. applyActiveProfiles 方法

applyActiveProfiles(PropertySource<?> defaultProperties) 方法,設置被激活的 Profile 環境

private void applyActiveProfiles(PropertySource<?> defaultProperties) {
    List<String> activeProfiles = new ArrayList<>();
    // 如果默認的配置信息不為空,通常為 `null`
    if (defaultProperties != null) {
        Binder binder = new Binder(ConfigurationPropertySources.from(defaultProperties),
                new PropertySourcesPlaceholdersResolver(this.environment));
        activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.include"));
        if (!this.activatedProfiles) {
            activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.active"));
        }
    }
    // 遍歷已加載的 Profile 對象,如果它不為 `null` 且不是默認的,那麼添加到需要 `activeProfiles` 激活的隊列中
    this.processedProfiles.stream().filter(this::isDefaultProfile).map(Profile::getName)
            .forEach(activeProfiles::add);
    // 設置 Environment 需要激活的環境名稱
    this.environment.setActiveProfiles(activeProfiles.toArray(new String[0]));
}

邏輯比較簡單,例如我們配置了 spring.profiles.active=dev,那麼這裡將設置 Environment 被激活的 Profile 為 dev

總結

本文分析了 Spring Boot 加載 application.yml 配置文件並應用於 Spring 應用的 Environment 環境對象的整個過程,主要是藉助於 Spring 的 ApplicationListener 事件監聽器機制,在啟動 Spring 應用的過程中,準備好 Environment 的時候會廣播 應用環境已準備好 事件,然後 ConfigFileApplicationListener 監聽到該事件會進行處理。

加載 application 配置文件的整個過程有點繞,嵌套有點深,想深入了解的話查看上面的內容,每個小節都進行了編號。

大致流程就是先加載出 application.yml 文件資源,然後找到需要的 Profile 對應的 PropertySource 屬性信息,包括公告配置,最後將這些 PropertySource 應用於 Environment。