Spring Boot 源码阅读——配置文件加载
开头
在 SpringBoot 中,配置文件是环境的一部分
“环境”,有一个专属的类—— Environment
里面包含了各种应用程序中重要的角色:配置文件、JVM 系统属性、系统环境变量、JNDI、servlet 上下文参数等等
这篇文章只对其中配置文件的部分进行解读,一些关系不大的代码用注释 // ..
替代,节省篇幅
找到 Environment
SpringBoot 的启动,是从一个 run 方法开始的
@SpringBootApplication
public class SpringbootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTestApplication.class, args);
}
}
从这个 run 方法,一步一步点进去,就来到了下面这个方法
为了方便阅读,我省略了与本文无关的代码
SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
// ...
try {
// ...
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// ...
}
catch (Throwable ex) {
// ...
}
// ...
return context;
}
上面这段代码中,我只保留了一行代码
这是 run 方法中 Environment 对象的第一次出现,同时,也是环境初始化的地方:prepareEnvironment
环境初始化
创建实例
SpringApplication.java
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// 根据应用程序类型,创建对应的 Environment 实例,并进行一些基本配置
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
// 这里开始,正式对环境进行初始化
listeners.environmentPrepared(bootstrapContext, environment);
// ...
return environment;
}
初始化——广播事件
这里的调用链比较长,满满往深处走,我们要找到事件广播到最后,被处理的地方
SpringApplicationRunListeners.java
void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
doWithListeners("spring.boot.application.environment-prepared",
(listener) -> listener.environmentPrepared(bootstrapContext, environment));
}
EventPublishingRunListener.java
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment) {
this.initialMulticaster.multicastEvent(
new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment));
}
SimpleApplicationEventMulticaster.java
@Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, resolveDefaultEventType(event));
}
SimpleApplicationEventMulticaster.java
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
SimpleApplicationEventMulticaster.java
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
doInvokeListener(listener, event);
}
}
SimpleApplicationEventMulticaster.java
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
// 走到这就到底了,由各个事件监听器来执行相关操作
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
// ..
}
}
SpringBoot 自带的事件监听器有非常非常多,我们这里触发的时间是环境准备(environment-prepared),那么理所当然的,相关操作肯定在 EnvironmentPostProcessorApplicationListener
里面
EnvironmentPostProcessorApplicationListener.java
@Override
public void onApplicationEvent(ApplicationEvent event) {
// 可以看到这个监听器只对三个事件进行了处理,正好有我们触发的这个事件
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent();
}
if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}
EnvironmentPostProcessorApplicationListener.java
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
SpringApplication application = event.getSpringApplication();
// 这个循环就是环境初始化的关键了,程序中有很多的后置处理器,包括我们的配置文件加载也在其中
for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(application.getResourceLoader(),
event.getBootstrapContext())) {
postProcessor.postProcessEnvironment(environment, application);
}
}
SpringBoot 提供了两个配置文件处理器,其中 ConfigFileApplicationListener
是旧版本中的处理器,在新版本已废弃,并被 ConfigDataEnvironmentPostProcessor
取代
找到了配置文件的处理器,那么接下来就可以开始阅读源码了
ConfigDataEnvironmentPostProcessor.java
void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
try {
this.logger.trace("Post-processing environment to add config data");
resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
// getConfigDataEnvironment 是对环境的一层封装,对配置文件的读取是后面的那个方法:processAndApply()
getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
}
catch (UseLegacyConfigProcessingException ex) {
// ...
}
}
ConfigDataEnvironment.java
void processAndApply() {
// importer 是一个很关键的对象,包含了扫描的目录、扫描到的文件
// 当然这里只是进行了实例化,还没有任何数据
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
this.loaders);
registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);
// processInitial 就是最核心的方法了,里面对所有 SpringBoot 约定的配置进行了扫描和加载
// 下面会着重看这一部分代码
ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
// 创建上下文环境
ConfigDataActivationContext activationContext = createActivationContext(
contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
// process 开头的方法,内部逻辑与 processInitial 相似,所以下面几行就不讲了
// 区别就像方法名一样,processInitial 是初始化,这两个与激活相关
contributors = processWithoutProfiles(contributors, importer, activationContext);
activationContext = withProfiles(contributors, activationContext);
contributors = processWithProfiles(contributors, importer, activationContext);
// 这一步将配置文件应用进环境
applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(),
importer.getOptionalLocations());
}
ConfigDataEnvironment.java
private ConfigDataEnvironmentContributors processInitial(ConfigDataEnvironmentContributors contributors, ConfigDataImporter importer) {
// ..
// 调用处理器的方法
contributors = contributors.withProcessedImports(importer, null);
// ..
return contributors;
}
从 processAndApply 方法可以看出,SpringBoot 的配置文件环境初始化包含三个步骤:
其中,扫描和加载由配置文件处理器来完成,而应用在这个方法的最后
配置文件处理器
ConfigDataEnvironmentContributors.java
ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
ConfigDataActivationContext activationContext) {
// 日志以及对象创建
// ..
while (true) {
// 依次获取所有的 contributor
ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
if (contributor == null) {
this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
return result;
}
// 一些对象的创建
// ..
// 从 contributor 获取扫描目录
List<ConfigDataLocation> imports = contributor.getImports();
this.logger.trace(LogMessage.format("Processing imports %s", imports));
// resolveAndload 就是加载配置文件
// resolve 是扫描,需要知道这里是一个循环,不同的 contributor 内含的扫描目录是不同的,其也有各自的含义,debug 时我们只需要看配置文件的扫描即可
// load 是加载,不同类型的配置文件有不同的加载器,后面会看到的
Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
// ...
}
}
ConfigDataImporter.java
Map<ConfigDataResolutionResult, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext, ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext, List<ConfigDataLocation> locations) {
try {
Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;
// 扫描
List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);
// 加载
return load(loaderContext, resolved);
}
catch (IOException ex) {
// ..
}
}
扫描
扫描——in
我们先来看扫描,扫描进行了非常多层的封装,要有耐心的一步步深入
ConfigDataImporter.java
/**
* 扫描代码片段1
*/
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext locationResolverContext, Profiles profiles, List<ConfigDataLocation> locations) {
List<ConfigDataResolutionResult> resolved = new ArrayList<>(locations.size());
for (ConfigDataLocation location : locations) {
// 将所有扫描结果加入集合,从这里的 resolve 方法去看扫描代码片段2
resolved.addAll(resolve(locationResolverContext, profiles, location));
}
return Collections.unmodifiableList(resolved);
}
ConfigDataImporter.java
/**
* 扫描代码片段2
*/
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext locationResolverContext, Profiles profiles, ConfigDataLocation location) {
try {
// 进入扫描代码片段3
return this.resolvers.resolve(locationResolverContext, location, profiles);
}
catch (ConfigDataNotFoundException ex) {
// ..
}
}
ConfigDataImporter.java
/**
* 扫描代码片段3
*/
List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) {
if (location == null) {
return Collections.emptyList();
}
for (ConfigDataLocationResolver<?> resolver : getResolvers()) {
if (resolver.isResolvable(context, location)) {
// 进入扫描代码片段4
return resolve(resolver, context, location, profiles);
}
}
throw new UnsupportedConfigDataLocationException(location);
}
ConfigDataImporter.java
/**
* 扫描代码片段4
*/
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolver<?> resolver, ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) {
// 这里的 debug 会复杂一点,因为传入了一个 Supplier 函数
// 如下面的扫描代码片段5,内部执行了 Supplier 函数的 get 方法,也就是这里的 lambda 表达式中的 resolve 方法
List<ConfigDataResolutionResult> resolved = resolve(location, false, () -> resolver.resolve(context, location));
if (profiles == null) {
return resolved;
}
List<ConfigDataResolutionResult> profileSpecific = resolve(location, true, () -> resolver.resolveProfileSpecific(context, location, profiles));
return merge(resolved, profileSpecific);
}
/**
* 扫描代码片段5
*/
private List<ConfigDataResolutionResult> resolve(ConfigDataLocation location, boolean profileSpecific, Supplier<List<? extends ConfigDataResource>> resolveAction) {
// 前面4个代码片段,不管是中间变量还是 return 的结果,都是 List<ConfigDataResolutionResult>
// 而这里出现了 List<ConfigDataResource>,显而易见,这一句代码是真正执行扫描的入口
// 上面的几段代码只是对结果集的封装与处理
// 代码片段4中提到,这个 get 就是 resolve,所以我们进入扫描代码片段6
List<ConfigDataResource> resources = nonNullList(resolveAction.get());
List<ConfigDataResolutionResult> resolved = new ArrayList<>(resources.size());
for (ConfigDataResource resource : resources) {
resolved.add(new ConfigDataResolutionResult(location, resource, profileSpecific));
}
return resolved;
}
StandardConfigDataLocationResolver.java
/**
* 扫描代码片段6
*/
@Override
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) throws ConfigDataNotFoundException {
// 上面几段代码的参数无非是 localtion、profile 等基本信息
// 而这里的 getReferences 获取的是一个依赖集
// 在进入下一个 resolve 方法前,我们先看看这个依赖集是什么作用
// 这里传入了两个参数,context 就不用多说了,location.split() 需要提一句
// SpringBoot 内置的扫描目录是以分号隔开的字符串,这里的 split 方法就是以分号来分割字符串,变成一个目录地址的数组
// 而依赖集就是根据 SpringBoot 内置的规则获取的依赖地址
return resolve(getReferences(context, location.split()));
}
依赖
StandardConfigDataLocationResolver.java
/**
* 依赖代码片段1
*/
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context, ConfigDataLocation[] configDataLocations) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
for (ConfigDataLocation configDataLocation : configDataLocations) {
// 是不是很熟悉,像极了 resolve 方法的层层嵌套,那么继续往里面看
references.addAll(getReferences(context, configDataLocation));
}
return references;
}
StandardConfigDataLocationResolver.java
/**
* 依赖代码片段2
*/
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context, ConfigDataLocation configDataLocation) {
String resourceLocation = getResourceLocation(context, configDataLocation);
try {
// 我们只看配置文件的扫描,配置文件扫描的是目录,所以这里只解析这段 if 里的代码
if (isDirectory(resourceLocation)) {
return getReferencesForDirectory(configDataLocation, resourceLocation, NO_PROFILE);
}
// ..
}
catch (RuntimeException ex) {
// ..
}
}
StandardConfigDataLocationResolver.java
/**
* 依赖代码片段3
*/
private Set<StandardConfigDataReference> getReferencesForDirectory(ConfigDataLocation configDataLocation, String directory, String profile) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
// 配置文件的 name 为 application,这是 SpringBoot 约定好的,不是我们能更改的
for (String name : this.configNames) {
// 进入下一个方法 依赖代码片段4
Deque<StandardConfigDataReference> referencesForName = getReferencesForConfigName(name, configDataLocation, directory, profile);
references.addAll(referencesForName);
}
return references;
}
StandardConfigDataLocationResolver.java
/**
* 依赖代码片段4
*/
private Deque<StandardConfigDataReference> getReferencesForConfigName(String name, ConfigDataLocation configDataLocation, String directory, String profile) {
Deque<StandardConfigDataReference> references = new ArrayDeque<>();
// PropertySourceLoader 在 SpringBoot启动时已经初始化完毕
// 在 spring.factories 文件中可以看到一共有两个 loader
// PropertiesPropertySourceLoader 和 YamlPropertySourceLoader
// 分别是 (properties 与 xml)加载器、(yaml 与 yml)加载器
for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {
// 不同加载器有对应的文件扩展名
for (String extension : propertySourceLoader.getFileExtensions()) {
// 根据目录 + 文件名 + 激活名 + 扩展名创建一个资源依赖,并传入了对应的资源加载器
StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, directory,
directory + name, profile, extension, propertySourceLoader);
if (!references.contains(reference)) {
references.addFirst(reference);
}
}
}
return references;
}
扫描——out
获取到依赖后,代码又回到了扫描的最后:扫描代码片段6
StandardConfigDataLocationResolver.java
/**
* 扫描代码片段6
*/
@Override
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) throws ConfigDataNotFoundException {
// 这个时候 getReferences 已经返回了一个依赖集
// 我们直接去看 resolve 方法怎么处理依赖
return resolve(getReferences(context, location.split()));
}
StandardConfigDataLocationResolver.java
/**
* 扫描代码片段7
* 从这个 resolve 方法开始,限定符已经变成了 private
* 开始涉及到真正的扫描逻辑了
*/
private List<StandardConfigDataResource> resolve(Set<StandardConfigDataReference> references) {
List<StandardConfigDataResource> resolved = new ArrayList<>();
for (StandardConfigDataReference reference : references) {
// 继续进入下一个 resolve 方法
resolved.addAll(resolve(reference));
}
if (resolved.isEmpty()) {
resolved.addAll(resolveEmptyDirectories(references));
}
return resolved;
}
StandardConfigDataLocationResolver.java
/**
* 扫描代码片段8
*/
private List<StandardConfigDataResource> resolve(StandardConfigDataReference reference) {
// 这个 isPattern 是判断依赖的路径字符串是否是模式串
// 比如 classpath:*
// 这类带 * 号的代表目录下的所有文件,那么就需要特殊的处理
// 我们加载配置文件的部分是确定文件地址的,所以走这个 if
if (!this.resourceLoader.isPattern(reference.getResourceLocation())) {
return resolveNonPattern(reference);
}
return resolvePattern(reference);
}
StandardConfigDataLocationResolver.java
/**
* 扫描代码片段9
*/
private List<StandardConfigDataResource> resolveNonPattern(StandardConfigDataReference reference) {
// 根据依赖地址加载资源
// 这里的加载和后面的 load 不是一个概念
// 这里只是将资源加载到内存,还没有开始解析
// 而 load 的加载是加载到 SpringBoot
Resource resource = this.resourceLoader.getResource(reference.getResourceLocation());
// 如果资源不存在并且是可跳过的,那么返回空集合
if (!resource.exists() && reference.isSkippable()) {
logSkippingResource(reference);
return Collections.emptyList();
}
// 资源存在的话就返回
return Collections.singletonList(createConfigResourceLocation(reference, resource));
}
那么到这里,资源的扫描获取就结束了,在 resolve 的层层调用中,资源又会被封装为 List
在进入下一节“加载”之前,我们不妨 debug 调试看看,SpringBoot 对资源文件会扫描哪些目录
扩展知识
扫描的目录
SpringBoot 需要加载的文件很多,不过配置文件的扫描都是以 application
开头的
我们可以在 getReferences 方法内打上断点,并加上判断条件:
"application".equals(this.configNames[0])
debug 后,这里会有 5 次停顿,查看 configDataLocation 的 value 就能知道扫描的目录有哪些,分别是:
value | 含义 |
---|---|
file:./ | 项目编译打包后 所在根目录 |
file:./config/ | 项目编译打包后 所在根目录下的 config 目录 |
file:./config/*/ | 项目编译打包后 所在根目录下的 config 目录中的任意子目录 |
classpath:/ | 项目编译打包后的 classes 目录(对应开发环境的 resources 目录) |
classpath:/config/ | 项目编译打包后的 classes 目录下的 config 目录 |
扫描的文件
目录找到了,接下来就要找扫描的文件有哪些了
上面也提到过,SpringBoot 默认提供两种加载器
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
分别能加载:
- properties 和 xml
- yaml 和 yml
那么即使不进行 debug,也能猜到扫描的文件只有 4 个
但是,SpringBoot 并不支持 xml 类型的配置文件,之所以会扫描,是因为在加载器中 xml 是和 properties 并列
所以实际能使用的配置文件格式只有 3种
当然该 debug 还是得 debug
这次我们在 getReferencesForConfigName 方法内打上断点,条件是:
"application".equals(name)
根据 debug 可以得出上面的结论,确实只扫描了这 4 个文件
而且顺序是:
- properties
- xml
- yml
- yaml
但是由于加入双端队列(Deque)的方式是 addFirst,所以在队列中的顺序刚好是相反的
看到这就引出下一个问题:如果 4 个配置文件共存,生效的是哪个?又或者是都生效,但是覆盖的顺序是什么?
这个问题就要看“加载”部分的源码才能知道了
加载
入口在 配置文件处理器 这一小节的最后一段代码
在看下面这段加载代码前,我们先通过 debug 查看参数 candidates
我在 resources 中创建了 3 种类型的配置文件,都会被扫描进来,所以这里的 candidates 有 3 个 element
顺序正如 上面 讲的:
- yaml
- yml
- properties
ConfigDataImporter.java
private Map<ConfigDataResolutionResult, ConfigData> load(ConfigDataLoaderContext loaderContext, List<ConfigDataResolutionResult> candidates) throws IOException {
Map<ConfigDataResolutionResult, ConfigData> result = new LinkedHashMap<>();
// 这里的循环是倒序的,忽略覆不覆盖的问题,加载的顺序又与 candidates 内元素的顺序相反
// 一来一去,加载的顺序就和扫描的顺序一致了
for (int i = candidates.size() - 1; i >= 0; i--) {
ConfigDataResolutionResult candidate = candidates.get(i);
ConfigDataLocation location = candidate.getLocation();
// 前面也提到过,资源已经被加载到内存,这里直接拿
ConfigDataResource resource = candidate.getResource();
if (resource.isOptional()) {
this.optionalLocations.add(location);
}
// 如果资源已经被加载了,那就添加路径
// 因为是 set,所以不会重复
if (this.loaded.contains(resource)) {
this.loadedLocations.add(location);
}
else {
try {
// 调用对应的加载器进行配置文件的加载,说是加载,其实用解析这个词更合适一点
// 进过两三次调用后,最终进入的就是两种类型的资源加载器的 load 方法
// 下面就不贴代码了
ConfigData loaded = this.loaders.load(loaderContext, resource);
if (loaded != null) {
this.loaded.add(resource);
this.loadedLocations.add(location);
result.put(candidate, loaded);
}
}
catch (ConfigDataNotFoundException ex) {
handle(ex, location, resource);
}
}
}
return Collections.unmodifiableMap(result);
}
上面这段代码,最容易产生疑惑的地方就是资源加载的 if 判断
为什么会重复加载资源呢,这就得回到最外层的代码。不过先别急,我们先把加载看完
上面已经将配置文件 load 解析完成了,但是还没绑定到我们的 SpringBoot,也就是说 SringBoot 还不能访问到配置信息
绑定并不是 load 的功能,load 是加载与解析
所以我们回到外层代码 配置文件处理器 的部分
ConfigDataEnvironmentContributors.java
ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer, ConfigDataActivationContext activationContext) {
// ..
while (true) {
// 这里是对 contributor 的遍历,可以想象成一个链表或者队列,contributor 包含环境初始化中的各个生命周期(这个词可能不太合适,但是我想不到更好的词了)
// file目录和classpath是不同的 contributor,这里以 classpath 为例
// 第一阶段是 INITAL_IMPORT,字面意思就是初始化导入
// 这个阶段的 contributor 中就已经包含配置文件了要扫描的路径,所以会进行一次 resolveAndLoad
// 扫描和加载结束后,回反馈给 contributor,具体表现为其内部的 children
// children 包含两种值:EMPTY_LOCATION 和 BOUND_IMPORT
// EMPTY_LOCATION 是上一阶段的扫描加载中为空的目录
// BOUND_IMPORT 是已进行绑定的配置文件
// 下面是我的 debug 数据,只取了成功加载的那一部分
// BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.properties] []
// BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.yml] []
// BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.yaml] []
//
// 第二阶段为 UNBOUND_IMPORT,含义为未绑定的资源
// 这一阶段的目的是绑定上一阶段获取到的资源
// 绑定成功后,同样会反馈给 contributor,不过这次写回的就是 null 了
ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
// ..
// 第二阶段:绑定已导入的资源
// 绑定:其实就是检测下资源存不存在然后创建一个binder
// binder 会尝试与一些东西进行绑定
// 不过对配置文件的加载来说,没有多大影响
if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(result, activationContext);
// 将绑定的结果回写入 contributor,并放回 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);
List<ConfigDataLocation> imports = contributor.getImports();
this.logger.trace(LogMessage.format("Processing imports %s", imports));
Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext, locationResolverContext, loaderContext, imports);
ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase, asContributors(imported));
// 将本次扫描加载的结果回写入 contributor,并放回 contributor 链
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext, result.getRoot().withReplacement(contributor, contributorAndChildren));
// ..
}
}
下面是完整代码配上执行步骤的图
经过上面这些花里胡哨的步骤后,result 终于完成了:
{BEFORE_PROFILE_ACTIVATION=[EXISTING null null []
, EXISTING null null []
, EXISTING null null []
, EXISTING null null []
, EXISTING null null []
, EXISTING null null []
, INITIAL_IMPORT null null []
EMPTY_LOCATION optional:file:./;optional:file:./config/;optional:file:./config/*/ null [IGNORE_IMPORTS]
, INITIAL_IMPORT null null []
BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.properties] []
BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.yml] []
BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.yaml] []
]}
这些数据看着很抽象,其实每一行都是一个 contributor 对象
下面几个 BOUND_IMPORT 的就包含了 resource,有兴趣可以去 debug 看看最终结果,我找了 application.properties 的截了个图如下
应用
配置文件处理器进行完扫描与加载后,将 result 返回到了 processAndApply 方法,看到 processAndApply,就能理解为什么上面会出现资源重复加载的问题了
- processInitial
- processWithoutProfiles
- processWithProfiles
这几个方法在内部调用的都是 withProcessedImports,只是参数不同,他们的差别就是配置文件的激活筛选
ConfigDataEnvironment.java
void processAndApply() {
// importer 是一个很关键的对象,包含了扫描的目录、扫描到的文件
// 当然这里只是进行了实例化,还没有任何数据
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
this.loaders);
registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);
// processInitial 就是最核心的方法了,里面对所有 SpringBoot 约定的配置进行了扫描和加载
ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
// 创建上下文环境
ConfigDataActivationContext activationContext = createActivationContext(
contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
// process 开头的方法,内部逻辑与 processInitial 相似,所以下面几行就不讲了
// 区别就像方法名一样,processInitial 是初始化,这两个与激活相关
contributors = processWithoutProfiles(contributors, importer, activationContext);
activationContext = withProfiles(contributors, activationContext);
contributors = processWithProfiles(contributors, importer, activationContext);
// 这一步将配置文件应用进环境
applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(),
importer.getOptionalLocations());
}
ConfigDataEnvironment.java
private void applyToEnvironment(ConfigDataEnvironmentContributors contributors, ConfigDataActivationContext activationContext, Set<ConfigDataLocation> loadedLocations, Set<ConfigDataLocation> optionalLocations) {
// 进行一些合法性检验
checkForInvalidProperties(contributors);
checkMandatoryLocations(contributors, activationContext, loadedLocations, optionalLocations);
// 获取环境的资源集合
MutablePropertySources propertySources = this.environment.getPropertySources();
// 应用
// 这里传入了环境的资源集合 propertySources
applyContributor(contributors, activationContext, propertySources);
// ..
}
ConfigDataEnvironment.java
private void applyContributor(ConfigDataEnvironmentContributors contributors, ConfigDataActivationContext activationContext, MutablePropertySources propertySources) {
// 遍历加载过程返回的 result,也就是一堆 contributor
for (ConfigDataEnvironmentContributor contributor : contributors) {
PropertySource<?> propertySource = contributor.getPropertySource();
// 找到 BOUND_IMPORT 型的 contributor
if (contributor.getKind() == ConfigDataEnvironmentContributor.Kind.BOUND_IMPORT && propertySource != null) {
if (!contributor.isActive(activationContext)) {
// ..
}
else {
// ..
// 加入环境的资源集合,并且是放在最后
// 还记得我们加载配置文件的顺序吗,和扫描顺序一致
// properties > xml > yml > yaml
// 所以环境的资源集合中,几个配置文件的相对顺序也是如此
propertySources.addLast(propertySource);
// ..
}
}
}
}
到这里,配置文件的应用就结束了,已经被写入环境 Environment
在启动类写个测试
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootTestApplication.class, args);
ConfigurableEnvironment environment = context.getEnvironment();
System.out.println(environment.getProperty("server.port"));
}
ConfigurationPropertySourcesPropertyResolver.java
@Override
@Nullable
public String getProperty(String key) {
return this.propertyResolver.getProperty(key);
}
ConfigurationPropertySourcesPropertyResolver.java
@Override
public String getProperty(String key) {
return getProperty(key, String.class, true);
}
ConfigurationPropertySourcesPropertyResolver.java
private <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
Object value = findPropertyValue(key);
if (value == null) {
return null;
}
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
return convertValueIfNecessary(value, targetValueType);
}
ConfigurationPropertySourcesPropertyResolver.java
private Object findPropertyValue(String key) {
ConfigurationPropertySourcesPropertySource attached = getAttached();
if (attached != null) {
ConfigurationPropertyName name = ConfigurationPropertyName.of(key, true);
if (name != null) {
try {
ConfigurationProperty configurationProperty = attached.findConfigurationProperty(name);
return (configurationProperty != null) ? configurationProperty.getValue() : null;
}
catch (Exception ex) {
}
}
}
return this.defaultResolver.getProperty(key, Object.class, false);
}
ConfigurationPropertySourcesPropertySource.java
ConfigurationProperty findConfigurationProperty(ConfigurationPropertyName name) {
if (name == null) {
return null;
}
// 这个类中的 source 就是 Environment 中的 propertySources
// 从前往后遍历
for (ConfigurationPropertySource configurationPropertySource : getSource()) {
ConfigurationProperty configurationProperty = configurationPropertySource.getConfigurationProperty(name);
// 直到某个 source 有这个属性,那就返回
// 而我们的 3 个配置文件在 source 中的顺序为
// properties > yml > yaml
// 所以配置文件都是能被读取进去的,也都能被应用,但是他们的属性有优先级
if (configurationProperty != null) {
return configurationProperty;
}
}
return null;
}
同目录下的配置文件优先级问题解决了,那不同目录呢
在 扫描的目录 中按扫描顺序,列举了 5 个目录
每一个目录被扫描后,会在 resolve 方法被 addAll 进集合
也就是先扫描的在前,后扫描的在后
而在加载中,for 循环是逆序的,所以目录的加载顺序刚好与扫描顺序相反
总结
- 扫描目录的顺序
- file:./
- file:./config/
- file:./config/*/
- classpath:/
- classpath:/config/
- 扫描文件的顺序
- properties
- xml
- yml
- yaml
- 加载文件的顺序
- properties
- xml
- yml
- yaml
- 加载目录的顺序
- classpath:/config/
- classpath:/
- file:./config/*/
- file:./config/
- file:./
- 越先被加载的配置文件,在环境中被应用的优先级越高