SpringBoot-启动过程源码解析
SpringBoot VERSION:2.1.x 这里使用的是Spring Boot 官方自带的Spring Boot Sample - SampleTomcatApplication。
启动前先设置参数:
启动类被 @SpringBootApplication注解修饰。跟踪run方法,我们可以看到跳转进入了SpringApplication类中:
继续往下:
新建SpringApplication实例
可以看到,new了一个SpringApplication出来,看下SpringApplication的构造函数:
1.设置primarySources,用于存储配置类
可以看到,这里就是将启动类的Class对象塞了进去。
2.根据类路径推断容器类型:
ClassUtils.isPresent方法中首先校验是否为基本类型、常用类型,如果是则返回;否则根据前后缀名校验是否为Array,不是则调用 Class.forName 尝试加载指定名称的类。
3.设置应用上下文初始器
尝试获取线程上下文类加载器,如果没获取到则依次获取获取当前类类加载器、系统类加载器。
通过加载META-INF/spring.factories配置文件获取到制定类的实现类类名,然后通过反射实例化对象。由于所有的对象都实现了ordered接口,所以可以将对象排序后返回。
4.设置应用监听器:
过程与3类似,也是通过加载META-INF/spring.factories配置文件获取到制定类的实现类类名,然后通过反射实例化对象。
5.推断main class
调用run方法
run总体流程如下图所示:
启动StopWatch用于记录SpringBoot启动时间,开启headless模式。
Headless 模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标。 Headless 模式虽然不是我们愿意见到的,但事实上我们却常常需要在该模式下工作,尤其是服务器端程序开发者。因为服务器(如提供 Web 服务的主机)往往可能缺少前述设备,但又需要使用他们提供的功能,生成相应的数据,以提供给客户端(如浏览器所在的配有相关的显示设备、键盘和鼠标的主机)。 简单地说就是有些功能需要硬件设备协助,比如显卡,但如果是服务器可能都没装显卡,这时就需要 JDK 调用自身的库文件去摸拟显卡的功能。
获取SpringApplicationRunListener
从Spring.factories中获取到 SpringApplicationRunListener的实现类名称并生成类实例。SpringApplicationRunListener只有一个默认的继承类EventPublishingRunListener:
该类获取所有的ApplicationListener的实现类并塞入初始广播器中,紧接着广播 应用启动 事件。
准备环境
下面针对重要步骤进行剖析:
创建环境
这里使用了上面创建SpringApplication时推断出的webApplicationType,默认内置容器Tomcat,所以创建了标准SERVLET环境,同时由于 StandardServletEnvironment是AbstractEnvironment的继承类,所以无参构造函数使用了AbstractEnvironment的逻辑,该方法中写入了属性源(存储了大量的property):
这里的SERVLET配置初始属性、SERVLET上下文初始属性都使用了StubPropertySource来进行占位(StubPropertySource类是个占位符类,因为在一开始不能真正完成所有配置的初始化,但是配置信息是有优先级的,所以先用存根来充当一个占位符定义好默认顺序,等容器refresh的时候再进行替换)。systemProperties指JVM系统配置,启动时我们配置了JVM启动项,写入了:
-Dvmoption=test复制代码
配置环境
获取转换服务
获取转换服务共享实例。
配置属性源
获取环境的属性源,这里获取到的就是上面创建环境一步中塞入的属性源。然后尝试写入默认属性,这里由于我们没有配置所以跳过。
接着尝试获取命令行属性,由于我们在启动的时候已经配置了命令行属性,所以这里会写入命令行属性:
--commandenvionmenttest_1=test_1 --commandenvionmenttest_2=test_2复制代码
首先会判断是否已经包含命令行参数属性源。如果已经包含那么会创建一个复合属性源,将获取到的属性源和命令行参数组成的新属性源一同放入到复合属性源的属性源集合(propertySources)中,然后再用复合属性源替换原先的属性源。(PS:这里为什么要判断是否已包含,何种场景下会已有该属性源还不是很清楚);否则直接将命令行属性源写入属性源集合最前面。
配置生效文件环境
synchronized同步获取activeProfiles,将additionalProfiles和生效activeProfiles写入到环境activeProfiles中,这里为了防止属性冲突将additionalProfiles放在了环境的activeProfiles最前面,后者将会生效。
环境属性源适配迭代器
这里没什么好说的,就是将环境的属性源集塞入了自身中,以configurationProperties为key。这边的remove方法用于刷新自身的configurationProperties。
广播 准备环境 事件
调用上文获取到的 SpringApplicationRunListeners广播事件,后面再详细展开。
绑定环境
这里的方法名以及方法名的注释有点歧义,从理解的源码来看,这里就是将环境中的spring.main开头的属性写入了应用中。当我们没有写入spring.main开头的属性时Binder其实没有起任何作用。在项目的application.property中写入spring.main开头的属性来验证下:
# banner图输出到控制台中spring.main.banner-mode=off# 是否允许通过注册与现有定义同名的定义来覆盖bean定义spring.main.allow-bean-definition-overriding=false复制代码
首先会去获取key为spring.main的属性,没有找到直接返回null。然后将spring.main开头的属性写入到SpringApplication中:
重新attach
这里会先对将环境转换为标准环境,然后再次attach。这样做的原因是因为在「环境准备」事件广播后一些监听器将属性源写入了环境中,所以这里需要再次attach。
创建应用上下文
前面在实例化SpringApplication的时候推断了web应用类型为SERVLET,所以这里实例化了 AnnotationConfigServletWebServerApplicationContext。AnnotationConfigServletWebServerApplicationContext在ServletWebServerApplicationContext基础上实现了AnnotationConfigRegistry。AnnotationConfigRegistry有两个方法,register和scan。AnnotationConfigServletWebServerApplicationContext通过实现这两个方法,从而可以通过注解的方式注入类。具体的过程我们后续文章再分析,这里只需要知道创建了上下文。
准备应用上下文
设置环境
将环境配置到上下文中,同时也设置了读取器和扫描器。
上下文后置处理
加载 类名生成器、资源加载器、类加载器和转换服务
应用上下文初始化器
还记得初始化器是从哪里获取的吗?没错,这里的上下文初始化器就是在上文实例化SpringApplication时通过spring.factories反射获得的。
这里获取初始化器并调用其initialize方法处理context,具体的后续再研究。
广播上下文准备事件
后面详细展开。
加载类&&加载配置
加载指定的单例类,加载sources。
广播上下文已加载事件
后面详细展开。
刷新应用上下文-核心
这里使用 AbstractApplicationContext.refresh方法,大概逻辑是通过工厂加载类以及一些类加载前后的处理与监听,具体内容后续在Spring文章中再具体深入。接下来会注册关机hook,用于优雅关机,关于优雅关机后面再研究。
刷新后处理动作
这里在新版本的SpringBoot中没有任何代码,而在老版本代码中调用了callRunners。
广播 应用已启动 事件
callRunners
调用 CommandLineRunner 和 ApplicationRunner 实现类的 run 方法。
广播 应用已运行 事件
至此,SpringBoot启动完成。
收获
扩展点
可以看到,在SpringBoot启动过程中,预留了许多的扩展点,开发者通过实现的对应的类和方法,即可在SpringBoot启动过程中实现自己需要的效果,这也是我们日常开发中所要考虑的:对修改闭合,对扩展开放。
spring.factories
在SpringBoot启动过程中,有多个地方通过spring.factories来实现接口的实现类的实例化。这种方式其实是SPI机制的一种应用,在日常开发中,我们可以应用这种机制实现插件式的开发,做到热插拔。具体关于SPI机制,后续再研究。
总结
大体流程如下:
实例化SpringApplication。
获取监听器。
准备环境。
创建应用上下文。
刷新应用上下文。
刷新后处理动作。
结语
以上我们大致的梳理了一遍SpringBoot的启动流程,许多细节以及延展开来的类、方法和设计并没有具体去剖析。后续在SpringBoot系列文章中我们继续来探索。