Appearance
循环依赖
循环依赖是 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。这也是推荐构造器注入的原因之一:尽早暴露设计问题。
