循环依赖
开篇:A 依赖 B,B 又依赖 A,怎么办?
先有鸡还是先有蛋?这个哲学问题在 Spring 中变成了:先创建 A 还是先创建 B?
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // A 依赖 B
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA; // B 依赖 A
}创建 A 的时候发现需要 B,于是去创建 B,创建 B 的时候又发现需要 A——死循环了。这就是循环依赖。
好消息是:Spring 通过一套精巧的三级缓存机制解决了大多数情况下的循环依赖问题。坏消息是:并不是所有循环依赖都能解决,而且从 SpringBoot 2.6 开始,Spring 默认关闭了对循环依赖的支持。
本篇会从原理到源码,带你搞懂三级缓存的每一步。
一、什么是循环依赖?
循环依赖就是两个或多个 Bean 之间形成了环形的依赖关系:
除了两两互相依赖,还可能出现更长的链:A → B → C → A。本质上只要依赖关系形成了环,就是循环依赖。
如果不加处理,Spring 在创建 Bean 的过程中会陷入无限递归:创建 A → 发现需要 B → 创建 B → 发现需要 A → 创建 A → ...
只有单例 Bean 才会出现需要 Spring 解决的循环依赖问题。Prototype Bean 出现循环依赖时,Spring 直接抛异常。
二、Spring 的三级缓存
Spring 解决循环依赖的核心武器是三个 Map,定义在 DefaultSingletonBeanRegistry 中:
public class DefaultSingletonBeanRegistry {
// 一级缓存:存放完全初始化好的 Bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存放提前暴露的 Bean(半成品,可能是代理对象)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:存放 Bean 的创建工厂(ObjectFactory)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}用一张图来理解它们的角色:
三级缓存各自的职责:
| 缓存级别 | 存储内容 | 什么时候放入 | 什么时候移除 |
|---|---|---|---|
| 一级缓存 | 完全初始化好的 Bean | Bean 创建完成后 | 容器销毁时 |
| 二级缓存 | 半成品 Bean(或其代理对象) | 从三级缓存获取后 | Bean 创建完成后(移入一级) |
| 三级缓存 | Bean 的 ObjectFactory | Bean 实例化后 | 被获取后(移入二级) |
简单记忆:一级存成品、二级存半成品、三级存工厂。
三、解决过程详解
以 ServiceA 和 ServiceB 互相依赖为例,完整的解决流程如下:
用文字总结关键步骤:
- 创建 A:实例化 A → 把 A 的工厂放入三级缓存 → 填充属性时发现依赖 B
- 创建 B:实例化 B → 把 B 的工厂放入三级缓存 → 填充属性时发现依赖 A
- 解决 B 的依赖:从三级缓存的工厂获取 A 的半成品,放入二级缓存 → B 拿到半成品 A → B 创建完成 → B 放入一级缓存
- 解决 A 的依赖:A 从一级缓存拿到完整的 B → A 创建完成 → A 放入一级缓存
核心原理:实例化和初始化是分开的。A 虽然还没初始化完,但已经实例化了——它在内存中有了地址。B 拿到的是 A 的引用(指针),等 A 后续初始化完成后,B 持有的引用自动指向完整的 A。这就像你预约了一个房间,虽然房间还在装修,但你已经拿到了门牌号。
源码中的关键方法
获取 Bean 时的缓存查找逻辑在 getSingleton() 方法中:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先查一级缓存
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 再查二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 双重检查后查三级缓存
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}逻辑很清晰:一级 → 二级 → 三级,逐级查找。命中三级缓存时,调用工厂获取对象并"升级"到二级缓存。
四、为什么需要三级,两级不行吗?
这是一个高频面试题。答案是:两级缓存可以解决普通对象的循环依赖,但无法优雅地处理需要 AOP 代理的情况。
如果只有两级缓存
假设去掉三级缓存,在 Bean 实例化后直接把半成品对象放入二级缓存。对于普通 Bean 没问题,但对于需要 AOP 代理的 Bean 就出问题了。
在 Spring 的设计中,AOP 代理是在 Bean 初始化的最后阶段(postProcessAfterInitialization)创建的。如果只用两级缓存,当 B 需要 A 时,A 还没走到后置处理阶段,代理还没创建。这时候 B 拿到的是 A 的原始对象,而不是代理对象。
要用两级缓存解决这个问题,就得在实例化之后就提前创建代理对象放入缓存。但这违背了 Spring 的设计原则:AOP 代理应该在初始化完成后由 BeanPostProcessor 创建。而且对于大部分不存在循环依赖的 Bean,提前创建代理是多余的开销。
三级缓存的精妙之处
三级缓存存的不是 Bean 实例,而是一个 ObjectFactory(工厂):
// doCreateBean() 中放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));getEarlyBeanReference() 方法会遍历所有 SmartInstantiationAwareBeanPostProcessor,如果 Bean 需要 AOP 代理,就提前创建代理对象返回;如果不需要,就返回原始对象。
这样做的好处:
- 没有循环依赖时:三级缓存的工厂永远不会被调用,AOP 代理正常在后置处理阶段创建
- 有循环依赖时:工厂按需创建代理对象,保证被依赖方拿到的是正确的对象(代理或原始)
一句话总结:三级缓存的存在是为了延迟代理对象的创建,只在发生循环依赖时才提前创建,保证了 Spring AOP 设计的一致性。
五、哪些循环依赖 Spring 解决不了?
Spring 的三级缓存不是万能的:
1. 构造器注入的循环依赖
@Component
public class A {
public A(B b) { this.b = b; } // 构造器注入
}
@Component
public class B {
public B(A a) { this.a = a; } // 构造器注入
}为什么不行?因为三级缓存的核心是先实例化、后注入——先 new 一个空壳对象放入缓存,再填充属性。但构造器注入在 new 的时候就要求依赖已经存在,实例化和注入没法分离,三级缓存无法介入。
2. Prototype 作用域的循环依赖
Prototype 每次 getBean() 都创建新实例,不会缓存。没有缓存就没法打破循环。Spring 遇到 Prototype 的循环依赖会直接抛出 BeanCurrentlyInCreationException。
3. SpringBoot 2.6+ 默认关闭了循环依赖支持
从 SpringBoot 2.6 开始,spring.main.allow-circular-references 默认为 false。即使是 setter 注入的单例 Bean 循环依赖,默认也会报错。Spring 团队的态度是:循环依赖本身就是设计问题,应该从架构层面消除。
循环依赖的检测
当 Spring 检测到无法解决的循环依赖时,会抛出类似以下的错误:
Error creating bean with name 'serviceA':
Requested bean is currently in creation:
Is there an unresolvable circular reference?Spring 通过一个 singletonsCurrentlyInCreation 的 Set 来追踪正在创建中的 Bean。Bean 开始创建时加入,完成后移除。如果发现依赖的 Bean 已经在这个 Set 中,就说明出现了循环依赖。
解决方案
| 场景 | 解决方案 |
|---|---|
| setter 注入循环依赖(SpringBoot 2.6+) | 配置 spring.main.allow-circular-references=true,或用 @Lazy |
| 构造器注入循环依赖 | 改成 setter 注入,或在构造器参数上加 @Lazy |
| Prototype 循环依赖 | 重新设计,消除循环依赖 |
| 根本方案 | 重构代码,引入中间层或事件机制打破循环 |
@Lazy 怎么解决循环依赖?
@Lazy 的原理是:当 Spring 遇到 @Lazy 标注的依赖时,不会立即创建真实对象,而是注入一个延迟代理对象。这个代理对象在第一次被实际使用时才会去容器中获取真正的 Bean。
@Component
public class A {
public A(@Lazy B b) {
// b 此时是一个延迟代理,不是真正的 B
// 直到第一次调用 b.someMethod() 时才会真正创建 B
this.b = b;
}
}由于注入的是代理而不是真实对象,创建 A 的时候不需要 B 已经存在,从而打破了循环。
实际项目中如何避免循环依赖
循环依赖是代码"坏味道"的信号,说明两个类的职责边界不清晰。推荐的重构方式:
- 引入中间层:A 和 B 互相依赖,抽取共同逻辑到 C,变成 A → C ← B
- 使用事件机制:A 不直接调用 B,而是发布事件,B 监听事件。
ApplicationEventPublisher是内置的解耦利器 - 合并:如果 A 和 B 耦合度极高,可能它们本就应该是一个类
- 接口隔离:通过接口拆分,让 A 只依赖 B 的部分能力
六、常见面试题精选
Q1:什么是 Spring 的三级缓存?
三级缓存是 DefaultSingletonBeanRegistry 中的三个 Map:
- 一级缓存
singletonObjects:存放完全初始化好的单例 Bean - 二级缓存
earlySingletonObjects:存放提前暴露的半成品 Bean(可能是代理对象) - 三级缓存
singletonFactories:存放 Bean 的创建工厂(ObjectFactory)
Q2:三级缓存是如何解决循环依赖的?
核心思路是提前暴露半成品对象。创建 A 时,实例化后把 A 的工厂放入三级缓存。当 B 需要 A 时,从三级缓存的工厂获取 A 的半成品(放入二级缓存),用它完成 B 的创建。B 完成后回到 A 的流程,从一级缓存拿到完整的 B,A 也完成创建。
Q3:为什么需要三级而不是两级?
两级缓存可以解决普通对象的循环依赖,但无法优雅处理 AOP 代理。三级缓存存的是 ObjectFactory,它可以在需要时按需创建代理对象——只有发生循环依赖时才提前创建代理,没有循环依赖时 AOP 代理正常在后置处理阶段创建,保持了设计一致性。
Q4:Spring 默认支持循环依赖吗?
看版本。SpringBoot 2.6 之前默认支持。SpringBoot 2.6 之后默认不支持(allow-circular-references=false),需要手动开启或用 @Lazy。Spring 团队认为循环依赖是设计问题,不应该被默认容忍。
小结
| 概念 | 一句话总结 |
|---|---|
| 循环依赖 | A 依赖 B、B 又依赖 A,形成依赖环 |
| 三级缓存 | 一级存成品、二级存半成品、三级存工厂 |
| 解决原理 | 实例化和初始化分离,先暴露半成品对象打破循环 |
| 三级 vs 二级 | 三级缓存的工厂可以按需创建 AOP 代理,保持设计一致性 |
| 解决不了 | 构造器注入、Prototype 作用域 |
| 最佳实践 | 循环依赖是设计问题,最终方案是重构消除它 |
循环依赖这个话题说到底就三句话:Spring 用三级缓存解决了大部分情况的循环依赖,核心原理是"先实例化后初始化",但构造器注入和 Prototype 搞不定。 面试能说清楚这三点,再加上"为什么需要三级不是两级"的 AOP 代理解释,就足够了。