跳到主要内容

Spring Boot 启动流程

Spring Boot 启动的主要工作,是准备运行环境、创建并刷新 Spring 容器,再让 Web 服务器和应用初始化任务进入可服务状态。理解这条路径后,才能判断一个启动错误发生在配置加载、Bean 创建,还是应用就绪阶段。

1. 启动入口提供配置来源

@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}

@SpringBootApplication 组合了三个职责:

  • @SpringBootConfiguration:把当前类标记为应用配置来源。
  • @EnableAutoConfiguration:根据类路径、配置和已有 Bean 导入自动配置。
  • @ComponentScan:从应用类所在包向下扫描组件。

启动类的位置会影响默认扫描范围。把它放进过深的业务包,可能让其他模块的组件无法被发现;直接扩大到根包,又可能扫描无关类型。

2. SpringApplication 先准备运行环境

run 方法不会立即创建所有 Bean。它先完成一组容器之外的准备工作:

  1. 判断应用类型,例如 Servlet Web、Reactive Web 或非 Web 应用。
  2. 注册初始化器和监听器,发布早期启动事件。
  3. 根据命令行、环境变量和配置文件创建 Environment
  4. 绑定影响启动本身的 spring.main.* 等属性。
  5. 选择并创建相应的 ApplicationContext

有些事件发生在 ApplicationContext 创建之前,因此仅把监听器声明为 @Bean 无法收到全部早期事件。

3. refresh 建立完整的容器

准备好上下文后,Spring Boot 会把启动类注册为配置来源,并调用 Spring Framework 的 refresh()。容器在这个阶段执行 BeanFactory 后处理器、解析配置类、注册 BeanDefinition、创建非懒加载单例、执行 Bean 生命周期回调并发布上下文刷新事件。

自动配置会和用户配置一起形成最终的 BeanDefinition 集合,处理时机位于配置类解析阶段,并不等待所有业务 Bean 创建完成。

备注

BeanDefinition 注册顺序与 Bean 实例创建顺序不是一回事。实例的先后主要由依赖关系、@DependsOn 和是否懒加载决定。

4. Web 服务器在容器刷新期间启动

Servlet 应用使用专门的 WebApplicationContext。Tomcat、Jetty 等嵌入式服务器由容器中的工厂创建,并在刷新流程中启动。Controller、Servlet、Filter 和监听器也会在这里完成注册。

因此,日志中出现端口监听并不表示全部业务初始化都已完成。服务器启动失败通常要继续检查端口占用、连接器配置、证书和 WebServerFactory;Bean 创建失败则沿依赖链寻找最早的 cause。

5. Runner 执行完后应用才进入就绪阶段

容器刷新成功后,Spring Boot 会依次调用 ApplicationRunnerCommandLineRunner,随后发布应用已就绪的事件并更新可用性状态。

Runner 适合做必须在接收流量前完成、且耗时可控的初始化。大批量数据回填、无限重试或无法设置超时的远程调用会拉长启动时间,也可能让进程已经存活却长期无法就绪。

如果任务不影响服务接流量,应考虑放到后台任务、独立迁移流程或显式运维操作中。

6. 启动成功、存活与就绪需要区分

  • 进程启动:JVM 已运行,不代表 Spring 容器可用。
  • 上下文刷新完成:核心 Bean 已创建,仍可能正在执行 Runner。
  • Liveness 正常:应用内部状态可恢复,不应仅因数据库暂时不可用就触发重启。
  • Readiness 正常:应用已经可以接收流量。

部署平台应根据 readiness 决定是否转发请求,根据 liveness 判断是否需要重启。把同一个外部依赖同时加入两种探针,依赖故障时可能让所有实例反复重启。

7. 用证据定位启动耗时

先记录总启动时间,再定位慢在哪一段:

  • 使用 ApplicationStartup 采集 Spring 启动步骤。
  • BufferingApplicationStartup 可以把步骤暴露给 Actuator 的 startup 端点。
  • FlightRecorderApplicationStartup 可把 Spring 步骤与类加载、分配、GC 等 JFR 事件关联。
  • 对 Runner、数据库迁移、连接池初始化和远程调用单独记录耗时与超时。

全局开启懒加载可以缩短启动时间,但会把配置错误和首次初始化成本推迟到请求阶段。先确认慢点,再决定是否对少量 Bean 使用懒加载。

8. 常见问题

8.1 为什么修改 spring.main.*@PropertySource 没有效果

@PropertySource 在 ApplicationContext 刷新时才加入 Environment,而部分日志和 spring.main.* 属性在刷新前已经读取。此类配置应放在配置数据、环境变量、系统属性或命令行中。

8.2 ApplicationReadyEvent 适合承载长期任务吗

不适合直接阻塞事件线程。监听器尚未返回时,调用方很难准确判断初始化是否完成;长期任务还缺少清晰的取消、重试和关闭协议。应交给受管理的执行器或独立作业,并暴露运行状态。

9. 面试题

9.1 Spring Boot 从 main 方法到应用就绪经历了什么

出现公司:字节跳动

考察重点

  • Environment、ApplicationContext 与 refresh() 的顺序。
  • 自动配置、Bean 生命周期和嵌入式服务器所在阶段。
  • Runner、启动事件与 readiness 的边界。

相关内容:第 1 节“启动入口提供配置来源”至第 7 节“用证据定位启动耗时”。

参考回答

SpringApplication.run 先判断应用类型,准备监听器、Environment 和配置属性,再创建对应的 ApplicationContext。启动类作为配置来源进入容器,自动配置与用户配置一起解析;随后调用 refresh(),执行后处理器、注册并实例化 Bean,同时在 Web 应用中创建嵌入式服务器。

上下文刷新成功后,Spring Boot 调用 ApplicationRunner 和 CommandLineRunner,再发布就绪事件并更新 availability。排查启动问题时,要先判断失败位于配置加载、Bean 创建、WebServer 启动还是 Runner,而不是只从最后一行异常开始看。