SpringBoot(二):启动流程与核心机制
SpringBoot(二):启动流程与核心机制
导语:
SpringApplication.run()那一行代码背后,是"推断应用类型 → 准备环境 → 创建容器 → refresh → 启动内嵌容器 → 发布就绪事件"的完整链路。本篇把它拆到方法级,并回答:启动事件体系怎么用、ApplicationRunner与CommandLineRunner谁先谁后、启动失败怎么定位、如何把启动时间从 30 秒压到 3 秒,最后补上 AOT/Native 与优雅停机,共 12 题。
一、启动流程
1. SpringApplication.run() 的完整启动流程是怎样的?
答: 拆成构造与运行两大阶段:
(1)构造 SpringApplication(new SpringApplication(...)):
deduceFromClasspath()推断应用类型:Servlet(有DispatcherServlet且无DispatcherHandler)/Reactive/None;setInitializers():从spring.factories/.imports读取ApplicationContextInitializer;setListeners():读取ApplicationListener;deduceMainApplicationClass():通过调用栈推导主类(用于打印 Banner/日志)。
(2)运行 run(args):
① 启动计时、创建 BootstrapContext(Boot 3.0+)、配置 headless
② getRunListeners(args) → 获取并启动 SpringApplicationRunListener(广播事件的中枢)
③ 发布 ApplicationStartingEvent
④ prepareEnvironment():创建 Environment、加载配置文件与命令行参数、发布 ApplicationEnvironmentPreparedEvent
⑤ printBanner()
⑥ createApplicationContext():按应用类型创建容器(Servlet → AnnotationConfigServletWebServerApplicationContext)
⑦ prepareContext():设置 Environment、注册 BeanNameGenerator/资源加载器、执行 ApplicationContextInitializer、
发布 ApplicationContextInitializedEvent、注册主配置类(primarySources)、发布 ApplicationPreparedEvent
⑧ refreshContext():调用 AbstractApplicationContext#refresh()(Bean 全量创建、自动配置解析)
· Web 应用在 onRefresh() 阶段 createWebServer() 创建并启动内嵌 Tomcat
⑨ afterRefresh():空实现(留给扩展)
⑩ stopWatch.stop()、发布 ApplicationStartedEvent → 调用 ApplicationRunner / CommandLineRunner
⑪ 发布 ApplicationReadyEvent(应用就绪)
⑫ 异常时:handleRunFailure() 发布 ApplicationFailedEvent、reportFailure() 用 FailureAnalyzer 输出友好报错核心理解:
SpringApplication.run()做的事,本质上是"把 Spring 的refresh()包裹进一套环境准备 + 事件广播 + 容器创建的脚手架"。所以"Spring Boot 启动流程"和"Spring 容器初始化流程"是包含关系——第 ⑧ 步就是《Spring(一)》第 3 题的那个refresh()。
2. 内嵌容器是什么时候启动的?怎么做到"随应用一起启动"?
答: 关键在 refresh() 的 onRefresh() 钩子:
refresh()
→ onRefresh() // ServletWebServerApplicationContext 重写
→ createWebServer() // 由 ServletWebServerFactory 创建
→ getWebServerFactory() // 从容器中取 TomcatServletWebServerFactory(自动配置注册)
→ getWebServer(...) // 创建 Tomcat 实例并 setConnector/setContext
→ webServer.start() // 【真正启动端口监听】
→ finishRefresh()
→ 发布 ServletWebServerInitializedEvent // 携带端口号要点:
- 容器是在 BeanFactory 准备完成、单例创建过程中启动的,因此 Tomcat 里的
Servlet/Filter能拿到完整的 Spring 容器; - 端口在启动后才确定(
server.port=0时为随机端口),可通过ServletWebServerInitializedEvent或@LocalServerPort获取; ServletWebServerFactory是自动配置注册的 Bean(ServletWebServerFactoryAutoConfiguration),因此"换容器"本质就是"换这个 Factory 的实现"。
3. Spring Boot 的启动事件体系是怎样的?有哪些扩展点?
答: 事件按启动顺序发布,配合 ApplicationListener / SpringApplicationRunListener 使用:
| 事件 | 时机 | 典型用途 |
|---|---|---|
ApplicationStartingEvent | 应用启动最初(Environment 前) | 早期初始化、监听器注册 |
ApplicationEnvironmentPreparedEvent | Environment 就绪、容器创建前 | 动态修改配置文件/激活 profile |
ApplicationContextInitializedEvent | 容器创建完成、BeanDefinition 加载前 | 修改容器属性 |
ApplicationPreparedEvent | 主配置类注册完成、refresh 前 | 追加 BeanDefinition |
ApplicationStartedEvent | refresh 完成、Runner 执行前 | 启动后前置逻辑 |
ApplicationReadyEvent | Runner 执行完成、应用就绪 | 预热缓存、注册服务、打印启动信息 |
ApplicationFailedEvent | 启动异常 | 异常告警、资源清理 |
两种注册方式:
spring.factories/.imports中声明org.springframework.context.ApplicationListener(最早可用,但也可在ApplicationStartingEvent之后用addListeners);SpringApplicationBuilder#listeners(...)或SpringApplication.addListeners(...)。
顺序要点(高频):
ApplicationRunner/CommandLineRunner在ApplicationStartedEvent之后、ApplicationReadyEvent之前执行——所以跑完 Runner 才代表"应用真的就绪"。
4. ApplicationRunner 和 CommandLineRunner 有什么区别?
答:
ApplicationRunner | CommandLineRunner | |
|---|---|---|
| 入参 | ApplicationArguments(可解析 --key=value、非选项参数) | String... args(原始数组,需自己解析) |
| 推荐 | ✅ 推荐(参数解析更规范) | 简单场景 |
共同点与注意:
- 都在 容器
refresh()完成之后执行(因此所有 Bean 都可用); - 多个 Runner 用
@Order/Ordered控制顺序(值越小越先执行); - Runner 中抛异常会导致启动失败(
ApplicationFailedEvent),因此要谨慎——不要在里面做重活或强依赖(如预热远程连接),否则会把"应用能启动"和"下游可用"强绑定,放大故障; - 需要"更早"执行 → 用
ApplicationListener<ApplicationStartedEvent>或SmartInitializingSingleton。
5. Spring Boot 如何推断应用类型(Servlet / Reactive / None)?
答: WebApplicationType.deduceFromClasspath():
类路径有 spring-webflux + DispatcherHandler,且【没有】spring-webmvc + DispatcherServlet → REACTIVE
类路径有 spring-webmvc + DispatcherServlet → SERVLET
两者都没有 → NONE(普通应用)
两者都有 → SERVLET(MVC 优先)推论(面试常问):
- 同时引入
spring-boot-starter-web和spring-boot-starter-webflux会启动 MVC;若要强制用 WebFlux,需SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)(或排除spring-webmvc); spring.main.web-application-type=servlet|reactive|none可显式指定;- 应用类型决定了创建哪种
ApplicationContext、以及是否启动内嵌 Web 容器。
6. 如何定制启动行为?(Banner、Builder、关闭某些特性)
答:
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class)
.bannerMode(Banner.Mode.OFF) // 关闭 Banner(生产常用)
.web(WebApplicationType.SERVLET)
.profiles("prod")
.lazyInitialization(true) // 谨慎:会把启动期错误推迟到运行期
.run(args);
}| 定制项 | 说明 |
|---|---|
| Banner | bannerMode(OFF/CONSOLE/LOG);自定义 banner.txt 或 SpringBootBanner |
spring.main.* | banner-mode、web-application-type、lazy-initialization、log-startup-info |
addInitializers / addListeners | 追加初始化器与监听器 |
setDefaultProperties | 最低优先级的默认属性 |
实践:生产建议关闭 Banner、开启启动信息日志(
log-startup-info=true会打印 Bean 数量/启动耗时),并不要全局开启懒加载。
7. 启动失败时,Spring Boot 是如何给出"友好报错"的?
答: 靠 FailureAnalyzer + FailureAnalysisReporter:
run()捕获异常后调用handleRunFailure()→reportFailure();- 遍历容器(及
spring.factories)中的FailureAnalyzer实现,找到能识别该异常的分析器,输出可读的失败原因 + 可行的修复建议(Action); - 常见分析器:
PortInUseFailureAnalyzer(端口占用)、BeanCreationFailureAnalyzer、NoSuchBeanDefinitionFailureAnalyzer、DataSourceBeanCreationFailureAnalyzer(数据源配置错误)、InvalidConfigurationPropertyValueFailureAnalyzer; - 找不到匹配分析器时,回退打印原始异常链。
自我扩展:自定义 FailureAnalyzer 并注册到 .imports,可为企业内部异常提供统一排查提示。
实用技巧:报错时看第一段
*************************** APPLICATION FAILED TO START ***************************的描述,通常已经直接告诉你哪个 Bean、什么原因、怎么改。
8. 如何加快 Spring Boot 的启动速度?
答: 按"收益/成本"排序:
| 手段 | 说明 | 适用 |
|---|---|---|
| 减少自动配置 | 用 exclude 排除无用自动配置;用 @Conditional 精准开关 | 通用,立即见效 |
| 按需引入 Starter | 别把 data-jpa + security + cloud 全塞进一个服务 | 通用 |
spring-context-indexer | 编译期生成 META-INF/spring.components,跳过类路径扫描 | 组件多的项目 |
| 延迟初始化 | spring.main.lazy-initialization=true / @Lazy | 谨慎:会推迟错误暴露、首次请求变慢 |
| 类数据共享(CDS)/ AppCDS | JVM 层面共享类元数据,减少类加载 | 启动敏感场景 |
| AOT 处理 | Boot 3 的 processAot 在构建期生成 BeanDefinition/代理类,减少运行期反射与条件评估 | Boot 3 + Native/JVM 均可 |
| GraalVM Native Image | 编译成原生可执行文件,启动从秒级降到几十毫秒 | 函数计算/Serverless、CLI |
| 异步/并行初始化 | spring.main.lazy-initialization 之外的 bootstrapExecutor(自定义 AbstractApplicationContext)、@Bean 拆解 | 高级优化 |
注意:启动加速不能牺牲正确性——延迟初始化会把"启动期失败"变成"运行期故障";Native Image 需处理反射/资源/JDK 代理的
RuntimeHints,并要注意第三方库兼容性。
二、AOT、Native 与生产就绪
9. 什么是 AOT 与 GraalVM Native Image?二者关系是什么?
答:
- AOT(Ahead-Of-Time)处理:在构建期提前完成原本运行期要做的事——解析
@Configuration、生成 BeanDefinition、生成 CGLIB 代理类、生成反射/资源 Hint,产物是编译后的 Java 代码 + 元数据。 - GraalVM Native Image:把应用(含 AOT 产物)编译成本地可执行文件,运行时不再需要 JIT 编译和类加载,因此启动极快、内存占用更低。
- 关系:AOT 是 Native Image 的前置条件,但 AOT 也可以单独用于 JVM 模式(加快启动、减少反射)。
收益与代价:
| 收益 | 代价 | |
|---|---|---|
| AOT | 启动更快、反射更少、可配合 Native | 构建更慢、部分动态特性受限 |
| Native Image | 启动毫秒级、内存小、适合 Serverless | 构建数分钟、峰值构建内存高、需处理 Hint、调试与生态兼容成本 |
迁移要点(加分): 反射/资源/JDK 代理/序列化需通过 @RegisterReflectionForBinding、RuntimeHintsRegistrar、@ImportRuntimeHints 显式登记;自定义动态特性需改造成构建期可知。
10. 如何实现优雅停机(Graceful Shutdown)?
答: 核心目标:停止接收新请求 → 等待正在处理的请求完成 → 再释放资源退出。
server:
shutdown: graceful # 启用优雅停机(Boot 2.3+,默认 immediate)
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 每阶段最大等待时间,超时强制关闭生效范围与注意:
- Web 容器层:Tomcat/Jetty/Undertow/Netty 会停止接收新连接并等待在途请求;
SmartLifecycle组件:按 phase 逆序停止(先停高 phase,再停低 phase),适合 MQ 消费者、线程池、定时任务的自定义停机逻辑;- 不覆盖所有场景:异步线程池中未完成的任务、MQ 消费中未 ack 的消息,需要自己在
@PreDestroy/SmartLifecycle#stop中 drain; - 与 K8s 配合:
terminationGracePeriodSeconds必须 ≥timeout-per-shutdown-phase,并配好preStop与就绪探针(先从 Service 摘除再停机),否则流量仍会打到正在关闭的 Pod。
一句话:优雅停机 = "先摘流量,再等存量,最后退出";少了任何一步都会出现"用户请求 502"或"消息重复/丢失"。
11. Spring Boot 的启动流程与 Spring 的 refresh() 是什么关系?
答: 包含关系,这是把两块知识打通的关键:
| 阶段 | 由谁负责 |
|---|---|
| 应用类型推断、Environment 准备、容器创建、事件广播、Runner 调用 | Spring Boot(SpringApplication) |
BeanDefinition 加载、BeanFactoryPostProcessor、BeanPostProcessor、单例创建、AOP 代理 | Spring Framework(AbstractApplicationContext#refresh()) |
| 内嵌 Web 容器的创建与启动 | Spring Boot 通过重写 onRefresh() 挂进 refresh() 流程 |
| 自动配置的解析 | Boot 的 AutoConfigurationImportSelector 被 ConfigurationClassPostProcessor 在 invokeBeanFactoryPostProcessors 阶段解析 |
答题模板:被问"Spring Boot 启动流程"时,先讲 Boot 的十二步骨架,再明确点出"第 8 步的 refresh() 就是 Spring 容器的初始化流程"——这样既显体系感,又能自然衔接《Spring(一)》。
12. 生产环境下 Spring Boot 启动还需要注意什么?
答: 一份"启动期检查清单":
| 关注点 | 建议 |
|---|---|
| 启动期依赖 | 不要在启动时强依赖下游可用(ApplicationRunner 里连远程 = 把可用性绑定在一起);探活与预热解耦 |
| 健康检查 | 暴露 /actuator/health,区分 liveness(存活)与 readiness(就绪);就绪探针必须在流量接入前通过 |
| 启动耗时 | 记录并告警(启动 > 60s 常见于组件扫描过多、DB 连接超时、DNS 解析慢) |
| 配置外部化 | 不同环境用外部配置/ConfigMap,不要打多套包 |
| 时区与编码 | 显式设置 -Duser.timezone、file.encoding=UTF-8,避免跨环境不一致 |
| JVM 容器参数 | -XX:MaxRAMPercentage + -XX:ActiveProcessorCount(见《高性能(二)》第 9 题) |
| 优雅停机 | server.shutdown=graceful + K8s preStop/terminationGracePeriodSeconds 对齐 |
| 启动日志 | 保留 Banner 关闭、开启 log-startup-info,便于排障与容量观测 |
本章小结:把
SpringApplication.run()记成"准备环境 → 创建容器 → refresh(含启动 Tomcat)→ 广播就绪"四段式,再记住"refresh 就是 Spring 的初始化流程",启动流程类的追问基本都能顺着答下去。
