Spring Boot · 第 02 章

启动流水线

上一章定格了容器里的静态零件(蓝图、实例、两类处理器)——这一章让它们动起来:run() 到 refresh() 按什么顺序执行。

本章你将建立的 schema

  • run() 三段:准备环境 → 建上下文 → refresh → callRunners
  • refresh() 12 阶段的关键顺序:改蓝图(BFPP)→ 注册 BPP → onRefresh 启 Tomcat → 预实例化单例
  • 内嵌 Tomcat 在 onRefresh(阶段 9)启动,早于 preInstantiateSingletons(阶段 11)

2.1SpringApplication.run() 宏观三段

SpringApplication.run() 是一条线性流程:先建环境,再建容器,最后把容器跑起来。

SpringApplication.run() 内部依次调用五个大步骤:

  1. prepareEnvironment() — 建 ConfigurableEnvironment,触发所有 EnvironmentPostProcessor(其中 ConfigDataEnvironmentPostProcessor 负责加载 application.yml/application.properties)。
  2. createApplicationContext() — 按应用类型选择上下文实现:Servlet Web 对应 AnnotationConfigServletWebServerApplicationContext,响应式对应 AnnotationConfigReactiveWebServerApplicationContext,非 Web 对应 AnnotationConfigApplicationContext。
  3. prepareContext() — 把主启动类注册成第一个 BeanDefinition,绑定 Environment,触发 ApplicationContextInitializer。
  4. refreshContext() — 调用 AbstractApplicationContext.refresh(),这是本章的主角。
  5. callRunners() — refresh 完成后,按 @Order 依次执行 ApplicationRunner 和 CommandLineRunner。
prepareEnvironment 建 ConfigurableEnvironment createApplicationContext 按应用类型建上下文 prepareContext 注册主类 BeanDefinition refreshContext() → refresh() 本章主角 · 容器初始化全在此 callRunners ApplicationRunner / CommandLineRunner
图 2.1SpringApplication.run() 宏观五步。注意:refreshContext() 之前的步骤都是准备阶段,容器真正的初始化工作全在 refresh 内部完成。
为什么要先建 Environment 再建 Context

上下文的类型(Servlet/Reactive/普通)需要通过 Environment 中的 classpath 信息来推断;application.yml 里的配置也可能影响哪些 BeanDefinition 被加载。因此 Environment 必须先于容器存在。

2.2refresh() 的 12 阶段

AbstractApplicationContext.refresh() 是 Spring 容器启动的主干,12 个方法调用按严格顺序执行,每步的前置条件都由上一步保证。

源码中 refresh() 的方法体如下(Spring Boot 4.0 / Spring Framework 7 起骨架未变):

AbstractApplicationContext.javaJava
// 示意——12 个阶段按顺序调用
public void refresh() throws BeansException, IllegalStateException {
    prepareRefresh();                          // ① 标记启动时间、验证必需属性
    ConfigurableListableBeanFactory beanFactory
        = obtainFreshBeanFactory();            // ② 获取/刷新 BeanFactory
    prepareBeanFactory(beanFactory);           // ③ 注入 ClassLoader、BPP(ApplicationContextAwareProcessor 等)
    postProcessBeanFactory(beanFactory);       // ④ 子类扩展点(Servlet 上下文在此注册 Scope)
    invokeBeanFactoryPostProcessors(beanFactory); // ⑤ ★ 执行所有 BFPP(含 ConfigurationClassPostProcessor)
    registerBeanPostProcessors(beanFactory);   // ⑥ ★ 实例化并注册所有 BPP
    initMessageSource();                       // ⑦ 国际化 MessageSource
    initApplicationEventMulticaster();         // ⑧ 事件广播器
    onRefresh();                               // ⑨ ★ 子类扩展——Servlet 上下文在此启动 Tomcat
    registerListeners();                       // ⑩ 注册 ApplicationListener
    finishBeanFactoryInitialization(beanFactory); // ⑪ ★ preInstantiateSingletons:预实例化非懒单例
    finishRefresh();                           // ⑫ 发布 ContextRefreshedEvent、启动 Lifecycle bean
}

下图是本章的核心——12 阶段竖直流水线,四个关键阶段用朱红标出:

