SpringBoot之淺析配置項解析(一)

在我們的開發工作總是離不了配置項相關的配置工作,SpringBoot也爲我們提供了@ConfigurationProperties註解來進行配置項信息的配置工作,同時也提供了幾個配置文件的默認加載位置,如:classpath:application.properties、classpath:application.yml、classpath:application.yaml、classpath:/config/application.properties、classpath:/config/application.yml、classpath:/config/application.yaml等。另外我們還可以在命令行中、系統屬性中、虛擬機參數中、Servlet上下文中進行配置項的配置,既然有這麼多的配置位置,程序在加載配置項的時候總得有一個先後順序吧,要不然系統不就亂套了。在SpringBoot中大概有這樣的一個先後加載順序(優先級高的會覆蓋優先級低的配置):

  1. 命令行參數。
  2. Servlet初始參數
  3. ServletContext初始化參數
  4. JVM系統屬性
  5. 操作系統環境變量
  6. 隨機生成的帶random.*前綴的屬性
  7. 應用程序以外的application.yml或者appliaction.properties文件
  8. classpath:/config/application.yml或者classpath:/config/application.properties
  9. 通過@PropertySource標註的屬性源
  10. 默認屬性

那麼SpringBoot是怎麼創建這樣的一個優先順序的呢?默認的application.properties是怎麼被加載的呢?在本章中我將把其中奧祕慢慢道出:
我在之前的SpringBoot啓動流程簡析的文章中說過,SpringBoot在啓動的過程中會從spring.factories中加載一些ApplicationListener,在這些ApplicationListener中其中就有一個我們今天要說的ConfigFileApplicationListener;我們之前也說過在啓動過程中會創建ConfigurableEnvironment,也會進行命令行參數的解析工作。在org.springframework.boot.SpringApplication#prepareEnvironment這個方法中有這樣的一段代碼:
prepareEnvironment
先創建應用可配置的環境變量,爲命令行進行環境變量配置工作:

    protected void configureEnvironment(ConfigurableEnvironment environment,
            String[] args) {
        //將命令行參數轉換爲org.springframework.core.env.PropertySource
        configurePropertySources(environment, args);
        //Profile的配置,這裏先不說明
        configureProfiles(environment, args);
    }
    protected void configurePropertySources(ConfigurableEnvironment environment,
            String[] args) {
        //從上面創建的ConfigurableEnvironment實例中獲取MutablePropertySources實例
        MutablePropertySources sources = environment.getPropertySources();
        //如果有defaultProperties屬性的話,則把默認屬性添加爲最後一個元素
        if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
            sources.addLast(
                    new MapPropertySource("defaultProperties", this.defaultProperties));
        }
        //這裏addCommandLineProperties默認爲true 如果有命令行參數的數
        if (this.addCommandLineProperties && args.length > 0) {
            //name爲:commandLineArgs
            String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
            //如果之前的MutablePropertySources中有name爲commandLineArgs的PropertySource的話,則把當前命令行參數轉換爲CompositePropertySource類型,和原來的PropertySource進行合併,替換原來的PropertySource
            if (sources.contains(name)) {
                PropertySource<?> source = sources.get(name);
                CompositePropertySource composite = new CompositePropertySource(name);
                composite.addPropertySource(new SimpleCommandLinePropertySource(
                        name + "-" + args.hashCode(), args));
                composite.addPropertySource(source);
                sources.replace(name, composite);
            }
            else {
                //如果之前沒有name爲commandLineArgs的PropertySource的話,則將其添加爲MutablePropertySources中的第一個元素,注意了這裏講命令行參數添加爲ConfigurableEnvironment中MutablePropertySources實例的第一個元素,且永遠是第一個元素
                sources.addFirst(new SimpleCommandLinePropertySource(args));
            }
        }
    }

從上面的代碼中我們可以看到,SpringBoot把命令行參數轉換爲PropertySource,並添加爲環境變量中的第一個元素!這裏簡單的提一下MutablePropertySources 這個類,它的UML如下所示:
MutablePropertySources
從上面的UML中我們可以看到,MutablePropertySources實現了Iterable接口,是一個可迭代的類,在這個類中有這樣的一個屬性:

private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<PropertySource<?>>();

一個類型爲PropertySource的CopyOnWriteArrayList,這裏用的是CopyOnWriteArrayList,而不是ArrayList、LinkedList,大家可以想一下這裏爲什麼用了CopyOnWriteArrayList。
propertySourceList
MutablePropertySources中的這些方法都是通過CopyOnWriteArrayList中的方法來實現的。我們在之前的文章中說明,SpringBoot創建的ConfigurableEnvironment實例是StandardServletEnvironment,其UML類圖如下:
StandardServletEnvironment,其在實例化的過程中,會調用父類的構造函數先實例化父類,其父類StandardEnvironment爲默認無參構造函數,AbstractEnvironment中的無參構造函數如下:

    public AbstractEnvironment() {
    //調用customizePropertySources方法進行定製PropertySource
    customizePropertySources(this.propertySources);
    }

