从 “完整对象” 视角看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();
       // 执行业务逻辑
   }
}
相关推荐
baviya2 小时前
一文彻底搞懂 Maven 依赖——从 <dependency> 到依赖冲突,带你看懂 Maven 的“江湖规矩”
java·maven
一瓢一瓢的饮 alanchan2 小时前
Flink原理与实战(java版)#第1章 Flink快速入门(第一节IDE词频统计)
java·大数据·flink·kafka·实时计算·离线计算·流批一体化计算
java_logo2 小时前
Docker 容器化部署 QINGLONG 面板指南
java·运维·docker·容器·eureka·centos·rabbitmq
那我掉的头发算什么2 小时前
【javaEE】多线程--认识线程、多线程
java·jvm·redis·性能优化·java-ee·intellij-idea
Pluchon2 小时前
硅基计划6.0 JavaEE 叁 文件IO
java·学习·java-ee·文件操作·io流
间彧2 小时前
如何在CI/CD流水线中自动化实现镜像扫描和推送到Harbor?
后端
9ilk2 小时前
【基于one-loop-per-thread的高并发服务器】--- 自主实现HttpServer
linux·运维·服务器·c++·笔记·后端
程序员卷卷狗2 小时前
联合索引的最左前缀原则与失效场景
java·开发语言·数据库·mysql
纪莫2 小时前
技术面:SpringCloud(SpringCloud有哪些组件,SpringCloud与Dubbo的区别)
java·spring·java面试⑧股