【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 {
    @Autowired
    private 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 {
    @Autowired
    private 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 {
    @Autowired
    private 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) {
    // 这里是重点,这里返回true
    if (isEligible(bean, beanName)) {
        // 工厂模式生成一个proxyFactory
        ProxyFactory 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,也就是切面,直接返回false
    if (this.advisor == null) {
        return false;
    }
    // 这里判断AsyncAnnotationAdvisor能否切入,因为我们的bean是打了@Aysnc注解,这里是一定能切入的,最终会返回true
    eligible = 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是一个代理对象,就会增加到actualDependentBeans
        for (String dependentBean : dependentBeans) {
            if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                actualDependentBeans.add(dependentBean);
            }
        }
        // 发现actualDependentBeans不为空,就发生了我们最开始的错误
        if (!actualDependentBeans.isEmpty()) {
        //...省略
		throw new BeanCurrentlyInCreationException
		return 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
    @Autowired
    private B b;
 
    @Async
    public 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
    @Autowired
    private 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
    @Autowired
    private B b;
 
    @Async
    public 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()实现了对代理对象的创建,因此若它被循环依赖了,就会报错。

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

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

相关文章

SDL系列(一)—— 小白入门

SDL &#xff08; Simple DirectMedia Layer &#xff09; 是一套开放源代码的 跨平台多媒体开发库 &#xff0c;使用 C 语 言写成。 SDL 提供了数种控制图像、声音、输出入的函数&#xff0c;让开发者只要用相同或是相似的 代码就可以开发出 跨多个平台&#xff08; Linu…

公共字段填充(AOP的使用)

Thread是线程池,ThreadLocal是线程变量,每个线程变量是封闭的,与其它线程变量分隔开来,在sky-common下的com.sky.context包下有一个Basecontext类 public class BaseContext {//每一个上下文创建了一个线程变量,用来存储long类型的id//创建三个方法,用来设置,取用,删除idpubli…

基于微信小程序+JAVA Springboot 实现的【房屋租赁管理系统】app+后台管理系统 (内附设计LW + PPT+ 源码+ 演示视频 下载)

项目名称 项目名称&#xff1a; 基于微信小程序的房屋租赁管理系统 在本次项目开发中&#xff0c;我们成功构建了一款基于微信小程序的房屋租赁管理系统&#xff0c;旨在通过现代化信息技术提升房屋租赁服务的效率和质量。以下是对本项目的全面总结&#xff1a; 项目背景与目…

07-Fortran基础--Fortran指针(Pointer)的使用

07-Fortran基础--Fortran指针Pointer的使用 0 引言1 指针&#xff08;Poionter&#xff09;的有关内容1.1 一般类型指针1.2 数组指针1.3 派生类(type)指针1.4 函数指针 2 可运行code 0 引言 Fortran是一种广泛使用的编程语言&#xff0c;特别适合科学计算和数值分析。Fortran 9…

43k Star!推荐一款功能强大的开源笔记软件!

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一份大厂面试资料《史上最全大厂面试题》&#xff0c;Springboot、微服务、算法、数据结构、Zookeeper、Mybatis、Dubbo、linux、Kafka、Elasticsearch、数据库等等 …

【2024系统架构设计】回顾历史,查缺补漏篇 ③

前言 hello,大家好: 💡💡💡 我们一起来备考软考高级系统架构设计师吧,本专栏提供综合知识、案例科目、论文(论点和部分示例范文)等内容,包括知识点总结和记忆小妙招哦。 🚀🚀🚀 可以减少资料查找和收集的时间,提高效率,我们一起集中精力学习干货吧! 💡…

Lora训练Windows[笔记]

一. 使用kohya_ss的GUI版本&#xff08;https://github.com/bmaltais/kohya_ss.git&#xff09; 这个版本跟stable-diffusion-webui的界面很像&#xff0c;只不过是训练模型专用而已&#xff0c;打开的端口同样是7860。 1.双击setup.bat,选择1安装好xformers,pytorch等和cuda…

Linux进程概念总结

这里总结下Linux进程概念总结❗ 冯诺依曼&#xff1a; CPU 运算器与控制器RAM 内存&#xff08;存储器&#xff09;Cache 缓存&#xff08;一种技术&#xff09;不属于冯诺依曼体系结构。ROM 磁盘&#xff08;输入输出设备&#xff09;磁盘 既可以从硬盘读取数据也可以向硬盘…

【全开源】JAVA语聊大厅语音聊天APP系统源码

语聊大厅语音聊天源码&#xff1a;打造专属的语音社交平台 核心功能 多人语音聊天&#xff1a;支持多人同时在线语音聊天&#xff0c;用户可以创建或加入不同的聊天室&#xff0c;与好友或陌生人进行实时互动。语音转文字&#xff1a;提供语音转文字功能&#xff0c;方便用户…

你好 GPT-4o!

你好 GPT-4o&#xff01; OpenAI公司宣布推出 GPT-4o&#xff0c;这是OpenAI的新旗舰模型&#xff0c;可以实时对音频、视觉和文本进行推理。 GPT-4o&#xff08;“o”代表“o​​mni”&#xff09;是迈向更自然的人机交互的一步——它接受文本、音频、图像和视频的任意组合作…

C++二叉搜索树搜索二叉树二叉排序树

C二叉搜索树 1. 二叉搜索树的概念 二叉搜索树&#xff08;BST,Binary Search Tree)&#xff0c;也称为二叉排序树或二叉查找树。它与一般二叉树的区别在于&#xff1a;每个结点必须满足“左孩子大于自己&#xff0c;右孩子小于自己”的规则。在这种规则的约束下&#xff0c;二…

海上定位测量难?千寻星基稳如“定”海神针_0416(update-2)

海上定位测量难&#xff1f;千寻星基稳如“定”海神针 近年来&#xff0c;随着海洋资源的开发和利用&#xff0c;海上定位测量的需求日益增加&#xff0c;它通过测量物体的位置和方向来确定海洋中的各项活动。其准确性和可靠性对于确保海上作业的安全和效率至关重要。 由于海…

想做好抖店?新手、老玩家切勿掉进这些坑,操作要慎重!

大家好&#xff0c;我是电商花花。 很多人都说做抖音小店不需要脑子&#xff0c;会抄就行&#xff0c;难道做店真的就是这样吗&#xff1f; 真的就是会抄&#xff0c;会简单选品&#xff0c;找一些达人就能出单&#xff0c;就能实现睡后收入了吗&#xff1f; 其实并不见得&a…

Latex问题1

问题 添加bib文件的引用后 \bibliographystyle{IEEEtran} \bibliography{IEEEabrv}之后&#xff0c;出现莫名其妙的错误&#xff0c;如下 IEEEabrv.bib是我的参考文献的bib文件&#xff0c;CCS_1.tex是我的tex文件&#xff0c;bib文件中的内容为 ARTICLE{1,author{Capponi,…

精选合作伙伴:如何挑选最适合您小程序商城开发的软件公司

在选择一家合适的软件公司来协助您开发并运营小程序商城时&#xff0c;选择过程无疑是一项关键而复杂的任务。市场上的软件公司繁多&#xff0c;各具特色&#xff0c;那么&#xff0c;如何在这众多的选择中找到最适合您的合作伙伴呢&#xff1f;以下将从需求梳理、公司实力评估…

jiebaNET中文分词器

最近我接手了一个有趣的需求&#xff0c;需要对用户评价进行分词&#xff0c;进行词频统计和情绪分析&#xff0c;并且根据词频权重制成词云图以供后台数据统计&#xff0c;于是我便引入了jieba分词器,但是我发现网上关于jiebaNET相关文档实在太少了&#xff0c;甚至连配置文件…

2.1.2 C++程序设计——程序基本概念

文章目录 展示大纲1、程序基本概念2、基本数据类型3、程序基本语句4、基本运算5、数学库常用函数6、结构化程序设计展示大纲 1、程序基本概念

分享开放原子AtomGit开源协作平台评测报告

AtomGit平台的总体介绍 开放原子开源基金会是致力于推动全球开源事业发展的非营利机构&#xff0c;于 2020 年 6 月在北京成立&#xff0c;由阿里巴巴、百度、华为、浪潮、360、腾讯、招商银行等多家龙头科技企业联合发起。目前有三个主要机构设置&#xff0c;技术监督委员会&…

3-3 基于RYU的流量风暴事件原理与响应策略

在传统网络中&#xff0c;存在着一定的广播流量&#xff0c;占据了一部分的网络带宽。同时&#xff0c;在有环的拓扑中&#xff0c;如果不运行某些协议&#xff0c;广播数据还会引起网络风暴&#xff0c;使网络瘫痪。 如有以下的一个网络拓扑结构&#xff08;3_2_topoplus.py) …

武汉星起航:亚马逊构建综合性商业生态,卖家买家共享全球化红利

在当今全球化日益加速的时代&#xff0c;亚马逊不仅以其卓越的电商平台服务全球消费者&#xff0c;更通过一系列前沿服务打造了一个综合性的商业生态系统。在这个生态系统中&#xff0c;卖家能够轻松拓展全球业务&#xff0c;买家则享受到了前所未有的购物体验。亚马逊以其独特…