精盡Spring Boot原始碼分析 - 配置載入

月圓吖發表於2021-07-08

該系列文章是筆者在學習 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。

相關文章