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

ops/2024/10/18 5:46:09/

使用异步@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的定义和初始化。

java">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

使用循环依赖

java">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");}
}
java">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

java">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 "测试循环依赖的异步使用:成功";}
}

springboot_118">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方法中,看核心逻辑代码:

java">// 这个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;
}
java">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方法时对进行检测

java">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成员上边
java">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成员上边
java">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成员上边为例:

java">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://www.ppmy.cn/ops/39238.html

相关文章

番外篇 | YOLOv8改进之利用SCINet解决黑夜目标检测问题 | 低照度图像增强网络

前言:Hello大家好,我是小哥谈。自校正照明网络(Self-Calibrating Illumination Network, SCINet)是一种基于深度学习的图像照明算法,可以自动分析图像的内容并根据图像内容自动优化照明。SCINet是一种专为低光照图像增强设计的框架。它通过级联照明学习过程和权重共享机制…

为什么 IP 地址通常以 192.168 开头?

在网络配置中&#xff0c;我们经常会遇到以 192.168 开头的 IP 地址&#xff0c;例如 192.168.0.1 或者 192.168.1.100。 这些地址通常用于局域网中&#xff0c;但为什么要选择以 192.168 开头呢&#xff1f; 本文将深入探讨这个问题&#xff0c;并解释其背后的原因和历史渊源…

idea上如何新建git分支

当前项目在dev分支&#xff0c;如果想在新分支上开发代码&#xff0c;如何新建一个分支呢&#xff1f;5秒搞定~ 1、工具类选择git&#xff0c;点击New Branch 或者右下角点击git分支&#xff0c;再点击New Branch 2、在弹出的Create New Branch弹窗中&#xff0c;输入你的新分支…

低代码技术赋能未来乡村建设:创新与实践

引言 随着我国新型城镇化进程的推进&#xff0c;乡村建设正面临着前所未有的挑战。如何在有限的人力、物力、财力资源下&#xff0c;高效推动乡村建设&#xff0c;实现城乡一体化发展&#xff0c;成为当下亟待解决的问题。低代码技术作为一种创新性的解决方案&#xff0c;为未来…

Python批量备份华为设备配置到FTP服务器

Excel表格存放交换机信息&#xff1a; 备份文件夹效果图&#xff1a; Windows系统配置计划任务定时执行python脚本&#xff1a; Program/script&#xff1a;C:\Python\python.exe Add arguments (optional)&#xff1a; D:\Python_PycharmProjects\JunLan_pythonProje…

源码拾贝三则

目录 一 一种枚举类型的新型使用方式 二 Eigen库中的LDLT分解 三 Eigen中的访问者模式 一 一种枚举类型的新型使用方式 ///D:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include\xiosbase enum _Iostate { // consta…

Aapache Tomcat AJP 文件包含漏洞(CVE-2020-1938)

1 漏洞描述 CVE-2020-1938 是 Apache Tomcat 中的一个严重安全漏洞&#xff0c;该漏洞涉及到 Tomcat 的 AJP&#xff08;Apache JServ Protocol&#xff09;连接器。由于 AJP 协议在处理请求时存在缺陷&#xff0c;攻击者可以利用此漏洞读取服务器上的任意文件&#xff0c;甚至…

Java自定义注解:从定义到解析,再到AOP切面与日志打印应用

目录 一、注解定义二、注解解析三、自定义注解结合AOP切面四、自定义注解用于日志打印五、区别总结六、应用场景总结 在Java开发中&#xff0c;注解是一种强大的元编程工具&#xff0c;它可以帮助我们提升代码的可读性和功能性。本文将深入探讨如何创建和使用自定义注解&#x…