【Spring之依赖注入】2. Spring处理@Async导致的循环依赖失败问题

使用异步@Async注解后导致的循环依赖失败详解

    • 1 问题复现
      • 1.1 配置类
      • 1.2 定义Service
      • 1.3 定义Controller
      • 1.4 启动springboot报错
    • 2.原因分析:看@Async标记的bean注入时机
      • 2.1 循环依赖生成过程
      • 2.2 自检程序 doCreateBean方法
    • 3.解决方案
      • 3.1 懒加载@Lazy
        • 3.1.1 将@Lazy写到A类的b成员上边
        • 3.1.2 将@Lazy写到B类的a成员上边
        • 3.1.3 原理分析
      • 3.2 不要让@Async的Bean参与循环依赖
      • 3.3 allowRawInjectionDespiteWrapping设置为true
    • 4. 扩展
      • 4.1 @Transactional注解为什么不会导致启动失败

我们知道Spring内部可以解决循环依赖的问题,但Spring的异步(@Async)会使得循环依赖失败。本文介绍其原因和解决方案。

1 问题复现

1.1 配置类

定义配置类,并添加@EnableAsync注解以启用异步功能。目的:就是使用我们自定义的线程池来进行异步执行
如下:

AsyncConfig类 是一个Spring配置类,用于定义和管理异步任务执行的配置。其中包含了Bean的定义和初始化。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;@EnableAsync
@Configuration
public class AsyncConfig {@Bean("asyncExecutor")public Executor asyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();// 设置核心线程数executor.setCorePoolSize(50);// 设置最大线程数executor.setMaxPoolSize(200);// 配置队列大小executor.setQueueCapacity(Integer.MAX_VALUE);// 设置线程活跃时间(秒)executor.setKeepAliveSeconds(60);// 设置默认线程名称executor.setThreadNamePrefix("THREAD-ASYNC");// 等待所有任务结束后再关闭线程池executor.setWaitForTasksToCompleteOnShutdown(true);//执行初始化executor.initialize();return executor;}
}

解析:

# 解析一下 asyncExecutor() 方法:
@Bean("asyncExecutor")1.这个注解表示该方法将返回一个对象,这个对象将被注册到Spring的应用上下文中作为一个Bean,并且该Bean的名称是 asyncExecutor。
2.方法最后返回了配置好的 ThreadPoolTaskExecutor 对象,这个对象将被注册为Spring应用上下文中的一个Bean,名为 asyncExecutor。
在定义了这个配置类之后,你就可以在Spring的其他组件中通过 @Autowired@Resource 注解来注入这个 Executor Bean,并使用它来执行异步任务。
3.同时,你也可以在方法上使用 @Async("asyncExecutor") 注解来指定使用 asyncExecutor 线程池来执行该方法。

1.2 定义Service

使用循环依赖

package com.dlkhs.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;@Service
public class A {@Autowiredprivate B b;@Async("asyncExecutor")public void print() {System.out.println("Hello World");}
}
package com.dlkhs.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class B {@Autowiredprivate A a;
}

1.3 定义Controller

package com.dlkhs.controller;import com.knife.service.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class HelloController {@Autowiredprivate A a;@GetMapping("/test")public String test() {a.print();return "测试循环依赖的异步使用:成功";}
}

1.4 启动springboot报错

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Bean with name 'a' has been injected into other beans [b] 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:624) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]

2.原因分析:看@Async标记的bean注入时机

我们从源码的角度来看一下被@Async标记的bean是如何注入到Spring容器里的。在我们开启@EnableAsync注解之后代表可以向Spring容器中注入AsyncAnnotationBeanPostProcessor,它是一个后置处理器,我们看一下他的类图。

在这里插入图片描述

真正创建代理对象的代码在AbstractAdvisingBeanPostProcessor中的postProcessAfterInitialization方法中,看核心逻辑代码:

// 这个map用来缓存所有被postProcessAfterInitialization这个方法处理的bean
private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);// 这个方法主要是为打了@Async注解的bean生成代理对象
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {// 这里是重点,这里返回trueif (isEligible(bean, beanName)) {// 工厂模式生成一个proxyFactoryProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);if (!proxyFactory.isProxyTargetClass()) {evaluateProxyInterfaces(bean.getClass(), proxyFactory);}// 切入切面并创建一个代理对象proxyFactory.addAdvisor(this.advisor);customizeProxyFactory(proxyFactory);return proxyFactory.getProxy(getProxyClassLoader());}// No proxy needed.return bean;
}
protected boolean isEligible(Class<?> targetClass) {// 首次从eligibleBeans这个map中一定是拿不到的Boolean eligible = this.eligibleBeans.get(targetClass);if (eligible != null) {return eligible;}// 如果没有advisor,也就是切面,直接返回falseif (this.advisor == null) {return false;}// 这里判断AsyncAnnotationAdvisor能否切入,因为我们的bean是打了@Aysnc注解,这里是一定能切入的,最终会返回trueeligible = AopUtils.canApply(this.advisor, targetClass);this.eligibleBeans.put(targetClass, eligible);return eligible;
}

