一、介绍
上一篇文章:springboot如何创建并配置环境3 - 配置扩展属性(上) 中我们介绍了springboot对配置文件的处理逻辑,但是由于篇幅过长,决定分上下集两部分讲解。
本文基于以下版本进行展开:
- jdk:1.8
- springboot:2.4.3
另外:由于篇幅过长,决定分四集文章来讲解分析
二、对contributors中的配置属性进行处理
上集对processAndApply()
方法的分析中,概括来讲就是分四步:①在确定profiles
前处理contributors
中的配置属性。②确定profiles
。③在确定profiles
后处理contributors
中的配置属性。④将contributors
中的配置属性应用到当前运行环境Environment
中。
因此下面我们来分析以下逻辑
- 处理
contributors
中的配置属性 - 确定
profiles
- 将配置属性应用到当前运行环境
Environment
中。
三、处理contributors
中的配置属性
该逻辑通过三个方法完成,分别是processInitial()
、processWithoutProfiles()
、processWithProfiles()
。而这三个方法其实内部实现都是通过调用contributors
对象的withProcessedImports()
方法完成的,他们之间的区别就是是否传入确定的profiles
。
因此我们主要对withProcessedImports()
方法进行分析。
先来看一下该方法的源码如下:
ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
ConfigDataActivationContext activationContext) {
// 根据环境激活上下文获取导入阶段,所谓导入阶段为profiles激活前和profiles激活后两个阶段
ImportPhase importPhase = ImportPhase.get(activationContext);
this.logger.trace(LogMessage.format("Processing imports for phase %s. %s", importPhase,
(activationContext != null) ? activationContext : "no activation context"));
// this表示contributors表示的对象,将该对象赋值给result,本质上仍然是保存contributor集合的contributors对象
ConfigDataEnvironmentContributors result = this;
int processed = 0;
while (true) {
// 从contributor集合中获取下一个将要处理的contributor
ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
if (contributor == null) {
// 如果contributor集合中没有要处理的元素,则返回该集合
this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
return result;
}
if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
// 如果contributor的类型为UNBOUND_IMPORT(未绑定导入)
// 从当前正处理的contributor对象中获取ConfigurationPropertySource
Iterable<ConfigurationPropertySource> sources = Collections
.singleton(contributor.getConfigurationPropertySource());
// 创建placeholder解析器,用来解析${}
PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(
result, activationContext, true);
// 创建binder对象,binder中包含了ConfigurationPropertySource解析器
Binder binder = new Binder(sources, placeholdersResolver, null, null, null);
// 将当前contributor复制给一个新的对象,并将类型修改为BOUND_IMPORT(已绑定导入)
ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);
// 根据原contributor集合重新创建一个Contributors对象,并将当前正处理的contributor对象进行替换
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
result.getRoot().withReplacement(contributor, bound));
continue;
}
// 创建位置解析器上下文
ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
result, contributor, activationContext);
// 创建配置数据加载器上下文
ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
// 从contributor中获取imports,imports中包含了当前contributor对象要处理的配置文件路径
List<ConfigDataLocation> imports = contributor.getImports();
this.logger.trace(LogMessage.format("Processing imports %s", imports));
// 调用importer的resolveAndLoad()方法来解析并读取配置数据
// 在importer中已经包含了位置解析器、配置数据加载器。
// 返回值是一个map对象,其中key中包含了配置文件的路径及其资源,value中包含的是配置文件中的配置数据
Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
locationResolverContext,
loaderContext,
imports);
this.logger.trace(LogMessage.of(() -> getImportedMessage(imported.keySet())));
// 将读取到的配置通过父子关系,设置为当前正处理的contributor的children,
// asContributors()方法将读取到的配置封装成contributor对象并设置其类型为UNBOUND_IMPORT(未绑定导入)
ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
asContributors(imported));
// 根据原contributor集合重新创建一个Contributors对象,并将当前正处理的contributor对象进行替换
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
result.getRoot().withReplacement(contributor, contributorAndChildren));
processed++;
}
}
其中,在contributor类型为UNBOUND_IMPORT
的if代码块中,主要是把该contributor的类型修改为BOUND_IMPORT
,并作为一个新的contributor对象将原contributor对象进行替换。如下所示
下面,我们分析withProcessedImports()
方法剩余部分逻辑:
其中,我们先进入importer.resolveAndLoad()
方法,该方法返回一个map对象(key为配置文件资源对象,value为从配置文件资源中加载的配置属性),其内部逻辑分两部分:①解析配置文件位置和资源,②加载配置属性。
1. 解析配置文件位置和资源
我们进入resolve()
方法查看如何解析配置文件的位置和资源。
从上面源码中可以看到,resolve()
方法对其locations
参数(配置文件位置)进行遍历,对每一个配置文件位置再调用重载的resolve()
方法进行解析。真正的解析过程是通过调用配置文件位置解析器resolvers
的resolve()
方法实现的。
前面讲过,springboot提供了两个配置文件解析器:①ConfigTreeConfigDataLocationResolver;②StandardConfigDataLocationResolver。我们从这两个解析器的isResolvable()
方法便可以判断出区别:前者用于解析带有前缀configtree:
的配置文件路径;后者解析任意配置文件路径。
下面我们以StandardConfigDataLocationResolver为例,分析如何解析配置文件位置。
其中resolve()
方法和resolveProfileSpecific()
方法逻辑大致相同,只是后者携带有效的profile参数。
因此我们分析其resolve()
方法,该方法先获取配置文件资源的引用,再根据该文件引用,获取该文件资源。
下面我们看如何获取配置文件资源的引用,以目录为例,查看getReferencesForDirectory()
方法
进一步查看配置文件引用的构造方法
由此我们便可以知道,springboot是如何根据spring.profiles.active
属性确定profiles对应的配置文件资源。
当我们得到配置文件资源的引用后,通过该引用获取对应的配置文件资源
我们再回到配置文件位置解析器resolvers
的resolve()
方法。
下面我们回到resolveAndLoad()
方法,其中resolved
集合中包含了默认指定的以及指定profile对应的配置文件资源。然后在调用load()
方法,从配置文件资源中加载配置属性即可。
2. 加载配置属性
这里我们关注resolveAndLoad()
方法对load()
方法的调用。
该方法以配置文件资源分析结果集合为参数,返回一个Map集合,其中key为配置文件资源分析结果,value为配置文件资源中的配置属性。
进入加载器loaders
的load()
方法,该方法用于加载指定配置文件资源中并返回该配置文件中的配置属性。
从该方法中看到,配置属性加载器有两种,分别是ConfigTreeConfigDataLoader
和StandardConfigDataLoader
,还记得前面我们分析的配置文件位置解析器也有两个分别是ConfigTreeConfigDataLocationResolver
和StandardConfigDataLocationResolver
,他们是一一对应的。
我们以StandardConfigDataLocationResolver
为例,查看它的load()
方法。
从该方法中我们看到,对配置文件中配置属性的加载是通过配置属性加载器中的load()
方法实现的,而该加载器又分为properties
和yaml
两种。
该load()
方法将我们在配置文件中定义的配置属性进行加载,并转化为propertySource
集合。再将该集合封装到ConfigData
对象中并返回。
而该加载器中对配置文件资源中的配置属性的加载过程我们这里就不做分析了,请有兴趣的读者自查。
最后再回到withProcessedImports()
方法
resolveAndLoad()
方法我们就分析结束了,该方法返回的imported
对象为map集合,其中key为配置文件资源分析结果,value为配置文件资源中的配置属性。最后通过withChildren()
方法将该map集合转为contributor
对象并保存到children
属性中,再通过withReplacement()
方法将contributor
对象更新。
至此我们在配置文件中定义的所有配置属性均已保存到contributors
对象中并返回。此时contributors
对象的结构如下
四、确定当前运行环境激活的profile
此过程由processAndApply()
方法中调用withProfiles()
方法完成
下面我们进入该方法源码查看
1. 获取附加的addtionalProfiles
从源码上看,addtionalProfiles
属性是在ConfigDataEnvironment
类的成员变量中直接定义的,且该属性是通过该类的构造方法设置的
class ConfigDataEnvironment {
// ...
private final Collection<String> additionalProfiles;
// ...
ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles,
ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
// ...
this.additionalProfiles = additionalProfiles;
// ...
}
}
通过该构造方法的调用链我们可以发现,该构造方法的调用如下所示
由此可知,附加profiles是从SpringApplication
类中获取的,那么是否也是由SpringApplication
类设置的呢?答案是肯定的。
在SpringApplication
类中有对应的方法定义
因此我们可以在springboot的主启动方法中通过以下方式设置
2. 获取spring.profiles.include定义的配置
进入getIncludedProfiles()
方法
从该方法中看到,对contributors
中的contributor
进行遍历,从中获取key为spring.profiles.include
的配置属性,将其添加到集合中并返回。
3. 获取spring.profiles.active定义的配置
进入Profiles
的构造方法查看,
该构造方法中定义了三种profiles,分别是spring.profiles.group
定义的profiles、spring.profiles.active
定义的profiles和spring.profiles.default
定义的profiles。我们逐个查看
spring.profiles.group
定义的profiles从
Profiles
的构造方法得知,springboot通过spring.profiles.group
定义profiles分组,且定义方式为Map
集合。我们通过下面示例说明
调式源码
spring.profiles.active
定义的profiles从
Profiles
的构造方法得知,springboot通过getActivatedProfiles()
方法获取spring.profiles.active
定义的profiles。
在getActivatedProfiles()
方法中,springboot获取spring.profiles.active
定义的profiles,并将前面获取的additionalProfiles
一同添加到集合中并返回,作为Profiles
实例的activeProfiles
属性。
spring.profiles.default
定义的profiles从
Profiles
的构造方法得知,springboot通过getDefaultProfiles()
方法获取spring.profiles.default
定义的profiles。
在`getDefaultProfiles()`方法中,springboot获取`spring.profiles.default`定义的profiles(默认为`default`),并将其添加到集合中并返回,作为`Profiles`实例的`defaultProfiles`属性。
最后,在将所有定义的profiles封装到Profiles实例
后,通过activationContext.withProfiles()
方法将该Profiles实例
添加到profiles激活上下文中。
五、将contributors中保存的配置信息应用到当前运行环境中
下面我们便到达processAndApply()
方法的最后一步,将contributors中保存的配置信息应用到当前运行环境中
下面进入applyToEnvironment
方法的源码
从源码可见,该方法虽然较长,但逻辑比较简单,就是将contributor集合中来自配置文件的配置属性添加到当前运行环境的配置属性集合中,然后对当前运行环境设置profiles。
此时,运行环境中的所有配置属性均已设置完毕,包含来自系统的配置属性以及来自配置文件的配置属性等,如下所示
至此,我们对springboot创建并配置运行环境的整个过程就分析结束了,其过程虽然繁琐,但如果认真梳理,其处理逻辑并不复杂,只需我们在阅读源码时耐得住寂寞沉得住气即可。
六、总结
- 在启动环境中主要保存配置信息和当前操作系统的配置信息以及环境变量。
- 针对不同的应用程序类型,springboot创建对应的运行环境实例,如
StandardEnvironment
、StandardServletEnvironment
、StandardReactiveWebEnvironment
。 - 在创建运行环境实例时,其构造器内部就已经首先将系统属性和环境变量保存到其内部属性中了。
- 通过观察者模式发布环境准备就绪事件,由监听该事件的各种监听器针对该事件进行不同的逻辑处理。
- 涉及到的设计模式
- 观察者模式:发布环境准备就绪事件,由对应的监听器执行逻辑
- 工厂模式:环境后处理器工厂
- 通过contributor对象临时保存所有配置文件中的配置属性
- 配置文件的格式有多种,
properties
、xml
、yaml
、以及yml
。 - 配置文件的位置有多种,
classpath:/
、classpath:/config/
、file:./
、file:./config/
、file:./config/*/
、以及指定的位置spring.config.location
- 最后将contributor对象集合中的配置属性再应用到运行环境中。
纸上得来终觉浅,绝知此事要躬行。
————————我是万万岁,我们下期再见————————