1. 概述
在使用 Spring Boot 时,我们可以很方便的在 application.properties
或 application.yml
配置文件中,添加相应的应用所需的配置。那么,究竟 Spring Boot 是如何实现该功能的呢,今儿我们就通过 Spring Boot 的源码,一探究竟!
艿艿的高能提示:这篇会非常长,建议胖友保持耐心。另外,最好边调试边看~
2. Profiles
在讲配置加载之前,不得不先提下 Spring Profiles 功能。
- 如果不熟悉的胖友,先看看 《详解 Spring 中的 Profile》 文章。
- 关于这一块,之前在 《【死磕 Spring】—— 环境 & 属性:PropertySource、Environment、Profile》 中,已经有详细的源码解析。
😈 Spring Boot 在 Spring Framework 的基础之上,可以手动附加新的 Profile 。在 《精尽 Spring Boot 源码分析 —— SpringApplication》 的 #
。代码如下:
// SpringApplication.java |
additionalProfiles
属性,可以设置创建的 SpringApplication 可以附加的additionalProfiles
属性。<X>
处,在原有 Spring Framework 中设置的 String Profiles 的基础上,又附加上了SpringApplication.additionalProfiles
配置的。
艿艿:这个小节,讲的有点绕,胖友辛苦理解下~
3. ConfigFileApplicationListener
Spring Boot 实现 application.properties
或 application.yml
配置文件的加载,关键在于 ConfigFileApplicationListener 类。在 《精尽 Spring Boot 源码分析 —— ApplicationListener》 中,我们已经简单介绍过它:
org.springframework.boot.context.config.ConfigFileApplicationListener
,实现 SmartApplicationListener、Ordered、EnvironmentPostProcessor 接口,实现 Spring Boot 配置文件的加载。
- 注意哟,ConfigFileApplicationListener 即是一个 SmartApplicationListener 实现类,又是一个 EnvironmentPostProcessor 实现类。
3.1 onApplicationEvent
实现 #onApplicationEvent(ApplicationEvent event)
方法,分别对 ApplicationEnvironmentPreparedEvent、ApplicationPreparedEvent 事件进行处理。代码如下:
// ConfigFileApplicationListener.java |
<1>
处,如果是 ApplicationEnvironmentPreparedEvent 事件,说明 Spring 环境准备好了,则调用#onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent)
方法,执行相应的处理。详细解析,见 「3.2 onApplicationEnvironmentPreparedEvent」 。<2>
处,如果是 ApplicationPreparedEvent 事件,说明 Spring 容器初始化好了,则调用#onApplicationPreparedEvent(ApplicationPreparedEvent)
方法,进行相应的处理。详细解析,见 「3.3 onApplicationPreparedEvent」 。
3.2 onApplicationEnvironmentPreparedEvent
#onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent)
方法,处理 ApplicationEnvironmentPreparedEvent 事件。代码如下:
// ConfigFileApplicationListener.java |
<1.1>
处,调用#loadPostProcessors()
方法,加载指定类型 EnvironmentPostProcessor 对应的,在META-INF/spring.factories
里的类名的数组。代码如下:// ConfigFileApplicationListener.java
List<EnvironmentPostProcessor> loadPostProcessors() {
return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
}- 默认情况下,返回的是 SystemEnvironmentPropertySourceEnvironmentPostProcessor、SpringApplicationJsonEnvironmentPostProcessor、CloudFoundryVcapEnvironmentPostProcessor 类。
<1.2>
处,加入自己,到postProcessors
数组中。因为自己也是一个 EnvironmentPostProcessor 实现类。<2>
处,排序postProcessors
数组。<3>
处,遍历postProcessors
数组,调用EnvironmentPostProcessor#postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application)
方法,逐个执行。
那么,我们开始来逐个看看每个 EnvironmentPostProcessor 实现类。考虑到 EnvironmentPostProcessor 应有的独立性(尊严!),我们单独开了 「4. EnvironmentPostProcessor」 小节,所以胖友先一起跳过来看看。
艿艿:这块涉及的逻辑非常多。本文的 4、5、6、7 小节,都和 「3.2 onApplicationEnvironmentPreparedEvent」 有关。
3.3 onApplicationPreparedEvent
#onApplicationPreparedEvent(ApplicationEvent event)
方法,处理 ApplicationPreparedEvent 事件。代码如下:
// ConfigFileApplicationListener.java |
- 添加 PropertySourceOrderingPostProcessor 处理器。关于 PropertySourceOrderingPostProcessor 类,见 「3.3.1 PropertySourceOrderingPostProcessor」 中。
3.3.1 PropertySourceOrderingPostProcessor
PropertySourceOrderingPostProcessor ,是 ConfigFileApplicationListener 内部类,实现 BeanFactoryPostProcessor、Ordered 接口,将 DEFAULT_PROPERTIES
的 PropertySource 属性源,添加到 environment
的尾部。代码如下:
// ConfigFileApplicationListener.java |
- 那么
DEFAULT_PROPERTIES
对应的 PropertySource 属性源,究竟是哪里来的呢?答案见SpringApplication.defaultProperties
相关,代码如下:
// SpringApplication.java |
4. EnvironmentPostProcessor
org.springframework.boot.context.config.EnvironmentPostProcessor
接口,在 Environment 加载完成之后,如果我们需要对其进行一些配置、增加一些自己的处理逻辑,那么请使用 EnvironmentPostProcessor 。代码如下:
// EnvironmentPostProcessor.java |
从实现上和作用上来说,和 《【死磕 Spring】—— IoC 之深入分析 BeanPostProcessor》 都是非常类似的。
EnvironmentPostProcessor 的实现类,如下图所示:EnvironmentPostProcessor 实现类
4.1 CloudFoundryVcapEnvironmentPostProcessor
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor
,实现 EnvironmentPostProcessor、Ordered 接口,实现对 Cloud Foundry 的支持。因为我们不使用 Cloud Foundry ,所以可以跳过对 CloudFoundryVcapEnvironmentPostProcessor 源码的了解。感兴趣的胖友,可以结合 《Spring Boot 参考指南(部署到云)》 文章,对源码进行研究。
4.2 SystemEnvironmentPropertySourceEnvironmentPostProcessor
艿艿:选看~
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor
,实现 EnvironmentPostProcessor、Ordered 接口,实现将 environment
中的 systemEnvironment
对应的 PropertySource 属性源对象,替换成 OriginAwareSystemEnvironmentPropertySource 对象。代码如下:
// SystemEnvironmentPropertySourceEnvironmentPostProcessor.java |
<1>
处,获得systemEnvironment
对应的 PropertySource 属性源。<2>
处,调用#replacePropertySource(...)
方法,将原始的 PropertySource 对象,替换成 OriginAwareSystemEnvironmentPropertySource 对象。其中,OriginAwareSystemEnvironmentPropertySource 是 SystemEnvironmentPropertySourceEnvironmentPostProcessor 内部类,继承 SystemEnvironmentPropertySource 类,实现 OriginLookup 接口,代码如下:
// SystemEnvironmentPropertySourceEnvironmentPostProcessor#OriginAwareSystemEnvironmentPropertySource.java
protected static class OriginAwareSystemEnvironmentPropertySource
extends SystemEnvironmentPropertySource implements OriginLookup<String> {
OriginAwareSystemEnvironmentPropertySource(String name, Map<String, Object> source) {
super(name, source);
}
public Origin getOrigin(String key) {
// 解析 key 对应的 property
String property = resolvePropertyName(key);
// 判断是否存在 property 对应的值。如果存在,则返回 SystemEnvironmentOrigin 对象
if (super.containsProperty(property)) {
return new SystemEnvironmentOrigin(property);
}
// 不存在,则返回 null
return null;
}
}- 重心是对
org.springframework.boot.origin.OriginLookup
接口的#getOrigin(String key)
方法的实现,实现查找key
对应的真正的property
。难道两者还会不同,答案是的。例如说:传入的key=foo.bar.baz
,返回的是property=FOO_BAR_BAZ
。 答案见
SystemEnvironmentPropertySource#checkPropertyName(String name)
方法,内部会进行各种灵活的替换“查找”。代码如下:// SystemEnvironmentPropertySource.java
private String checkPropertyName(String name) {
// Check name as-is
if (containsKey(name)) {
return name;
}
// Check name with just dots replaced
String noDotName = name.replace('.', '_');
if (!name.equals(noDotName) && containsKey(noDotName)) {
return noDotName;
}
// Check name with just hyphens replaced
String noHyphenName = name.replace('-', '_');
if (!name.equals(noHyphenName) && containsKey(noHyphenName)) {
return noHyphenName;
}
// Check name with dots and hyphens replaced
String noDotNoHyphenName = noDotName.replace('-', '_');
if (!noDotName.equals(noDotNoHyphenName) && containsKey(noDotNoHyphenName)) {
return noDotNoHyphenName;
}
// Give up
return null;
}- 各种符号,转成
_
来查找对应的属性。
- 各种符号,转成
- 重心是对
😈 那么具体有什么样的逻辑上的需要呢?暂时还没怎么看到,嘿嘿。所以,知道就好,暂时先不去深究。
4.3 SpringApplicationJsonEnvironmentPostProcessor
艿艿:选看~
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor
,实现 EnvironmentPostProcessor、Ordered 接口,解析 environment
中的 spring.application.json
或 SPRING_APPLICATION_JSON
对应的 JSON 格式的属性值,创建新的 PropertySource 对象,添加到其中。代码如下:
// SpringApplicationJsonEnvironmentPostProcessor.java |
map(JsonPropertyValue::get).filter(Objects::nonNull)
代码段,调用JsonPropertyValue#get(PropertySource<?> propertySource)
方法,environment
中的spring.application.json
或SPRING_APPLICATION_JSON
对应的 JSON 格式的属性值。代码如下:// SpringApplicationJsonEnvironmentPostProcessor.java
public static final String SPRING_APPLICATION_JSON_PROPERTY = "spring.application.json";
public static final String SPRING_APPLICATION_JSON_ENVIRONMENT_VARIABLE = "SPRING_APPLICATION_JSON";
private static class JsonPropertyValue {
private static final String[] CANDIDATES = { SPRING_APPLICATION_JSON_PROPERTY, SPRING_APPLICATION_JSON_ENVIRONMENT_VARIABLE };
private final PropertySource<?> propertySource;
private final String propertyName;
private final String json;
JsonPropertyValue(PropertySource<?> propertySource, String propertyName, String json) {
this.propertySource = propertySource;
this.propertyName = propertyName;
this.json = json;
}
public String getJson() {
return this.json;
}
public Origin getOrigin() {
return PropertySourceOrigin.get(this.propertySource, this.propertyName);
}
public static JsonPropertyValue get(PropertySource<?> propertySource) {
// 遍历 CANDIDATES 数组
for (String candidate : CANDIDATES) {
// 获得 candidate 对应的属性值
Object value = propertySource.getProperty(candidate);
if (value instanceof String
&& StringUtils.hasLength((String) value)) {
// 创建 JsonPropertyValue 对象,然后返回
return new JsonPropertyValue(propertySource, candidate, (String) value);
}
}
return null;
}
}- 比较简单,胖友看下就明白列。
调用
#processJson(ConfigurableEnvironment environment, JsonPropertyValue propertyValue)
方法,执行处理 JSON 字符串。详细解析,见 「4.3.1 processJson」 。
4.3.1 processJson
#processJson(ConfigurableEnvironment environment, JsonPropertyValue propertyValue)
方法,执行处理 JSON 字符串。代码如下:
// SpringApplicationJsonEnvironmentPostProcessor.java |
<1>
处,解析 json 字符串,成 Map 对象。<2>
处,创建 JsonPropertySource 对象,添加到environment
中。其中,JsonPropertySource 继承 MapPropertySource 类,实现 OriginLookup 接口,代码如下:// SpringApplicationJsonEnvironmentPostProcessor#JsonPropertySource.java
private static class JsonPropertySource extends MapPropertySource
implements OriginLookup<String> {
private final JsonPropertyValue propertyValue;
JsonPropertySource(JsonPropertyValue propertyValue, Map<String, Object> source) {
super(SPRING_APPLICATION_JSON_PROPERTY, source);
this.propertyValue = propertyValue;
}
public Origin getOrigin(String key) {
return this.propertyValue.getOrigin();
}
}- 使用的
name
为SPRING_APPLICATION_JSON_PROPERTY=spring.application.json
。
- 使用的
<2.1>
处,调用#flatten(String prefix, Map<String, Object> result, Map<String, Object> map)
方法,将 JSON 解析后的 Map 可能存在的内嵌的 Map 对象,转换成多条 KV 格式的配置对。代码如下:// SpringApplicationJsonEnvironmentPostProcessor.java
private Map<String, Object> flatten(Map<String, Object> map) {
Map<String, Object> result = new LinkedHashMap<>();
flatten(null, result, map);
return result;
}
private void flatten(String prefix, Map<String, Object> result, Map<String, Object> map) {
String namePrefix = (prefix != null) ? prefix + "." : "";
map.forEach((key, value) -> extract(namePrefix + key, result, value));
}
"unchecked") (
private void extract(String name, Map<String, Object> result, Object value) {
if (value instanceof Map) { // 内嵌的 Map 格式
flatten(name, result, (Map<String, Object>) value);
} else if (value instanceof Collection) { // 内嵌的 Collection
int index = 0;
for (Object object : (Collection<Object>) value) {
extract(name + "[" + index + "]", result, object);
index++;
}
} else { // 普通格式,添加到 result 中
result.put(name, value);
}
}<2.2>
处,调用#addJsonPropertySource(ConfigurableEnvironment environment, PropertySource<?> source)
方法,添加到environment
中。代码如下:// SpringApplicationJsonEnvironmentPostProcessor.java
private void addJsonPropertySource(ConfigurableEnvironment environment, PropertySource<?> source) {
MutablePropertySources sources = environment.getPropertySources();
// 获得需要添加到 source 所在的 PropertySource 之前的名字
String name = findPropertySource(sources);
// 添加到 environment 的 sources 中。
// 这么做的效果是,source 高于 SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME && JNDI_PROPERTY_SOURCE_NAME 之前
if (sources.contains(name)) {
sources.addBefore(name, source);
} else {
sources.addFirst(source);
}
}
private String findPropertySource(MutablePropertySources sources) {
// 在 Servlet 环境下,且有 JNDI_PROPERTY_SOURCE_NAME 属性,则返回 StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME
if (ClassUtils.isPresent(SERVLET_ENVIRONMENT_CLASS, null)
&& sources.contains(StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME)) {
return StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME;
}
// 否则,返回 SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME 属性
return StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME;
}
😈 当然,因为绝大多数情况下,我们并不会去使用 spring.application.json
或 SPRING_APPLICATION_JSON
去进行配置。所以呢,这块逻辑等胖友真的有需要,再来瞅瞅落。
4.4 ConfigFileApplicationListener
艿艿:真正的重头戏~
实现 #addPropertySources(ConfigurableEnvironment environment,ResourceLoader resourceLoader)
方法,代码如下:
// ConfigFileApplicationListener.java |
<1>
处,添加 RandomValuePropertySource 到environment
中。详细解析,见 「5. RandomValuePropertySource」 中。<2>
处,创建 Loader 对象,并调用Loader#load()
方法,进行加载。详细解析,见 「4.4.1 Loader」 中。
4.4.1 Loader
艿艿:高能预警,关于这一块,内容会比较扎实(很多)!
Loader 是 ConfigFileApplicationListener 的内部类,负责加载指定的配置文件。构造方法如下:
// ConfigFileApplicationListener.java |
<1>
处,创建 PropertySourcesPlaceholdersResolver 对象。详细解析,胖友可以跳到 「6. PropertySourcesPlaceholdersResolver」 中,瞅一眼,然后继续回到此处。<2>
处,创建 DefaultResourceLoader 对象。这个是 Spring Framework 中的类,用于资源的加载。当然,这不是本文的重点,暂时先忽略。<3>
处,加载指定类型 PropertySourceLoader 对应的,在META-INF/spring.factories
里的类名的数组。- 默认情况下,返回的是 PropertiesPropertySourceLoader、YamlPropertySourceLoader 类。详细解析,见 「7. PropertySourceLoader」 。
4.4.1.1 load
#load()
方法,加载配置。代码如下:
// ConfigFileApplicationListener#Loader.java |
<1>
处,初始化变量。每个变量的意思,看代码中的注释。<2>
处,初始化 Spring Profiles 相关。详细解析,见 「4.4.1.1 initializeProfiles」 中。胖友可以先跳过去看一眼,然后回来。<3>
处,遍历profiles
数组,逐个加载对应的配置文件。<3.1>
处,调用#addProfileToEnvironment(String profile)
方法,添加到environment.activeProfiles
中。代码如下:// ConfigFileApplicationListener#Loader.java
private void addProfileToEnvironment(String profile) {
// 如果已经在 activeProfiles 中,则返回
for (String activeProfile : this.environment.getActiveProfiles()) {
if (activeProfile.equals(profile)) {
return;
}
}
// 如果不在,则添加到 environment 中
this.environment.addActiveProfile(profile);
}- ~
<3.2>
处,调用#load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
方法,加载配置。这块会比较复杂,晚点再求看 「4.4.1.1.2 load」 。<3.3>
处,添加到processedProfiles
中,表示已处理。
<4>
处,调用#resetEnvironmentProfiles(List<Profile> processedProfiles)
方法,获得真正加载的 Profile 们,添加到 environment 中。代码如下:// ConfigFileApplicationListener#Loader.java
private void resetEnvironmentProfiles(List<Profile> processedProfiles) {
// 获得真正加载的 Profile 们,添加到 environment 中。
String[] names = processedProfiles.stream()
.filter((profile) -> profile != null && !profile.isDefaultProfile())
.map(Profile::getName).toArray(String[]::new);
this.environment.setActiveProfiles(names);
}- 因为每个 Profile 可能不存在对应的配置文件,只有真正加载到配置文件的 Profile 们,才会设置到
environment.activeProfiles
属性中。
- 因为每个 Profile 可能不存在对应的配置文件,只有真正加载到配置文件的 Profile 们,才会设置到
<5>
处,调用#load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
方法,加载配置。这块会比较复杂,晚点再求看 「4.4.1.1.2 load」 。- 和
<3.2>
处,有一些不同,主要差别在于传入的方法参数。 - 在补充一点,有点不造怎么解释了。😈
<5>
处的作用是,将profile=null
的情况中,将配置文件里有配置文件spring.profiles
内容,且属于基于的 Profile ,也添加到profile=null
在Loader.loaded
的映射中。具体的,可以看 「666. 彩蛋」 提供的示例。😈 真的是,好绕啊!!!!
- 和
<6>
处,调用#addLoadedPropertySources()
方法,将加载的配置对应的 MutablePropertySources 到environment
中。代码如下:// ConfigFileApplicationListener.java
private static final String DEFAULT_PROPERTIES = "defaultProperties";
// ConfigFileApplicationListener#Loader.java
private void addLoadedPropertySources() {
MutablePropertySources destination = this.environment.getPropertySources();
// 获得当前加载的 MutablePropertySources 集合
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
Collections.reverse(loaded); // <Z>
// 声明变量
String lastAdded = null; // 下面循环,最后找到的 MutablePropertySources 的名字
Set<String> added = new HashSet<>(); // 已添加到 destination 中的 MutablePropertySources 的名字的集合
// <X> 遍历 loaded 数组
for (MutablePropertySources sources : loaded) {
// <Y> 遍历 sources 数组
for (PropertySource<?> source : sources) {
// 添加到 destination 中
if (added.add(source.getName())) {
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}
private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded, PropertySource<?> source) {
if (lastAdded == null) {
if (destination.contains(DEFAULT_PROPERTIES)) {
destination.addBefore(DEFAULT_PROPERTIES, source);
} else {
destination.addLast(source);
}
} else {
destination.addAfter(lastAdded, source);
}
}<X>
和<Y>
处,为什么是两层遍历呢?因为一个 Profile 可以对应多个配置文件。例如说,Profile 为prod
,对应applicaion-prod.properties
和application-prod.yml
两个配置文件。- 这样,我们就可以从
environment
中,读取加载到的配置文件。 <Z>
处,为什么要反转一下呢?因为,配置在越后面的 Profile ,优先级越高,所以需要进行反转。举个例子spring.profiles.active=prod,dev
,那么 Profile 的优先级是dev > prod > null
。
下面,我们就可以跳到 「4.4.1.1.2 load」 小节,看看这个关键的逻辑。
艿艿:逻辑真的有点复杂!目的简单,过程中的逻辑细节比较多。和我一起,保持耐心!!!
4.4.1.1.1 initializeProfiles
在 #initializeProfiles()
方法之前,我们先来看 Profile 类。它是 ConfigFileApplicationListener 的内部类,是 Spring Profiles 的封装对象。代码如下:
// ConfigFileApplicationListener#Profile.java |
然后,继续来看 #initializeProfiles()
方法,初始化 Spring Profiles 相关。代码如下:
// ConfigFileApplicationListener#Loader.java |
<1>
处,添加null
到profiles
中。用于加载默认的配置文件。优先添加到profiles
中,因为希望默认的配置文件先被处理。<2.1>
处,调用#getProfilesActivatedViaProperty()
方法,获得激活的 Profile 们(从配置中)。代码如下:// ConfigFileApplicationListener#Loader.java
private Set<Profile> getProfilesActivatedViaProperty() {
if (!this.environment.containsProperty(ACTIVE_PROFILES_PROPERTY)
&& !this.environment.containsProperty(INCLUDE_PROFILES_PROPERTY)) {
return Collections.emptySet();
}
Binder binder = Binder.get(this.environment);
Set<Profile> activeProfiles = new LinkedHashSet<>();
activeProfiles.addAll(getProfiles(binder, INCLUDE_PROFILES_PROPERTY)); // spring.profiles.include
activeProfiles.addAll(getProfiles(binder, ACTIVE_PROFILES_PROPERTY)); // spring.profiles.active
return activeProfiles;
}- 读取
"spring.profiles.include"
和"spring.profiles.active"
对应的 Profile 们。
- 读取
<2.2>
处,调用#getOtherActiveProfiles(Set<Profile> activatedViaProperty)
方法,先添加激活的 Profile 们(不在配置中)到profiles
中。代码如下:// ConfigFileApplicationListener#Loader.java
private List<Profile> getOtherActiveProfiles(Set<Profile> activatedViaProperty) {
return Arrays.stream(this.environment.getActiveProfiles()).map(Profile::new)
.filter((profile) -> !activatedViaProperty.contains(profile))
.collect(Collectors.toList());
}- “不在配置中”,例如说:
SpringApplication.additionalProfiles
。
- “不在配置中”,例如说:
<2.3>
处,调用#addActiveProfiles(Set<Profile> profiles)
方法,再添加激活的 Profile 们(在配置中)到profiles
中。代码如下:// ConfigFileApplicationListener#Loader.java
void addActiveProfiles(Set<Profile> profiles) {
if (profiles.isEmpty()) {
return;
}
// 如果已经标记 activatedProfiles 为 true ,则直接返回
if (this.activatedProfiles) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Profiles already activated, '" + profiles + "' will not be applied");
}
return;
}
// 添加到 profiles 中
this.profiles.addAll(profiles);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Activated activeProfiles " + StringUtils.collectionToCommaDelimitedString(profiles));
}
// 标记 activatedProfiles 为 true 。
this.activatedProfiles = true;
// 移除 profiles 中,默认的配置们。
removeUnprocessedDefaultProfiles();
}
private void removeUnprocessedDefaultProfiles() {
this.profiles.removeIf(
(profile) -> (profile != null && profile.isDefaultProfile()));
}<3>
处,如果没有激活的 Profile 们,则添加默认的 Profile 。此处的“默认”是,指的是配置文件中的"spring.profiles.default"
对应的值。- 这块比较绕,胖友最好自己调试下。例如说,我们在 JVM 启动增加
--spring.profiles.active=prod
,则结果如下图:`profiles`
4.4.1.1.2 load
#load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
方法,加载指定 Profile 的配置文件。代码如下:
// ConfigFileApplicationListener#Loader.java |
<1>
处,调用#getSearchLocations()
方法,获得要检索配置的路径们。代码如下:// ConfigFileApplicationListener.java
// 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/";
/**
* The "config location" property name.
*/
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
/**
* The "config additional location" property name.
*/
public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
// ConfigFileApplicationListener#Loader.java
private Set<String> getSearchLocations() {
// 获得 `"spring.config.location"` 对应的配置的值
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
return getSearchLocations(CONFIG_LOCATION_PROPERTY);
}
// 获得 `"spring.config.additional-location"` 对应的配置的值
Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
// 添加 searchLocations 到 locations 中
locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
return locations;
}
private Set<String> getSearchLocations(String propertyName) {
Set<String> locations = new LinkedHashSet<>();
// 如果 environment 中存在 propertyName 对应的值
if (this.environment.containsProperty(propertyName)) {
// 读取属性值,进行分割后,然后遍历
for (String path : asResolvedSet(this.environment.getProperty(propertyName), null)) {
// 处理 path
if (!path.contains("$")) {
path = StringUtils.cleanPath(path);
if (!ResourceUtils.isUrl(path)) {
path = ResourceUtils.FILE_URL_PREFIX + path;
}
}
// 添加到 path 中
locations.add(path);
}
}
return locations;
}
// 优先使用 value 。如果 value 为空,则使用 fallback
private Set<String> asResolvedSet(String value, String fallback) {
List<String> list = Arrays.asList(StringUtils.trimArrayElements(
StringUtils.commaDelimitedListToStringArray((value != null)
? this.environment.resolvePlaceholders(value) : fallback)));
Collections.reverse(list);
return new LinkedHashSet<>(list);
}- 看似比较长,逻辑并不复杂。
- 如果配置了
"spring.config.location"
,则使用它的值,作为要检索配置的路径。 - 如果配置了
"spring.config.additional-location"
,则使用它作为附加要检索配置的路径。当然,还是会添加默认的DEFAULT_SEARCH_LOCATIONS
路径。 - 默认情况下,返回的值是
DEFAULT_SEARCH_LOCATIONS
。因为,绝大数情况,我们并不会做相应的配置。😈 所以,胖友如果懒的看逻辑,就记得这个结论就可以了。
<2>
处,调用#getSearchNames()
方法,获得要检索配置的文件名集合。代码如下:// ConfigFileApplicationListener.java
private static final String DEFAULT_NAMES = "application";
private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
// ConfigFileApplicationListener#Loader.java
private Set<String> getSearchNames() {
// 获得 `"spring.config.name"` 对应的配置的值
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
return asResolvedSet(property, null);
}
// 添加 names or DEFAULT_NAMES 到 locations 中
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}- 通过
DEFAULT_NAMES
静态属性,我们可以知道,默认都去的配置文件名(不考虑后缀)为"application"
。 - 剩余的逻辑,胖友简单看看即可。
- 默认情况下,返回的值是
DEFAULT_NAMES
。因为,绝大数情况,我们并不会做相应的配置。😈 所以,胖友如果懒的看逻辑,就记得这个结论就可以了。
- 通过
<3>
处,遍历names
数组,逐个调用#load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
方法,逐个加载 Profile 指定的配置文件。详细解析,见 「4.4.1.1.3 load」 。
4.4.1.1.3 load
#load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
方法,逐个加载 Profile 指定的配置文件。代码如下:
// ConfigFileApplicationListener#Loader.java |
<1>
处的逻辑,可以无视。具体的原因,见我添加的注释。<2>
处,遍历propertySourceLoaders
数组,逐个使用 PropertySourceLoader 读取配置。<3>
处,遍历每个 PropertySourceLoader 可处理的文件后缀集合。例如说,PropertiesPropertySourceLoader 可处理.properties
和.xml
后缀,YamlPropertySourceLoader 可处理.yml
和.yaml
后置。<4>
处,添加到processed
中。一个文件后缀,有且仅能被一个 PropertySourceLoader 所处理。<5>
处,调用#loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
方法,加载 Profile 指定的配置文件(带后缀)。详细解析,见 「4.4.1.1.7 loadForFileExtension」 。
4.4.1.1.4 Document
因为稍后在讲解
#loadForFileExtension(...)
方法需要用到,所以插播下。
Document ,是 ConfigFileApplicationListener 的内部类,用于封装 PropertySourceLoader 加载配置文件后。代码如下:
// ConfigFileApplicationListener#Document.java |
4.4.1.1.4.1 DocumentsCacheKey
DocumentsCacheKey ,是 ConfigFileApplicationListener 的内部类,用于表示加载 Documents 的缓存 KEY 。代码如下:
// ConfigFileApplicationListener#DocumentsCacheKey.java |
- 因为一个配置文件在 「4.4.1.1.7 loadForFileExtension」 方法中,我们会看到可能存在重复加载的情况,所以通过缓存,避免重新读取~
4.4.1.1.5 DocumentFilterFactory
因为稍后在讲解
#loadForFileExtension(...)
方法需要用到,所以插播下。
DocumentFilterFactory ,是 ConfigFileApplicationListener 的内部接口,用于创建 DocumentFilter 对象。代码如下:
// ConfigFileApplicationListener#DocumentFilterFactory.java |
在 「4.4.1.1 load」 中,我们在该方法中,已经看到它的两个匿名实现类,如下:
// ConfigFileApplicationListener#Loader.java |
- 它们分别调用对应的方法,创建 DocumentFilter 对象。
4.4.1.1.5.1 DocumentFilter
DocumentFilter ,是 ConfigFileApplicationListener 的内部接口,用于匹配配置加载后的 Document 对象。代码如下:
// ConfigFileApplicationListener#DocumentFilter.java |
- 在下面的文章中,我们会看到,加载的配置文件后,返回的是 Document 对象。但是,返回的 Document 对象,需要和 Profile 进行匹配。
4.4.1.1.5.2 getPositiveProfileFilter
#getPositiveProfileFilter(Profile profile)
方法,代码如下:
// ConfigFileApplicationListener#Loader.java |
<1>
处,当传入的profile
为空时,要求document.profiles
也要为空。<2>
处,当传入的profile
非空时,要求environment.activeProfiles
包含document.profiles
包含profile
。
可能比较绕,胖友先看 《Spring-boot动态profiles的实践》 文章的 「Spring Boot的Profiles属性」 部分。
什么意思呢?假设一个
application-prod.properties
的配置文件,一般我们的理解是对应 Profile 为prod
的情况,对吧?!但是,如果说我们在配置文件中增加了spring.profiles=dev
,那它实际是属于 Profile 为dev
的情况。艿艿:第一次知道还有这样的设定!!!
当然,我们绝大都数情况,并不会去定义
spring.profiles
属性。所以呢,分成两种情况:艿艿:仔细理解,我也懵逼了好多小时!!!!
profile
为null
的情况,处理默认情况,即我们未定义spring.profiles
属性。profile
非null
的情况,处理配置文件中定义了spring.profiles
属性,则需要使用profile
和spring.profiles
匹配,并且它要属于environment.activeProfiles
中已经激活的。
😈 所以呢,我们在 「4.4.1.1 load」 中,看到它是在加载指定 Profile 的配置文件所使用。
4.4.1.1.5.3 getPositiveProfileFilter
#getPositiveProfileFilter(Profile profile)
方法,代码如下:
// ConfigFileApplicationListener#Loader.java |
<1>
处,要求传入的profile
为空。因为呢,「4.4.1.1 load」 中,看到它是在加载无 Profile 的配置文件所使用。<2>
处,要求document.profiles
非空。一般情况下,我们在application.properties
中,也并不会填写spring.profiles
属性值。这就是说,这个方法默认基本返回false
。<3>
处,environment.activeProfiles
包含document.profiles
。
4.4.1.1.6 DocumentConsumer
因为稍后在讲解
#loadForFileExtension(...)
方法需要用到,所以插播下。
DocumentConsumer ,是 ConfigFileApplicationListener 的内部接口,用于处理传入的 Document 。代码如下:
// ConfigFileApplicationListener#DocumentConsumer.java |
在 「4.4.1.1 load」 中,我们在该方法中,已经看到它的一个匿名实现类,如下:
// ConfigFileApplicationListener#Loader.java |
- 差别在于传入的
#addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod, boolean checkForExisting)
方法的addMethod
参数不同。为什么呢?- 对于有 Profile 的情况,使用前者
MutablePropertySources::addLast
,将 Document 的 PropertySource 添加到尾部。 - 对于无 Profile 的情况,使用后者
MutablePropertySources::addFirst
,将 Document 的 PropertySource 添加到头部。 - 最终,我们看到
#addLoadedPropertySources()
方法中,会执行Collections.reverse(loaded)
代码段,进行颠倒。为什么呢?这样,就能很巧妙的实现application-prod.properties
的优先级,高于application.properties
,从而实现相同属性时,覆盖读取~,即读取的是application-prod.properties
的属性配置。
- 对于有 Profile 的情况,使用前者
4.4.1.1.6.1 addToLoaded
#addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod, boolean checkForExisting)
方法,将 Document 的 PropertySource ,添加到 Loader.loaded
中。代码如下:
// ConfigFileApplicationListener#Loader.java |
- 创建了一个 DocumentConsumer 对象。而其内部的逻辑,很简单,胖友自己瞅瞅即可。
4.4.1.1.7 loadForFileExtension
#loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
方法,加载 Profile 指定的配置文件(带后缀)。代码如下:
// ConfigFileApplicationListener#Loader.java |
<1>
处,获得两个 DocumentFilter 对象。为什么是两个呢?胖友思考下。实际上,我们在 「4.4.1.1.5.2 getPositiveProfileFilter」 中,已经说明了答案。<2>
处,加载 Profile 指定的配置文件(带后缀)。有三种情况,胖友认真看<2.1>
、<2.2>
、<2.3>
处的代码注释。<3>
处,加载(无需带 Profile)指定的配置文件(带后缀)。- 关于
#load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer)
方法,真正加载 Profile 指定的配置文件(带后缀)。详细解析,见 「4.4.1.1.8 load」 。
4.4.1.1.8 load
#load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer)
方法,真正加载 Profile 指定的配置文件(带后缀)。代码如下:
// ConfigFileApplicationListener#Loader.java |
<1.1>
处,判断指定的配置文件是否存在。若不存在,则直接返回。<1.2>
处,如果没有文件后缀的配置文件,则忽略,不进行读取。<1.3>
处,调用#loadDocuments(PropertySourceLoader loader, String name, Resource resource)
方法,加载配置文件,并返回 Document 数组。详细解析,见 「4.4.1.1.8.1 loadDocuments」 小节。<1.4>
处,如果没加载到,则直接返回。<2>
处,遍历加载到documents
数组,逐个调用DocumentFilter#match(Document document)
方法,进行匹配。若匹配成功,则添加到loaded
中。<2.1>
处,#addActiveProfiles(Set<Profile> profiles)
方法,已经在 「4.4.1.1.1 initializeProfiles」 中,详细解析。<2.2>
处,#addIncludedProfiles(Set<Profile> profiles)
方法,代码如下:// ConfigFileApplicationListener#Loader.java
private void addIncludedProfiles(Set<Profile> includeProfiles) {
// 整体逻辑是 includeProfiles - processedProfiles + profiles
LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
this.profiles.clear();
this.profiles.addAll(includeProfiles);
this.profiles.removeAll(this.processedProfiles);
this.profiles.addAll(existingProfiles);
}- ~
<3>
处,遍历loaded
数组,调用DocumentConsumer#accept(Profile profile, Document document)
方法,添加到本地的Loader.loaded
中。此处,在结合 「4.4.1.1.6 DocumentConsumer」 一起看。
😈 至此,Spring Boot 加载配置的功能,基本是完成了。总的来说,理解大体的流程,还是相对比较容易的。但是,想要扣懂这个过程的每一个细节,需要多多的调试。
艿艿:可能有些细节写的不到位,或者解释的不到位,又或者讲的不正确。所以,有任何疑惑,请立刻马上赶紧给我星球留言提问哈。
4.4.1.1.8.1 loadDocuments
#loadDocuments(PropertySourceLoader loader, String name, Resource resource)
方法,加载配置文件,并返回 Document 数组。代码如下:
// ConfigFileApplicationListener#Loader.java |
<1>
处,创建 DocumentsCacheKey 对象,从loadDocumentsCache
缓存中加载 Document 数组。<2.1>
处,如果不存在,则调用PropertySourceLoader#load(String name, Resource resource)
方法,加载指定配置文件。详细的解析,见 「7. PropertySourceLoader」 中。<2.2>
处,调用#asDocuments(List<PropertySource<?>> loaded)
方法,将返回的 PropertySource 数组,封装成 Document 数组。代码如下:// ConfigFileApplicationListener#Loader.java
private List<Document> asDocuments(List<PropertySource<?>> loaded) {
if (loaded == null) {
return Collections.emptyList();
}
return loaded.stream().map((propertySource) -> {
// 创建 Binder 对象
Binder binder = new Binder(
ConfigurationPropertySources.from(propertySource),
this.placeholdersResolver);
return new Document(propertySource,
binder.bind("spring.profiles", STRING_ARRAY).orElse(null), // 读取 "spring.profiles" 配置
getProfiles(binder, ACTIVE_PROFILES_PROPERTY), // 读取 "spring.profiles.active" 配置
getProfiles(binder, INCLUDE_PROFILES_PROPERTY)); // 读取 "spring.profiles.include" 配置
}).collect(Collectors.toList());
}
private Set<Profile> getProfiles(Binder binder, String name) {
return binder.bind(name, STRING_ARRAY).map(this::asProfileSet).orElse(Collections.emptySet());
}
private Set<Profile> asProfileSet(String[] profileNames) {
List<Profile> profiles = new ArrayList<>();
for (String profileName : profileNames) {
profiles.add(new Profile(profileName));
}
return new LinkedHashSet<>(profiles);
}<2.3>
处,添加到loadDocumentsCache
缓存中。
5. RandomValuePropertySource
org.springframework.boot.env.RandomValuePropertySource
,继承 PropertySource 类,提供随机值的 PropertySource 实现类。
不了解 Spring Boot 从配置文件中获取随机数,可以看看 《Spring Boot学习–从配置文件中获取随机数》 文章。
5.1 addToEnvironment
#addToEnvironment(ConfigurableEnvironment environment)
静态方法,创建 RandomValuePropertySource 对象,添加到 environment
中。代码如下:
// RandomValuePropertySource.java |
5.2 getProperty
实现 #getProperty(String name)
方法,获得 name
对应的随机值。代码如下:
// RandomValuePropertySource.java |
<1>
处,必须以"random."
前缀。在获取属性值,name
前后的${}
已经被去掉。<2>
处,调用#getRandomValue(String type)
方法,根据类型,获得随机值。代码如下:// RandomValuePropertySource.java
private Object getRandomValue(String type) {
// int
if (type.equals("int")) {
return getSource().nextInt();
}
// long
if (type.equals("long")) {
return getSource().nextLong();
}
// int 范围
String range = getRange(type, "int");
if (range != null) {
return getNextIntInRange(range);
}
// long 范围
range = getRange(type, "long");
if (range != null) {
return getNextLongInRange(range);
}
// uuid
if (type.equals("uuid")) {
return UUID.randomUUID().toString();
}
// md5
return getRandomBytes();
}
private String getRange(String type, String prefix) {
if (type.startsWith(prefix)) {
int startIndex = prefix.length() + 1;
if (type.length() > startIndex) {
return type.substring(startIndex, type.length() - 1);
}
}
return null;
}
private int getNextIntInRange(String range) {
String[] tokens = StringUtils.commaDelimitedListToStringArray(range);
int start = Integer.parseInt(tokens[0]);
if (tokens.length == 1) {
return getSource().nextInt(start);
}
return start + getSource().nextInt(Integer.parseInt(tokens[1]) - start);
}
private long getNextLongInRange(String range) {
String[] tokens = StringUtils.commaDelimitedListToStringArray(range);
if (tokens.length == 1) {
return Math.abs(getSource().nextLong() % Long.parseLong(tokens[0]));
}
long lowerBound = Long.parseLong(tokens[0]);
long upperBound = Long.parseLong(tokens[1]) - lowerBound;
return lowerBound + Math.abs(getSource().nextLong() % upperBound);
}
private Object getRandomBytes() {
byte[] bytes = new byte[32];
getSource().nextBytes(bytes);
return DigestUtils.md5DigestAsHex(bytes);
}- 比较简单,瞅瞅即明白。
😈 这个谜题,是不是被揭晓了~
6. PropertySourcesPlaceholdersResolver
org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver
,实现 PropertySource 对应的值是占位符的解析器 。
6.1 构造方法
// PropertySourcesPlaceholdersResolver.java |
- 其中,创建的
helper
属性,为 PropertyPlaceholderHelper 对象。其中SystemPropertyUtils.PLACEHOLDER_PREFIX
为${
,SystemPropertyUtils.PLACEHOLDER_SUFFIX
为}
。这样,例如说 RandomValuePropertySource 的${random.int}
等等,就可以被 PropertySourcesPlaceholdersResolver 所处理。
6.2 resolvePlaceholders
实现 #resolvePlaceholders(Object value)
方法,解析占位符。代码如下:
// PropertySourcesPlaceholdersResolver.java |
如果
value
是 String 类型,才可能是占位符。满足时,调用PropertyPlaceholderHelper#replacePlaceholders(String value, PlaceholderResolver placeholderResolver)
方法,解析出占位符里面的内容。- 例如说:PropertySourcesPlaceholdersResolver 中,占位符是
${}
,那么${random.int}
被解析后的内容是random.int
。 解析到占位符后,则回调
#resolvePlaceholder(String placeholder)
方法,获得占位符对应的值。代码如下:// PropertySourcesPlaceholdersResolver.java
protected String resolvePlaceholder(String placeholder) {
if (this.sources != null) {
// 遍历 sources 数组,逐个获得属性值。若获取到,则进行返回
for (PropertySource<?> source : this.sources) {
Object value = source.getProperty(placeholder);
if (value != null) {
return String.valueOf(value);
}
}
}
return null;
}- 这样,😈 RandomValuePropertySource 是不是就被串起来喽。
- 例如说:PropertySourcesPlaceholdersResolver 中,占位符是
7. PropertySourceLoader
org.springframework.boot.env.PropertySourceLoader
接口,加载指定配置文件,返回 PropertySource 数组。代码如下:
// PropertySourceLoader.java |
- 可能胖友,和我开始一样,为什么一个配置文件,加载后会存在多个 PropertySource 对象呢?下面,我们来见分晓~
7.1 PropertiesPropertySourceLoader
org.springframework.boot.env.PropertiesPropertySourceLoader
,实现 PropertySourceLoader 接口,加载 .xml
和 .properties
类型的配置文件。代码如下:
// PropertiesPropertySourceLoader.java |
<1>
处,返回可处理的文件类型,为properties
和xml
。<2.1>
处,调用#loadProperties(Resource resource)
方法,读取指定配置文件,返回 Map 对象。代码如下:// PropertiesPropertySourceLoader.java
private Map<String, ?> loadProperties(Resource resource) throws IOException {
String filename = resource.getFilename();
// 读取 XML 后缀的配置文件
if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
return (Map) PropertiesLoaderUtils.loadProperties(resource);
}
// 读取 Properties 后缀的配置文件
return new OriginTrackedPropertiesLoader(resource).load();
}- 根据
.xml
和.properties
后缀,使用不同的读取方法。至于读取配置的逻辑,暂时不在本文的范畴,hoho 。感兴趣的胖友,自己去瞅瞅。
- 根据
<2.2>
处,如果 Map 为空,返回空数组。<2.3>
处,将 Map 封装成 OriginTrackedMapPropertySource 对象,然后返回单元素的数组。org.springframework.boot.env.OriginTrackedMapPropertySource
,继承 MapPropertySource 类,实现 OriginLookup 接口,代码如下:// OriginTrackedMapPropertySource.java
public final class OriginTrackedMapPropertySource extends MapPropertySource
implements OriginLookup<String> {
"unchecked", "rawtypes" }) ({
public OriginTrackedMapPropertySource(String name, Map source) {
super(name, source);
}
public Object getProperty(String name) {
// 获得属性值
Object value = super.getProperty(name);
// 如果是 OriginTrackedValue 封装类型,则返回其真实的值
if (value instanceof OriginTrackedValue) {
return ((OriginTrackedValue) value).getValue();
}
return value;
}
public Origin getOrigin(String name) {
// 获得属性值
Object value = super.getProperty(name);
// 如果是 OriginTrackedValue 封装类型,则返回其 Origin
if (value instanceof OriginTrackedValue) {
return ((OriginTrackedValue) value).getOrigin();
}
return null;
}
}
7.2 YamlPropertySourceLoader
org.springframework.boot.env.YamlPropertySourceLoader
,实现 PropertySourceLoader 接口,加载 .yaml
和 .yml
类型的配置文件。代码如下:
// YamlPropertySourceLoader.java |
<1>
处,返回可处理的文件类型,为yaml
和yml
。<2>
处,如果不存在org.yaml.snakeyaml.Yaml
类,说明没有引入 snakeyaml 依赖,则抛出 IllegalStateException 异常。<3.1>
处,调用OriginTrackedYamlLoader#load()
方法,加载配置,返回 Map 数组。😈 哎哟,此处就是我们的好奇了,返回的是 Map 数组列!我们先来看看 《Spring Boot 使用 YML 文件配置多环境》 的 「1 一个yml文件」 ,每个----
分割线,会解析成一个对应的Map<String, Object>
对象。<3.2>
处,如果 Map 数组为空,返回空数组。<3.3>
处,将 Map 数组,封装成 OriginTrackedMapPropertySource 数组,然后返回。
666. 彩蛋
卧槽,真心是内心无比卧槽。比我预先的,长太太太多了。
如果有解释不到位的地方,麻烦胖友在星球提出下哟。hoho ,春节期间初二,在老家写完~
参考和推荐如下文章:
- oldflame-Jm 《Spring boot源码分析-profiles环境(4)》
- oldflame-Jm 《Spring boot源码分析-ApplicationListener应用环境(5)》
- youzhibing2904 《spring-boot-2.0.3不一样系列之源码篇 - run方法(二)之prepareEnvironment,绝对有值得你看的地方》
如下内容,是艿艿的草稿,胖友可以先不去了解。
假设 Profile 为 prod,dev
。读取结果如下:
如下过程,省略读取不到配置文件的情况。
- getPositiveProfileFilter 的情况
profile=null
部分- 读取
classpath:/application.properties
,匹配成功。 - 读取
classpath:/application.yaml
,匹配成功。
- 读取
profile=prod
- 读取
classpath:/application-prod.properties
,匹配成功(基于defaultFilter
)。 - 读取
classpath:/application-prod.properties
,匹配失败(基于profileFilter
)。 - 读取
classpath:/application-prod.properties
,匹配失败(基于profileFilter
)。 - 读取
classpath:/application.yaml
,匹配失败(基于profileFilter
)。【匹配上 prod 那部分】
- 读取
profile=dev
- 读取
classpath:/application-dev.properties
,匹配成功(基于defaultFilter
)。 - 读取
classpath:/application-dev.properties
,匹配失败(基于profileFilter
)。 - 读取
classpath:/application-dev.properties
,匹配失败(基于profileFilter
)。 - 读取
classpath:/application.yaml
,匹配失败(基于profileFilter
)。【匹配上 dev 那部分】
- 读取
- getNegativeProfileFilter 的情况
profile=null
部分- 读取
classpath:/application.properties
,匹配失败。 - 🙂🙂🙂 读取
classpath:/application.yaml
,匹配成功【匹配上 prod、dev 那部分】。比较有意思 😈 ,用于解决profile=null
的情况,可以把激活的 Profile ,也匹配上。但是在目前这个情况下,即使匹配上,在#addToLoaded(...)
方法中的 DocumentConsumer 的逻辑时,会在checkForExisting
那块的逻辑,如果要校验已经存在的情况,则如果已经存在,则直接return
被排除掉。因为在profile=prod
和profile=dev
部分,已经匹配上该部分的配置。- = =~当然,实在搞不懂这个逻辑,也不用纠结这个。重点是搞懂 Spring Boot 加载配置的核心逻辑。也就是,ConfigFileApplicationListener 整体的逻辑。
- 艿艿又思考了下,貌似突然也有点想不通,什么情况下,这块逻辑会成功加载到 Profile 不匹配的配置文件,即在在
#addToLoaded(...)
方法中的 DocumentConsumer 的逻辑时,会在checkForExisting
那块的逻辑,如果要校验已经存在的情况,则如果不存在。
- 读取