SpringBoot成长记4:Run方法中配置文件的处理
上一节,我们主要了解了SpringBoot的一个扩展点设计SpringApplicationRunListeners。并没有找到我们想要找到的Spring容器创建和web容器启动、自动装配配置的这些核心功能。
之前我们说过,xxxxEnvironment表示了配置文件的封装,这一节就让我们来看下,SpringBoot启动过程中,如何通过处理配置文件,设置到Environment对象中的。
我们接着往下继续分析run方法,会看到如下代码:
public ConfigurableApplicationContext run(String... args) {
//1、扩展点 SpringApplicationRunListeners listeners.starting();
//2、配置文件的处理(待分析)
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
//其他逻辑
}
我们还是抓大放小,核心关注这一段逻辑,明显是反复出现了environment这个词汇相关的代码,肯定就是这里处理的配置文件。
这段代码主要的逻辑可以概括如下:
1)DefaultApplicationArguments命令行参数的解析和封装,也不是我们关注的重点
2)prepareEnvironment这个方法应该就是真正配置文件的处理逻辑
3)后面这两个方法,configureIgnoreBeanInfo、printBanner,明细就是基于配置设置一个参数、打印一下Banner日志输出而已,一看就不是重点。
即如下图所示:
通过上面的初步分析,我们可以看到,核心关注的应该是prepareEnvironment这个方法的逻辑,那接下来就让我们看看它是如何处理的配置文件吧。
SpringBoot的配置文件解析如何设计的?
在分析SpringBoot prepareEnvironment方法是如何处理配置文件之前,你可以思考下,如果让你编写配置文件的解析,你会怎么考虑呢?你可能会考虑:
从哪里找到配置文件,之后根据文件格式解析下配置文件,那每个配置文件我都可以抽象为一个对象,对象中可以有配置文件的名称,位置,具体配置值等等。最后配置文件可能是多个 ,需要一个集合来存放这些对象。构成一个列表,表示多个配置文件解析后的结果。
这个思路其实你稍微思考下,就可以得出。
那么SpringBoot其实也没有什么高深的,它大体也是这个思路,你有这个思路,再去理解代码,其实就会轻松很多。这个思想是我想要教给你们的,对理解代码会非常有用。
让我们来一起看下代码:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
这个方法的脉络,大体就是:
1)创建了一个ConfigurableEnvironment
2)之后不断往里面设置或者放了一些值,很可能就是各种配置文件的解析后的结果。configureEnvironment、attach之类的。
3)中间还执行了关键的listeners的扩展点的一个方法—environmentPrepared()
整体如下图所示:
上面的逻辑看着还是比较多的,但是核心就是创建了ConfigurableEnvironment,之后给它设置了一堆属性而已。
创建的ConfigurableEnvironment配置对象抽象了哪些东西?
ConfigurableEnvironment是通过一个方法getOrCreateEnvironment()创建得来的。
你可以打开接口看下它的脉络,而ConfigurableEnvironment这个类时一个接口,定义了一些和配置相关方法。
profile表示多环境配置看,MutablePropertySources是个很关键的对象,内部包含了一个List对象,这个你可以猜想到它就是对配置文件的抽象对象,每一个PropertySource表示一个配置文件。其余的就是一些get方法了,整体类图如下所示:
了解了这个接口,你大体应该有个印象了, 配置文件抽象的关键点,就是PropertySource+profile封装到ConfigurableEnvironment这个接口实现类中。
这个其实跟我们之前提到过的,让你自己设计配置文件的解析的思想很类似的。你可以思考下这里的亮点:
1)PropertySource name可以用作配置文件名称,T source属性是泛型,可以放常见的key-value的properties和yml的配置文件解析结果,此时T就是一个Map,也可以放其他配置格式解析成的对象,这里没有限制的。这点设计非常好。
2)通过List统一管理多个配置文件,并且通过MutablePropertySources封装对这个集合的常见操作。
3)至于profile的设计,就是支持多环境配置,只要通过一个Set profile集合就可以实现,这个思路其实很简单。
创建时候默认添加了那些配置文件的解析?
当你知道了上面的设计思想,那么理解代码就不困难了。首先是创建Environment的代码:
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}
根据webApplicationType应用类型,之前创建SpringApplication时推断过,默认是SERVLET。所以这里创建了一个实现类是StandardServletEnvironment。
而这个实现类,默认加载了几个配置。虽然默认构造方法为空,但是父类的构造方法调用了customizePropertySources,实际还是初始化了一些配置文件。代码如下:
public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";
public StandardServletEnvironment() {
}
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new StubPropertySource("servletConfigInitParams"));
propertySources.addLast(new StubPropertySource("servletContextInitParams"));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource("jndiProperties"));
}
super.customizePropertySources(propertySources);
}
public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
WebApplicationContextUtils.initServletPropertySources(this.getPropertySources(), servletContext, servletConfig);
}
}
public class StandardEnvironment extends AbstractEnvironment {
/** System environment property source name: {@value}. */
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
/** JVM system properties property source name: {@value}. */
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
}
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
//省略其他
public AbstractEnvironment() {
customizePropertySources(this.propertySources);
}
}
可以看出来默认,创建的时候调用了父类的构造函数,配置文件主要添加了如下:
1)servletConfigInitParams、servletContextInitParams,从名字上看,我们可以连蒙带猜下,stub应该是表示是桩,可能是预留存储servlet相关配置用的,默认都是空的值。jndiProperties配置根据条件加载,默认没有加载。它配置的值默认是空的Object()
2)systemEnvironment和systemProperties配置,从名字可以猜出来,是系统属性和系统环境变量相关配置,配置的值解析后为Map
最终将所有的配置解析好放入到了List中。
也就是执行完创建,此时已经相当于构建了4个配置对象了,也就是4个PropertySource了。如下图所示:
你可能有一个疑惑,这些不同的配置是怎么来的?
其实,你细心思考下,你会发现,这些配置有的是自己构建的,有的是从系统环境变量加载的,有的是从配置文件读取的。
通过抽象了一个接口PropertySource。定义不同的实现,来实现配置文件的解析,这个设计思想值得我们学习,这就是经典的java面向接口的多态编程思想。
目前为止配置文件处理创建就完成了:
ConfigurableEnvironment其他的添砖加瓦操作
创建了ConfigurableEnvironment之后,执行了其他的一些方法。核心思路主要是给ConfigurableEnvironment设置其他的一些属性,比如转换器、profile,在增加一些PropertySource而已。
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
//1.创建Environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
//2.ConfigurableEnvironment其他的添砖加瓦操作
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
上面代码的这些添砖加瓦的我就不仔细介绍了,主要就是设置一些属性,或者添加了配置文件对象。直接给大家画一个图概括下就好了,主要逻辑如下图红色箭头所示:
通过上图,可以看到,主要执行的逻辑,具体如下:
1)configureEnvironment
a) environment.setConversionService 设置(默认转换器Service) ApplicationConversionService 单例初始化,不是重点;
b) configurePropertySources() 命令行参数的覆盖一些属性,默认不传,什么都不做,不是重点;
c) configureProfiles() 判断是否有 spring.acitve.profiles,默认没有指定 ,设置activeprifles(List)就是空。这个值得了解下,启动时的参数如果增加了这个,在这里就会生效的,如spring.acitve.profiles=dev,prod,这里会记录下。activeprifles=dev,prod。(作用的话,其实是用于之后会额外补充加载配置文件,也就是补充PropertySource,这个一会儿在listeners.environmentPrepared分析中会看到的。)
2)ConfigurationPropertySources.attach(environment) 添加 一套配置配置文件到List开头,也就是默认添加了一个configurationProperties名字的PropertySource,或者说添加了一个Environment,因为Environment封装了PropertySource,这个配置从注释上看,可能是为了动态的添加和配置一些值用的,知道attach是补充了一个配置文件的对象封装就可以了,其他的暂时不是重点;
3)listeners.environmentPrepared(environment),事件发布Listener 发布一个环境事件 ApplicationEnvironmentPreparedEvent。配置文件的处理的扩展点核心做了些什么?按事件筛选过的listener,轮流进行一堆事件处理(和之前发布ApplicationStartEvent事件一样的机制),重要逻辑,补充了配置文件。
ConfigFileApplicationListener加载配置的核心处理,很关键,通过Loader,propertySourceLoaders 解析property(properties,xml) yaml文件的(yml,yaml)从而补充了新的PropertySource。
AnsiOutputApplicationListener ANSI字符编码处理
LoggingApplicationListener 日志相关
ClasspathLoggingApplicationListener 什么都没做
Backgroundpreinitializer 什么都没做
DelegatingApplicationListener 什么都没做
FileEncodingApplicationListener 编码相关,不重要
4)bindToSpringApplication(environment) 设置environment到SpringApplication (但是实际没有设置上…比较奇怪,不重要的逻辑,不深究了),不是重点;
5)EnvironmentConverter 如果environment不是webApplicationType指定环境配置,这里转换成StandardServletEnvironment(默认SERVLET) 之前已经是,所以之类不转换了,不是重点;
6)ConfigurationPropertySources.attach(environment) 再次attach 担心转换后,这个属性不在了,不是重点;
通过上面的图和说明,基本解释了ConfigurableEnvironment的各种添砖加瓦的操作。其实如果你抓大放小其实这些都不是算是关键,真正的关键就是创建和封装了ConfigurableEnvironment,另一个比较重要的就是执行扩展点了。也就是执行listeners.environmentPrepared(environment),补充了Spring在ClassPath下的相关配置文件对象
小结
今天我们主要分析了SpringBoot在启动过程中:
1)配置文件对象的抽象封装如何设计的
2)listeners扩展点,对配置文件的处理做了扩展,补充了Spring在ClassPath下的相关配置文件对象
本文由博客群发一文多发等运营工具平台 OpenWrite 发布