循环依赖:三级缓存与解决思路
引言
Bean A 依赖 Bean B,Bean B 又依赖 Bean A(构造器或 setter 注入都可能造成)。没有特殊机制时这会是“先有鸡还是先有蛋”。Spring 对 singleton + 属性注入的循环引用有一套著名的“三级缓存”解法,但它有边界,先看清机制再谈怎么正确解决。
核心概念
- 出现前提:A、B 都是 singleton,且循环发生在属性注入/setter 阶段(对象已实例化、还没装配完)。
- 三级缓存(DefaultSingletonBeanRegistry):
singletonObjects(一级):完整初始化好的成品 Bean;earlySingletonObjects(二级):提前暴露的“半成品”引用;singletonFactories(三级):ObjectFactory 工厂,延迟产出早期引用,让 AOP 能在此时介入生成代理。
- 流程:创建 A → 实例化后把“能产出 A 早期引用的工厂”放入三级缓存 → 填充属性时发现要 B → 创建 B → B 填充属性要 A → 从三级缓存取工厂拿到 A 的早期引用(必要时是代理)→ B 完成并进入一级缓存 → A 拿到 B 继续装配完成。最终注入方拿到的仍是完整实例。
- 默认行为变化(版本锚点):Spring Framework 6.0 与 Spring Boot 2.6 起默认不再允许自动解决循环引用(
allowCircularReferences默认 false,以官方文档为准),出现即抛BeanCurrentlyInCreationException并提示可用@Lazy。
代码示例
@Component
public class ServiceA {
private final ServiceB b;
public ServiceA(ServiceB b) { this.b = b; } // 构造器循环 → 无法靠三级缓存解决
}
@Component
public class ServiceB {
private final ServiceA a;
public ServiceB(ServiceA a) { this.a = a; } // 启动即抛异常
}
// 推荐解法一:@Lazy 打断,让一方拿到延迟代理
@Component
public class ServiceB2 {
private final ServiceA a;
public ServiceB2(@Lazy ServiceA a) { this.a = a; }
}
// 推荐解法二:重构依赖方向,用事件/中间层解耦,从根上消除环
注意点
- 三级缓存只救属性注入/setter 的 singleton 环;构造器注入的环、prototype 的环都会直接抛
BeanCurrentlyInCreationException,不要指望缓存。 - 新版默认关掉自动解决后,正确姿势是消除环或
@Lazy,而不是打开setAllowCircularReferences(true)硬扛。 - 循环依赖常是设计味道(上帝对象、职责耦合),先重构,兜底手段才考虑
@Lazy。 - 三级缓存与 AOP 配合时,注入方可能拿到“早期代理”,理解
getEarlyBeanReference介入时机即可,不必深挖每个内部类。 - 面试高频问“为什么二级不够”:三级缓存存在的价值是延迟到真正被引用时才决定是否生成代理(以官方源码注释为准)。
小结
三级缓存 = 一级成品 + 二级早期引用 + 三级 ObjectFactory,解决的是“singleton + 属性注入”的循环引用,代价是引入早期引用复杂度。新版 Spring 默认禁止循环引用,意味着官方态度是“用 @Lazy 或重构,别依赖缓存兜底”。