上一节,我们主要了解了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 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是通过一个方法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设置其他的一些属性,比如转换器、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下的相关配置文件对象