容器启动内核:refresh() 十二步全流程拆解
新手看到 refresh() 十二个方法名,第一反应通常是「背不下来」。其实这一篇只讲一件事:你写的那一行 new AnnotationConfigApplicationContext(...),在容器内部是一条有严格先后的流水线——先把「有哪些对象」这张清单定下来,再把「怎么加工对象」的钩子挂上,最后才真正开始造对象。顺序一旦乱,就会出现「AOP 静默不生效」「启动炸在半路」这类最难查的问题。所以本篇不是让你背十二个名字,而是让你记住三段时间。
先把全文反复出现的词一句话解释清楚:
- 容器:帮你创建并管理对象的那个「大管家」对象(
ApplicationContext),你要东西就向它要(getBean) - Bean:交给容器创建和管理的那个普通 Java 对象
- BeanDefinition:描述「怎么造某个 Bean」的一份数据(类名、单例还是原型、懒不懒加载),此时对象还不存在
refresh():容器把自己从「空壳」变成「随时能交货」的那套流程,共十二步,是AbstractApplicationContext的方法- BeanFactoryPostProcessor(简称 BFPP):在任何对象被造出来之前批量修改「有哪些 Bean」的处理器——改图纸的人
- BeanPostProcessor(简称 BPP):在每个对象出生的前一刻和后一刻各插一刀的钩子——改造成品的人
@PostConstruct:对象刚建好、属性也填完之后自动调一次的方法注解,属于「出厂自检」- Runner(
ApplicationRunner/CommandLineRunner):容器完全就绪之后才被叫起来干活的启动任务 - 反射:程序运行时「看着类名去调构造器」的技术;容器没编译期绑定你的类,全靠反射按定义里的类名把对象造出来
refresh() 就是开一家店的全过程。定选址=准备 Environment(第 1 步,水电网通不通、租金多少先确认,对应配置和占位符);画施工图=生成 BeanDefinition(第 2 步,只有图纸没有实物);通水电买厨具=装配容器内建组件(第 3~4 步);装修阶段改图纸=BeanFactoryPostProcessor(第 5 步,墙还能拆、房间还能加);给员工做礼仪培训=BeanPostProcessor 注册上岗(第 6 步,规矩必须在任何人开工前讲完);装收银机、拉广播线=MessageSource 与事件机制(第 7~10 步);试营业后厨全部备菜=预实例化所有非懒加载单例(第 11 步,最耗时);剪彩=发布 ContextRefreshedEvent(第 12 步,正式宣布开业);开门迎客=执行 Runner(已在 refresh() 之外,顾客此刻才进得来)。

这家店里有两个人最容易被搞混——拿着红笔改施工图的(BeanFactoryPostProcessor,第 5 步)和菜端上桌后负责回炉加工、换盘的(BeanPostProcessor,第 6 步之后上岗)。前者决定「菜单上有没有这道菜」,后者只能决定「这盘菜怎么摆」。第二节有一张对照图把这条分界线钉死。
学完这一篇,你应该能回答三个问题:
- 我在类上写的
@Autowired到底是在第几步开始起作用的?为什么第 5 步时它还完全不存在? - 非懒加载单例是在哪一步被
new出来的?如果那一步失败,前面已经造好的对象会怎样? - 我的自定义逻辑该挂在 BFPP、BPP、
@PostConstruct还是 Runner 上?各自「看得见什么、看不见什么」?
几乎所有 Spring 教程都从这一行开始,但很少有人停下来问:这一行到底触发了多少件事?
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()早已把所有单例预实例化完毕
等价写法其实是三步合并:new AnnotationConfigApplicationContext() → register(AppConfig.class) → refresh()。把这个等式记住,后面所有问题都会变得清晰。

refresh() 是 AbstractApplicationContext 的方法,全文骨架(去掉异常处理)如下:
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() - 读源码时先看这段骨架,再逐层点进去,比漫无目的地跳转高效得多

