Spring未能自动解决循环依赖的问题

article/2025/8/28 15:12:04

有过经验的同学应该都知道Spring能够自动解决循环依赖的问题,依靠的是它为单例池提供的三级缓存。如果你还不清楚三级缓存具体是怎么个解法的话,可以看一下这篇文章【图文详解】Spring是如何解决循环依赖的?

本文中的问题来源于我在开发项目时,偶然碰到了循环依赖的报错,错误内容如下所示,如果你也遇到了类似的报错,那恭喜你找对地方了

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.

 这个报错的意思是:AService 这个 Bean 的原始版本被注入到了 BService 中,但是 AService 最终又被包装进行了代理。也就是说 BService中持有了 AService 的原始对象而不是最终生成的代理对象,这肯定是不得行的,所以 Spring 在启动时就报错了。

你可能比较疑惑,Spring不是自己能处理循环依赖么?产生上述问题的原因如下:Spring的单例池三级缓存,在第三级缓存其实可以解决处理一部分代理场景,比如@Transaction 和我们自定义的切面,但是仅是一部分,或者说绝大部分,但不是全部!像@Async这种注解生成的代理就不在第三级缓存的监测范围内,而是会在Bean的初始化阶段(initializeBean)针对@Async生成对应的代理。所以Spring通过单例池三级缓存解决循环依赖时处理不了@Async这种代理情况,最终导致上面的异常。

补充一下,在SpringBoot的2.x版本下,会产生上述问题。SpringBoot最新的3.x版本已经解决了这个问题。

我的问题原因就是因为在产生循环依赖的类里使用了 @Async注解。

解决办法:

  1. 升级SpriingBoot版本到最新版本。我尝试了 3.5.0 版本,没有再出现这个问题,但是需要将jdk版本升级到17(SpringBoot 3.5.0版本要求),如果你的线上jdk版本较低的话,可以放弃这个办法。
  2. 使用@Lazy注解。在产生循环依赖的 @Autowired  上加上 @Lazy 注解,让这些属性不在Spring容器启动时加载,而是延迟到真正使用这些属性时再加载,这时候Bean都已经创建好了,该代理的也早就代理过了,不会再出现上述问题。但是注意当带@Async注解的类 再和 其他类又产生循环依赖关系时也要加上@Lazy注解。
  3. 重新规划代码,将带@Async的方法提到一个单独的类中,不和其他类产生循环依赖。

下面是一个源码跟踪的详细步骤,感兴趣的可以接着看下

先定义两个产生循环依赖的Service类, 两个Service类都只有一个方法 say, 并且AService 的 say 方法添加了 @Async 注解;两个Service类 分别引用了对方,产生了循环依赖。

@Service
public class AService {@Autowiredprivate BService bService;@Asyncpublic void say() {System.out.println("AService -- say");}}@Service
public class BService {@Autowiredprivate AService aService;public void say() {System.out.println("BService -- say");}}

这种情况下容器启动时就会报错

2025-05-28 16:26:41.970 ERROR 20343 --- [  restartedMain] o.s.boot.SpringApplication               : Application run failedorg.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:649) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.12.jar:5.3.12]at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.12.jar:5.3.12]at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.12.jar:5.3.12]at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) [spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) [spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.run(SpringApplication.java:338) [spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) [spring-boot-2.5.6.jar:2.5.6]at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332) [spring-boot-2.5.6.jar:2.5.6]at com.hml.SpringBootSimpleApplication.main(SpringBootSimpleApplication.java:16) [classes/:na]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_372]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_372]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_372]at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_372]at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.5.6.jar:2.5.6]

下面我采用刚才提到的解法2:添加@Lazy注解来试试。

 这是改造后的两个Service, 仅仅是在 AService的 @Autowired private BService bService; 属性上加上了 @Lazy 注解。

@Service
public class AService {@Autowired@Lazyprivate BService bService;@Asyncpublic void say() {System.out.println("AService -- say");}}@Service
public class BService {@Autowiredprivate AService aService;public void say() {System.out.println("BService -- say");}}

这时候再启动项目就不会再报错了。下来通过源码来解释下原因

Spring容器在启动时会默认提前加载所有Bean到单例池,并且如果Bean中有 @Autowired 引用其他类的话,也会将对应类的Bean创建出来,并将其对象引用拿过来放到 @Autowired 属性中。

那AService和BService谁先加载呢?在AbstractBeanFactory类的doGetBean方法上打断点,在我们定义的这两个Bean创建时将流程停住。

启动后可以发现,Spring先加载了AService。在AbstractAutowireCapableBeanFactory的doCreateBean方法中会执行Bean生命周期的关键三步 1.实例化Bean  2.给Bean填充属性  3.初始化Bean。

