Spring使用@Async出现循环依赖原因以及解决方案

场景复现

1、首先项目需要打开spring的异步开关,在application主类上加@EnableAsync
2、创建一个包含了@Async方法的异步类MessageService:

@Service
public class MessageService {@Resource    private TaskService taskService;   @Async    public void send(){taskService.shit();    }
}

3、创建另一个正常类TaskService,与异步类形成循环引用的关系(注意MessageService和TaskService在同一个包内,并且order为默认,因此会先扫描MessageService再扫描TaskService):

@Service
public class TaskService {@Resource    private MessageService messageService;  public void shit(){System.out.println();    }
}

4、启动springboot项目成功报错

问题出现的原因

在分析原因之前,我们需要提前知道两个重要的点:

  1. spring的aop代理(包括@Transactional 事务代理),都是在AbstractAutowireCapableBeanFactory的populateBean方法后的initializeBean当中的applyBeanPostProcessorsAfterInitialization方法里,通过特定的后置处理器里创建代理对象(如果用@Autowired则是AnnotationAwareAspectJAutoProxyCreator)
  2. 然而1.当中描述的代理过程,是在这个类不涉及到循环引用的情况下才会执行,也就是说满足百分之90的情况,而循环引用的情况会做特殊的处理,即提前创建代理对象。

举个例子: T类是个包含了@Transactional方法的类,属于需要被代理的对象,并且通过@Resource(或者@Autowired)的方式依赖了A ,A类中也以同样的方式注入了T,并且T类先于A类开始实例化过程,那么简单的实例化流程就是:

  • T的BeanDefinition被spring拿到后,根据构造器实例化一个T对象(原始对象而非代理对象),并包装成objectFactory放入singletonFactories(三级缓存)中 然后执行populateBean方法开始注入属性的流程,其中会利用CommonAnnotationBeanPostProcessor(@Resource用这个后置处理器,@Autowired用 AutowiredAnnotationBeanPostProcessor)执行T的属性注入步骤,遍历T中所依赖的属性
  • 发现T依赖了A,会先到beanFactory的一至三级缓存中,通过A的beanName查询A对象,如果没找到,即A还没有被实例化过,那么会将A作为实例化的目标,重复a.步骤:将A实例化后的对象包装成objectFactory放入singletonFactories,接着对A执行populateBean来注入属性
  • 遍历A的属性,发现A依赖了T,然后尝试去beanFactory中获取T的实例,发现三级缓存中存在T的objectFactory,因此执行objectFactory.getObject方法企图获取T的实例。然而这个objectFactory并非是简单把对象返回出去,而是在当初包装的时候,就将AbstractAutowireCapableBeanFactory的getEarlyBeanReference方法写入getObject当中
  • 在getEarlyBeanReference方法里,会遍历所有SmartInstantiationAwareBeanPostProcessor的子类型的后置处理器,执行对应的getEarlyBeanReference方法,此时会将第1.点提到的代理过程提前,即通过 AnnotationAwareAspectJAutoProxyCreator(SmartInstantiationAwareBeanPostProcessor的子类)来创建一个代理对象,并放入二级缓存earlySingletonObjects当中,然后将这个代理对象通过field.set的形式(默认形式)注入到A,至此就完成了普通aop对象的循环引用处理

出现本文标题中循环引用异常的原因分析

包含了@Async 方法的类与@Transactional的类相似,也会被替换成一个新的代理类,但是与普通aop不同的是,@Async不会在 getEarlyBeanReference 阶段执行创建代理的逻辑(这么做的原因暂时没仔细分析),而是被延迟到了initializeBean步骤当中(即1.提到的90%的代理情况),这样一来就会导致TaskService注入的并不是最终创建完成的MessageService的代理对象,很明显这样的结果是不合理的,而在代码层面,spring的AbstractAutowireCapableBeanFactory当中,在initializeBean和将bean放入一级缓存之间,有这么一段容易被忽视的代码,用于把控最终的循环引用结果正确性:

