1. 程式人生 > >Spring Boot 外部化配置(一)- Environment、ConfigFileApplicationListener

Spring Boot 外部化配置(一)- Environment、ConfigFileApplicationListener

目錄

  • 前言
  • 1、起源
  • 2、外部化配置的資源型別
  • 3、外部化配置的核心
    • 3.1 Environment
      • 3.1.1、ConfigFileApplicationListener
      • 3.1.2、關聯 SpringConfigurationPropertySources

前言

        最近在學習Spring Boot相關的課程,過程中以筆記的形式記錄下來,方便以後回憶,同時也在這裡和大家探討探討,文章中有漏的或者有補充的、錯誤的都希望大家能夠及時提出來,本人在此先謝謝了!

開始之前呢,希望大家帶著幾個問題去學習:
1、Spring Boot 外部化配置是什麼?
2、整體流程或結構是怎樣的?
3、核心部分是什麼?
4、怎麼實現的?
這是對自我的提問,我認為帶著問題去學習,是一種更好的學習方式,有利於加深理解。好了,接下來進入主題。

1、起源

        這篇文章我們就來討論 Spring Boot 的外部化配置功能,該功能主要是通過外部的配置資源實現與程式碼的相互配合,來避免硬編碼,提供應用資料或行為變化的靈活性。相信小夥伴們在日常工作中都有使用過,如在 properties

或者 YAML 檔案中定義好key value格式的資料後,就可在程式中通過 @Value 註解獲取該value值。還可以定義一些同外部元件約定好的key,如以 springredis 等元件名為字首的key,之後相應的元件就可讀取到該value值進行工作。當然,這只是外部化配置的一小部分內容,接下來進行詳細討論。

2、外部化配置的資源型別

        先來看看外部化配置的幾種資源型別,除了 propertiesYAML 外,還有環境變數、系統屬性、啟動引數等。所有的資源型別將近二十種,這裡只介紹我們比較熟悉的:
1、properties :這個應該都知道,就是在以 .properties

為字尾的檔案中定義key value格式資料。
2、YAML:檔案格式是以 .yml 為字尾,檔案中的資料也是key value格式,如下:

user:
  name: loong
  age: 10

這裡的key就是 user.nameuser.age

3、 環境變數:這是通過 System.getenv() 方式獲取的預設配置,也是key value格式,下面列出部分配置,其它的還請自行了解,如下:

名稱 Key
Java安裝目錄 JAVA_HOME
classpath環境變數 CLASSPATH
使用者臨時檔案目錄 TEMP
計算機名 COMPUTERNAME
使用者名稱 USERNAME

4、 系統屬性:這是通過 System.getProperties() 方式獲取的預設配置,也是key value格式,下面列出部分配置,其它的還請自行了解,如下:

名稱 Key
執行時環境版本 java.version Java
Java安裝目錄 java.home
要使用的 JIT編譯器的名稱 java.compiler
作業系統的架構 os.arch
作業系統的版本 os.version

5、 啟動引數:這個在 Spring Boot SpringApplication 啟動類(二)這篇文章中討論過。一種是在 jar 包執行時行時傳遞的引數,如:java -jar xxx.jar name=張三 pwa=123 ,還有一種是在 IDEA 的 Program arguments 中輸入資料:

可以看到,外部化配置中的資料都是key value格式。這裡還要注意它們的載入順序,當key相同時,會出現覆蓋的情況。

3、外部化配置的核心

        接下來,我們的重心來圍繞 propertiesYAML 配置檔案,這兩者也是我們日常工作中常用的。首先來看取值方式,在 Spring 時代有 Environment@ValueXML 三種方式,在 Spring Boot 時代則是 @ConfigurationProperties 方式。其中,涉及到了一個核心類,它就是 Environment ,該物件不僅可以獲取所有的外部化配置資料,就連另外幾種取值方式的資料來源也是從該類中獲取。這裡,主要對 Environment@ConfigurationProperties 進行詳細討論,筆者認為 Environment@ConfigurationProperties 才是 Spring Boot 外部化配置的核心所在。