① prepareRefresh 标记启动时间 · 验证必需属性 阶段 1 ② obtainFreshBeanFactory 获取 / 刷新 BeanFactory 阶段 2 ③ prepareBeanFactory 注入 ClassLoader · Aware BPP 阶段 3 ④ postProcessBeanFactory 子类扩展(注册 Servlet Scope) 阶段 4 ⑤ invokeBeanFactoryPostProcessors ★ 执行所有 BFPP ConfigurationClassPostProcessor 加载蓝图 阶段 5 ⑥ registerBeanPostProcessors ★ 实例化并注册所有 BPP 按 PriorityOrdered → Ordered → rest 排序 阶段 6 ⑦ initMessageSource 国际化资源 阶段 7 ⑧ initApplicationEventMulticaster 事件广播器 阶段 8 ⑨ onRefresh ★ createWebServer → 内嵌 Tomcat 启动 ServletWebServerApplicationContext 重写此方法 阶段 9 ⑩ registerListeners 注册 ApplicationListener 阶段 10 ⑪ finishBeanFactoryInitialization ★ preInstantiateSingletons 预实例化所有非懒加载单例 Bean 阶段 11 ⑫ finishRefresh 发布 ContextRefreshedEvent·启动 Lifecycle 阶段 12
图 2.2refresh() 12 阶段竖直流水线,四个关键阶段(⑤⑥⑨⑪)用朱红标出。注意:阶段 9(Tomcat 启动)早于阶段 11(单例预实例化)——这是容器启动中最常被误解的顺序关系。

因果顺序:为什么必须这样排列

阶段 5 在阶段 6 之前:invokeBeanFactoryPostProcessors 负责收集并改写所有 BeanDefinition——包括解析 @Configuration 类、加载自动配置(第 3 章细讲)。只有 BeanDefinition 全部就绪,注册 BPP 时才能找到所有 BeanPostProcessor 类型的 Bean。如果顺序颠倒,部分 BPP 的 BeanDefinition 尚未存在,注册就会遗漏。

阶段 6 在阶段 11 之前:BPP 必须先注册,才能在实例化普通单例时介入生命周期(postProcessBeforeInitialization / postProcessAfterInitialization)。如果 BPP 没注册就开始造 Bean,代理(AbstractAutoProxyCreator,第 4 章)就没机会把 Bean 替换成代理对象。这是 第 1 章两类后置处理器 时序约束的直接体现。

改蓝图 → 注册 BPP → 实例化单例:这条因果链是理解容器启动顺序的第一刀。三步缺一不可,且顺序不可颠倒。

2.3反直觉:Tomcat 在 onRefresh(阶段 9)启动

大多数工程师直觉上认为"所有单例造好之后 Tomcat 才能接请求"——这个直觉是错的。

ServletWebServerApplicationContext 重写了 onRefresh(),在第 9 阶段调用 createWebServer()。此时 TomcatWebServer 被构造出来,端口绑定(connector 初始化)发生在这里。而非懒业务单例(包括各种 @Service、@Repository)要到阶段 11 的 preInstantiateSingletons 才开始构造。

关键细节:createWebServer() 只是建了 server 实例,真正让 Tomcat 开始接受外部流量的 start() 调用在阶段 12 的 finishRefresh() 内。因此阶段 9 之后,端口已被绑定但尚未对外服务;启动若在阶段 11 失败(某个 Bean 抛异常),Spring 会触发优雅关闭,Tomcat 随之关闭,不会有僵尸端口留下。

想一想

一个非懒 @Service 在 @PostConstruct 方法里抛出异常,此时端口是否已经被监听?外部能否建立 TCP 连接?

展开答案(先停 10 秒)

端口已被绑定(createWebServer 在阶段 9 完成),但 Tomcat 的 start() 尚未调用(在阶段 12)——操作系统层面端口已占用,但 Tomcat 的 connector 未进入 STARTED 状态,外部 TCP 握手会被操作系统接受后立即被关闭。Spring 在捕获到异常后会调用 destroyBeans() 并关闭上下文,TomcatWebServer 随之停止,端口最终释放。应用不会以"半启动"状态存活。

ServletWebServerApplicationContext.javaJava
// 示意:ServletWebServerApplicationContext 重写 onRefresh
@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createWebServer(); // 构造 TomcatWebServer,绑定端口
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}

