1. 简介
本文将从源码的角度解析配置文件的加载流程,请带着以下几个问题去阅读:
- 命令行,虚拟机,配置文件配置的spring.profiles.active原理是什么?
- 配置文件的目录和配置文件的名字是谁规定的?
- 不同配置文件的优先级是怎么实现的?
- 多个配置文件加载顺序是什么?
2. spring.profiles.active
本文的重点是配置文件的加载顺序,但这和spring.profiles.active是分不开的,所以先对spring.profiles.active作简要的介绍。
2.1 设置方法
1、在配置文件中指定 spring.profiles.active=dev
2、命令行:
java -jar MySpringApp***.jar --spring.profiles.active=dev;
3、虚拟机参数;
-Dspring.profiles.active=dev
上述三种方法的优先级: 命令行>虚拟机参数>配置文件指定。
2.2 原码
从下面的代码大致了解一下它们的加载顺序及原理:
// SpringApplication
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 虚拟机参数
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 命令行
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
// 配置文件
listeners.environmentPrepared(environment);
// ......
return environment;
}
- 虚拟机参数:由jvm解析,直接写到SystemProperties里面,在创建environment的时候,所有的SystemProperties都会写到environment里面。
- 命令行:由environment负责解析,覆盖同名的虚拟机参数。
- 配置文件:不管1和2中设置了怎样的spring.profiles.active,springboot总是先去解析application.yml(或者其他后缀)。然后从application.yml中得到spring.profiles.active,如果1和2中没有设置 spring.profiles.active,那么配置文件中spring.profiles.active就起作用,去解析对应的application-xxx.yml。
接下去具体分析配置文件加载顺序
3. 加载配置文件
加载配置文件的时间是在SpringApplication的environment配置完毕之后,listeners发布ApplicationEnvironmentPreparedEvent事件,被ConfigFileApplicationListener接收,然后就到了下面的方法onApplicationEnvironmentPreparedEvent。
// ConfigFileApplicationListener
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
// 加入一个RandomValuePropertySource,用于获得配置文件中的随机值${random.int}
RandomValuePropertySource.addToEnvironment(environment);
// 加载配置文件
// Loader是一个ConfigFileApplicationListener的内部类
new Loader(environment, resourceLoader).load();
}
接下去看Loader(environment, resourceLoader).load()。
// ConfigFileApplicationListener#Loader
void load() {
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
(defaultProperties) -> {
// 待解析的profiles
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
// 标记当前是否设置了spring.profiles.active
this.activatedProfiles = false;
// 储存配置文件解析结果 profile->MutablePropertySources
// 每个profile对应多个PropertySource(比如class:/application.yaml,file:/...)
// 多个PropertySource存成一个list,保存到MutablePropertySources里面。
this.loaded = new LinkedHashMap<>();
// <1> 往this.profiles里面加一个null,然后再从environment里面拿profiles
initializeProfiles();
// <2> 根据profiles的顺序,依次解析application-xxx
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// <3> 把各个配置文件解析的结果加到environment里面。
addLoadedPropertySources();
applyActiveProfiles(defaultProperties);
});
}
3.1 整体流程、
<1>,<2>,<3> 对应代码注释中的<1>,<2>,<3>
<1> initializeProfiles
- 往this.profiles里面加一个null,这个null非常重要!!!这就是刚开始会解析application.yml(或者其他后缀.xxx)的原因!!
- 从environment里面拿profiles,也就是命令行或者虚拟机配的spring.profiles.active,加到this.profiles里面。
如果命令行配置了spring.profiles.active=dev,那么最后this.profiles列表就是[null, dev]。如果命令行和虚拟机都没配,那就是[null,default]。
<2> 加载配置文件
- 根据this.profiles,配置目录,配置文件后缀开始循环解析application-xxx.xxx,null就是解析application.xxx,如果没有找到对应的配置文件就跳过。
- 把一个配置文件加载成Document对象,如果这个对象符合当前profile(用DocumentFilter检验),就继续3,4步骤,否则回到1。
- 如果当前没有设置过spring.profiles.active,也就是this.activatedProfiles=false,那就把配置文件中解析到的spring.profiles.active加到this.profiles里面,并且把this.activatedProfiles置为true。如果spring.profiles.active有多个值,比如dev,prod,那就按顺序把dev,prod依次加到this.profiles里面。
- 把Document对象转换成PropertySource,存到this.loaded里面,一个profile对应一个PropertySource的列表。
<3>把配置信息写到environment里面
- 把this.loaded进行reverse,就是倒序。
- 把倒序后的this.loaded,依次写到environment里面,这样就实现了同名配置信息dev的比默认的优先级高。
3.2 源码
简单分析下<2>和<3>的源码
<2> 加载配置文件
主要解决三个问题:
- this.profiles中的 null为什么对应默认的配置文件?
- 配置文件的目录和配置文件的名字是谁规定的?
- profile(dev)怎么和application-dev.yaml联系起来?
// ConfigFileApplicationListener#Loader
// Note the order is from least to most specific (last one wins)
private static final String DEFAULT_SEARCH_LOCATIONS =
"classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";
private static final String DEFAULT_NAMES = "application";
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
// getSearchLocations()获得配置文件的目录,默认就是DEFAULT_SEARCH_LOCATIONS,优先级是从后往前递减。--spring.config.location可以在命令行和虚拟机手动设置,取代默认位置
getSearchLocations().forEach((location) -> {
boolean isDirectory = location.endsWith("/");
// getSearchNames()获得配置文件的文件名,默认就是application
Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
上面代码的注释就解释了配置文件的目录和配置文件的名字是哪里规定的。
代码整体的逻辑就是遍历各个规定的目录,然后去加载配置文件,接下去就分析加载。
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
// ...
Set<String> processed = new HashSet<>();
// 获得加载器,就是两个:一个处理yaml(yml),一个处理propertites(xml)
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
// 真正的加载
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
开始看loadForFileExtension的代码之前,稍微解释一下loadForFileExtension各个入参的意思:
- loader:yaml或propertites加载器
- prefix:配置文件目录+文件名(比如classpath:/application)
- fileExtension:(比如.yaml,.yml)
- profile:比如(null,dev,prod)
- DocumentFilterFactory,用来制造DocumentFilter,DocumentFilter是判断当前配置文件符不符合要求,具体后面说。
- DocumentConsumer,用来把加载好的配置文件存到this.loaded里面。里面有很多lamda,就不展开了太绕了。
DocumentFilterFactory
这个也是一个lamda,执行这个方法就能获得DocumentFilter了,看一下里面的逻辑。
private DocumentFilter getPositiveProfileFilter(Profile profile) {
return (Document document) -> {
if (profile == null) {
// 如果profile是null,那么document.getProfiles()也要为null。
return ObjectUtils.isEmpty(document.getProfiles());
}
// 如果profile不为null,那么document.getProfiles()要包含profile,并且当前环境要支持document.getProfiles()。
return ObjectUtils.containsElement(document.getProfiles(), profile.getName())
&& this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
};
}
document.getProfiles()就是spring.profiles=dev这样的配置,我能想到的场景是在同一个配置文件里面配不同的环境,一个例子。感觉不是很常用。
所以总结一下上面这个DocumentFilter的逻辑:在不设置spring.profiles的情况下,只有传入的profile是null,才返回true。设置spring.profiles的情况下,要和传入的profile匹配。
继续回到加载的逻辑,这里就是实现加载的地方了。
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
// 根据DocumentFilterFactory的解释,理解一下为什么要用两个DocumentFilter。
// 一般来说只有null这个会起作用
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
// 这里就是拼接appcliton-xxx.yaml的地方!!!
String profileSpecificFile = prefix + "-" + profile + fileExtension;
// 执行两遍加载,用两个DocumentFilter过滤
// 两次加载一起起作用的场景: 同一个yaml配置文件用---分隔
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// 主要是为了spring.profiles=dev这种配置。
// 比如默认配置文件定义了spring.profiles.active=dev, 还用---定义了spring.profiles=dev,
// 这样在默认配置文件中就有两个profile了,profile为null时只会加载默认的。dev的这个环境要等到profile为dev时才能加载,
// 这时就需要执行下面的逻辑,回过头去重新加载默认的配置文件,加载里面的dev环境。
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
// 尝试加载默认的application.yaml,在不配spring.profiles的情况下,只有profile为null,
// 才能加载成功。这就是为什么刚开始都会加载application.yaml的原因。
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
不是很好理解的地方写了比较多的注释。
<3> 把配置信息写到environment里面
这里最关键的就是倒序的操作。
loaded里面是一个个MutablePropertySources,对应每一个profile。每个MutablePropertySources里面存了一个List<PropertySource<?>>,倒序操作改变的是List< MutablePropertySources >的顺序,不会改变里面的List<PropertySource<? > > 的顺序。
private void addLoadedPropertySources() {
MutablePropertySources destination = this.environment.getPropertySources();
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
// 倒序
Collections.reverse(loaded);
String lastAdded = null;
Set<String> added = new HashSet<>();
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (added.add(source.getName())) {
// 依次加到environment里面。
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}
这个需要结合从environment里面获取属性的流程来理解:environment里面有一个PropertySource列表,从environment里面获取属性时,从前到后遍历这个列表,如果当前source里面存了需要的属性,就不会再往后遍历了。
总结
按刚开始的四个问题进行总结:
- spring.profiles.active原理在2中进行了解释
- 配置文件的目录和配置文件的名字定义的地方在3.2 源码 的 <2> 加载配置文件进行了解释
- 配置文件的优先级和加载顺序其实是一个问题。
注意:加载顺序和优先级只在纵向上不同!!
加载顺序
加载顺序,先按profile,再按目录,最后按后缀排序。
- profile顺序:null,spring.profiles.active
- 目录顺序:classpath:/, classpath:/config/, file:./, file:./config/*/, file:./config/ 从后往前
- 后缀顺序:propertites,xml,yml,yaml
每个profile的所有配置文件在加载时按目录顺序和后缀顺序存成一个list。n个profile就有n个list。
每个profile的list按profile的顺序组成一个LIST,LIST里面的成员就是n个list。
优先级
最后倒序时,只有LIST会进行倒序!!
也就是说只有profile的优先级是先加载的优先级低。
每个profile内的各个配置文件都是先加载的优先级高。