当前位置: 首页 > news >正文

循环依赖问题

循环依赖与 Spring 处理机制

日期: 2025-09-22
循环依赖是 Spring 中常见的依赖注入问题,指两个或多个 Bean 之间互相依赖形成闭环(如 A 依赖 B,B 依赖 A,或 A→B→C→A 等)。理解循环依赖的产生原因、Spring 的处理机制及解决方案,对开发中避免和处理这类问题非常重要。

什么是循环依赖?

举个简单例子:

  • Bean A 的创建需要依赖 Bean B(A 中包含 B 的属性或通过构造器注入 B)。

  • Bean B 的创建需要依赖 Bean A(B 中包含 A 的属性或通过构造器注入 A)。

此时,Spring 容器在初始化 A 时发现需要 B,初始化 B 时又发现需要 A,形成 “鸡生蛋、蛋生鸡” 的闭环,若处理不当会导致容器初始化失败。

不同注入方式下的循环依赖问题

Spring 对循环依赖的处理能力,取决于依赖注入的方式:

字段注入 /setter 注入:Spring 可自动解决

字段注入(直接在属性上用 @Autowired)或 setter 注入(通过 setXxx() 方法注入)的循环依赖,Spring 能通过三级缓存自动处理。

为什么能解决?

这类注入的特点是:Bean 的实例化(调用构造器创建对象)和依赖注入(设置属性)是分离的。

  • 先实例化 A(此时 A 是 “半成品”,未设置属性),再去注入 B。

  • 发现 B 未实例化,就先实例化 B(也是 “半成品”),再去注入 A。

  • 此时 A 已经实例化(存在早期引用),可以注入到 B 中,B 完成初始化后,再注入到 A 中,最终 A 也完成初始化。

构造器注入:Spring 无法直接解决

如果 A 和 B 都通过构造器注入对方(如 A 的构造器参数是 B,B 的构造器参数是 A),Spring 会直接抛出 BeanCurrentlyInCreationException 异常。

为什么无法解决?

构造器注入的特点是:Bean 的实例化阶段就需要依赖对象(必须先拿到依赖才能创建当前对象)。

  • 初始化 A 时,需要先通过构造器传入 B,但 B 还未创建。

  • 初始化 B 时,需要先通过构造器传入 A,但 A 还未创建。

  • 双方都卡在 “需要对方才能出生” 的阶段,形成死锁,Spring 无法处理。

Spring 如何解决字段 /setter 注入的循环依赖?(三级缓存机制)

Spring 通过三级缓存的协作,实现了对字段 /setter 注入循环依赖的处理,核心是 “提前暴露未完全初始化的 Bean 引用”。

三级缓存的具体作用如下:

缓存名称 作用
一级缓存 singletonObjects 存储完全初始化完成的单例 Bean(最终可用的成品 Bean)。
二级缓存 earlySingletonObjects 存储实例化完成但未初始化的 Bean(半成品,仅实例化未设置属性)。
三级缓存 singletonFactories 存储Bean 的工厂对象ObjectFactory),用于生成 Bean 的早期引用(处理 AOP 代理场景)。

工作流程(以 A 依赖 B,B 依赖 A 为例):

  1. 初始化 A:
  • 调用 A 的构造器,实例化 A(此时 A 是半成品,未设置属性)。

  • 向三级缓存 singletonFactories 中存入 A 的工厂对象(用于后续生成 A 的早期引用)。

  • 准备给 A 注入依赖 B(发现 B 未初始化)。

  1. 转而去初始化 B:
  • 调用 B 的构造器,实例化 B(半成品)。

  • 向三级缓存 singletonFactories 中存入 B 的工厂对象。

  • 准备给 B 注入依赖 A(发现 A 已实例化)。

  1. 从缓存中获取 A 的早期引用:
  • 从三级缓存 singletonFactories 中获取 A 的工厂对象,生成 A 的早期引用(若 A 需要 AOP 代理,此时会生成代理对象)。

  • 将 A 的早期引用移到二级缓存 earlySingletonObjects 中(方便后续直接获取)。

  • 将 A 的早期引用注入到 B 中,B 完成初始化,存入一级缓存 singletonObjects

  1. 完成 A 的初始化:
  • 从一级缓存中获取已初始化的 B,注入到 A 中。

  • A 完成初始化,存入一级缓存 singletonObjects,并从二、三级缓存中移除。

