
Spring Boot 启动流程:run() 里到底发生了什么
关键词标签:Spring Boot、SpringApplication、启动流程、ApplicationContext、源码解析
一个反直觉的结论
很多人以为 SpringApplication.run() 是一个"启动方法",但翻开源码会发现:它本身几乎没有业务逻辑,真正的启动逻辑被拆成了多个 *Runner 和 *Listener 的回调,通过 SpringApplicationRunListeners 广播事件驱动。换句话说,run() 更像一个事件调度器,而不是一个"初始化函数"。
理解这一点,很多困惑就迎刃而解:为什么 ApplicationContextInitializer 能在 refresh() 之前拿到环境?为什么 ApplicationRunner 和 CommandLineRunner 的执行顺序由 @Order 决定?为什么某些 Bean 在 @PostConstruct 里拿不到 Environment?答案都藏在 run() 的骨架里。
本文以 Spring Boot 3.2.x(org.springframework.boot:spring-boot)源码为基础,逐段拆解 SpringApplication.run(String... args) 的执行路径。假设我们有一个 order-service 应用,main 方法里只写了一行 SpringApplication.run(OrderServiceApplication.class, args),接下来发生的所有事情都从这里展开。
一、run() 的整体骨架
SpringApplication.run(String... args) 的实现非常短,核心是构造一个 ConfigurableApplicationContext,然后调用 refreshContext:
public ConfigurableApplicationContext run(String... args) {
Startup startup = Startup.create();
if (this.registerShutdownHook) {
SpringApplication.shutdownHook.enableShutdownHookAddition();
}
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
startup.started();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), startup);
}
listeners.started(context, startup.timeTakenToStarted());
callRunners(context, applicationArguments);
} catch (Throwable ex) {
throw handleRunFailure(context, ex, listeners);
}
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
} catch (Throwable ex) {
throw handleRunFailure(context, ex, null);
}
return context;
}
(源码位置:org.springframework.boot.SpringApplication#run(java.lang.String...),Spring Boot 3.2.x。)
把这段代码抽象成阶段,大致是:
| 阶段 | 关键动作 | 触发的事件 |
|---|---|---|
| 准备 | 创建 BootstrapContext、加载 RunListeners |
starting |
| 环境 | prepareEnvironment 构建 Environment |
environmentPrepared |
| 上下文 | createApplicationContext + prepareContext |
contextPrepared / contextLoaded |
| 刷新 | refreshContext 调用 AbstractApplicationContext#refresh |
------ |
| 收尾 | afterRefresh、callRunners、ready |
started / ready / failed |
下面逐段看几个最容易被忽略、但出问题时最关键的节点。
二、prepareEnvironment:环境不是"创建"出来的,是"广播"出来的
prepareEnvironment 的源码结构如下:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
这里有两个容易踩的点:
2.1 environmentPrepared 才是配置真正被加载的时刻
getOrCreateEnvironment() 只是根据 webApplicationType 返回一个空的 StandardServletEnvironment 或 StandardEnvironment,里面只有 systemProperties 和 systemEnvironment 两个 PropertySource。真正把 application.yml、application.properties、profile 文件、spring.config.import 都加载进来的,是 listeners.environmentPrepared(...) 广播出去的 EnvironmentPostProcessorApplicationListener,它内部会调 EnvironmentPostProcessor 链,最终走到 ConfigDataEnvironmentPostProcessor。
所以:在 ApplicationContextInitializer 里读 Environment 是能读到配置的 ,因为 initializers 是在 prepareContext 里执行的,晚于 environmentPrepared。但如果在 EnvironmentPostProcessor 里想读 @ConfigurationProperties 绑定的 Bean,是读不到的------那时候 Bean 还没创建。
2.2 bindToSpringApplication 决定了 spring.main.* 的优先级
private void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
} catch (Exception ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}
这一步把 spring.main.banner-mode、spring.main.web-application-type、spring.main.lazy-initialization 等属性直接绑定到 SpringApplication 实例上。注意顺序 :它发生在 environmentPrepared 之后、createApplicationContext 之前。这意味着,如果你在 application.yml 里写 spring.main.web-application-type: none,它是生效的;但如果你在 EnvironmentPostProcessor 里动态改这个值,需要保证在 bindToSpringApplication 之前完成,否则不生效。
三、createApplicationContext 与 prepareContext
createApplicationContext 的逻辑非常简单------根据 webApplicationType 反射实例化对应的 ApplicationContext:
SERVLET→AnnotationConfigServletWebServerApplicationContextREACTIVE→AnnotationConfigReactiveWebServerApplicationContextNONE→AnnotationConfigApplicationContext
真正复杂的是 prepareContext:
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
addAotGeneratedInitializerIfNecessary(this.initializers);
applyInitializers(context);
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory dlbf) {
dlbf.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
几个值得注意的细节:
3.1 applyInitializers 早于 load
ApplicationContextInitializer 的执行时机在 refresh() 之前,也在 BeanDefinition 加载之前。它的典型用途是往 Environment 里塞属性、注册 BeanFactoryPostProcessor。但不能 在这里 getBean(),因为容器还没刷新。
3.2 load 就是把主类注册为 BeanDefinition
load 最终调用 BeanDefinitionLoader#load,把 OrderServiceApplication.class 注册进去。这时候 @SpringBootApplication 上的 @ComponentScan、@EnableAutoConfiguration 还没解析------它们是在 refresh() → invokeBeanFactoryPostProcessors 阶段,由 ConfigurationClassPostProcessor 处理的。
3.3 contextLoaded 之后才是 refresh
refreshContext 里最终调用了 AbstractApplicationContext#refresh(),也就是那个经典的 12 步方法:obtainFreshBeanFactory → prepareBeanFactory → postProcessBeanFactory → invokeBeanFactoryPostProcessors → registerBeanPostProcessors → initMessageSource → initApplicationEventMulticaster → onRefresh → registerListeners → finishBeanFactoryInitialization → finishRefresh。
对于 Web 应用,onRefresh 会被 ServletWebServerApplicationContext 覆写,在这里创建并启动内嵌的 Tomcat/Jetty/Undertow 。所以"端口被占用"这类报错,抛出的位置在 refresh() 内部,而不是 run() 的顶层------这也是为什么 SpringApplication 的 handleRunFailure 能拿到 context 并优雅关闭。
四、callRunners:Runner 与 CommandLineRunner 的真实差异
afterRefresh 之后,callRunners 会依次调用 ApplicationRunner 和 CommandLineRunner:
private void callRunners(ApplicationContext context, ApplicationArguments args) {
context.getBeanProvider(Runner.class).orderedStream().forEach((runner) -> {
if (runner instanceof ApplicationRunner applicationRunner) {
callRunner(applicationRunner, args);
} else if (runner instanceof CommandLineRunner commandLineRunner) {
callRunner(commandLineRunner, args);
}
});
}
(Spring Boot 3.2 起把两者抽象成 Runner 接口,之前的版本是分两次 getBeanProvider 调用。)
关键点 :两者都实现了 Runner,orderedStream() 会按 @Order / Ordered 排序。所以一个 ApplicationRunner 和一个 CommandLineRunner 混用时,谁先执行取决于 @Order 值,而不是"ApplicationRunner 优先"。这是很多博客写错的地方。
ApplicationRunner 的参数是 ApplicationArguments,能拿 --foo=bar 这类选项;CommandLineRunner 拿的是原始 String[],需要自己解析。选哪个取决于你是否需要区分 option 和 non-option 参数。
五、什么时候别用 / 别踩的坑
- 别在
EnvironmentPostProcessor里getBean。此时ApplicationContext还没创建,BeanFactory也不存在。要用 Bean,请改用ApplicationListener<ApplicationPreparedEvent>或ApplicationContextInitializer。 - 别指望
@PostConstruct里能拿到ApplicationRunner注册的副作用 。@PostConstruct在finishBeanFactoryInitialization期间执行,早于callRunners。 - 别在
ApplicationContextInitializer里启动线程池并期望它随上下文关闭 。它拿到的context还没refresh,注册的DisposableBean不会自动生效,除非你手动绑定到context的 lifecycle。 - 别把
spring.main.lazy-initialization=true当作万金油 。它只影响普通单例 Bean 的实例化时机,ApplicationRunner、SmartInitializingSingleton、@EventListener等依然会在启动阶段被触发。 - 别在
run()的返回值上做文章 。返回的ConfigurableApplicationContext已经是 refreshed 状态,但如果你在main里try-with-resources关闭它,SpringApplication注册的 shutdown hook 会与之竞争,可能触发重复关闭的告警。
六、小结与后续
SpringApplication.run() 的本质是一条由事件驱动的启动流水线 :starting → environmentPrepared → contextPrepared → contextLoaded → started → ready,每一步都对应一个明确的扩展点。理解了这条流水线,ApplicationContextInitializer、EnvironmentPostProcessor、ApplicationRunner、ApplicationListener 的选型就不再是玄学。
参考的官方文档与源码:
- Spring Boot 官方文档《SpringApplication》章节:https://docs.spring.io/spring-boot/reference/features/spring-application.html
SpringApplication源码(GitHub):https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.javaAbstractApplicationContext#refresh(Spring Framework):https://github.com/spring-projects/spring-framework/blob/main/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java
本系列前面已经讲过 Spring Boot 自动配置的加载机制、Spring Boot 3 与 2 的差异,这里补充的是"启动时序"这一层------自动配置只是 refresh() 中 invokeBeanFactoryPostProcessors 的一个子步骤,把它放回整条流水线里,很多"为什么自动配置不生效"的问题会更容易定位。下一篇会继续沿着 refresh() 往下钻,聊聊 BeanFactoryPostProcessor 与 BeanPostProcessor 的执行顺序,以及它们在 AOT 场景下的边界变化。