Spring 循环依赖:从原理到解决方案的全面解析

Spring 循环依赖:从原理到解决方案的全面解析

一、循环依赖的定义与分类

1. 什么是循环依赖?

在 Spring 框架中,循环依赖指的是多个 Bean 之间形成了依赖闭环。例如:

  • Bean A 依赖 Bean B
  • Bean B 依赖 Bean C
  • Bean C 又依赖 Bean A
    此时,这三个 Bean 之间就形成了循环依赖关系。当容器尝试初始化这些 Bean 时,会陷入无法完成初始化的死循环。

2. 循环依赖的三种类型

根据依赖注入方式的不同,循环依赖可分为三类:

类型描述
构造器循环依赖多个 Bean 通过构造函数互相依赖,如A构造器注入BB构造器注入A
setter 循环依赖多个 Bean 通过 setter 方法互相依赖,如A.setB(b)B.setA(a)
字段注入循环依赖多个 Bean 通过字段直接注入互相依赖,如@Autowired private B b;@Autowired private A a;

二、Spring 如何处理循环依赖?

1. Spring 处理循环依赖的核心机制

Spring 通过三级缓存机制解决 setter 和字段注入的循环依赖,而构造器循环依赖则无法自动解决。

三级缓存的定义(DefaultSingletonBeanRegistry类):
  1. 一级缓存(singletonObjects):存储完全初始化的 Bean 实例。
  2. 二级缓存(earlySingletonObjects):存储已创建但未完全初始化的 Bean 实例(早期暴露的对象)。
  3. 三级缓存(singletonFactories):存储 Bean 的工厂对象,用于生成代理对象等后置处理。

2. 解决 setter 循环依赖的流程示例

A依赖B,B依赖A的 setter 循环依赖为例:

  1. 初始化 A:创建 A 的实例,放入二级缓存,并标记为 “未完全初始化”。
  2. 注入 B 到 A:发现 A 依赖 B,开始初始化 B。
  3. 初始化 B:创建 B 的实例,放入二级缓存,然后尝试注入 A 到 B。
  4. 注入 A 到 B:此时 A 已在二级缓存中,B 获取 A 的早期实例并完成注入,B 初始化完成后放入一级缓存。
  5. 完成 A 的初始化:A 获取到已初始化的 B,完成注入后放入一级缓存。

3. 三级缓存的核心作用

  • 三级缓存的存在是为了处理 AOP 代理:当 Bean 需要代理时,三级缓存存储的工厂对象会在早期暴露阶段生成代理实例,避免循环依赖中出现 “原始对象” 和 “代理对象” 的不一致问题。

三、构造器循环依赖为何无法解决?

1. 构造器循环依赖的初始化流程

假设A构造器注入BB构造器注入A

  1. 初始化 A 时,需要先创建 B 的实例。
  2. 初始化 B 时,又需要先创建 A 的实例。
  3. 由于构造器依赖必须在对象创建时完成,两者互相等待,导致初始化阻塞。

2. 示例代码与异常

@Component
public class A {private final B b;// 构造器注入B,导致循环依赖public A(B b) {this.b = b;}
}@Component
public class B {private final A a;public B(A a) {this.a = a;}
}

启动 Spring 容器时会抛出org.springframework.beans.factory.UnsatisfiedDependencyException,提示无法解析循环依赖。

四、循环依赖的解决方案

1. 针对构造器循环依赖:

方案一:使用 setter 注入替代构造器注入
@Component
public class A {private B b;// 使用setter注入,允许Spring通过三级缓存解决循环依赖public void setB(B b) {this.b = b;}
}
方案二:使用 @Lazy 延迟初始化

通过@Lazy让 Spring 注入代理对象,延迟依赖解析:

@Component
public class A {private final B b;// 注入B的代理对象,避免初始化时立即创建Bpublic A(@Lazy B b) {this.b = b;}
}
方案三:拆分 Bean,打破依赖链

将复杂 Bean 拆分为多个小 Bean,避免直接依赖。

2. 针对 setter / 字段循环依赖:

通常无需特殊处理,Spring 三级缓存可自动解决。若遇到问题,可能是以下原因:

  • Bean 使用了@PostConstruct等初始化方法,且方法中存在循环逻辑。
  • 自定义 BeanPostProcessor 干扰了三级缓存的正常工作。

3. 通用最佳实践:

  1. 优先使用构造器注入:明确依赖关系,但需避免构造器循环依赖。
  2. 谨慎使用 @Autowired 字段注入:可能隐藏依赖关系,推荐搭配 setter 注入。
  3. 使用 @DependsOn:强制指定 Bean 初始化顺序,打破隐性循环依赖。
  4. 模块化设计:通过拆分服务或引入中间层,避免跨模块的直接依赖。

五、Spring Boot 中循环依赖的排查与工具

1. 日志排查

启动时添加 JVM 参数-Dspring.main.allow-circular-references=true(Spring Boot 2.6+),允许循环依赖并打印警告日志。

