在复杂的 Spring 应用开发中,循环依赖是高频出现且易引发困惑的技术点。多数开发者遇到BeanCurrentlyInCreationException异常时,仅知晓是循环依赖导致,却不理解底层原因与 Spring 的处理机制。本文从循环依赖本质出发,拆解其失败场景、处理原理及解决方案,帮助开发者系统性掌握这一核心知识点。
一、循环依赖的本质与核心矛盾
1.1 什么是循环依赖
循环依赖指两个或多个模块(或 Bean)形成相互依赖的闭环关系,并非 "错误" 结构,实际业务中常用于处理复杂关联(如订单服务与用户服务、课程服务与成绩服务)。其关系可通过以下图示清晰呈现:
依赖 依赖 依赖 依赖 依赖 模块A/BeanA 模块B/BeanB 模块C/BeanC 模块D/BeanD 模块E/BeanE
我们常说的 "处理循环依赖",并非消除业务依赖关系,而是解决Bean 构建过程中因循环依赖导致的实例化失败问题。
1.2 循环依赖的核心矛盾:"完整对象" 的构建顺序
构建循环依赖失败的根本原因只有一个:无法按顺序构造 "完整" 的依赖对象。"完整" 的定义需结合框架特性:
- 简单 Java 对象:"完整" 仅需完成
new操作(实例化),对象创建后即可使用; - Spring Bean:"完整" 包含两个核心阶段,任一阶段失败均视为 "不完整":
- 实例化(Instantiation):通过构造函数创建 Bean 的原始对象(执行
new操作); - 初始化(Initialization):完成属性注入(
@Autowired)、初始化方法执行(@PostConstruct、afterPropertiesSet)、AOP 代理生成等操作。
二、Spring 中循环依赖的失败场景
Spring Bean 构建分为实例化和初始化两个阶段,循环依赖的失败也集中在这两个环节,以下结合具体代码场景分析。
2.1 实例化阶段失败:构造函数循环依赖
实例化阶段的循环依赖,本质是 Bean 的构造函数依赖形成闭环,导致 Spring 无法按顺序创建原始对象。
失败案例
java
@Component
public class CircularA {
private CircularB circularB;
// 构造函数依赖CircularB
public CircularA(CircularB circularB) {
this.circularB = circularB;
}
@Component
public static class CircularB {
private CircularA circularA;
// 构造函数依赖CircularA
public CircularB(CircularA circularA) {
this.circularA = circularA;
}
}
}
失败原因 :
Spring 创建 Bean 时,需先通过构造函数实例化对象。尝试创建CircularA时,发现需CircularB作为构造参数,转而创建CircularB;但创建CircularB时,又需CircularA作为构造参数,形成 "先有鸡还是先有蛋" 的死循环,最终抛出BeanCurrentlyInCreationException。
解决方案: 使用 ObjectProvider 延迟依赖获取
构造函数依赖的核心问题是 "即时依赖",需通过 "延迟依赖" 打破闭环。Spring 提供的ObjectProvider接口是最佳实践,它本质是 "Bean 查找器",允许在需要时从容器中获取 Bean,而非构造时直接依赖。
优化后的代码:
java
@Component
public class CircularA {
// 依赖ObjectProvider\<CircularB>,而非直接依赖CircularB
private final ObjectProvider\<CircularB> circularBProvider;
// 构造函数注入ObjectProvider(容器可直接实例化,无循环依赖)
public CircularA(ObjectProvider\<CircularB> circularBProvider) {
this.circularBProvider = circularBProvider;
}
// 业务方法中按需获取Bean(延迟初始化)
public void doBusiness() {
// getIfUnique():获取唯一Bean,无则返回null;也可使用getObject()强制获取
CircularB circularB = circularBProvider.getIfUnique();
if (circularB != null) {
// 执行业务逻辑
}
}
@Component
public static class CircularB {
private final CircularA circularA;
// 此处可直接依赖CircularA(因CircularA已通过ObjectProvider打破闭环)
public CircularB(CircularA circularA) {
this.circularA = circularA;
}
}
}
关键注意事项
使用ObjectProvider时,禁止在 Bean 的构建阶段(构造函数、初始化方法)调用 getIfUnique()/ getObject(),否则会因依赖 Bean 未完成实例化,再次触发循环依赖失败。
2.2 初始化阶段失败:初始化时触发依赖 Bean 的构建
初始化阶段是 Spring Bean 完成属性注入、业务初始化的关键环节。若在初始化方法中主动获取循环依赖的 Bean,会触发依赖 Bean 的构建,而此时当前 Bean 尚未完成初始化,最终导致失败。
失败案例
java
@Component
public class CircularC implements InitializingBean {
private final ObjectProvider\<CircularD> circularDProvider;
// 构造函数注入ObjectProvider,实例化阶段无问题
public CircularC(ObjectProvider\<CircularD> circularDProvider) {
this.circularDProvider = circularDProvider;
}
@Component
public static class CircularD {
private final CircularC circularC;
// 依赖CircularC的实例
public CircularD(CircularC circularC) {
this.circularC = circularC;
}
}
// 初始化方法(Spring会在实例化后自动调用)
@Override
public void afterPropertiesSet() throws Exception {
// 错误:在初始化阶段获取CircularD
CircularD circularD = circularDProvider.getIfUnique();
// 执行依赖CircularD的初始化逻辑
}
}
失败原因
- Spring 先实例化
CircularC,将其包装为ObjectFactory存入缓存,随后执行初始化方法afterPropertiesSet(); - 初始化方法中调用
circularDProvider.getIfUnique(),触发CircularD的构建; CircularD的构造函数依赖CircularC,但此时CircularC虽已实例化,仍处于初始化阶段(未完成),Spring 认为其不是 "完整 Bean",最终抛出异常。
解决方案:延迟初始化逻辑到业务调用时
初始化阶段应仅处理当前 Bean 的内部状态,避免依赖其他 Bean。如需使用依赖 Bean,需将相关逻辑延迟到具体业务方法被调用时执行:
优化后的代码:
java
@Component
public class CircularC implements InitializingBean {
private final ObjectProvider\<CircularD> circularDProvider;
private CircularD circularD; // 延迟初始化依赖Bean
public CircularC(ObjectProvider\<CircularD> circularDProvider) {
this.circularDProvider = circularDProvider;
}
@Component
public static class CircularD {
private final CircularC circularC;
public CircularD(CircularC circularC) {
this.circularC = circularC;
}
}
// 初始化方法仅处理内部状态,不依赖其他Bean
@Override
public void afterPropertiesSet() throws Exception {
// 初始化当前Bean的内部属性(无外部依赖)
System.out.println("CircularC初始化完成(无外部依赖)");
}
// 业务方法中初始化依赖Bean并使用
public void doBusiness() {
// 延迟到业务调用时获取CircularD
if (circularD == null) {
this.circularD = circularDProvider.getIfUnique();
}
// 执行依赖CircularD的业务逻辑
}
}
三、Spring 处理循环依赖的核心原理:三级缓存机制
Spring 能处理大部分循环依赖场景,核心在于三级缓存机制 。多数开发者误以为是 "两级缓存",但第三级缓存(singletonFactories)是解决 AOP 代理等特殊场景的关键。
3.1 三级缓存的定义
Spring 容器中维护三个核心缓存(均为单例 Bean 专用),具体作用如下表所示:
| 缓存名称 | 类型 | 作用描述 |
|---|---|---|
| singletonObjects | Map<String, Object> | 一级缓存:存储完全初始化完成的单例 Bean(最终可直接使用的 Bean) |
| earlySingletonObjects | Map<String, Object> | 二级缓存:存储实例化完成但未初始化的单例 Bean(原始对象或代理对象) |
| singletonFactories | Map<String, ObjectFactory<?>> | 三级缓存:存储 Bean 的ObjectFactory(对象工厂),用于延迟生成 Bean 实例 |
3.2 核心设计思想
Spring 处理循环依赖的核心逻辑是 "提前暴露实例化后的 Bean,延迟初始化",具体可拆解为三个关键点:
- 实例化与初始化分离:Bean 实例化后(
new操作完成),立即通过ObjectFactory暴露到三级缓存,此时 Bean 虽未完成初始化,但已具备 "可被引用" 的基础; - 循环依赖时按需获取:当其他 Bean 依赖当前 Bean 时,先从二级缓存获取;若二级缓存无,则通过三级缓存的
ObjectFactory生成实例(可能是原始对象或 AOP 代理对象),存入二级缓存后返回; - 最终完成初始化:所有依赖 Bean 构建完成后,当前 Bean 继续执行初始化流程,最终存入一级缓存,供后续直接使用。
3.3 三级缓存处理循环依赖的完整流程
以 "BeanA依赖BeanB、BeanB依赖BeanA" 为例,三级缓存的处理流程如下:
Spring启动 , 开始创建BeanA 实例化BeanA new BeanA 创建BeanA的ObjectFactory , 存入singletonFactories BeanA开始初始化: 需要注入BeanB Spring开始创建BeanB 实例化BeanB new BeanB 创建BeanB的ObjectFactory , 存入singletonFactories BeanB开始初始化: 需要注入BeanA 查询BeanA: 一级缓存无>二级缓存>无三级缓存获取ObjectFactory 调用ObjectFactory.getObject 生成BeanA实例 原始或代理 将BeanA实例存入二级缓存 , 移除三级缓存中的ObjectFactory BeanB注入BeanA , 完成初始化 , 存入一级缓存 BeanA注入BeanB , 完成初始化 , 存入一级缓存 循环依赖处理完成 , BeanA , BeanB均可用
3.4 三级缓存的关键作用:解决 AOP 代理场景
三级缓存(singletonFactories)的核心价值在于处理 Bean 被 AOP 代理的场景。假设BeanA需要被 AOP 代理(如添加@Transactional注解):
- 若直接将实例化后的原始
BeanA存入二级缓存,BeanB注入的会是原始对象,而非代理对象,后续使用时会缺失 AOP 功能; - 三级缓存的
ObjectFactory可在getObject()方法中触发 AOP 代理逻辑:当BeanB需要注入BeanA时,通过ObjectFactory动态生成代理对象,再存入二级缓存,确保BeanB注入的是代理后的 "正确对象"。
这也是 Spring 设计三级缓存而非两级缓存的根本原因 ------延迟生成代理对象,确保循环依赖场景下注入的是最终可用的代理 Bean。
四、Spring 循环依赖的特殊场景与解决方案
除核心场景外,部分特殊情况也会导致循环依赖失败,需针对性处理。
4.1 构造函数 + 字段注入混合循环依赖
失败案例
java
@Component
public class CircularE {
@Autowired // 字段注入CircularF
private CircularF circularF;
// 无参构造函数(实例化无问题)
public CircularE() {}
@Component
public static class CircularF {
private CircularE circularE;
// 构造函数依赖CircularE
public CircularF(CircularE circularE) {
this.circularE = circularE;
}
}
}
失败原因 :
Spring 的 Bean 加载顺序默认按类名排序(不确定):
- 若先加载
CircularF:CircularF的构造函数依赖CircularE,此时CircularE尚未实例化,直接失败; - 若先加载
CircularE:CircularE实例化后存入三级缓存,初始化时注入CircularF;CircularF构造函数依赖CircularE,从三级缓存获取实例,构建成功。
解决方案:使用 @DependsOn 指定加载顺序
通过@DependsOn注解强制 Spring 先加载不依赖构造函数的 Bean(CircularE),确保其提前实例化并暴露到缓存中。
优化后的代码:
java
@Component
public class CircularE {
@Autowired
private CircularF circularF;
public CircularE() {}
// 指定CircularF依赖CircularE,强制先加载CircularE
@DependsOn("circularE")
@Component
public static class CircularF {
private CircularE circularE;
public CircularF(CircularE circularE) {
this.circularE = circularE;
}
}
}
4.2 多例 Bean(Prototype)的循环依赖
问题说明 :
Spring 默认仅处理单例 Bean(Singleton) 的循环依赖,多例 Bean(@Scope("prototype"))的循环依赖无法通过三级缓存解决。
原因:多例 Bean 每次获取都会创建新实例,不会存入singletonObjects、earlySingletonObjects等缓存。当BeanA(多例)依赖BeanB(多例)、BeanB依赖BeanA时,会陷入 "创建 BeanA→需要 BeanB→创建 BeanB→需要 BeanA→创建新 BeanA→需要新 BeanB" 的无限循环。
解决方案:
- 尽量避免多例 Bean 的循环依赖:多例 Bean 通常用于无状态场景,可通过业务拆分消除循环依赖;
- 用
ObjectProvider延迟获取:在多例 Bean 中注入ObjectProvider<目标Bean>,避免构造时触发依赖 Bean 的创建,在业务方法中按需获取。
示例代码:
java
@Scope("prototype")
@Component
public class PrototypeA {
private final ObjectProvider\<PrototypeB> prototypeBProvider;
public PrototypeA(ObjectProvider\<PrototypeB> prototypeBProvider) {
this.prototypeBProvider = prototypeBProvider;
}
public void doBusiness() {
// 业务方法中获取PrototypeB(每次获取都是新实例)
PrototypeB prototypeB = prototypeBProvider.getObject();
// 执行业务逻辑
}
}
@Scope("prototype")
@Component
public class PrototypeB {
private final ObjectProvider\<PrototypeA> prototypeAProvider;
public PrototypeB(ObjectProvider\<PrototypeA> prototypeAProvider) {
this.prototypeAProvider = prototypeAProvider;
}
public void doBusiness() {
PrototypeA prototypeA = prototypeAProvider.getObject();
// 执行业务逻辑
}
}