// finishRefresh 内部才真正 start Tomcat(阶段 12)
@Override
protected void finishRefresh() {
    super.finishRefresh();
    WebServer webServer = startWebServer(); // 调用 TomcatWebServer.start()
    if (webServer != null) {
        publishEvent(new ServletWebServerInitializedEvent(webServer, this));
    }
}

2.4callRunners 与启动后扩展点

refresh() 结束后,SpringApplication.run() 调用 callRunners(),按 @Order 顺序依次执行所有 ApplicationRunner 和 CommandLineRunner。此时容器已完全就绪,Tomcat 也已开始接流量。

在 refresh 内部(阶段 11),当所有单例造好后,容器还会依次调用 SmartInitializingSingleton.afterSingletonsInstantiated()——这发生在 refresh 内部、callRunners 之前,也在 Tomcat 正式 start() 之前(阶段 12 之前)。这个时机适合做"需要所有单例就绪、但不想等到 HTTP 流量进来"的预热。

启动后扩展点对比
扩展点 触发时机 在 refresh 内/外 Tomcat 是否已接流量
@PostConstruct 单个 Bean 初始化时(阶段 11 内) refresh 内 否(start 在阶段 12)
SmartInitializingSingleton 所有单例造好后(阶段 11 末尾) refresh 内 否
ApplicationRunner / CommandLineRunner refresh 完成后,callRunners() refresh 外 是
ApplicationReadyEvent callRunners() 完成后 refresh 外 是
时间 ← refresh() 内 → onRefresh Tomcat create @PostConstruct 各 Bean 初始化时 SmartInitializing Singleton 所有单例就绪后 (阶段 11 末) finishRefresh Tomcat start 开始接流量 ApplicationRunner callRunners() ReadyEvent
图 2.3启动后扩展点在时间轴上的位置。注意:SmartInitializingSingleton 在 Tomcat 开始接流量之前触发,适合不希望预热期间被外部请求打断的场景。
交叉回指

第 1 章的两类后置处理器讲了 BFPP 改蓝图、BPP 改实例——本章的 12 阶段流水线正是这两条规则的具体落点:BFPP 在阶段 5 集中执行,BPP 在阶段 6 注册,单例在阶段 11 实例化。理解了因果链,阶段顺序就不再是需要死记的知识。

§本章 self-check

先合上教程,把答案写下来再展开对照。

  1. refresh() 里 BFPP 和 BPP 谁先执行、为什么必须这个顺序?
  2. 内嵌 Tomcat 在 refresh() 的哪个阶段启动?绑定端口和开始接流量分别发生在哪步?
  3. ApplicationRunner 在 refresh() 之前还是之后运行?
答案(先做完再展开)
  1. BFPP(阶段 5)早于 BPP(阶段 6):必须先执行所有 BeanFactoryPostProcessor 把 BeanDefinition 全部收集完,才能扫描到所有 BeanPostProcessor 类型的 BeanDefinition 并注册它们。若顺序颠倒,BPP 注册不完整,后续实例化时部分 BPP 不在列,代理等增强会被遗漏。
  2. 阶段 9(onRefresh):createWebServer() 被调用,Tomcat 实例构造并绑定端口。但 TomcatWebServer.start()(真正接受 HTTP 流量)要到阶段 12(finishRefresh)才调用。
  3. refresh() 之后——SpringApplication.run() 在 refreshContext() 返回后调用 callRunners(),此时容器完全就绪,Tomcat 已开始接流量。
进阶挑战 · 刚好够不着

预热时机选择:SmartInitializingSingleton 还是 ApplicationRunner?

要在"所有单例已就绪、但还没开始接 HTTP 流量"时做缓存预热(从数据库加载热点数据写入本地缓存),应该用 SmartInitializingSingleton 还是 ApplicationRunner?两者差别在哪,选错会产生什么后果?

提示(卡住再展开)

SmartInitializingSingleton.afterSingletonsInstantiated() 在阶段 11 末尾、finishRefresh(Tomcat start)之前触发——预热期间外部流量还不会打进来,缓存处于一致状态时再开放服务。ApplicationRunner 在 callRunners() 里执行,此时 Tomcat 已接流量——若预热耗时长,早进来的请求会拿到空缓存。对于"不希望在预热未完成时对外服务"的场景,SmartInitializingSingleton 是正确选择;如果预热只是"最好有"而非"必须先有",ApplicationRunner 的好处是能拿到 ApplicationArguments 并且与其他 runner 的顺序更可控。