2. IDE 工具辅助

  • IntelliJ IDEA:通过Analyze Dependencies功能检测循环依赖。
  • Spring Tool Suite:使用依赖分析视图定位问题 Bean。

3. 编程式排查

通过ConfigurableApplicationContext.getBeanFactory()获取DefaultListableBeanFactory,调用isPrototypeCurrentlyInCreation()等方法诊断循环依赖。

六、深度解析:三级缓存的源码视角

1. 关键源码路径(AbstractBeanFactory.doGetBean):

// 从一级缓存获取Bean
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && !isSingletonCurrentlyInCreation(beanName)) {return getObjectForBeanInstance(sharedInstance, name, beanName, null);
}// 标记Bean为“正在创建”
beforeSingletonCreation(beanName);
try {// 从二级缓存获取早期实例sharedInstance = getSingleton(beanName, false);if (sharedInstance != null) {// 处理早期实例return getObjectForBeanInstance(sharedInstance, name, beanName, null);}// 创建Bean实例(未初始化)BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);Object bean = instanceWrapper.getWrappedInstance();// 将早期实例放入三级缓存addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));// 填充依赖(可能触发循环依赖)populateBean(beanName, mbd, instanceWrapper);// 初始化Bean(调用后置处理器等)exposedObject = initializeBean(beanName, exposedObject, mbd);// 将完全初始化的Bean放入一级缓存addSingleton(beanName, exposedObject);
} finally {afterSingletonCreation(beanName);
}

2. 核心方法解析:

  • getSingleton(beanName, true):尝试从一级缓存获取 Bean,若不存在则创建。
  • addSingletonFactory:将 Bean 的工厂对象存入三级缓存,用于生成早期实例。
  • getEarlyBeanReference:处理 AOP 代理等后置操作,返回早期实例。

七、总结:循环依赖的本质与设计哲学

Spring 通过三级缓存解决循环依赖的核心,是利用 “早期暴露” 机制打破初始化死锁:将未完全初始化的 Bean 提前暴露到二级缓存,允许其他 Bean 先获取其引用,后续再完成初始化。

但构造器循环依赖无法解决,这体现了 Spring 的设计原则:构造器依赖应代表 “强依赖”,而强依赖不应形成循环。在实际开发中,合理的依赖设计(如模块化、单向依赖)比依赖 Spring 的循环依赖处理机制更重要。

理解循环依赖的原理与解决方案,不仅能帮助开发者快速定位问题,还能加深对 Spring Bean 生命周期和依赖注入机制的理解,从而写出更健壮的代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/82723.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

n 阶矩阵 A 可逆的充分必要条件是 ∣ A ∣ ≠ 0

n 阶矩阵 A 可逆的充分必要条件是 ∣ A ∣ ≠ 0 |A| \neq 0 ∣A∣0 的几何意义 1. 行列式的几何意义回顾 行列式 ∣ A ∣ |A| ∣A∣(或 det ⁡ ( A ) \det(A) det(A))表示矩阵 A A A 所对应的线性变换对空间的体积缩放因子: ∣ A ∣ &…

Rockey Linux 安装ffmpeg

1.环境准备 Rockey linux 9.2 ffmpeg 静态资源包 这个是我自己的: https://download.csdn.net/download/liudongyang123/90920340https://download.csdn.net/download/liudongyang123/90920340 这个是官网的 Releases BtbN/FFmpeg-Builds GitHub 以上两个资…

wordcount在集群上的测试

1.将louts.txt文件从cg计算机复制到master节点上面,存放在/usr/local/hadoop 需要输入密码:83953588abc scp /root/IdeaProjects/mapReduceTest/lotus.txt root172.18.0.2:/usr/local/hadoop /WordCountTest/input 2.将lotus.txt文件从master这台机器…

AI+制造:中小企业的低成本智能化转型

文章内容过长,可以考虑直接跳转到文章末尾查看概要图 在制造业竞争日益激烈的今天,中小企业正面临着前所未有的挑战:人力成本持续攀升、能源消耗居高不下、质量控制难度增加。与此同时,数字化转型已成为行业共识,但高…

Linux C/C++编程 —— 线程技术总结

一、线程基本概念 线程是进程内的一个执行单元,多个线程共享进程的资源(如内存、文件描述符等),但每个线程拥有自己的寄存器、栈等。与进程相比,线程的创建、切换开销较小,能更高效地利用 CPU 资源。 二、…

Femap许可证与网络安全策略

随着科技的快速发展,网络安全问题已成为各行各业关注的焦点。在电磁仿真领域,Femap作为一款领先的软件,其许可证的安全性和网络策略的重要性不言而喻。本文将探讨Femap许可证与网络安全策略的关系,确保您的电磁仿真工作能够在一个…

深度解析:SQLynx 如何筑牢数据库安全防线​

在数据驱动业务发展的时代,数据库作为企业核心资产的 “保险箱”,其安全性至关重要。一旦数据库遭遇攻击、数据泄露或被误操作,将给企业带来不可估量的损失。而 SQLynx 作为一款专注于数据库安全管理的工具,凭借其多项创新技术与功…

