头秃了,二十三张图带你从源码了解Spring Boot 的启动流程~
持续原创输出,点击上方蓝字关注我

目录
前言 源码版本 从哪入手? 源码如何切分? 如何创建SpringApplication? 设置应用类型 设置初始化器(Initializer) 设置监听器(Listener) 设置监听器(Listener)
执行run()方法 获取、启动运行过程监听器 环境构建 创建IOC容器 IOC容器的前置处理 刷新容器 IOC容器的后置处理 发出结束执行的事件 执行Runners 总结
总结
前言
Spring Boot
专栏已经写了五十多天了,前面二十章从基础应用到高级整合避重就轻介绍的都是工作、面试中常见的知识点。
今天开始底层源码介绍的阶段,相对内容比较深一点,作者也尽可能介绍的通俗易懂,层次分明一点。相信读过我写的Mybatis
专栏的文章都知道,只要跟着作者的步骤,方法一步步研究,其实源码并不难。
这篇文章花了四天时间精雕细琢,力求介绍的通俗易懂,毕竟源码相对难度更高些,希望通过作者拆分讲解能够帮助到读者。
如果没读过作者的前二十篇文章,点击前往
源码版本
作者Spring Boot
是基于2.4.0
。每个版本有些变化,读者尽量和我保持一致,以防源码有些出入。
从哪入手?
相信很多人尝试读过Spring Boot
的源码,但是始终没有找到合适的方法。那是因为你对Spring Boot
的各个组件、机制不是很了解,研究起来就像大海捞针。
至于从哪入手不是很简单的问题吗,当然主启动类了,即是标注着@SpringBootApplication
注解并且有着main()
方法的类,如下一段代码:
@SpringBootApplication
public class AnnotationDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AnnotationDemoApplication.class, args);
}
}
话不多说,DEBUG
伺候,别怕,搞它........

源码如何切分?
SpringApplication
中的静态run()
方法并不是一步完成的,最终执行的源码如下:
//org.springframework.context.ConfigurableApplicationContext
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
很显然分为两个步骤,分别是创建SpringApplication
和执行run()
方法,下面将分为这两个部分介绍。
如何创建SpringApplication?
创建即是new
对象了,DEBUG
跟进代码,最终执行的SpringApplication
构造方法如下图:

如上图中标注的注释,创建过程重用的其实分为②
、③
、④
这三个阶段,下面将会一一介绍每个阶段做了什么事。
设置应用类型
这个过程非常重要,直接决定了项目的类型,应用类型分为三种,都在WebApplicationType
这个枚举类中,如下:
NONE
:顾名思义,什么都没有,正常流程走,不额外的启动web容器
,比如Tomcat
。SERVLET
:基于servlet
的web程序,需要启动内嵌的servlet
web容器,比如Tomcat
。REACTIVE
:基于reactive
的web程序,需要启动内嵌reactive
web容器,作者不是很了解,不便多说。
判断的依据很简单,就是加载对应的类,比如加载了DispatcherServlet
等则会判断是Servlet
的web程序。源码如下:
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
这里我引入了spring-boot-starter-web
,肯定是Servlet
的web程序。
设置初始化器(Initializer)
初始化器ApplicationContextInitializer
是个好东西,用于IOC
容器刷新之前初始化一些组件,比如ServletContextApplicationContextInitializer
。
那么如何获取初始化器呢?跟着上图中的代码进入,在SpringApplication
中的如下图中的方法:

相对重要的就是第一步获取初始化器的名称了,这个肯定是全类名
了,详细源码肯定在loadFactoryNames()
方法中了,跟着源码进入,最终调用的是#SpringFactoriesLoader.loadSpringFactories()
方法。
loadSpringFactories()
方法就不再详细解释了,其实就是从类路径META-INF/spring.factories
中加载ApplicationContextInitializer
的值。
在spring-boot-autoconfigure
的spring.factories
文件中的值如下图:

上图中的只是一部分初始化器,因为
spring.factories
文件不止一个。
下图中是我的demo
中注入的初始化器,现实项目中并不止这些。

