从 “完整对象” 视角看Spring 循环依赖

在复杂的 Spring 应用开发中,循环依赖是高频出现且易引发困惑的技术点。多数开发者遇到BeanCurrentlyInCreationException异常时,仅知晓是循环依赖导致,却不理解底层原因与 Spring 的处理机制。本文从循环依赖本质出发,拆解其失败场景、处理原理及解决方案,帮助开发者系统性掌握这一核心知识点。

一、循环依赖的本质与核心矛盾

1.1 什么是循环依赖

循环依赖指两个或多个模块(或 Bean)形成相互依赖的闭环关系,并非 "错误" 结构,实际业务中常用于处理复杂关联(如订单服务与用户服务、课程服务与成绩服务)。其关系可通过以下图示清晰呈现:
依赖 依赖 依赖 依赖 依赖 模块A/BeanA 模块B/BeanB 模块C/BeanC 模块D/BeanD 模块E/BeanE

我们常说的 "处理循环依赖",并非消除业务依赖关系,而是解决Bean 构建过程中因循环依赖导致的实例化失败问题

1.2 循环依赖的核心矛盾:"完整对象" 的构建顺序

构建循环依赖失败的根本原因只有一个:无法按顺序构造 "完整" 的依赖对象。"完整" 的定义需结合框架特性:

  • 简单 Java 对象:"完整" 仅需完成new操作(实例化),对象创建后即可使用;
  • Spring Bean:"完整" 包含两个核心阶段,任一阶段失败均视为 "不完整":
  1. 实例化(Instantiation):通过构造函数创建 Bean 的原始对象(执行new操作);
  2. 初始化(Initialization):完成属性注入(@Autowired)、初始化方法执行(@PostConstructafterPropertiesSet)、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的初始化逻辑
   }
}

失败原因

  1. Spring 先实例化CircularC,将其包装为ObjectFactory存入缓存,随后执行初始化方法afterPropertiesSet()
  2. 初始化方法中调用circularDProvider.getIfUnique(),触发CircularD的构建;
  3. 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,延迟初始化",具体可拆解为三个关键点:

  1. 实例化与初始化分离:Bean 实例化后(new操作完成),立即通过ObjectFactory暴露到三级缓存,此时 Bean 虽未完成初始化,但已具备 "可被引用" 的基础;
  2. 循环依赖时按需获取:当其他 Bean 依赖当前 Bean 时,先从二级缓存获取;若二级缓存无,则通过三级缓存的ObjectFactory生成实例(可能是原始对象或 AOP 代理对象),存入二级缓存后返回;
  3. 最终完成初始化:所有依赖 Bean 构建完成后,当前 Bean 继续执行初始化流程,最终存入一级缓存,供后续直接使用。

3.3 三级缓存处理循环依赖的完整流程

以 "BeanA依赖BeanBBeanB依赖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 加载顺序默认按类名排序(不确定):

  • 若先加载CircularFCircularF的构造函数依赖CircularE,此时CircularE尚未实例化,直接失败;
  • 若先加载CircularECircularE实例化后存入三级缓存,初始化时注入CircularFCircularF构造函数依赖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 每次获取都会创建新实例,不会存入singletonObjectsearlySingletonObjects等缓存。当BeanA(多例)依赖BeanB(多例)、BeanB依赖BeanA时,会陷入 "创建 BeanA→需要 BeanB→创建 BeanB→需要 BeanA→创建新 BeanA→需要新 BeanB" 的无限循环。
解决方案

  1. 尽量避免多例 Bean 的循环依赖:多例 Bean 通常用于无状态场景,可通过业务拆分消除循环依赖;
  2. 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();
       // 执行业务逻辑
   }
}
相关推荐
葫芦和十三12 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp12 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑13 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯13 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan16 小时前
多Agent之间的区别
后端
青石路17 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充18 小时前
1.面向对象设计思想
后端
IT_陈寒18 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro19 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗19 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端