非常非常好的问题,别看Spring Boot就用一行run方法就启动了,其实内部背后执行了一整套复杂的启动流程。
当年我第一次接触Spring Boot的时候,第一个疑问就是:
Tomcat跑哪儿去了?
怎么不需要配置Tomcat就能把java web程序启动起来了? 当时真觉得很神奇。像我这种老程序员,当时写java web程序时,老麻烦了:
- 要先下载Tomcat包,解压后配置service.xml文件;
- 把Tomcat的路径配置在Intellij Idea里;
- 编写完java代码后,点击run按钮,将代码部署到Tomcat里去;
跟我同一个时代的程序员,当时应该都有一个想法,要是Tomcat是内置的就好了。想不到若干年后,Spring Boot做了这个事情。
我们可以先从【Tomcat哪儿去了】这个问题入手,慢慢铺开来讲。
问题 :Spring Boot 为什么不需要外置的Tomcat?
Spring Boot想把Tomcat内嵌进来并启动,是不能靠自己的,得靠Tomcat自身,就是说Tomcat得自己先去做到一件事:
支持使用方可以通过new的方式,将自己启动起来。
也就是说,容器应该支持从独立进程 变成了可编程对象,直接在代码里启动。事实上在Tomcat 7的时候,内部多了一个叫org.apache.catalina.startup.Tomcat的类,就是用来支持内嵌启动的:
java
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.start();
tomcat.getServer().await();
Spring Boot就是用了这个类,将Tomcat里在代码里直接启动的。当Tomcat提供了这样的能力后,Spring Boot就只需要再做一件事就可以了。就是把Tomcat当成jar包引入进来就可以了。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
这个 starter 里直接包含了:
xml
<!-- 内嵌 Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
而spring-boot-starter-tomcat会把真正的Tomcat jar包tomcat-embed-core.jar引入进来的。
但是由于还有像Jetty和Undertow等容器,因此这块,必须做统一的封装,这个就是ServletWebServerFactory接口的作用。
Spring Boot通过ServletWebServerFactory接口,将内嵌集成Web容器的方式标准化了。
那么到这里,我们可以开始讲代码细节了吗? 不行,因为还差一个上帝的视角。如果你一下子就进入代码细节,很容易迷失方向的。
要理解Spring Boot的启动流程,是得有一些前置的基础知识的。
理解Spring Boot启动流程的前置知识点
Spring Boot(我讲解的是2.7版本)的启动流程本质上是一场精密编排的自动化构建过程。要真正理解它,你需要先理解以下这四个核心概念。
第一个核心机制:Java SPI 与spring.factories
Spring Boot的零配置,始于一套增强版的 Java SPI (Service Provider Interface) 机制。它要解决的问题是:
Spring Boot 怎么知道要加载哪些类?配置都藏在哪?
JDK 原生的 SPI 是查找META-INF/services,而 Spring Boot 2.7 的核心通过SpringFactoriesLoader类,专门扫描所有 jar 包下的META-INF/spring.factories文件。这是一个巨大的清单文件。在spring-boot-autoconfigure包里,这个文件列出了非常多的自动配置类(如RedisAutoConfiguration、JdbcTemplateAutoConfiguration)。
那知道这个对理解启动流程有什么作用呢?
有的,因为启动的要处理的第一件事情就是搜集信息 。Boot 会读取这些文件,把所有潜在的扩展点(监听器、初始化器、自动配置类)都加载到内存中备用。
第二个核心机制:按需装配和约定优于配置
在使用SPI把相关的配置类 加载进来后,还需要执行一个按需装配的逻辑。这个是为了解决如下的一个问题:
加载了那么多的配置类,如果全部都启动,会有卡死和冲突的问题。
一定是要过滤 一下的。我们用非常常见的**@ConditionalOnClass 和 @ConditionalOnMissingBean**来说明。
**@ConditionalOnClass的意思是类存在,我才生效;类不存在,我装作没看见。**比如:
java
@ConditionalOnClass(RedisTemplate.class)
@Configuration
public class RedisAutoConfiguration {
...
}
意思是,只有当你的项目依赖里有RedisTemplate这个类,我这份 Redis 自动配置才会加载。
Boot是通用框架,不知道你到底要不要用Redis,因此只有条件满足了,才会去加载的。这就是按需装配。
而**@ConditionalOnMissingBean**呢,则是说,你没配,我帮你配,你配了,则听你的。比如:
java
@ConditionalOnMissingBean(DataSource.class)
@Bean
public DataSource dataSource() {
...
}
意思是,如果容器里还没有DataSource ,我就帮你创建一个默认的,但是如果你自己配置了:
java
@Bean
public DataSource myDataSource() {
...
}
则不创建了。这就是约定优于配置。
第三个核心机制:观察者模式与生命周期
这个是为了解决如下的问题:
启动流程这么长,如何解耦各个阶段?又如何干预?
你千万不要以为Boot的启动流程是流水账一样的,它其实更像是:
每到一个阶段,就发一个事件, 谁关心这个事件,谁就出来干活。
这就是设计模式中的观察者模式。
而其中最关键的就是【阶段性广播员】EventPublishingRunListener。启动过程中的每个阶段 ,都是它负责去广播,发布事件,而这个时候,所有注册的监听器都会收到。注意,这里是所有的监听器都会收到阶段性事件的,但只有监听这个事件类型的监听器会执行。
那启动过程中到底有哪些关键的阶段呢? 如下:
ApplicationStartingEvent(启动开始,还没开始干活)ApplicationEnvironmentPreparedEvent(环境准备好了,配置读进去了)ApplicationContextInitializedEvent(容器创建了,初始化器执行了)ApplicationPreparedEvent(Bean 定义加载了,但还没实例化)ApplicationStartedEvent(启动完成,可以对外服务了)
上面我写的这段一定要理解哈,因为源码里充满了multicastEvent()。如果你不理解这个机制,就会觉得代码非常跳跃,明明在这里执行,怎么突然跑到那个类里去了?理解了事件,你才能把复杂的长流程切割成**阶段,**然后按照阶段去理解,就相对清晰很多了。
最后终于可以启动Spring的容器了
Spring Boot 复杂的启动流程中,最终的目的就是为了启动一个 Spring 的ApplicationContext。这里一定要理解好,Spring Boot底层也是基于Spring的,因此你可以简单如下理解:
Spring Boot本质上只是帮你准备好一切,最终真正干活的,还是Spring Framework。
最终就是想通过调用Spring中ApplicationContext的refresh方法,将Spring容器彻底激活。好,到了这里,可以用最简洁的语言来描述Spring Boot的启动流程了。
- 构建 Environment ;
- 创建合适的 ApplicationContext;
- 调用 refresh()。
从源代码的角度来分析启动流程了
注意,由于篇幅问题,我这里只会列出入口的部分,你顺着入口一步一步看就可以了。只要你用我上面介绍的知识,我相信你再次去分析SpringApplication.run()里底层实现,就不会那么晕了。
在此之前,我先用一张整体的架构图,帮你建立整体的架构认知。

