相信很多人面试都被问到过spring boot配置的加载的顺序是怎么样的,他是怎么一个实现方式,我也被问到过,接下来这篇文章将详细解析一下spring boot配置的加载流程到底是怎么实现的。
在开始前我准备几个问题
新建spring boot项目
添加配置文件(application.properties)
#application.properties
name=application-properties
添加controller
@RestController
@RequestMapping
public class TestController {
@Value("${name}")
private String name;
@RequestMapping("/getValue")
public String getValue(){
return "name:"+name;
}
}
我们从springboot的入口方法(run)一直点进去可以看到一个164行有个叫this.prepareEnvironment
的方法,这里是环境配置准备的入口
我们在这个方法的下方debugger住,我们可以看到ConfigurableEnvironment已经加载好了,在框住的红色区域内正是我们配置文件的name和对应的属性
在这里我们记住几个重要的关键信息
很明显,我们的配置是加载在propertySourcesList里面的
我们继续进入prepareEnvironment
方法,可以看到一个叫listeners.environmentPrepared
的方法。
顾名思义,好像是使用监听器加载环境配置?
我们再进入到environmentPrepared
去看看
可以发现,监听器们会在此处调用执行方法,doWithListeners
会在此处执行传入进来的监听器,spring.boot.application.environment-prepared
表示环境准备的监听器
我们继续往下看看,进入listener.environmentPrepared
方法
这里建议大家看着源码一步一步跟着追踪,不然可能会比较难懂
SpringApplicationRunListener.environmentPrepared
SpringApplicationRunListener
是个接口,往下看到他的实现方法EventPublishingRunListener
initialMulticaster.multicastEvent
invokeListener
和doInvokeListener
,这里就是执行监听器里面的监听事件doInvokeListener
中可以看到listener.onApplicationEvent(event)
ApplicationListener
是个接口,有个叫onApplicationEvent
的方法,我们找到他的实现类EnvironmentPostProcessorApplicationListener
,这个是环境处理监听器,里面有个this.onApplicationEnvironmentPreparedEvent
的方法while(var4.hasNext()) {
//获取环境处理器
EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var4.next();
//执行所有实现了环境处理器的接口
postProcessor.postProcessEnvironment(environment, application);
}
我们可以看到,在EnvironmentPostProcessorApplicationListener
中循环调用了EnvironmentPostProcessor
的postProcessEnvironment
方法
而spring boot在spring.factories
提供了一个提供了一个spi,把所有EnvironmentPostProcessor的实现类通过spring.factories
的方式加载到spring boot中,如下图所示
ps:spring.factories是一个spring boot的重点知识,这里不会重点展开
这里我们留下一个问题:既然这里提供了一个spi机制,我们是不是也可以通过这个机制实现一个Environment处理器呢
我们继续看到 postProcessor.postProcessEnvironment(environment, application)
方法,找到其中一个EnvironmentPostProcessor
的实现类,叫做ConfigDataEnvironmentPostProcessor
,找到他的实现方法postProcessEnvironment
这里有两个重点,一个是getConfigDataEnvironment
,另一个是processAndApply
getConfigDataEnvironment
用来加载解析起,比如properties或者yml,yaml那些processAndApply
用来解析数据并且把数据设置进去environment里面我们先来看getConfigDataEnvironment
方法,点进去看本质是new了一个ConfigDataEnvironment
,我们从他的构造方法可以看到有个叫做this.createConfigDataLocationResolvers
方法,解析器就是在这加载的。
可能到这里还是有点蒙,这个解析器到底是个啥,我们再点进去看看。作为一个码农,一定要刨根到底~
点进去发现还是一样,new了一个ConfigDataLocationResolvers
这里有又有两个重点
SpringFactoriesLoader.loadFactoryNames(ConfigDataLocationResolver.class, resourceLoader.getClassLoader())
通过spring.factories
获取两个解析器,重点是StandardConfigDataLocationResolver
为标准化解析器
instantiator.instantiate(resourceLoader.getClassLoader(), names)
实例化两个解析器,ConfigTreeConfigDataLocationResolver
这里细讲,我们来看StandardConfigDataLocationResolver
这里又细分3个小点
DEFAULT_CONFIG_NAMES
我们可以看到。他默认是找到application
,所以为什么我们的配置文件都以application命名的原因
再次通过spring.factories
获取数据源加载器PropertiesPropertySourceLoader
和YamlPropertySourceLoader
看名字就知道,这两个加载分别加载了properties和ymal的文件
PropertiesPropertySourceLoader
是用来解析properties和xml的
YamlPropertySourceLoader
是用来解析yml和yaml的
所以在最后会和解析器中的application拼接成application.properties或者application.yml等
最终configNames
确定是application
还是其他名称,没有外部定义的话默认为application
,所以为什么很多人问在spring boot中配置文件以bootstrap命名会失效,因为在寻常spring boot项目中解析器压根就没有定义bootstrap的文件名,所以无法解析。而在spring cloud中会有一个监听器往binder里加了个bootstrap变量,binder.bind("spring.config.name", String[].class)
。
processAndApply
字面意思:处理和应用
首先我们找到里面的processInitial
中的contributors.withProcessedImports
里面
这里有个while循环,会在红框的resolveAndLoad
这里循环两次分别解析yml和properties,就是用到上面3.1所说的PropertiesPropertySourceLoader
和YamlPropertySourceLoader
进去resolveAndLoad
可以看到他调用了两个方法一个是resolve
和load
application.properties
或者application.yml
等propertySourcesList
里。相当于先加载到内存,最终把所有流程处理完才加载到propertySourcesList
里继续回看到processAndApply方法,最下面有个applyToEnvironment
,我们点进去看看
可以看到,前面check方法都是用来检查配置的,我们主要看applyContributor
方法
后面的方法根据关键字profiles
,activeProfiles
不难看出,是用来加载不同环境的配置,例如dev,test,prod等
可以看到配置文件最终是在这里添加到propertySourcesList
里面的
这里浅谈一下,毕竟这章重点在上面已经讲完了
在run方法的refreshContext
里,有个refresh
就是在这加载的,如下图所示
然后通过反射生成一个AutowiredAnnotationBeanPostProcessor
实例
我们可以看到他的构造方法里面添加了Autowired
和Value
两个注解
最后在AutowiredMethodElement
内部类里将值自动装配进去
我们在上面的2.1说到,我们是不是也可以通过这个机制实现一个Environment处理器?当然是可以的
#test.properties
name=test-propertis
注释application.properties里面的配置
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);
}
}
}
新建META-INF
文件夹,并添加spring.factories文件
org.springframework.boot.env.EnvironmentPostProcessor=\
com.nssnail.main.config.TestEnvironmentPostProcessor
访问http://localhost:8080/getValue接口(代码在上面准备工作目录下)
结果显示访问成功,并没有报错
上面的配置只适用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);
}
}
}
我们可以看到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的顺序有关的
既然我们可以通过实现EnvironmentPostProcessor
接口来扩展自定义配置文件,他们我们是不是可以通过实现PropertySourceLoader
或者ConfigDataLocationResolver
来扩展自定义配置解析器呢?这里大家可以自己动手试下
ConfigurableEnvironment
的propertySourcesList
中的无法加载bootstrap配置
的,而在spring cloud项目中一般有配置外部的bootstrap变量,所以可以正常加载AutowiredAnnotationBeanPostProcessor
中实现propertySourcesList的顺序
有关ps:有不对的地方欢迎指出,转载注明出处,谢谢