[toc]
一、介绍
在上一篇文章:springboot创建并配置环境2 - 配置基础环境 中,我们介绍了springboot如何配置基础环境变量。本篇文章讨论如何处理配置文件。即来自不同位置的配置属性,如:classpath
路径下的application.yml
、bootstrap.yml
、使用@PropertySource
注解指定的文件、以及来自项目外部的配置文件等。
本文基于以下版本进行展开:
- jdk:1.8
- springboot:2.4.3
另外:由于篇幅过长,决定分四集文章来讲解分析
二、配置文件application.yml
我们在项目的resources目录下新建配置文件application.yml
,并添加如下配置
然后进入断点调试。
当代码执行到下面这一行时,我们查看此时环境实例中保存的配置属性都是前面我们讲过的,来自配置文件中的属性还没有被保存。
当我们进行到下一行时,此时来自配置文件中的属性就已经被保存到环境中来了
很明显,springboot通过观察者模式发布一个环境准备就绪事件,由监听该事件的监听器处理不同的逻辑,即以下代码
// 通过观察者模式发布一个环境准备就绪事件,由监听该事件的监听器处理不同的逻辑
listeners.environmentPrepared(bootstrapContext, environment);
我们进入environmentPrepared()
方法一探究竟,该方法源码如下所示,大致意思就是向this.listeners
传入Consumer
对象,由this.listeners
中的监听器来执行这个Consumer
对象。
插入一嘴,this.listeners
是SpringApplicationRunListener
即运行时监听器的集合,如下所示
该集合是进入run()
方法后执行的第二个关键步骤,第一个关键步骤就是创建BootstrapContext
上下文。
而对SpringApplicationRunListener
即运行时监听器的集合的获取则是从META-INF
目录下的spring.factories
文件中获取的。
而运行时监听器的集合中内置的该监听器只有一种,即EventPublishingRunListener
事件发布监听器,该监听器专门用来发布事件。而我们当前正要发布一个环境准备就绪事件。
而且我们在调试代码过程中也看到了,此时需要发布的是环境准备就绪事件,调用的是监听器的environmentPrepared()
方法,所以我们进入EventPublishingRunListener
监听器的这个方法
此时才真正的创建了环境准备就绪事件的实例ApplicationEnvironmentPreparedEvent
,并通过this.initialMulticaster
来广播该事件。其实这里涉及到的只是springboot对观察者模式的实现。但是为了进一步摸清整个环境配置过程的来龙去脉,也无所谓了。
下面我们进入multicastEvent()
方法来看看是如何广播的。
该方法通过getApplicationListeners()
方法获取项目中监听当前事件的所有监听器,并对其进行遍历,然后不同的监听器针对该事件进行不同的逻辑处理。
从上面的截图中我们发现,针对环境准备就绪事件的监听器有6个,他们监听到该事件后处理不同的逻辑。
EnvironmentPostProcessorApplicationListener
:使用环境后处理器对环境进行配置AnsiOutputApplicationListener
:控制日志的颜色,决定输出的日志文本是否具有颜色LoggingApplicationListener
:对日志进行配置BackgroundPreinitializer
:提前初始化耗时任务的后台线程DelegatingApplicationListener
:将监听到的事件再次发布,由指定的监听器执行FileEncodingApplicationListener
:如果系统文件编码与环境中设置的期望值不匹配,则立刻停止应用程序的启动。
从上面的说明来看,我们当前关注的是如何将配置文件中的配置信息加载到环境中,因此我们只需要关注EnvironmentPostProcessorApplicationListener
在监听到环境准备就绪事件后执行什么处理逻辑即可。
下面我们进入监听器EnvironmentPostProcessorApplicationListener
,在springboot中,监听器通过onApplicationEvent()
方法监听事件,该方法如下所示
前面讲过,当前程序发布的事件为环境准备就绪事件ApplicationEnvironmentPreparedEvent
,所以显然我们将目光放在onApplicationEnvironmentPreparedEvent()
方法上来处理该事件。
在该处理方法中,根据当前事件中的bootstrapContext
属性(也就是启动程序上下文)获取到对应的环境后处理器,
我们简单看一下如何获取环境后处理器的,进入getEnvironmentPostProcessors()
方法
从该方法中可以看到,它的实现是通过工厂模式从环境后处理器工厂获取到环境后处理器的。但是我们这一路走来,并不知道该工厂中有哪些后处理器,甚至该工厂是在什么时候实例化的都不知道。
首先我们看一下当前监听器EnvironmentPostProcessorApplicationListener
的构造方法,从构造方法中寻找答案
答案已经揭晓,环境后处理器工厂对象的实例化是在此监听器的构造方法中完成的,它通过环境后处理器工厂EnvironmentPostProcessorsFactory
的静态方法fromSpringFactories()
实例化。
另外,环境后处理器工厂EnvironmentPostProcessorsFactory
在这里的实现类使用的是ReflectionEnvironmentPostProcessorsFactory
,该实现类通过classNames
属性保存着spring.factories
文件中的所有环境后处理器的类路径,当需要从该工厂中获取环境后处理器时,该工厂通过反射获取环境后处理器的实例。
回到正题,我们需要知道根据当前事件中的bootstrapContext
属性(也就是启动程序上下文)获取到对应的环境后处理器有哪些,打断点进行代码调试,如下
由此可见,对启动环境的处理可不止是从配置文件中获取配置这么简单,springboot对环境的处理又细分为这么多种:
RandomValuePropertySourceEnvironmentPostProcessor
:在环境中添加随机数的配置信息SystemEnvironmentPropertySourceEnvironmentPostProcessor
:将环境中以保存的系统环境变量相关的属性进行替换,将原本保存环境变量的SystemEnvironmentPropertySource
实例替换成其子类OriginAwareSystemEnvironmentPropertySource
。SpringApplicationJsonEnvironmentPostProcessor
:将当前环境中已经保存的属性集合中出现的key为spring.application.json
或SPRING_APPLICATION_JSON
,value为json字符串的属性转换成map形式。CloudFoundryVcapEnvironmentPostProcessor
:与远程配置中心相关。我们可以理解为从远程配置中心读取配置ConfigDataEnvironmentPostProcessor
:与配置数据相关。该处理器专门负责读取各个位置的配置文件中的配置信息。其实在springboot中,有另一个处理器ConfigFileApplicationListener(配置文件监听器)
,两者的作用相同,但是后者被springboot打上了@Deprecated
,说明被启用了,想必从命名上来看后者是一个监听器,相比之下前者更适合。以下为后者配置文件监听器的注释// 从springboot2.4.0版本开始, // 弃用ConfigFileApplicationListener, // 使用ConfigDataEnvironmentPostProcessor Deprecated since 2.4.0 in favor of ConfigDataEnvironmentPostProcessor
接下来我们在来看ConfigDataEnvironmentPostProcessor(配置数据环境后处理器)
的postProcessEnvironment()
方法实现
从该方法的源码中,我们发现它也没做什么特别重要的事,也没有对配置文件做出什么动作。
其实就只有三件事:
- 初始化一个资源加载器。很明显,它用来加载resources目录下的配置文件资源。
- 创建一个
ConfigDataEnvironment
对象。 - 调用
ConfigDataEnvironment
对象的processAndApply()
方法。
所以,我们把目光再次转向ConfigDataEnvironment
类。该类有几个非常熟悉的常量,如下所示
从这几个常量中我们可以肯定,ConfigDataEnvironment
类就是负责读取配置文件中的配置信息的类了。应该是重中之重了吧。
所以我们应当分两步分析此类:①分析构造函数。②分析processAndApply()
方法。
构造函数
该构造方法对一大堆的属性进行了初始化(如上图所示),我们对其中两个属性的初始化做一个简单了解
binder
属性:包含环境中的所有配置信息在上图中大致介绍了该属性的作用,下面我们看一下
Binder.get()
方法的源码
从上面源码中,我们可以看到,这里的binder
对象是通过Binder中的静态方法get()
以当前环境为参数去创建Binder实例的。Binder实例中包含了当前环境中key为configurationProperties
的属性(其实就是所有的属性),以及解析以${}
为placeholder的属性的解析器。
所以,在这里我们初步知道PropertySourcesPlaceholdersResolver
是一个用来处理下图中firstName
配置的解析器
因此,binder
属性中包含了以下成分:
① 当前环境中的所有配置信息
② 处理配置文件中placeholder
的解析器
resolvers
属性:配置文件位置解析器集合在初始化该属性时,我们看到是通过调用
createConfigDataLocationResolvers()
方法完成的。从方法名也可以知道,resolvers
属性是各种配置文件位置的解析器(用来解析文件位置)。看一下该方法的实现:
下面我们就来看一下通过该方法获得的配置文件位置解析器的实例有哪些
这里我们简单介绍一下标准配置文件解析器StandardConfigDataLocationResolver
。
该解析器内部维护了两个属性:
static final String CONFIG_NAME_PROPERTY = "spring.config.name";
private static final String[] DEFAULT_CONFIG_NAMES = {
"application" };
private final List<PropertySourceLoader> propertySourceLoaders;
spring.config.name
:表示我们指定配置文件的名称。application
:默认的配置文件名称。propertySourceLoaders
:配置属性加载器。有两种加载器:PropertiesPropertySourceLoader
和YamlPropertySourceLoader
,分别从properties和xml
和yml和yaml
两种类型的配置文件中读取配置属性。
该解析器的功能就是根据传入的路径加上配置文件名称,并结合其配置属性加载器。得到确定的配置文件资源。如:传入路径classpath:/
,该解析器将返回四个对应的配置文件资源classpath:/application.peroperties
、classpath:/application.xml
、classpath:/application.yaml
和classpath:/application.yml
。
loaders
属性:配置文件解析器集合配置文件解析器通过直接创建
ConfigDataLoaders
实例完成初始化,在该类的构造方法中,与上面resolvers
属性的初始化逻辑相同,也是从META-INF/spring.factories
文件中获取配置文件解析器的集合,然后对其进行实例化。
那我们看一下配置文件加载器都有哪些实现类:
看到这里我们应该对resolvers
属性和loaders
属性之间的关系有个了解了:
① resolvers属性用来主要用来解析配置文件所在的目录位置。解析目录获取配置文件,
② loaders属性用来加载从目录中获取到的配置文件。即从配置文件中加载配置信息
③ 两种解析器与两种加载器是存在对应关系的。树形解析器对应树形加载器。标准解析器对应标准加载器。
contributors
属性:配置信息贡献者集合,每个贡献者提供不同的信息,可能是已收集的配置信息如环境变量、也可能是配置文件的信息。该属性是通过
createContributors()
方法进行初始化的。该方法将当前环境中的配置属性和指定的配置文件路径封装到ConfigDataEnvironmentContributors
对象中
由于springboot对ConfigDataEnvironmentContributors
的封装和该方法的实现逻辑过于复杂,为了弄清楚该实现,我们将对其进行细化的分析。
1、首先,第一行代码我们无需过多分析,就是获取当前启动环境中已经收集到的配置信息。如系统属性、环境变量、随机变量。这些在前面分析过了。
2、然后看DefaultPropertiesPropertySource
类的静态方法hasMatchingName()
。在遍历配置信息时将其中key为defaultProperties
的默认属性找出来,对其进行单独处理。
3、ConfigDataEnvironmentContributor
类的静态方法ofExisting()
。
通过提供静态方法ofExisting()
来创建一个ConfigDataEnvironmentContributor
类的实例,并且该实例被标记为EXISTING
。在此过程中,还会将传入的propertySource对象转为configurationPropertySource对象。
在看一下ConfigDataEnvironmentContributor
类的构造方法如下
4、通过for
循环遍历下来,经过我们的分析,不难发现该循环的目的是通过遍历propertySource数组,将该数组转化为contributor数组。
转换过程的示意图如下所示
至此,springboot将最初获取到的配置信息(如:系统属性、命令行参数等)添加到contributor中去了。
5、将配置文件基本信息添加到contributor中。
在createContributors()
方法中,还有一个很重要的逻辑,即将配置文件的基本信息添加到contributor中,配置文件的基本信息其实指的就是配置文件的路径配置。例如我们常用的classpath:/
、classpath:/config/
、file:./config/
等配置。
springboot对上面的常量定义如下:
static final String IMPORT_PROPERTY = "spring.config.import";
private static final ConfigDataLocation[] EMPTY_LOCATIONS = new ConfigDataLocation[0];
static final String ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
static final String LOCATION_PROPERTY = "spring.config.location";
static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
static {
List<ConfigDataLocation> locations = new ArrayList<>();
locations.add(ConfigDataLocation.of("optional:classpath:/"));
locations.add(ConfigDataLocation.of("optional:classpath:/config/"));
locations.add(ConfigDataLocation.of("optional:file:./"));
locations.add(ConfigDataLocation.of("optional:file:./config/"));
locations.add(ConfigDataLocation.of("optional:file:./config/*/"));
DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
}
在上面截图的方法中,我们看到,getInitialImportContributors()
方法调用了三次bindLocations()
方法,并将每一次bindLocations()
方法的返回值添加到initialContributors
集合中。
我们通过断点查看这三个bindLocations()
方法的调用返回给我们什么东西
第一次调用该方法的结果如下所示。
我们将变量进行替换后,该方法的调用其实是下面这样的
bindLocations(binder, "spring.config.import", new ConfigDataLocation[0])
所以我们猜测它的目的就是将spring.config.import
配置的值转化为ConfigDataLocation
类型的数组。而我们的演示没有对其进行配置,所以使用new ConfigDataLocation[0]
作为兜底进行返回,得到的结果是一个ConfigDataLocation
类型的空数组。
第二次调用该方法的结果如下所示。
我们将变量进行替换后,该方法的调用其实是下面这样的
bindLocations(binder, "spring.config.additional-location", new ConfigDataLocation[0])
所以我们猜测它的目的就是将spring.config.additional-location
配置的值转化为ConfigDataLocation
类型的数组。而我们的演示没有对其进行配置,所以使用new ConfigDataLocation[0]
作为兜底进行返回,得到的结果同样也是一个ConfigDataLocation
类型的空数组。
第三次调用该方法的结果如下所示。
我们将变量进行替换后,该方法的调用其实是下面这样的
bindLocations(binder, "spring.config.location", DEFAULT_SEARCH_LOCATIONS);
//DEFAULT_SEARCH_LOCATIONS表示默认查找位置,如果没有配置spring.config.location,就使用 DEFAULT_SEARCH_LOCATIONS 作为兜底
通过上面三次对bindLocations()
方法的调用,我们得到了三个ConfigDataLocation
类型的数组,然后在将这三个数组逐个添加到contributor集合中
前面我们分析过,将系统变量封装为contributor实例时是通过ConfigDataEnvironmentContributor
类的静态方法ofExisting()
标记为EXISTING
的,即表示已存在的属性。那么在这里针对配置文件路径的contributor,则是通过另一个静态方法ofInitialImport()
标记为INITIAL_IMPORT
的,即表示初始导入的属性,它只能表示配置文件的位置,我们后面还需要通过该位置去找到对应的配置文件并读取其中的配置。
所以,又分析了这么大一堆,我们明白了getInitialImportContributors()
方法的作用,就是将配置文件的位置转换成对应的contributor集合。如下图所示
再回到createContributors()
方法中,该方法将我们根据系统变量转化的contributor集合和根据配置文件位置转化的contributor集合进行合并
现在我们得到的contributor集合如下所示
6、在得到contributor集合后,springboot还对该集合进行再次封装,将该集合封装到ConfigDataEnvironmentContributors
对象中,并通过root
和children
将其封装成一个树形的结构。
首先我们看到在静态方法of()
中,将枚举BEFORE_PROFILE_ACTIVATION
作为key,参数contributor集合作为value封装到一个map对象中,然后将该map对象作为children
属性传递到ConfigDataEnvironmentContributor()
构造方法中,此时得到了一个新的被标记为ROOT
的contributor对象中。该对象的结构如下所示
然后将该contributor对象作为root
属性保存到ConfigDataEnvironmentContributors
对象中,在这里注意区分两个类的区别:
- ConfigDataEnvironmentContributor:保存配置信息或配置文件位置的contributor类
- ConfigDataEnvironmentContributors:保存ConfigDataEnvironmentContributor的集合
此时ConfigDataEnvironmentContributors
对象的结构如下所示
综上所述,我们对ConfigDataEnvironment
的构造方法做一个小总结
1、binder
属性:当前环境中的所有配置信息, 处理配置文件中placeholder
的解析器。
2、resolvers
属性:配置文件位置解析器集合。
3、loaders
属性:配置文件解析器集合。
4、contributors
属性:保存了profile生效前(BEFORE_PROFILE_ACTIVATION)的属性配置(如:系统属性、环境变量、配置文件位置)。
processAndApply()
方法
该方法是从配置文件中读取配置的核心方法,在前面环境后处理器部分中,调用的就是ConfigDataEnvironment
的processAndApply()
方法。
void processAndApply() {
// 创建配置数据导入器,该导入器中的加载器loaders用来读取配置文件中的数据,然后由导入器将数据保存到contributors中
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
this.loaders);
registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);
// 加载默认指定配置目录中的文件名为application的配置文件中的配置信息,保存到当前contributor的children中,并标记为bound_import
ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
// 创建profile上下文,用来保存当前项目激活的profile
ConfigDataActivationContext activationContext = createActivationContext(
contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
// 在不考虑profile的情况下,将第二步未处理的contributor进行处理,同样保存到contributor的children中,并标记为bound_import
contributors = processWithoutProfiles(contributors, importer, activationContext);
// 确定当前激活的profile,并保存到profile上下文中
activationContext = withProfiles(contributors, activationContext);
// 根据已确定的profile,加载默认指定配置目录中对应当前profile的配置文件中的配置信息,保存到当前contributor的children中,并标记为bound_import
contributors = processWithProfiles(contributors, importer, activationContext);
// 将contributor中全部标记为bound_import的配置属性保存到运行环境environment中
applyToEnvironment(contributors, activationContext);
}
在该方法中,主要有以下部分逻辑,
- 从默认指定的配置文件中读取配置信息并保存到contributors中。通过调用
processInitial()
方法完成。
默认的配置文件路径前面ConfigDataEnvironment
类中已经介绍过,分别是:classpath:/
、classpath:/config/
、file:./
、file:./config/
、file:./config/*/
。以此5个默认的路径、默认的配置文件名称application
和默认的配置文件格式properties、xml、yaml、yml
为参数,分别获取其对应的配置文件资源。并从中读取配置属性。
然后将读取到的配置属性封装为contributor对象并标记为BEFORE_PROFILE_ACTIVATION
,再将该contributor对象保存到其父contributor对象
的children集合中。
以下图的contributors
的结构为例:当处理右边第一个以optional:classpath:/
为属性的contributor对象时,我们发现该对象的children属性为空,此时从classpath:/
路径下以application
命名的配置文件中读取配置并将配置信息封装为contributor对象,再将该对象标记为BEFORE_PROFILE_ACTIVATION
并保存到上一层的children属性中。
由此看出,springboot非常巧妙地利用父子层级的关系来分别表示以配置文件路径为属性的contributor和封装着从配置文件中获取的配置信息的contributor。
初始化环境激活上下文即ConfigDataActivationContext。通过调用
createActivationContext()
方法完成。在忽略环境激活上下文的情况下处理当前已收集的contributors。通过调用
processWithoutProfiles()
方法完成。确定当前运行环境激活的profile。通过调用
withProfiles()
方法完成。在前面springboot已经把所有位置上以
application
为名称的配置文件中的配置属性读取并保存到contributors
中了。此时只需要再从中获取spring.profiles.active
对应的属性,并将获取到的profiles属性保存到环境激活上下文中。根据已确定的环境激活上下文读取对应的配置信息到contributors中。通过调用
processWithProfiles()
方法完成。再一次根据默认的配置文件路径
classpath:/
、classpath:/config/
、file:./
、file:./config/
、file:./config/*/
。以5个默认的路径、默认的配置文件名称application
后拼接-
和激活的profiles,以及默认的配置文件格式properties、xml、yaml、yml
为参数,再次分别获取其对应的配置文件资源。并从中读取配置属性。例如默认配置文件中spring.profiles.active = dev
,则此时再次从application-dev.yml
中读取配置。然后将读取到的配置属性封装为contributor对象并标记为
AFTER_PROFILE_ACTIVATION
(因为该对象是根据激活的profiles得到的),再将该contributor对象保存到其父contributor对象
的children集合中。最后再以父子层级的关系将该contributor对象保存到其父级的children属性中。
将contributors中保存的所有配置属性应用到当前运行环境
Environment
中。
纸上得来终觉浅,绝知此事要躬行。
————————我是万万岁,我们下期再见————————