3.1 Environment

該類在 Spring Boot SpringApplication啟動類(二) 的 2.3 章節有簡要說明過,這裡我們展開詳細討論。首先回顧一下程式碼:

public class SpringApplication {

    ...
    
    public ConfigurableApplicationContext run(String... args) {
        ...
        
        try {
            ...
            
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        
        ...
        
    }
    ...
}

SpringApplication 執行階段的 run 方法中通過 prepareEnvironment 方法了建立 ConfigurableEnvironment 的實現類物件,ConfigurableEnvironment 是一個介面,且繼承了 Environment 。進入建立方法:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    listeners.environmentPrepared(environment);
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

首先看第一行,通過 getOrCreateEnvironment 方法建立 ConfigurableEnvironment 物件:

private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    }
    switch (this.webApplicationType) {
    case SERVLET:
        return new StandardServletEnvironment();
    case REACTIVE:
        return new StandardReactiveWebEnvironment();
    default:
        return new StandardEnvironment();
    }
}

Spring Boot SpringApplication 啟動類(二) 的 2.3 章節說過, webApplicationType 儲存的是應用的型別,有 ReactiveServlet 等,是在 SpringApplication 準備階段推匯出來的,而本專案推匯出來是 Servlet 型別,所以例項化的是 StandardServletEnvironment 物件:

public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {

    public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";

    public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";

    public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";

    @Override
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
        propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
        if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
            propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
        }
        super.customizePropertySources(propertySources);
    }
    
    ...

}

而該類又繼承了 StandardEnvironment 類。且重寫了 customizePropertySources 方法,並呼叫了父類的 customizePropertySources 方法。我們繼續往下深入:

public class StandardEnvironment extends AbstractEnvironment {

    public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

    public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
    
    @Override
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(
                new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
        propertySources.addLast(
                new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }

}

繼續看它的 AbstractEnvironment 父抽象類:

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    
    ...
    
    private final MutablePropertySources propertySources = new MutablePropertySources();
    
    ...
    
    public AbstractEnvironment() {
        customizePropertySources(this.propertySources);
    }
    ...
}

可以看到,最終會有一個 AbstractEnvironment 抽象類。在 StandardServletEnvironment 初始化時,會呼叫 AbstractEnvironment 的構造方法,裡面呼叫了子類重寫的 customizePropertySources 方法,且入參是 MutablePropertySources 物件,該物件是 Environment 的一個屬性,是底層真正儲存外部化配置的。之後, StandardServletEnvironmentStandardEnvironmentcustomizePropertySources 方法相繼執行,主要是往 MutablePropertySources 物件中新增外部化配置。其中我們前面所說的環境變數和系統屬性是在 StandardEnvironment 重寫的方法中進行載入。

我們回到外面的 prepareEnvironment 方法,繼續往下走。接著執行的是 configureEnvironment 方法,該方法主要是把啟動引數加入到 MutablePropertySources 中。之後,我們斷點看看有多少種外部化配置:

有五種,且真正儲存資料的是 MutablePropertySources 中的 PropertySource 實現類集合。

這裡簡要介紹一下 PropertySource ,我們將其稱之為配置源,官方定義它是外部化配置的API描述方式,是外部化配置的一個媒介。 用我們的話來說,它是一個抽象類,提供了統一儲存外部化配置資料的功能,而每種外部化配置有具體的實現類,主要提供不同的基礎操作,如 getcontains 等 。我們來看看 PropertySource 物件的資料格式,一般包含:

name : 外部化配置的名稱
source : 儲存配置中的資料,底層一般資料格式都是key value


