1. 程式人生 > >從SpringBoot原始碼分析 配置檔案的載入原理和優先順序

從SpringBoot原始碼分析 配置檔案的載入原理和優先順序

從SpringBoot原始碼分析 配置檔案的載入原理和優先順序

本文從SpringBoot原始碼分析 配置檔案的載入原理和配置檔案的優先順序

 

  跟入原始碼之前,先提一個問題

  SpringBoot 既可以載入指定目錄下的配置檔案獲取配置項,也可以通過啟動引數(VM Options)傳入配置項,為什麼通過啟動引數傳入的配置項會“頂掉”配置檔案中的配置?

 

示例

 

application.yml 

server.port: 8888
spring.profiles.active: dev

 

 

application-dev.yml 

spring.think: hello

 

在IDEA中使用命令列配置項 

VM Options 

-Dserver.port=5555

如下圖:

 

啟動結果:

Tomcat started on port(s): 5555 (http) with context path ''

 

  同時在application.yml 和 啟動引數(VM options)中設定 server.port

, 最終採用了 啟動引數 中的值。

 

  下面開始從main函式啟動處,跟入SpringBoot原始碼,看看SpringBoot是如何處理的。

 

 

系統說明

JDK:1.8

SpringBoot 版本: 2.0.2.RELEASE

IDE: IntelliJ IDEA 2017

 

 

跟入原始碼正文 

#ApplicationConfigLoadFlow.java
    public static void main(String[] args) {
        SpringApplication.run
(ApplicationConfigLoadFlow.class, args); }

 

   從SpringApplication.run 函式開始,一個方法一個方法的跟入原始碼。需要跟入的方法給與註釋或高亮。

 

IDEA 快捷鍵:

進入方法:  Ctrl + 滑鼠左鍵

游標前進/後退: Ctrl + Shirt + 右方向鍵/左方向鍵

 

  依次跟入原始碼:

#SpringApplication.java
return run(new Class<?>[] { primarySource }, args)
#SpringApplication.java
return new SpringApplication(primarySources).run(args);

複製程式碼

 #SpringApplication.java
    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            //跟入
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            configureIgnoreBeanInfo(environment);
            configureIgnoreBeanInfo(environment);

複製程式碼

 

  進入public ConfigurableApplicationContext run(String... args) 方法後,我們重點看 prepareEnvironment這個方法。

  這個方法之前的原始碼的從類名和原始碼註釋上知道stopWatch用於計時,上下文context還未初始化,listeners監聽器儲存了EventPushlingRunListener。

 

  通過IDEA 一行行debug可以看到是在 prepareEnvironment方法執行後,server.port 配置項才被載入入 environment 環境配置中。

  如下圖所示。注意:配置檔案中的配置還未載入,請先接著往後看。


 

  因此,我們重新打斷點跟入prepareEnvironment方法。

 

複製程式碼

