Spring Boot启动全流程源码:SpringApplication.run()的完整10步
Spring Boot启动全流程源码:SpringApplication.run()的完整10步
适读人群:有Spring Boot使用经验、希望深入理解启动机制的Java后端开发者 | 阅读时长:约18分钟
开篇故事
去年双十一前两周,我们系统上了一个新服务,本地跑得好好的,一到预发环境就起不来。报错信息很诡异——ApplicationContext failed to start,日志刷了满屏,愣是找不到根因。
我当时急得满头大汗,一边盯着日志一边翻文档,找了两个小时。最后还是我们组的老李一眼看出来了:是个自定义的BeanDefinitionRegistryPostProcessor在处理时依赖了还没注册的Bean,触发了提前初始化,导致整个BeanFactory状态乱掉了。
他怎么这么快就看出来的?老李笑着说:你把SpringApplication.run()的源码从头到尾捋一遍,再来看这个错你就秒懂了。
那次之后我花了整整一个周末,把Spring Boot 3.x的启动流程从头到尾过了一遍,打了几十个断点,把每一步干了什么都记下来。今天把这些沉淀整理出来,分享给大家。
这篇文章会把SpringApplication.run()拆解成10个关键步骤,每一步对应哪个方法、做了什么事、有哪些扩展点,都会说清楚。知道了这些,以后遇到启动问题,你也能像老李一样快速定位。
一、为什么要看这段源码
SpringApplication.run()是所有Spring Boot应用的入口,一行代码背后隐藏着极其复杂的初始化逻辑。
很多开发者遇到以下问题时完全无从下手:
- 启动报
NoSuchBeanDefinitionException但Bean明明写了 - 某个
@Configuration没生效,但不知道为什么 - 自定义的
EnvironmentPostProcessor没执行 - Bean初始化顺序不对导致NPE
根本原因是不知道Spring Boot启动时做了什么,按什么顺序做。
Spring Boot 3.x(基于Spring Framework 6.x)的启动流程入口在org.springframework.boot.SpringApplication类,核心方法是run(String... args),源码在spring-boot-3.x/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java第300行附近。
二、源码核心路径解析
2.1 整体10步全景
先用一张时序图把10步串起来:
2.2 逐步深入源码
Step 1:记录启动时间
SpringApplication.java run方法第一行(约第303行):
long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;createBootstrapContext()会触发所有Bootstrapper的initialize方法,这是Spring Boot 2.4引入的早期启动上下文,专门用于在ApplicationContext创建之前需要使用的组件(比如配置中心客户端)。
Step 2:配置Headless属性
configureHeadlessProperty();这一步把java.awt.headless设为true,避免在服务器环境下因没有显示器而报错。看起来不起眼,但在某些环境确实救过命。
Step 3:获取并启动RunListeners
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);getRunListeners通过SpringFactoriesLoader加载META-INF/spring.factories(Spring Boot 3.x改为META-INF/spring/org.springframework.boot.SpringApplicationRunListener.factories)中注册的SpringApplicationRunListener实现。
默认只有一个:EventPublishingRunListener,它负责把生命周期事件转发给ApplicationEventMulticaster。
Step 4:准备Environment(重点)
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);这一步干的事最多:
- 创建
Environment对象(Web环境是ApplicationServletEnvironment) - 加载默认的
PropertySource(命令行参数、系统属性等) - 触发
EnvironmentPostProcessor(ConfigDataEnvironmentPostProcessor就在这里加载application.yml) - 绑定
spring.main.*配置到SpringApplication - 发布
ApplicationEnvironmentPreparedEvent
// SpringApplication.java prepareEnvironment方法核心片段
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext,
ApplicationArguments applicationArguments) {
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
// 触发EnvironmentPostProcessor,加载配置文件就在这里
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"...");
bindToSpringApplication(environment);
// ...
return environment;
}Step 5:打印Banner
Banner printedBanner = printBanner(environment);就是那个Spring的启动Logo。可以自定义banner.txt放在resources目录,或者关掉(spring.main.banner-mode=off)。
Step 6:创建ApplicationContext
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);createApplicationContext()内部调用ApplicationContextFactory.DEFAULT.create(this.webApplicationType),根据应用类型创建不同的Context:
- Servlet Web:
AnnotationConfigServletWebServerApplicationContext - Reactive Web:
AnnotationConfigReactiveWebServerApplicationContext - 非Web:
AnnotationConfigApplicationContext
Step 7:准备Context(重点)
prepareContext(bootstrapContext, context, environment, listeners,
applicationArguments, printedBanner);这一步做了以下关键操作:
- 把
Environment设置到Context - 调用
ApplicationContextInitializer.initialize()——这是一个重要扩展点 - 发布
ApplicationContextInitializedEvent - 注册主启动类为BeanDefinition(
load(context, sources.toArray(new Object[0]))) - 发布
ApplicationPreparedEvent
// 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); // 触发ApplicationContextInitializer
listeners.contextPrepared(context); // 发布contextPrepared事件
bootstrapContext.close(context);
// ...
load(context, sources.toArray(new Object[0])); // 加载主类BeanDefinition
listeners.contextLoaded(context); // 发布contextLoaded事件
}Step 8:刷新Context(核心中的核心)
refreshContext(context);这里实际调用的是AbstractApplicationContext.refresh(),这是整个Spring IoC容器初始化的心脏,后续我会单独出文章详解。核心步骤包括:
invokeBeanFactoryPostProcessors:执行所有BeanFactoryPostProcessor(含ConfigurationClassPostProcessor,负责扫描@Component、处理@Configuration等)registerBeanPostProcessors:注册BeanPostProcessorfinishBeanFactoryInitialization:初始化所有单例BeanfinishRefresh:启动嵌入式Web服务器
Step 9:afterRefresh钩子
afterRefresh(context, applicationArguments);默认是空实现,子类可以覆写。
Step 10:广播启动完成事件
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments); // 执行CommandLineRunner和ApplicationRunner
// ...
listeners.ready(context, timeTakenToReady);callRunners会执行所有CommandLineRunner和ApplicationRunner,这是很多初始化逻辑的落脚点。
2.3 扩展点全景图
三、完整代码示例
3.1 自定义EnvironmentPostProcessor(在配置加载后篡改配置)
// 实现EnvironmentPostProcessor,在application.yml加载后执行
@Order(Ordered.LOWEST_PRECEDENCE - 10)
public class DecryptEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String ENCRYPTED_PREFIX = "ENC(";
private static final String ENCRYPTED_SUFFIX = ")";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
// 遍历所有PropertySource,找到加密的配置项并解密
MutablePropertySources propertySources = environment.getPropertySources();
// 收集所有需要解密的属性
Map<String, Object> decryptedProperties = new HashMap<>();
for (PropertySource<?> propertySource : propertySources) {
if (propertySource instanceof EnumerablePropertySource<?> eps) {
for (String propertyName : eps.getPropertyNames()) {
Object value = eps.getProperty(propertyName);
if (value instanceof String strValue
&& strValue.startsWith(ENCRYPTED_PREFIX)
&& strValue.endsWith(ENCRYPTED_SUFFIX)) {
String encrypted = strValue.substring(
ENCRYPTED_PREFIX.length(),
strValue.length() - ENCRYPTED_SUFFIX.length()
);
decryptedProperties.put(propertyName, decrypt(encrypted));
}
}
}
}
if (!decryptedProperties.isEmpty()) {
// 将解密后的属性以最高优先级加入
propertySources.addFirst(
new MapPropertySource("decryptedProperties", decryptedProperties)
);
}
}
private String decrypt(String encrypted) {
// 实际项目中接AES解密
return new String(Base64.getDecoder().decode(encrypted));
}
}注册方式(Spring Boot 3.x):
# src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.factories
com.example.DecryptEnvironmentPostProcessor3.2 利用ApplicationContextInitializer注入早期配置
// ApplicationContextInitializer在BeanDefinition加载之前执行
// 适合做:注册额外PropertySource、设置Profile、注册早期BeanDefinition
public class NacosDynamicConfigInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
// 从Nacos加载远程配置,在本地配置之前插入(优先级更高)
try {
Properties nacosConfig = loadFromNacos(environment);
Map<String, Object> configMap = new HashMap<>();
nacosConfig.forEach((k, v) -> configMap.put(k.toString(), v));
environment.getPropertySources().addFirst(
new MapPropertySource("nacos-config", configMap)
);
System.out.println("[NacosInit] Loaded " + configMap.size()
+ " properties from Nacos");
} catch (Exception e) {
// 远程配置加载失败不阻断启动,但要告警
System.err.println("[NacosInit] Failed to load Nacos config: "
+ e.getMessage());
}
}
private Properties loadFromNacos(ConfigurableEnvironment environment) {
String serverAddr = environment.getProperty("nacos.server-addr", "localhost:8848");
String dataId = environment.getProperty("nacos.data-id", "application");
String group = environment.getProperty("nacos.group", "DEFAULT_GROUP");
// 实际调用Nacos SDK...
return new Properties();
}
}3.3 CommandLineRunner vs ApplicationRunner的选择
// CommandLineRunner:参数是原始String数组,适合简单场景
@Component
@Order(1)
public class DataInitRunner implements CommandLineRunner {
@Autowired
private DataInitService dataInitService;
@Override
public void run(String... args) throws Exception {
// 此时所有Bean已经初始化完毕,可以安全使用
if (Arrays.asList(args).contains("--init-data")) {
dataInitService.initBaseData();
}
}
}
// ApplicationRunner:参数是ApplicationArguments,更结构化
@Component
@Order(2)
public class WarmUpRunner implements ApplicationRunner {
@Autowired
private CacheService cacheService;
@Override
public void run(ApplicationArguments args) throws Exception {
// 支持 --option=value 形式的参数解析
if (args.containsOption("warm-up")) {
List<String> values = args.getOptionValues("warm-up");
cacheService.warmUp(values);
}
}
}四、踩坑实录
坑1:在BeanDefinitionRegistryPostProcessor中注入其他Bean导致启动失败
现象:启动时报BeanCurrentlyInCreationException或某些Bean没有被代理(AOP失效)。
根因:BeanDefinitionRegistryPostProcessor执行时机非常早,在Step 8 refresh的invokeBeanFactoryPostProcessors阶段,这时候大多数BeanPostProcessor还没有注册,如果在此阶段触发了某个Bean的实例化,那个Bean就会在没有AOP代理的情况下被创建并缓存,后续取到的就是"裸"对象。
// 错误写法:在BDRegistryPostProcessor里直接注入依赖
@Component
public class BadRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Autowired
private SomeService someService; // 危险!触发提前实例化
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 此时someService可能是未被代理的裸对象!
someService.doSomething();
}
}
// 正确写法:通过BeanFactory懒获取
@Component
public class GoodRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor,
BeanFactoryAware {
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 在refresh完成后才会真正被调用,但这里不要用依赖,只做BeanDefinition注册
// 如果必须用,确保理解执行时机
}
}坑2:EnvironmentPostProcessor中读取配置文件路径不对
现象:EnvironmentPostProcessor里读spring.config.location,拿到的是null。
根因:EnvironmentPostProcessor执行时,ConfigDataEnvironmentPostProcessor(也是一个EnvironmentPostProcessor,Order=Ordered.HIGHEST_PRECEDENCE + 10)可能还没跑,application.yml还没被加载进来。
解决方案:把自定义的EnvironmentPostProcessor的@Order设为比ConfigDataEnvironmentPostProcessor更低(数值更大):
// ConfigDataEnvironmentPostProcessor的Order是 Ordered.HIGHEST_PRECEDENCE + 10 = -2147483638
// 我们要在它之后执行,所以Order要更大(优先级更低)
@Order(Ordered.HIGHEST_PRECEDENCE + 20) // 确保在ConfigData之后
public class MyEnvPostProcessor implements EnvironmentPostProcessor {
// 此时application.yml已经加载,可以安全读取
}坑3:ApplicationContextInitializer和EnvironmentPostProcessor的注册方式混淆
现象:自定义的ApplicationContextInitializer没有执行。
根因:Spring Boot 3.x的SPI机制从META-INF/spring.factories迁移到了META-INF/spring/目录下的具名文件,文件名就是接口全类名。
# Spring Boot 2.x(旧)
# META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.example.MyInitializer
# Spring Boot 3.x(新)
# META-INF/spring/org.springframework.context.ApplicationContextInitializer.factories
com.example.MyInitializer两种方式在3.x中都支持,但如果你从网上抄的2.x写法,且依赖的spring-core版本有差异,有时会静默失败,很难排查。
坑4:多个CommandLineRunner的执行顺序不是注册顺序
现象:A Runner依赖B Runner的初始化结果,但B还没跑A就先跑了。
根因:callRunners在收集Runner时会调用AnnotationAwareOrderComparator.sort(),按@Order或Ordered接口排序。不加@Order的Bean排序是不确定的(取决于BeanFactory的注册顺序)。
// 明确指定顺序,数值越小越先执行
@Component
@Order(1)
public class DatabaseInitRunner implements CommandLineRunner { ... }
@Component
@Order(2) // 依赖DatabaseInitRunner执行完毕
public class CacheWarmUpRunner implements CommandLineRunner { ... }五、总结与延伸
把SpringApplication.run()的10步梳理一遍,你会发现Spring Boot的启动流程设计得非常有层次:
| 步骤 | 关键操作 | 主要扩展点 |
|---|---|---|
| 1-2 | 初始化BootstrapContext | Bootstrapper |
| 3 | 通知启动开始 | SpringApplicationRunListener |
| 4 | 准备Environment | EnvironmentPostProcessor |
| 5-6 | 创建ApplicationContext | ApplicationContextFactory |
| 7 | 初始化Context | ApplicationContextInitializer |
| 8 | 刷新容器(核心) | BeanFactoryPostProcessor, BeanPostProcessor |
| 9-10 | 启动完成通知 | CommandLineRunner, ApplicationRunner |
理解这个顺序,以后遇到启动问题,先判断是哪个阶段出了问题,然后对应去看对应的扩展点实现,一般能快速定位。
下一篇我们会深入DispatcherServlet,看请求进来之后是怎么一步步找到你的@Controller方法的。