这也告诉我们自定义一个
ApplicationContextInitializer
只需要实现接口,在spring.factories
文件中设置即可。
设置监听器(Listener)
监听器(ApplicationListener
)这个概念在Spring
中就已经存在,主要用于监听特定的事件(ApplicationEvent
),比如IOC容器刷新、容器关闭等。
Spring Boot
扩展了ApplicationEvent
构建了SpringApplicationEvent
这个抽象类,主要用于Spring Boot
启动过程中触发的事件,比如程序启动中、程序启动完成等。如下图:

监听器如何获取?从源码中知道其实和初始化器(ApplicationContextInitializer
)执行的是同一个方法,同样是从META-INF/spring.factories
文件中获取。
在spring-boot-autoconfigure
的spring.factories
文件中的值如下图:

spring.factories
文件不止一个,同样监听器也不止以上这些。
作者demo
中注入的一些监听器如下图:

总结
SpringApplication
的构建都是为了run()
方法启动做铺垫,构造方法中总共就有几行代码,最重要的部分就是设置应用类型、设置初始化器、设置监听器。
「注意」:初始化器和这里的监听器都要放置在
spring.factories
文件中才能在这一步骤加载,否则不会生效,因此此时IOC容器
还未创建,即使将其注入到IOC容器
中也是不会生效的。
作者简单的画了张执行流程图,仅供参考,如下:

执行run()方法
上面分析了SpringApplication
的构建过程,一切都做好了铺垫,现在到了启动的过程了。
作者根据源码将启动过程分为了「8步」,下面将会一一介绍。
1. 获取、启动运行过程监听器
SpringApplicationRunListener
这个监听器和ApplicationListener
不同,它是用来监听应用程序启动过程的,接口的各个方法含义如下:
public interface SpringApplicationRunListener {
// 在run()方法开始执行时,该方法就立即被调用,可用于在初始化最早期时做一些工作
void starting();
// 当environment构建完成,ApplicationContext创建之前,该方法被调用
void environmentPrepared(ConfigurableEnvironment environment);
// 当ApplicationContext构建完成时,该方法被调用
void contextPrepared(ConfigurableApplicationContext context);
// 在ApplicationContext完成加载,但没有被刷新前,该方法被调用
void contextLoaded(ConfigurableApplicationContext context);
// 在ApplicationContext刷新并启动后,CommandLineRunners和ApplicationRunner未被调用前,该方法被调用
void started(ConfigurableApplicationContext context);
// 在run()方法执行完成前该方法被调用
void running(ConfigurableApplicationContext context);
// 当应用运行出错时该方法被调用
void failed(ConfigurableApplicationContext context, Throwable exception);
}
如何获取运行监听器?
在SpringApplication#run()
方法中,源码如下:
//从spring.factories中获取监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
跟进getRunListeners()
方法,其实还是调用了loadFactoryNames()
方法从spring.factories
文件中获取值,如下:
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
最终注入的是EventPublishingRunListener
这个实现类,创建实例过程肯定是通过反射了,因此我们看看它的构造方法,如下图:

这个运行监听器内部有一个事件广播器(SimpleApplicationEventMulticaster
),主要用来广播特定的事件(SpringApplicationEvent
)来触发特定的监听器ApplicationListener
。
EventPublishingRunListener
中的每个方法用来触发SpringApplicationEvent
中的不同子类。
如何启动运行监听器?
在SpringApplication#run()
方法中,源码如下:
//执行starting()方法
listeners.starting(bootstrapContext, this.mainApplicationClass);
执行SpringApplicationRunListeners
的starting()
方法,跟进去其实很简单,遍历执行上面获取的运行监听器,这里只有一个EventPublishingRunListener
。因此执行的是它的starting()
方法,源码如下图:

上述源码中逻辑很简单,其实只是执行了multicastEvent()
方法,广播了ApplicationStartingEvent
事件。至于multicastEvent()
内部方法感兴趣的可以看看,其实就是遍历ApplicationListener
的实现类,找到监听ApplicationStartingEvent
这个事件的监听器,执行onApplicationEvent()
方法。
总结
这一步其实就是广播了ApplicationStartingEvent
事件来触发监听这个事件的ApplicationListener
。
因此如果自定义了
ApplicationListener
并且监听了ApplicationStartingEvent
(应用程序开始启动)事件,则这个监听器将会被触发。
2. 环境构建
这一步主要用于加载系统配置以及用户的自定义配置(application.properties
),源码如下,在run()
方法中:
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
prepareEnvironment
方法内部广播了ApplicationEnvironmentPreparedEvent
事件,源码如下图:

环境构建这一步加载了系统环境配置、用户自定义配置并且广播了
ApplicationEnvironmentPreparedEvent
事件,触发监听器。
3. 创建IOC容器
源码在run()
方法中,如下:
context = createApplicationContext();
跟进代码,真正执行的是ApplicationContextFactory
方法,如下图:

根据webApplicationType
决定创建的类型,很显然,我这里的是servlet
,因此创建的是AnnotationConfigServletWebServerApplicationContext
。
这一步仅仅是创建了
IOC容器
,未有其他操作。
4. IOC容器的前置处理
这一步真是精华了,在刷新容器之前做准备,其中有一个非常关键的操作:将启动类注入容器,为后续的自动化配置奠定基础。源码如下:
prepareContext(context, environment, listeners, applicationArguments,printedBanner);
prepareContext()
源码解析如下图,内容还是挺多的:

从上图可以看出步骤很多,下面将会详细介绍几个重点的内容。
调用初始化器
在SpringApplication
构建过程中设置的初始化器,从spring.factories
取值的。执行的流程很简单,遍历执行,源码如下图:

将自定义的
ApplicationContextInitializer
放在META-INF/spring.factories
中,在此时也是会被调用。
加载启动类,注入容器
这一步是将主启动类加载到IOC容器
中,作为后续自动配置的入口。
在SpringApplication
构建过程中将主启动类放置在primarySources
这个集合中,此时的getAllSources()
即是从其中取值,如下图:

这里取出的就是主启动类,当然你的项目中可能不止一个,接下来就是将其加载到IOC容器中了,源码如下:
load(context, sources.toArray(new Object[0]));
跟着代码进去,其实主要逻辑都在BeanDefinitionLoader.load()
方法,如下图:

将主启动类加载到
beanDefinitionMap
,后续该启动类将作为开启自动配置化的入口,后续章节详细介绍。
两次广播事件
这一步涉及到了两次事件广播,分别是ApplicationContextInitializedEvent
和ApplicationPreparedEvent
,对应的源码如下:
listeners.contextPrepared(context);
load(context, sources.toArray(new Object[0]));
5. 刷新容器
刷新容器完全是Spring
的功能了,比如初始化资源,初始化上下文广播器等,这个就不再详细介绍,有兴趣可以看看Spring
的源码。
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
//调用创建的容器applicationContext中的refresh()方法
((AbstractApplicationContext)applicationContext).refresh();
}
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
/**
* 刷新上下文环境
*/
prepareRefresh();
/**
* 初始化BeanFactory,解析XML,相当于之前的XmlBeanFactory的操作,
*/
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
/**
* 为上下文准备BeanFactory,即对BeanFactory的各种功能进行填充,如常用的注解@Autowired @Qualifier等
* 添加ApplicationContextAwareProcessor处理器
* 在依赖注入忽略实现*Aware的接口,如EnvironmentAware、ApplicationEventPublisherAware等
* 注册依赖,如一个bean的属性中含有ApplicationEventPublisher(beanFactory),则会将beanFactory的实例注入进去
*/
prepareBeanFactory(beanFactory);
try {
/**
* 提供子类覆盖的额外处理,即子类处理自定义的BeanFactoryPostProcess
*/
postProcessBeanFactory(beanFactory);
/**
* 激活各种BeanFactory处理器,包括BeanDefinitionRegistryBeanFactoryPostProcessor和普通的BeanFactoryPostProcessor
* 执行对应的postProcessBeanDefinitionRegistry方法 和 postProcessBeanFactory方法
*/
invokeBeanFactoryPostProcessors(beanFactory);
/**
* 注册拦截Bean创建的Bean处理器,即注册BeanPostProcessor,不是BeanFactoryPostProcessor,注意两者的区别
* 注意,这里仅仅是注册,并不会执行对应的方法,将在bean的实例化时执行对应的方法
*/
registerBeanPostProcessors(beanFactory);
/**
* 初始化上下文中的资源文件,如国际化文件的处理等
*/
initMessageSource();
/**
* 初始化上下文事件广播器,并放入applicatioEventMulticaster,如ApplicationEventPublisher
*/
initApplicationEventMulticaster();
/**
* 给子类扩展初始化其他Bean
*/
onRefresh();
/**
* 在所有bean中查找listener bean,然后注册到广播器中
*/
registerListeners();
/**
* 设置转换器
* 注册一个默认的属性值解析器
* 冻结所有的bean定义,说明注册的bean定义将不能被修改或进一步的处理
* 初始化剩余的非惰性的bean,即初始化非延迟加载的bean
*/
finishBeanFactoryInitialization(beanFactory);
/**
* 通过spring的事件发布机制发布ContextRefreshedEvent事件,以保证对应的监听器做进一步的处理
* 即对那种在spring启动后需要处理的一些类,这些类实现了ApplicationListener<ContextRefreshedEvent>,
* 这里就是要触发这些类的执行(执行onApplicationEvent方法)
* 另外,spring的内置Event有ContextClosedEvent、ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、RequestHandleEvent
* 完成初始化,通知生命周期处理器lifeCycleProcessor刷新过程,同时发出ContextRefreshEvent通知其他人
*/
finishRefresh();
}
finally {
resetCommonCaches();
}
}
}
6. IOC容器的后置处理
一个扩展方法,源码如下:
afterRefresh(context, applicationArguments);
默认为空,如果有自定义需求可以重写,比如打印一些启动结束日志等。
7. 发出结束执行的事件
同样是EventPublishingRunListener
这个监听器,广播ApplicationStartedEvent
事件。
但是这里广播事件和前几次不同,并不是广播给
SpringApplication
中的监听器(在构建过程中从spring.factories
文件获取的监听器)。因此在IOC容器
中注入的监听器(使用@Component
等方式注入的)也能够生效。前面几个事件只有在spring.factories
文件中设置的监听器才会生效。
跟着代码进入,可以看到started()
方法源码如下:

这里并没有用事件广播器SimpleApplicationEventMulticaster
广播事件,而是使用ConfigurableApplicationContext
直接在IOC容器
中发布事件。
8. 执行Runners
Spring Boot
提供了两种Runner
让我们定制一些额外的操作,分别是CommandLineRunner
和ApplicationRunner
,关于这两个的区别,后面文章详细介绍。
调用的源码如下:
callRunners(context, applicationArguments);
跟进代码,其实真正调执行的是如下方法:

逻辑很简单,从IOC容器
中获取,遍历调用。
总结
Spring Boot
启动流程相对简单些,作者将其细分了以上八个步骤,希望能够帮助读者理解,流程图如下:

总结
Spring Boot
启动流程就介绍到这里了,需要重点理解run()
方法执行的八个步骤以及事件、初始化器、监听器等组件的执行时间点。
作者每一篇文章都很用心,这篇源码解析花了三天时间精雕细琢,力求讲解的通俗易懂,希望能够帮助到你。
另外作者的第一本PDF
书籍已经整理好了,由浅入深的详细介绍了Mybatis基础以及底层源码,有需要的朋友公号回复关键词「Mybatis进阶」即可获取,目录如下:


头秃了,二十三张图带你从源码了解Spring Boot 的启动流程~的更多相关文章
- Java学习-039-源码 jar 包的二次开发扩展实例(源码修改)
最近在使用已有的一些 jar 包时,发现有些 jar 包中的一些方法无法满足自己的一些需求,例如返回固定的格式,字符串处理等等,因而需要对原有 jar 文件中对应的 class 文件进行二次开发扩展, ...
- hadoop2.5.2学习及实践笔记(二)—— 编译源代码及导入源码至eclipse
生产环境中hadoop一般会选择64位版本,官方下载的hadoop安装包中的native库是32位的,因此运行64位版本时,需要自己编译64位的native库,并替换掉自带native库. 源码包下的 ...
- Android版数据结构与算法(二):基于数组的实现ArrayList源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 本片我们分析基础数组的实现--ArrayList,不会分析整个集合的继承体系,这不是本系列文章重点. 源码分析都是基于"安卓版" ...
- 【Storm】Storm实战之频繁二项集挖掘(附源码)
一.前言 针对大叔据实时处理的入门,除了使用WordCount示例之外,还需要相对更深入点的示例来理解Storm,因此,本篇博文利用Storm实现了频繁项集挖掘的案例,以方便更好的入门Storm. 二 ...
- Hadoop源码学习笔记之NameNode启动场景流程二:http server启动源码剖析
NameNodeHttpServer启动源码剖析,这一部分主要按以下步骤进行: 一.源码调用分析 二.伪代码调用流程梳理 三.http server服务流程图解 第一步,源码调用分析 前一篇文章已经锁 ...
- Netty源码分析 (十二)----- 心跳服务之 IdleStateHandler 源码分析
什么是心跳机制? 心跳说的是在客户端和服务端在互相建立ESTABLISH状态的时候,如何通过发送一个最简单的包来保持连接的存活,还有监控另一边服务的可用性等. 心跳包的作用 保活Q:为什么说心跳机制能 ...
- Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码
在文章:Mybatis源码解析,一步一步从浅入深(一):创建准备工程,中我们为了解析mybatis源码创建了一个mybatis的简单工程(源码已上传github,链接在文章末尾),并实现了一个查询功能 ...
- 【一起学源码-微服务】Nexflix Eureka 源码十二:EurekaServer集群模式源码分析
前言 前情回顾 上一讲看了Eureka 注册中心的自我保护机制,以及里面提到的bug问题. 哈哈 转眼间都2020年了,这个系列的文章从12.17 一直写到现在,也是不容易哈,每天持续不断学习,输出博 ...
- JVM 源码分析(二):搭建 JDK 8 源码调试环境(Windows 上使用 CLion)
前言 一.准备源码 二.安装 "Bootstrap JDK" 三.配置编译环境 四.编译与测试 五.安装 CMake 和 GDB 五.准备远程调试 六.开始远程调试 前言 上一篇文 ...
随机推荐
- day10 Pyhton学习
一.昨日内容回顾 函数: 定义:对功能或者动作的封装 def 函数名(形参): 函数体 函数名(实参) return: 返回,当程序运行到return的时候,终止函数的执行 一个函数一定拥有返回值 ...
- 算出cron表达式接下来几次执行时间
目录 1.使用cron库 2.总结 1.使用cron库 需要使用的go库:[点击跳转]. 具体使用方法可以参照例子使用,下面主要实现计算接下来几次cron表达式执行时间. package main i ...
- C语言实现和 *.ini文件。
本文之前由于技术不到位,写的比较挫,最近花了大半天时间写了一个高级点的版本. 这里是我写的代码,已经上传到github了.跳转到github 主要是使用了链表保存ini文件的内容,在程序运行最初会初始 ...
- linux(centos8):安装kubernetes worker节点并加入到kubernetes集群(kubernetes 1.18.3)
一,安装kubernetes前的准备工作 安装前的准备工作(master\worker都要进行) 参见: https://www.cnblogs.com/architectfore ...
- lumen laravel response对象返回数据
Route::get('home', function () { $content = "内容"; $status = 301; $value = 'text/html'; // ...
- centos8平台给sudo配置日志
一,sudo日志的用途: 我们可以记录下来用户账号在哪个时间进行过sudo 这样不需要再从secure日志中查找用户的sudo记录 说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://w ...
- CentOS7下RabbitMQ服务安装配置 (亲测有效)
erlang 21.3 rabbitmq-server 3.7.14 下载地址 链接: https://pan.baidu.com/s/1g_T1Q_6zpyO3AepS0ZPgYQ 提取码: abq ...
- CPU 运算实现过程
总结 加法运算过程:十进制:1+1=2二进制01+01=10实现过程: 在做加法时CPU内部会调用加法器,实际上加法运算器所做的工作就是按位与操作和进位运算!所谓的进位运算规则和十进制一样满10进一而 ...
- ffmpeg+Python实现B站MP4格式音频与视频的合并
目录 安装 官网下载 环境变量 验证 ffmpeg的使用 Python实现自动处理 文件结构 番剧缓存结构 常规缓存结构 文件信息 代码 具体代码 代码说明 安装 官网下载 http://ffmpeg ...
- Ubuntu18.04上安装NS-3
目录 第一步:处理gcc/g++版本 第二步:安装相关依赖 第三步:正式安装 第四步:测试 我自己前后安装过好几次NS3了,网上其他相关的博客质量都不是很好,因此自己总结了一个ns3的安装过程. 首先 ...