#SpringApplication.java
    private ConfigurableEnvironment prepareEnvironment(
            SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        // Create and configure the environment
        //跟入
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        configureEnvironment(environment, applicationArguments.getSourceArgs());

複製程式碼

 

     同樣的套路,通過debug發現實在getOrCreateEnvironment方法執行後得到server.port的值 

 

複製程式碼

#SpringApplication.java
    private ConfigurableEnvironment getOrCreateEnvironment() {
        if (this.environment != null) {
            return this.environment;
        }
        if (this.webApplicationType == WebApplicationType.SERVLET) {
            //跟入 
            return new StandardServletEnvironment();
        }

複製程式碼

 

  虛擬機器啟動引數的載入 是在StandardServletEnvironment 的例項化過程中完成的。

  跟入StandardServletEnvironment的例項化過程之前,大家需要先了解 Java模板模式 。

  看一下StandardServletEnvironment的類繼承關係圖(通過IDEA 右鍵 類名 --> Diagrams --> Show Diagrams Popup 即可顯示下圖)



 

抽象父類AbstractEnvironment的例項化方法中,呼叫了可由子類繼承的customizePropertySources方法。

 

複製程式碼

#AbstractEnvironment.java
    public AbstractEnvironment() {
        //跟入
        customizePropertySources(this.propertySources);
        if (logger.isDebugEnabled()) {
            logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
        }
    }

複製程式碼

 

  實體化的過程中回過頭來呼叫了子類StandardServletEnvironment的customizePropertySources方法

 

複製程式碼

#StandardServletEnvironment.java
    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方法

 

複製程式碼

#StandardEnvironment.java
    protected void customizePropertySources(MutablePropertySources propertySources) {
        //跟入
        propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
        propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }

複製程式碼

 

 

  通過IDEA 的變數監聽功能,可以看到正是StandardEnvironment類的getSystemProperties()方法獲取到了之前設定的虛擬機器啟動引數server.port的值。

 

  繼續跟進去

#AbstractEnvironment.java
    public Map<String, Object> getSystemProperties() {
        try {
            //跟入
            return (Map) System.getProperties();

 

複製程式碼

#System.java
    public static Properties getProperties() {
        SecurityManager sm = getSecurityManager();
        if (sm != null) {
            sm.checkPropertiesAccess();
        }

        return props;

複製程式碼

 

  我們搜尋一下有沒有什麼地方初始化 props

 
#System.java
    private static Properties props;
    private static native Properties initProperties(Properties props);

 

  發現了靜態方法 initProperties,從方法名上即可知道在類被載入的時候 就初始化了 props, 這是個本地方法,繼續跟的話需要看對應的C++程式碼。

 

  回到StandardEnvironment類的customizePropertySources方法

 

複製程式碼

#StandardEnvironment.java
    protected void customizePropertySources(MutablePropertySources propertySources) {
        //SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME: systemProperties
        //跟入
        propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
        propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }

複製程式碼

 

#MutablePropertySources.java
    /**
     * Add the given property source object with lowest precedence.
     * 新增屬性源,並使其優先順序最低
     */
    public void addLast(PropertySource<?> propertySource) {

 

 再看一下MutablePropertySources的類註釋

 

複製程式碼

 * <p>Where <em>precedence</em> is mentioned in methods such as {@link #addFirst}
 * and {@link #addLast}, this is with regard to the order in which property sources
 * will be searched when resolving a given property with a {@link PropertyResolver}.
 *
 * addFist 和 add Last 會設定屬性源的優先順序,
 * PropertyResolver解析配置時會根據優先順序使用配置源
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.1
 * @see PropertySourcesPropertyResolver
 */
public class MutablePropertySources implements PropertySources {

複製程式碼

 

 

問題2:

 

  此時我們已經看到虛擬機器的啟動引數先新增到系統當中,那麼後面新增進來的Property Source屬性源的優先順序是否比 SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME(systemProperties) 屬性源的優先順序高呢?

 

  回到SpringApplication的prepareEnvironment方法



  同樣的debug套路發現listeners.environmentPrepared執行後,application.yml 和 application-dev.yml 兩個配置檔案的配置項都被載入完成,所以我們繼續跟入environmentPrepared方法

 

  在跟入environmentPrepared方法之前,需要了解 Java事件監聽機制

 

  跟入environmentPrepared中的原始碼

 

複製程式碼

#SpringApplicationRunListeners.java
    public void environmentPrepared(ConfigurableEnvironment environment) {
        for (SpringApplicationRunListener listener : this.listeners) {
            //跟入
            listener.environmentPrepared(environment);
        }
    }

複製程式碼

 

複製程式碼

#EventPublishingRunListener.java
    public void environmentPrepared(ConfigurableEnvironment environment) {
        //廣播ApplicationEnvrionmentPreparedEvnet事件
        //跟入
        this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
                this.application, this.args, environment));
    }

複製程式碼

 

複製程式碼

#SimpleApplicationEventMulticaster.java
    public void multicastEvent(ApplicationEvent event) {
        //跟入
        multicastEvent(event, resolveDefaultEventType(event));
    }

    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        //注意此時 getApplicationListeners(event, type) 返回結果
        //包含 監聽器 *ConfigFileApplicationListener*
                for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            Executor executor = getTaskExecutor();
            if (executor != null) {
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                //跟入
                invokeListener(listener, event);
            }
        }
    }

複製程式碼

 

複製程式碼

#SimpleApplicationEventMulticaster.java
    /**
     * Invoke the given listener with the given event.
     * 呼叫對應事件的監聽者
     * @param listener the ApplicationListener to invoke
     * @param event the current event to propagate
     * @since 4.1
     */
    protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
        ErrorHandler errorHandler = getErrorHandler();
        if (errorHandler != null) {
            try {
                doInvokeListener(listener, event);
            }
            catch (Throwable err) {
                errorHandler.handleError(err);
            }
        }
        else {
            //跟入
            doInvokeListener(listener, event);
        }
    }

    private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
        try {
            //跟入
            listener.onApplicationEvent(event);
        }

複製程式碼

 

#ApplicationListener.java
    //實現介面的監聽器當中,有並跟入ConfigFileApplicationListener的實現
    void onApplicationEvent(E event);
 

複製程式碼

#ConfigFileApplicationListener.java
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            //跟入
            onApplicationEnvironmentPreparedEvent(
                    (ApplicationEnvironmentPreparedEvent) event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent(event);
        }
    }

    private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            //跟入:當postProcessor 為 ConfigFileApplicationListener
            postProcessor.postProcessEnvironment(event.getEnvironment(),
                    event.getSpringApplication());
        }
    }

複製程式碼

 

複製程式碼

#ConfigFileApplicationListener.java
    public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        //跟入
        addPropertySources(environment, application.getResourceLoader());
    }

    protected void addPropertySources(ConfigurableEnvironment environment,
            ResourceLoader resourceLoader) {
        //environment的屬性源中包含 systemProperties 屬性源 即包含 server.port啟動引數
        RandomValuePropertySource.addToEnvironment(environment);
        //跟入 load()方法
        new Loader(environment, resourceLoader).load();
    }

