从SpringBoot源码分析 配置文件的加载原理和优先级

本文从SpringBoot源码分析 配置文件的加载原理和配置文件的优先级跟入源码之前,先提一个问题:SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数(VM Options)传入配置项,为什么通过启动参数传入的配置项会“顶掉”配置文件中的配置?示例:application.yml

<strong>server.port</strong>: 8888
spring.profiles.active: dev

application-dev.yml

spring.think: hello

在IDEA中使用命令行配置项VM Options

-D<strong>server.port</strong>=5555

如下图:

图片描述
启动结果:

Tomcat started on <strong>port</strong>(s): 5555 (http) with context path ''

同时在application.yml 和 启动参数(VM options)中设置server.port, 最终采用了 启动参数 中的值。
下面开始从main函数启动处,跟入SpringBoot源码,看看SpringBoot是如何处理的。系统说明JDK:1.8SpringBoot 版本: 2.0.2.RELEASEIDE: IntelliJ IDEA 2017跟入源码正文

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

从SpringApplication.run函数开始,一个方法一个方法的跟入源码。需要跟入的方法给与注释或高亮。IDEA 快捷键:进入方法: Ctrl + 鼠标左键光标前进/后退: Ctrl + Shirt + 右方向键/左方向键依次跟入源码:

#SpringApplication.java
return <strong>run</strong>(new Class<?>[] { primarySource }, args)
#SpringApplication.java
return new SpringApplication(primarySources).<strong>run</strong>(args);
 #SpringApplication.java
    public ConfigurableApplicationContext <strong>run</strong>(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 =<strong> prepareEnvironment</strong>(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 =<strong> getOrCreateEnvironment</strong>();
        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<strong> StandardServletEnvironment</strong>();
        }

虚拟机启动参数的加载 是在StandardServletEnvironment 的实例化过程中完成的。跟入StandardServletEnvironment的实例化过程之前,大家需要先了解Java模板模式。看一下StandardServletEnvironment的类继承关系图(通过IDEA 右键 类名 --> Diagrams --> Show Diagrams Popup 即可显示下图)

图片描述

抽象父类AbstractEnvironment的实例化方法中,调用了可由子类继承的customizePropertySources方法。

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

实体化的过程中回过头来调用了子类StandardServletEnvironment的customizePropertySources方法

#StandardServletEnvironment.java
    protected void<strong> customizePropertySources</strong>(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.<strong>customizePropertySources</strong>(propertySources);
    }

又调用了父类StandardEnvironment的customizePropertySources方法

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

图片描述
通过IDEA 的变量监听功能,可以看到正是StandardEnvironment类的getSystemProperties()方法获取到了之前设置的虚拟机启动参数server.port的值。继续跟进去

#AbstractEnvironment.java
    public Map<String, Object><strong> getSystemProperties</strong>() {
        try {
            //跟入
            return (Map) System.<strong>getProperties</strong>();
#System.java
    public static Properties <strong>getProperties</strong>() {
        SecurityManager sm = getSecurityManager();
        if (sm != null) {
            sm.checkPropertiesAccess();
        }

        return <strong>props</strong>;

我们搜索一下有没有什么地方初始化 props


#System.java
    private static Properties <strong>props</strong>;
    private static native Properties <strong>initProperties</strong>(Properties props);

发现了静态方法 initProperties,从方法名上即可知道在类被加载的时候 就初始化了 props, 这是个本地方法,继续跟的话需要看对应的C++代码。回到StandardEnvironment类的customizePropertySources方法

#StandardEnvironment.java
    protected void<strong> customizePropertySources</strong>(MutablePropertySources propertySources) {
        //SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME: systemProperties
        //跟入
        propertySources.addLast(new<strong> MapPropertySource</strong>(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 <strong>addLast</strong>(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<strong> environmentPrepared</strong>(ConfigurableEnvironment environment) {
        for (SpringApplicationRunListener listener : this.listeners) {
            //跟入
            listener.<strong>environmentPrepared</strong>(environment);
        }
    }
#EventPublishingRunListener.java
    public void<strong> environmentPrepared</strong>(ConfigurableEnvironment environment) {
        //广播ApplicationEnvrionmentPreparedEvnet事件
        //跟入
        this.initialMulticaster.<strong>multicastEvent</strong>(new ApplicationEnvironmentPreparedEvent(
                this.application, this.args, environment));
    }
#SimpleApplicationEventMulticaster.java
    public void<strong> multicastEvent</strong>(ApplicationEvent event) {
        //跟入
<strong>        multicastEvent</strong>(event, resolveDefaultEventType(event));
    }

    @Override
    public void <strong>multicastEvent</strong>(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 {
                //跟入
<strong>                invokeListener</strong>(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 <strong>invokeListener</strong>(ApplicationListener<?> listener, ApplicationEvent event) {
        ErrorHandler errorHandler = getErrorHandler();
        if (errorHandler != null) {
            try {
                doInvokeListener(listener, event);
            }
            catch (Throwable err) {
                errorHandler.handleError(err);
            }
        }
        else {
            //跟入
<strong>            doInvokeListener</strong>(listener, event);
        }
    }

    private void<strong> doInvokeListener</strong>(ApplicationListener listener, ApplicationEvent event) {
        try {
            //跟入
            listener.<strong>onApplicationEvent</strong>(event);
        }
#ApplicationListener.java
    //实现接口的监听器当中,有并跟入ConfigFileApplicationListener的实现
    void <strong>onApplicationEvent</strong>(E event);

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

    private void<strong> onApplicationEnvironmentPreparedEvent</strong>(
            ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            //跟入:当postProcessor 为 ConfigFileApplicationListener
            postProcessor.<strong>postProcessEnvironment</strong>(event.getEnvironment(),
                    event.getSpringApplication());
        }
    }
#ConfigFileApplicationListener.java
    public void<strong> postProcessEnvironment</strong>(ConfigurableEnvironment environment,
            SpringApplication application) {
        //跟入
<strong>        addPropertySources</strong>(environment, application.getResourceLoader());
    }

    protected void<strong> addPropertySources</strong>(ConfigurableEnvironment environment,
            ResourceLoader resourceLoader) {
        //environment的属性源中包含 systemProperties 属性源 即包含 server.port启动参数
        RandomValuePropertySource.addToEnvironment(environment);
        //跟入 load()方法
        new Loader(environment, resourceLoader).<strong>load</strong>();
    }

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

#ConfigFileApplicationListener.java
        public void<strong> load</strong>() {
            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);
            }
            //跟入
            <strong>load</strong>(null, this::getNegativeProfileFilter,
                    addToLoaded(MutablePropertySources::addFirst, true));
            addLoadedPropertySources();
        }
#ConfigFileApplicationListener.java
        private void<strong> load</strong>(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) -><strong> load</strong>(location, name, profile, filterFactory, consumer));
            });
        }
#ConfigFileApplicationListener.java
        private void<strong> load</strong>(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中完成
<strong>                    loadForFileExtension</strong>(loader, prefix, fileExtension, profile,
                            filterFactory, consumer);
                }
            }
        }

继续跟入 loadForFileExtension 方法,可以了解载入一个配置文件的更多细节。回到之前的load()方法

#ConfigFileApplicationListener.java
        public void<strong> load</strong>() {
            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));
            //跟入
            <strong>addLoadedPropertySources</strong>();
#ConfigFileApplicationListener.java
        private void<strong> addLoadedPropertySources</strong>() {
            //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文件