• 从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中的配置项。
      
    参考:
  • 相关阅读:
    【转载】C++指针随想
    微信小程序实现电子签名
    js数组常用方法
    css文本两端对齐
    js判断某个数组中是否包含另一个数组
    react 限制小数点位数
    原生js 操作class 原生js获取父元素
    转发: JS中的call()和apply()方法和区别 --小白变色记
    fail2Ban ubuntu
    VSCode 搭建 Vue项目 lite-server
  • 原文地址:https://www.cnblogs.com/tanliwei/p/9304072.html
Copyright © 2020-2023  润新知