Spring Boot 的启动流程是怎样的?
简化版
SpringApplication.run() 会判断应用类型、准备 Environment、创建并刷新合适的 ApplicationContext,在刷新阶段完成配置类解析、自动配置、Bean 创建以及 Web 服务器启动。上下文刷新成功后发布相应启动事件并执行 ApplicationRunner、CommandLineRunner,全部完成后应用才进入 ready 状态。
详细版
主流程可概括为:创建 SpringApplication → 准备启动监听与 Environment → 打印 Banner → 创建 ApplicationContext → 加载主配置源 → refresh() 容器 → 启动内嵌服务器 → 发布 started 事件 → 执行 Runner → 发布 ready 事件。任一关键阶段失败会触发失败处理并关闭未完成的上下文。
真正的大量工作发生在 refresh():调用 BeanFactory 后处理器解析配置类和自动配置,注册 BeanPostProcessor,初始化事件广播器,创建非懒加载单例,并完成 Web 上下文的服务器创建。Runner 位于容器刷新之后,不适合承担容器注册 Bean 的工作。
完整版教学
一、入口不只是创建一个容器
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
SpringApplication 会根据类路径判断应用大致属于非 Web、Servlet Web 还是 Reactive Web,并据此选择 ApplicationContext 类型。判断是启动默认策略,不代表类路径混入不需要的 Web 依赖没有影响。
二、先准备 Environment
在创建和刷新容器前,Boot 会准备 Environment,把命令行、系统属性、环境变量、配置文件等属性源合并,并处理激活的 Profile。许多自动配置条件和 Bean 属性都依赖这些结果,因此 Environment 必须先于大部分 Bean 创建准备好。
需要在 BeanDefinition 加载前修改环境的扩展,应使用对应的 Environment 后处理机制,而不是等普通 Bean 初始化后再改配置;后者通常已经错过条件判断和绑定时机。
三、创建并准备 ApplicationContext
Boot 创建合适的上下文,将主类等 primary sources 注册为配置源,应用初始化器也会在刷新前参与配置。随后进入 Spring Framework 的 refresh() 模板流程。
主类上的组件扫描、配置类导入和自动配置都在配置类解析过程中转化为 BeanDefinition。@EnableAutoConfiguration 只是候选自动配置的入口,最终是否注册仍由条件判断决定。
四、refresh 为什么是核心
刷新阶段会依次完成 BeanFactory 准备、BeanFactoryPostProcessor 调用、BeanPostProcessor 注册、事件与国际化基础设施初始化,以及非懒加载 singleton 的创建。依赖注入、生命周期回调和 AOP 代理都在这条容器主线上发生。
Servlet Web 应用还会创建 WebServer 并把 DispatcherServlet 等组件接入服务器。端口占用、Bean 创建异常或自动配置失败,通常都发生在上下文刷新尚未成功完成时。
五、Runner 和启动事件的顺序
上下文刷新完成后,Boot 发布“上下文已启动”阶段的事件,然后按顺序调用 ApplicationRunner 和 CommandLineRunner。两者都用于容器就绪后的启动任务,区别主要是参数形式:前者接收解析后的 ApplicationArguments,后者接收原始字符串数组。
所有 Runner 正常执行完成后才发布 ApplicationReadyEvent。因此耗时 Runner 会延迟 ready;Runner 抛异常也会使启动失败。多个 Runner 可实现 Ordered 或使用 @Order 明确顺序。
六、如何放置启动逻辑
Bean 自身初始化约束可放在 @PostConstruct 或初始化回调,但此时整个应用未必已经 ready;需要访问所有 Bean 的应用级初始化可使用 Runner。向注册中心报告可用、预热缓存或执行迁移时,还要考虑失败策略、超时、幂等与是否阻塞就绪状态。
不要把所有逻辑都塞进 main 方法。选择扩展点的标准是它所需的上下文是否已经准备好,以及失败应不应该阻止应用启动。
七、把事件与可用状态放到时间线上
官方事件顺序能解释“容器已刷新”为什么不等于“应用已经接流量”。ApplicationStartedEvent 在 refresh 完成后、Runner 前发布,随后应用进入 live;所有 Runner 完成后才发布 ApplicationReadyEvent,之后 readiness 才进入接受流量状态。假设 refresh 用 4 秒、3 个 Runner 分别用 1、2、5 秒且串行执行,ready 最早约在 12 秒后,而不是第 4 秒。
Starting
→ EnvironmentPrepared
→ ContextInitialized
→ Prepared
→ refresh()
→ BeanDefinition / BPP / singleton / WebServer
→ Started
→ Liveness CORRECT
→ ApplicationRunner + CommandLineRunner
→ Ready
→ Readiness ACCEPTING_TRAFFIC
失败任一点 → ApplicationFailedEvent
| 扩展点 | 所处阶段 | 适合任务 | 不适合任务 |
|---|---|---|---|
| EnvironmentPostProcessor | 上下文创建前 | 增加或修改早期属性源 | 依赖普通业务 Bean |
| ApplicationContextInitializer | refresh 前 | 调整上下文 | 假设所有 Bean 已创建 |
| Bean 初始化回调 | 单个 Bean 创建中 | 校验自身配置、准备内部结构 | 访问全部单例或长耗时外部任务 |
| Runner | refresh 后、ready 前 | 启动任务、必要预热 | 无边界的长期阻塞任务 |
| Ready 事件监听 | Runner 后 | ready 后通知 | 必须阻止 ready 的初始化 |
监听早期 SpringApplicationEvent 时,上下文可能尚不存在,因此不能只靠一个普通 @Bean 监听器注册所有阶段。事件监听默认也可能在启动线程执行,长任务会直接拉长启动时间。
启动记忆钩子:refresh 成功代表容器成立,Started 代表 Runner 将开始,Ready 才代表 Runner 已结束;live 与 ready 不要混为一谈。
八、常见误区与追问
- 误区:ApplicationStartedEvent 就表示应用已经 ready。 它发生在 Runner 之前,ApplicationReadyEvent 才位于所有 Runner 正常完成之后。
- 误区:所有启动逻辑都应该放在 @PostConstruct。 此时 Bean 仍处于容器创建链中,长耗时或访问其他未就绪 Bean 会增加启动和死锁风险。
- 误区:开启 lazy initialization 只会让启动更快,没有代价。 它会把配置错误推迟到首次访问,还需为最终全部 Bean 预留足够内存。
- 追问:ApplicationRunner 与 CommandLineRunner 有何区别? 执行阶段相同,前者接收解析后的 ApplicationArguments,后者接收原始字符串数组。
- 追问:怎样分析启动慢在哪里? 可配置 ApplicationStartup 收集 StartupStep,结合 JFR、startup 端点或日志定位具体阶段,而不是只看总耗时。
- 追问:Runner 抛异常会怎样? run 流程进入失败处理并发布失败事件,应用不会正常进入 ready 状态。
九、加强记忆
启动主线是“先环境、再容器、刷新时创建 Bean 和服务器、刷新后执行 Runner、最后 ready”。自动配置和 Bean 生命周期都落在 ApplicationContext 刷新阶段,Runner 则是容器成功刷新后的应用级启动任务。