核心逻辑:通过三级缓存提前暴露 Bean 的早期引用(半成品),让依赖方可以先拿到引用完成自身初始化,最终反向完成被依赖方的初始化,打破循环。

构造器注入循环依赖的解决方案

构造器注入的循环依赖无法被 Spring 自动解决,但可通过 @Lazy 注解手动处理:

// A 类的构造器注入 B 时,添加 @Lazy
@Component
public class A {private B b;// 对 B 进行延迟注入:注入的是 B 的代理对象,而非真实对象public A(@Lazy B b) {this.b = b;}
}@Component
public class B {private A a;public B(A a) {this.a = a;}
}

原理

@Lazy 会让 Spring 为 B 创建一个代理对象(而非立即实例化 B),注入到 A 的构造器中。此时 A 可以完成实例化,后续当真正需要使用 B 时,代理对象才会触发 B 的实际实例化(此时 A 已存在),从而打破循环。

最佳实践:尽量避免循环依赖

虽然 Spring 能解决部分循环依赖,但循环依赖本质上是代码设计不够合理的表现(模块职责耦合),长期会导致代码维护困难。更推荐从设计上避免:

拆分职责:将 A 和 B 共同依赖的逻辑抽离到第三方组件(如 C),让 A 和 B 都依赖 C,而非互相依赖。

原:A ←→ B优化:A → C,B → C(消除循环)

使用接口隔离:通过接口定义清晰的依赖边界,避免直接依赖具体实现类。

优先用字段 /setter 注入:若必须依赖,字段 /setter 注入更易被 Spring 处理(但本质还是建议解耦)。

总结:循环依赖的核心是 “依赖闭环”,Spring 通过三级缓存解决了字段 /setter 注入的循环依赖,但构造器注入需手动用 @Lazy 处理。最好的方式是在设计阶段通过职责拆分避免循环依赖的产生。

http://www.hskmm.com/?act=detail&tid=13758

相关文章:

  • 无意中在应用层瞥见了一个微内核的操作系统调度器
  • 数据结构思维题选做(长期更新)
  • 政治笔记/错题
  • 9.22模拟赛总结
  • 莫队 n的序列,多次查询一段区间内的数字的个数
  • 【mysql】mysql客户端中文显示乱码
  • 揭秘“牛牛透视”
  • k8s系列--控制器yml(15)
  • 学生管理系统案例初步分析报告
  • 【mysql】mysql5.6 版本修改用户的登录
  • AT_abc200_e [ABC200E] Patisserie ABC 2 题解
  • 日总结 5
  • Linux驱动开发(1)概念、环境与代码框架 - 实践
  • Diffutoon下载介绍:真人视频转动漫工具,轻松获得上千点赞
  • 9月22号
  • 0.5*8 边形 != 式
  • 题解:AT_agc052_c [AGC052C] Nondivisible Prefix Sums
  • 寻路算法
  • 2025年9月22日 - 20243867孙堃2405
  • day 1
  • [Paper Reading] METAGPT: META PROGRAMMING FOR A MULTI-AGENT COLLABORATIVE FRAMEWORK
  • 二进制 - 20243867孙堃2405
  • 学习问题日记-1
  • 记一次生产环境内存溢出记录
  • 四舍六入五成双
  • 借助 Apache Phoenix,使用标准 SQL 和 JDBC 接口来操作 HBase
  • 学生信息管理系统
  • 如何让AI生成多页面APP原型图?AI原型设计实用指南
  • 代码随想录算法训练营第五天 | leetcode 242 349 202 1
  • CF2146 Codeforces Round 1052 (Div. 2) 游记