至此方法上有@Aysnc注解的bean就创建完成了,结果是生成了一个代理对象

2.1 循环依赖生成过程

正确的循环依赖

  • beanA开始初始化,beanA实例化完成后给beanA的依赖属性beanB进行赋值;
  • beanB开始初始化,beanB实例化完成后给beanB的依赖属性beanA进行赋值;

但是我们上述的例子有@Async注解:所以属于不正确的循环依赖

  • 因为beanB是支持循环依赖的,所以可以在earlySingletonObjects中可以拿到beanB的早期的引用,但是因为beanA所在的方法上有@Aysnc注解,所以并不能在earlySingletonObjects中可以拿到早期的引用;
  • 接下来执行执行initializeBean(Object existingBean, String beanName)方法,这里beanB可以正常实例化完成,但是因为beanA上有@Aysnc注解,所以向Spring IOC容器中增加了一个代理对象,也就是说beanBbeanA并不是一个原始对象,而是一个代理对象

总结:B实例完成了实例化(也就是说B里面的属性A是原始对象),但A实例却是个代理对象,所以导致B实例里面的是属性A不是最终放入到容器的实例对象;所以在执行自检程序之后,就报错了;

2.2 自检程序 doCreateBean方法

接下来进行执行doCreateBean方法时对进行检测

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)){String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet< (dependentBeans.length);
// 重点在这里,这里会遍历所有依赖的bean,如果beanB依赖beanA和缓存中的beanA不相等
// 也就是说beanB本来依赖的是一个原始对象beanA,但是这个时候发现beanA是一个代理对象,就会增加到actualDependentBeansfor (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}// 发现actualDependentBeans不为空,就发生了我们最开始的错误if (!actualDependentBeans.isEmpty()) {//...省略throw new BeanCurrentlyInCreationExceptionreturn exposedObject;}

不一致情况:也就是说beanB本来依赖的是一个原始对象beanA,但是这个时候发现beanA是一个代理对象

执行自检程序:由于allowRawInjectionDespiteWrapping默认值是false,表示不允许上面不一致的情况发生,就报错了;(若一致则会被赋值为true)

3.解决方案

一共有三种解决方案:

  • 懒加载:使用@Lazy或者@ComponentScan(lazyInit = true ) 【注:后者不建议使用】
  • 不让@Async的方法有循环依赖
  • 将allowRawInjectionDespiteWrapping设置为true【非常不建议】

3.1 懒加载@Lazy

使用@Lazy。不建议使用@ComponentScan(lazyInit = true),因为它是全局的,容易产生误伤。

两种实例写法

  • 法1. A类注入的b成员上边写@Lazy
  • 法2: B类注入的a成员上边写@Lazy
3.1.1 将@Lazy写到A类的b成员上边
package com.dlkhs.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;@Service
public class A {@Lazy@Autowiredprivate B b;@Asyncpublic void print() {System.out.println("Hello World");}
}
3.1.2 将@Lazy写到B类的a成员上边
package com.dlkhs.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;@Component
public class B {@Lazy@Autowiredprivate A a;
}
3.1.3 原理分析

以@Lazy放到A类注入的b成员上边为例:

package com.dlkhs.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;@Service
public class A {@Lazy@Autowiredprivate B b;@Asyncpublic void print() {System.out.println("Hello World");}
}

假设 A 先加载,在创建 A 的实例时,会触发依赖属性 B 的加载,在加载 B 时发现它是一个被 @Lazy 标记过的属性。那么就不会去直接加载 B,而是产生一个代理对象注入到了 A 中,这样 A 就能正常的初始化完成放入一级缓存了。

B 加载时,将前边生成的B代理对象取出,再注入 A 就能直接从一级缓存中获取到 A,这样 B 也能正常初始化完成了。所以,循环依赖的问题就解决了。

3.2 不要让@Async的Bean参与循环依赖

通俗说就是,不要让有参与循环依赖对象类里含有异步执行的方法;

若当前对象必须要有循环依赖的话,则考虑把该异步执行的方法移植到相关serviceimpl类外面;

即:新建一个类,加上@Service注解,然后把之前要异步执行的方法和注入的循环依赖对象,放进去即可;

3.3 allowRawInjectionDespiteWrapping设置为true

不建议使用!!!

配置后,容器启动虽然不报错了。但是:Bean A的@Aysnc方法不起作用了。因为Bean B里面依赖的a是个原始对象,所以它不能执行异步操作(即使容器内的a是个代理对象)

4. 扩展

4.1 @Transactional注解为什么不会导致启动失败

  • 疑惑:同为创建动态代理对象,同作为注解标注在类/方法上,为何@Transactional就不会出现这种启动报错呢?

  • 原因:它们代理的创建的方式不同;

    • @Transactional创建代理的方式:使用自动代理创建器InfrastructureAdvisorAutoProxyCreator(AbstractAutoProxyCreator的子类),它实现了getEarlyBeanReference()方法从而很好的对循环依赖提供了支持;
    • @Async创建代理的方式:使用AsyncAnnotationBeanPostProcessor单独的后置处理器。它只在一处postProcessAfterInitialization()实现了对代理对象的创建,因此若它被循环依赖了,就会报错。

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

FANUC机器人工具坐标偏移的用法

一、工具坐标偏移的使用场景 在机器人位置不改变的情况下&#xff0c;工业机器人使用默认工具坐标系示教的一系列运动点位&#xff0c;要保持原本点位位置不变的情况下&#xff0c;改变机器人工具坐标的参数&#xff0c;就要用到机器人坐标转化的功能。在FANUC机器人上体现为机…

单链表经典oj题(2)

前言 这次将要把剩下的oj题将以图解和自己的理解把它讲解完&#xff0c;希望对大家有所帮助&#xff0c;这次的讲解也是干货 第一题 21. 合并两个有序链表 - 力扣&#xff08;LeetCode&#xff09; ok这次就简单点&#xff0c;大家自己去看题目了 将两个升序链表合并为一个…

流量分析(一)

数据库类流量分析 MySQL流量 常规操作&#xff0c;查找flag ctfhub{} 注意要选择字符集 Redis流量 查找ctfhub结果没找到 尝试把其变成十六进制继续进行查找 看到了前半段flag 接着往下看 找到了后半段的flag MongoDB流量 还是一样查找ctfhub 字符串没找到 转成十六进制也没…

《软件方法(下)》8.3.2.2 警惕拼凑泛化(202405更新)

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 8.3 建模步骤C-2 识别类的关系 8.3.2 识别泛化关系 8.3.2.1 识别泛化的思路 &#xff08;3&#xff09;自上而下&#xff08;从一般到特殊&#xff09; 如图8-92所示&#xff0c;这…

目标检测——道路检测数据集

引言 亲爱的读者们&#xff0c;您是否在寻找某个特定的数据集&#xff0c;用于研究或项目实践&#xff1f;欢迎您在评论区留言&#xff0c;或者通过公众号私信告诉我&#xff0c;您想要的数据集的类型主题。小编会竭尽全力为您寻找&#xff0c;并在找到后第一时间与您分享。 …

鹦鹉优化算法原理及代码实现

鹦鹉(Pyrrhura Molinae)表现出四种不同的行为特征&#xff1a;觅食、停留、交流和对陌生人的恐惧。这些行为(如图1所示)在现实环境中构成了我们设计PO动机的基础。 觅食&#xff1a;驯化的鹦鹉(Pyrrhura Molinae)的觅食行为令人着迷&#xff0c;因为个体选择在食物丰富的小群体…

第十三届蓝桥杯决赛(国赛)真题 Java A 组【原卷】

文章目录 发现宝藏【考生须知】试题 A: 火柴棒数字试题 B: 小蓝与钥匙试题 C: 内存空间试题 D: 斐波那契数组试题 E: 交通信号试题 F: 数组个数试题 G: 六六大顺试题 H : \mathrm{H}: H: 选素数试题 I: 图书借阅试题 J \mathrm{J} J : 括号序列树 发现宝藏 前些天发现了一个…

Web服务器--虚拟主机配置

实验1&#xff1a;建立两个基于ip地址访问的网站&#xff0c;要求如下 该网站ip地址的主机位为100&#xff0c;设置DocumentRoot为/www/ip/100&#xff0c;网页内容为&#xff1a;this is 100。 该网站ip地址主机位为200&#xff0c;设置DocumentRoot为/www/ip/200&#xff0c…

数据结构--顺序表和链表的区别

顺序表和链表之间各有优劣&#xff0c;我们不能以偏概全&#xff0c;所以我们在使用时要关注任务的注重点&#xff0c;以此来确定我们要使用两者中的哪一个。 不同点&#xff1a; 存储空间上&#xff1a; 顺序表在物理结构上是一定连续的&#xff0c;而链表(这里以带头双向循环…

凸优化理论学习一|最优化及凸集的基本概念

文章目录 一、优化问题&#xff08;一&#xff09;数学优化&#xff08;二&#xff09;凸优化 二、凸集&#xff08;一&#xff09;一些标准凸集&#xff08;二&#xff09;保留凸性的运算&#xff08;三&#xff09;正常锥和广义不等式&#xff08;四&#xff09;分离和支撑超…

网络基础-ICMP协议

ICMP&#xff08;Internet Control Message Protocol&#xff0c; Internet控制消息协议&#xff09; ICMP协议是IP协议的辅助协议&#xff0c;用于在IP网络上发送控制消息&#xff0c;它通常被用于诊断网络故障、执行网络管理任务以及提供一些错误报告&#xff1b;对于收集各…

彩虹聚合DNS管理系统

聚合DNS管理系统可以实现在一个网站内管理多个平台的域名解析&#xff0c;目前已支持的域名平台有&#xff1a;阿里云、腾讯云、华为云、西部数码、CloudFlare。本系统支持多用户&#xff0c;每个用户可分配不同的域名解析权限&#xff1b;支持API接口&#xff0c;支持获取域名…

MySQL的表级锁

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;面经 ⛺️稳中求进&#xff0c;晒太阳 表级锁 介绍 对于表锁&#xff0c;分为两类&#xff1a; 表共享读锁表独占写锁 语法 1. 加锁&#xff1a;lock tables 表名... read/write 2.…

PHP 提取数组中的特定的值

需求&#xff1a; 前端展示&#xff1a; &#xff08;1&#xff09;之前的页面&#xff1a; &#xff08;2&#xff09;修改后的页面&#xff1a; 之前接口返回的数据 &#xff1a; 解决办法&#xff1a;提取tags 中的 ’约 的数组 添加到一个新的数组中去 1&#xff1a;一开…

CSAPP笔记——第一章计算机系统漫游

hello&#xff0c;你好鸭&#xff0c;我是Ethan&#xff0c;很高兴你能来阅读&#xff0c;昵称是希望自己能不断精进&#xff0c;向着优秀程序员前行!&#x1f4aa;&#x1f4aa;&#x1f4aa; 目前博客主要更新Java系列、项目案例、计算机必学四件套等。✔️✔️✔️ 人生之败…

OpenCV中的模块:点云配准

点云配准是点云相关的经典应用之一。配准的目的是估计两个点云之间位姿关系从而完成两者对应点之间的对齐/对应,因而在英文中又叫“align”、“correspondence”。笔者曾经是基于OpenCV进行三维重建的,并且从事过基于深度学习的6DoF位置估计等工作。在这些工作中,除了重建点…

深度学习课程论文精读——ESRGAN

目录 1.研究概述 2.论文创新 2.1 改进生成器的网络框架 2.2 改进判别器 2.3 改进感知损失 2.4 网络插值 3.实验 3.1 评价指标 3.2 训练细节 3.3 对比实验 3.4 消融实验 3.5 网络插值 4.总结 5.阅读参考 文章标题&#xff1a;《ESRGAN: Enhanced Super-Resolution…

SDXL-ControlNet模型MistoLine:引领高精度图像生成的革新高质量图像模型

在数字艺术的浩瀚星空中&#xff0c;MistoLine犹如一颗璀璨的新星&#xff0c;以其对SDXL-ControlNet技术的深度整合&#xff0c;展示了对多种线稿类型的非凡适应能力&#xff0c;并在高精度图像生成领域树立了新的标杆。 GitHub&#xff1a;https://github.com/TheMistoAI/Mi…

Web实时通信的学习之旅:轮询、WebSocket、SSE的区别以及优缺点

文章目录 一、通信机制1、轮询1.1、短轮询1.2、长轮询 2、Websocket3、Server-Sent Events 二、区别1、连接方式2、协议3、兼容性4、安全性5、优缺点5.1、WebSocket 的优点&#xff1a;5.2、WebSocket 的缺点&#xff1a;5.3、SSE 的优点&#xff1a;5.4、SSE 的缺点&#xff1…

代码随想录day62 | 单调栈P2 | ● 503. ● 42.

终于来到了大名鼎鼎的接雨水, 舍友的23年暑期面试就是接雨水 XD 503.下一个更大元素II 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[0] &#xff09;&#xff0c;返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是…