使用异步@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容器中增加了一个代理对象,也就是说beanB
的beanA
并不是一个原始对象,而是一个代理对象
总结: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()实现了对代理对象的创建,因此若它被循环依赖了,就会报错。