前言

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

在开始前我准备几个问题

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

准备工作

  1. 新建spring boot项目

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

    #application.properties
    name=application-properties
    
  3. 添加controller

    @RestController
    @RequestMapping
    public class TestController {
    
        @Value("${name}")
        private String name;
    
        @RequestMapping("/getValue")
        public String getValue(){
            return "name:"+name;
        }
    }
    

深入源码解析

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);
}

在这里插入图片描述

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

注释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.3 添加spring.factories文件

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

在这里插入图片描述

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

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);
        }
    }
}

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:有不对的地方欢迎指出,转载注明出处,谢谢

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