複製程式碼

 

  跟入load之前,需要了解  java lambda表示式

 

複製程式碼

#ConfigFileApplicationListener.java
        public void load() {
            this.profiles = new LinkedList<>();
            this.processedProfiles = new LinkedList<>();
            this.activatedProfiles = false;
            this.loaded = new LinkedHashMap<>();
            initializeProfiles();
            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                load(profile, this::getPositiveProfileFilter,
                        addToLoaded(MutablePropertySources::addLast, false));
                this.processedProfiles.add(profile);
            }
            //跟入
            load(null, this::getNegativeProfileFilter,
                    addToLoaded(MutablePropertySources::addFirst, true));
            addLoadedPropertySources();
        }

複製程式碼

 

複製程式碼

#ConfigFileApplicationListener.java
        private void load(Profile profile, DocumentFilterFactory filterFactory,
                DocumentConsumer consumer) {
            //getSearchLocations()預設返回:
            //[./config/, file:./, classpath:/config/, classpath:/]
            //即搜尋這些路徑下的檔案
            getSearchLocations().forEach((location) -> {
                boolean isFolder = location.endsWith("/");
                //getSearchNames()返回:application
                Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
                //跟入load(.....)
                names.forEach(
                        (name) -> load(location, name, profile, filterFactory, consumer));
            });
        }

複製程式碼

 

複製程式碼

