Spring 循环依赖(circular dependency) 指的是多个 Bean 之间的相互依赖,比如:
- A 依赖 B,B 又依赖 A;
- 或者 A → B → C → A 这种嵌套循环依赖。
这是一个常见又棘手的问题,但 Spring 是可以解决部分类型的循环依赖的! 🌱
下面我们来详细说说 Spring 是 怎么解决循环依赖的 ,以及在哪些情况下解决不了。
🔁 一、Spring 解决循环依赖的核心机制:三级缓存
Spring 的 Bean 生命周期中,在创建单例 Bean 时,使用了三级缓存机制来应对循环依赖问题。
✅ Spring 可解决的循环依赖:
构造方法之外的依赖注入 (也称为Setter 注入 / 字段注入)
📦 三级缓存的具体解释
Spring 为了避免无限递归创建 Bean,使用了 3 个缓存(都在 DefaultSingletonBeanRegistry
中):
缓存名 | 类型 | 作用 |
---|---|---|
singletonObjects | 一级缓存 | 存放完全初始化好的单例 Bean(可直接使用) |
earlySingletonObjects | 二级缓存 | 存放还未初始化完成的早期 Bean(已实例化但还没注入依赖) |
singletonFactories | 三级缓存 | 存放 ObjectFactory(可生成早期 Bean 的代理对象) |
🧩 举个简单例子:A → B → A
java
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
Spring 的处理过程如下:
- 创建 A(A 被标记为 "正在创建")
- A 依赖 B,开始创建 B
- B 又依赖 A,发现 A 正在创建 → 从三级缓存中拿 A 的早期引用(代理或实例)
- B 注入了 A 的早期引用,B 创建完成
- 回到 A,注入 B,A 创建完成
- 把 A 的完整对象放入一级缓存
✅ 最终,A 和 B 都创建成功,并互相注入。
🚫 二、Spring 无法解决的循环依赖
1. 构造器注入导致的循环依赖(Constructor Injection)
java
@Component
public class A {
public A(B b) {}
}
@Component
public class B {
public B(A a) {}
}
- 构造函数中就需要对方的完整对象
- 此时 Spring 无法提前暴露早期对象
- 所以抛出:
BeanCurrentlyInCreationException
解决方法:
- 将构造器注入改为字段或 setter 注入
2. 原型(@Scope("prototype")
)Bean 的循环依赖
- Spring 只对 单例 Bean 做了三级缓存处理
- 原型 Bean 每次都新建,Spring 不会缓存它
- 所以 原型作用域的循环依赖,Spring 无法解决
3. 通过 AOP 代理增强的 Bean,未提前暴露代理对象
有些 AOP 场景中,Spring 无法及时将代理对象放入三级缓存,也会导致依赖注入失败。
✅ 三、开发中如何避免/解决循环依赖
✅ 建议一:优先使用构造器注入 + 明确设计依赖关系
- 构造器注入更安全、清晰,但要避免循环
- 如果存在循环依赖,说明你的设计可能需要重构
✅ 建议二:使用 @Lazy
延迟注入
- Spring 会在需要时再去注入依赖,打破初始化顺序
java
@Autowired
@Lazy
private A a;
✅ 建议三:提取公共依赖,重构服务划分
- 将 A 和 B 共同依赖的部分抽成一个新的类 C
🧠 总结:Spring 怎么解决循环依赖?
类型 | 是否能解决 | 原因 |
---|---|---|
Setter/字段注入 + 单例 Bean | ✅ 可以 | 通过三级缓存(提前暴露早期 Bean 引用) |
构造器注入 | ❌ 不行 | 无法在构造阶段暴露代理对象 |
原型 Bean | ❌ 不行 | Spring 不缓存原型 Bean |
AOP复杂代理 | ⚠️ 有条件支持 | 看 Spring 是否能生成早期代理对象 |