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…

Python 异步编程:使用 `asyncio.to_thread` 和 `asyncio.Queue` 处理任务队列

Python 异步编程&#xff1a;使用 asyncio.to_thread 和 asyncio.Queue 处理任务队列 1. 什么是 asyncio.to_thread&#xff1f;2. 什么是 asyncio.Queue&#xff1f;3. 示例代码&#xff1a;使用 asyncio.to_thread 和 asyncio.Queue 处理任务队列示例代码代码解释运行结果 4.…

海螺 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蓝…

Ubuntu22.04 更换源

Ubuntu22.04 更换网易163源 1、编辑/etc/apt/sources.list文件 1 sudo nano /etc/apt/sources.list 2、清空文件内容&#xff0c;拷贝下列163源到文本。 1 2 3 4 5 6 7 8 deb http://mirrors.163.com/ubuntu/ jammy main restricted universe multiverse deb http://mirro…

http 请求返回307

错误的原因 发现发生临时重定向的原因是我在具体的 url 后面少加了一个 / /xxx 这个发生了临时重定向请求/xxx/ 这个请求不会 可能是因为我请求的服务器对最终请求是否添加 / 有特殊的处理,在开发中,最好由对接方实际的url请求值 HTTP 状态码 307 Temporary Redirect 是一种临时…

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

在当今数字化浪潮的推动下&#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…

React四官方文档总结一UI与交互

代码下载 React官网已经都是函数式组件文档&#xff0c;没有类组件文档&#xff0c;但是还是支持类组件这种写法。 UI 描述 组件 组件 是 React 的核心概念之一&#xff0c;它们是构建用户界面&#xff08;UI&#xff09;的基础。React 允许你将标签、CSS 和 JavaScript 组…

一个简单的Http根据规则自动路由

在日常项目中&#xff0c;有时候会根据一些规则/标识进行Http路由匹配&#xff0c;下面我实现一个简单的Http根据systemCode自动路由&#xff1b; ControllerServicePropertiesProperties类Yaml 测试路由到baidu Controller import com.example.demo.service.GatewayRoutingS…

Windows安装PM2 注意事项与错误查改

Windows安装 1. 首先应确保 node 和 npm 已经安装&#xff1a; node -v #查看 node 版本 npm -v #查看 npm 版本注意有时候即使npm安装了&#xff0c;但并没有进行配置环境变量&#xff0c;后面会将如何配置。 2. 尝试安装pm2到全局路径 运行以下命令对 PM2 进行全局安装&a…

Mycat2安装配置

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

查找算法和排序算法

文章目录 1.基本查找1.1需求一:1.2需求二: 2.二分查找3.分块查找3.1分块查找的扩展 4.冒泡排序5.选择排序6.插入排序7.快速排序(递归算法) 1.基本查找 在数组或集合中挨个元素查找需要的元素 1.1需求一: 需求:定义一个方法利用基本查找,查询某个元素是否存在 数据如下&#…

威胁 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>();…