引言: 这一行代码背后发生了什么
每一个 Spring Boot 应用都从 SpringApplication.run(MyApplication.class, args) 开始。这一行代码背后,Spring Boot 完成了:推断应用类型(Servlet/Reactive/None)、创建 Environment 并加载 17 级 PropertySource、创建 ApplicationContext、执行 refresh() 刷新容器、注册并启动嵌入式 Web 服务器、执行 Runner 回调——整个过程涉及 20+ 个步骤,每一步都有明确的扩展点和可观测性。这篇文章把这条启动链路完整拆开,从 SpringApplication 构造到 ApplicationContext 刷新再到 Runner 执行,讲清楚每一步在做什么、为什么这样设计、以及哪些环节可以扩展。
一、SpringApplication 的构造:推断应用类型与初始化引导注册表


推断应用类型
new SpringApplication(MyApplication.class) 的构造函数中,Spring Boot 做的第一件事是推断应用类型。它通过检查 classpath 上是否存在特定类来判断:
// SpringApplication 构造函数中的逻辑(简化)
this.webApplicationType = WebApplicationType.deduceFromClasspath();
| Classpath 上的类 | 推断结果 | 说明 |
|---|---|---|
存在 DispatcherServlet 且不存在 DispatcherHandler |
SERVLET |
传统 Servlet Web 应用 |
存在 DispatcherHandler 且不存在 DispatcherServlet |
REACTIVE |
WebFlux 响应式应用 |
| 两者都不存在 | NONE |
非 Web 应用(如批处理) |
这个推断直接影响后续两个关键决策:创建什么类型的 ApplicationContext(AnnotationConfigServletWebServerApplicationContext 还是 AnnotationConfigReactiveWebServerApplicationContext),以及是否启动嵌入式 Web 服务器。
初始化引导注册表
构造函数的第二步是加载 META-INF/spring.factories 中的 ApplicationContextInitializer 和 ApplicationListener:
this.initializers = getSpringFactoriesInstances(ApplicationContextInitializer.class);
this.listeners = getSpringFactoriesInstances(ApplicationListener.class);
这一步通过 SpringFactoriesLoader 从 classpath 上所有 jar 包的 spring.factories 文件中读取 SPI 扩展,实例化后存入 initializers 和 listeners 列表。这些初始化器和监听器会在后续的启动流程中被逐个调用。
推断主类
构造函数还会从调用栈中推断 main 方法所在的类(mainApplicationClass),这个类会被用作 @SpringBootApplication 注解的载体,用于后续的 @ComponentScan 范围确定。
二、SpringApplication.run() 的全流程:从启动到就绪