更新时间相差8个小时

下面的java代码在updateFill方法里面生成的modifiedTime时间是当前时间是正确的,为什么到service层testCommonFieldAutoUpdate方法里面去更新的时候modifiedTime就差8个小时呢?代码如下所示: Slf4j Component public class MpMetaObjectHand…

Windows逆向工程提升之IMAGE_TLS_DIRECTORY

公开视频 -> 链接点击跳转公开课程博客首页 -> ​​​链接点击跳转博客主页 目录 TLS的作用 TLS的实现 静态 TLS​​ 动态 TLS​​ 内部实现 回调机制 TLS Directory 的结构 TLS的作用 TLS (Thread Local Storage) 是一种用于为多线程应用程序提供线程独立存储空…

云效流水线Flow使用记录

概述 最近在频繁使用阿里云云效的几款产品,如流水线。之前写过一篇,参考云效流水线缓存问题。 这篇文章来记录更多问题。 环境变量 不管是云效流水线Flow还是应用交付AppStack(基于流水线,后文不再赘述)&#xff0…

Android中获取控件尺寸进阶方案

在Android开发中,很多场景都需要获取控件(View)的宽高信息,比如动态布局调整、动画效果实现等。然而,直接在Activity的onCreate()中调用控件的getWidth()或getHeight()`方法,得到结果却是0,因为控件还没完成布局测量。 本文总结了几种获取控件大小的实用方法,并对各方…

android 输入系统

一、输入系统的核心角色与分层架构 Android 输入系统的本质是桥梁:一端连接硬件驱动产生的原始事件,另一端将事件精准派发给应用窗口。整个过程涉及三层架构和多个关键组件,可类比为 “快递分拣系统”: 1. 硬件与内核层&#xf…

ubuntu中,c和c+程序,预编译、编译、链接和运行命令

目录 安装编译器一.c编译运行(粗暴简单)1.编写 C 程序:2. 预处理:3.编译:4. 汇编:5. 链接:6.运行 二.c编译运行(粗暴简单)1.编写 C 程序:2.预处理&#xff1a…

【FastAPI】--2.进阶教程(一)

【FastAPI】--基础教程-CSDN博客 app.get和post的参数: 参数类型说明pathstr路由路径(如 "/marks"),必填。response_modelType[T]定义响应数据的模型(如 percent),会自动校验和序列…

KT6368A通过蓝牙芯片获取手机时间详细说明,对应串口指令举例

一、功能简介 KT6368A双模蓝牙芯片支持连接手机,获取手机的日期、时间信息,可以同步RTC时钟 1、无需安装任何app,直接使用系统蓝牙即可实现 2、同时它不影响音频蓝牙,还支持一些简单的AT指令进行操作 3、实现的方式&#xff1…

【平面波导外腔激光器专题系列】用于光纤传感的低噪声PLC外腔窄线宽激光器

----翻译自Mazin Alalusi等人的文章 摘要 高性价比的 1550 nm DWDM平面外腔 &#xff08;PLANEX&#xff09; 激光器是干涉测量、布里渊、LIDAR 和其他光传感应用的最佳选择。其线宽<3kHz、低相位/频率噪声和极低的RIN。 简介 高性能光纤分布式传感技术是在过去几年中开发…

企业微信内部网页开发流程笔记

背景 基于ai实现企微侧边栏和工作台快速问答小助&#xff0c;需要h5开发&#xff0c;因为流程不清楚摸索半天&#xff0c;所以记录一下 一、网页授权登录 1. 配置步骤 1.1 设置可信域名 登录企业微信管理后台 进入"应用管理" > 选择开发的具体应用 > “网…

WORD 转 PDF 工具:排版 / 图片 / 表格批量转换提升办公效率

各位办公小能手们&#xff0c;今天来聊聊文档工具里的WORD转PDF工具&#xff01;这玩意儿到底是干啥的呢&#xff1f;咱来好好说道说道。 先说核心功能。第一个就是格式转换&#xff0c;能把Word文档转换成PDF&#xff0c;不管是格式、排版&#xff0c;还是图片、表格啥的&…

从逆流监测到智慧用电:ADL200N-CT系列单相导轨表赋能家庭绿色能源

在新能源浪潮席卷全球的当下&#xff0c;阳台光伏以及家庭储能&#xff08;户储&#xff09;系统逐渐成为众多追求绿色生活、渴望实现能源自主家庭的新选择。它不仅能有效利用太阳能等清洁能源&#xff0c;还能在用电高峰时段为家庭提供稳定电力支持&#xff0c;降低用电成本。…

std::thread的说明与示例

源于通义千问 在 C 中&#xff0c;std::thread 支持传递多种类型的函数作为线程入口点。你可以传递普通函数、类的成员函数、Lambda 表达式、函数对象&#xff08;仿函数&#xff09;等。以下是详细的说明和示例。 1. 传递普通函数 普通函数是最简单的用法。 示例 #include…