AService 创建Bean的过程中,在执行到第二步 populateBean 时,发现有个 @Autowired BServce bService属性,于是开始获取 BService 这个 Bean, 由于BService这个Bean还未创建,所以就开始了创建 BService这个Bean的流程。在创建BService Bean时,发现它又要注入 AService, 于是又反过来去获取 AService这个Bean, 然后就来到了问题关键点,AService 这个Bean当前被放在单例池第三级缓存中,三级缓存中放的是个ObjectFactory, 它通过 getObject 方法来获取Bean, 这个getObject方法是通过一个 lambda表达式来声明的,即通过getEarlyBeanReference来获取Bean, getEarlyBeanReference方法会判断这个Bean需不需要被代理,如果需要被代理的话就在这个方法里生成一个代理后的Bean。但是这一步只会有两个BeanPostProcessor来处理Bean的代理逻辑:

org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor

而这两个BeanPostProcessor 都无法针对 @Async 生成代理,最终getEarlyBeanReference方法执行完后发现 AService 不需要被代理😮,于是返回了AService的原始对象,最终这个原始对象的引用被放到了 BService 的 @Autowired AService aService; 属性里。

随后BService 这个Bean创建完了, AService 拿到 BService的引用, 放到它的 @Autowired BService bService属性中了, 然后 AService 开始进行“初始化”(initializeBean)过程,在“初始化”过程中会再进行判断这个 Bean 需不需要被代理,当然 initializeBean 过程中判断代理用到的 BeanPostProcessor 不仅包含 单例池第三级缓存 判断代理用到的 BeanPostProcessor,而且更多,足足有12个BeanPostProcessor,其中就包括 AsyncAnnotationBeanPostProcessor,这个类就是用来处理 @Async 代理的。于是 AService 在执行完“初始化”过程后就生成了一个代理后的Bean, 看!问题就出来了,AService 最终需要被放到单例池中(注意此时AService还没放入单例池)的是代理后的Bean, 但是 BService持有的却是 代理前的AService Bean, Spring在判断到这个情况后就报异常了

在AbstractAutowireCapableBeanFactory的doCreateBean方法中

两个Bean创建的时序图如下

知道了原因,就知道为什么我们用@Lazy注解可以解决这个问题了, @Lazy注解修饰 @Autowired后,AService中的  bService属性就会延时加载,在AService创建Bean的过程中就不会去寻找 BService Bean的引用,创建完 AService Bean后 单例池中的 AService Bean就已经是代理过的对象了 ,AService的代理对象也建好了, 后续代码逻辑中再触发获取AService Bean时就直接能从单例池中获取代理后的AService了。

使用@Lazy注解属于头痛医头脚痛医脚的治疗方式,个人觉得最好的方式还是重新规划下代码,将使用 @Async 注解的方法单独提到一个干净的类里并且不和其他类形成循环依赖。


http://www.hkcw.cn/article/uVwHZRrxbE.shtml

相关文章

RV1126 FFMPEG推流器理解

一.FFMPEG概念 概念:FFMPEG是一种音视频推流工具,把RV1126编码的视频,通过FFMPEG推流到流媒体服务器上,让大家都能访问和观看。为什么RV1126 编码的视频码流要利用 FFMPEG 框架推送到流媒体服务器,之前通过终端ffplay…

TeleAI发布TeleChat2.5及T1正式版,双双开源上线魔乐社区!

5月12日,中国电信开源TeleChat系列四个模型,涵盖复杂推理和通用问答的多个尺寸模型,包括TeleChat-T1-35B、TeleChat-T1-115B、TeleChat2.5-35B和TeleChat2.5-115B,实测模型性能均有显著的性能效果。TeleChat系列模型基于昇思MindS…

TMS320F28388D使用sysconfig配置IPC

第1章 配置IPC底层代码 使用IPC的动机: 我计划我的项目中要使用RS485,CANFD通信和EtherCAT通信,由于通信种类较多,而对于电机控制来说大部分数据都是重复的,并且有些数据可以很久才改变一次,所以我计划使…

Linux的线程同步

一、互斥锁(互斥量) 互斥锁是一种特殊的变量,有上锁(lock)和解锁(unlock)两种状态。 当处于解锁状态时,线程想获取该互斥锁,就可以获取不被阻塞,互斥锁变为…

.NET WinForm图像识别二维码/条形码

