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