核心就两步:
- 构造阶段:推断应用类型、加载扩展组件;
- 运行阶段:准备环境、创建容器、刷新容器、启动完成。
SpringApplication.run()对应的代码入口在这里:
java
public ConfigurableApplicationContext run(String... args) {
// 1. 触发启动事件
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 2. 准备环境(加载 application.properties/yml)
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 3. 创建 ApplicationContext(根据应用类型创建不同的上下文)
context = createApplicationContext();
// 4. 准备上下文(注册启动类、执行初始化器)
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 5. 刷新上下文(核心!启动 Tomcat、实例化 Bean)
refreshContext(context);
// 6. 触发启动完成事件
listeners.started(context, timeTakenToStartup);
// 7. 执行 CommandLineRunner/ApplicationRunner
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
}
return context;
}
其中的refreshContext(context),就是 Tomcat 真正启动的地方,如果你对里面的代码感兴趣,可以去看ServletWebServerApplicationContext里面的onRefresh方法实现就可以了。
java
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer(); // 创建 Web 容器
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
如果你是第一次看这段代码,会有个疑问,里面的ServletWebServerFactory是哪来的? 这个就是我们上面提到的自动装配和按需装配的思想,从ServletWebServerFactoryConfiguration得到的。并结合ConditionalOnClass和ConditionalOnMissingBean以及EnableAutoConfiguration注解,以及AutoConfigurationImportSelector类来完成的。
你只需要顺着SpringApplication.run()的入口,结合我们上面的前置知识,大概看个几周时间,基本就能记住一些细节和了解到一些设计方法和设计思想了。
写在最后
Spring Boot 的启动流程看起来复杂,实际上核心思路很清晰:
- 推断应用类型(SERVLET/xxxxxxx)
- 加载自动配置
- 创建 ApplicationContext(根据类型创建不同的上下文)
- 刷新容器 (在
onRefresh()钩子中启动 Tomcat) - 触发启动完成事件(执行用户自定义任务)
Spring Boot 能把传统 SSH 项目的部署方式从war 包 + 外置容器简化到jar 包直接运行,核心就是这套 自动配置 + 内嵌容器 的架构设计。
2014 年 Spring Boot刚发布的时候,很多人觉得这不就是把Tomcat 打成 jar 包吗。但真正理解了启动原理后你会发现,这背后是一整套精巧的设计:
- SPI 机制的扩展点设计;
- 观察者模式的使用;
- 条件注解的按需加载;
- 工厂模式的容器抽象;
多读源代码,对编码水平和技术设计能力水平的提升,是有好处的。
最近在知乎出了
- 「应付6000万会员的秒杀系统专栏」
- 「几亿用户,百万并发的C端商品系统实战」
- 「技术团队DDD领域驱动设计三年落地实战」
- 「应付亿级用户规模的支付系统代码实战」
- 「应付亿级用户的会员体系代码实战」
专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:
- 老码头的技术浮生录
它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。「另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。」
当前星球里免费看的专栏是:
- 「应付6000万会员的秒杀系统专栏」
- 「几亿用户,百万并发的C端商品系统实战」
- 「技术团队DDD领域驱动设计三年落地实战」
- 「应付亿级用户规模的支付系统代码实战」
- 「应付亿级用户的会员体系代码实战」
知识星球内后续将推出20+个付费专栏,覆盖电商全链路:
| 选购线 | 用户会员营销线 | 中后台 |
|---|---|---|
| 购物车服务 | 营销系统 | 订单系统 |
| 商品服务 | 用户系统 | 支付系统 |
| 菜单服务 | 结算服务 |
从前台选购到中后台结算,星球成员全部免费,后续新增也不额外收费。
我的知乎账号:
- SamDeepThinking