循环依赖:三级缓存与解决思路

引言

Bean A 依赖 Bean B,Bean B 又依赖 Bean A(构造器或 setter 注入都可能造成)。没有特殊机制时这会是“先有鸡还是先有蛋”。Spring 对 singleton + 属性注入的循环引用有一套著名的“三级缓存”解法,但它有边界,先看清机制再谈怎么正确解决。

核心概念

  • 出现前提:A、B 都是 singleton,且循环发生在属性注入/setter 阶段(对象已实例化、还没装配完)。
  • 三级缓存(DefaultSingletonBeanRegistry)
    1. singletonObjects(一级):完整初始化好的成品 Bean;
    2. earlySingletonObjects(二级):提前暴露的“半成品”引用;
    3. 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 或重构,别依赖缓存兜底”。

笔记加载中…