run() 方法内部按顺序执行以下步骤:
1. 创建 StopWatch 并启动
StopWatch stopWatch = new StopWatch();
stopWatch.start();
这个 StopWatch 记录了整个启动过程的耗时,最后在 ApplicationStartedEvent 日志中输出。
2. 创建 BootstrapContext
Spring Boot 2.4+ 引入的 BootstrapContext 是一个独立的引导上下文,在 ApplicationContext 创建之前存在,用于承载"引导阶段"需要的 Bean(如 BootstrapRegistryInitializer 注册的 Bean)。引导完成后,BootstrapContext 会被关闭,其中的 Bean 会被迁移到主 ApplicationContext 或直接丢弃。
3. 设置 java.awt.headless
对于非 Windows 环境,System.setProperty("java.awt.headless", "true") 确保在没有图形界面的服务器环境中,AWT 相关的类也能正常工作(比如生成验证码图片)。
4. 获取并启动 SpringApplicationRunListeners
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, mainApplicationClass);
SpringApplicationRunListener 是启动流程的"生命周期钩子",通过 spring.factories 加载。默认实现是 EventPublishingRunListener,它会在每个启动阶段广播对应的 SpringApplicationEvent(如 ApplicationStartingEvent、ApplicationEnvironmentPreparedEvent 等)。
5. 准备 Environment
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
这是整个启动流程中最重要的步骤之一。prepareEnvironment() 做了三件事:创建 Environment、添加命令行参数到 PropertySource、添加 ConfigurationPropertySources 作为最高优先级的 PropertySource。关于 Environment 的详细机制,参见 098 号文章。
6. 打印 Banner
Banner printedBanner = printBanner(environment);
默认打印 Spring Boot 的 ASCII 艺术字 Banner,可以通过 spring.banner.location 自定义,或设置 spring.main.banner-mode=off 关闭。
7. 创建 ApplicationContext
context = createApplicationContext();
根据第一步推断的 webApplicationType 创建对应的 ApplicationContext 实现类。对于 SERVLET 类型,创建 AnnotationConfigServletWebServerApplicationContext。
8. 准备 ApplicationContext
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
这一步做了大量工作:设置 Environment、注册 ApplicationContextInitializer、调用所有 initializers、加载 sources(即 @SpringBootApplication 标注的主类)、注册 SpringApplicationArguments 和 banner 为 Bean、触发 ApplicationContextInitializedEvent。
9. 刷新 ApplicationContext
refreshContext(context);
这是 Spring 容器启动的核心——调用 AbstractApplicationContext.refresh() 方法。这个 12 步方法完成了 Bean 定义的加载、BeanFactoryPostProcessor 的处理、Bean 的实例化和初始化、ApplicationEventMulticaster 的创建等。refresh() 是 Spring Framework 的核心链路,不是 Spring Boot 特有的,但 Spring Boot 在 refresh() 过程中通过 ServletWebServerApplicationContext 在 onRefresh() 阶段创建了嵌入式 Web 服务器。
10. 记录启动耗时
stopWatch.stop();
11. 发布 ApplicationReadyEvent
listeners.ready(context, startupStep);
这是整个启动流程的最后一个事件,标志应用已完全就绪,可以接收外部请求。很多"启动后一次性初始化"的逻辑(如缓存预热)都监听这个事件。
12. 调用 Runners
callRunners(context, applicationArguments);
三、ApplicationRunner 与 CommandLineRunner:启动后的回调

ApplicationRunner 和 CommandLineRunner 是 Spring Boot 提供的两个启动后回调接口。它们的区别在于参数类型:
// CommandLineRunner:接收原始 String[ ] 参数
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) {
// args 是原始命令行参数
}
}
// ApplicationRunner:接收封装后的 ApplicationArguments 对象
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// args.getOptionNames() 获取选项名
// args.getOptionValues("name") 获取选项值
// args.getNonOptionArgs() 获取非选项参数
}
}
ApplicationRunner 的 ApplicationArguments 是一个更友好的封装,它解析了命令行参数中的 --key=value 格式,提供了 getOptionNames()、getOptionValues() 等方法。CommandLineRunner 接收的 String... args 是原始的字符串数组,不做任何解析。
执行顺序
多个 Runner 之间可以通过 @Order 注解控制执行顺序:
@Component
@Order(1)
public class CacheWarmUpRunner implements ApplicationRunner { ... }
@Component
@Order(2)
public class DataInitRunner implements ApplicationRunner { ... }
所有 Runner 都在 ApplicationReadyEvent 发布之后、main 方法返回之前执行。如果 Runner 中抛出异常,启动会失败。
Runner 的使用场景
- 缓存预热:启动后加载热点数据到 Redis 或本地缓存。
- 数据初始化:检查数据库中的必需数据(如默认配置项),不存在则插入。
- 健康检查:验证依赖的外部服务是否可达。
- 注册服务:向注册中心(如 Nacos、Eureka)注册服务实例。
四、启动流程中的事件广播

Spring Boot 在启动流程中发布了一系列事件,你可以通过 ApplicationListener 或 @EventListener 在特定阶段执行自定义逻辑:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
ApplicationStartingEvent |
启动刚开始,Environment 尚未创建 | 日志初始化、提前注册监听器 |
ApplicationEnvironmentPreparedEvent |
Environment 创建完成 | 根据环境变量调整配置 |
ApplicationContextInitializedEvent |
ApplicationContext 创建完成,但尚未 refresh | 注册额外的 Bean 定义 |
ApplicationPreparedEvent |
refresh 之前,Bean 定义已加载 | 最后的机会修改 Bean 定义 |
ApplicationStartedEvent |
refresh 完成,但 Runner 尚未执行 | 启动成功后的日志 |
ApplicationReadyEvent |
应用完全就绪,可以接收请求 | 缓存预热、服务注册 |
ApplicationFailedEvent |
启动过程抛出异常 | 启动失败告警 |
五、自定义启动流程

