聊一聊Spring中当循环依赖遇到@Async的那点事儿
今天为了弥补上一篇【 聊一聊在Spring中Bean的循环依赖那些事儿 】遗漏的知识点,特此,今天想聊一聊Spring中当循环依赖遇到@Async的那点事儿,避免童鞋们在工作中再次入坑(坑已踩过)!!!
1、问题背景概述
从上一篇【 聊一聊在Spring中Bean的循环依赖那些事儿 】我们可以基本掌握Spring中Bean之间循环依赖的工作原理,但在工作中,我们时不时会需要引入【异步线程】的概念,从字面上理解分为主线程和子线程。
例如:用户成为会员,此时需要用户补充唯一标识身份信息等,但对于后端逻辑来说,主要步骤分为如下几步:
- 根据唯一标识存储用户基本信息
- 注册成功后给用户发送短信
- 注册成功后给用户发放新用户优惠券
问题分析:
有做过类似需求的朋友们会发现,第 2 和 3 步骤需要结合第三方接口才能完成,而调用第三方接口总会存在因网络超时或其他原因,导致程序会执行报错,所以结合需求来说,向第 2 或 3 步骤在执行报错的情况下,是不应该影响主流程的,故此我们可以认为:
- 用户注册功能为主流程(即主线程)。
- 第 2 或 3 步骤为子流程(即子线程)。
- 子流程不能影响主流程。
结合以上问题分析可以得到,我们可以采用异步线程的概念,此处可能举例不是特别的恰当,但我主要是想引入异步线程的概念,方便我们下面继续的深入探究它。
2、异步线程使用
1、第一步在主启动类上或者新建一个配置类,通过注解@EnableAsync相当于开启异步配置
2、第二步是在类或方法上标注注解 @Async
3、自定义配置线程池(根据阿里巴巴编码规范要求),原因也是非常的明确 !!!
3、当循环依赖遇到@Async时程序运行报错
3.1 核心代码部分
@Service
public class A {
@Autowired
private B b;
/**
* 案例描述:
*/
@Async // 引发报错的导火索
public void methodA() {
System.out.println("方法B执行了...");
System.out.println(b);
System.out.println("方法B执行了...");
}
}
@Service
public class B {
@Autowired
private A a;
/**
* 案例描述:
*/
public void methodB() {
System.out.println("方法A执行了...");
System.out.println(a);
System.out.println("方法A执行了...");
}
}
3.2 报错运行原因
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:649) ~[spring-beans-5.3.26.jar:5.3.26]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.26.jar:5.3.26]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.26.jar:5.3.26]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.26.jar:5.3.26]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.26.jar:5.3.26]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.26.jar:5.3.26]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) ~[spring-beans-5.3.26.jar:5.3.26]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:920) ~[spring-context-5.3.26.jar:5.3.26]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.26.jar:5.3.26]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) [spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.7.10.jar:2.7.10]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.7.10.jar:2.7.10]
at com.mrgao.demo.XbqxDemoApplication.main(XbqxDemoApplication.java:24) [classes/:na]
根据代码运行原因,我们可以追踪到报错的地方
从此处我们可以发现, 仅仅是在原来Bean循环依赖的基础上,增加了一个@Async注解,从而导致Bean创建异常;
3.3 Debug运行调试
看到这里,有没有小伙伴们仔细的发现到,主要是由于这两个属性值不相等,才会抛出BeanCurrentlyInCreationException异常,因为this.allowRawInjectionDespiteWrapping属性默认为false,且在循环依赖的场景下,必然会抛出BeanCurrentlyInCreationException。
4、分析原因
上一篇文章【 聊一聊在Spring中Bean的循环依赖那些事儿 】我也有分析过,主要原因是:AsyncAnnotationBeanPostProcessor不是SmartInstantiationAwareBeanPostProcessor的实例对象,所以从ObjectFactory的getObject()获取对象时,此刻的Bean属于原Bean对象,即就是没有AOP增强后的Bean对象,二级缓存中存的原Bean对象。
另一方面原因: 是在initializeBean的过程中,对象A的方法上恰好存在 @Async 注解(通过AopUtils#canApply() 满足Bean增强的条件),所以在执行完AsyncAnnotationBeanPostProcessor的postProcessAfterInitialization()方法后,得到的是增强后的Bean(即就是增强后的代理Bean对象)
所以根据属性划分为如下:
- exposedObject(增强后的Bean)。
- earlySingletonReference(二级缓存中的Bean)。
- bean(原始bean对象)。
- exposedObject 不等于 bean,即就是增强后的对象Bean与原对象bean不相等。
4.1 AsyncAnnotationBeanPostProcessor#postProcessAfterInitialization()
AsyncAnnotationBeanPostProcessor增强Bean的代码执行逻辑如下所示
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (this.advisor == null || bean instanceof AopInfrastructureBean) {
// 如果当前bean是AopInfrastructureBean的实例对象 或 不存在切面处理时,直接返回
return bean;
}
if (bean instanceof Advised) {
// 如果当前bean是Advised的实例对象
Advised advised = (Advised) bean;
if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
// Add our local Advisor to the existing proxy's Advisor chain...
if (this.beforeExistingAdvisors) {
advised.addAdvisor(0, this.advisor);
}
else {
advised.addAdvisor(this.advisor);
}
return bean;
}
}
// 核心代码判断 (判断的条件是:类或方法中查看是否存在@Async注解)
if (isEligible(bean, beanName)) {
// 条件满足 创建代理对象
ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
if (!proxyFactory.isProxyTargetClass()) {
evaluateProxyInterfaces(bean.getClass(), proxyFactory);
}
proxyFactory.addAdvisor(this.advisor);
customizeProxyFactory(proxyFactory);
ClassLoader classLoader = getProxyClassLoader();
if (classLoader instanceof SmartClassLoader && classLoader != bean.getClass().getClassLoader()) {
classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
}
// 返回代理Bean对象
return proxyFactory.getProxy(classLoader);
}
// 此处表示没有对当前Bean进行代理
return bean;
}
4.2 Spring中如何规避@Async引发的问题呢
在Spring中提供了一种解决方案,就是在执行populateBean()时,之前引发问题的根本原因是:A依赖B且 B依赖A,然后A从三级缓存中得到的不是增强后的Bean,换个角度思考下,就可以认为:在B依赖注入A的时候创建一个增强后的代理Bean,这样是否就可以满足呢?然而Spring中也是这么来做的,那就是通过@Lazy注解标识为当前Bean为懒加载的Bean,直接创建代理对象,在使用的时候才会注册所依赖的Bean。
4.3 代码处理逻辑如下
- 引用类对象: DefaultListableBeanFactory#resolveDependency()
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
return createOptionalDependency(descriptor, requestingBeanName);
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
}
else {
// @Lazy核心处理逻辑
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
// 如果不为空时,则可以跳过执行doResolveDependency(),若是三级缓存开启时,后面会从三级获取代理对象的
if (result == null) {
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
// 返回result
return result;
}
}
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
}
4.3.1 判断@Lazy代码逻辑
protected boolean isLazy(DependencyDescriptor descriptor) {
// 先从类上获取@Lazy
for (Annotation ann : descriptor.getAnnotations()) {
Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
// 再从方法上获取@Lazy
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
}
return false;
}
4.3.2 创建代理Bean逻辑
当调用目标代理Bean时,会监听 DynamicAdvisedInterceptor#intercept()。
protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor, final @Nullable String beanName) {
BeanFactory beanFactory = getBeanFactory();
Assert.state(beanFactory instanceof DefaultListableBeanFactory,
"BeanFactory needs to be a DefaultListableBeanFactory");
final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory;
TargetSource ts = new TargetSource() {
@Override
public Class<?> getTargetClass() {
return descriptor.getDependencyType();
}
@Override
public boolean isStatic() {
return false;
}
@Override
public Object getTarget() {
// 注意:当执行调用时会触发此处
Set<String> autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null);
// 从容器中获取目标Bean对象
Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null);
if (target == null) {
Class<?> type = getTargetClass();
if (Map.class == type) {
return Collections.emptyMap();
}
else if (List.class == type) {
return Collections.emptyList();
}
else if (Set.class == type || Collection.class == type) {
return Collections.emptySet();
}
throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(),
"Optional dependency not present for lazy injection point");
}
if (autowiredBeanNames != null) {
for (String autowiredBeanName : autowiredBeanNames) {
if (dlbf.containsBean(autowiredBeanName)) {
// 注册所依赖的bean
dlbf.registerDependentBean(autowiredBeanName, beanName);
}
}
}
// 返回目标bean
return target;
}
@Override
public void releaseTarget(Object target) {
}
};
// 创建代理Bean
ProxyFactory pf = new ProxyFactory();
pf.setTargetSource(ts);
Class<?> dependencyType = descriptor.getDependencyType();
if (dependencyType.isInterface()) {
pf.addInterface(dependencyType);
}
// 返回代理Bean
return pf.getProxy(dlbf.getBeanClassLoader());
}
4.4 小结
综上所述,我们已经大致清楚@Async和Spring循环依赖的工作原理,可能有些地方需要童鞋们自己Debug调试跟踪下,这样可能理解的更快些,那么我们在工作中如何快速的避免这种问题呢,我认为可以通过以下两点进行优化。
- 结合Spring官方来分析,Spring中默认不开启循环依赖,间接的告知我们应减少使用Bean之间的循环依赖。
- 如果工作中,不得不使用循环依赖时,其中之一的解决的方法是通过@Lazy注解。另外一种解决方法是增加一层(增加一层调用:例如 A和B互相循环依赖彼此,而A需要增加@Async,那么换一种思路,我们可以新增一层C ,C增加@Async,然后 C再调用A,总之就是避免@Async和循环依赖二者都存在的情况)。
5、疑问点
看到这里,童鞋们是不是不由心里产生了疑惑呢?那就是AOP切面遇到循环依赖为什么不会报上边那个BeanCurrentlyInCreationException创建异常呢?其工作原理都是生成代理Bean对象,为什么AOP切面是支持的,@Async却是不支持的。
最后,如果我上述表达有错误的地方,麻烦童鞋们能及时私信我,希望咱们能一起进步! 也希望童鞋们能够更多的支持与点赞哦,对于本文存在的疑问点,下一篇我会篇文章,来为童鞋们解决疑惑(本文先暂留个悬念)。