Spring Boot会按照一定的优先级读取配置文件,优先级见官网,注册到系统环境中的时间是不一样的,也就是说配置生效范围不同,例如在application.yml
中设置config是不会生效的:
spring:
config:
location: 'file:./my-app-config/application.properties'
也就是说,加载资源逻辑先被执行了,然后配置才被读入到Spring中,自然配置参数是无效的。怎么让配置参数在适合的时机读取到Spring中,怎么保证执行的先后顺序,这就需要对Spring Boot配置文件的读取流程有一定的了解。
为了探究加载的顺序,可以写一个Hello World的Spring Boot程序,配置文件中将日志等级调到最低:
spring:
main:
web-application-type: none
logging:
path: '../springbootconfig/'
level:
org.springframework.boot:
trace
启动程序后查看输出的日志,截取了前几行,可以发现配置文件的加载和ConfigFileApplicationListener
相关:
2019-05-26 14:19:50.623 DEBUG 13852 --- [restartedMain] .c.l.ClasspathLoggingApplicationListener : Application started with classpath: [file:/G:/spring%20boot%20configuration/out/production/classes/, file:/G:/spring%20boot%20configuration/out/production/resources/]
2019-05-26 14:19:50.729 INFO 13852 --- [restartedMain] demo.App : Starting App on DESKTOP-HTJRA28 with PID 13852 (started by 66439 in G:\spring boot configuration)
2019-05-26 14:19:50.729 INFO 13852 --- [restartedMain] demo.App : The following profiles are active: prod
2019-05-26 14:19:50.729 DEBUG 13852 --- [restartedMain] o.s.boot.SpringApplication : Loading source class demo.App
2019-05-26 14:19:50.811 TRACE 13852 --- [restartedMain] o.s.b.c.c.ConfigFileApplicationListener : Skipped missing config 'file:./config/application.properties' (file:./config/application.properties)
2019-05-26 14:19:50.811 TRACE 13852 --- [restartedMain] o.s.b.c.c.ConfigFileApplicationListener : Skipped missing config 'file:./config/application.xml' (file:./config/application.xml)
ConfigFileApplicationListener
截取部分该类上的注释,简单翻译下:
EnvironmentPostProcessor
会从已知的路径加载系统环境的配置参数,默认的,会从以下路径读取application.properties
以及application.yml
:
classpath:
file:./
classpath:config/
file:./config/:
加载路径和配置文件名,可以使用setSearchLocations(String)
和serSearchNames(String)
定义,此外,也能通过配置项中的spring.config.location
和spring.config.name
来定义。
回顾之前说的在application.yml
中设置config不会生效,这里就很容易理解了,以spring.config.location
为例结合源码:当设置了spring.config.location
就返回自定义的搜索路径,否则用默认的搜索路径,所以在默认搜索路径下的配置文件设置spring.config.location
是不会有效果的,应该已经过了获取路径的逻辑了。
private Set<String> getSearchLocations() {
//CONFIG_LOCATION_PROPERTY = "spring.config.location"
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
return getSearchLocations(CONFIG_LOCATION_PROPERTY);
}
//CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location"
Set<String> locations = getSearchLocations(
CONFIG_ADDITIONAL_LOCATION_PROPERTY);
//DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
DEFAULT_SEARCH_LOCATIONS));
return locations;
}
大致了解了ConfigFileApplicationListener
是做什么的,接下来需要知道这个类是什么时候执行相关的方法的。首先从类型知道只是一个listener,类声明是:public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered
,先来看所实现的接口。
EnvironmentPostProcessor
就是Spring上下文的配置切面,同样,简单翻译说明下该类的注释。
当应用的上下文被刷新前,允许自定义应用的
Evironment
。实现EnvironmentPostProcessor
的类必须在META-INF/spring.factories
中以org.springframework.boot.env.EnvironmentPostProcessor
为键,实现类的全限定名为值声明,多个实现类用逗号分隔。官方建议同时实现org.springframework.core.Ordered
接口定义优先级。
这里需要说明下在Spring中,Environment、Profile、Property的关系。
- Environment
和字面意思一样,是一个应用的运行环境,是面向系统层面的,提供统一访问各种配置源的配置的方式。也就是Spring提供的一个对外接口,用来访问在当前环境下系统各个参数。
Environment包含一个或多个Profile。 - Profile
系统中并没有Profile这个类(其实有一个ConfigFileApplicationListener.Profile
的内部类,但也仅仅记录了name[String]和defaultProfile[boolean]两个属性),是用个String在系统中表达的。在现实中可能会有多个不同的运行环境(注意不是Spring的Environment),例如开发环境、测试环境、生成环境等,不同的环境下应该会有不同的配置或者bean,这时就可以用profile来标志不同的环境。需要说明的是,Profile并不局限与用作环境的分隔,你可以将任何概念定义为一个Profile,毕竟在Spring中它只是一个String。
在配置上,一个Profile会有多个Property。 - Property
这个就是我们所说的配置了。
总之,Spring会根据Profile的不同,合并各个Profile下的配置文件(last win),然后注入到Environment中。
SmartApplicationListener
监听系统事件的一个接口,不同于ApplicationListener,额外提供了supportsEventType(eventType)
, supportsSourceType(sourceType)
两个过滤方法,官方建议使用GenericApplicationListener
,GenericApplicationListener
额外还继承了Ordered接口。
Ordered
该接口只有一个方法int getOrder()
,看名字也知道是用来排序的,例如排优先级等,不过排优先级的话建议使用PriorityOrdered
接口。
ConfigFileApplicationListener监听的事件
说完了ConfigFileApplicationListener
所实现的各个接口,首先应该关注的就是该监听器所监听的事件,也就是SmartApplicationListener
的实现:
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);
}
@Override
public boolean supportsSourceType(Class<?> aClass) {
return true;
}
监听器会对两个应用程序事件做处理,先来看这两个事件是什么,具体的Spring启动流程网上有很多讲解,这里就不再赘述:
- ApplicationEnvironmentPreparedEvent
在SpringApplication
启动时,当Environment
第一次准备好做检查、修改时,会发布该事件。 - ApplicationPreparedEvent
在SpringApplication
启动并且ApplicationContext
已经准备完毕但未刷新时发布事件,此时bean已经加载完成,Environment
已经可以使用。
也就是说,会在两个时刻处理配置文件,首先来看在ApplicationEnvironmentPreparedEvent
,主要是获取了切面(具体有哪些切面在EnvironmentPostProcessor切面讨论),然后执行对Environment
的修改:
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
//从META-INF/spring.factories从读取所有EnvironmentPostProcessor的实现
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);//优先级排序
//处理Environment
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
接下来是处理Environment
的方法:
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
//添加配置的到指定的Environment
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
//随机数配置生成器,用于解析配置文件中random.开头的配置值
RandomValuePropertySource.addToEnvironment(environment);
//一个内部类,用于加载配置,主要的逻辑都在这个内部类中
new Loader(environment, resourceLoader).load();
}
Loader
的逻辑在后边Loader会详细分析,先回过头看下对另一个事件ApplicationPreparedEvent
的处理:
private void onApplicationPreparedEvent(ApplicationEvent event) {
this.logger.replayTo(ConfigFileApplicationListener.class);
addPostProcessors(((ApplicationPreparedEvent) event).getApplicationContext());
}
//ApplicationContext添加对PropertySource优先级的处理切面
protected void addPostProcessors(ConfigurableApplicationContext context) {
context.addBeanFactoryPostProcessor(new PropertySourceOrderingPostProcessor(context));
}
在Environment
可以使用后,会优先级做调整。
EnvironmentPostProcessor切面
从前面的分析从可以知道,当Spring发布ApplicationEnvironmentPreparedEvent
事件时,监听器会获取当前类加载器上的所有EnvironmentPostProcessor切面,然后按优先级逐个处理。这里用静态分析的方式已经无法继续阅读源码,以下的分析会在调试模式下进行。
一个Spring上下文只会发布一次ApplicationEnvironmentPreparedEvent
,分析onApplicationEnvironmentPreparedEvent
方法,首先需要知道类加载器上都加载了哪些EnvironmentPostProcessor
的实现:
List<EnvironmentPostProcessor> loadPostProcessors() {
//断点处
return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
}
通过调试可知,ClassLoader是ApplicationClassLoader,也就是默认的系统类加载器,继续调试代码可以发现,Spring将加载记录存在了一个cache里,第一次加载时会从META-INF/spring.factories
读取类的全限定名,这也是为什么要求实现了EnvironmentPostProcessor
接口的类需要在META-INF/spring.factories
注册的原因。
Hello World加载到的类名如下,这些类会被稍后实例化并按Ordered
接口返回的值排序:
EnvironmentPostProcessor子类
排序后返回的List会添加上ConfigFileApplicationListener
,也就是一直在讨论的这个监听器,最后的结果如下,这些都是会影响到Environment的切面,我们来逐个简单分析:
排序后的结果
- SystemEnvironmentPropertySourceEnvironmentPostProcessor
这个切面就是把系统参数--environment.getPropertySources().get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)
的PropertySource
类用OriginAwareSystemEnvironmentPropertySource
进行封装,然后replace。 - SpringApplicationJsonEnvironmentPostProcessor
将当前参数(也就是系统参数)的key为spring.application.json
或者SPRING_APPLICATION_JSON
(存在前者则忽略后者)的字符串解析为json,并将解析到的json包装成JsonPropertySource
,注入到配置中。如果存在JNDI则注入到JNDI前,否则如果存在系统环境变量,则注入到系统环境变量前(本例的情形),否则作为第一个PropertySource。 - CloudFoundryVcapEnvironmentPostProcessor
这个是Spring Cloud相关的,这里并未用到,不展开了。 - ConfigFileApplicationListener
也就是前面讨论的监听器,这里不再重复。后面会讨论Loader的实现。 - DevToolsHomePropertiesPostProcessor
具体在另外一片文章Spring Boot 配置和配置文件中的Devtools全局配置有提到,这里不再赘述 - DevToolsPropertyDefaultsPostProcessor
项目依赖spring-boot-devtools
后,会导入一些spring-boot-devtools
的默认配置,代码很简单自行调试源码即可。
Loader
加载配置文件的逻辑几乎都在Loader
里,所以单独拿出来分析下Loader的逻辑。Loader
是ConfigFileApplicationListener
的一个内部类,首先看构造函数:
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
//资源加载策略,提供从文件路径、类路径等加载资源的实现
this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
//加载配置的策略,提供从资源加载为配置的实现
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader());
}
资源加载器是DefaultResourceLoader
,可以从文件路径、类路径、URL加载资源。
配置加载器默认有两种:
- PropertiesPropertySourceLoader
从.properties\xml加载配置 - YamlPropertySourceLoader
从.yml.yaml加载配置
加载.load()
方法如下,为了方便,标记为load0:
public void load0() {
this.profiles = new LinkedList<>();//所有的profiles
this.processedProfiles = new LinkedList<>();//已经处理好了的profiles
this.activatedProfiles = false;//当前处理的profile是不是active的
this.loaded = new LinkedHashMap<>();//已经加载了的profiles
initializeProfiles();//初始化
//到这一步,profiles会有两个值,null和default
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
//为了方便,标记喂load1
load1(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
//将所有this.loaded的配置加载到Environment中
addLoadedPropertySources();
}
初始化方法:初始化了一些状态、default profile:
private void initializeProfiles() {
// null代表profile为default,并作为最低优先级的profile
this.profiles.add(null);
// 获取spring.profiles.active/spring.profiles.include标识的profile
Set<Profile> activatedViaProperty = getProfilesActivatedViaActiveProfileProperty();
//通过this.environment.getActiveProfiles()添加已经被标注为激活的profile
processOtherActiveProfiles(activatedViaProperty);
//如果activatedViaProperty不为空,则调用this.environment.addActiveProfile(profile)添加到environment中
addActiveProfiles(activatedViaProperty);
if (this.profiles.size() == 1) { // 只有一个默认值null,defaultProfileName为default
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
ConfigFileApplicationListener.Profile defaultProfile = new ConfigFileApplicationListener.Profile(defaultProfileName, true);
this.profiles.add(defaultProfile);//添加default profile
}
}
}
addToLoaded:当配置文件转换喂Document后,进行合并及添加到Environment中
//第一个参数时添加PropertySource的方法,用的是MutablePropertySources::addLast, 第二个参数是false
private DocumentConsumer addToLoaded(
BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
if (checkForExisting) {
for (MutablePropertySources merged : this.loaded.values()) {
if (merged.contains(document.getPropertySource().getName())) {
return;
}
}
}
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
addMethod.accept(merged, document.getPropertySource());
};
}
load使用多态,有不同的实现,为了方便,用序号标记。
load1:遍历所有配置文件的文件夹路径,读取配置。
private void load1(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
//getSearchLocations()一开始就分析过,就是从自定义的路径里读取,没有自定义的就从默认路径读取,会用SpEL进行解析
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
//非文件夹就是一个空集合
//getSearchNames()是配置文件名(不包含后缀),如果spring.config.name有定义则取其值,否则默认值为application,会用SpEL进行解析
Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
names.forEach(
(name) -> load(location, name, profile, filterFactory, consumer));
});
}
load2:解析某个文件夹下,指定文件名(不包含文件类型)的配置文件。
/**
* location: 文件夹路径
* name:文件名
* profile:所属的profile
*/
private void load2(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {//解析.properties\.yml这样的隐藏文件
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
}
}
}
//propertySourceLoaders是properties\yml配置解析器
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
String prefix = location + name;
fileExtension = "." + fileExtension;
loadForFileExtension(loader, prefix, fileExtension, profile, filterFactory, consumer);
}
loadForFileExtension:加载指定配置文件
/**
* loader:配置解析器
* prefix:文件夹+文件名
* fileExtension:文件类型
*/
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
//DocumentFilter就是判断文件是否符合profile
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
// Try profile-specific file & profile section in profile file (gh-340)
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// Try profile specific sections in files we've already processed
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile
+ fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
load3(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
load3:加载指定配置文件
//location配置文件完整的路径,包含文件类型
private void load3(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) {
try {
Resource resource = this.resourceLoader.getResource(location);
String description = getDescription(location, resource);//打印日志用的
if (profile != null) {
description = description + " for profile " + profile;
}
if (resource == null || !resource.exists()) {//资源不存在
this.logger.trace("Skipped missing config " + description);
return;
}
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {//类型为空
this.logger.trace("Skipped empty config extension " + description);
return;
}
String name = "applicationConfig: [" + location + "]";
//使用加载器加载,并转换为内部的结构类型
/*这里实现比较复杂,有兴趣可以自己去调源码,在加载了配置文件后,
*会去找是否存在spring.profiles\spring.profiles.include的配置,
*如果有则会把profile加入到Document的activeProfiles\includeProfiles中
*/
List<Document> documents = loadDocuments(loader, name, resource);
if (CollectionUtils.isEmpty(documents)) {
this.logger.trace("Skipped unloaded config " + description);
return;
}
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
//如果存在activeProfiles\includeProfiles,则this.profiles中,后续继续处理
addActiveProfiles(document.getActiveProfiles());
addProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
//添加到Environment中
loaded.forEach((document) -> consumer.accept(profile, document));
this.logger.debug("Loaded config file " + description);
}
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load property "
+ "source from location '" + location + "'", ex);
}
}
作者:看不见的BUG
链接:https://www.jianshu.com/p/f31af43c29c6
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。