我們繼續往下走,接著呼叫了 SpringApplicationRunListenersenvironmentPrepared 方法。在上篇文章
Spring Boot SpringApplication 啟動類(二) 的 2.1 小節講過,當 Spring Boot 執行到某一階段時,會通過 SpringSimpleApplicationEventMulticaster 事件廣播器進行事件廣播,之後 ,相應監聽器就會監聽到該事件,執行調監聽器的 onApplicationEvent 方法。這裡表示 Spring Boot 到了 ConfigurableEnvironment 構建完成時階段。我們進入該方法:

class SpringApplicationRunListeners {

    ...

    private final List<SpringApplicationRunListener> listeners;

    public void environmentPrepared(ConfigurableEnvironment environment) {
        for (SpringApplicationRunListener listener : this.listeners) {
            listener.environmentPrepared(environment);
        }
    }
    ...
}

真正呼叫的是 SpringApplicationRunListener 集合中的 environmentPrepared 方法。 SpringApplicationRunListener 是一個介面,它具有唯一實現類 EventPublishingRunListener

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

    ...
    
    private final SimpleApplicationEventMulticaster initialMulticaster;

    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        this.initialMulticaster
                .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
    }
    ...
}

可以看到,最終通過 SimpleApplicationEventMulticastermulticastEvent 方法釋出 ApplicationEnvironmentPreparedEvent 事件。上面說過, Spring Boot 監聽器會監聽到該事件,其中一個名為 ConfigFileApplicationListener 的監聽器,監聽到該事件後會進行載入 applicationYAML 配置檔案的操作,接下來,我們具體的來看一看該類實現。

