Spring Boot循环依赖的陷阱与解决方案:如何打破“Bean创建死循环”?

引言

在Spring Boot开发中,你是否遇到过这样的错误信息?
The dependencies of some of the beans in the application context form a cycle
这表示你的应用出现了循环依赖。尽管Spring框架通过巧妙的机制解决了部分循环依赖问题,但在实际开发中(尤其是使用构造器注入时),开发者仍需警惕此类问题。本文将深入探讨循环依赖的根源,分析Spring的解决策略,并提供多种实战解决方案。


一、什么是循环依赖?

循环依赖指两个或多个Bean相互依赖对方,形成一个闭环。例如:

  • Bean A​ 的创建需要注入 ​Bean B
  • Bean B​ 的创建又需要注入 ​Bean A

此时,Spring容器在初始化Bean时会陷入“死循环”。以下是一个典型示例:

@Service
public class ServiceA {private final ServiceB serviceB;public ServiceA(ServiceB serviceB) { // 构造器注入ServiceBthis.serviceB = serviceB;}
}@Service
public class ServiceB {private final ServiceA serviceA;public ServiceB(ServiceA serviceA) { // 构造器注入ServiceAthis.serviceA = serviceA;}
}

启动应用时,Spring会抛出异常:
BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Requested bean is currently in creation


二、Spring如何解决循环依赖?

Spring通过三级缓存机制解决单例Bean的循环依赖问题:

  1. 一级缓存​(singletonObjects):存放完全初始化好的Bean。
  2. 二级缓存​(earlySingletonObjects):存放提前曝光的半成品Bean(仅实例化,未填充属性)。
  3. 三级缓存​(singletonFactories):存放Bean的工厂对象,用于生成半成品Bean。

解决流程​(以A和B相互依赖为例):

  1. 创建A时,先实例化A(未填充属性),并将A的工厂放入三级缓存。
  2. 填充A的属性时发现需要B,开始创建B。
  3. 创建B时,实例化B后,发现需要A,此时从三级缓存中通过工厂获取A的半成品对象。
  4. B完成初始化,放入一级缓存。
  5. A继续填充B的实例,完成初始化,放入一级缓存。

关键限制​:该机制仅支持单例Bean通过属性注入的场景。​构造器注入会直接失败!


三、为何构造器注入会导致循环依赖失败?

构造器注入要求Bean在实例化阶段立即获得依赖对象,而三级缓存机制需要在属性注入阶段解决依赖。因此,当两个Bean都使用构造器注入时,Spring无法提前曝光半成品Bean,导致循环依赖无法解决。


四、解决方案:打破循环依赖的四种方法
1. ​改用Setter/Field注入(谨慎使用)​

将构造器注入改为Setter或字段注入,允许Spring延迟注入依赖:

@Service
public class ServiceA {private ServiceB serviceB;@Autowired // Setter注入public void setServiceB(ServiceB serviceB) {this.serviceB = serviceB;}
}
  • 优点​:快速解决问题。
  • 缺点​:破坏了不可变性(字段非final),且可能掩盖设计问题。
2. ​使用@Lazy延迟加载

在依赖对象上添加@Lazy,告知Spring延迟初始化Bean:

@Service
public class ServiceA {private final ServiceB serviceB;public ServiceA(@Lazy ServiceB serviceB) {this.serviceB = serviceB; // 实际注入的是代理对象}
}
  • 原理​:Spring生成代理对象,只有在首次调用时才会真正初始化目标Bean。
  • 适用场景​:解决构造函数注入的循环依赖。
3. ​重新设计代码结构

通过分层或提取公共逻辑,消除循环依赖:

  • 方案一​:引入中间层(如ServiceC),将A和B的共同依赖转移到C。
  • 方案二​:使用事件驱动(ApplicationEvent),解耦直接依赖。
// 事件驱动示例
@Service
public class ServiceA {@Autowiredprivate ApplicationEventPublisher eventPublisher;public void doSomething() {eventPublisher.publishEvent(new EventA());}
}@Service
public class ServiceB {@EventListenerpublic void handleEventA(EventA event) {// 处理事件}
}
4. ​使用ObjectProvider(推荐)​

在构造器中注入ObjectProvider,按需获取依赖:

@Service
public class ServiceA {private final ServiceB serviceB;public ServiceA(ObjectProvider<ServiceB> serviceBProvider) {this.serviceB = serviceBProvider.getIfUnique();}
}
  • 优点​:保持构造器注入的不可变性,显式控制依赖获取时机。
  • 注意​:需确保依赖Bean存在且唯一。

五、最佳实践与预防措施
  1. 优先使用构造器注入​:保持Bean的不可变性和明确依赖,但需警惕循环依赖。
  2. 定期检测循环依赖​:
    • 使用IDE插件(如IntelliJ的Circular Dependencies分析)。
    • 通过Maven/Gradle插件(如spring-boot-dependencies-analysis)。
  3. 代码分层规范​:
    • 严格遵循分层架构(Controller → Service → Repository)。
    • 避免同一层内的Bean相互依赖。
  4. 单元测试验证​:编写集成测试,验证Bean的初始化过程。
@SpringBootTest
public class CircularDependencyTest {@Autowiredprivate ApplicationContext context;@Testvoid contextLoads() {// 若启动无异常,则通过测试assertNotNull(context.getBean(ServiceA.class));}
}

六、总结

循环依赖是Spring开发中的常见陷阱,其本质是代码设计问题。尽管Spring提供了部分解决方案,但重构代码消除循环依赖才是根本之道。通过合理使用注入方式、代码分层和工具检测,开发者可以有效避免此类问题,构建高可维护性的应用。

记住​:

  • 慎用@Lazy和Setter注入,它们可能掩盖设计缺陷。
  • 构造器注入 + 合理分层 = 更健壮的系统!

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

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

相关文章

如何阅读、学习 Tcc (Tiny C Compiler) 源代码?如何解析 Tcc 源代码?

阅读和解析 TCC&#xff08;Tiny C Compiler&#xff09; 的源代码需要对编译器的基本工作原理和代码结构有一定的了解。以下是分步骤的指南&#xff0c;帮助你更高效地学习和理解 TCC 的源代码&#xff1a; 1. 前置知识准备 C 语言基础&#xff1a;TCC 是用 C 语言编写的&…

Java Set系列集合详解:HashSet、LinkedHashSet、TreeSet底层原理与使用场景

Java Set系列集合详解&#xff1a;HashSet、LinkedHashSet、TreeSet底层原理与使用场景 一、Set系列集合概述 1. 核心特点 无序性&#xff1a;存取顺序不一致&#xff08;LinkedHashSet除外&#xff09;。唯一性&#xff1a;元素不重复。无索引&#xff1a;无法通过索引直接访…

解决 CentOS 7 镜像源无法访问的问题

在国内使用 CentOS 系统时&#xff0c;经常会遇到镜像源无法访问或者下载速度慢的问题。尤其是默认的 CentOS 镜像源通常是国外的&#xff0c;如果你的网络环境无法直接访问国外服务器&#xff0c;就会出现无法下载包的情况。本文将介绍如何修改 CentOS 7 的镜像源为国内镜像源…

云计算与大数据进阶 | 26、解锁云架构核心:深度解析可扩展数据库的5大策略与挑战(上)

在云应用/服务的 5 层架构里&#xff0c;数据库服务层稳坐第 4 把交椅&#xff0c;堪称其中的 “硬核担当”。它的复杂程度常常让人望而生畏&#xff0c;不少人都将它视为整个架构中的 “终极挑战”。 不过&#xff0c;也有人觉得可扩展存储系统才是最难啃的 “硬骨头”&#…

Linux——UDP/TCP协议理论

1. UDP协议 1.1 UDP协议格式 系统内的UDP协议结构体&#xff1a; 注1&#xff1a;UDP协议的报头大小是确定的&#xff0c;为8字节 注2&#xff1a;可以通过报头中&#xff0c;UDP长度将UDP协议的报头和有效载荷分离&#xff0c;有效载荷将存储到接收缓冲区中等待上层解析。 注…

考研复习全年规划

25考研以330分成功上岸。 备考期间&#xff0c;我深知学习规划的重要性&#xff0c;为大家精心整理了一份初试备考时间线任务规划&#xff0c;希望能为正在备考的同学们提供参考。如果你对如何规划学习路线仍感迷茫&#xff0c;不妨参考这份时间表&#xff0c;合理分配时间&…

PhpStudy | PhpStudy 环境配置 —— PhpStudy 目录结构 环境变量配置 · Windows 篇

&#x1f31f;想了解这个工具的其它相关笔记&#xff1f;看看这个&#xff1a;[网安工具] 服务器环境配置工具 —— PhpStudy 使用手册 在前面的章节中&#xff0c;笔者详细介绍了如何在 Windows 和 Linux 系统中安装 PhpStudy&#xff0c;但可能会有崽崽在安装完成后发现依旧…

DDS(数据分发服务) 和 P2P(点对点网络) 的详细对比

1. 核心特性对比 维度 DDS P2P 实时性 微秒级延迟&#xff0c;支持硬实时&#xff08;如自动驾驶&#xff09; 毫秒至秒级&#xff0c;依赖网络环境&#xff08;如文件传输&#xff09; 架构 去中心化发布/订阅模型&#xff0c;节点自主发现 完全去中心化&#xff0c;节…

java中XML的使用

文章目录 什么是XML特点XML作用XML的编写语法基本语法特殊字符编写 约束XML的书写格式DTD文档schema文档属性命名空间XML命名空间的作用 解析XML的方法​​DOM解析XMLDOM介绍DOM解析包&#xff1a;org.w3c.dom常用接口DOM解析包的使用保存XML文件添加DOM节点修改/删除DOM节点 S…

Spring Boot异步任务失效的8大原因及解决方案

Spring Boot异步任务失效的8大原因及解决方案 摘要:在使用Spring Boot的@Async实现异步任务时,你是否遇到过异步不生效的问题?本文总结了8种常见的异步失效场景,并提供对应的解决方案,帮助你彻底解决异步任务失效的难题。 一、异步失效的常见场景 1. 未启用异步支持 ❌ …

QT6 源(104)篇一:阅读与注释QAction,其是窗体菜单栏与工具栏里的菜单项,先给出属性测试,再给出成员函数测试,最后给出信号函数的学习于举例测试

&#xff08;1&#xff09; &#xff08;2&#xff09; &#xff08;3&#xff09;接着给出成员函数测试 &#xff1a; &#xff08;4&#xff09; 给个信号函数的举例 &#xff1a; &#xff08;5&#xff09; 谢谢

visual studio生成动态库DLL

visual studio生成动态库DLL 创建动态库工程 注意 #include “pch.h” 要放在上面 完成后点击生成 创建一个控制台项目 设置项目附加目录为刚才创建的动态库工程Dll1&#xff1a; 配置附加库目录&#xff1a; 配置动态库的导入库&#xff08;.lib&#xff09;&#xff1a;链…

matlab多智能体网络一致性研究

一个基于连续时间多智能体系统&#xff08;Multi-Agent Systems, MAS&#xff09;的一阶一致性协议的MATLAB仿真代码&#xff0c;包含网络拓扑建模、一致性协议设计和收敛性分析。代码支持固定拓扑和时变拓扑&#xff0c;适用于学术研究。 1. 基础模型与代码框架 (1) 网络拓扑…

【omnet++】omnet++6.0.3中调用python

版本&#xff1a; omnet 6.0.3 Ubuntu 20.04.6 LTS omnet的installguide中对ubuntu版本是有要求的&#xff0c;找到对应版本下载即可 先安装omnet再安装anaconda omnet 6.0.3安装 别在网上找教程了&#xff0c;官方的installguide手册是最好的。按照手册安装一些依赖包后 so…

【C++】 —— 笔试刷题day_29

一、排序子序列 题目解析 一个数组的连续子序列&#xff0c;如果这个子序列是非递增或者非递减的&#xff1b;这个连续的子序列就是排序子序列。 现在给定一个数组&#xff0c;然后然我们判断这个子序列可以划分成多少个排序子序列。 例如&#xff1a;1 2 3 2 2 1 可以划分成 …

UE RPG游戏开发练手 第二十七课 普通攻击2

UE RPG游戏开发练手 第二十七课 普通攻击2 1. 创建普通攻击的蒙太奇动画 2.打开4个蒙太奇动画&#xff0c;修改插槽为FullBody,修改动画速度 3.编辑动画蓝图&#xff0c;插入FullBody插槽让普通攻击动画得以播放 4. 编辑GA_LightAttack技能蓝图

MySQL——日志

undo log(回滚日志)&#xff1a;引擎层生成的日志&#xff0c;实现了事务的原子性&#xff0c;用于事务回滚和MVCC。redo log(重做日志)&#xff1a;引擎层生成的日志&#xff0c;实现了事务的持久性&#xff0c;用于非正常关闭的数据恢复。bin log(归档日志)&#xff1a;Serve…

QML 动画控制、顺序动画与并行动画

目录 引言相关阅读基础属性说明工程结构示例代码解析示例1&#xff1a;手动控制动画&#xff08;ControlledAnimation.qml&#xff09;示例2&#xff1a;顺序动画&#xff08;SequentialAnimationDemo.qml&#xff09;示例3&#xff1a;并行动画&#xff08;ParallelAnimationD…

PowerShell 实现 conda 懒加载

问题 执行命令conda init powershell会在 profile.ps1中添加conda初始化的命令。 即使用户不需要用到conda&#xff0c;也会初始化conda环境&#xff0c;拖慢PowerShell的启动速度。 解决方案 本文展示了如何实现conda的懒加载&#xff0c;默认不加载conda环境&#xff0c;只…

R语言学习--Day03--数据清洗技巧

在一般情况下&#xff0c;我们都是在数据分析的需求前提下去选择使用R语言。而实际上&#xff0c;数据分析里&#xff0c;百分之八十的工作&#xff0c;都是在数据清洗。并不只是我们平时会提到的异常值处理或者是整合格式&#xff0c;更多会涉及到将各种各样的数据整合&#x…