容器启动内核:refresh() 十二步全流程拆解

bee2026-10-0870 分钟0 次阅读
一行 new AnnotationConfigApplicationContext() 背后,是 12 个精密步骤。逐步拆解 refresh() 的每一步职责、对应扩展点与源码位置,看懂 Spring 启动时真正发生的事。
1 / 133
小节
〇、30 秒看懂
2 / 133

新手看到 refresh() 十二个方法名,第一反应通常是「背不下来」。其实这一篇只讲一件事:你写的那一行 new AnnotationConfigApplicationContext(...),在容器内部是一条有严格先后的流水线——先把「有哪些对象」这张清单定下来,再把「怎么加工对象」的钩子挂上,最后才真正开始造对象。顺序一旦乱,就会出现「AOP 静默不生效」「启动炸在半路」这类最难查的问题。所以本篇不是让你背十二个名字,而是让你记住三段时间。

3 / 133

先把全文反复出现的词一句话解释清楚:

4 / 133
  • 容器:帮你创建并管理对象的那个「大管家」对象(ApplicationContext),你要东西就向它要(getBean)
  • Bean:交给容器创建和管理的那个普通 Java 对象
  • BeanDefinition:描述「怎么造某个 Bean」的一份数据(类名、单例还是原型、懒不懒加载),此时对象还不存在
  • refresh():容器把自己从「空壳」变成「随时能交货」的那套流程,共十二步,是 AbstractApplicationContext 的方法
  • BeanFactoryPostProcessor(简称 BFPP):在任何对象被造出来之前批量修改「有哪些 Bean」的处理器——改图纸的人
  • BeanPostProcessor(简称 BPP):在每个对象出生的前一刻和后一刻各插一刀的钩子——改造成品的人
  • @PostConstruct:对象刚建好、属性也填完之后自动调一次的方法注解,属于「出厂自检」
  • Runner(ApplicationRunner / CommandLineRunner):容器完全就绪之后才被叫起来干活的启动任务
  • 反射:程序运行时「看着类名去调构造器」的技术;容器没编译期绑定你的类,全靠反射按定义里的类名把对象造出来
5 / 133
类比

refresh() 就是开一家店的全过程。定选址=准备 Environment(第 1 步,水电网通不通、租金多少先确认,对应配置和占位符);画施工图=生成 BeanDefinition(第 2 步,只有图纸没有实物);通水电买厨具=装配容器内建组件(第 3~4 步);装修阶段改图纸=BeanFactoryPostProcessor(第 5 步,墙还能拆、房间还能加);给员工做礼仪培训=BeanPostProcessor 注册上岗(第 6 步,规矩必须在任何人开工前讲完);装收银机、拉广播线=MessageSource 与事件机制(第 7~10 步);试营业后厨全部备菜=预实例化所有非懒加载单例(第 11 步,最耗时);剪彩=发布 ContextRefreshedEvent(第 12 步,正式宣布开业);开门迎客=执行 Runner(已在 refresh() 之外,顾客此刻才进得来)。

6 / 133
架构图
图 · 本篇地图:十二步对应的开业时间轴
图 · 本篇地图:十二步对应的开业时间轴
7 / 133
类比

这家店里有两个人最容易被搞混——拿着红笔改施工图的(BeanFactoryPostProcessor,第 5 步)和菜端上桌后负责回炉加工、换盘的(BeanPostProcessor,第 6 步之后上岗)。前者决定「菜单上有没有这道菜」,后者只能决定「这盘菜怎么摆」。第二节有一张对照图把这条分界线钉死。

8 / 133

学完这一篇,你应该能回答三个问题:

9 / 133
  • 我在类上写的 @Autowired 到底是在第几步开始起作用的?为什么第 5 步时它还完全不存在?
  • 非懒加载单例是在哪一步被 new 出来的?如果那一步失败,前面已经造好的对象会怎样?
  • 我的自定义逻辑该挂在 BFPP、BPP、@PostConstruct 还是 Runner 上?各自「看得见什么、看不见什么」?
10 / 133
小节
一、从一个 main 方法出发
11 / 133

几乎所有 Spring 教程都从这一行开始,但很少有人停下来问:这一行到底触发了多少件事?