下面这张表是本文的核心,建议收藏。每一步都标注了做什么、在哪个方法、你可以在哪里插手:
| 步骤 | 关键方法 | 一步职责 | 你能插入的扩展点 |
|---|---|---|---|
| 1 | prepareRefresh | 记录启动时间、标记 active、校验必需的属性占位符 | ApplicationContextInitializer、覆盖 initPropertySources |
| 2 | obtainFreshBeanFactory | 创建 DefaultListableBeanFactory,把配置类/XML 解析成 BeanDefinition | BeanDefinitionRegistryPostProcessor 可继续加定义 |
| 3 | prepareBeanFactory | 给工厂装"内建标配":ClassLoader、ApplicationContextAwareProcessor、忽略的依赖接口 | 覆盖 prepareBeanFactory 追加忽略项 |
| 4 | postProcessBeanFactory | 留给子类的钩子,此时定义已冻结、实例未创建 | 子类重写(如 WebApplicationContext) |
| 5 | invokeBeanFactoryPostProcessors | 执行所有 BFPP,解析 @Configuration、@Bean、@Import,生成最终定义 | BeanFactoryPostProcessor / BeanDefinitionRegistryPostProcessor |
| 6 | registerBeanPostProcessors | 实例化并排序注册所有 BeanPostProcessor | 实现 BeanPostProcessor 并让它 Ordered |
| 7 | initMessageSource | 注册国际化的 MessageSource 单例 | 自己注册名为 messageSource 的 Bean |
| 8 | initApplicationEventMulticaster | 注册事件广播器,默认是简单同步实现 | 注册名为 applicationEventMulticaster 的 Bean |
| 9 | onRefresh | 留给子类,Spring Boot 启动 Web 服务器就在这一步 | 子类重写(如 ServletWebServerApplicationContext) |
| 10 | registerListeners | 把 ApplicationListener 注册进广播器,并补发早期事件 | ApplicationListener |
| 11 | finishBeanFactoryInitialization | 预实例化所有非懒加载单例,最耗时的一步 | BeanPostProcessor、SmartInitializingSingleton |
| 12 | finishRefresh | 初始化生命周期处理器,发布 ContextRefreshedEvent | ApplicationListener<ContextRefreshedEvent> |
前 4 步是"打扫屋子"(准备环境、建工厂、装标配),第 5~6 步是"立规矩"(定义与后置器),第 7~10 步是"备好基础设施"(事件、监听器),第 11~12 步才真正"生产对象并宣布开业"。按这个节奏理解,十二步就不再是一串死记硬背的方法名。
十二个名字要靠表格背,但顺序要靠手点。下面这台单步调试台左边是 refresh() 的骨架(去掉了异常处理与注释),右边同步刷新「此刻容器里有多少定义、多少对象」。连点「下一步」,重点盯两格:第 ⑤ 步结束时 singletonObjects 仍是 0,第 ⑥ 步结束时它仍然是 0——直到第 ⑦ 步才一次性涨上去:
public void refresh() { // AbstractApplicationContext synchronized (startupShutdownMonitor) { // ① 一把锁同时管住启动与关闭 prepareRefresh(); // ② 第 1 步:记启动时间、校验占位符 bf = obtainFreshBeanFactory(); // ③ 第 2 步:工厂诞生,定义进表 prepareBeanFactory(bf); // ④ 第 3 步:装内建标配 postProcessBeanFactory(bf); // ⑤ 第 4 步:留给子类的兜底钩子 invokeBeanFactoryPostProcessors(bf); // ⑥ 第 5 步:改图纸的最后窗口 registerBeanPostProcessors(bf); // ⑦ 第 6 步:钩子先上岗 ... // ⑧ 第 7~10 步:国际化、事件、监听器 finishBeanFactoryInitialization(bf); // ⑨ 第 11 步:预实例化所有单例 finishRefresh(); // ⑩ 第 12 步:发布 ContextRefreshedEvent }}| 容器状态 | active = true |
| beanDefinitionCount | 0 |
| singletonObjects | 0 |
refreshprepareRefresh新手最难的决定其实是「我自己的逻辑该挂在哪一步」。第十节的沙盘把四个候选时机做成一个开关,切一档就立刻告诉你那一步看得见什么、看不见什么,建议读完第三、四节后回去操作一遍。

上面这条时间轴里最容易搞混的是第 5 步与第 6 步,因为它们的中文名只差一个词(都叫「后置处理器」),干的却是相反的事。把它们彻底分开记:
第 5 步的 BFPP 是改图纸——房子还没盖,它能决定有几间房、哪间当仓库;第 6 步之后上岗的 BPP 是改造已经做出来的菜——菜已端上桌,只能撒点胡椒粉、换个盘子(包一层 AOP 代理),但菜单本身早已定死。想改「有没有这道菜」必须趁图纸阶段,想改「这盘菜长什么样」只能在出菜以后,这就是全篇最重要的一句话。

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

这一步最容易被低估。它做的远不止"跑几个后置处理器",而是决定容器里到底有哪些 Bean:
// AbstractApplicationContext#invokeBeanFactoryPostProcessors 内部逻辑要点// 1. 先执行 BeanDefinitionRegistryPostProcessor(能继续加定义)// 其中最重量级的是 ConfigurationClassPostProcessor// 2. 再执行普通 BeanFactoryPostProcessor(只能改定义)PostProcessorRegistrationDelegate .invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());ConfigurationClassPostProcessor 会:扫描 @ComponentScan 指定包 → 解析 @Configuration 类 → 处理 @Bean 方法、@Import、@PropertySource → 把这一切翻译成 BeanDefinition。所以"注解能不能生效"取决于这一步,而不是构造器。
此步结束时,beanDefinitionMap 里的定义已基本定型,而没有任何单例被创建。这是"改定义"的最后窗口,也是自动配置的立足点。
BeanPostProcessor(BPP)影响每个 Bean 的初始化前后,它们的顺序直接决定 AOP 代理、@Autowired 解析、@PostConstruct 的执行次序,因此 Spring 认真地对它们分级:
// 优先级:PriorityOrdered > Ordered > 无序(无序的按注册顺序)// 结果被分成两组:普通 BPP 与 MergedBeanDefinitionPostProcessor// 前者先注册,后者在预实例化时还会被回调改写合并后的定义- 实现
PriorityOrdered的 BPP 最先注册(如AutowiredAnnotationBeanPostProcessor依赖的ConfigurationClassPostProcessor相关) - 实现
Ordered的其次 - 都没有的排最后,顺序不确定——所以自定义 BPP 要影响别人,务必实现
Ordered
坑:自定义 BeanPostProcessor 若不实现 Ordered,在与其他 BPP 交互时(比如你在 BPP 里取某个已被代理的 Bean)顺序可能和你预期相反,出现"有时对有时错"的诡异 Bug。
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { // 例如为 @Autowired 的 ConversionService 提前准备 if (beanFactory.containsBeanDefinition(CONVERSION_SERVICE_BEAN_NAME)) { /* ... */ } // 冻结所有定义,之后禁止再改 beanFactory.freezeConfiguration(); // 核心:预实例化所有非懒加载的单例 beanFactory.preInstantiateSingletons();}preInstantiateSingletons() 会遍历所有 beanDefinitionNames,跳过抽象类、非单例、lazy-init=true 的定义,对其余单例调用 getBean(name)。你在业务代码里遇到的"构造器被调用、@PostConstruct 执行、AOP 代理生成",全发生在这里。遍历结束后,还会统一回调实现了 SmartInitializingSingleton 的 Bean。
protected void finishRefresh() { clearResourceCaches(); // 清缓存 initLifecycleProcessor(); // 生命周期处理器就位 getLifecycleProcessor().onRefresh(); // 启动实现了 Lifecycle 的组件 publishEvent(new ContextRefreshedEvent(this)); // 关键:发布刷新完成事件}ContextRefreshedEvent是使用最广的启动钩子:缓存预热、连接池初始化、启动时数据加载都可以监听它- 但要注意:该事件会发布多次(子容器也会发),监听时务必判断
event.getApplicationContext()是否是自己关心的那一个 Lifecycle组件的start()也在此刻触发——Spring Boot里内嵌 Tomcat 的启动次序与之相关
打开 logging.level.org.springframework=DEBUG,你会看到如下日志。我给每一行标上对应的步骤:
# 第 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 secondsRefreshing ...是第 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 即可。
refresh() 的 catch 块很短,但含义很重:
catch (BeansException ex) { destroyBeans(); // 销毁本次已创建的所有单例,避免半成品泄漏 cancelRefresh(ex); // 撤销 active 标记,重置上下文状态 throw ex; // 原样抛出,让调用方感知启动失败}destroyBeans()会遍历已注册的单例并调用其销毁回调——这就是"启动失败也要优雅收尾"cancelRefresh()把active置回 false,使上下文可被再次尝试 refresh- 注意:抛出的异常类型通常是
BeanCreationException,真正的原因藏在getMostSpecificCause()里
Spring Boot 在此基础上加了 FailureAnalyzer(失败分析器),把丑陋的堆栈翻译成人话:
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 内置了十多个分析器(端口占用、缺少数据源、配置缺失等),它们让"启动失败"从折磨变成提示
这套收尾动作是顺序执行的,动画一次放完只要几秒,但它决定了你线上看到的现象是「干净地失败」还是「留一堆半成品」:

第 12 步上岗的那个 Lifecycle 处理器,反过来也管关闭:容器 close() 时,每个阶段(SmartLifecycle 按角色分组)都要在时限内停下来。这个数字拖一拖就明白它跟编排平台的关系:
- Boot 的默认值就是 30s,K8s 的 terminationGracePeriodSeconds 默认也是 30s
- 前提是你别把 server.shutdown 设成 graceful 之后再给业务加 25 秒的收尾——两头一挤就被强杀
- 超过这个时限,容器不会给你报错,只留一行日志
不要盲目 try { refresh(); } catch (Exception e) { / 忽略 / }。启动失败意味着容器处于"半初始化"状态,继续使用它极可能埋下难以排查的空指针。
Q1:为什么 refresh() 要加 synchronized 锁?
// AbstractApplicationContextprivate final Object startupShutdownMonitor = new Object();public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { /* ... */ }}refresh()与close()用的是同一把锁,防止"一边启动一边关闭"的竞态- 它还保证同一个上下文不会被并发 refresh 两次,否则会出现重复创建单例、事件重复发布
- 注意这把锁是方法级串行,与业务线程无关;启动完成后它不再生效
Q2:SpringApplication.run() 与 refresh() 是什么关系?
run()是Spring Boot的门面:准备Environment→ 打印 Banner → 创建ApplicationContext→ 调用refreshContext()(内部就是refresh()) → 执行ApplicationRunner/CommandLineRunner- 换句话说,
refresh()只是run()的一个关键子步骤;run()还多做了环境准备、事件监听、Banner、runner 回调等 - 记住一句话:
refresh()管"容器内部",run()管"整个应用装配"
| 维度 | refresh() | SpringApplication.run() |
|---|---|---|
| 所属类 | AbstractApplicationContext | SpringApplication |
| 主要职责 | 刷新容器:定义、后置器、预实例化单例 | 装配整个应用:环境、Banner、监听器、runner |
| 包含关系 | 被 refreshContext() 调用 | 内部调用 refresh() |
| 异常语义 | 抛 BeansException(如 BeanCreationException) | 汇总后可能转成 IllegalStateException 或原样抛出 |
在 BeanFactoryPostProcessor 里调用 getBean() 会触发提前实例化。很多人想在 BFPP 里"检查一下某个 Bean 能不能创建",于是写了 beanFactory.getBean("userService")——这会立刻让 userService 被实例化,而此时 BeanPostProcessor 还没注册,导致它拿不到 AOP 代理、@Autowired 也不会被处理,最终得到一个"残废"的 Bean。正确做法是操作 BeanDefinition,而不是对象。
refresh() 之前手动 setXxx 的时机。像 ctx.setParent(...)、ctx.register(...)、registerBeanDefinition(...) 必须在 refresh() 之前调用,否则会抛 IllegalStateException(容器已激活)。因为 refresh() 第 11 步完成后单例已入池、定义已冻结,再改就晚了。
光看方法名记不住顺序,必须让它动起来。下面四个实验按「先整体 → 再钩子 → 再看定义 → 最后看容器之外」的顺序排列,每个只需切换右上角的参数就能换视角。
第一个实验是主线:先看「完整十二步」逐步点亮,注意哪些步骤标了关键色;再切「只盯关键的4步」,你会发现 invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization、finishRefresh 这四步决定了 90% 的行为,其余八步只是把它们粘起来:
第二个实验专治新手最容易翻车的一处——钩子的时机。选「BeanPostProcessor 注册」,走到第③④步会看到:一旦某个 Bean 因为被 BPP 依赖而提前出生,它就永久逃过了加工,日志里那句 is not eligible for getting processed by all BeanPostProcessors 就是这么来的。第⑥步的门禁卡类比值得多看两遍:
第三个实验回答「图纸到底长在哪张表上」。选「注册表是什么」,看清 beanDefinitionMap 此刻只有描述信息、singletonObjects 还是空的;这正是第七节坑的根源——在定义阶段调 getBean() 等于强行插队:
第四个实验说明「自动配置塞进来的定义也是在这一步落地的」。选「装配生效」,看 ConfigurationClassPostProcessor 如何把成千上万个 @Bean 当成普通配置类一样解析进注册表;再用「装配报告」学会用 --debug 反查某个 Bean 为什么没出现:
最后一个实验把镜头拉远:refresh() 只是 SpringApplication.run() 的第 ④ 步。选「八步流程」对照开业时间轴,再切「Runner 执行」确认剪彩之后才轮到你的启动逻辑——这直接决定了第十节沙盘该怎么选档位:
第 6 步注册的那批钩子里,有一个专门负责「该不该给这个 Bean 包一层代理」——它就是 AOP 能不能生效的闸门。把参数切到「在后置处理里拦截」,你会看到它逐一询问每个候选 Bean;这也解释了第七节那条 not eligible for getting processed by all BeanPostProcessors 为什么等于「这个对象逃过了代理」:
而第 11 步真正 birth 一个 Bean 时,走的是一条固定的八站流水线。这个实验把 preInstantiateSingletons 之后的那段展开——先跑「单例」全程,再用「观察销毁回调」看第五节 destroyBeans() 到底调了谁:
实验按完了,换成自己下命令。这台控制台连着浏览器里那个真容器,beans / conditions / query 的回显都由内核现算——按下面这个顺序敲,正好是 refresh() 的时间顺序:
boot 之后立刻敲 beans,你会发现列表里既有你的业务 Bean,也有一堆以 org.springframework. 开头的内建名字——后者就是第 3 步「通水电」时塞进来的标配。分不清哪是自己写的时,看前缀最快。
新手写扩展点时的痛苦来源很具体:同一个需求,挂在四个不同时机里能看到的对象数量完全不同。挂在第 5 步连一个实例都没有;挂在 Runner 又嫌太晚(内嵌容器已经在接流量了)。下面这个沙盘把四个候选做成一个开关,切一档立刻看那一步的视野与危险:
当前位置:refresh() 第 5 步 invokeBeanFactoryPostProcessors能看见:beanDefinitionMap 里的全部定义(约 40 条)、Environment、占位符 ${}看不见:任何业务对象 —— getBean("userService") 会立刻把它强行实例化危险动作:在此处取 Bean,该对象将逃过后续 BPP 的加工适合做:批量改 scope / lazy、注入额外定义、替换实现类
把这四档连着切一遍,你会发现唯一的分界线就是「对象出生了没有」。第 5 步只有图纸,第 6 步只有钩子,第 11 步才有成品,Runner 拿到的是整家店。选型口诀:改「有没有」→ BFPP;改「长什么样」→ BPP;只管自己 → @PostConstruct;要全局就绪 → Runner。
热身题,答案就在第八节的 refresh 实验里:
重头戏,对应第三节 3.2 与第十节沙盘的第二档:
这一列「报错原文」都能整段复制去搜索。它们的共同点是:异常说的是「哪一步出了事」,而病因往往写在 caused by 链的最后一层。
| 报错原文(片段) | 真实原因 | 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 · 第十节沙盘第二档 |
新手读这类长堆栈的通病是从上往下硬啃。正确姿势是只看三行:① 最外层异常类型(判断炸在第几步);② 第一个 Error creating bean with name 'xxx'(判断哪个对象出事);③ 最后一个 Caused by:(真正的病因)。中间几十行都是同类连带,跳过去不影响结论。
上面那套读法必须有实战。下面这段就是第 11 步最常见的崩溃现场,先别看表格,点出你认为的凶手帧:
新同事加了一个仓储类,本地启动直接失败;日志刷了大半屏,他从头往下读了十分钟,一句也没读懂。
目标:用一段不到 60 行的代码,把「第 5 步改图纸」「第 6 步上岗的钩子」「第 11 步成品出生」三件事同时打印出来,亲眼确认它们的先后。
第一步,pom.xml 只要一个依赖:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.1.8</version> </dependency></dependencies>第二步,一个业务类(src/main/java/com/example/demo/HelloService.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; }}第三步,一个 BFPP 和一个 BPP(src/main/java/com/example/demo/Observers.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; } }}第四步,主程序(src/main/java/com/example/demo/RefreshLab.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(); }}预期输出(顺序本身就是答案):
=== 开始 refresh ===[BFPP] 定义总数 = 7[BFPP] helloService 是否已实例化 = false[BPP] 我拿到了成品:HelloService [构造] HelloService 被 new 出来 [自检] @PostConstruct 执行(第 11 步内部)=== refresh 结束,容器已就绪 ===Hello, Spring[BFPP]打在[构造]之前,且containsSingleton为false:证明第 5 步只有图纸、没有任何对象[BPP]拿到的是已存在的实例:证明 BPP 处理的是成品而非定义- 若你的输出里
[构造]出现在[BFPP]之前,说明有 Bean 被提前拉起来了——回去看第七节第一个坑
每条只改一处,先猜结论再跑:
- 把
bf.getBeanDefinition("helloService").setLazyInit(true)保留,然后观察输出。你会观察到:[构造]与[自检]两行从 refresh 期间消失了,直到main里那句ctx.getBean(HelloService.class)才出现——这就是第 11 步的「跳过 lazy 定义」被你自己复现了一次。 - 在 BFPP 的方法里加一行
beanFactory.getBean("helloService")。你会观察到:[构造]跑到[BFPP]之后立刻打印,而[BPP]那一行再也不出现。这就是「AOP 静默不生效」的最小模型,对应速查表最后一行。 - 给
Observers.DishFinisher加上implements Ordered { public int getOrder() { return 1; } },再加第二个 BPP 返回Ordered.LOWEST_PRECEDENCE。你会观察到:两个 BPP 的打印顺序变得稳定可预测;去掉Ordered后顺序会随定义注册顺序漂移。 - 把
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:——两种都对应速查表第四行。
提示:做完第 2 条再回去跑第八节的 refresh 实验(pp 参数),框架视角和你手工复现的现象应当严丝合缝。
做一个「启动时机观测器」StartupPhaseLogger,把容器各个阶段的视野差异量化成一份报告——这也是你能写进简历的第一个「框架级」小工具。
需求:
- 提供若干实现类,分别挂在
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复现出来,并在报告里标注它对应的阶段
验收清单:① 输出的五阶段序号严格递增且能与 refresh() 十二步一一对应;② getSingletonCount() 在 BFPP 阶段明显小于 BPP 阶段;③ 把 BFPP 里那句 getBean 注释掉后,告警行消失且该 Bean 重新获得代理;④ 全程只用 ConfigurableListableBeanFactory 的公开方法,不依赖反射私有字段。
不看上文,把十二步压缩成三段时间,并说出每段各自在解决什么问题。
非懒加载单例在哪一步被创建?那一步之前 singletonObjects 里有几个业务对象?
想改「有哪些 Bean」用什么接口、在第几步?想改「对象长什么样」又用什么、什么时候生效?
自定义 BeanPostProcessor 里注入一个普通 Bean 会导致什么后果?日志里哪一句是它的特征指纹?
refresh() 第 11 步抛错时,前面已经造好的 Bean 去了哪里?为什么说它是「全有或全无」?
一二三四打地基(环境·工厂·标配),五六立规矩(改图纸·培训员工),七八九十备家伙(文案·广播·空钩·监听),十一才出锅(预实例化),十二剪彩(发事件),开门迎客是 Runner(已在 refresh 之外)。
refresh() 不是黑箱,而是十二个职责清晰的步骤。记住三组分期——第 1~4 步准备环境与工厂、第 5~6 步处理定义与注册后置器、第 7~12 步备好基础设施并预实例化单例;记住两个窗口——BeanFactoryPostProcessor 改定义(第 5 步)、BeanPostProcessor 改实例(第 6 步之后);记住一个终点——第 12 步发布 ContextRefreshedEvent。看懂这十二步,Spring 的启动对你就不再有任何秘密。