3.1.1、ConfigFileApplicationListener

        我們直接進入該類:

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
    
    ...
    
    private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
    
    public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

    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_NAMES = "application";
    
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        // 1、通過 instanceof 判斷事件的型別,如果是 ApplicationEnvironmentPreparedEvent 事件,則執行 onApplicationEnvironmentPreparedEvent 方法
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
        }
        ...
    }
    
    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        // 2、呼叫 loadPostProcessors 方法,返回 Environment 的後置處理器集合,我們跳到 2.1 檢視方法實現
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        
        // 2.2、把自己也加入該集合
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        
        // 2.3、遍歷 EnvironmentPostProcessor 集合,執行它們的 postProcessEnvironment 方法,我們跳到 3 檢視當前類的該方法實現
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }
    }
    
    // 2.1 是我們比較熟悉的 loadFactories 方法,在 Spring Boot 自動裝配(二) 的 2.1.2 小節講過,loadFactories 方法是從 spring.factories 檔案中載入 key 為 EnvironmentPostProcessor 的實現類集合
    List<EnvironmentPostProcessor> loadPostProcessors() {
        return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
    }
    
    // 3、 執行到該方法時,會呼叫 addPropertySources 方法,入參是上文載入 ConfigurableEnvironment 物件
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        addPropertySources(environment, application.getResourceLoader());
    }
    
    protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
        
        // 4、 我們主要關注這裡,通過 Loader 的構造方法建立該物件,並呼叫它的 load 方法
        new Loader(environment, resourceLoader).load();
    }
    
    private class Loader {
    
        private final ConfigurableEnvironment environment;
        
        private final List<PropertySourceLoader> propertySourceLoaders;
    
        // 4.1、 構造方法中會初始化一些屬性
        Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
            ...
            this.environment = environment;
            
            // 又是我們比較熟悉的 loadFactories 方法,在 Spring Boot 自動裝配(二) 的 2.1.2 小節講過,loadFactories 方法是從 spring.factories 檔案中載入 key 為 PropertySourceLoader 的實現類集合。這裡載入的是 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 兩個實現類,看類名可初步斷定是處理 properties 和 YAML 檔案的
            this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,getClass().getClassLoader());
        }
        
        public void load() {
            ...
            // 5、這裡會繼續呼叫它過載的 load 方法
            load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
            ...
            
            // 9、這是最後一步,將當前類中的 MutablePropertySources 中的 PropertySource 物件,全部塞到 ConfigurableEnvironment 的 MutablePropertySources 物件中。我們跳到 9.1 進行檢視
            addLoadedPropertySources();
        }
        
        private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            
            // 5.1、首先執行 getSearchLocations 方法,看方法名大致能猜出是獲取搜尋路徑的,我們跳到 5.1.1 檢視該方法的實現
            getSearchLocations().forEach((location) -> {
            
                // 5.2、開始遍歷該集合,先判斷該路徑是否是以反斜槓結尾,是的話則該路徑為資料夾;不是的話,則該路徑為檔案的完整路徑,類似於 classPath:/application.properties 
                boolean isFolder = location.endsWith("/");
                
                // 5.3、 如果是資料夾路徑,則通過 getSearchNames 獲取檔案的名稱,不是則返回空集合,我們跳到 5.3.1 檢視 getSearchNames 方法
                Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
                
                // 5.4、再呼叫 load 的過載方法,這裡,location 是路徑名,name是檔名,我們跳到 6 進行檢視
                names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
            });
        }
        
        // 5.1.1、這個方法就是獲取載入 application 和 YAML 檔案路徑的
        private Set<String> getSearchLocations() {
        
            //  可以看到 CONFIG_LOCATION_PROPERTY 的值為 spring.config.location,也就是說,先判斷我們有沒有手動設定搜尋路徑,有的話直接返回該路徑。該值一般通過啟動引數的方式設定
            if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
                return getSearchLocations(CONFIG_LOCATION_PROPERTY);
            }
            
            // 該 CONFIG_ADDITIONAL_LOCATION_PROPERTY 變數的值為 spring.config.additional-location,這也是用於手動設定搜尋路徑,不過和上面不同的是,不會覆蓋 接下來預設的搜尋路徑
            Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
            
            // 這裡就是獲取預設的搜尋路徑,通過 DEFAULT_SEARCH_LOCATIONS 變數的值 classpath:/,classpath:/config/,file:./,file:./config/,將該值用逗號分隔,加入集合並返回。到這一步,我們至少獲取到了4個載入 application 和 YAML 檔案的路徑
            locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
            return locations;
        }
        
        // 5.3.1 
        private Set<String> getSearchNames() {
        
            // CONFIG_LOCATION_PROPERTY 變數值為 spring.config.name ,同樣先判斷有沒有手動設定檔名稱,有的話,直接返回
            if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
                String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
                return asResolvedSet(property, null);
            }
            
            // 如果沒有,則通過 DEFAULT_NAMES 變數值返回預設的檔名,變數值為 application
            return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
        }
        
        // 6、
        private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
                DocumentConsumer consumer) {
                
            // 6.1、 上面 5.2 說過 name 為空時,表示 location 是完整的檔案路徑。之後進入這個 if 
            if (!StringUtils.hasText(name)) {
            
                // 6.1.1、propertySourceLoaders 屬性是在 4.1 處被初始化的,儲存的是 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 兩個類。這裡對這兩個類進行遍歷
                for (PropertySourceLoader loader : this.propertySourceLoaders) {
                
                    // 我們跳到 6.1.2 檢視 canLoadFileExtension 方法實現,入參 location 是檔案的完整路徑
                    if (canLoadFileExtension(loader, location)) {
                        
                        // 這裡又是一個 load 過載方法,我們跳到 7 進行檢視
                        load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
                        return;
                    }
                }
            }
            Set<String> processed = new HashSet<>();
            for (PropertySourceLoader loader : this.propertySourceLoaders) {
            
                // 6.2 這裡和 6.1.3 類似,獲取副檔名
                for (String fileExtension : loader.getFileExtensions()) {
                    if (processed.add(fileExtension)) {
                        
                        // 進入 6.3、檢視該方法實現。關注重點的兩個引數:一個是路徑名 + 檔名,還有一個 “.” +副檔名
                        loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
                                consumer);
                    }
                }
            }
        }
        
        // 6.1.2、 該方法作用是 判斷 name 完整路徑名是否以指定的副檔名結尾
        private boolean canLoadFileExtension(PropertySourceLoader loader, String name) {
        
            // 6.1.3、呼叫 PropertySourceLoader 的 getFileExtensions 方法。當你的實現類是 PropertiesPropertySourceLoader 時,該方法返回 properties、xml;如果是 YamlPropertySourceLoader 則返回 yml、yaml。從這裡可以看出,能被處理的檔案格式有這四種
            return Arrays.stream(loader.getFileExtensions())
                    .anyMatch((fileExtension) -> StringUtils.endsWithIgnoreCase(name, fileExtension));
        }
        
        // 6.3 到了這裡,prefix 和 fileExtension 都是進行拼接好的值,如 prefix = classpath:/applicarion,fileExtension = .properties
        private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
                Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            
            ...
            
            // 這裡同樣呼叫節點 7 的過載方法,通過 prefix + fileExtension 形成完整的檔案路徑名,通過入參進行傳遞。如 classpath:/applicarion.properties
            load(loader, prefix + fileExtension, profile, profileFilter, consumer);
        }
        
        // 7、
        private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
                DocumentConsumer consumer) {
            try {
            
                // 這裡呼叫 ResourceLoader 的 getResource 方法,通過 location 檔案路徑,讀取獲取該檔案資源,之後就好辦了
                Resource resource = this.resourceLoader.getResource(location);
                
                ...
                
                // 具體解析在過程 loadDocuments 中,這裡就不繼續跟蹤了,大致是以流的方式解析檔案。解析之後會生成一個 PropertySource 物件,該物件在上面說過,表示一個外部化配置源物件,儲存配置中的資料。之後,會將該物件封裝到 Document 中
                List<Document> documents = loadDocuments(loader, name, resource);
                
                ...
                
                if (!loaded.isEmpty()) {
                
                    // 遍歷 documents 集合,當執行 consumer.accept 時會進入 addToLoaded 方法,這是 Java8 的寫法。consumer 物件引數來自節點 5 。我們跳到 8 檢視 addToLoaded 實現
                    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);
            }
        }
        
        // 8、BiConsumer 是 JAVA8 的函式介面,表示定義一個帶有兩個引數且不返回結果的操作,通過節點 5 我們知道,這個操作是 MutablePropertySources::addFirst 。
        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;
                        }
                    }
                }
                MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
                        (k) -> new MutablePropertySources());
                        
                // 當呼叫 BiConsumer 的 accept 方法時,定義的操作會執行,兩個入參分別是 MutablePropertySources 物件和配置檔案源物件 PropertySource。該操作會呼叫 MutablePropertySources 的 addFirst 方法把該配置檔案源物件新增至其中。最後我們去看看前面 load 方法中的最後一步 9
                addMethod.accept(merged, document.getPropertySource());
            };
        }
        
        // 9.1
        private void addLoadedPropertySources() {
        
            // 獲取當前上下文環境中的 MutablePropertySources 物件
            MutablePropertySources destination = this.environment.getPropertySources();
            
            // 獲取當前類中的 MutablePropertySources 集合
            List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
            Collections.reverse(loaded);
            String lastAdded = null;
            Set<String> added = new HashSet<>();
            
            // 遍歷 loaded 集合及其中所有的 PropertySource ,也就是 application 或 YAML 配置檔案源物件
            for (MutablePropertySources sources : loaded) {
                for (PropertySource<?> source : sources) {
                    if (added.add(source.getName())) {
                    
                        // 我們進入 9.2 檢視該方法,主要引數是上下文環境中的 MutablePropertySources 物件和配置檔案源物件
                        addLoadedPropertySource(destination, lastAdded, source);
                        lastAdded = source.getName();
                    }
                }
            }
        }

        // 9.2 
        private void addLoadedPropertySource(Mutab lePropertySources destination, String lastAdded,
                PropertySource<?> source) {
            if (lastAdded == null) {
                if (destination.contains(DEFAULT_PROPERTIES)) {
                    destination.addBefore(DEFAULT_PROPERTIES, source);
                }
                else {
                    destination.addLast(source);
                }
            }
            else {
            
                // 最後通過將 source 新增到 environment 中的 MutablePropertySources 物件中。
                destination.addAfter(lastAdded, source);
            }
        }
        
        // 至此,properties 和 YAML 配置檔案就被載入到了上下文環境共享的 Environment 中,之後如 @Value 等獲取值都是從該物件中獲取
    }
}