//是否允许提前暴露,可以理解为是否允许循环引用
if (earlySingletonExposure) {//遍历一到三级缓存,拿到的beanObject earlySingletonReference = getSingleton(beanName, false);//如果缓存中的对象不为空if (earlySingletonReference != null) {//exposedObject是执行了initializeBean之后的对象,bean是通过构造器创建的原始对象//如果两者相等,则将exposedObject设置为缓存中的对象if (exposedObject == bean) {exposedObject = earlySingletonReference;}   //如果两者不是同一个对象,并且不允许直接注入原生对象(默认false),且当前beanName有被其他的bean所依赖else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {//则获取所有依赖了该beanName的对象String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {//如果这个对象已经处于一级缓存当中,则添加到actualDependentBeans,即依赖该对象的bean是一个走完了整个流程,不会再有机会回炉重做的beanif (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}//最后判断actualDependentBeans是否为空,不为空就抛循环引用的异常if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] 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 " +"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");}}}
}
  • 我们结合这段代码来分析@Async 循环引用场景:
    • 先看第4行,首先这个时候肯定还没进入一级缓存,而我们知道@Async在 getEarlyBeanReference 中并没有执行代理,因此第4行获取到的 earlySingletonReference 是messageService的原始对象
    • 进入第9行,判断exposedObject == bean,由于@Async的代理过程发生在initializeBean中, 因此exposedObject是代理对象,而bean是通过构造器直接实例化的原始对象,因此肯定不相等
    • 进入第12行,allowRawInjectionDespiteWrapping默认为false,而messageService是被TaskService所引用的,因此 hasDependentBean(beanName)为true ,会进入14行代码块
    • 重点是判断18行的!removeSingletonIfCreatedForTypeCheckOnly(dependentBean),该方法代码为:
protected boolean removeSingletonIfCreatedForTypeCheckOnly(String beanName) {
//如果不是已经完全创建好的bean,就返回true,否则返回falseif (!this.alreadyCreated.contains(beanName)) {removeSingleton(beanName);return true;}else {return false;}
}

这里就要回到场景复现时提到的:

3、注意MessageService和TaskService在同一个包内,并且order为默认,因此会先扫描MessageService再扫描TaskService。

    • 由于messageService先被扫描,因此会在messageService的populateBean当中,执行TaskService的实例化过程,而TaskService此时并不知道messageService是一个需要代理的类,因此将一个未代理的messageService注入之后,心安理得地执行了initializeBean以及后续的初始化操作,然后标记为成功创建并装入一级缓存。
    • 也就是说,此时spring判断TaskService是一个已经完全实例化并初始化完成的对象。因此removeSingletonIfCreatedForTypeCheckOnly方法会返回false,则18行返回的是true,所以TaskService会被加入到actualDependentBeans当中,最终抛出BeanCurrentlyInCreationException异常
    • 简单来说,spring认为如果一个bean在initializeBean前后不一致,并且一个已经完全初始化的beanA注入了这个未完全初始化的beanB,在spring的流程中beanA就再也没有机会改变注入的依赖了,所以会抛异常。
    • 而如果先实例化TaskService再实例化MessageService,就不会有这个问题(不信可以将TaskService改成ATaskService试试),因为如果在实例化TaskService的时候没有发现提前暴露出来的MessageService,就会专注于创建MessageService的过程,实例化并初始化完成后才会回到TaskService并将MessageService注入

为什么@Lazy可以解决这个问题

@Lazy 被大多数人理解为:当使用到的时候才会加载这个类。

这个也算是spring希望我们看到的,但是这个描述实际上不完全准确。举个例子:


@Service
public class TaskService {@Resource    @Lazyprivate MessageService messageService;  public void shit(){System.out.println();}
}
    • 这里在messageService属性上面加了@Lazy。在实例化TaskService,并populateBean的时候,在 CommonAnnotationBeanPostProcessor 的 getResourceToInject方法中, spring发现messageService被@Lazy注解修饰,便会将其包装成一个代理对象:即创建一个TargetSource,重写getTarget方法,返回的是 CommonAnnotationBeanPostProcessor 里的 getResource(beanName)方法(方法体中的逻辑,可以理解为从工厂的三层缓存中获取对象)。也就是说,注入给TaskService的是一个MessageService的代理对象(这是本文出现的第三种代理场景)。
    • 而spring在实例化MessageService的时候,不会管他是否是由@Lazy 修饰的,只会将其当做一个普通的bean去创建,成功后就会放入一级缓存(所以严格来讲,不能说是“使用到了再去加载”)。
    • 容器启动完成后,TaskService在需要使用messageService的方法时,会执行代理对象的逻辑,获取到TargetSource,调用getResource从三层缓存中获取messageService的真实对象,由于messageService此时已经被spring完整地创建好了,处于一级缓存singletonObjects当中,因此拿到之后可以放心使用。