#ConfigFileApplicationListener.java
        private void load(String location, String name, Profile profile,
                DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            //name預設為:application,所以這個if分支略過
            if (!StringUtils.hasText(name)) {
                for (PropertySourceLoader loader : this.propertySourceLoaders) {
                    if (canLoadFileExtension(loader, location)) {
                        load(loader, location, profile,
                                filterFactory.getDocumentFilter(profile), consumer);
                    }
                }
            }
            //this.propertySourceLoaders: PropertiesPropertySourceLoader,YamlPropertySourceLoader
            for (PropertySourceLoader loader : this.propertySourceLoaders) {
                //PropertiesPropertySourceLoader.getFileExtensions(): properties, xml
                //YamlPropertySourceLoader.getFileExtensions(): yml, yaml
                for (String fileExtension : loader.getFileExtensions()) {
                    //location: [./config/, file:./, classpath:/config/, classpath:/]
                    //name: application
                    String prefix = location + name;
                    fileExtension = "." + fileExtension;
                    //profile: null, dev
                    //相當於對(location, fileExtension, profile)做笛卡爾積,
                    //遍歷每一種可能,然後載入
                    //載入檔案的細節在loadForFileExtension中完成
                    loadForFileExtension(loader, prefix, fileExtension, profile,
                            filterFactory, consumer);
                }
            }
        }

複製程式碼

 

  繼續跟入 loadForFileExtension 方法,可以瞭解載入一個配置檔案的更多細節。

 

  回到之前的load()方法

 

複製程式碼

#ConfigFileApplicationListener.java
        public void load() {
            this.profiles = new LinkedList<>();
            this.processedProfiles = new LinkedList<>();
            this.activatedProfiles = false;
            this.loaded = new LinkedHashMap<>();
            initializeProfiles();
            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                load(profile, this::getPositiveProfileFilter,
                        addToLoaded(MutablePropertySources::addLast, false));
                this.processedProfiles.add(profile);
            }
            load(null, this::getNegativeProfileFilter,
                    addToLoaded(MutablePropertySources::addFirst, true));
            //跟入
            addLoadedPropertySources();

複製程式碼

 

複製程式碼

#ConfigFileApplicationListener.java
        private void addLoadedPropertySources() {
            //destination: 進入ConfigFileApplicationListener監聽器前已有的配置 
            //即destination中包含 systemProperties 配置源
            MutablePropertySources destination = this.environment.getPropertySources();
            String lastAdded = null;
            //loaded: 此次監聽通過掃描檔案載入進來的配置源
            //loaded: application.yml, appcalition-dev.yml
            List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
            //倒序後 loaded: application-dev.yml, application.yml
            Collections.reverse(loaded);
            //先處理 application-dev.yml
            for (MutablePropertySources sources : loaded) {
                for (PropertySource<?> source : sources) {
                    //第一次進入: lastAdded:null
                    if (lastAdded == null) {
                        if (destination.contains(DEFAULT_PROPERTIES)) {
                            destination.addBefore(DEFAULT_PROPERTIES, source);
                        }
                        else {
                            //第一次進入: 把application-dev.yml至於最低優先順序
                            destination.addLast(source);
                        }
                    }
                    else {
                        //第二次進入:
                        //讓 application.yml 優先順序比 application-dev.yml 低
                        destination.addAfter(lastAdded, source);
                    }
                    //第一次遍歷結束: lastAdded: application-dev
                    lastAdded = source.getName();
                }
            }
        }

複製程式碼

 

 執行後得到各自的優先順序,如下圖:

 


   systemProperties優先順序高,解析器會優先使用 systemProperties中的 server.port 配置項即 5555 所以最終Tomcat 啟動埠是 5555

 

  從中也可以看出,如果application.yml 和 application-dev.yml中有相同的配置項,會優先採用application-dev.yml中的配置項。

  

參考:

1. SpringBoot原始碼分析之SpringBoot的啟動過程

2. SpringBoot原始碼分析之配置環境的構造過程 

3. springboot啟動時是如何載入配置檔案application.yml檔案
|
原文地址 :https://www.cnblogs.com/tanliwei/p/9304072.html