可以看到,ConfigFileApplicationListener 主要功能就是將 propertiesYAML 檔案載入到 Environment 中。另外還存在一個 @PropertySource 註解,也是載入指定的配置檔案到 Environment 中。

3.1.2、關聯 SpringConfigurationPropertySources

我們回到最外面的 prepareEnvironment 方法,來看看執行完監聽方法時 ConfigurableEnvironment 中載入了多少種外部化配置:


有七種,包括新增的 properties 配置檔案。

之後還有一個操作,通過 ConfigurationPropertySources.attach 關聯 SpringConfigurationPropertySources ,這個是下一小節需要用到。我們進入 attach 方法檢視:


public final class ConfigurationPropertySources {
    
    private static final String ATTACHED_PROPERTY_SOURCE_NAME = "configurationProperties";
    
    public static void attach(Environment environment) {
        Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
        
        // 獲取 ConfigurableEnvironment 中的 MutablePropertySources 
        MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
        
        // 獲取名為 configurationProperties 的外部化配置源物件
        PropertySource<?> attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME);
        
        // 如果存在,則把該物件移除
        if (attached != null && attached.getSource() != sources) {
            sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
            attached = null;
        }
        
        // 不存在,則新增一個配置源物件,具體物件型別為 ConfigurationPropertySourcesPropertySource,源物件中的資料為 SpringConfigurationPropertySources 
        if (attached == null) {
            sources.addFirst(new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
                    new SpringConfigurationPropertySources(sources)));
        }
    }
}

