主题 09Spring IOC 与循环依赖
「Spring 怎么解决循环依赖」是八股里背得最完整、答得最虚的一道题。 几乎所有人都能说出「三级缓存」,但接着问「哪三级、每一级里到底存了什么、 为什么两级不行、哪种情况它救不了」,答得上来的不到两成。
这一章不背结论。我们用反射把三个缓存的内容实时打印出来, 在循环依赖发生的那几十毫秒里,一格一格看它在哪个 map 里进、哪个 map 里出; 再把构造器注入、原型、代理包装、@Lazy 四种场景各跑一遍, 看它在哪一步炸、报什么错。所有输出都是本机跑出来的原文。
9.1面试官问 Spring 时,真正在问什么
Spring 是唯一一门「用得最多、说得最浅」的技术。日常开发里你只需要会写
@Service / @Autowired,但面试要的是容器内部的因果链:
为什么这么设计、不这么设计会怎样、出问题时报什么错。
| 你听到的问题 | 它其实在问 | 说错的代价 |
|---|---|---|
| 「说说 Spring 的 IOC」 | BeanDefinition 是图纸,容器按图施工 | 只会说「控制反转,把创建对象的权力交给容器」 |
| 「Bean 的生命周期」 | 十个回调的确切顺序 | 把 afterPropertiesSet 说在 @PostConstruct 之前 |
| 「怎么解决循环依赖」 | 三级缓存各存什么 | 背得出三个名字,说不出第二级为什么要提前暴露 |
| 「构造器注入能解决吗」 | 不能,且原因在时机上 | 答「能,加个 @Lazy 就行」——那已经不是构造器注入了 |
| 「@Async / @Transactional 的坑」 | 代理包装 + 循环依赖 = 启动失败 | 只会说「同类调用不生效」 |
| 「单例 Bean 线程安全吗」 | 容器只保证唯一,不保证无状态 | 答「安全」→ 追问一个成员变量就崩 |
spring-context 裸容器。因为要讲的是 DefaultSingletonBeanRegistry 里的三个
Map,Boot 只是把它们藏得更深。实测版本:Spring Framework 7.0.9(即 Boot 4.1 的内核)+ JDK 17.0.12; 涉及版本差异的地方会附 6.1.13(Boot 3.x 内核)的对照输出。
源码是单文件
IocDemo.java,一个 main 按参数分发:
java IocDemo cycle 只跑实验 1,all 跑全部十一组。关于输出的两个约定:① 长行快照(比如三种缓存那一行)为了看清结构做了拆行, 内容一字未改;② 行尾以
<-- 开头的是我加的批注,不是程序打印的。
9.2容器替你做的三件事:读图纸、造对象、塞依赖
先看不用容器时,一个典型的三层调用要写多少废话:
// 手工装配:每一层都得自己 new,还得自己保证"只 new 一次"
UserDao dao = new UserDaoImpl(new DataSource(...));
UserService service = new UserServiceImpl(dao, new MailSender(...));
OrderService order = new OrderServiceImpl(service, dao);
// 明天 DataSource 要换连接池?—— 上面每一行都得改
容器做的事可以一句话概括:把「对象图」从代码里搬到注解/配置里,由容器按图施工。
关键在「按图」两个字——容器不是随手 new,它先把所有声明解析成
BeanDefinition(图纸),再照着图纸造。
| BeanDefinition 上的字段 | 来自哪里 | 实测能观察到的差异 |
|---|---|---|
beanClass | @Component 的类 / @Bean 的返回类型 | 决定由构造器造还是由工厂方法造 |
scope | @Scope("prototype") | 单例 90ns/次,原型 1092ns/次(实验 6) |
initMethodName | @Bean(initMethod=…) | 排在 afterPropertiesSet 之后(实验 8) |
dependsOn | @DependsOn | 只影响创建顺序,不注入任何东西 |
lazyInit | @Lazy | 不参与 refresh() 的预实例化,第一次 getBean 才造 |
9.3refresh() 十四步里,真正干活的是这四步
AbstractApplicationContext.refresh() 有十四步,面试让你全背下来是耍流氓。真正和「Bean 从哪来」相关的只有四步:
| # | 方法 | 它决定了什么 |
|---|---|---|
| 1 | obtainFreshBeanFactory() | 建出 DefaultListableBeanFactory,此时它还是空的,一张图纸都没有 |
| 2 | invokeBeanFactoryPostProcessors() | 扫描 @Component、解析 @Configuration,把所有类变成 BeanDefinition 塞进容器 |
| 3 | registerBeanPostProcessors() | 把 BeanPostProcessor 实例化并排好序——它们必须在任何业务 Bean 之前就位 |
| 4 | finishBeanFactoryInitialization() | 调 preInstantiateSingletons(),把所有非懒加载单例一次性造完 |
这四步的耗时分布很反直觉。实验 1 的日志里,A 的构造器直到第 599 毫秒才打印:
=== case1 setter 循环依赖 ===
[ 1.2ms] refresh() 开始
[ 599.1ms] A -> 构造器执行完毕(此时 b 仍为 null)
[ 648.5ms] B -> 构造器执行完毕(此时 a 仍为 null)
[ 658.1ms] B -> setA 注入 A@2096194236
[ 663.1ms] A -> setB 注入 B@655713354
[ 679.2ms] refresh() 结束
前 599 毫秒一个业务 Bean 都没造——全花在加载 Spring 自己的类、解析注解、注册后置处理器上。 而同一份代码里的第四个容器(实验 6)只要 4.1 毫秒:
=== case6 单例 vs 原型 ===
[ 0.0ms] refresh() 开始
[ 4.1ms] refresh() 结束
9.4一条 Bean 的流水线:doCreateBean 五步
getBean() 最终落到 AbstractAutowireCapableBeanFactory.doCreateBean()。五个动作,顺序固定:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. 实例化:只调构造器,此时所有字段都是默认值
Object bean = createBeanInstance(mbd, beanName, args);
// 2. 提前暴露:把"造好但没填完"的对象包装成 ObjectFactory 放进第三级缓存
// —— 循环依赖能不能解,全看这一步在不在、在什么时候
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// 3. 填充属性:解析 @Autowired,把依赖塞进去(这里可能递归触发别的 Bean 创建)
populateBean(beanName, mbd, bean);
// 4. 初始化(顺序固定):
// Aware -> BPP.before -> @PostConstruct -> afterPropertiesSet -> initMethod -> BPP.after
Object exposedObject = initializeBean(beanName, bean, mbd);
// 5. 注册销毁回调,等容器 close() 时按逆序执行
registerDisposableBeanIfNecessary(beanName, bean, mbd);
return exposedObject;
}
第 1 步和第 3 步的分离,是 Spring 能解决循环依赖的唯一前提: 对象可以「先出生、后吃饭」。构造器注入没有这个缝隙,所以无解(9.8 节实测)。
第 3 步会递归。实验 7 用构造器写了一条 A → B → C 的链,看创建顺序:
=== case7 依赖链 A->B->C 的创建顺序 ===
[ 0.0ms] refresh() 开始
[ 2.3ms] ChainC 构造(最底层先创建)
[ 3.0ms] ChainB 构造
[ 3.5ms] ChainA 构造
[ 4.0ms] refresh() 结束
A 需要 B,B 需要 C,但创建顺序是 C → B → A。
因为要造 A 就得先有 B,要造 B 就得先有 C。依赖方向与创建方向永远相反——
这也是「循环依赖 = 有向图里的环」这个类比的来源:有环就永远找不到那个「可以最先造」的点。
9.5十个回调,一个都不能背错
这是八股里最容易被抓细节的一题。下面这份输出是一次完整生命周期的原文,从构造到销毁:
=== case8 生命周期回调顺序 ===
[ 0.0ms] Dep 构造
[ 12.0ms] 1 构造器
[ 70.9ms] 2 属性注入
[ 71.9ms] 3 Aware 回调 setBeanName(life)
[ 72.2ms] 3 Aware 回调 setBeanFactory
[ 72.4ms] 4 BPP.postProcessBeforeInitialization
[ 72.6ms] 5 @PostConstruct
[ 72.8ms] 6 afterPropertiesSet
[ 76.5ms] 7 initMethod
[ 76.8ms] 7' BPP.postProcessAfterInitialization(在初始化方法之后)
[ 92.8ms] --- 容器就绪,开始 close() ---
[ 94.4ms] 8 @PreDestroy
[ 94.6ms] 9 DisposableBean.destroy
[ 94.8ms] 10 destroyMethod
[ 97.3ms] --- close() 结束 ---
| 序 | 回调 | 来自哪个接口 / 注解 | 典型用途 |
|---|---|---|---|
| 1 | 构造器 | 类本身 | 只做赋值,别做 IO |
| 2 | 属性注入 | @Autowired | 此时依赖才可用 |
| 3 | setBeanName / setBeanFactory | BeanNameAware 等 | 拿容器信息,一般不用 |
| 4 | postProcessBeforeInitialization | BeanPostProcessor | @PostConstruct 就是被它调起来的 |
| 5 | @PostConstruct | jakarta.annotation | 初始化逻辑的首选位置 |
| 6 | afterPropertiesSet | InitializingBean | 老写法,会耦合 Spring 接口 |
| 7 | initMethod | @Bean(initMethod=…) | 接第三方类时唯一的选择 |
| 7' | postProcessAfterInitialization | BeanPostProcessor | AOP 代理在这一步生成 |
| 8 | @PreDestroy | jakarta.annotation | 关连接池、停线程 |
| 9 | destroy | DisposableBean | 同上,老写法 |
| 10 | destroyMethod | @Bean(destroyMethod=…) | 同 7 |
①
postProcessAfterInitialization 在 initMethod 之后,名字里的 "AfterInitialization" 指的是「初始化方法全部跑完之后」,
所以在 BPP 里拿到的 bean 已经是完整可用的——AOP 代理正是在这里把原对象换掉。②
@PostConstruct 在 afterPropertiesSet 之前,因为它本身是
CommonAnnotationBeanPostProcessor(一个 BPP)在 postProcessBeforeInitialization 里反射调用的。③ 销毁顺序是
@PreDestroy → DisposableBean.destroy → destroyMethod,与初始化顺序方向一致(都是注解 → 接口 → 自定义方法)。
9.6单例与原型:90ns 和 1092ns
「单例」这个词被用滥了:Spring 的「单例」不是设计模式里的单例,它只是「容器内每名字一份」。 两者的代价差别可以直接测出来:
=== case6 单例 vs 原型 ===
单例 getBean x200000 : 18.1 ms(90 ns/次)
原型 getBean x200000 : 218.5 ms(1092 ns/次)
单例对象是否同一个: true
原型对象是否同一个: false
原型实例是否被容器持有: 容器内 proto 单例数=0
| 对比项 | singleton(默认) | prototype |
|---|---|---|
| 创建时机 | refresh() 时全部造完 | 每次 getBean() 现造 |
| 容器是否持有 | 是(singletonObjects 里) | 否,实测容器内该名字的单例数 = 0 |
| 取一次的开销 | 90 ns(一次 Map 查找) | 1092 ns(走完整 doCreateBean),12 倍 |
| 销毁回调 | 会执行 | 一次都不执行(见下方实测) |
| 循环依赖 | setter/字段注入可解 | 无解(没有单例缓存可放) |
最后一行容易被忽略,但它是高频考点。把 @PreDestroy、DisposableBean、
destroyMethod 三种销毁回调全挂到一个原型 Bean 上,然后 getBean 两次再 close():
=== case11 原型 bean 的销毁回调 ===
[ 1.4ms] refresh() 开始
[ 246.3ms] refresh() 结束
[ 246.5ms] getBean(pl) 两次
[ 258.8ms] ProtoLife 构造
[ 261.0ms] ProtoLife 构造
[ 261.3ms] close() 开始
[ 266.6ms] close() 结束 —— 上面有几行销毁回调?
registerDisposableBeanIfNecessary() 里对原型直接返回——
容器根本没留引用,也就无从回调。原型 Bean 里开的连接、起的线程,
只能自己在业务代码里手动关,容器不会兜底。这是「用 prototype 解决成员变量共享问题」时最常见的生产事故来源。
9.7三级缓存:把三个 Map 打印出来看
先给结论再上证据。三个缓存都在 DefaultSingletonBeanRegistry 里:
| 级别 | 字段 | 存什么 | 什么时候被放进去 |
|---|---|---|---|
| 一级 | singletonObjects | 成品:依赖已注入、初始化已完成的 Bean | doCreateBean 最后一步 |
| 二级 | earlySingletonObjects | 半成品:造好了、依赖还没填完的对象本身 | 第一次有人来要早期引用时 |
| 三级 | singletonFactories | ObjectFactory(能造出早期引用的工厂) | createBeanInstance 之后立刻 |
光看表格没有体感。实验 1 在 A、B 互相注入的过程中,每走一步就用反射把三个 Map 打一次:
=== case1 setter 循环依赖 ===
[ 1.2ms] refresh() 开始
[ 599.1ms] A -> 构造器执行完毕(此时 b 仍为 null)
[ 648.5ms] B -> 构造器执行完毕(此时 a 仍为 null)
[ 658.1ms] B -> setA 注入 A@2096194236
[cache B.setA 之后] singletonObjects=ConcurrentHashMap/14(业务:[])
earlySingletonObjects=ConcurrentHashMap/1(业务:[a])
singletonFactories=ConcurrentHashMap/1(业务:[b])
[ 663.1ms] A -> setB 注入 B@655713354
[cache A.setB 之后] singletonObjects=ConcurrentHashMap/15(业务:[b])
earlySingletonObjects=ConcurrentHashMap/1(业务:[a])
singletonFactories=ConcurrentHashMap/0(业务:[])
[ 679.2ms] refresh() 结束
A@ctx=IocDemo$A@7cf162bc B@ctx=IocDemo$B@2715644a
b.a == a ? true
a.b == b ? true
[cache refresh 完成后] singletonObjects=ConcurrentHashMap/17(业务:[a, b])
earlySingletonObjects=ConcurrentHashMap/0(业务:[])
singletonFactories=ConcurrentHashMap/0(业务:[])
(singletonObjects 的总数里还有十几个是 environment、messageSource、
internalAutowiredAnnotationProcessor 这类容器自建 Bean,为了看清楚只保留了业务 key。)
把上面这段翻译成时序,就是这道题的标准答案骨架:
| 步 | 发生了什么 | 此刻三个缓存里的业务 key |
|---|---|---|
| ① | 造 A:调构造器,A 实例已存在但 b=null | 三级 {a} |
| ② | 填 A 的属性,发现需要 B → 转去造 B | 三级 {a} |
| ③ | 造 B:调构造器,B 实例已存在但 a=null | 三级 {a, b} |
| ④ | 填 B 的属性,发现需要 A → A 正在创建中,去缓存找早期引用 | 二级 {a},三级 {b} |
| ⑤ | B 装配完成,进入一级缓存;回到 A,把 B 注入进去 | 一级 {b},二级 {a} |
| ⑥ | A 装配完成,也进一级;把二级、三级里关于 a 的残留清掉 | 一级 {a, b} |
第 ④ 步是整道题的题眼:「从三级缓存取 ObjectFactory,调 getObject() 拿到早期引用,
然后把结果放进二级缓存,并删掉三级缓存里的工厂」——这正是实测输出里
earlySingletonObjects=[a] 且 singletonFactories=[b] 同时成立的原因。
为什么两级不行
如果只有「二级」——singletonFactories 里直接存对象,会怎样?
关键在于什么时候决定要不要生成代理。
| 方案 | 做法 | 代价 |
|---|---|---|
| 只有一级 | 成品池,无早期引用 | 循环依赖直接死(构造器那套) |
| 两级(成品 + 早期对象) | 造完就把对象塞进早期池 | 此时还不知道该不该包装代理;等 BPP 决定要包,B 手里已经是裸对象了 |
| 三级(成品 + 早期对象 + 工厂) | 先放工厂;真有人来要,才调用它现场决定返回裸对象还是代理 | 多一次 Map 查找,换来「代理只生成一次、且拿到的都是代理」 |
ObjectFactory 的 getObject() 里调的是 getEarlyBeanReference(),
其中会问所有 SmartInstantiationAwareBeanPostProcessor:「你要不要包装这个早期引用?」
AbstractAutoProxyCreator(@Transactional/@Async 的底层)就靠这个钩子,
把代理在早期就生成好并缓存,保证 B 拿到的是代理而不是裸对象。
9.8循环依赖:两种活法,四种死法
「Spring 解决了循环依赖」这句话只对一个子集成立: 单例 + setter/字段注入。其余场景全部启动即失败。逐条实测如下。
| 场景 | 能否解决 | 实测结果 |
|---|---|---|
单例 + @Autowired 字段 | 能 | 正常启动,b.a == a 为 true |
| 单例 + setter 注入 | 能 | 同上(实验 1) |
| 单例 + 构造器注入 | 不能 | BeanCurrentlyInCreationException(实验 2) |
单例 + 关掉 allowCircularReferences | 不能 | 同上,且从「字段注入」就开始炸(实验 3) |
| 原型 + setter 注入 | 不能 | BeanCurrentlyInCreationException(实验 10) |
任一场景 + 参数加 @Lazy | 能(绕过) | 注入的是 CGLIB 代理,真正用的时候才去容器取(实验 5) |
死法一:构造器注入(最常见)
异常类型: org.springframework.beans.factory.UnsatisfiedDependencyException
异常消息: Error creating bean with name 'c1': Unsatisfied dependency expressed through
constructor parameter 0: Error creating bean with name 'c2': Unsatisfied
dependency expressed through constructor parameter 0: Error creating bean with
name 'c1': Requested bean is currently in creation: Is there an unresolvable
circular reference or an asynchronous initialization dependency?
--- 根因 ---
org.springframework.beans.factory.BeanCurrentlyInCreationException: ...
为什么构造器不行?回到 9.4 的 doCreateBean:addSingletonFactory() 排在
createBeanInstance() 之后。而构造器注入要求「参数先就位才能造对象」,
于是 A 的构造器还没返回,第三级缓存里就没有 A 的工厂,B 来要 A 只能得到「正在创建中」。
暴露早期引用的前提是对象已经存在,构造器阶段对象还不存在。
死法二:关掉 allowCircularReferences
ctx.setAllowCircularReferences(false);
// 同一对 A/B,只是不再允许循环
异常消息: Error creating bean with name 'a': Unsatisfied dependency expressed through
method 'setB' parameter 0: ... Requested bean is currently in creation ...
--- 根因 ---
org.springframework.beans.factory.BeanCurrentlyInCreationException
false!
也就是说在 Boot 2.6+ 里,字段注入的循环依赖也会启动失败,
报的正是上面这条消息。很多人以为自己项目「一直能跑」,
其实是因为老版本(≤2.5)默认 true。这条在面试里能直接区分「读过源码」和「背过博客」。
死法三:原型
=== case10 prototype 循环依赖 ===
[ 164.1ms] P1 构造
[ 213.9ms] P2 构造
异常消息: Error creating bean with name 'p1': Unsatisfied dependency expressed through
method 'setP2' parameter 0: ... Requested bean is currently in creation ...
--- 根因 ---
org.springframework.beans.factory.BeanCurrentlyInCreationException
原型根本没有「单例池」可放。三级缓存是 DefaultSingletonBeanRegistry 的字段,
名字里就写着 Singleton——原型 Bean 不会进这三个 Map,自然谈不上提前暴露。
活法:@Lazy 把环断开
=== case5 @Lazy 断环 ===
[ 3.1ms] LA -> 构造
[ 77.1ms] LA -> setLb 注入 IocDemo$LB$$SpringCGLIB$$0
[ 77.4ms] LA -> 注入对象是 CGLIB 代理? true
[ 77.8ms] LB -> 构造(此刻还没有任何人需要真正的 LB)
[ 79.2ms] refresh() 结束
@Lazy 的思路不是「解决环」,而是把环拆成一条延迟的边:
注入的不是 LB 本身,而是一个 CGLIB 代理(实测类名带 $$SpringCGLIB$$),
代理内部持有 BeanFactory,第一次真正调用方法时才去 getBean。
环在启动期消失了,代价是:把启动期错误推迟成运行期错误。
9.9代理 + 循环依赖:两个版本的两种表现
这一节是给「我已经知道三级缓存了」的人准备的。真实项目里循环依赖 + AOP 代理(
@Transactional、@Async、@Cacheable)会撞上一个更凶的错误。
用自定义 BeanPostProcessor 模拟 AOP 包装(返回 Proxy),同一份代码在两个版本上跑:
// 7.0.9 实测
=== case4 循环依赖 + BeanPostProcessor 包装 ===
[ 3.9ms] PA -> 构造
[ 4.7ms] PB -> 构造
[ 5.6ms] PB -> setIa 注入 IocDemo$PA <-- 拿到的是【裸对象】
[ 6.4ms] BPP -> 把 pa 包装成代理(此刻 PB 手里已经是原始对象)
信息: Bean 'pa' marked for pre-instantiation (not lazy-init) but currently
initialized by other thread - skipping it in mainline thread
[ 8.3ms] PB -> 构造 <-- pb 被【重新创建了一次】
[ 8.6ms] PA -> 构造
[ 8.7ms] BPP -> 把 pa 包装成代理
[ 8.9ms] PB -> setIa 注入 $Proxy16 <-- 这次拿到了代理
[ 9.9ms] refresh() 结束
// 6.1.13 实测(同代码)
异常类型: org.springframework.beans.factory.BeanCurrentlyInCreationException
异常消息: Error creating bean with name 'pa': Bean with name 'pa' has been injected
into other beans [pb] in its raw version as part of a circular reference,
but has eventually been wrapped. This means that said other beans do not
use the final version of the bean. ...
| 版本 | 现象 | 根因(已核对字节码) |
|---|---|---|
| 6.1.13 | 直接抛 BeanCurrentlyInCreationException,提示「以 raw version 注入,最终却被包装」 |
doCreateBean 末尾比对「早期引用」与「最终对象」,不一致就报错。这是保护性失败 |
| 7.0.9 | 不报错。pb 被构造两次,最终拿到代理 |
7.0 重写了预实例化:DefaultListableBeanFactory 新增
bootstrapExecutor 字段与 preInstantiateSingleton()(返回
CompletableFuture),主线程遇到「正被别的线程初始化」的 Bean 会跳过并重试 |
生产上看到
has been injected into other beans [...] in its raw version,
正确动作是拆环,不是调 allowRawInjectionDespiteWrapping。
9.10三类 getBean 异常、速记卡与自查
接口定义了、实现也写了,启动却报 NoSuchBeanDefinitionException——
这类问题不值得花时间,但要知道报错长什么样,才能一眼定位:
=== case9 三类 getBean 异常 ===
[未 refresh] IllegalStateException: ...AnnotationConfigApplicationContext@6659c656
has not been refreshed yet
[找不到] org.springframework.beans.factory.NoSuchBeanDefinitionException:
No bean named 'notExist' available
[多个候选] NoUniqueBeanDefinitionException: No qualifying bean of type 'IocDemo$Pay'
available: expected single matching bean but found 2: wx,ali
[指定名字] WxPay
| 异常 | 触发条件 | 怎么修 |
|---|---|---|
IllegalStateException | 容器还没 refresh() 就 getBean | 先 refresh();ApplicationContext 构造器里已自动调用 |
NoSuchBeanDefinitionException | 名字写错 / 没在扫描范围内 / 是接口没实现类 | 查 @ComponentScan 的包路径、类上有没有注解 |
NoUniqueBeanDefinitionException | 同类型有两个以上候选(实测 wx, ali) | 加 @Primary、@Qualifier("wx"),或按名字取 |
五张速记卡
| 卡 | 内容 |
|---|---|
| 卡 1 · 图纸 | 注解/XML 都先变成 BeanDefinition;容器照着它造,不照着类造。 |
| 卡 2 · 五步 | 实例化 → 提前暴露 → 填充属性 → 初始化 → 注册销毁。第 1、2 步之间的缝隙,就是循环依赖的全部空间。 |
| 卡 3 · 十回调 | 构造 → 注入 → Aware → BPP.before → @PostConstruct → afterPropertiesSet → initMethod → BPP.after → @PreDestroy → destroy → destroyMethod。 |
| 卡 4 · 三级 | 一级成品、二级半成品、三级工厂。第三级存在的唯一理由:推迟「要不要包代理」的决定。 |
| 卡 5 · 活一种死四种 | 只有单例 + setter/字段能解;构造器、原型、关开关全失败;@Lazy 是把环推迟到运行期。 |
六个自测(能脱口而出才算过)
- 三级缓存里,
singletonFactories存的是ObjectFactory而不是对象,为了什么? - A、B 循环依赖,实测在哪一步之后
earlySingletonObjects里才出现a?此时三级缓存里还剩谁? @PostConstruct和afterPropertiesSet谁先执行?各自是被谁调起来的?- 原型 Bean 上挂
@PreDestroy,容器close()时会执行吗?为什么? - 为什么构造器注入的循环依赖无法靠三级缓存解决?请从
doCreateBean的语句顺序回答。 - 线上报
has been injected into other beans [...] in its raw version,你会改哪三处?
动手清单与自查表
| # | 动手项 | 可观察的判据(做到了会看到什么) |
|---|---|---|
| 1 | 跑实验 1,用反射打印三个缓存的 key | B.setA 之后:二级 [a] + 三级 [b] 同时成立 |
| 2 | 把 A、B 改成构造器注入,重跑 | 启动失败,根因 BeanCurrentlyInCreationException,消息含 currently in creation |
| 3 | 加一行 ctx.setAllowCircularReferences(false) |
连字段注入也失败——这条就是 Boot 2.6+ 的默认行为 |
| 4 | 把 A、B 的 @Scope 改成 prototype |
同样失败,因为三个缓存里一个原型 key 都没有 |
| 5 | 给 setLb 的参数加 @Lazy |
启动成功,注入对象类名带 $$SpringCGLIB$$ |
| 6 | 把生命周期实验的销毁回调全挂到原型 Bean 上 | close() 后零行销毁日志,构造却执行了两次 |
A 改成构造器注入 B,
B 保持 setter 注入 A。照样失败——
因为环里只要有一条边是「构造器」边,容器就无法在造出 A 之前拿到 A 的早期引用。
一条构造器边,整条环全废,这与「环上其他边是 setter」无关。
| 卡住了怎么办 | 去哪查 |
|---|---|
| 不知道 Bean 有没有被创建 | ctx.getBeanDefinitionNames() 看图纸,ctx.containsBean(name) 看成品 |
| 不知道谁依赖谁 | ctx.getBeanFactory().getDependentBeans(name)(只管 @DependsOn);实质依赖看启动日志的创建顺序 |
| 不知道为何启动失败 | 从异常链最底层的 cause 往上读,第一条 BeanCurrentlyInCreationException 才是真因 |
| 怀疑代理问题 | 打印 bean.getClass().getName(),看有没有 $$SpringCGLIB$$ 或 $Proxy |
| 想看清回调顺序 | 写一个 BeanPostProcessor,在 before/after 各打一行——比读文档快 |