0.
0.0. 历史文章整理
玩转 Spring Boot 入门篇
玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)
玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)
玩转 Spring Boot 集成篇(Redis)
玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)
玩转 Spring Boot 集成篇(RabbitMQ)
玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)
玩转 Spring Boot 集成篇(任务动态管理代码篇)
玩转 Spring Boot 集成篇(定时任务框架Quartz)
玩转 Spring Boot 原理篇(源码环境搭建)
玩转 Spring Boot 原理篇(核心注解知多少)
玩转 Spring Boot 原理篇(自动装配前凑之自定义Starter)
玩转 Spring Boot 原理篇(自动装配源码剖析)
0.1. Spring Boot 启动流程简图
通过读 Spring Boot 启动流程的源码,大体勾勒了上面一个简易的流程图,通过此图能够看出 Spring Boot 的启动生命周期以及事件贯穿其中,所以搞定了生命周期以及事件,那么Spring Boot 启动流程也就理解个八九不离十了。
在正式开始读 Spring Boot 启动流程源码之前,先大体了解 Spring Boot 生命周期以及相应的事件。
- starting: 开始启动中
- environmentPrepared:环境已备好
- contextPrepared:准备应用上下文、实例化
- contextLoaded:上下文准备好
- started:应用启动成功
- ready:应用已准备好,可以处理接收请求
- failed:启动过程遇到异常,启动失败
SpringApplicationRunListeners 类定义了 Spring Boot 启动时的生命周期,而在每个生命周期节点会广播相应的事件。
其内部广播事件实际上是 SpringApplicationRunListener 接口对应的实现类支持,而该接口只有一个实现类 EventPublishingRunListener。
1. Spring Boot 源码剖析
万物之始,大道至简。
当应用开始执行时,会调用 SpringApplication.run 方法开始 Spring Boot 启动之旅。
在 SpringApplication.run 方法内部,接着会调用 SpringApplication 重载的 run 方法,最终会调用 SpringApplication 的构造方法创建事例,并调用 run(args) 方法来完成真正的启动操作。
为了清晰撸码,最初级的读源码方式,便是尝试在源代码上加点注释,采用控制台打印关键步骤的方式剖析源码。
代码语言:javascript复制public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
System.out.println("【Spring Boot 源码剖析】-【SpringApplication 构造方法】- 进入");
this.resourceLoader = resourceLoader;
System.out.println("初始化资源加载器 resourceLoader,默认为值为:" resourceLoader);
System.out.println("判断主要加载资源类 primarySources:" primarySources " 是否为空,若为空则抛出 IllegalArgumentException 异常");
Assert.notNull(primarySources, "PrimarySources must not be null");
System.out.println("初始化主要加载资源类合并去重");
System.out.println("t 实现方式:通过数组转换成 List,然后构造成LinkedHashSet,完成去重操作");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
System.out.println("初始化主要加载资源类合并去重后结果为:" primarySources);
System.out.println("推断当前 WEB 应用类型,定义共有三种:SERVLET(servlet web 项目)、REACTIVE(响应式 web 项目)、NONE(非 web 项目)");
this.webApplicationType = WebApplicationType.deduceFromClasspath();
System.out.println("当前 WEB 应用类型为:" webApplicationType);
System.out.println("初始化引导程序注册的初始化器");
System.out.println("t 从 META-INF/spring.factories 读取 BootstrapRegistryInitializer 类的实例名称集合并去重");
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
System.out.println("t 引导程序注册的初始化器初始化后的值为:" this.bootstrapRegistryInitializers);
System.out.println("设置应用上下文初始化器");
System.out.println("t 从 META-INF/spring.factories 读取 ApplicationContextInitializer 类的实例名称集合并去重、排序");
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
System.out.println("t 应用上下文初始化器的初始化后的值为:" this.initializers);
System.out.println("设置监听器");
System.out.println("t 从 META-INF/spring.factories 读取 ApplicationListener 类的实例名称集合并去重、排序");
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
System.out.println("t 监听器列表初始化后的值为:" this.listeners);
System.out.println("初始化主入口应用类");
System.out.println("t 通过当前调用栈,获取 main 方法所在类,并赋值给 mainApplicationClass");
this.mainApplicationClass = deduceMainApplicationClass();
System.out.println("t 程序入口应用类为:" this.mainApplicationClass);
System.out.println("【Spring Boot 源码剖析】-【SpringApplication 构造方法】- 执行完毕");
}
通过控制台日志输出,可以把 SpringApplication 构造方法完成的功能简单概括如下。
纵然 SpringApplication 构造方法里面实现再复杂,也不会脱离构造方法的功能,就是完成一系列参数的初始化操作罢了。
在 SpringApplication 构造方法调用完毕后,接着会调用 SpringApplication 对象的 run 方法,坊间也称之为 Spring Boot 启动时的运行方法,若想探究 Spring Boot 的启动流程,此方法不容错过。
代码语言:javascript复制public ConfigurableApplicationContext run(String... args) {
//1、记录运行开始时间
long startTime = System.nanoTime();
//2、创建引导程序注册的上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
//3、初始化应用上下文
ConfigurableApplicationContext context = null;
//4、设置系统属性“java.awt.headless”的值,默认为true,用于运行headless服务器,进行简单的图像处理,
// 多用于在缺少显示屏、键盘或者鼠标时的系统配置,很多监控工具如 jconsole 需要将该值设置为 true
configureHeadlessProperty();
//5、创建所有spring运行监听器并发布应用启动事件,简单说的话就是获取SpringApplicationRunListener类型的实例(EventPublishingRunListener对象),
// 并封装进SpringApplicationRunListeners对象,然后返回这个SpringApplicationRunListeners对象。
// 说的再简单点,getRunListeners就是准备好了运行时监听器EventPublishingRunListener
SpringApplicationRunListeners listeners = getRunListeners(args);
// 发布 SprintBoot 启动事件:ApplicationStartingEvent
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
//6、初始化默认应用参数类
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//7、根据运行监听器、引导启动上下文和应用参数来准备 Spring 环境
// 创建和配置environment,发布事件:ApplicationEnvironmentPreparedEvent
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
//8、打印 banner 信息
Banner printedBanner = printBanner(environment);
//9、创建应用上下文,可以理解为创建一个容器
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
//10、准备应用上下文,该步骤包含一个非常关键的操作,将启动类注入容器,为后续开启自动化提供基础
// 发布事件:ApplicationContextInitializedEvent,打印启动日志,打印 profile 信息,
// 发布 ApplicationContext 加载完毕事件:ApplicationPreparedEvent
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//11、刷新应用上下文,内部完成 Spring IOC 容器创建过程,并且会进行自动装配的操作
// 发布事件:ContextRefreshedEvent,标志着 ApplicationContext 初始化完成
refreshContext(context);
//12、应用上下文刷新后置处理,做一些扩展功能
afterRefresh(context, applicationArguments);
//13、计算启动耗时
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
//14、输出日志记录执行主类名、时间信息
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
//15、发布程序已启动事件:ApplicationStartedEvent
listeners.started(context, timeTakenToStartup);
//16、调用 ApplicationRunner 和 CommandLineRunner
callRunners(context, applicationArguments);
} catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
//17、发布应用上下文就绪事件:ApplicationReadyEvent,标志着可以处理接收的请求了
listeners.ready(context, timeTakenToReady);
} catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
//18、返回应用上下文
return context;
}
Spring Boot 启动步骤确实比较多,挑几行代码解说一二。
1.1. createBootstrapContext()
方法内部会创建 DefaultBootstrapContext 的一个实例,然后对 bootstrapRegistryInitializers (构造方法中从 META-INF/spring.factories 读取 BootstrapRegistryInitializer 类的实例名称集合并完成初始化操作)进行逐个初始化操作(个人理解:可以做应用启动前的初始化检查动作,例如从远端拿证书文件,获取密钥,服务启动时的口令等等)。
1.2. starting()
从 spring.factories 读取 SpringApplicationRunListener 类实例名称,只有 EventPublishingRunListener 一个配置,所以调用 SpringApplicationRunListener 的方法,本质上调用的是 EventPublishingRunListener 这个实现类的方法。
接着创建 spring 运行监听器 SpringApplicationRunListeners 的实例,然后调用 starting 方法。
代码语言:javascript复制SpringApplicationRunListeners listeners = getRunListeners(args);
// 发布 SprintBoot 启动事件:ApplicationStartingEvent
listeners.starting(bootstrapContext, this.mainApplicationClass);
starting 方法内部会调用 doWithListeners 方法进行遍历监听器列表,触发发布启动事件。
发布启动事件,最终由 EventPublishingRunListener 来负责完成。
至此,会调用 multicastEvent 来完成 ApplicationStartingEvent 启动事件的发布。
1.3. prepareEnvironment()
根据运行监听器、注册引导上下文和应用参数来准备环境。
接着调用 environmentPrepared 来广播 ApplicationEnvironmentPreparedEvent 事件。
最后交由 EventPublishingRunListener 来发布 ApplicationEnvironmentPreparedEvent 事件。
至此,会调用 multicastEvent 来完成 ApplicationEnvironmentPreparedEvent 环境准备工作已完成的事件发布。
1.4. printBanner() 方法
当环境就绪后,接着便是创建 Banner 及打印。
Banner 的形式,SpringBootBanner 为默认控制台输出,ResourceBanner 加载资源文件,例如banner.txt,ImageBanner 为图片资源 banner。
在 printBanner 方法内部,会创建 SpringApplicationBannerPrinter 实例,然后调用 print 方法完成 banner 打印。
1.5. createApplicationContext()
创建应用上下文(容器),方法内部会依据应用类型创建不同的上下文容器。
1.6. prepareContext()
准备应用上下文,方法内部会调用 applyInitializers(context) 方法,此方法会对所有的 ApplicationContextInitializer 进行初始化操作。
接着会调用 listeners.contextPrepared(context) 触发 ApplicationContextInitializedEvent 上下文已实例化事件,接着会调用 getAllSources 方法加载所有资源,并将 Bean 加载到上下文中,然后广播 ApplicationPreparedEvent 上下文已准备好事件。
1.7. refreshContext(context)
刷新应用上下文,内部完成 Spring IOC 容器创建过程,并且会进行自动装配的操作以及发布ContextRefreshedEvent 事件,标志着 ApplicationContext 初始化完成。
1.8. afterRefresh()
此方法是应用上下文刷新后置的处理,可以做一下功能扩展。
1.9.0. listeners.started()
接着会计算启动耗时,并输出包含类名、启动耗时等信息的启动日志,然后发布 ApplicationStartedEvent 应用启动成功事件。
1.9.1. listeners.ready()
接着会调用 listeners.ready() 方法,并发布 ApplicationReadyEvent 应用已准备好事件,标志着应用可以处理接收的请求了。
1.9.2. handleRunFailure()
如果启动执行过程中出现异常,则会调用 listener.failed() 发布 ApplicationFailedEvent 事件。
至此,Spring Boot 应用就启动成功了,启动流程也就完事儿了,其实搞懂了 Spring Boot 的生命周期以及广播的事件,启动流程大体也就清晰了。
2. 例行回顾
本文采取 Debug 的方式跟了一下 Spring Boot 启动流程的源码,旨在感受一下启动机制的设计,这种设计在开发轮子时或许能够借鉴一下呢?
为了方便记忆,结合 Spring Boot 启动生命周期以及事件广播机制,把 Spring Boot 启动流程浓缩成了一副简图便于记忆。
另外 Spring Boot 内嵌 Tomcat 是如何实现的呢?优雅停机是如何实现的呢?感兴趣可以自行先跟一下源码,下次将继续一起走进源码进行剖析。
一起聊技术、谈业务、喷架构,少走弯路,不踩大坑,会持续输出更多精彩分享,敬请期待!
参考资料:
https://spring.io/
https://start.spring.io/
https://spring.io/projects/spring-boot
https://github.com/spring-projects/spring-boot
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
https://stackoverflow.com/questions/tagged/spring-boot
《Spring Boot实战》《深入浅出Spring Boot 2.x》
《一步一步学Spring Boot:微服务项目实战(第二版)》
《Spring Boot揭秘:快速构建微服务体系》