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。它先完成一组容器之外的准备工作:
- 判断应用类型,例如 Servlet Web、Reactive Web 或非 Web 应用。
- 注册初始化器和监听器,发布早期启动事件。
- 根据命令行、环境变量和配置文件创建
Environment。 - 绑定影响启动本身的
spring.main.*等属性。 - 选择并创建相应的
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 会依次调用 ApplicationRunner 和 CommandLineRunner,随后发布应用已就绪的事件并更新可用性状态。
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,而不是只从最后一行异常开始看。