12 / 133
代码对照
代码java
package com.example;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class BootDemo {    public static void main(String[] args) {        // 看似平淡的一行,背后是 refresh() 的十二个步骤        AnnotationConfigApplicationContext ctx =                new AnnotationConfigApplicationContext(AppConfig.class);        HelloService hello = ctx.getBean(HelloService.class);        System.out.println(hello.sayHello("Spring"));        ctx.close();    }}
解读
  • new AnnotationConfigApplicationContext(AppConfig.class):构造器只做两件事——把 AppConfig 登记为配置类,然后在最后一行调用 refresh()
  • 真正"干活"的全在 refresh() 里,构造器本身不解析任何注解、不创建任何 Bean
  • getBean(...) 之所以能立刻取到对象,是因为 refresh() 早已把所有单例预实例化完毕
13 / 133

等价写法其实是三步合并:new AnnotationConfigApplicationContext() → register(AppConfig.class) → refresh()。把这个等式记住,后面所有问题都会变得清晰。

14 / 133
架构图
图 1 · refresh() 十二步全流程
图 1 · refresh() 十二步全流程
15 / 133

refresh() 是 AbstractApplicationContext 的方法,全文骨架(去掉异常处理)如下:

16 / 133
代码对照
代码java
public void refresh() throws BeansException, IllegalStateException {    synchronized (this.startupShutdownMonitor) {        prepareRefresh();                                  // 第 1 步        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 2        prepareBeanFactory(beanFactory);                   // 第 3 步        try {            postProcessBeanFactory(beanFactory);           // 第 4 步            invokeBeanFactoryPostProcessors(beanFactory);  // 第 5 步            registerBeanPostProcessors(beanFactory);       // 第 6 步            initMessageSource();                           // 第 7 步            initApplicationEventMulticaster();             // 第 8 步            onRefresh();                                   // 第 9 步            registerListeners();                           // 第 10 步            finishBeanFactoryInitialization(beanFactory);  // 第 11 步:重头戏在此            finishRefresh();                               // 第 12 步        } catch (BeansException ex) {            destroyBeans();            cancelRefresh(ex);            throw ex;        }    }}
解读
  • 整个方法被 synchronized 包住,说明同一个上下文不允许并发 refresh
  • 第 4 步之前没有 try,意味着"准备工作"失败不会走销毁逻辑;第 4 步之后失败会 destroyBeans()
  • 读源码时先看这段骨架,再逐层点进去,比漫无目的地跳转高效得多
17 / 133
小节
二、十二步逐一拆解
18 / 133
原理动画
动图 · 八步看懂容器启动
动图 · 八步看懂容器启动
19 / 133

下面这张表是本文的核心,建议收藏。每一步都标注了做什么、在哪个方法、你可以在哪里插手:

20 / 133
对照表
步骤关键方法一步职责你能插入的扩展点
1prepareRefresh记录启动时间、标记 active、校验必需的属性占位符ApplicationContextInitializer、覆盖 initPropertySources
2obtainFreshBeanFactory创建 DefaultListableBeanFactory,把配置类/XML 解析成 BeanDefinitionBeanDefinitionRegistryPostProcessor 可继续加定义
3prepareBeanFactory给工厂装"内建标配":ClassLoader、ApplicationContextAwareProcessor、忽略的依赖接口覆盖 prepareBeanFactory 追加忽略项
4postProcessBeanFactory留给子类的钩子,此时定义已冻结、实例未创建子类重写(如 WebApplicationContext)
5invokeBeanFactoryPostProcessors执行所有 BFPP,解析 @Configuration、@Bean、@Import,生成最终定义BeanFactoryPostProcessor / BeanDefinitionRegistryPostProcessor
6registerBeanPostProcessors实例化并排序注册所有 BeanPostProcessor实现 BeanPostProcessor 并让它 Ordered
7initMessageSource注册国际化的 MessageSource 单例自己注册名为 messageSource 的 Bean
8initApplicationEventMulticaster注册事件广播器,默认是简单同步实现注册名为 applicationEventMulticaster 的 Bean
9onRefresh留给子类,Spring Boot 启动 Web 服务器就在这一步子类重写(如 ServletWebServerApplicationContext)
10registerListeners把 ApplicationListener 注册进广播器,并补发早期事件ApplicationListener
11finishBeanFactoryInitialization预实例化所有非懒加载单例,最耗时的一步BeanPostProcessor、SmartInitializingSingleton
12finishRefresh初始化生命周期处理器,发布 ContextRefreshedEventApplicationListener<ContextRefreshedEvent>
21 / 133
提示

前 4 步是"打扫屋子"(准备环境、建工厂、装标配),第 5~6 步是"立规矩"(定义与后置器),第 7~10 步是"备好基础设施"(事件、监听器),第 11~12 步才真正"生产对象并宣布开业"。按这个节奏理解,十二步就不再是一串死记硬背的方法名。

22 / 133

十二个名字要靠表格背,但顺序要靠手点。下面这台单步调试台左边是 refresh() 的骨架(去掉了异常处理与注释),右边同步刷新「此刻容器里有多少定义、多少对象」。连点「下一步」,重点盯两格:第 ⑤ 步结束时 singletonObjects 仍是 0,第 ⑥ 步结束时它仍然是 0——直到第 ⑦ 步才一次性涨上去:

23 / 133
单步调试台
单步台refresh() 骨架单步走:定义何时定型、对象何时出生1 / 9
按顺序点九次,盯右边两个数字:beanDefinitionCount 与 singletonObjects
被调试的代码
1public void refresh() { // AbstractApplicationContext
2 synchronized (startupShutdownMonitor) { // ① 一把锁同时管住启动与关闭
3 prepareRefresh(); // ② 第 1 步:记启动时间、校验占位符
4 bf = obtainFreshBeanFactory(); // ③ 第 2 步:工厂诞生,定义进表
5 prepareBeanFactory(bf); // ④ 第 3 步:装内建标配
6 postProcessBeanFactory(bf); // ⑤ 第 4 步:留给子类的兜底钩子
7 invokeBeanFactoryPostProcessors(bf); // ⑥ 第 5 步:改图纸的最后窗口
8 registerBeanPostProcessors(bf); // ⑦ 第 6 步:钩子先上岗
9 ... // ⑧ 第 7~10 步:国际化、事件、监听器
10 finishBeanFactoryInitialization(bf); // ⑨ 第 11 步:预实例化所有单例
11 finishRefresh(); // ⑩ 第 12 步:发布 ContextRefreshedEvent
12 }
13}
此刻的变量
容器状态active = true
beanDefinitionCount0
singletonObjects0
调用栈
1refresh
2prepareRefresh
1这一步只做三件小事:记下启动时间戳、把 active 置真、校验必需的占位符。第三件最容易被人忽略——如果某个 ${...} 没有默认值又没配,异常就在这儿抛,而不是等到用 Bean 时。
24 / 133

新手最难的决定其实是「我自己的逻辑该挂在哪一步」。第十节的沙盘把四个候选时机做成一个开关,切一档就立刻告诉你那一步看得见什么、看不见什么,建议读完第三、四节后回去操作一遍。

25 / 133
原理动画
动图 · 八步看懂容器启动(本节速览)
动图 · 八步看懂容器启动(本节速览)
26 / 133

上面这条时间轴里最容易搞混的是第 5 步与第 6 步,因为它们的中文名只差一个词(都叫「后置处理器」),干的却是相反的事。把它们彻底分开记:

27 / 133
类比

第 5 步的 BFPP 是改图纸——房子还没盖,它能决定有几间房、哪间当仓库;第 6 步之后上岗的 BPP 是改造已经做出来的菜——菜已端上桌,只能撒点胡椒粉、换个盘子(包一层 AOP 代理),但菜单本身早已定死。想改「有没有这道菜」必须趁图纸阶段,想改「这盘菜长什么样」只能在出菜以后,这就是全篇最重要的一句话。

28 / 133
架构图
图 · 改图纸 vs 改造成品
图 · 改图纸 vs 改造成品
29 / 133

再把镜头对准你写的某一个类:它从「一张图纸」变成「桌上那道菜」,其实横跨了第 2、5、6、11 步四个节点。下面这段动画就是这一趟旅程,看清每一步能碰到的到底是定义还是对象:

30 / 133
原理动画
动图 · 一个单例在十二步里的旅程
动图 · 一个单例在十二步里的旅程
31 / 133
小节
三、重点深挖其中四步
32 / 133
小节
3.1 第 5 步:invokeBeanFactoryPostProcessors —— 注解世界的翻译官
33 / 133

这一步最容易被低估。它做的远不止"跑几个后置处理器",而是决定容器里到底有哪些 Bean:

34 / 133
java
// AbstractApplicationContext#invokeBeanFactoryPostProcessors 内部逻辑要点// 1. 先执行 BeanDefinitionRegistryPostProcessor(能继续加定义)//    其中最重量级的是 ConfigurationClassPostProcessor// 2. 再执行普通 BeanFactoryPostProcessor(只能改定义)PostProcessorRegistrationDelegate        .invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
35 / 133

ConfigurationClassPostProcessor 会:扫描 @ComponentScan 指定包 → 解析 @Configuration 类 → 处理 @Bean 方法、@Import、@PropertySource → 把这一切翻译成 BeanDefinition。所以"注解能不能生效"取决于这一步,而不是构造器。

36 / 133
注意

此步结束时,beanDefinitionMap 里的定义已基本定型,而没有任何单例被创建。这是"改定义"的最后窗口,也是自动配置的立足点。

37 / 133
小节
3.2 第 6 步:registerBeanPostProcessors —— 排序比注册更重要
38 / 133

BeanPostProcessor(BPP)影响每个 Bean 的初始化前后,它们的顺序直接决定 AOP 代理、@Autowired 解析、@PostConstruct 的执行次序,因此 Spring 认真地对它们分级:

39 / 133
代码对照
代码java
// 优先级:PriorityOrdered > Ordered > 无序(无序的按注册顺序)// 结果被分成两组:普通 BPP 与 MergedBeanDefinitionPostProcessor// 前者先注册,后者在预实例化时还会被回调改写合并后的定义
解读
  • 实现 PriorityOrdered 的 BPP 最先注册(如 AutowiredAnnotationBeanPostProcessor 依赖的 ConfigurationClassPostProcessor 相关)
  • 实现 Ordered 的其次
  • 都没有的排最后,顺序不确定——所以自定义 BPP 要影响别人,务必实现 Ordered

坑:自定义 BeanPostProcessor 若不实现 Ordered,在与其他 BPP 交互时(比如你在 BPP 里取某个已被代理的 Bean)顺序可能和你预期相反,出现"有时对有时错"的诡异 Bug。

40 / 133
小节
3.3 第 11 步:finishBeanFactoryInitialization —— 真正的重头戏
41 / 133
java
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {    // 例如为 @Autowired 的 ConversionService 提前准备    if (beanFactory.containsBeanDefinition(CONVERSION_SERVICE_BEAN_NAME)) { /* ... */ }    // 冻结所有定义,之后禁止再改    beanFactory.freezeConfiguration();    // 核心:预实例化所有非懒加载的单例    beanFactory.preInstantiateSingletons();}
42 / 133

preInstantiateSingletons() 会遍历所有 beanDefinitionNames,跳过抽象类、非单例、lazy-init=true 的定义,对其余单例调用 getBean(name)。你在业务代码里遇到的"构造器被调用、@PostConstruct 执行、AOP 代理生成",全发生在这里。遍历结束后,还会统一回调实现了 SmartInitializingSingleton 的 Bean。

43 / 133
小节
3.4 第 12 步:finishRefresh —— 宣布开业
44 / 133
代码对照
代码java
protected void finishRefresh() {    clearResourceCaches();                    // 清缓存    initLifecycleProcessor();                 // 生命周期处理器就位    getLifecycleProcessor().onRefresh();      // 启动实现了 Lifecycle 的组件    publishEvent(new ContextRefreshedEvent(this)); // 关键:发布刷新完成事件}
解读
  • ContextRefreshedEvent 是使用最广的启动钩子:缓存预热、连接池初始化、启动时数据加载都可以监听它
  • 但要注意:该事件会发布多次(子容器也会发),监听时务必判断 event.getApplicationContext() 是否是自己关心的那一个
  • Lifecycle 组件的 start() 也在此刻触发——Spring Boot 里内嵌 Tomcat 的启动次序与之相关
45 / 133
小节
四、启动日志对照:每一行对应第几步
46 / 133

打开 logging.level.org.springframework=DEBUG,你会看到如下日志。我给每一行标上对应的步骤:

47 / 133
代码对照
代码text
# 第 1 步 · prepareRefresh2024-06-01 17:02:11.301 [main] DEBUG o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@1a2b3c4d# 第 5 步 · invokeBeanFactoryPostProcessors(ConfigurationClassPostProcessor 在干活)2024-06-01 17:02:11.412 [main] INFO  o.s.c.a.ConfigurationClassPostProcessor - Cannot enhance @Configuration bean definition 'AppConfig' since its singleton instance has been created too early# 第 6 步 · registerBeanPostProcessors2024-06-01 17:02:11.470 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'# 第 11 步 · finishBeanFactoryInitialization(大批单例在此被 new 出来)2024-06-01 17:02:11.520 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'helloService'2024-06-01 17:02:11.523 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'userService'# 第 12 步 · finishRefresh2024-06-01 17:02:11.589 [main] DEBUG o.s.c.e.EventPublicationInterceptor - ...2024-06-01 17:02:11.610 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Started BootDemo in 0.312 seconds
解读
  • Refreshing ... 是第 1 步打的头,Started ... in x seconds 是第 12 步的收尾,中间一小段就是整个启动
  • 大量 Creating shared instance of singleton bean 'xxx' 集中在第 11 步,这一段的耗时通常占启动时间的 80% 以上
  • 日志里出现 "created too early" 警告,往往就是你踩了第七节的坑

要点:调优启动速度时,把 DEBUG 日志里第 11 步的 Creating shared instance 按耗时排序,就是最直接的优化清单——哪些 Bean 不该在启动时创建,交给 @Lazy 即可。

48 / 133
小节
五、refresh 失败会发生什么
49 / 133

refresh() 的 catch 块很短,但含义很重:

50 / 133
代码对照
代码java
catch (BeansException ex) {    destroyBeans();       // 销毁本次已创建的所有单例,避免半成品泄漏    cancelRefresh(ex);    // 撤销 active 标记,重置上下文状态    throw ex;             // 原样抛出,让调用方感知启动失败}
解读
  • destroyBeans() 会遍历已注册的单例并调用其销毁回调——这就是"启动失败也要优雅收尾"
  • cancelRefresh() 把 active 置回 false,使上下文可被再次尝试 refresh
  • 注意:抛出的异常类型通常是 BeanCreationException,真正的原因藏在 getMostSpecificCause() 里
51 / 133

Spring Boot 在此基础上加了 FailureAnalyzer(失败分析器),把丑陋的堆栈翻译成人话:

52 / 133
代码对照
代码java
public class MyPortFailureAnalyzer extends AbstractFailureAnalyzer<BindException> {    @Override    protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) {        return new FailureAnalysis(            "端口被占用,无法启动服务:" + cause.getMessage(),            "请修改 server.port 或关闭占用该端口的进程。",            cause);    }}
解读
  • 注册方式:在 META-INF/spring.factories(Boot 3 用 META-INF/spring/org.springframework.boot.diagnostics.FailureAnalyzer.imports)里声明
  • Boot 内置了十多个分析器(端口占用、缺少数据源、配置缺失等),它们让"启动失败"从折磨变成提示
53 / 133

这套收尾动作是顺序执行的,动画一次放完只要几秒,但它决定了你线上看到的现象是「干净地失败」还是「留一堆半成品」:

54 / 133
原理动画
动图 · refresh 失败时的收尾五连
动图 · refresh 失败时的收尾五连
55 / 133

第 12 步上岗的那个 Lifecycle 处理器,反过来也管关闭:容器 close() 时,每个阶段(SmartLifecycle 按角色分组)都要在时限内停下来。这个数字拖一拖就明白它跟编排平台的关系:

56 / 133
参数调节台
调节台关闭阶段给多久:超时不是等得越久越安全
spring.lifecycle.timeout-per-shutdown-phase
30秒当前 0 – 120
默认档:与 K8s 宽限期刚好对齐
  • Boot 的默认值就是 30s,K8s 的 terminationGracePeriodSeconds 默认也是 30s
  • 前提是你别把 server.shutdown 设成 graceful 之后再给业务加 25 秒的收尾——两头一挤就被强杀
  • 超过这个时限,容器不会给你报错,只留一行日志
在途请求被截断8%
停机耗时60%
这个旋钮真正的对手不是业务耗时,而是编排平台给你的那几秒——两头必须一起配。
57 / 133
警告

不要盲目 try { refresh(); } catch (Exception e) { / 忽略 / }。启动失败意味着容器处于"半初始化"状态,继续使用它极可能埋下难以排查的空指针。

58 / 133
小节
六、面试追问:两个高频问题
59 / 133

Q1:为什么 refresh() 要加 synchronized 锁?

60 / 133
代码对照
代码java
// AbstractApplicationContextprivate final Object startupShutdownMonitor = new Object();public void refresh() throws BeansException, IllegalStateException {    synchronized (this.startupShutdownMonitor) { /* ... */ }}
解读
  • refresh() 与 close() 用的是同一把锁,防止"一边启动一边关闭"的竞态
  • 它还保证同一个上下文不会被并发 refresh 两次,否则会出现重复创建单例、事件重复发布
  • 注意这把锁是方法级串行,与业务线程无关;启动完成后它不再生效
61 / 133

Q2:SpringApplication.run() 与 refresh() 是什么关系?

62 / 133
  • run() 是 Spring Boot 的门面:准备 Environment → 打印 Banner → 创建 ApplicationContext → 调用 refreshContext()(内部就是 refresh()) → 执行 ApplicationRunner / CommandLineRunner
  • 换句话说,refresh() 只是 run() 的一个关键子步骤;run() 还多做了环境准备、事件监听、Banner、runner 回调等
  • 记住一句话:refresh() 管"容器内部",run() 管"整个应用装配"
63 / 133
对照表
维度refresh()SpringApplication.run()
所属类AbstractApplicationContextSpringApplication
主要职责刷新容器:定义、后置器、预实例化单例装配整个应用:环境、Banner、监听器、runner
包含关系被 refreshContext() 调用内部调用 refresh()
异常语义抛 BeansException(如 BeanCreationException)汇总后可能转成 IllegalStateException 或原样抛出
64 / 133
小节
七、两个坑:扩展点用错了会出事
65 / 133
坑

在 BeanFactoryPostProcessor 里调用 getBean() 会触发提前实例化。很多人想在 BFPP 里"检查一下某个 Bean 能不能创建",于是写了 beanFactory.getBean("userService")——这会立刻让 userService 被实例化,而此时 BeanPostProcessor 还没注册,导致它拿不到 AOP 代理、@Autowired 也不会被处理,最终得到一个"残废"的 Bean。正确做法是操作 BeanDefinition,而不是对象。

66 / 133
坑

refresh() 之前手动 setXxx 的时机。像 ctx.setParent(...)、ctx.register(...)、registerBeanDefinition(...) 必须在 refresh() 之前调用,否则会抛 IllegalStateException(容器已激活)。因为 refresh() 第 11 步完成后单例已入池、定义已冻结,再改就晚了。

67 / 133
小节
八、上手体验:四个内核实验把十二步跑给你看
68 / 133

光看方法名记不住顺序,必须让它动起来。下面四个实验按「先整体 → 再钩子 → 再看定义 → 最后看容器之外」的顺序排列,每个只需切换右上角的参数就能换视角。

69 / 133

第一个实验是主线:先看「完整十二步」逐步点亮,注意哪些步骤标了关键色;再切「只盯关键的4步」,你会发现 invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization、finishRefresh 这四步决定了 90% 的行为,其余八步只是把它们粘起来:

70 / 133
内核实验
TeaVMrefresh() 十二步逐一点亮未启动
先选「完整十二步」看顺序,再切「只盯关键的4步」抓主干
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
71 / 133

第二个实验专治新手最容易翻车的一处——钩子的时机。选「BeanPostProcessor 注册」,走到第③④步会看到:一旦某个 Bean 因为被 BPP 依赖而提前出生,它就永久逃过了加工,日志里那句 is not eligible for getting processed by all BeanPostProcessors 就是这么来的。第⑥步的门禁卡类比值得多看两遍:

72 / 133
内核实验
TeaVM钩子为什么必须先上岗未启动
重点看③④两步:早出生的 Bean 带着「未被增强」的身份活一辈子
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
73 / 133

第三个实验回答「图纸到底长在哪张表上」。选「注册表是什么」,看清 beanDefinitionMap 此刻只有描述信息、singletonObjects 还是空的;这正是第七节坑的根源——在定义阶段调 getBean() 等于强行插队:

74 / 133
内核实验
TeaVM第 5 步结束时,注册表里有什么、缺什么未启动
对照两张表:beanDefinitionMap 已有几十条,singletonObjects 仍为空
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
75 / 133

第四个实验说明「自动配置塞进来的定义也是在这一步落地的」。选「装配生效」,看 ConfigurationClassPostProcessor 如何把成千上万个 @Bean 当成普通配置类一样解析进注册表;再用「装配报告」学会用 --debug 反查某个 Bean 为什么没出现:

76 / 133
内核实验
TeaVM别人的 @Bean 是在哪一步进了我的注册表未启动
选「装配生效」看它仍是第 5 步的产物;切「装配报告」学查 Negative matches
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
77 / 133

最后一个实验把镜头拉远:refresh() 只是 SpringApplication.run() 的第 ④ 步。选「八步流程」对照开业时间轴,再切「Runner 执行」确认剪彩之后才轮到你的启动逻辑——这直接决定了第十节沙盘该怎么选档位:

78 / 133
内核实验
TeaVMrefresh 之外的三步:选址、剪彩、开门迎客未启动
先用「八步流程」定位 refreshContext 的位置,再用「Runner 执行」看你的逻辑真正何时开跑
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
79 / 133

第 6 步注册的那批钩子里,有一个专门负责「该不该给这个 Bean 包一层代理」——它就是 AOP 能不能生效的闸门。把参数切到「在后置处理里拦截」,你会看到它逐一询问每个候选 Bean;这也解释了第七节那条 not eligible for getting processed by all BeanPostProcessors 为什么等于「这个对象逃过了代理」:

80 / 133
内核实验
TeaVM第 6 步上岗的钩子里,谁负责包代理未启动
看候选判定与代理工厂两步;对照第七节那个「静默不生效」的坑
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
81 / 133

而第 11 步真正 birth 一个 Bean 时,走的是一条固定的八站流水线。这个实验把 preInstantiateSingletons 之后的那段展开——先跑「单例」全程,再用「观察销毁回调」看第五节 destroyBeans() 到底调了谁:

82 / 133
内核实验
TeaVM第 11 步里,一个 Bean 是怎么走完一生的未启动
重点看销毁阶段:refresh 失败时这些回调照样执行,这就是「优雅收尾」的全部内容
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
83 / 133
小节
8.1 换成命令行:把十二步敲出来
84 / 133

实验按完了,换成自己下命令。这台控制台连着浏览器里那个真容器,beans / conditions / query 的回显都由内核现算——按下面这个顺序敲,正好是 refresh() 的时间顺序:

85 / 133
内核控制台
86 / 133
提示

boot 之后立刻敲 beans,你会发现列表里既有你的业务 Bean,也有一堆以 org.springframework. 开头的内建名字——后者就是第 3 步「通水电」时塞进来的标配。分不清哪是自己写的时,看前缀最快。

87 / 133
小节
九、自定义扩展点该挂在哪一步?
88 / 133
决策
决策你需要在"所有 Bean 都已经创建完成、但容器尚未发布启动完成事件"的时刻,统一做一次数据初始化。这个逻辑应该挂在哪一步?
89 / 133
小节
十、沙盘:我的逻辑挂在哪一步才看得见需要的东西
90 / 133

新手写扩展点时的痛苦来源很具体:同一个需求,挂在四个不同时机里能看到的对象数量完全不同。挂在第 5 步连一个实例都没有;挂在 Runner 又嫌太晚(内嵌容器已经在接流量了)。下面这个沙盘把四个候选做成一个开关,切一档立刻看那一步的视野与危险:

91 / 133
沙盘
沙盘扩展点时机沙盘:你的逻辑能看见什么
运行结果
当前位置:refresh() 第 5 步 invokeBeanFactoryPostProcessors
能看见:beanDefinitionMap 里的全部定义(约 40 条)、Environment、占位符 ${}
看不见:任何业务对象 —— getBean("userService") 会立刻把它强行实例化
危险动作:在此处取 Bean,该对象将逃过后续 BPP 的加工
适合做:批量改 scope / lazy、注入额外定义、替换实现类
图纸阶段:改墙可以,找人不行。这一改就是全篇最重要的一句话——想改「有没有」在这里。
92 / 133
提示

把这四档连着切一遍,你会发现唯一的分界线就是「对象出生了没有」。第 5 步只有图纸,第 6 步只有钩子,第 11 步才有成品,Runner 拿到的是整家店。选型口诀:改「有没有」→ BFPP;改「长什么样」→ BPP;只管自己 → @PostConstruct;要全局就绪 → Runner。

93 / 133
小节
十一、随堂自测
94 / 133

热身题,答案就在第八节的 refresh 实验里:

95 / 133
随堂自测
随堂自测一个没有加 `@Lazy` 的单例 Bean,是在 `refresh()` 的哪一步被真正 `new` 出来的?
先自己选一个,选中立刻告诉你对不对
96 / 133

重头戏,对应第三节 3.2 与第十节沙盘的第二档:

97 / 133
随堂自测
随堂自测为什么 `BeanPostProcessor` 必须在 `registerBeanPostProcessors()`(第 6 步)就注册完,而不能等业务 Bean 需要时再补上?
先自己选一个,选中立刻告诉你对不对
98 / 133
小节
十二、常见报错速查
99 / 133

这一列「报错原文」都能整段复制去搜索。它们的共同点是:异常说的是「哪一步出了事」,而病因往往写在 caused by 链的最后一层。

100 / 133
对照表
报错原文(片段)真实原因30 秒自救深挖看第几篇
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in class path resource [com/example/AppConfig.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is ... NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.OrderRepository' available第 11 步预实例化时,某个单例的依赖在注册表里找不到。最常见是那个类忘了标 @Service/@Component,或它所在的包不在 @ComponentScan 范围内读法只有一条:从最后一个 Caused by 往前看,根因永远在最内层;外层那一串 Error creating bean with name 只是连带失败。IDEA 里搜 Error creating bean with name 定位第一处即真凶本篇第五节 · #7 BeanDefinition
Web server failed to start. Port 8080 was already in use.(原始异常为 org.springframework.boot.web.server.PortInUseException)第 12 步 finishRefresh 起内嵌 Tomcat 时发现端口已被占。前面几千个 Bean 都造好了才失败,看着像成功了一半,其实整体作废Boot 已经替你翻译成 APPLICATION FAILED TO START + ACTION 两段人话。直接找占用者:Windows `netstat -ano \findstr :8080,macOS/Linux lsof -i :8080;临时避让用 server.port=0` 随机端口本篇第四节日志对照 · #5 第一个工程
org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'helloService' defined in URL [file [./beans.xml]]: There is already [Generic bean: class [com.example.HelloServiceImpl]] bound.同一个 beanName 被注册了两次且定义不兼容。Boot 2.1 起 spring.main.allow-bean-definition-overriding 默认 false,第 5 步就直接拦下九成是 XML 与注解混用、或 @ComponentScan 范围重叠导致同一家族被扫两遍。读异常只要抓两处:冲突的名字和 defined in 后面的两个来源;确需覆盖再显式打开开关(不建议长期开)本篇第七节坑 · #7 BeanDefinition
java.lang.IllegalStateException: org.springframework.context.annotation.AnnotationConfigApplicationContext@... has been closed already(对同一个上下文二次 refresh() 时,则先在日志里看到 Exception encountered during context initialization - cancelling refresh attempt: ...,取用 Bean 时报 ... has not been refreshed yet)你在合法时机之外重复操作了同一个上下文:close() 之后还在 getBean,或手写容器时把 register(...) + refresh() 调了两遍。startupShutdownMonitor 那把锁只防并发,不防你重复刷新记住一次性流程:new → register → refresh() → 取用 → close(),每一环只出现一次。需要干净状态就新建一个上下文;测试里交给 @SpringBootTest 管生命周期,别自己 refresh()本篇第六节 Q1 · #8 本篇
No qualifying bean of type 'com.example.Notifier' available: expected single matching bean but found 2: smsNotifier,mailNotifier(实际抛的是 NoSuchBeanDefinitionException 的子类 NoUniqueBeanDefinitionException)定义阶段一切正常,歧义发生在按类型解析那一步:同类型有两个候选而注入点上没有消歧线索三条修法按优先级:注入点加 @Qualifier("smsNotifier");给首选实现加 @Primary;确实要全量就写 List<Notifier> 让容器把两个都注进来本篇第八节 bd 实验 · #6 IoC 与 DI
没有报错的那一条:启动日志出现 Bean 'dataSource' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying),随后该 Bean 上的 @Transactional / 自定义切面静默不生效你的自定义 BeanPostProcessor 依赖了一个普通 Bean(或直接注入了 DataSource)。为了让 BPP 满足注入,那个 Bean 被迫在第 6 步之前出生,于是绕过了 postProcessAfterInitialization,没被包成代理三种解法任选:BPP 里改成注入 ObjectProvider<T> 或 @Lazy 代理;改为实现 BeanFactoryAware 延后取用;把这个 Bean 的代理职责交回 AOP 侧。切记自定义 BPP 要实现 Ordered,否则还会叠加顺序问题本篇第三节 3.2 · 第十节沙盘第二档
101 / 133
警告

新手读这类长堆栈的通病是从上往下硬啃。正确姿势是只看三行:① 最外层异常类型(判断炸在第几步);② 第一个 Error creating bean with name 'xxx'(判断哪个对象出事);③ 最后一个 Caused by:(真正的病因)。中间几十行都是同类连带,跳过去不影响结论。

102 / 133

上面那套读法必须有实战。下面这段就是第 11 步最常见的崩溃现场,先别看表格,点出你认为的凶手帧:

103 / 133
报错急救
报错急救BeanCreationException

新同事加了一个仓储类,本地启动直接失败;日志刷了大半屏,他从头往下读了十分钟,一句也没读懂。

APPLICATION FAILED TO START
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in class path resource [com/example/AppConfig.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.OrderRepository' available: expected at least 1 bean which qualifies as autowire candidate
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentNames(ConstructorResolver.java:601)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1375)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1219)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:337)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:591)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.OrderRepository' available
... 34 more
点你认为的「凶手行」(可反复试)
不会也没关系:先猜异常名,再猜哪一行在做决定。
104 / 133
小节
十三、动手练习
105 / 133
小节
第一档 · 照做
106 / 133

目标:用一段不到 60 行的代码,把「第 5 步改图纸」「第 6 步上岗的钩子」「第 11 步成品出生」三件事同时打印出来,亲眼确认它们的先后。

107 / 133

第一步,pom.xml 只要一个依赖:

108 / 133
xml
<dependencies>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-context</artifactId>        <version>6.1.8</version>    </dependency></dependencies>
109 / 133

第二步,一个业务类(src/main/java/com/example/demo/HelloService.java):

110 / 133
java
package com.example.demo;import jakarta.annotation.PostConstruct;public class HelloService {    public HelloService() {        System.out.println("   [构造] HelloService 被 new 出来");    }    @PostConstruct    public void init() {        System.out.println("   [自检] @PostConstruct 执行(第 11 步内部)");    }    public String sayHello(String name) {        return "Hello, " + name;    }}
111 / 133

第三步,一个 BFPP 和一个 BPP(src/main/java/com/example/demo/Observers.java):

112 / 133
java
package com.example.demo;import org.springframework.beans.BeansException;import org.springframework.beans.factory.config.BeanFactoryPostProcessor;import org.springframework.beans.factory.config.BeanPostProcessor;import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;import org.springframework.stereotype.Component;public class Observers {    /** 改图纸的人:第 5 步 */    @Component    static class BlueprintTweaker implements BeanFactoryPostProcessor {        @Override        public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {            System.out.println("[BFPP] 定义总数 = " + bf.getBeanDefinitionCount());            System.out.println("[BFPP] helloService 是否已实例化 = " + bf.containsSingleton("helloService"));            bf.getBeanDefinition("helloService").setLazyInit(false);        }    }    /** 改造成品的人:第 6 步注册,第 11 步回调 */    @Component    static class DishFinisher implements BeanPostProcessor {        @Override        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {            if ("helloService".equals(beanName)) {                System.out.println("[BPP] 我拿到了成品:" + bean.getClass().getSimpleName());            }            return bean;        }    }}
113 / 133

第四步,主程序(src/main/java/com/example/demo/RefreshLab.java):

114 / 133
java
package com.example.demo;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration@ComponentScan(basePackageClasses = RefreshLab.class)public class RefreshLab {    @Bean    public HelloService helloService() {        return new HelloService();    }    public static void main(String[] args) {        System.out.println("=== 开始 refresh ===");        AnnotationConfigApplicationContext ctx =                new AnnotationConfigApplicationContext(RefreshLab.class);        System.out.println("=== refresh 结束,容器已就绪 ===");        System.out.println(ctx.getBean(HelloService.class).sayHello("Spring"));        ctx.close();    }}
115 / 133

预期输出(顺序本身就是答案):

116 / 133
代码对照
代码text
=== 开始 refresh ===[BFPP] 定义总数 = 7[BFPP] helloService 是否已实例化 = false[BPP] 我拿到了成品:HelloService   [构造] HelloService 被 new 出来   [自检] @PostConstruct 执行(第 11 步内部)=== refresh 结束,容器已就绪 ===Hello, Spring
解读
  • [BFPP] 打在 [构造] 之前,且 containsSingleton 为 false:证明第 5 步只有图纸、没有任何对象
  • [BPP] 拿到的是已存在的实例:证明 BPP 处理的是成品而非定义
  • 若你的输出里 [构造] 出现在 [BFPP] 之前,说明有 Bean 被提前拉起来了——回去看第七节第一个坑
117 / 133
小节
第二档 · 变体
118 / 133

每条只改一处,先猜结论再跑:

119 / 133
  1. 把 bf.getBeanDefinition("helloService").setLazyInit(true) 保留,然后观察输出。你会观察到:[构造] 与 [自检] 两行从 refresh 期间消失了,直到 main 里那句 ctx.getBean(HelloService.class) 才出现——这就是第 11 步的「跳过 lazy 定义」被你自己复现了一次。
  2. 在 BFPP 的方法里加一行 beanFactory.getBean("helloService")。你会观察到:[构造] 跑到 [BFPP] 之后立刻打印,而 [BPP] 那一行再也不出现。这就是「AOP 静默不生效」的最小模型,对应速查表最后一行。
  3. 给 Observers.DishFinisher 加上 implements Ordered { public int getOrder() { return 1; } },再加第二个 BPP 返回 Ordered.LOWEST_PRECEDENCE。你会观察到:两个 BPP 的打印顺序变得稳定可预测;去掉 Ordered 后顺序会随定义注册顺序漂移。
  4. 把 new AnnotationConfigApplicationContext(RefreshLab.class) 换成先 new 无参、register(...)、再 refresh(),然后在 ctx.close() 之后再调一次 ctx.getBean(HelloService.class)。你会观察到:抛 java.lang.IllegalStateException: ... has been closed already;若改成重复调两次 refresh(),日志里会先出现 Exception encountered during context initialization - cancelling refresh attempt:——两种都对应速查表第四行。
120 / 133

提示:做完第 2 条再回去跑第八节的 refresh 实验(pp 参数),框架视角和你手工复现的现象应当严丝合缝。

121 / 133
小节
第三档 · 造一个
122 / 133

做一个「启动时机观测器」StartupPhaseLogger,把容器各个阶段的视野差异量化成一份报告——这也是你能写进简历的第一个「框架级」小工具。

123 / 133

需求:

124 / 133
  • 提供若干实现类,分别挂在 BeanFactoryPostProcessor、BeanPostProcessor、SmartInitializingSingleton、ApplicationListener<ContextRefreshedEvent>、CommandLineRunner 五个位置上
  • 每个位置打印同一组探针:当前 beanFactory.getBeanDefinitionCount()、beanFactory.getSingletonCount()、以及 beanFactory.containsSingleton("helloService")
  • 用一个静态 AtomicInteger 序号给每行输出编号,最终形成一张「阶段 × 能看到几个对象」的表
  • 额外挑战:在 BeanPostProcessor 那一路里,故意 @Autowired 一个普通 Bean,把日志中出现的那句 is not eligible for getting processed by all BeanPostProcessors 复现出来,并在报告里标注它对应的阶段
125 / 133

验收清单:① 输出的五阶段序号严格递增且能与 refresh() 十二步一一对应;② getSingletonCount() 在 BFPP 阶段明显小于 BPP 阶段;③ 把 BFPP 里那句 getBean 注释掉后,告警行消失且该 Bean 重新获得代理;④ 全程只用 ConfigurableListableBeanFactory 的公开方法,不依赖反射私有字段。

126 / 133
小节
十四、要点自查
127 / 133
自检

不看上文,把十二步压缩成三段时间,并说出每段各自在解决什么问题。

128 / 133
自检

非懒加载单例在哪一步被创建?那一步之前 singletonObjects 里有几个业务对象?

129 / 133
自检

想改「有哪些 Bean」用什么接口、在第几步?想改「对象长什么样」又用什么、什么时候生效?

130 / 133
自检

自定义 BeanPostProcessor 里注入一个普通 Bean 会导致什么后果?日志里哪一句是它的特征指纹?

131 / 133
自检

refresh() 第 11 步抛错时,前面已经造好的 Bean 去了哪里?为什么说它是「全有或全无」?

132 / 133
口诀

一二三四打地基(环境·工厂·标配),五六立规矩(改图纸·培训员工),七八九十备家伙(文案·广播·空钩·监听),十一才出锅(预实例化),十二剪彩(发事件),开门迎客是 Runner(已在 refresh 之外)。

133 / 133
总结

refresh() 不是黑箱,而是十二个职责清晰的步骤。记住三组分期——第 1~4 步准备环境与工厂、第 5~6 步处理定义与注册后置器、第 7~12 步备好基础设施并预实例化单例;记住两个窗口——BeanFactoryPostProcessor 改定义(第 5 步)、BeanPostProcessor 改实例(第 6 步之后);记住一个终点——第 12 步发布 ContextRefreshedEvent。看懂这十二步,Spring 的启动对你就不再有任何秘密。