在StandardServletEnvironment和StandardEnvironment分別重寫了這個方法,其調用爲StandardServletEnvironment中的customizePropertySources方法,其源碼如下:

    protected void customizePropertySources(MutablePropertySources propertySources) {
    //SERVLET_CONFIG_PROPERTY_SOURCE_NAME 爲 servletConfigInitParams 添加servletConfigInitParams 的PropertySource
        propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
    //SERVLET_CONTEXT_PROPERTY_SOURCE_NAME爲servletContextInitParams 添加servletContextInitParams 的PropertySource
        propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
    //如果有JNDI
        if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
    //JNDI_PROPERTY_SOURCE_NAME 爲 jndiProperties 添加jndiProperties  的PropertySource
            propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
        }
        //調用父類的StandardEnvironment中的customizePropertySources
        super.customizePropertySources(propertySources);
    }
    protected void customizePropertySources(MutablePropertySources propertySources) {
    //SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME 爲 systemProperties 添加systemProperties 的PropertySource
        propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
    //SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME 爲 systemEnvironment 添加systemEnvironment的PropertySource
        propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }

從上面的分析中我們可以看到在創建StandardServletEnvironment的實例的時候,會向org.springframework.core.env.AbstractEnvironment#propertySources中按順序添加:name分別爲:servletConfigInitParams、servletContextInitParams、jndiProperties 、systemProperties、systemEnvironment 的PropertySource,再按照我們前面的分析將name爲commandLineArgs的PropertySource放到第一位,則org.springframework.core.env.AbstractEnvironment#propertySources的順序到現在爲:commandLineArgs、servletConfigInitParams 、servletContextInitParams 、jndiProperties 、systemProperties 、systemEnvironment,是不是和我們前面說的對照起來了?我們一直在說PropertySource,也一直在說PropertySource中的name,對於PropertySource我們可以理解爲帶name的、存放 name/value 的property pairs;那麼其中的name我們應該如何理解呢?在org.springframework.core.env.MutablePropertySources中有這樣一個方法:addAfter,其作用是將某個PropertySource的實例添加到某個name的PropertySource的後面,其源碼如下所示:

    public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
        //確定所傳入的relativePropertySourceName和所傳入的propertySource的name不相同
        assertLegalRelativeAddition(relativePropertySourceName, propertySource);
        //如果之前添加過此PropertySource 則移除
        removeIfPresent(propertySource);
        //獲取所傳入的relativePropertySourceName的位置
        int index = assertPresentAndGetIndex(relativePropertySourceName);
        //將傳入的propertySource添加到相應的位置
        addAtIndex(index + 1, propertySource);
    }

在上面的代碼中有assertPresentAndGetIndex這樣的一段代碼比較重要:

    int index = assertPresentAndGetIndex(relativePropertySourceName);

    private int assertPresentAndGetIndex(String name) {
        //獲取name爲某個值的PropertySource的位置,
        int index = this.propertySourceList.indexOf(PropertySource.named(name));
        if (index == -1) {
            throw new IllegalArgumentException("PropertySource named '" + name + "' does not exist");
        }
        return index;
    }

在上面的代碼中獲取PropertySource的元素的位置的時候,是調用List中的indexOf方法來進行查找的,但是其參數爲PropertySource.named(name)產生的對象,我們之前往propertySourceList中放入的明明是PropertySource類型的對象,這裏在查找的時候爲什麼要用PropertySource.named(name)產生的對象來進行索引位置的查找呢?PropertySource.named(name)產生的對象又是什麼呢?

    public static PropertySource<?> named(String name) {
        return new ComparisonPropertySource(name);
    }

PropertySource.named(name)產生的對象是ComparisonPropertySource的實例,它也是PropertySource的一個子類,那麼爲什麼用它也能查找到之前放入到propertySourceList中的元素的位置呢?通過翻開indexOf這個方法的源碼我們知道,它是通過調用元素的equals方法來判斷是否是同一個元素的,而湊巧的是在PropertySource中重寫了equals這個方法:

    public boolean equals(Object obj) {
        return (this == obj || (obj instanceof PropertySource &&
                ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) obj).name)));
    }

到這裏就很明顯了,PropertySource中的name屬性是用來判斷是否是同一個元素的,即是否是同一個PropertySource的實例!我們在創建PropertySource類型的子類的時候都會傳入一個name,直接用我們創建的PropertySource來進行位置的查找不就可以了嗎?爲什麼還要創建出來一個ComparisonPropertySource類呢?通過翻看ComparisonPropertySource這個類的源碼我們可以發現,在這個類中調用getSource、containsProperty、getProperty方法都會拋出異常,並且除了這三個方法之外沒有多餘的方法,如果直接用我們創建的PropertySource的話,保不齊你會重寫它的equals方法,是不是?用ComparisonPropertySource的話,即使你在別的PropertySource實現類重寫了PropertySource方法,在查找其順序是也要按照Spring定義的規則來,並且ComparisonPropertySource只能做幹查找元素位置這一件事,其他的事它什麼也幹不了,這又是不是設計模式中的某一個原則的體現呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章