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) {
    //遍历一到三级缓存,拿到的bean
   Object 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是一个走完了整个流程,不会再有机会回炉重做的bean
            if (!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,否则返回false
   if (!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    
    @Lazy
    private 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当中,因此拿到之后可以放心使用。

致谢

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用 NumPy 和 Matplotlib 实现交互式数据可视化

使用 NumPy 和 Matplotlib 实现交互式数据可视化 在数据分析中&#xff0c;交互式可视化可以更好地帮助我们探索和理解数据。虽然 Matplotlib 是静态绘图库&#xff0c;但结合一些技巧和 Matplotlib 的交互功能&#xff08;widgets、event handlers&#xff09;&#xff0c;我…

Git创建和拉取项目分支的应用以及Gitlab太占内存,如何配置降低gitlab内存占用进行优化

一、Git创建和拉取项目分支的应用 1. 关于git创建分支&#xff0c; git创建分支&#xff0c;可以通过git管理平台可视化操作创建&#xff0c;也可以通过git bash命令行下创建&#xff1a; A. 是通过git管理平台创建&#xff1a; 进入gitlab管理平台具体的目标项目中&#xff…

mac电脑设置chrome浏览器语言切换为日语英语等不生效问题

在chrome中设置了语言&#xff0c;并且已经置顶了&#xff0c;但是不生效&#xff0c;在windows上直接有设置当前语言为chrome显示语言&#xff0c;但是mac上没有。 解决办法 在系统里面有一个单独给chrome设置语言的&#xff1a; 单独给它设定成指定的语言&#xff0c;然后重…

Find My平板键盘|苹果Find My技术与键盘结合,智能防丢,全球定位

‌平板键盘的主要用途包括提高输入效率、支持轻量化办公、提供丰富的文本编辑功能以及快捷操作。相比于直接在屏幕上打字&#xff0c;使用键盘可以显著提升输入速度&#xff0c;减少输入错误&#xff0c;特别是对于需要大量文字输入的场景&#xff0c;如写作、记录笔记等‌。平…

如何在算家云搭建GPT-SOVITS(语音转换)

一、模型介绍 GPT-SOVITS是一款强大的小样本语音转换和文本转语音 WebUI工具。它集成了声音伴奏分离、自动训练集分割、中文ASR和文本标注等辅助工具。 具有以下特征&#xff1a; 零样本 TTS&#xff1a; 输入 5 秒的声音样本并体验即时文本到语音的转换。少量样本 TTS&…

【linux网络编程】| 网络基础 | 解析IP与Mac地址的区别

前言&#xff1a;本节内容讲解一些网络基础相关的知识点&#xff0c; 不涉及网络代码&#xff01;同样的本节内容是作为前一篇的补充知识点&#xff0c; 前一篇文章地址&#xff1a;【linux网络编程】 | 网络基础Ⅰ| 认识网络-CSDN博客&#xff0c;本篇文章内容较少&#xff0c…

Unreal Engine5安装Niagara UI Renderer插件

系列文章目录 文章目录 系列文章目录前言一、如何下载安装Niagara UI Renderer插件 前言 在2024.10.24号的今天发现unreal engine官网已经没有虚幻商城了&#xff0c;取而代之的是FAB ‌虚幻商城已经停止运营&#xff0c;Epic Games推出了新的数字资产商店FAB。‌ Epic Games…

重构商业生态:DApp创新玩法与盈利模式的深度剖析

随着区块链技术的发展&#xff0c;DApp&#xff08;去中心化应用&#xff09;正在从实验走向成熟。DApp以去中心化、透明性和不可篡改性为基础&#xff0c;结合智能合约&#xff0c;逐步改变传统商业运作模式&#xff0c;创造新的市场生态。本文将从DApp的独特优势、创新玩法和…

解决Docker部署ocserv的时候,遇到客户端经常重连问题

本章教程,主要介绍在Docker部署ocserv的时候,客户端连接的时候,会出现每4分钟重连问题。 解决办法 这是ocserv的核心配置文件ocserv.conf,它通常是在/etc/ocserv/目录下,主要影响每4分钟重连的参数是auth-timeout,单位是秒,原本这个默认值是240,经过单位换算,恰巧等于…

AI赋能R-Meta分析核心技术:从热点挖掘到高级模型、助力高效科研与论文发表

Meta分析是针对某一科研问题&#xff0c;根据明确的搜索策略、选择筛选文献标准、采用严格的评价方法&#xff0c;对来源不同的研究成果进行收集、合并及定量统计分析的方法&#xff0c;现已广泛应用于农林生态&#xff0c;资源环境等方面&#xff0c;成为Science、Nature论文的…

MySQL 初阶——多版本控制 MVCC

一、版本链&#xff08;undo 日志&#xff09; a. 什么是版本链 版本链就是一条以事务为节点的单链表。其 next 指针指向前一个版本的事务。 b. 版本链的增删 当一个事务被完成时&#xff0c;这个事务就会被加入到版本链里去&#xff1b;当要回滚时&#xff0c;版本链就会删…

微服务网关Zuul

一、Zuul简介 Zuul是Netflix开源的微服务网关&#xff0c;包含对请求的路由和过滤两个主要功能。 1&#xff09;路由功能&#xff1a;负责将外部请求转发到具体的微服务实例上&#xff0c;是实现外部访问统一入口的基础。 2&#xff09;过滤功能&#xff1a;负责对请求的过程…

多元线性回归【正规方程/sklearn】

多元线性回归【正规方程/sklearn】 1. 基本概念1.1 线性回归1.2 一元简单线性回归1.3 最优解1.4 多元线性回归 2. 正规方程求最优解2.1 线性回归的损失函数&#xff08;最小二乘法&#xff09;2.2 推导正规方程2.3 正规方程练习2.4 使用sklearn计算多元线性方程2.5 凸函数 3. 线…

masm 6.15下载及DOSBox自动挂载

这里写目录标题 工具参考masm下载准备自动挂载 工具 系统&#xff1a;Windows 11 应用&#xff1a;DOSBox 0.74-3 masm 6.15文件 参考 DOSBox 下载安装教程&#xff1a;本人写的《DOSBox下载安装&#xff08;Windows系统 DOSBox 0.74-3&#xff09;》 https://blog.csdn.ne…

STM32-Modbus协议(一文通)

Modbus协议原理 RT-Thread官网开源modbus RT-Thread官方提供 FreeModbus开源。 野火有移植的例程。 QT经常用 libModbus库。 Modbus是什么&#xff1f; Modbus协议&#xff0c;从字面理解它包括Mod和Bus两部分&#xff0c;首先它是一种bus&#xff0c;即总线协议&#xff0c;和…

监督学习之逻辑回归

逻辑回归&#xff08;Logistic Regression&#xff09; 逻辑回归是一种用于二分类&#xff08;binary classification&#xff09;问题的统计模型。尽管其名称中有“回归”二字&#xff0c;但逻辑回归实际上用于分类任务。它的核心思想是通过将线性回归的输出映射到一个概率值…

如何限制电脑软件的安装?

1.修改注册表&#xff08;需谨慎操作&#xff0c;建议备份注册表&#xff09;&#xff1a; 打开“运行”对话框&#xff0c;输入 regedit 打开注册表编辑器。 导航到 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer。 创建新的DWORD值&…

2024双11买什么东西比较好?双十一购物清单,双十一囤货清单排名

今年双十一好价确实多&#xff0c;一方面是年底促销&#xff0c;一方面国补也很给力&#xff0c;种草很久的产品趁着这个时间下单最好不过了&#xff0c;不知道各位有哪些心水好物&#xff0c;我今年入手了不少生活用品和数码类产品&#xff0c;下文就挑选几款我觉得特别值得入…

基于Multisim的四人智力竞赛抢答器设计与仿真

1&#xff09;设计任务 设计一台可供 4 名选手参加比赛的智力竞赛抢答器。 用数字显示抢答倒计时间&#xff0c;由“9”倒计到“0”时&#xff0c;无人抢答&#xff0c;蜂鸣器连续响 1 秒。选手抢答时&#xff0c;数码显示选手组号&#xff0c;同时蜂鸣器响 1 秒&#xff0c;倒…

使用Prometheus对微服务性能自定义指标监控

背景 随着云计算和容器化技术的不断发展&#xff0c;微服务架构逐渐成为现代软件开发的主流趋势。微服务架构将大型应用程序拆分成多个小型、独立的服务&#xff0c;每个服务都可以独立开发、部署和扩展。这种架构模式提高了系统的可伸缩性、灵活性和可靠性&#xff0c;但同时…