Skip to content

循环依赖

  循环依赖是 Spring 面试中的高频难题。Spring 通过三级缓存解决了单例 Bean 的 Setter 注入循环依赖,但构造器注入的循环依赖无法解决。

一、什么是循环依赖

java
// A 依赖 B,B 依赖 A,形成闭环
@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

二、三级缓存

Spring 的三级缓存(DefaultSingletonBeanRegistry):

  一级缓存(singletonObjects):
    ConcurrentHashMap<String, Object>
    存放完全初始化好的单例 Bean
    对外暴露的 getBean() 从这里获取

  二级缓存(earlySingletonObjects):
    HashMap<String, Object>
    存放提前暴露的 Bean(已实例化但未完成属性填充和初始化)
    用于解决循环依赖

  三级缓存(singletonFactories):
    HashMap<String, ObjectFactory<?>>
    存放 Bean 的工厂对象(可以生成 Bean 的早期引用)
    用于生成代理对象
java
// Spring 源码中的三级缓存定义
public class DefaultSingletonBeanRegistry {

    // 一级缓存
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    // 二级缓存
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

    // 三级缓存
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}

三、三级缓存解决循环依赖流程

假设 A 依赖 B,B 依赖 A:

  ① 创建 A
     ├─ 实例化 A(调用构造器)→ 原始对象 A
     ├─ 把 A 的工厂放入三级缓存 singletonFactories
     ├─ 属性填充:发现需要 B
     └─ 去获取 B

  ② 创建 B
     ├─ 实例化 B(调用构造器)→ 原始对象 B
     ├─ 把 B 的工厂放入三级缓存 singletonFactories
     ├─ 属性填充:发现需要 A
     └─ 去获取 A

  ③ 获取 A
     ├─ 一级缓存没有 A
     ├─ 二级缓存没有 A
     ├─ 三级缓存有 A 的工厂 → 调用 getObject() 获取 A 的早期引用
     ├─ 把 A 的早期引用放入二级缓存
     ├─ 从三级缓存移除 A
     └─ 返回 A 的早期引用

  ④ B 拿到 A 的早期引用 → 注入到 B → B 初始化完成
     └─ B 放入一级缓存

  ⑤ 回到 A 的属性填充 → 拿到 B(一级缓存)→ 注入到 A → A 初始化完成
     └─ A 放入一级缓存

  完成:A 持有的 B 是完全初始化的 B,B 持有的 A 是最终就是 A(同一个引用)

四、为什么需要三级缓存?

只用二级缓存可以吗?

  二级缓存:存原对象
  三级缓存:存 ObjectFactory(可以生成代理对象)

  关键问题:AOP 代理对象可能在属性填充后才创建

  如果 A 需要 AOP 代理:
    ① 实例化 A → 放入三级缓存(工厂)
    ② B 需要 A → 从三级缓存获取 A 的早期引用
    ③ 此时 A 还没有初始化,但工厂可以提前创建 AOP 代理
    ④ 如果只有二级缓存,存的是原始对象,B 拿到的就是原始对象而不是代理对象

  三级缓存的存在就是为了在 Bean 初始化之前,
  提前暴露一个可以生成代理对象的工厂

五、构造器注入循环依赖

java
// ❌ 构造器注入循环依赖,Spring 无法解决
@Component
public class A {
    private final B b;
    public A(B b) {  // 构造器需要 B,但 B 还没创建 → 死循环
        this.b = b;
    }
}

@Component
public class B {
    private final A a;
    public B(A a) {
        this.a = a;
    }
}

// 报错:BeanCurrentlyInCreationException
// 原因:构造器注入要求实例化时就提供依赖,但循环依赖时双方都无法实例化完成
能解决 vs 不能解决:

  ✅ Setter 注入 + singleton  → 可以解决(三级缓存)
  ✅ 字段注入 + singleton     → 可以解决(三级缓存)
  ❌ 构造器注入                → 无法解决(实例化时就需要依赖)
  ❌ prototype 作用域          → 无法解决(不缓存,无法提前暴露)

六、避免循环依赖

java
// ① 使用 @Lazy 延迟注入
@Component
public class A {
    @Autowired
    @Lazy
    private B b;  // 注入的是代理对象,真正用到时才去获取 B
}

// ② 重构设计,引入中间层
@Component
public class A {
    @Autowired
    private C c;  // A 和 B 都依赖 C,C 不依赖 A 和 B
}

// ③ 使用构造器注入(推荐)
// 构造器注入的循环依赖会报错,迫使你尽早发现并修复设计问题

七、面试要点

Q1:Spring 如何解决循环依赖?

  通过三级缓存:实例化后把工厂放入三级缓存 → 属性填充时发现依赖 → 从三级缓存获取依赖的早期引用 → 注入 → 双方都初始化完成后从一级缓存获取完全体。

Q2:为什么需要三级缓存,二级不行吗?

  二级缓存只存原始对象,三级存 ObjectFactory 可以提前生成 AOP 代理对象。如果一个 Bean 需要 AOP 代理,在属性填充之前就能通过工厂拿到代理对象,而不是原始对象。

Q3:构造器注入的循环依赖能解决吗?

  不能。构造器注入要求实例化时就提供依赖,但循环依赖时双方都无法实例化完成,会抛出 BeanCurrentlyInCreationException。这也是推荐构造器注入的原因之一:尽早暴露设计问题。