☰
Spring Boot 启动流程:run() 里到底发生了什么
2026/10/14 16:13:43 网站建设 项目流程

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、加载RunListenersstarting
环境prepareEnvironment构建EnvironmentenvironmentPrepared
上下文createApplicationContext+prepareContextcontextPrepared/contextLoaded
刷新refreshContext调用AbstractApplicationContext#refresh——
收尾afterRefresh、callRunners、readystarted/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.1environmentPrepared才是配置真正被加载的时刻

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.2bindToSpringApplication决定了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→AnnotationConfigServletWebServerApplicationContext
  • REACTIVE→AnnotationConfigReactiveWebServerApplicationContext
  • NONE→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.1applyInitializers早于load

ApplicationContextInitializer的执行时机在refresh()之前,也在 BeanDefinition 加载之前。它的典型用途是往Environment里塞属性、注册BeanFactoryPostProcessor。但不能在这里getBean(),因为容器还没刷新。

3.2load就是把主类注册为 BeanDefinition

load最终调用BeanDefinitionLoader#load,把OrderServiceApplication.class注册进去。这时候@SpringBootApplication上的@ComponentScan、@EnableAutoConfiguration还没解析——它们是在refresh()→invokeBeanFactoryPostProcessors阶段,由ConfigurationClassPostProcessor处理的。

3.3contextLoaded之后才是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 参数。


五、什么时候别用 / 别踩的坑

  1. 别在EnvironmentPostProcessor里getBean。此时ApplicationContext还没创建,BeanFactory也不存在。要用 Bean,请改用ApplicationListener<ApplicationPreparedEvent>或ApplicationContextInitializer。
  2. 别指望@PostConstruct里能拿到ApplicationRunner注册的副作用。@PostConstruct在finishBeanFactoryInitialization期间执行,早于callRunners。
  3. 别在ApplicationContextInitializer里启动线程池并期望它随上下文关闭。它拿到的context还没refresh,注册的DisposableBean不会自动生效,除非你手动绑定到context的 lifecycle。
  4. 别把spring.main.lazy-initialization=true当作万金油。它只影响普通单例 Bean 的实例化时机,ApplicationRunner、SmartInitializingSingleton、@EventListener等依然会在启动阶段被触发。
  5. 别在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.java
  • AbstractApplicationContext#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 场景下的边界变化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询