一、什么是循环依赖
循环依赖(Circular Dependency) 指多个 Bean 在相互注入时形成闭环,导致依赖无法解析。例如:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
在创建 Bean A
时发现需要 B
,而 B
又需要 A
,这就像鸡生蛋、蛋生鸡的问题。
二、通俗易懂的生活例子:两位厨师借锅
假设我们有两个厨师:
- 厨师 A 做菜需要用厨师 B 的锅
- 厨师 B 做菜又需要用厨师 A 的锅
常规方式:直接做菜
两人都等着对方先把锅拿出来,结果两人都饿死了 —— 死锁,循环依赖
Spring 的方式:先把锅提前摆出来
Spring 采取了一个折中的方式:
“你们先把锅拿出来(对象创建但还没做菜),虽然菜没做好(属性没填),但锅已经可以用了。”
所以:
- 厨师 A 先把锅放在桌上;
- 厨师 B 可以先用这个锅继续做菜;
- 然后大家再慢慢把菜做完。
这就是 Spring 用“三级缓存”提前暴露 bean 的原理。
三、Spring 的解决机制
3.1 可解决的范围
- 可以解决 字段注入 和 Setter 注入 的循环依赖;
- 构造器注入 无法解决。
3.2 三级缓存机制
Spring 使用三级缓存管理单例 Bean 的生命周期:
缓存层级 | 名称 | 作用 |
---|---|---|
一级缓存 | singletonObjects |
已完全初始化的 Bean |
二级缓存 | earlySingletonObjects |
提前曝光但尚未初始化完成的 Bean |
三级缓存 | singletonFactories |
用于生成早期 Bean 的工厂(ObjectFactory ) |
通过这种方式,Spring 可以“在 Bean 初始化完成之前,就让别人能引用到我”,从而打破循环。
四、代码示例:字段注入成功解决循环依赖
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB;
public void print() {
System.out.println("This is ServiceA");
}
}
@Component
public class ServiceB {
@Autowired
private ServiceA serviceA;
public void print() {
System.out.println("This is ServiceB");
}
}
启动成功,无异常
Spring 自动利用三级缓存,在 Bean 未完成初始化前就让彼此可以“提前引用”。
五、失败示例:构造函数注入无法解决
@Component
public class X {
private final Y y;
@Autowired
public X(Y y) {
this.y = y;
}
}
@Component
public class Y {
private final X x;
@Autowired
public Y(X x) {
this.x = x;
}
}
结果:
BeanCurrentlyInCreationException: Error creating bean with name 'x'
构造器注入要求必须先创建完整对象,Spring 无法“提前曝光”引用。
六、解决循环依赖的策略
推荐做法:
- 避免循环设计:服务间职责清晰,减少相互依赖。
- 使用
@Lazy
注解:推迟 Bean 的创建时机。 - 采用事件驱动或观察者模式:避免强依赖耦合。
示例:使用 @Lazy
延迟注入
@Component
public class A {
@Autowired
@Lazy
private B b;
}
七、总结
特性 | 支持循环依赖? | 是否推荐? |
---|---|---|
字段注入 | 支持 | 推荐 |
Setter 注入 | 支持 | 推荐 |
构造器注入 | 不支持 | 慎用 |
Spring 使用三级缓存机制 提前曝光 Bean 引用,打破依赖死循环。但构造函数注入由于生命周期限制,不在其解决范围内。
八、Spring 解决循环依赖的完整执行流程(含源码路径)
当我们使用 @Autowired
注入依赖时,Spring 会执行如下流程(以字段注入为例):
8.1 Bean 创建流程图
getBean("A")
│
┌─────────▼─────────┐
│createBeanInstance │ (创建 Bean 实例 A)
└─────────┬─────────┘
│
┌─────────▼────────────┐
│add singletonFactory │ (将 ObjectFactory 放入三级缓存)
└─────────┬────────────┘
│
┌─────────▼────────────┐
│populateBean() │ (依赖注入,发现需要 B)
└─────────┬────────────┘
│
┌─────────▼────────────┐
│getBean("B") │
└─────────┬────────────┘
│
如果 B 也依赖 A ——> 从三级缓存中取出 A 的 early reference 引用
8.2 涉及关键源码位置
步骤 | 类名 | 方法 |
---|---|---|
创建 Bean | AbstractAutowireCapableBeanFactory |
createBean() |
添加三级缓存 | DefaultSingletonBeanRegistry |
addSingletonFactory() |
从三级缓存提取 | DefaultSingletonBeanRegistry |
getSingleton() |
判断循环依赖状态 | DefaultSingletonBeanRegistry |
isSingletonCurrentlyInCreation() |
九、Spring 中三级缓存的具体作用解析
9.1 singletonObjects(一层缓存)
作用:存放已经完成初始化的单例 Bean。
一旦 Bean 完成构造、属性注入、初始化方法等生命周期后,就会进入这个 Map 中。
Map<String, Object> singletonObjects;
9.2 earlySingletonObjects(二层缓存)
作用:存放提前曝光但尚未初始化完成的 Bean。
当某个 Bean 正在创建中,但已经被别的 Bean 所依赖,Spring 会从三级缓存生成一个对象(不是最终的成品),放入此处。
Map<String, Object> earlySingletonObjects;
9.3 singletonFactories(三层缓存)
作用:存放生成早期 Bean 引用的
ObjectFactory
。
通过它可以生成 earlySingletonObject
并填充到二级缓存,实现“懒加载”。
Map<String, ObjectFactory<?>> singletonFactories;
9.4 内部关键逻辑简化代码
// 如果一级缓存没有,尝试从二级缓存取
singletonObject = this.earlySingletonObjects.get(beanName);
// 如果二级缓存也没有,再从三级缓存中通过 ObjectFactory 提前创建
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
十、调试技巧:如何在 IDE 中观察循环依赖过程
10.1 推荐断点设置点
AbstractAutowireCapableBeanFactory.createBean()
DefaultSingletonBeanRegistry.getSingleton()
populateBean()
中的依赖注入逻辑addSingleton()
添加到一级缓存的地方
10.2 实际调试步骤
- 设置断点在
getSingleton()
和addSingletonFactory()
。 - 创建两个循环依赖 Bean 并启动 Spring Boot 项目。
- 观察 IDE 中对象在三级缓存中如何切换。
- 利用调试窗口查看:
singletonObjects
、earlySingletonObjects
和singletonFactories
内容。
十一、特殊场景说明
11.1 @Scope(“prototype”) 不支持循环依赖
因为原型 Bean 每次都会新建对象,不会放入缓存,Spring 不会缓存其早期引用,所以也就无法提前暴露。
@Scope("prototype")
@Component
public class A {
@Autowired
private B b;
}
@Scope("prototype")
@Component
public class B {
@Autowired
private A a;
}
启动时会报错。
11.2 构造器注入 + @Lazy:也无法解决
即使你写了:
@Autowired
public A(@Lazy B b) { ... }
仍然 无法提前暴露构造器参数,所以不能解决循环依赖,构造器注入必须避免环依赖设计。
十二、小结与建议
场景 | 能否解决 | 原因 |
---|---|---|
字段注入 | 支持 | 可通过三级缓存提前暴露 |
Setter 注入 | 支持 | 同上 |
构造器注入 | 不支持 | 无法提前暴露 |
原型作用域 | 不支持 | 不进入缓存机制 |
使用 @Lazy 字段注入 |
可辅助解决 | 推迟初始化打破闭环 |
最佳建议:设计上避免循环依赖。Spring 能帮你兜底,但不是让你滥用。
十三、补充
Spring 循环依赖解决流程
BeanA 创建阶段
步骤 1-3:创建 BeanA
- 创建 BeanA(1):Spring 调用
getBean("BeanA")
开始实例化。 - 实例化 BeanA(2):通过构造函数或反射
new BeanA()
得到对象,属性尚未赋值。 - 未初始化的 BeanA 加入三级缓存(3):此时把
ObjectFactory<BeanA>
放入singletonFactories
,BeanA 的引用可以被别人提前拿到(虽然属性未注入)。
BeanB 创建阶段
步骤 5-8:创建 BeanB 并注入 BeanA
- BeanA 依赖 BeanB(5):发现 BeanA 中注入了 BeanB,开始调用
getBean("BeanB")
。 - 实例化 BeanB(6):使用反射或构造函数创建 BeanB 对象(属性 A 还未注入)。
- BeanB 加入三级缓存(7):将 BeanB 的工厂也放入
singletonFactories
。 - BeanB 依赖 BeanA(9):Spring 在注入 BeanA 时发现它“正在创建”,进入循环依赖处理流程。
三级缓存介入解决循环依赖
步骤 9-11:从三级缓存提前获取 BeanA
- 二级缓存为空,转查三级缓存:Spring 发现一级缓存(已初始化的 Bean)找不到 A,尝试从二级缓存
earlySingletonObjects
获取。 - 从三级缓存中调用 ObjectFactory(9):将其返回的 BeanA 放入二级缓存中,返回该引用用于注入。
- 返回提前暴露的 BeanA(10)
- BeanA 正式加入二级缓存(11)
此时 BeanB 中已经注入了 BeanA,但 BeanA 还未初始化完成!这是关键!
BeanB 初始化完成
- BeanB 完成属性注入 + 初始化方法(12)
- BeanB 放入一级缓存(13):表示创建完成,可以安全使用。
- 返回 BeanB 给 BeanA(14)
回到 BeanA,完成剩余初始化
- 完成 BeanA 属性注入(15):此时 B 已完成并在一级缓存中,注入顺利。
- BeanA 放入一级缓存(16):生命周期完成。
总结三大缓存参与点
缓存 | 存什么 | 在哪用到 | 示例说明 |
---|---|---|---|
三级缓存 singletonFactories |
创建早期引用的工厂 | 让别人可以“先看到我” | BeanA 提前暴露引用给 BeanB |
二级缓存 earlySingletonObjects |
实例化未初始化的 Bean | 从三级缓存转入供别人使用 | BeanA 被提前用来注入 |
一级缓存 singletonObjects |
完全初始化好的 Bean | 真正用于返回 | BeanA 和 BeanB 最终归宿 |
整体理解小结
- 为什么能解决? 因为 Spring 把正在创建中的 Bean 的“引用”提前暴露出来了。
- 为什么构造器注入不行? 因为构造器阶段就需要完整 Bean,Spring 还来不及提前暴露。
- 为什么要三级? 二级缓存只保存对象,三级缓存保存“生成引用的工厂”,能延迟加载且更灵活。
- 实际作用? 避免 Bean 死循环,保证注入成功。