需求:图像识别出一张图片中的二维码或者条形码,并读取其中的内容。 一、安装ZXing.NET(此处建议使用0.14.0,高版本的不少地方进行优化,如果能处理好也可) Install-Package ZXing.Net 二、WinForm示例代码(含关键优化&#xff0…

理解教材意图轻松积累常见数列

教学感悟 以前在传授数列时只是机械的要求学生记住常见的数列,至于“哪些才算是常见的数列?这些数列是怎么来的”,心里比较糊涂,在有一次的教学中,偶然回忆起:函数教学时教材要求掌握一些常见的函数&#…

[预训练]Encoder-only架构的预训练任务核心机制

原创文章1FFN前馈网络与激活函数技术解析:Transformer模型中的关键模块2Transformer掩码技术全解析:分类、原理与应用场景3【大模型技术】Attention注意力机制详解一4Transformer核心技术解析LCPO方法:精准控制推理长度的新突破5Transformer模…

【大模型MCP】MCP 深度解析:AI 时代的「USB-C」接口——原理、对比、实战代码与行业落地

摘要 模型上下文协议 (Model Context Protocol, MCP) 诞生于 2024 年11月,由 Anthropic 牵头,在 2025-03-26 发布 1.0 正式规范。它以 JSON-RPC 2.0 消息结构承载于 WebSocket / SSE 等全双工传输之上,把「初始化 → 工具目录 → 调用 → 结…

超声波清洗机的作用是什么?使用超声波清洗机可以去除毛刺吗?

在现代制造业中,表面质量对产品的性能和外观至关重要。超声波清洗机作为一种高效的清洗工具,在去除表面污垢和缺陷方面发挥着关键作用。本文深圳科伟达将介绍超声波清洗机的作用,以及它是否能够有效去除毛刺。 超声波清洗机的作用&#xff1…

非常好看网站维护带倒计时模板+维护结束模板

非常好看网站维护带倒计时模板维护结束模板 网站维护带倒计时模板维护结束模板.zip - 蓝奏云

STM32F103_Bootloader程序开发05 - Keil修改生成文件的路径与文件名,自动生成bin格式文件

导言 通过Keil的相关配置,可以灵活地修改输出文件的保存路径及文件名称。在Bootloader程序开发过程中,合理配置输出文件对于后续固件升级和自动化脚本处理至关重要。完成路径和文件名配置后,还可以借助Keil自带的fromelf.exe工具,…

【MQ】消息队列的核心价值及技术选型

文章目录 一、什么是消息队列二、消息队列的核心功能三、主流消息队列技术对比ActiveMQ:逐渐淡出的老牌产品Kafka:大数据领域的王者RabbitMQ:企业级可靠选择Apache Pulsar:云原生时代的"新贵"NATS:轻量级消息…

本地部署消息代理软件 RabbitMQ 并实现外部访问( Windows 版本 )

RabbitMQ 是由 Erlang 语言开发的 消息中间件,是一种应用程序之间的通信方法。支持多种编程和语言和协议发展,用于实现分布式系统的可靠消息传递和异步通信等方面。 本文将详细介绍如何在 Windows 系统本地部署 RabbitMQ 并结合路由侠实现外网访问本…

如何在 Odoo 18 中创建 PDF 报告

如何在 Odoo 18 中创建 PDF 报告 Qweb 是 Odoo 强大的模板引擎,旨在轻松将 XML 数据转换为 HTML 文档。其功能特性包括基于属性的自定义、条件逻辑、动态内容插入及多样化的报告模板选项。这种多功能性使 Qweb 成为制作个性化、视觉吸引力强的报告、电子邮件和文档…

【面板数据】上市公司供应链网络地位数据(2001-2024年)

企业是产业链供应链的微观主体,其供应链网络布局是增强产业链供应链稳定性的重中之重,企业在供应链网络中的地位,已成为衡量其市场影响力和风险承受能力的重要指标。本次是上市公司供应链网络地位数据,数据年份为2001-2024年。 一…

EasyFileCount(文件查重工具) v3.0.5.1 便携版

EasyFileCount 是一款免费方便、快捷的文件管理软件,采用Java开发,主打一个清爽简洁、安全无害的使用体验,支持多盘高性能扫描。遍历扫描文件夹的速度还算比较快的,当然所谓的快慢取决于CPU、系统、硬盘种类、文件或文件夹的数量等…

【在线五子棋对战】一、项目简介 环境搭建

文章目录 Ⅰ. 项目简介Ⅱ. 开发环境Ⅲ. 核心技术Ⅳ. 环境搭建① CentOS-7.6环境搭建1、安装wget工具2、更换软件源3、安装第三方软件源4、安装lrzsz传输工具5、安装高版本gcc/g编译器6、安装gdb调试器7、安装git8、安装cmake9、安装boost库10、安装Jsoncpp库11、安装MySQL数据库…

MFC坦克大战游戏制作

MFC坦克大战游戏制作 前言 现在的游戏制作一般是easyx,有没有直接只用mfc框架的,笔者研究了一番,做出了一个雏形,下面把遇到的问题总结出来 一、MFC框架制作游戏 初步设想,MFC可以选用 对话框 或者 单文档 结构&…

12:遨博机器人SDK二次开发

一、流程步骤 1.获取当前点关节坐标 2.走当前点关节坐标 1.获取目标点x,y,z(位置坐标,以m为单位,需要*1000变成mm)和四元素(位姿坐标) 2.四元素→欧拉角(弧度制) 3.欧拉角&#x…

XPlifeapp:高效打印,便捷生活

在数字化时代,虽然电子设备的使用越来越普遍,但打印的需求依然存在。无论是学生需要打印课表、资料,还是职场人士需要打印名片、报告,一个高效便捷的打印软件都能大大提高工作效率。XPlifeapp就是这样一款超级好用的手机打印软件&…