致谢

感谢@挡不住的牛味浓 的分享。原文链接

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

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

相关文章

【linux】服务器Ubuntu20.04安装cuda11.8教程

【linux】服务器Ubuntu20.04安装cuda11.8教程 文章目录 【linux】服务器Ubuntu20.04安装cuda11.8教程到官网找到对应版本下载链接终端操作cudnn安装到官网下载下载后解压进入解压后的目录&#xff1a;将头文件复制到 /usr/local/cuda/include/ 目录&#xff1a;将库文件复制到 …

语音语言模型最新综述! 关于GPT-4o背后技术的尝试

近期,大型语言模型(LLMs)在生成文本和执行各种自然语言处理任务方面展现出了卓越的能力,成为了强大的AI驱动语言理解和生成的基础模型。然而&#xff0c;仅依赖于基于文本模态的模型存在显著局限性。这促使了基于语音的生成模型的发展,使其能够更自然、直观地与人类互动。 为了…

在银河麒麟系统中Qt连接达梦数据库

解决在银河麒麟系统中使用Qt连接达梦数据库提示&#xff1a;project Error library odbc is not defined问题 一、编译ODBC 下载解压unixODBC&#xff08;http://www.unixodbc.org/unixODBC-2.3.1.tar.gz&#xff09; 打开终端&#xff0c;切换到unixODBC-2.3.1目录下&#x…

海螺 2.27.1 |AI生成视频 AI音乐 语音通话

嗨&#xff01;我是小海螺&#xff0c;你的AI智能伙伴&#xff0c;帮助你学习工作效率加倍&#xff01;我无所不知&#xff0c;又像朋友陪你左右&#xff0c;遇到问题&#xff0c;就问我吧。我所使用的技术&#xff0c;是MiniMax公司自研的万亿参数MoE大模型。我们希望能与用户…

【北京迅为】itop-龙芯2k1000开发指南Linux基础入门vim 编辑器

【北京迅为】itop-龙芯2k1000开发指南Linux基础入门vim 编辑器 龙芯2K1000处理器集成2个64位GS264处理器核&#xff0c;主频1GHz&#xff0c;以及各种系统IO接口&#xff0c;集高性能与高配置于一身。支持4G模块、GPS模块、千兆以太网、16GB固态硬盘、双路UART、四路USB、WIFI蓝…

沈阳乐晟睿浩科技有限公司抖音小店领域的强者

在当今数字化浪潮的推动下&#xff0c;电子商务以其便捷性、高效性和广泛的覆盖面&#xff0c;成为了推动经济发展的新引擎。而抖音小店&#xff0c;作为短视频平台上的新兴电商形态&#xff0c;更是凭借其庞大的用户基础、精准的内容推送机制以及独特的购物体验&#xff0c;迅…

使用query-string库出现错误Module parse failed: Unexpected token

环境 node v12query-string 9.1.0 报错信息 Failed to compile../node_modules/query-string/base.js 350:14 Module parse failed: Unexpected token (350:14) File was processed with these loaders:* ./node_modules/babel-loader/lib/index.js You may need an additio…

【Multisim14.0正弦波>方波>三角波】2022-6-8

缘由有没有人会做啊Multisim14.0-其他-CSDN问答参考方波、三角波、正弦波信号产生 - 豆丁网

echarts给Y轴的不同轴线设置不同的颜色的样式

官方文档 option {xAxis: {type: category,data: [Mon, Tue, Wed, Thu, Fri, Sat, Sun]},yAxis: {type: value},series: [{data: [150, 230, 224, 218, 135, 147, 260],type: line,}] }; 效果&#xff1a; 需要添加参数markLine option {xAxis: {type: category,data: [M…

Mycat2安装配置

