Spring Boot · 第 02 章
启动流水线
上一章定格了容器里的静态零件(蓝图、实例、两类处理器)——这一章让它们动起来:run() 到 refresh() 按什么顺序执行。
本章你将建立的 schema
run()三段:准备环境 → 建上下文 → refresh → callRunnersrefresh()12 阶段的关键顺序:改蓝图(BFPP)→ 注册 BPP → onRefresh 启 Tomcat → 预实例化单例- 内嵌 Tomcat 在
onRefresh(阶段 9)启动,早于preInstantiateSingletons(阶段 11)
2.1SpringApplication.run() 宏观三段
SpringApplication.run() 是一条线性流程:先建环境,再建容器,最后把容器跑起来。
SpringApplication.run() 内部依次调用五个大步骤:
prepareEnvironment()— 建ConfigurableEnvironment,触发所有EnvironmentPostProcessor(其中ConfigDataEnvironmentPostProcessor负责加载application.yml/application.properties)。createApplicationContext()— 按应用类型选择上下文实现:Servlet Web 对应AnnotationConfigServletWebServerApplicationContext,响应式对应AnnotationConfigReactiveWebServerApplicationContext,非 Web 对应AnnotationConfigApplicationContext。prepareContext()— 把主启动类注册成第一个 BeanDefinition,绑定Environment,触发ApplicationContextInitializer。refreshContext()— 调用AbstractApplicationContext.refresh(),这是本章的主角。callRunners()— refresh 完成后,按@Order依次执行ApplicationRunner和CommandLineRunner。
SpringApplication.run() 宏观五步。注意:refreshContext() 之前的步骤都是准备阶段,容器真正的初始化工作全在 refresh 内部完成。上下文的类型(Servlet/Reactive/普通)需要通过 Environment 中的 classpath 信息来推断;application.yml 里的配置也可能影响哪些 BeanDefinition 被加载。因此 Environment 必须先于容器存在。
2.2refresh() 的 12 阶段
AbstractApplicationContext.refresh() 是 Spring 容器启动的主干,12 个方法调用按严格顺序执行,每步的前置条件都由上一步保证。
源码中 refresh() 的方法体如下(Spring Boot 4.0 / Spring Framework 7 起骨架未变):
// 示意——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 阶段竖直流水线,四个关键阶段用朱红标出:
refresh() 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 重写 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 外 | 是 |
SmartInitializingSingleton 在 Tomcat 开始接流量之前触发,适合不希望预热期间被外部请求打断的场景。第 1 章的两类后置处理器讲了 BFPP 改蓝图、BPP 改实例——本章的 12 阶段流水线正是这两条规则的具体落点:BFPP 在阶段 5 集中执行,BPP 在阶段 6 注册,单例在阶段 11 实例化。理解了因果链,阶段顺序就不再是需要死记的知识。
§本章 self-check
先合上教程,把答案写下来再展开对照。
refresh()里 BFPP 和 BPP 谁先执行、为什么必须这个顺序?- 内嵌 Tomcat 在
refresh()的哪个阶段启动?绑定端口和开始接流量分别发生在哪步? ApplicationRunner在refresh()之前还是之后运行?
答案(先做完再展开)
- BFPP(阶段 5)早于 BPP(阶段 6):必须先执行所有
BeanFactoryPostProcessor把 BeanDefinition 全部收集完,才能扫描到所有BeanPostProcessor类型的 BeanDefinition 并注册它们。若顺序颠倒,BPP 注册不完整,后续实例化时部分 BPP 不在列,代理等增强会被遗漏。 - 阶段 9(
onRefresh):createWebServer()被调用,Tomcat 实例构造并绑定端口。但TomcatWebServer.start()(真正接受 HTTP 流量)要到阶段 12(finishRefresh)才调用。 refresh()之后——SpringApplication.run()在refreshContext()返回后调用callRunners(),此时容器完全就绪,Tomcat 已开始接流量。
预热时机选择:SmartInitializingSingleton 还是 ApplicationRunner?
要在"所有单例已就绪、但还没开始接 HTTP 流量"时做缓存预热(从数据库加载热点数据写入本地缓存),应该用 SmartInitializingSingleton 还是 ApplicationRunner?两者差别在哪,选错会产生什么后果?
提示(卡住再展开)
SmartInitializingSingleton.afterSingletonsInstantiated() 在阶段 11 末尾、finishRefresh(Tomcat start)之前触发——预热期间外部流量还不会打进来,缓存处于一致状态时再开放服务。ApplicationRunner 在 callRunners() 里执行,此时 Tomcat 已接流量——若预热耗时长,早进来的请求会拿到空缓存。对于"不希望在预热未完成时对外服务"的场景,SmartInitializingSingleton 是正确选择;如果预热只是"最好有"而非"必须先有",ApplicationRunner 的好处是能拿到 ApplicationArguments 并且与其他 runner 的顺序更可控。