前言
最深刻了解一个框架的思想的方式,莫过于看源码,本系列旨在于从Springboot底层源码(Version - 2.6.6)出发,一步步了解springboot是如何运行起来的。
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
首先我们从run方法开始逐步深入其中。
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start(); // 采用计时器计时
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
//根据启动参数获取相应的监听器并启动监听,此处可以通过spring.factories对spring的启动过程进行增强。
//通过实现自定义的Listener可以在spring的不同启动时间实现自定义逻辑
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();
Collection exceptionReporters;
try {
//创建默认的参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// step 1 准备相应的启动环境
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
......
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}
......
}
可以看到,spring的run方法里面还是颇为复杂的,对于前述的一些准备工作如:定时器的启动、监听器的监听等,限于篇幅原因,这里就不展开描述了,本期我们主要介绍step 1对应的内容,了解spring是如何对启动的环境进行配置和准备的,我们日常输入的命令行参数又是如何被装载的。
主要内容
prepareEnvironment
准备环境信息整体的源代码如下:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// 这里会根据当前的web服务器状态判断启动的环境类型,1、服务型;2、响应型
ConfigurableEnvironment environment = getOrCreateEnvironment();
//紧接着Spring会去配置当前的环境信息。主要是设置启动参数。
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = convertEnvironment(environment);
}
ConfigurationPropertySources.attach(environment);
return environment;
}
可以看到第一步Springboot获取了一个环境变量ConfigurableEnvironment,这里其实内部根据当前服务的不同状态会返回不同的类型,如servlet类型、reactive类型。获取了环境变量后,紧接着会去配置相应的环境信息。方法内容采用了模版模式进行约束。
(有关模版模式的内容,我在之前的文章有聊到哦~链接)
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
environment.setConversionService(new ApplicationConversionService());
}
configurePropertySources(environment, args);
configureProfiles(environment, args);
//空方法 按照doc描述,原本这应是通过配置spring.profiles.active来控制环境生效的profile,但是可能因为某种原因被废弃了。
}
对于这整个模版方法configureEnvironment中最关键的方法其实是configurePropertySources方法:
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
// 获取到环境的整个配置信息
MutablePropertySources sources = environment.getPropertySources();
if (!CollectionUtils.isEmpty(this.defaultProperties)) {
//添加环境默认配置信息
DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources);
}
//如果当前有启动配置参数则同environment的原有配置进行融合。
if (this.addCommandLineProperties && args.length > 0) {
//常量值为“commandLineArgs”
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(
new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
}else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
这里先介绍一下MutablePropertySources,这个变量其实是springBoot用于保存配置信息的主要变量。其大致的设计如下所示:
其中MutablePropertySources保存的是一个List<PropertySource>,该对象里面包含多个不同类型的配置信息,有用于servlet配置初始化的、servlet上下文初始化的以及系统本身启动环境的配置参数信息。每一个PropertySource其实又等同与一个map结构,对应每个配置名字及配置内容信息。一些配置信息如下图所示:
了解了MutablePropertySources后,其实能够大致明白configurePropertySources方法最本质的目的,是将当前环境的配置信息,同启动命令行的配置信息进行融合,并将启动命令行排序在最前面,从而保证命令行参数的优先级最高。
执行完configureEnvironment这个模版方法后,Springboot接下来会执行attach方法,具体源代码罗列如下:
public static void attach(Environment environment) {
Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
//获取出所有的配置信息
MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
//这里会直接获取“configurationProperties”对应的变量信息,初次获取都会是null
PropertySource<?> attached = getAttached(sources);
if (attached == null || !isUsingSources(attached, sources)) {
//新建一个ConfigurationPropertySourcesPropertySource类型的变量
attached = new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
new SpringConfigurationPropertySources(sources));
}
//将attached变量添加到sources的变量中
sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
sources.addFirst(attached);
}
可以看到,attach方法最主要的作用,其实是将前述提到的各个不同的ConfigurableEnvironment的配置信息MutablePropertySources都转换成了ConfigurationPropertySourcesPropertySource的形式,从而允许后面的方法能够对其进行统一调用。本质上来说,是起到一个类似于适配器转换的作用。
在执行完attach方法之后,Springboot接下来会执行下面的这行代码:
listeners.environmentPrepared(bootstrapContext, environment);
见名知意,这里很明显就是要对Springboot自带的的listener下发某些命令。追进源码查看,可以看到是调用了doWithListener方法。
private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction,
Consumer<StartupStep> stepAction) {
//首先根据步骤类型,创建出一个StartupStep 对象,该对象主要用于记录该任务分配给各监听器后的执行状态、执行时间等
StartupStep step = this.applicationStartup.start(stepName);
//让每个监听器都执行相应的方法。
this.listeners.forEach(listenerAction);
if (stepAction != null) {
stepAction.accept(step);
}
// end部分收集数据
step.end();
}
doWithListener方法这里执行的主要功能是首先根据任务的名称创建一个用于记录任务状态、执行时间等数据的对象。随后,调用其所管理的listeners执行listenerAction的动作。这里的listenerAction对应的具体源代码如下:
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment) {
this.initialMulticaster.multicastEvent(
new ApplicationEnvironmentPreparedEvent(bootstrapContext,
this.application, this.args, environment));
}
本质上这个listenerAction是会用initialMulticaster的发布多事件的方法,遍历对应Application下的监听器,逐个调用进行处理。initialMulticaster中对应的listener罗列如下所示:
由于我们发送的事件类型是ApplicationEnvironmentPreparedEvent,因此AnsiOutputApplicationListener、LoggerApplicationListener等监听器就会被激活然后进行相应处理,会往整个环境中去添加一些配置信息。但是具体每个监听器处理了什么内容,限于篇幅原因,这里就不展开了。
总之,从中能够得到的一个设计思想就是,根据不同的事件类型,往不同的监听器中去投放消息,从而达到异步解耦合的目的。
DefaultPropertiesPropertySource.moveToEnd(environment);
在往监听者发送完消息之后,SpringBoot还会执行一次moveToEnd的代码,这里的逻辑比较简单,就是将default对应的Source配置信息移动到最后,确保优先级顺序。紧接着,Spring执行了下面的代码:
bindToSpringApplication(environment);
//上面一句对应的关键源码
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
见名知意,就是将当前的环境捆绑到SpringApplication上。(捆绑,怪羞耻的)
这部分主要分为两个部分,1、get部分;2、bind部分。我们首先来看get部分。
public static Binder get(Environment environment, BindHandler defaultBindHandler) {
//get这里,其实拿到的就是我们之前在attached部分的代码塞进的配置信息source.
Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(environment);
//随后,根据环境会初始化相应的占位符解析器,会定义默认的占位符格式为"${}"
PropertySourcesPlaceholdersResolver placeholdersResolver = new PropertySourcesPlaceholdersResolver(environment);
//依据这些生成相应的Binder
return new Binder(sources, placeholdersResolver, null, null, defaultBindHandler);
}
从源码和注释中不难了解到,get部分主要是将相应的配置信息、占位符解析器保存到相应的Binder对象中。从另一方面看,我们也从这里也对应到在我们配置文件中的**"${?}"**占位符的解析是如何实现的。
紧接着,我们对另一部分bind进行分析,其关键的底层源码如下所示:
private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,
boolean allowRecursiveBinding, boolean create) {
try {
//这里获取的replacementTarget其实就是SpringApplication
Bindable<T> replacementTarget = handler.onStart(name, target, context);
if (replacementTarget == null) {
//不为空,会往下走
return handleBindResult(name, target, handler, context, null, create);
}
target = replacementTarget;
//绑定处 - 绑定相应对象的属性信息和数据信息 初始化绑定Application时都会是null
Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
//绑定结果也会返回null
return handleBindResult(name, target, handler, context, bound, create);
}
catch (Exception ex) {
return handleBindError(name, target, handler, context, ex);
}
}
根据Debug的运行显示,Bind这个部分内容,基本没有对Application或Binder做什么实质性的动作。其实看下来,这个方法本身应该是针对容器的对象bean进行数据绑定和属性绑定,从其对应的数据绑定器可以看出一二。大胆预测一下,后面的bean的加载和初始化的某些阶段,可能也会涉及到这个方法的调用。
因此归根结底,**bindToSpringApplication(environment);**这个方法只是针对Application创建了一个占位符和Binder对象。
ConfigurationPropertySources.attach(environment);
最后,Spring代码又执行了一次attach,这里这样做的原因是因为在调用监听器方法的时候,部分监听器会往环境中添加相应配置信息,为了保持configurationProperties配置数据的优先级,因此再次调用的attach方法。
总结
总结一下,SpringBoot对于环境装配主要的大步骤分为以下8个:
1、创建environment变量,同时根据父类的构造函数会从系统、配置文件中相应去加载数据。
2、将前序我们读取到的配置信息,按照优先级顺序依次塞到MutablePropertySources中。
3、采用attach方法,将MutablePropertySources转成标准的ConfigurationPropertySourcesPropertySource,供后续的方法调用。
4、调用EventPublishingRunListener的multicastEvent方法,将ApplicationEnvironmentPreparedEvent发送到对应的监听器中,并往environment中添加一些配置信息。
5、将DefaultPropertiesPropertySource移动到末尾。
6、对SpringbootApplication生成相应的Binder以及占位符解析器。
7、判断当前是否为自定义的environment,如果是则进行转换。
8、再次调用attach方法,保证配置信息的优先级。(避免因为第4步中加入的配置信息影响了优先级顺序。)
回头来看,Spring环境装配这部分内容其实要做的东西相对明确,就是从各个地方(操作系统、JVM、配置文件或监听器)去获取相应的配置信息。Springboot将这些方式收集到的配置信息统一保存起来并封装好,根据需求生成对应的configureEnvironment。
但在这其中,涉及到的设计模式其实也很多,比如监听器模式、模版模式。一些通用的地方其实还用了工厂模式。通过借鉴和学习Spring对于设计模式的使用,想必对后续自己的设计和程序优化有很大的帮助。