循环依赖与 Spring 处理机制
日期: 2025-09-22
循环依赖是 Spring 中常见的依赖注入问题,指两个或多个 Bean 之间互相依赖形成闭环(如 A 依赖 B,B 依赖 A,或 A→B→C→A 等)。理解循环依赖的产生原因、Spring 的处理机制及解决方案,对开发中避免和处理这类问题非常重要。
什么是循环依赖?
举个简单例子:
-
Bean A 的创建需要依赖 Bean B(A 中包含 B 的属性或通过构造器注入 B)。
-
Bean B 的创建需要依赖 Bean A(B 中包含 A 的属性或通过构造器注入 A)。
此时,Spring 容器在初始化 A 时发现需要 B,初始化 B 时又发现需要 A,形成 “鸡生蛋、蛋生鸡” 的闭环,若处理不当会导致容器初始化失败。
不同注入方式下的循环依赖问题
Spring 对循环依赖的处理能力,取决于依赖注入的方式:
字段注入 /setter 注入:Spring 可自动解决
字段注入(直接在属性上用 @Autowired
)或 setter 注入(通过 setXxx()
方法注入)的循环依赖,Spring 能通过三级缓存自动处理。
为什么能解决?
这类注入的特点是:Bean 的实例化(调用构造器创建对象)和依赖注入(设置属性)是分离的。
-
先实例化 A(此时 A 是 “半成品”,未设置属性),再去注入 B。
-
发现 B 未实例化,就先实例化 B(也是 “半成品”),再去注入 A。
-
此时 A 已经实例化(存在早期引用),可以注入到 B 中,B 完成初始化后,再注入到 A 中,最终 A 也完成初始化。
构造器注入:Spring 无法直接解决
如果 A 和 B 都通过构造器注入对方(如 A 的构造器参数是 B,B 的构造器参数是 A),Spring 会直接抛出 BeanCurrentlyInCreationException
异常。
为什么无法解决?
构造器注入的特点是:Bean 的实例化阶段就需要依赖对象(必须先拿到依赖才能创建当前对象)。
-
初始化 A 时,需要先通过构造器传入 B,但 B 还未创建。
-
初始化 B 时,需要先通过构造器传入 A,但 A 还未创建。
-
双方都卡在 “需要对方才能出生” 的阶段,形成死锁,Spring 无法处理。
Spring 如何解决字段 /setter 注入的循环依赖?(三级缓存机制)
Spring 通过三级缓存的协作,实现了对字段 /setter 注入循环依赖的处理,核心是 “提前暴露未完全初始化的 Bean 引用”。
三级缓存的具体作用如下:
缓存名称 | 作用 |
---|---|
一级缓存 singletonObjects |
存储完全初始化完成的单例 Bean(最终可用的成品 Bean)。 |
二级缓存 earlySingletonObjects |
存储实例化完成但未初始化的 Bean(半成品,仅实例化未设置属性)。 |
三级缓存 singletonFactories |
存储Bean 的工厂对象(ObjectFactory ),用于生成 Bean 的早期引用(处理 AOP 代理场景)。 |
工作流程(以 A 依赖 B,B 依赖 A 为例):
- 初始化 A:
-
调用 A 的构造器,实例化 A(此时 A 是半成品,未设置属性)。
-
向三级缓存
singletonFactories
中存入 A 的工厂对象(用于后续生成 A 的早期引用)。 -
准备给 A 注入依赖 B(发现 B 未初始化)。
- 转而去初始化 B:
-
调用 B 的构造器,实例化 B(半成品)。
-
向三级缓存
singletonFactories
中存入 B 的工厂对象。 -
准备给 B 注入依赖 A(发现 A 已实例化)。
- 从缓存中获取 A 的早期引用:
-
从三级缓存
singletonFactories
中获取 A 的工厂对象,生成 A 的早期引用(若 A 需要 AOP 代理,此时会生成代理对象)。 -
将 A 的早期引用移到二级缓存
earlySingletonObjects
中(方便后续直接获取)。 -
将 A 的早期引用注入到 B 中,B 完成初始化,存入一级缓存
singletonObjects
。
- 完成 A 的初始化:
-
从一级缓存中获取已初始化的 B,注入到 A 中。
-
A 完成初始化,存入一级缓存
singletonObjects
,并从二、三级缓存中移除。
核心逻辑:通过三级缓存提前暴露 Bean 的早期引用(半成品),让依赖方可以先拿到引用完成自身初始化,最终反向完成被依赖方的初始化,打破循环。
构造器注入循环依赖的解决方案
构造器注入的循环依赖无法被 Spring 自动解决,但可通过 @Lazy
注解手动处理:
// A 类的构造器注入 B 时,添加 @Lazy
@Component
public class A {private B b;// 对 B 进行延迟注入:注入的是 B 的代理对象,而非真实对象public A(@Lazy B b) {this.b = b;}
}@Component
public class B {private A a;public B(A a) {this.a = a;}
}
原理:
@Lazy
会让 Spring 为 B 创建一个代理对象(而非立即实例化 B),注入到 A 的构造器中。此时 A 可以完成实例化,后续当真正需要使用 B 时,代理对象才会触发 B 的实际实例化(此时 A 已存在),从而打破循环。
最佳实践:尽量避免循环依赖
虽然 Spring 能解决部分循环依赖,但循环依赖本质上是代码设计不够合理的表现(模块职责耦合),长期会导致代码维护困难。更推荐从设计上避免:
拆分职责:将 A 和 B 共同依赖的逻辑抽离到第三方组件(如 C),让 A 和 B 都依赖 C,而非互相依赖。
原:A ←→ B优化:A → C,B → C(消除循环)
使用接口隔离:通过接口定义清晰的依赖边界,避免直接依赖具体实现类。
优先用字段 /setter 注入:若必须依赖,字段 /setter 注入更易被 Spring 处理(但本质还是建议解耦)。
总结:循环依赖的核心是 “依赖闭环”,Spring 通过三级缓存解决了字段 /setter 注入的循环依赖,但构造器注入需手动用 @Lazy
处理。最好的方式是在设计阶段通过职责拆分避免循环依赖的产生。