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

news2024/9/28 11:12:35

使用异步@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()实现了对代理对象的创建,因此若它被循环依赖了,就会报错。

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

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

相关文章

根据部门id删除该部门下的员工(事务)

application.properties&#xff1a; 或&#xff1a; application.yml&#xff1a; 新表&#xff1a; 日志对象类&#xff1a; 日志service类&#xff1a; 日志service接口&#xff1a; 日志mapper类&#xff1a; 部门service类&#xff1a; 员工mapper类&#xff1a;

某大型集团SAP数字化转型方案(95页PPT)

一、资料介绍 《某大型集团SAP数字化转型方案》是一份详尽的95页PPT资料&#xff0c;旨在为某大型集团提供一套全面而深入的SAP数字化转型方案。该方案紧密结合了集团的业务特点和发展需求&#xff0c;以SAP系统为核心&#xff0c;通过数字化技术的运用&#xff0c;实现业务流…

【redis】Redis五种常用数据类型和内部编码,以及对String字符串类型的总结

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…

ESP32引脚入门指南(七):从理论到实践(IIC)

引言 IIC&#xff08;Inter-Integrated Circuit&#xff09;&#xff0c;又称为IC&#xff0c;是一种简单而高效的多主控器串行通信协议&#xff0c;常用于微控制器和各种外围设备之间的通信。在ESP32系列芯片中&#xff0c;IIC协议被广泛应用于连接各种传感器、存储器和其他支…

计算机网络实验1:交换机基本配置管理

实验目的和要求 安装Packer Tracer&#xff0c;了解Packer Tracer的基本操作掌握交换机基本命令集实验项目内容 认识Packet Tracer软件 交换机的基本配置与管理 交换机的端口配置与管理 交换机的端口聚合配置 交换机划分Vlan配置 实验环境 硬件&#xff1a;PC机&#x…

HTML5 + CSS3实现卖茶女与水果男的巅峰微信聊天对决,看完后笑一整天

记得之前看过一段卖茶女与水果男的聊天视频&#xff0c;当时觉得真有意思&#xff0c;竟然还可以这样热爱自己的事业。我就想&#xff0c;用HTML5 CSS3实现一下这个过程&#xff0c;锻炼了技术&#xff0c;也娱乐了开发人员&#xff0c;多有意思的一件事啊。 目录 1 实现思路…

Dbeaver连接一段时间不操作后断开的问题

右键数据库连接点击编辑连接点击初始化将连接保持改成60s

杰理AC632N 通过写flash修改蓝牙名字

杰理修改蓝牙名字一般有他自己的一个工具的,如下图,在编译前修改 现在用写flash的方式更改: 主要看杰理CFG_BT_NAME这个宏里面的.c文件 杰理给这里的flash都定义好每个宏的信息,你需要读写就行,修改后记得重启上电才能生效.如果你要自定义数据写进去flash断电不丢失,估计就是在…

智慧公厕的技术基础、保障技术和应用价值

近年来&#xff0c;随着信息技术的快速发展&#xff0c;智慧公厕逐渐成为城市管理的热点项目。智慧公厕利用物联网技术与大数据、云计算、网络通信、自动化控制等先进技术相结合&#xff0c;公共厕所的管理变得更加快捷高效&#xff0c;实现了真正的智能化使用和智慧化管理。下…

盘点自动驾驶的技术发展趋势

自动驾驶技术在不断发展变快&#xff0c;我们之前提过算法岗如今越来越卷&#xff0c;从今年的就业局势看&#xff0c;前年还属于蓝海行业的自动驾驶&#xff0c;今年就已经满满关上了招揽之门——呈红海之势。作为在这个行业中摸爬滚打的一以子&#xff0c;我们到底该如何纵观…

Goland GC

Goland GC 引用Go 1.3 mark and sweep 标记法Go 1.5 三色标记法屏障机制插入屏障删除写屏障总结 Go 1.8 混合写屏障(hybrid write barrier)机制总结 引用 https://zhuanlan.zhihu.com/p/675127867 Garbage Collection&#xff0c;缩写为GC&#xff0c;一种内存管理回收的机制…

ABAP 直连sqlserver或oracle数据库

1、事务码DBCO,配置链接 2、测试链接&#xff1a;sm38 执行ADBC_TEST_CONNECTION 3、运行时会报驱动找不到的错误&#xff0c;解决方法&#xff1a; S4 HANA连接其他数据库&#xff08;oracle,sqlserver)

代码审计平台sonarqube的安装及使用

docker搭建代码审计平台sonarqube 一、代码审计关注的质量指标二、静态分析技术分类三、使用sonarqube的目的四、sonarqube流程五、docker快速搭建sonarqube六、sonarqube scanner的安装和使用七、sonarqube对maven项目进行分析八、sonarqube分析报告解析九、代码扫描规则定制十…

mysql--join

Join 两个表都有一个主键索引 id 和一个索引 a&#xff0c;字段 b 上无索引&#xff0c;表 t2 里插入了 1000 行数据&#xff0c;在表 t1 里插入的是 100 行数据 CREATE TABLE t2 (id int(11) NOT NULL,a int(11) DEFAULT NULL,b int(11) DEFAULT NULL,PRIMARY KEY (id),KEY …

图机器学习入门:基本概念介绍

图机器学习&#xff08;Graph Machine Learning&#xff0c;简称Graph ML&#xff09;是机器学习的一个分支&#xff0c;专注于利用图形结构的数据。在图形结构中&#xff0c;数据以图的形式表示&#xff0c;其中的节点&#xff08;或顶点&#xff09;表示实体&#xff0c;边&a…

【LeetCode算法】28. 找出字符串中第一个匹配项的下标

提示&#xff1a;此文章仅作为本人记录日常学习使用&#xff0c;若有存在错误或者不严谨得地方欢迎指正。 文章目录 一、题目二、思路三、解决方案四、JAVA截取字符串的常用方法4.1 通过subString()截取字符串* 一、题目 给你两个字符串 haystack 和 needle &#xff0c;请你在…

某大型央企主数据项目实战,上线前后深度对比与解析

数据已成为数字化转型的核心&#xff0c;如何将数据转化为战略资产&#xff0c;是当今企业迫切需要解决的问题。其中主数据作为企业的“黄金数据”&#xff0c;是数据资产管理实践方式的重要切入方法之一。某大型央企成功实施了主数据管理项目&#xff0c;实现了从数据孤岛到一…

寻找最大价值的矿堆 - 矩阵

系列文章目录 文章目录 系列文章目录前言一、题目描述二、输入描述三、输出描述四、Java代码五、测试用例 前言 本人最近再练习算法&#xff0c;所以会发布一些解题思路&#xff0c;希望大家多指教 一、题目描述 给你一个由’0’(空地)、‘1’(银矿)、‘2’(金矿)组成的地图…

揭秘自行车对人体的神奇力量

想象一下&#xff0c;如果有一种魔法&#xff0c;能让你每施展一次就多一个小时的寿命&#xff0c;你会心动吗&#xff1f;这种魔法确实存在&#xff0c;它就是——骑自行车。没错&#xff0c;你没有听错&#xff0c;根据最新的研究数据&#xff0c;“骑行一小时&#xff0c;长…

vue布局设置——使用 el-drawer 打造个性化 Admin 后台布局设置

在前端开发中&#xff0c;我们常常需要为 admin 后台构建灵活且个性化的布局设置。今天&#xff0c;我要分享的是如何利用 el-drawer 来实现这样一个有趣的功能。 首先&#xff0c;我们来看一下主要的设置参数&#xff1a; 1. theme: 用于定义主题&#xff0c;可以根据需求切换…