Spring Boot 的启动流程可以通过多种方式扩展:
1. ApplicationContextInitializer
在 ApplicationContext refresh 之前执行,可以修改 Environment 或注册 BeanFactoryPostProcessor:
public class MyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
// 在 refresh 之前修改 Environment
context.getEnvironment().getSystemProperties().put("my.key", "value");
}
}
注册方式:在 META-INF/spring.factories 中声明,或通过 SpringApplication.addInitializers() 编程式添加。
2. SpringApplicationRunListener
监听启动流程的每个阶段,可以在每个阶段之间插入自定义逻辑:
public class MyRunListener implements SpringApplicationRunListener {
public MyRunListener(SpringApplication application, String[ ] args) { }
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) { }
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment) { }
// ... 其他阶段
}
3. 编程式启动
new SpringApplicationBuilder(MyApplication.class)
.profiles("prod")
.initializers(new MyInitializer())
.listeners(new MyListener())
.run(args);
SpringApplicationBuilder 提供了流畅的 API 来构建 SpringApplication 实例,适合在启动前需要动态决策的场景。
六、常见陷阱

陷阱一:Runner 中抛出异常导致启动失败
Runner 的异常不会被 Spring Boot 静默处理——如果 Runner 抛出未捕获的异常,callRunners 方法会直接抛出 IllegalStateException,导致启动失败。这意味着 Runner 中如果有"可恢复"的错误(比如外部的缓存服务暂时不可达),应该用 try-catch 包裹,而不是让异常冒泡。
陷阱二:在 ApplicationStartedEvent 和 ApplicationReadyEvent 之间做选择
ApplicationStartedEvent 在 refresh() 完成后立即发布,此时嵌入式 Web 服务器可能还没有启动完毕(onRefresh() 阶段创建了 Web 服务器,但 finishRefresh() 中的 start() 还没执行)。ApplicationReadyEvent 在 Web 服务器启动完成、应用可以接收请求后才发布。如果你的逻辑需要调用自己的 HTTP 接口(比如自检),必须在 ApplicationReadyEvent 之后——在 ApplicationStartedEvent 时 Web 服务器还没开始监听端口。
陷阱三:推断主类失败
SpringApplication 通过 new RuntimeException().getStackTrace() 推断 mainApplicationClass。如果你的 main 方法不在一个标准类中(比如通过脚本引擎调用),这个推断可能失败。此时需要显式传入主类:new SpringApplication(MyConfig.class).run(args)。
陷阱四:BootstrapContext 的 Bean 迁移
BootstrapContext 中的 Bean 在引导完成后会被关闭。如果你的 Bean 在引导阶段被创建并注册到 BootstrapContext,但在主 ApplicationContext 中还需要使用,需要确保它通过 BootstrapRegistryInitializer 注册时指定了 ApplicationContext 迁移策略。否则,BootstrapContext 关闭后 Bean 会被销毁,主 ApplicationContext 中找不到它。
小结:启动流程的"可观测"与"可扩展"
Spring Boot 的启动流程设计的核心思想是"可观测"和"可扩展"——每个阶段都通过事件广播让外部代码感知,每个节点都提供了扩展点(ApplicationContextInitializer、SpringApplicationRunListener、Runner)。理解这条链路后,你不仅能回答"Spring Boot 是怎么启动的",还能在面试中说清楚"如果我想在启动时做一件事,应该挂在哪个钩子上"——这个"选对钩子"的能力,比"知道有哪些钩子"更能体现对框架的深入理解。
(以上正文阅读时长约 40-50 分钟,以下问答部分不计入正文阅读时长)
文末答疑:面试官会怎么问
问 1:**SpringApplication.run()**和**new SpringApplicationBuilder().run()**有什么区别?在什么场景下应该用后者?
答:SpringApplication.run() 是静态方法,内部就是 new SpringApplication(primarySources).run(args),适合"默认配置就够了"的场景。SpringApplicationBuilder 提供了流畅的链式 API,适合需要动态配置的场景:比如根据环境变量决定是否启用某些 profile、根据命令行参数注册不同的 ApplicationContextInitializer、或者在父子容器架构中构建层次化的 ApplicationContext。如果你需要在一个进程中启动多个 Spring Boot 应用(比如一个管理端口 + 一个业务端口),SpringApplicationBuilder 的 parent()/child() 方法是唯一的选择。
问 2:**ApplicationContextInitializer**和**BeanFactoryPostProcessor**都可以在容器 refresh 之前修改 Bean 定义,它们有什么区别?分别在什么阶段执行?
答:执行顺序是 ApplicationContextInitializer → BeanFactoryPostProcessor。ApplicationContextInitializer 在 ApplicationContext 创建后、refresh() 之前被调用,此时 BeanFactory 还没有被 refresh() 初始化,Bean 定义还没有加载,所以 ApplicationContextInitializer 只能修改 Environment 或注册 BeanFactoryPostProcessor,不能直接修改 Bean 定义。BeanFactoryPostProcessor 在 refresh() 的 invokeBeanFactoryPostProcessors 阶段被调用,此时 Bean 定义已经加载完毕,可以直接修改 Bean 定义(如修改属性值、替换类名等)。简单说:ApplicationContextInitializer 是"在 Bean 定义加载之前",BeanFactoryPostProcessor 是"在 Bean 定义加载之后"。
问 3:如果**main**方法里调用了两次**SpringApplication.run()**,第二次调用会发生什么?会有两个独立的 Spring 容器吗?
答:是的,会有两个独立的 ApplicationContext。每次 SpringApplication.run() 都会创建一个新的 SpringApplication 实例(或复用已有的实例),然后创建新的 ApplicationContext 并执行 refresh()。两个容器之间完全独立,有各自的 Bean 定义、各自的 Environment、各自的 Web 服务器。如果两次调用都创建了 Web 服务器,第二个会启动失败(端口冲突),除非配置了不同的端口。这个特性在集成测试中偶尔有用——比如需要模拟两个独立的应用在同一个 JVM 中交互。
问 4:**CommandLineRunner**和**ApplicationRunner**的异常是怎么处理的?如果 Runner 中调用了**System.exit()**会发生什么?
答:Runner 的异常会被 callRunners 方法捕获并包装成 IllegalStateException 重新抛出,最终导致 SpringApplication.run() 失败返回。如果 Runner 中调用了 System.exit(),JVM 直接退出,Spring Boot 的 ApplicationContext 没有被正常关闭——@PreDestroy 方法不会被调用,正在执行的事务可能丢失,连接池不会被关闭。这就是为什么 Runner 中不应该调用 System.exit()——如果需要异常退出,应该抛出异常让 Spring Boot 的 ExitCodeGenerator 机制处理,它会在 SpringApplication.exit() 后返回一个错误码给调用方。
问 5:Spring Boot 3.x 的 AOT 处理对启动流程有什么影响?它在哪些阶段做了优化?
答:AOT(Ahead-of-Time)处理在编译期完成了原本在运行时做的事情,主要影响三个阶段:第一,条件评估被提前到编译期——ConditionEvaluator 的逻辑在编译期执行,结果直接写入生成的代码,运行时跳过条件判断。第二,Bean 定义被提前生成——ConfigurationClassPostProcessor 的解析逻辑在编译期完成,运行时直接注册 Bean 定义而不是解析 @Configuration 类。第三,spring.factories 和 AutoConfiguration.imports 的加载被提前——编译期读取所有候选自动配置类,过滤掉条件不满足的,只保留生效的类。这些优化在 JVM 部署模式下效果有限(因为条件评估本身很快),但在 GraalVM Native Image 部署模式下,AOT 处理是必需的——Native Image 不支持运行时反射和动态类加载,条件评估必须在编译期完成。