到這裡,ConfigurableEnvironment 又新增了一個 ConfigurationPropertySourcesPropertySource 型別的配置源物件。我們主要來關注 SpringConfigurationPropertySources 物件,可以看到,這裡是通過它的帶參構造器建立該物件,引數 sources 是從 ConfigurableEnvironment 中獲取的 MutablePropertySources 物件。我們進入 SpringConfigurationPropertySources 類中檢視:

class SpringConfigurationPropertySources implements Iterable<ConfigurationPropertySource> {
    
    ...
    
    private final Iterable<PropertySource<?>> sources;
    
    SpringConfigurationPropertySources(Iterable<PropertySource<?>> sources) {
        Assert.notNull(sources, "Sources must not be null");
        this.sources = sources;
    }
    
    ...
}

可以看到,外部 ConfigurableEnvironmentMutablePropertySources 關聯到了該類中的 Iterable (繼承關係) 物件,這裡是一個伏筆,下一節底層實現需依賴該屬性。

至此, Environment 的建立過程及載入外部化配置的過程就到這裡結束,我們簡要回顧一下該流程:

  1. 首先 Environment 是一個較為特殊的類,術語稱之為應用執行時的環境。它儲存了所有的外部化配置,可以通過它獲取任意配置資料,並且 @Value@ConfigurationProperties 等其它獲取配置資料的方式都依賴於該類。
  2. 通過判斷應用的型別,來建立不同環境的 Environment ,有 ServletReactive、非 Web 型別。
  3. 之後會相繼新增外部化配置到該類中,每種外部化配置都對應了一個 PropertySource 配置源物件。
  4. 重點介紹了載入 propertiesYAML 的方式。主要是通過回撥 Spring Boot 的監聽器 ConfigFileApplicationListener 進行處理。

因篇幅過長, @ConfigurationProperties 內容另起一章。