Spring Boot 启动原理
很多人第一次学 Spring Boot 时会觉得神奇:为什么一个 main 方法就能把整个项目跑起来?
这一章我们就专门把启动过程拆开。
如果要深入理解 SpringApplication.run、应用类型推断、Environment、ApplicationContext、自动配置导入、refresh()、内嵌 Tomcat、Runner、启动事件和启动慢排查,建议继续看:Spring Boot启动流程全过程原理。
从入口开始看
标准启动代码通常是这样:
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}真正的重点在 SpringApplication.run(...)。
启动流程总览
flowchart TD
A["调用 SpringApplication.run"] --> B["推断应用类型"]
B --> C["加载 ApplicationContext"]
C --> D["准备 Environment"]
D --> E["读取配置文件"]
E --> F["打印 Banner"]
F --> G["创建并刷新容器"]
G --> H["触发自动配置"]
H --> I["创建 Web 容器"]
I --> J["注册 DispatcherServlet"]
J --> K["应用启动完成"]你可以把它理解成“先准备环境,再创建容器,再把 Web 能力挂上去”。
第一步:推断应用类型
Spring Boot 会根据类路径里的依赖判断当前应用属于哪一种:
NONE:不是 Web 应用。SERVLET:传统 Servlet Web 应用,最常见。REACTIVE:响应式 Web 应用。
如果项目里有 spring-webmvc,通常会判断为 SERVLET。
第二步:准备运行环境
Environment 可以理解成“配置的总入口”,里面会收集很多配置来源:
- 命令行参数。
- 系统环境变量。
- JVM 参数。
application.ymlapplication-{profile}.yml
这一步完成后,Spring Boot 才知道端口、上下文路径、数据库地址等信息。
第三步:创建容器
不同应用类型,会选择不同的容器实现。
例如 Servlet 应用通常会使用:
AnnotationConfigServletWebServerApplicationContext这个容器除了普通 Spring 容器能力,还额外支持 Web 服务器相关功能。
第四步:刷新容器
刷新容器是 Spring 启动中的核心阶段。
这一步会完成很多关键动作:
- 扫描组件。
- 注册 BeanDefinition。
- 实例化单例 Bean。
- 处理依赖注入。
- 处理各种后置处理器。
- 启动内嵌服务器。
自动配置是怎么生效的
本节先放在启动流程里帮助你定位自动配置发生的位置。完整自动配置链路、Boot 2/Boot 3 声明文件区别、条件注解、排查方法和自定义 Demo,看 Spring Boot 自动配置原理。
@EnableAutoConfiguration
它会触发自动配置机制。
自动配置并不是凭空猜,而是通过读取一批预定义配置类,再根据条件判断是否加载。
条件注解的作用
自动配置类里经常能看到这些注解:
@ConditionalOnClass@ConditionalOnMissingBean@ConditionalOnProperty@ConditionalOnWebApplication
它们的意思分别类似于:
- 类路径里有某个类才生效。
- 容器里没有某个 Bean 才生效。
- 配置项满足条件才生效。
- 必须是 Web 应用才生效。
也就是说,Spring Boot 不是硬塞配置,而是“条件满足再装配”。
自动配置完整链路
自动配置不是“启动时到处扫描 jar 包”。它有一条非常清晰的链路:
@SpringBootApplication
-> @EnableAutoConfiguration
-> @Import(AutoConfigurationImportSelector.class)
-> 读取候选自动配置类
-> 条件过滤
-> 注册 BeanDefinition
-> refresh 阶段创建 Beanflowchart TD
A["启动类有 @SpringBootApplication"] --> B["组合注解里包含 @EnableAutoConfiguration"]
B --> C["@Import 导入 AutoConfigurationImportSelector"]
C --> D["读取自动配置候选名单"]
D --> E["Boot 3: AutoConfiguration.imports"]
D --> F["Boot 2: spring.factories"]
E --> G["去重、排除 exclude、条件预过滤"]
F --> G
G --> H["把自动配置类交给 Spring 解析"]
H --> I{"条件注解是否满足"}
I -- "否" --> J["跳过"]
I -- "是" --> K["注册 BeanDefinition"]
K --> L["容器刷新时创建 Bean"]几个点一定要分清:
@EnableAutoConfiguration自己不直接创建 Bean。AutoConfigurationImportSelector负责找候选自动配置类。- 候选类不是全部生效,还要过条件注解。
- 最终仍然回到 Spring IOC:注册 BeanDefinition,再创建 Bean。
@SpringBootApplication 三件事
| 注解 | 作用 | 如果没有会怎样 |
|---|---|---|
@SpringBootConfiguration | 表示启动类也是配置类 | 主启动类不能作为配置源 |
@ComponentScan | 扫描业务组件 | Controller、Service 可能进不了容器 |
@EnableAutoConfiguration | 导入自动配置候选类 | Web、JSON、数据源等默认能力不会自动装配 |
这也解释了为什么启动类不要随便放在很深的子包里。默认组件扫描从启动类所在包开始向下扫描,如果启动类放错位置,业务 Bean 可能根本扫描不到。
flowchart TD
A["com.company.Application"] --> B["扫描 com.company.*"]
B --> C["controller/service/repository 能扫到"]
D["com.company.module.Application"] --> E["只扫描 com.company.module.*"]
E --> F["com.company.common 下的 Bean 可能扫不到"]SpringApplication.run() 到底做了什么
可以按“准备环境 -> 创建容器 -> 刷新容器 -> 启动完成”四段理解。
| 阶段 | 关键动作 | 为什么重要 |
|---|---|---|
创建 SpringApplication | 推断应用类型、加载初始化器和监听器 | 决定用普通容器还是 Web 容器 |
准备 Environment | 读取命令行、环境变量、配置文件、Profile | 后续自动配置要读取配置 |
创建 ApplicationContext | 根据应用类型选择上下文实现 | Servlet 应用会用 WebServer 容器 |
refresh() | 注册 BeanDefinition、实例化单例 Bean、执行后置处理器 | Spring 核心启动流程 |
| 启动 WebServer | 创建 Tomcat/Jetty/Undertow,绑定端口 | Web 服务开始接请求 |
| 发布事件 | ApplicationStartedEvent、ApplicationReadyEvent 等 | 监控、预热、初始化扩展 |
更完整的流程:
flowchart TD
A["main 方法调用 run"] --> B["创建 SpringApplication"]
B --> C["推断 WebApplicationType"]
C --> D["准备监听器和初始化器"]
D --> E["准备 Environment 和 Profile"]
E --> F["创建 ApplicationContext"]
F --> G["加载启动类和自动配置类"]
G --> H["refresh 容器"]
H --> I["执行 BeanFactoryPostProcessor"]
I --> J["注册 BeanPostProcessor"]
J --> K["实例化非懒加载单例 Bean"]
K --> L["启动内嵌 WebServer"]
L --> M["执行 Runner"]
M --> N["发布启动完成事件"]refresh() 为什么是核心
refresh() 是 Spring 容器真正“活起来”的阶段。很多自动配置看起来是 Boot 做的,但最终仍然是在 refresh() 中注册、解析、创建 Bean。
你可以重点记这几类动作:
- 准备 BeanFactory。
- 执行
BeanFactoryPostProcessor,允许修改 BeanDefinition。 - 注册
BeanPostProcessor,为生命周期和 AOP 做准备。 - 初始化消息源、事件广播器。
- 对 Web 应用创建并启动 WebServer。
- 实例化所有非懒加载单例 Bean。
- 发布容器刷新完成事件。
如果某个项目启动慢,通常要从这几个方向排查:
| 现象 | 可能原因 |
|---|---|
| 卡在创建 Bean | 构造器、@PostConstruct、初始化方法里有慢逻辑 |
| 卡在数据源 | 数据库连接不可用或连接超时太长 |
| 卡在配置中心 | 配置拉取慢或网络不通 |
| 卡在类扫描 | 包扫描范围过大、反射扫描太多 |
| 卡在 WebServer | 端口占用、Servlet/Filter 初始化慢 |
为什么能少写 XML
传统项目里,很多内容要手工在 XML 中声明。
而 Spring Boot 做了两件事:
- 用注解替代大量 XML。
- 用自动配置补齐通用场景。
所以开发者只需要写差异化配置,而不需要重复写基础配置。
内嵌 Tomcat 是什么时候启动的
它通常发生在容器刷新阶段。
流程可以简单理解为:
- Boot 发现当前是 Servlet Web 应用。
- 容器中存在 WebServerFactory。
- 刷新时创建 Tomcat。
- 绑定端口。
- 注册 Servlet、Filter、Listener。
- 开始接收请求。
flowchart TD
A["启动类调用 run"] --> B["Boot 创建并刷新 ApplicationContext"]
B --> C["容器解析 BeanDefinition"]
C --> D["创建非懒加载单例 Bean"]
D --> E["创建内嵌 Tomcat WebServer"]
E --> F["注册 Servlet、Filter 和 Listener"]
F --> G["Tomcat 绑定端口"]
G --> H["Boot 发布启动完成事件"]Tomcat 是谁创建的
Servlet Web 应用中,Spring Boot 通常会创建 ServletWebServerApplicationContext。它在 refresh() 过程中会调用自己的 WebServer 创建逻辑,通过容器里的 ServletWebServerFactory 创建 Tomcat。
简化链路是:
refresh()
-> onRefresh()
-> createWebServer()
-> ServletWebServerFactory#getWebServer()
-> Tomcat 启动并绑定端口对应到 Bean:
| Bean | 作用 |
|---|---|
TomcatServletWebServerFactory | 创建内嵌 Tomcat |
DispatcherServlet | Spring MVC 前端控制器 |
DispatcherServletRegistrationBean | 把 DispatcherServlet 注册到 Servlet 容器 |
FilterRegistrationBean | 注册 Filter |
ServletRegistrationBean | 注册自定义 Servlet |
请求进来后的链路是:
flowchart TD
A["浏览器或调用方"] --> B["Tomcat Connector"]
B --> C["Filter 链"]
C --> D["DispatcherServlet"]
D --> E["HandlerMapping 找 Controller"]
E --> F["HandlerAdapter 调用方法"]
F --> G["HttpMessageConverter 写响应"]
G --> H["返回客户端"]所以“内嵌 Tomcat”不是把外部 Tomcat 复制进代码里,而是应用启动时在同一个 JVM 里创建并启动一个 Servlet 容器。
引入 starter-web 就一定是 Tomcat 吗
默认情况下,spring-boot-starter-web 会引入 Tomcat。但可以排除 Tomcat 并替换为 Jetty 或 Undertow。
Maven 示例:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>如果是 WebFlux,默认栈通常是 Reactor Netty,不是 Spring MVC + Tomcat 这条 Servlet 链路。
application.yml 为什么能自动生效
因为 Spring Boot 在启动早期就会加载配置文件,并把内容放进 Environment。
随后,无论是:
- 自动配置类读取属性。
@Value注入属性。@ConfigurationProperties绑定属性。
本质上都在读 Environment 中的数据。
常见启动扩展点
CommandLineRunner
项目启动完成后执行,适合做初始化任务。
ApplicationRunner
和 CommandLineRunner 类似,但参数处理更友好。
监听启动事件
可以监听:
- 容器准备完成。
- 容器刷新完成。
- 应用启动完成。
- 应用启动失败。
这样我们就能在不同阶段做日志、监控、预热等工作。
生产排查:启动慢怎么定位
不要只看“应用启动用了 90 秒”这种总耗时,要拆阶段。
打开自动配置报告
启动时加:
java -jar app.jar --debug可以看到哪些自动配置生效、哪些没生效,以及条件判断原因。排查“为什么某个 Bean 没有自动配置出来”时很有用。
使用 Actuator 查看启动阶段
如果接入 Actuator,可以关注 startup、health、metrics、threaddump 等端点。生产环境不要无权限暴露敏感端点。
常见修复方向
| 问题 | 修复 |
|---|---|
| 初始化方法访问慢外部系统 | 改成启动后异步预热,或设置短超时 |
| Bean 构造器做大量计算 | 延迟到业务需要时,或缓存结果 |
| 包扫描过大 | 调整启动类位置和扫描范围 |
| 配置中心不可用导致等待 | 配置超时、降级默认值、启动失败策略 |
| 端口被占用 | 修改 server.port 或释放端口 |
医疗采集平台里,医院接口探活、字典预热、历史批次恢复这些逻辑不要全部阻塞在 Bean 初始化里。核心服务可以先启动,再由 Runner 或后台任务按可观测、可超时、可重试的方式执行。
小白容易踩的坑
1. 启动类放错包
默认组件扫描是从启动类所在包开始往下扫。
如果启动类放得太深或太偏,可能扫不到 Controller、Service。
2. 以为所有 Bean 都是自动生成
很多核心组件确实由 Boot 帮你创建,但业务 Bean 仍然需要你自己写。
3. 不理解“覆盖默认配置”
Spring Boot 有默认值,但不代表不能改。
只要你显式声明自己的配置或自己的 Bean,很多默认行为都可以被覆盖。
本章小结
Spring Boot 启动原理可以压缩成一句话:
先准备配置环境,再创建并刷新 Spring 容器,最后在条件满足时自动装配 Web 基础设施并启动内嵌服务器。
如果你能把这句话真正理解掉,后面学习自动配置、Starter、自定义扩展时就不会只停留在“会用”的层面。
