什么是循环依赖?看个生活化的例子
想象这样一个场景:
小明要组装一台电脑,他需要先有主板才能安装 CPU;而小红负责装主板,却坚持要先看到 CPU 才能确定主板型号。两人互相等待对方的 "半成品",结果电脑永远装不起来 ------ 这就是生活中的 "循环依赖"。
在 Spring 框架中,这个问题同样存在:当两个或多个 Bean 形成相互依赖的闭环时,就会产生循环依赖。比如:
- BeanA 需要 BeanB 才能完成初始化
- 而 BeanB 同时也需要 BeanA 才能完成初始化
就像两个人面对面站着,都想让对方先迈出一步,最终陷入僵持。

一、代码视角:循环依赖的三种典型场景
1. 构造器注入循环依赖
构造器注入时,若 Bean 依赖关系形成闭环,Spring 容器在实例化阶段就会陷入"死锁"。示例代码如下:
java
// A的构造器需要B
public class A {
private B b;
public A(B b) {
this.b = b;
System.out.println("A初始化完成");
}
}
// B的构造器需要A
public class B {
private A a;
public B(A a) {
this.a = a;
System.out.println("B初始化完成");
}
}
此时,Spring 尝试创建 A 需先实例化 B,创建 B 又得先实例化 A,循环往复,最终抛出BeanCurrentlyInCreationException。
2. 字段注入的循环依赖
java
// 字段注入
public class A {
// A 依赖 B
@Autowired
private B b;
}
public class B {
// B 依赖 A
@Autowired
private A a;
}
不过这种场景下,Spring 借助三级缓存机制,能巧妙化解循环依赖,让 Bean 顺利完成创建(后续会详细讲解三级缓存逻辑)。
3. setter 方法注入的循环依赖
java
// setter 方法注入
public class A {
private B b;
@Autowired // 通过setter注入B
public void setB(B b) {
this.b = b;
}
}
public class B {
private A a;
@Autowired // 通过setter注入A
public void setA(A a) {
this.a = a;
}
}
这种情况和字段注入类似,Spring 也能自动处理,因为 setter 注入也是在对象实例化之后执行的。
二、Spring 如何破解循环依赖?
Spring 容器默认能处理单例 Bean 的字段/ setter 方法注入循环依赖,核心依靠三级缓存 实现,涉及 DefaultSingletonBeanRegistry 中的三个关键Map:
java
// 部分源码
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
//1、"一级缓存":最终存储单例Bean成品的容器,即实例化和初始化都完成的Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
//2、"二级缓存":存储提前暴露的、未完全初始化的 Bean 半成品(已被其他 Bean 引用)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
//3、"三级缓存":存储 Bean 工厂,用于生成 Bean 早期引用(未被引用的半成品 Bean 用工厂包装)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
......
}
处理流程(以beanA 和 beanB 相互依赖为例)
-
创建 beanA 初始阶段 :Spring启动 beanA创建,先完成实例化(通过构造器生成对象),但未执行属性注入等初始化步骤。此时,会把能生成 beanA 早期引用 (即一个尚未填充属性和初始化、但已经被实例化的对象)的 objectFactory 放入三级缓存(singletonFactories),标记 beanA "正在创建中"。
-
beanA 依赖 beanB 触发 beanB 创建:beanA 进入属性注入环节,发现依赖 beanB,于是转而创建 beanB。
-
创建 beanB 及处理对 beanA 的依赖 :beanB 实例化后,注入依赖时发现需要 beanA 。Spring 按"一级缓存→二级缓存→三级缓存" 的顺序查找:一级缓存没有 beanA(此时 beanA没有完全初始化),二级缓存也没有 beanA;接着从三级缓存中取出 beanA 对应的 objectFactory,通过 getObject( ) 方法拿到 beanA 早期引用,并将其放入二级缓存(earlySingletonObjects),同时从三级缓存中移除 beanA 工厂(避免重复处理)。随后用该早期引用完成 beanB 的依赖注入,beanB 继续完成自身初始化,之后放入一级缓存(singletonObjects)。
-
beanA 完成初始化 :回到 beanA 创建流程,此时 beanB 已经在一级缓存中,可正常注入给 beanA 。beanA 完成剩余初始化步骤,最终也放入**一级缓存(singletonObjects)**中。
通过三级缓存的"接力",Spring让相互依赖的 Bean 能在合适的阶段获取依赖,打破循环依赖的"闭环枷锁"。
下面是比较好理解的描述:

三、特殊场景的循环依赖及解决方案
1. 构造器注入循环依赖
如前文所述,Spring 无法用三级缓存解决构造器注入的循环依赖,会抛出异常。
解决方案:
- 调整注入方式:改用字段注入或 setter 注入,让 Spring 能通过三级缓存机制处理。
- 延迟初始化:在构造器参数上添加 @Lazy 注解,延迟依赖 Bean 的初始化。
示例:
java
public class A {
private B b;
// 对 B 延迟初始化,解决构造器注入循环依赖
public A(@Lazy B b) {
this.b = b;
}
}
2. 原型(Prototype)Bean 的循环依赖
原型 Bean 每次获取都会创建新实例,且不纳入三级缓存管理(缓存仅针对单例 Bean ),Spring 无法解决其循环依赖。
解决方案:
1)转为单例 Bean:若业务允许,将原型 Bean 改为单例,利用三级缓存化解依赖。
2)手动获取依赖:通过 ApplicationContext 手动获取依赖。
比如在 @PostConstruct 方法中主动获取:
java
public class A {
@Autowired
private ApplicationContext context;
private B b;
@PostConstruct
public void init() {
// 手动获取 B,避免循环依赖
this.b = context.getBean(B.class);
}
}
3)重构解耦:从设计层面优化,引入中间层(如抽象接口、事件机制 )拆分循环依赖的 Bean,消除闭环依赖。
四、关键补充和强调
三级缓存的根本目的:三级缓存机制不是为了解决所有循环依赖,而是为了解决单例Bean的Setter注入/字段注入方式的循环依赖。它无法解决构造器注入的循环依赖(因为构造器注入需要在实例化时就获得完整的依赖Bean,而那时自身甚至还没有被实例化,更无法提供早期引用)。
为什么是"三级"缓存,而不是两级?
这是一个非常重要的点。理论上,似乎可以直接把早期引用放到二级缓存,省去三级缓存。
关键在于AOP代理。如果Bean需要被AOP代理,它的早期引用和最终Bean是不一样的。生成代理对象的时机正是在getEarlyBeanReference(即三级缓存ObjectFactory的getObject()方法)中。
如果只有二级缓存,那么一个被代理的Bean,它的早期引用(代理对象)就需要在实例化后立马创建,并放入二级缓存。但这违背了Spring的设计原则,Bean的生命周期应该是清晰的、可扩展的。三级缓存通过一个ObjectFactory巧妙地将"生成早期引用"这个动作延迟到了真正发生循环依赖、需要被注入的时候。这是一种**"懒加载"**思想,只有在必要时才进行代理操作,提升了效率。
简单说:三级缓存 (singletonFactories) 负责生产,二级缓存 (earlySingletonObjects) 负责存储。
"将其放入二级缓存,同时从三级缓存中移除"的原因:
正如上面所说,是为了保证单例和避免重复执行getEarlyBeanReference逻辑(比如生成重复的代理对象)。一旦一个早期引用被创建并放入二级缓存,后续所有对该Bean早期引用的请求都直接从这里获取,保证了一致性。