★ 循环依赖:三级缓存如何解决、哪些场景解不了
结论先行:Spring 对"单例 + 字段/Setter 注入"的循环依赖(A 依赖 B、B 依赖 A)默认可以解决,机制是三级缓存 + 提前暴露半成品引用:A 实例化后先把 ObjectFactory 放入三级缓存,注入 B 时发现 B 又需要 A,就从三级缓存取出工厂生成 A 的早期引用给 B,等两边都装配完再升级为成品。构造器注入循环、prototype 作用域解不了;且 Spring Boot 2.6 起默认禁止循环引用(allow-circular-references=false,需显式开启)。
一、三级缓存是什么
| 级别 | 名称 | 结构 | 存放内容 |
|---|---|---|---|
| 一级 | singletonObjects | Map<String, Object> | 创建完成的成品单例 |
| 二级 | earlySingletonObjects | Map<String, Object> | 提前暴露的半成品(已实例化、未完成初始化) |
| 三级 | singletonFactories | Map<String, ObjectFactory>> | ObjectFactory 工厂,延迟决策"要不要包代理" |
二、解决流程(A↔B)
- 创建 A:实例化原始 A,放入三级缓存(singletonFactories);
- A 属性填充,发现需要 B → 开始创建 B;
- B 实例化后放入三级缓存,B 填充属性发现需要 A;
- B 调 getSingleton("A", true):一级没有 → 二级没有 → 三级有,执行 factory.getObject() 得到 A 的早期引用,升级到二级缓存(三级移除);
- B 拿到 A 的早期引用,完成自身创建,B 进入一级缓存;
- 回到 A:注入完整的 B,A 完成初始化,A 升级到一级缓存并清理二、三级。
三、为什么必须是三级而不是二级
- 三级缓存存的是 ObjectFactory(函数式工厂),关键作用是延迟到"被提前引用"这一刻才判断目标是否需要 AOP 代理;
- 若用二级缓存直接存原始半成品,当 A 需要 @Transactional/@Async 代理时,就会出现"B 手里是裸对象、A 自己却被包了代理"的不一致;
- 三级工厂让 Spring 在暴露引用前先走 getEarlyBeanReference 生成正确的代理,保证大家拿到同一个(代理)对象;
- 若 Spring 能确定该 Bean 无需提前代理,二级缓存理论上也够用——所以面试追问"为什么三级"答"为了延迟创建 AOP 代理的时机"。
@Service
public class A { @Autowired private B b; } // 字段注入:可解
@Service
public class B { @Autowired private A a; }
// 构造器注入:解不了 → BeanCurrentlyInCreationException
@Service
public class A2 { public A2(B2 b) { } }
// 打破方法:对其中一方加 @Lazy(延迟到使用时再解析)
常见追问 / 记忆点
- 追问 1:哪些场景解不了?答:构造器注入循环(实例化就必须有依赖)、prototype 作用域(不进单例缓存)、以及 Boot 2.6+ 默认开关关闭的场景。
- 追问 2:如何破环?答:@Lazy 延迟注入、@Autowired 改字段/setter、重构抽出依赖方向、三级缓存只对单例生效。
- 记忆点:一成品、二半成品、三工厂;"工厂延迟定代理,早暴露、后升级"。构造器注入循环 = 报错 BeanCurrentlyInCreation。