• 深入解析spring boot配置加载原理,配置文件的加载顺序是怎么实现的?


    前言

    相信很多人面试都被问到过spring boot配置的加载的顺序是怎么样的,他是怎么一个实现方式,我也被问到过,接下来这篇文章将详细解析一下spring boot配置的加载流程到底是怎么实现的。

    在开始前我准备几个问题

    1. 简单的spring boot项目里面能不能加载bootstrap依赖?
    2. 配置文件的加载顺序是怎么实现的?

    准备工作

    1. 新建spring boot项目

    2. 添加配置文件(application.properties)

      #application.properties
      name=application-properties
      
      • 1
      • 2
    3. 添加controller

      @RestController
      @RequestMapping
      public class TestController {
      
          @Value("${name}")
          private String name;
      
          @RequestMapping("/getValue")
          public String getValue(){
              return "name:"+name;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

    深入源码解析

    1. 寻找加载配置的入口方法

    我们从springboot的入口方法(run)一直点进去可以看到一个164行有个叫this.prepareEnvironment的方法,这里是环境配置准备的入口

    我们在这个方法的下方debugger住,我们可以看到ConfigurableEnvironment已经加载好了,在框住的红色区域内正是我们配置文件的name和对应的属性

    在这里我们记住几个重要的关键信息

    1. environment
    2. propertySources
    3. propertySourcesList

    很明显,我们的配置是加载在propertySourcesList里面的

    在这里插入图片描述

    2. spring boot中的监听器

    我们继续进入prepareEnvironment方法,可以看到一个叫listeners.environmentPrepared的方法。

    在这里插入图片描述

    顾名思义,好像是使用监听器加载环境配置?

    我们再进入到environmentPrepared去看看

    在这里插入图片描述

    可以发现,监听器们会在此处调用执行方法,doWithListeners会在此处执行传入进来的监听器,spring.boot.application.environment-prepared表示环境准备的监听器

    我们继续往下看看,进入listener.environmentPrepared方法

    这里建议大家看着源码一步一步跟着追踪,不然可能会比较难懂

    1. 点进后看到SpringApplicationRunListener.environmentPrepared
    2. SpringApplicationRunListener是个接口,往下看到他的实现方法EventPublishingRunListener
    3. 继续往下initialMulticaster.multicastEvent
    4. 可以看到invokeListenerdoInvokeListener,这里就是执行监听器里面的监听事件
    5. doInvokeListener中可以看到listener.onApplicationEvent(event)
    6. 进去方法里可以看到,ApplicationListener是个接口,有个叫onApplicationEvent的方法,我们找到他的实现类EnvironmentPostProcessorApplicationListener,这个是环境处理监听器,里面有个this.onApplicationEnvironmentPreparedEvent的方法
    7. 接下来重点来了,我们可以看到下面这块代码
    while(var4.hasNext()) {
        //获取环境处理器
        EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var4.next();
        //执行所有实现了环境处理器的接口
        postProcessor.postProcessEnvironment(environment, application);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    2.1 EnvironmentPostProcessor接口

    我们可以看到,在EnvironmentPostProcessorApplicationListener中循环调用了EnvironmentPostProcessorpostProcessEnvironment方法

    而spring boot在spring.factories提供了一个提供了一个spi,把所有EnvironmentPostProcessor的实现类通过spring.factories的方式加载到spring boot中,如下图所示

    ps:spring.factories是一个spring boot的重点知识,这里不会重点展开

    在这里插入图片描述

    这里我们留下一个问题:既然这里提供了一个spi机制,我们是不是也可以通过这个机制实现一个Environment处理器呢

    3.如何找到application命名的配置文件的?

    我们继续看到 postProcessor.postProcessEnvironment(environment, application)方法,找到其中一个EnvironmentPostProcessor的实现类,叫做ConfigDataEnvironmentPostProcessor,找到他的实现方法postProcessEnvironment

    在这里插入图片描述

    这里有两个重点,一个是getConfigDataEnvironment,另一个是processAndApply

    1. getConfigDataEnvironment用来加载解析起,比如properties或者yml,yaml那些
    2. processAndApply用来解析数据并且把数据设置进去environment里面

    3.1 解析getConfigDataEnvironment方法,为什么bootstrap配置会失效?

    我们先来看getConfigDataEnvironment方法,点进去看本质是new了一个ConfigDataEnvironment,我们从他的构造方法可以看到有个叫做this.createConfigDataLocationResolvers方法,解析器就是在这加载的。

    在这里插入图片描述

    可能到这里还是有点蒙,这个解析器到底是个啥,我们再点进去看看。作为一个码农,一定要刨根到底~

    点进去发现还是一样,new了一个ConfigDataLocationResolvers

    在这里插入图片描述

    这里有又有两个重点

    1. SpringFactoriesLoader.loadFactoryNames(ConfigDataLocationResolver.class, resourceLoader.getClassLoader())

      通过spring.factories获取两个解析器,重点是StandardConfigDataLocationResolver为标准化解析器

      在这里插入图片描述

    2. instantiator.instantiate(resourceLoader.getClassLoader(), names)

      实例化两个解析器,ConfigTreeConfigDataLocationResolver这里细讲,我们来看StandardConfigDataLocationResolver

      在这里插入图片描述

      这里又细分3个小点

      1. DEFAULT_CONFIG_NAMES我们可以看到。他默认是找到application,所以为什么我们的配置文件都以application命名的原因

      2. 再次通过spring.factories获取数据源加载器PropertiesPropertySourceLoaderYamlPropertySourceLoader

        在这里插入图片描述

        看名字就知道,这两个加载分别加载了properties和ymal的文件

        PropertiesPropertySourceLoader是用来解析properties和xml的

        在这里插入图片描述

        YamlPropertySourceLoader是用来解析yml和yaml的

        在这里插入图片描述

        所以在最后会和解析器中的application拼接成application.properties或者application.yml等

      3. 最终configNames确定是application还是其他名称,没有外部定义的话默认为application,所以为什么很多人问在spring boot中配置文件以bootstrap命名会失效,因为在寻常spring boot项目中解析器压根就没有定义bootstrap的文件名,所以无法解析。而在spring cloud中会有一个监听器往binder里加了个bootstrap变量,binder.bind("spring.config.name", String[].class)

    3.2 processAndApply预加载配置

    processAndApply字面意思:处理和应用

    首先我们找到里面的processInitial中的contributors.withProcessedImports里面

    在这里插入图片描述

    在这里插入图片描述

    这里有个while循环,会在红框的resolveAndLoad这里循环两次分别解析yml和properties,就是用到上面3.1所说的PropertiesPropertySourceLoaderYamlPropertySourceLoader

    在这里插入图片描述

    进去resolveAndLoad可以看到他调用了两个方法一个是resolveload

    1. resolve,加载解析器,把解析器的属性拼接成application.properties或者application.yml
    2. load,加载配置文件的数据,但是此时还并没有加载进propertySourcesList里。相当于先加载到内存,最终把所有流程处理完才加载到propertySourcesList

    在这里插入图片描述

    3.3 将加载的配置放到propertySourcesList中

    继续回看到processAndApply方法,最下面有个applyToEnvironment,我们点进去看看

    在这里插入图片描述

    可以看到,前面check方法都是用来检查配置的,我们主要看applyContributor方法

    后面的方法根据关键字profilesactiveProfiles不难看出,是用来加载不同环境的配置,例如dev,test,prod等

    在这里插入图片描述

    可以看到配置文件最终是在这里添加到propertySourcesList里面的

    在这里插入图片描述

    4.什么时候加到@value注解里

    这里浅谈一下,毕竟这章重点在上面已经讲完了

    在run方法的refreshContext里,有个refresh就是在这加载的,如下图所示

    在这里插入图片描述

    然后通过反射生成一个AutowiredAnnotationBeanPostProcessor实例

    我们可以看到他的构造方法里面添加了AutowiredValue两个注解

    在这里插入图片描述

    最后在AutowiredMethodElement内部类里将值自动装配进去

    在这里插入图片描述

    扩展

    1. 实现自定义命名的配置文件

    我们在上面的2.1说到,我们是不是也可以通过这个机制实现一个Environment处理器?当然是可以的

    1.1 添加test.properties文件

    #test.properties
    name=test-propertis
    
    • 1
    • 2

    注释application.properties里面的配置

    1.2 添加TestEnvironmentPostProcessor类并实现EnvironmentPostProcessor接口

    public class TestEnvironmentPostProcessor implements EnvironmentPostProcessor {
        private final Properties properties=new Properties();
        private String propertiesFile="test.properties";
    
        @Override
        public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
            Resource resource=new ClassPathResource(propertiesFile);
            PropertySource<?> propertySource = loadProperties(resource);
            //添加到propertySourcesList中
            environment.getPropertySources().addLast(propertySource);
        }
    
        private PropertySource<?> loadProperties(Resource resource){
            if(!resource.exists()){
                throw new RuntimeException("file not exist");
            }
            try {
                //加载test.properties
                properties.load(resource.getInputStream());
                return new PropertiesPropertySource(resource.getFilename(),properties);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    1.3 添加spring.factories文件

    新建META-INF文件夹,并添加spring.factories文件

    在这里插入图片描述

    org.springframework.boot.env.EnvironmentPostProcessor=\
      com.nssnail.main.config.TestEnvironmentPostProcessor
    
    • 1
    • 2

    1.4 调试

    访问http://localhost:8080/getValue接口(代码在上面准备工作目录下)

    结果显示访问成功,并没有报错

    在这里插入图片描述

    1.5 自定义yml

    上面的配置只适用properties,那么像自定义yml配置怎么办呢,代码如下所示

    public class TestEnvironmentPostProcessor implements EnvironmentPostProcessor {   
    	private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
    
        @Override
        public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
            String propertiesFile = "test.yml";
            Resource resource=new ClassPathResource(propertiesFile);
            PropertySource<?> propertySource = loadProperties(resource);
            //添加到propertySourcesList中
            environment.getPropertySources().addLast(propertySource);
        }
    
        private PropertySource<?> loadProperties(Resource resource){
            if(!resource.exists()){
                throw new RuntimeException("file not exist");
            }
            try {
                return loader.load("test",resource).get(0);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    2. 自定义配置文件的顺序

    我们可以看到propertySourcesList有四个add方法,如下图所示

    在这里插入图片描述

    方法名含义
    addFirst加在propertySourcesList的最前面
    addLast加在propertySourcesList的最后面
    addAfter加在propertySourcesList中某个配置的后面
    addBefore加在propertySourcesList中某个配置的前面

    配置文件的加在顺序跟这个propertySourcesList的顺序有关,最前面的配置优先级越高,所以我们在上面的addLast是放在最后面,意为优先级最低

    这是我们再次把application.properties的配置放开

    在这里插入图片描述

    test.properties保持不变

    在这里插入图片描述

    访问http://localhost:8080/getValue

    很明显,返回的是application-properties

    在这里插入图片描述

    这时我们将TestEnvironmentPostProcessor中的addLast改为addFirst

    在这里插入图片描述

    再次访问http://localhost:8080/getValue

    可以看到,返回的是test-properties,这验证了上面的观点,配置文件的优先级是跟propertySourcesList的顺序有关的

    在这里插入图片描述

    3. 实现其他spi接口?

    既然我们可以通过实现EnvironmentPostProcessor接口来扩展自定义配置文件,他们我们是不是可以通过实现PropertySourceLoader或者ConfigDataLocationResolver来扩展自定义配置解析器呢?这里大家可以自己动手试下

    总结

    1. 配置是放在ConfigurableEnvironmentpropertySourcesList中的
    2. 寻常spring boot项目默认是无法加载bootstrap配置的,而在spring cloud项目中一般有配置外部的bootstrap变量,所以可以正常加载
    3. 自动装配在AutowiredAnnotationBeanPostProcessor中实现
    4. 配置文件的加载顺序跟propertySourcesList的顺序有关

    ps:有不对的地方欢迎指出,转载注明出处,谢谢

  • 相关阅读:
    IaC:实现持续交付和 DevOps 自动化的关键
    【C/C++笔试练习】继承和派生的概念、虚函数的概念、派生类的析构函数、纯虚函数的概念、动态编译、多态的实现、参数解析、跳石板
    MySQL - UNION 与 UNION ALL
    《Web安全基础》06. 逻辑漏洞&越权
    在 macOS 上管理 Node版本
    画图带你彻底弄懂三级缓存和循环依赖的问题
    如何学习BCGControlBar?
    PPI数据集分析
    Android 系统启动到App 界面完全展示终于明白(图文版)
    【创建springboot-maven项目搭建mybatis框架】(超详细)
  • 原文地址:https://blog.csdn.net/qq_26751319/article/details/126543629