Spring Boot 启动流程全过程原理
Spring Boot 启动不是“一个 main 方法就完了”。SpringApplication.run() 背后会推断应用类型、准备配置环境、创建容器、加载 BeanDefinition、执行自动配置、刷新 IOC 容器、启动内嵌 WebServer、发布事件、执行 Runner。任何一步出问题,都可能导致启动慢、Bean 没创建、端口没起来、自动配置没生效。
一句话先建立直觉:
Spring Boot 启动流程就是:先准备运行环境,再创建并刷新 Spring 容器,最后按应用类型启动 WebServer 并发布启动完成事件。
学习目标
学完这一页,你要能说清楚:
SpringApplication.run()大体做了哪些事。- Boot 怎么推断当前是普通应用、Servlet Web 还是 Reactive Web。
Environment在启动流程中为什么这么早准备。ApplicationContext为什么有不同实现。- 自动配置发生在启动流程的哪一步。
refresh()为什么是 Spring 容器启动核心。- 内嵌 Tomcat 什么时候创建,谁创建。
ApplicationRunner、CommandLineRunner和启动事件有什么区别。- 启动慢、端口占用、Bean 创建失败、自动配置没生效怎么排查。
- 商业项目里哪些初始化逻辑不能阻塞启动。
最小启动代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}表面上只有一行:
SpringApplication.run(DemoApplication.class, args);实际它会完成:
- 创建
SpringApplication。 - 准备监听器和初始化器。
- 准备配置环境。
- 创建 Spring 容器。
- 加载启动类和自动配置。
- 刷新容器。
- 启动 WebServer。
- 执行启动完成后的回调。
总体流程图
flowchart TD
A["main 方法"] --> B["SpringApplication.run"]
B --> C["创建 SpringApplication"]
C --> D["推断 WebApplicationType"]
D --> E["加载初始化器和监听器"]
E --> F["准备 Environment"]
F --> G["打印 Banner"]
G --> H["创建 ApplicationContext"]
H --> I["加载启动类、业务组件、自动配置类"]
I --> J["refresh 刷新容器"]
J --> K["实例化非懒加载单例 Bean"]
K --> L["启动内嵌 WebServer"]
L --> M["发布 Started/Ready 事件"]
M --> N["执行 Runner"]这张图要和 Spring、Spring Boot、Web 三条线合起来看:
| 阶段 | 属于哪条线 | 说明 |
|---|---|---|
| 准备 Environment | Boot 工程化 | 配置、Profile、命令行参数 |
| 加载自动配置 | Boot 自动配置 | 候选配置类、条件注解 |
| refresh | Spring IOC | BeanDefinition、Bean 生命周期 |
| 启动 WebServer | Boot Web | Tomcat/Jetty/Undertow |
| DispatcherServlet | Spring MVC | 请求分发 |
创建 SpringApplication
SpringApplication.run(DemoApplication.class, args) 内部会先创建 SpringApplication 对象。
它会准备一些基础信息:
- 主启动类是谁。
- 应用类型是什么。
- 初始化器有哪些。
- 监听器有哪些。
- 是否打印 Banner。
- 是否允许懒加载。
可以手动创建:
import org.springframework.boot.SpringApplication;
public class DemoApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(DemoApplication.class);
application.setAdditionalProfiles("dev");
application.run(args);
}
}这说明 run 不是魔法,它是一个可配置的启动器。
推断 WebApplicationType
Spring Boot 会根据 classpath 推断应用类型:
| 类型 | 场景 | 常见容器 |
|---|---|---|
NONE | 命令行、批处理、非 Web 服务 | AnnotationConfigApplicationContext |
SERVLET | Spring MVC、传统 Web | AnnotationConfigServletWebServerApplicationContext |
REACTIVE | WebFlux 响应式应用 | AnnotationConfigReactiveWebServerApplicationContext |
判断逻辑可以简化理解为:
flowchart TD
A["检查 classpath"] --> B{"有 WebFlux 且没有 Spring MVC 吗"}
B -- "是" --> C["REACTIVE"]
B -- "否" --> D{"有 Servlet 和 Spring MVC 相关类吗"}
D -- "是" --> E["SERVLET"]
D -- "否" --> F["NONE"]为什么这一步重要?
因为应用类型决定后面创建哪种 ApplicationContext,也决定是否需要启动内嵌 WebServer。
初始化器和监听器
Spring Boot 启动过程中有两个容易混淆的扩展:
| 扩展 | 作用 | 时机 |
|---|---|---|
ApplicationContextInitializer | 在容器创建后、refresh 前修改容器 | 容器早期 |
ApplicationListener | 监听启动事件、失败事件、刷新事件 | 多个阶段 |
初始化器示例:
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
public class StartupMarkerInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
context.getBeanFactory().registerSingleton("startupMarker", "context-created");
}
}事件监听示例:
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
public class ReadyListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
System.out.println("应用已经可以接收请求");
}
}区别:Initializer 是“改容器”,Listener 是“听事件”。
准备 Environment
Environment 准备很早,因为后续很多动作都依赖配置:
- 哪个 Profile 生效。
- 端口是多少。
- 是否启用某个自动配置。
- 数据源地址是什么。
- 日志级别是什么。
配置加载链路:
flowchart TD
A["准备 Environment"] --> B["读取命令行参数"]
A --> C["读取系统属性"]
A --> D["读取环境变量"]
A --> E["读取 application.yml"]
A --> F["读取 Profile 配置"]
A --> G["读取配置中心或自定义 PropertySource"]
B --> H["合并 PropertySource"]
C --> H
D --> H
E --> H
F --> H
G --> H为什么配置解密要放在早期?
因为自动配置和配置绑定会在后面读取配置。如果你等业务 Bean 创建后才解密,很多条件判断已经结束了。
创建 ApplicationContext
不同应用类型创建不同容器:
| 应用类型 | ApplicationContext |
|---|---|
| 普通非 Web | AnnotationConfigApplicationContext |
| Servlet Web | AnnotationConfigServletWebServerApplicationContext |
| Reactive Web | AnnotationConfigReactiveWebServerApplicationContext |
Servlet Web 容器不仅有普通 IOC 能力,还能创建 WebServer。
如果你引入 spring-boot-starter-web,通常就是 Servlet Web 应用,默认会创建支持内嵌 Tomcat 的上下文。
加载启动类和自动配置
启动类上的 @SpringBootApplication 是组合注解:
flowchart TD
A["@SpringBootApplication"] --> B["@SpringBootConfiguration"]
A --> C["@ComponentScan"]
A --> D["@EnableAutoConfiguration"]
B --> E["启动类作为配置类"]
C --> F["扫描业务组件"]
D --> G["导入自动配置候选类"]这一步会让 Spring 知道:
- 启动类是配置类。
- 要从启动类所在包开始扫描组件。
- 要导入自动配置候选类。
启动类放错包是新手常见坑:
com.company.Application
com.company.user.UserController
com.company.order.OrderService可以扫到。
com.company.user.Application
com.company.order.OrderService默认可能扫不到 com.company.order。
自动配置发生在哪里
自动配置不是在某个神秘线程里发生。它发生在配置类解析阶段,最终仍然变成 BeanDefinition。
flowchart TD
A["@EnableAutoConfiguration"] --> B["AutoConfigurationImportSelector"]
B --> C["读取 spring.factories 或 AutoConfiguration.imports"]
C --> D["得到候选自动配置类"]
D --> E["处理 exclude、去重、排序"]
E --> F["条件注解判断"]
F --> G["注册 BeanDefinition"]
G --> H["refresh 阶段创建 Bean"]所以自动配置和 Spring IOC 是一条链:
自动配置负责把默认 Bean 定义放进容器,IOC 负责真正创建和管理 Bean。refresh 为什么是核心
ApplicationContext.refresh() 是 Spring 容器真正启动的核心。
可以拆成:
flowchart TD
A["refresh"] --> B["准备 BeanFactory"]
B --> C["执行 BeanFactoryPostProcessor"]
C --> D["注册 BeanPostProcessor"]
D --> E["初始化消息源和事件广播器"]
E --> F["Servlet Web 应用创建 WebServer"]
F --> G["实例化非懒加载单例 Bean"]
G --> H["完成容器刷新"]关键点:
- BeanDefinition 在这里被进一步处理。
- BeanPostProcessor 在这里注册,为 AOP、事务、生命周期增强做准备。
- 非懒加载单例 Bean 通常在这里实例化。
- Servlet Web 应用的 WebServer 也在刷新过程中创建。
如果启动卡住,很多时候就是卡在 refresh() 的某个阶段。
Bean 创建阶段发生什么
非懒加载单例 Bean 会在容器启动阶段创建。
大致流程:
flowchart TD
A["BeanDefinition"] --> B["实例化对象"]
B --> C["依赖注入"]
C --> D["Aware 回调"]
D --> E["BeanPostProcessor 初始化前"]
E --> F["@PostConstruct / init-method"]
F --> G["BeanPostProcessor 初始化后"]
G --> H["单例 Bean 可用"]这解释了启动慢的常见原因:
- 构造方法里访问外部系统。
@PostConstruct做大批量预热。- 初始化方法里执行慢 SQL。
- BeanPostProcessor 做大量扫描。
- 创建代理时依赖复杂。
商业项目里,Bean 初始化阶段要尽量轻,不要把不可控长任务塞进去。
内嵌 Tomcat 什么时候启动
Servlet Web 应用里,WebServer 通常在 refresh() 过程中创建。
简化链路:
refresh()
-> onRefresh()
-> createWebServer()
-> ServletWebServerFactory#getWebServer()
-> Tomcat 启动并绑定端口流程图:
flowchart TD
A["ServletWebServerApplicationContext"] --> B["refresh"]
B --> C["onRefresh"]
C --> D["createWebServer"]
D --> E["查找 ServletWebServerFactory"]
E --> F["TomcatServletWebServerFactory"]
F --> G["创建 Tomcat"]
G --> H["注册 Servlet、Filter、Listener"]
H --> I["绑定端口并启动"]关键 Bean:
| Bean | 作用 |
|---|---|
TomcatServletWebServerFactory | 创建内嵌 Tomcat |
DispatcherServlet | Spring MVC 前端控制器 |
DispatcherServletRegistrationBean | 把 DispatcherServlet 注册到 Servlet 容器 |
FilterRegistrationBean | 注册 Filter |
ServletRegistrationBean | 注册自定义 Servlet |
请求为什么能进入 Controller
启动成功后,请求链路大致是:
flowchart TD
A["客户端请求"] --> B["Tomcat Connector"]
B --> C["Filter 链"]
C --> D["DispatcherServlet"]
D --> E["HandlerMapping 找到 Controller 方法"]
E --> F["HandlerAdapter 调用方法"]
F --> G["参数解析、校验、业务调用"]
G --> H["HttpMessageConverter 写 JSON 响应"]这就是为什么 Spring Boot 启动流程不能只学到端口监听,还要知道 DispatcherServlet 是怎么注册进 WebServer 的。
Runner 和启动事件
Spring Boot 启动后常见两个 Runner:
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class CacheWarmupRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
System.out.println("启动完成后预热缓存");
}
}import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class StartupCheckRunner implements CommandLineRunner {
@Override
public void run(String... args) {
System.out.println("启动完成后检查依赖");
}
}区别:
| 扩展 | 参数 | 适合场景 |
|---|---|---|
ApplicationRunner | ApplicationArguments,参数更结构化 | 读取启动参数、轻量初始化 |
CommandLineRunner | 原始 String... args | 简单启动后任务 |
注意:Runner 仍然会影响应用“完全启动完成”的时间。不要在 Runner 里做不可控长任务。
常见启动事件
| 事件 | 大致时机 | 适合做什么 |
|---|---|---|
ApplicationStartingEvent | 刚开始启动 | 极少直接使用 |
ApplicationEnvironmentPreparedEvent | Environment 准备好 | 观察配置、日志准备 |
ApplicationPreparedEvent | Context 准备好但未 refresh | 观察容器早期状态 |
ApplicationStartedEvent | 容器已刷新,Runner 未执行 | 启动阶段统计 |
ApplicationReadyEvent | Runner 执行后,应用可服务 | 注册实例、轻量预热 |
ApplicationFailedEvent | 启动失败 | 告警、诊断日志 |
不要把所有初始化都放到一个事件里。要先判断你需要的资源是否已经准备好。
商业项目启动设计建议
不要在构造方法里访问外部系统
错误:
public HospitalClient() {
remoteServer.ping();
}后果:
- 外部系统慢会拖慢启动。
- 外部系统不可用会导致服务无法启动。
- 连接超时太长时排查困难。
更好的方式:
- 创建 Bean 时只设置配置。
- 健康检查负责判断依赖可用性。
- 必要预热放 Runner,并设置超时和降级。
字典预热不要无限阻塞
医疗采集平台常见启动动作:
- 加载医院字典。
- 恢复历史批次。
- 注册采集任务。
- 检查第三方接口。
这些都要有超时、日志、指标和失败策略。不能让一个医院接口慢,拖死整个服务启动。
启动慢怎么排查
flowchart TD
A["启动慢"] --> B["看日志卡在哪个阶段"]
B --> C["Environment/配置中心"]
B --> D["Bean 创建"]
B --> E["WebServer 启动"]
B --> F["Runner 执行"]
C --> G["配置中心网络、超时、Profile"]
D --> H["构造器、PostConstruct、慢SQL、外部调用"]
E --> I["端口占用、Filter/Servlet初始化"]
F --> J["缓存预热、字典加载、历史任务恢复"]排查清单:
- 先看最后一条日志停在哪。
- 开启
debug=true看自动配置报告。 - 看是否卡在配置中心拉取。
- 搜索项目里的
@PostConstruct、InitializingBean、ApplicationRunner。 - 查 Bean 构造方法里是否访问数据库、Redis、HTTP。
- 查连接池初始化超时。
- 查端口是否被占用。
- 接入 Actuator startup 端点或应用启动指标。
- 对启动后预热任务加耗时日志和超时。
常见启动失败
| 异常或现象 | 常见原因 | 排查方向 |
|---|---|---|
| 端口占用 | server.port 被占 | 换端口或释放端口 |
| Bean 找不到 | 扫描路径不对、条件不满足 | 启动类包位置、conditions |
| Bean 冲突 | 多个同类型 Bean | @Primary、@Qualifier、条件装配 |
| 配置绑定失败 | 类型错误、缺必填项 | /actuator/configprops、启动异常 |
| 数据源启动慢 | 数据库不可达、超时长 | 连接串、网络、连接池参数 |
| 自动配置没生效 | 依赖缺失、exclude、配置开关 | debug=true、conditions |
面试标准回答
Spring Boot 启动流程
Spring Boot 启动从 SpringApplication.run 开始,先创建 SpringApplication,推断应用类型,加载初始化器和监听器,然后准备 Environment、读取配置和 Profile,再根据应用类型创建 ApplicationContext。之后解析启动类、组件扫描和自动配置,把 BeanDefinition 注册到容器,执行 refresh() 创建非懒加载单例 Bean。Servlet Web 应用会在刷新过程中创建并启动内嵌 Tomcat,最后发布启动完成事件并执行 Runner。
refresh() 为什么重要
refresh() 是 Spring 容器真正启动的核心阶段,会准备 BeanFactory、执行 BeanFactoryPostProcessor、注册 BeanPostProcessor、初始化事件广播器、创建非懒加载单例 Bean。AOP、事务、生命周期回调、自动配置 Bean 的创建都离不开这个阶段。
内嵌 Tomcat 什么时候启动
Servlet Web 应用会创建 ServletWebServerApplicationContext,它在 refresh() 的 onRefresh() 阶段调用 createWebServer(),通过 ServletWebServerFactory 创建 Tomcat,注册 Servlet、Filter、Listener,最后绑定端口并开始接收请求。
Runner 适合做什么
ApplicationRunner 和 CommandLineRunner 在容器启动完成后执行,适合做轻量缓存预热、启动检查、注册状态等。它们不适合放不可控长任务,否则会拖慢启动甚至导致启动失败。
启动慢怎么排查
先看日志卡在哪个阶段,再区分是配置中心、Bean 创建、WebServer 启动还是 Runner 执行。重点检查构造器、@PostConstruct、初始化方法、数据源连接、外部 HTTP 调用、配置中心、端口占用和启动后预热任务。可以配合 debug=true、Actuator startup、线程栈和耗时日志定位。
