Spring Boot 启动流程:run() 里到底发生了什么

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 → 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.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 参数。


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

  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 自动配置的加载机制、Spring Boot 3 与 2 的差异,这里补充的是"启动时序"这一层------自动配置只是 refresh() 中 invokeBeanFactoryPostProcessors 的一个子步骤,把它放回整条流水线里,很多"为什么自动配置不生效"的问题会更容易定位。下一篇会继续沿着 refresh() 往下钻,聊聊 BeanFactoryPostProcessor 与 BeanPostProcessor 的执行顺序,以及它们在 AOT 场景下的边界变化。

相关推荐
云絮.1 小时前
Mybatis(一)
java·数据库·mybatis
星始流年2 小时前
AI 时代,软件程序员未来 10—15 年的职业前景
前端·后端·ai编程
猪猪拆迁队2 小时前
跨电脑复制共享-局域网直连踩坑
前端·后端·go
与驴OO2 小时前
聊聊Agent的记忆系统如何设计
后端
希洛兔2 小时前
里氏替换原则详解:子类必须能透明替换基类(含继承风险演示 + 正确重构 + JDK 案例)
java·里氏替换原则
tachibana22 小时前
Go 语言的 Map 是否是并发安全的?
开发语言·数据结构·后端·golang·go·语言
dd聊技术2 小时前
发布前的那十几秒:AI 预检为什么不用「主编 Agent」
后端
kevien_G12 小时前
微服务之OpenFeign最佳实践
java·微服务·架构
Bazingga2 小时前
LLM记忆实战:从一次164秒慢问,到上下文治理与记忆版图
后端