安装配置 安装 目前Mycat2下载地址已经不可访问&#xff0c;安装包可从参考资料[1]获取 下载后解压zip文件&#xff0c;将jar放在lib目录下 编辑配置文件 编辑conf文件夹下的prototypeDs.datasource.json 更改数据库相关信息 启动 windows环境下启动Mycat 以管理员身份运行…

威胁 Windows 和 Linux 系统的新型跨平台勒索软件:Cicada3301

近年来&#xff0c;网络犯罪世界出现了新的、日益复杂的威胁&#xff0c;能够影响广泛的目标。 这一领域最令人担忧的新功能之一是Cicada3301勒索软件&#xff0c;最近由几位网络安全专家进行了分析。他们有机会采访了这一危险威胁背后的勒索软件团伙的成员。 Cicada3301的崛…

微信小程序中关闭默认的 `navigationBar`,并使用自定义的 `nav-bar` 组件

要在微信小程序中关闭默认的 navigationBar&#xff0c;并使用自定义的 nav-bar 组件&#xff0c;你可以按照以下步骤操作&#xff1a; 1. 关闭默认的 navigationBar 在你的页面的配置文件 *.json 中设置 navigationBar 为 false。你需要在页面的 JSON 配置文件中添加以下代码…

C#从零开始学习(用户界面)(unity Lab4)

这是书本中第四个unity Lab 在这次实验中,将学习如何搭建一个开始界面 分数系统 点击球,会增加分数 public void ClickOnBall(){Score;}在OneBallBehaviour类添加下列方法 void OnMouseDown(){GameController controller Camera.main.GetComponent<GameController>();…

js纯操作dom版购物车(实现购物车功能)

代码&#xff1a; <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>Document</title>&l…

jenkins国内插件源

Jenkins是一个开源的持续集成和持续部署&#xff08;CI/CD&#xff09;工具, 可以大大减轻部署的工作量, 但是jenkins作为一个国外的软件, 在国内下载插件会很麻烦, 因此我们可以将其换为国内源 更换步骤 替换国内插件下载地址 以linux为例 首先, jenkins初始化完成之后不会…

如何制作一台自己想要的无人机?无人机改装调试技术详解

制作一台符合个人需求的无人机并对其进行改装调试&#xff0c;是一个既具挑战性又充满乐趣的过程。以下是从设计、选购材料、组装、调试到改装的详细步骤&#xff1a; 一、明确需求与设计 1. 明确用途与性能要求&#xff1a; 确定无人机的使用目的&#xff0c;如航拍、比赛、…

推荐一款功能强大的数据备份工具:Iperius Backup Full

Iperius Backup是一款非常灵活而且功能强大的数据备份工具&#xff0c;程序可以非常好的保护您的文件和数据的安全。支持DAT备份、LTO备份、NAS备份、磁带备份、RDX驱动器、USB备份、并且支持zip压缩和军事级别的AES 256位数据加密技术! 主要特色 云备份 Iperius可以自动地发…

.net 根据html的input type=“week“控件的值获取星期一和星期日的日期

初始化 "week" 控件值&#xff1a; //MVC部分 public ActionResult WeeklyList() {int weekNo new GregorianCalendar().GetWeekOfYear(System.DateTime.Now, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Sunday);string DefaultWeek DateTime.No…

代码随想录算法训练营Day39 | 卡玛网-46.携带研究材料、416. 分割等和子集

目录 卡玛网-46.携带研究材料 416. 分割等和子集 卡玛网-46.携带研究材料 题目 卡玛网46. 携带研究材料&#xff08;第六期模拟笔试&#xff09; 题目描述&#xff1a; 小明是一位科学家&#xff0c;他需要参加一场重要的国际科学大会&#xff0c;以展示自己的最新研究成…

论文速读:YOLO-G,用于跨域目标检测的改进YOLO(Plos One 2023)

原文标题&#xff1a;YOLO-G: Improved YOLO for cross-domain object detection 中文标题&#xff1a;YOLO-G&#xff1a;用于跨域目标检测的改进YOLO 论文地址&#xff1a; 百度网盘 请输入提取码 提取码&#xff1a;z8h7 代码地址&#xff1a; GitHub - airy975924806/yolo…