当前位置: 首页 > news >正文

@Async注解的坑,小心

背景

前段时间,一个同事小姐姐跟我说她的项目起不来了,让我帮忙看一下,本着助人为乐的精神,这个忙肯定要去帮。

于是,我在她的控制台发现了如下的异常信息:

Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [BService] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495)at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317)at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)

看到BeanCurrentlyInCreationException这个异常,我的第一反应是出现了循环依赖的问题。但是仔细一想,Spring不是已经解决了循环依赖的问题么,怎么还报这个错。于是,我就询问小姐姐改了什么东西,她说在方法上加了@Async注解。

这里我模拟一下当时的代码,AService 和 BService 相互引用,AService的 save() 方法加了 @Async 注解。

@Component
public class AService {@Resourceprivate BService bService;@Asyncpublic void save() {}
}
@Component
public class BService {@Resourceprivate AService aService;}

也就是这段代码会报BeanCurrentlyInCreationException异常,难道是@Async注解遇上循环依赖的时候,Spring无法解决?为了验证这个猜想,我将@Async注解去掉之后,再次启动项目,项目成功起来了。于是基本可以得出结论,那就是@Async注解遇上循环依赖的时候,Spring的确无法解决。

虽然问题的原因已经找到了,但是又引出以下几个问题:

  • @Async注解是如何起作用的?

  • 为什么@Async注解遇上循环依赖,Spring无法解决?

  • 出现循环依赖异常之后如何解决?

@Async注解是如何起作用的?

@Async注解起作用是靠AsyncAnnotationBeanPostProcessor这个类实现的,这个类会处理@Async注解。AsyncAnnotationBeanPostProcessor这个类的对象是由@EnableAsync注解放入到Spring容器的,这也是为什么需要使用@EnableAsync注解来激活让@Async注解起作用的根本原因。

AsyncAnnotationBeanPostProcessor

图片

类体系

这个类实现了 BeanPostProcessor 接口,实现了 postProcessAfterInitialization 方法,是在其父类AbstractAdvisingBeanPostProcessor 中实现的,也就是说当Bean的初始化阶段完成之后会回调 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法。之所以会回调,是因为在Bean的生命周期中,当Bean初始化完成之后,会回调所有的 BeanPostProcessor 的 postProcessAfterInitialization 方法,代码如下:

@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)throws BeansException {Object result = existingBean;for (BeanPostProcessor processor : getBeanPostProcessors()) {Object current = processor.postProcessAfterInitialization(result, beanName);if (current == null) {return result;}result = current;}return result;
}

AsyncAnnotationBeanPostProcessor 对于 postProcessAfterInitialization 方法实现:

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {if (this.advisor == null || bean instanceof AopInfrastructureBean) {// Ignore AOP infrastructure such as scoped proxies.return bean;}if (bean instanceof 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;}}if (isEligible(bean, beanName)) {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;
}

该方法的主要作用是用来对方法入参的对象进行动态代理的,当入参的对象的类加了@Async注解,那么这个方法就会对这个对象进行动态代理,最后会返回入参对象的代理对象出去。至于如何判断方法有没有加@Async注解,是靠 isEligible(bean, beanName) 来判断的。由于这段代码牵扯到动态代理底层的知识,这里就不详细展开了。

图片

AsyncAnnotationBeanPostProcessor作用

综上所述,可以得出一个结论,那就是当Bean创建过程中初始化阶段完成之后,会调用 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 的方法,对加了@Async注解的类的对象进行动态代理,然后返回一个代理对象回去。

虽然这里我们得出@Async注解的作用是依靠动态代理实现的,但是这里其实又引发了另一个问题,那就是事务注解@Transactional又或者是自定义的AOP切面,他们也都是通过动态代理实现的,为什么使用这些的时候,没见抛出循环依赖的异常?难道他们的实现跟@Async注解的实现不一样?不错,还真的不太一样,请继续往下看。

AOP是如何实现的?

我们都知道AOP是依靠动态代理实现的,而且是在Bean的生命周期中起作用,具体是靠 AnnotationAwareAspectJAutoProxyCreator 这个类实现的,这个类会在Bean的生命周期中去处理切面,事务注解,然后生成动态代理。这个类的对象在容器启动的时候,就会被自动注入到Spring容器中。

AnnotationAwareAspectJAutoProxyCreator 也实现了BeanPostProcessor,也实现了 postProcessAfterInitialization 方法。

@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (!this.earlyProxyReferences.contains(cacheKey)) {//生成动态代理,如果需要被代理的话return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;
}

通过 wrapIfNecessary 方法就会对Bean进行动态代理,如果你的Bean需要被动态代理的话。

图片

AnnotationAwareAspectJAutoProxyCreator作用

也就说,AOP和@Async注解虽然底层都是动态代理,但是具体实现的类是不一样的。一般的AOP或者事务的动态代理是依靠 AnnotationAwareAspectJAutoProxyCreator 实现的,而@Async是依靠 AsyncAnnotationBeanPostProcessor 实现的,并且都是在初始化完成之后起作用,这也就是@Async注解和AOP之间的主要区别,也就是处理的类不一样。

Spring是如何解决循环依赖的?

Spring在解决循环依赖的时候,是依靠三级缓存来实现的。我曾经写过一篇关于三级缓存的文章,如果有不清楚的小伙伴可以看一下 有关循环依赖和三级缓存的这些问题,你都会么?(面试常问)这篇文章,本文也算是这篇三级缓存文章的续作。

简单来说,通过缓存正在创建的对象对应的ObjectFactory对象,可以获取到正在创建的对象的早期引用的对象,当出现循环依赖的时候,由于对象没创建完,就可以通过获取早期引用的对象注入就行了。

而缓存ObjectFactory代码如下:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {if (!this.singletonObjects.containsKey(beanName)) {this.singletonFactories.put(beanName, singletonFactory);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}
}

所以缓存的ObjectFactory对象其实是一个lamda表达式,真正获取早期暴露的引用对象其实就是通过 getEarlyBeanReference 方法来实现的。

getEarlyBeanReference 方法:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (BeanPostProcessor bp : getBeanPostProcessors()) {if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);}}}return exposedObject;
}

getEarlyBeanReference 实现是调用所有的 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 方法。

而前面提到的 AnnotationAwareAspectJAutoProxyCreator 这个类就实现了 SmartInstantiationAwareBeanPostProcessor 接口,是在父类中实现的:

@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (!this.earlyProxyReferences.contains(cacheKey)) {this.earlyProxyReferences.add(cacheKey);}return wrapIfNecessary(bean, beanName, cacheKey);
}

这个方法最后会调用 wrapIfNecessary 方法,前面也说过,这个方法是获取动态代理的方法,如果需要的话就会代理,比如事务注解又或者是自定义的AOP切面,在早期暴露的时候,就会完成动态代理。

这下终于弄清楚了,早期暴露出去的原来可能是个代理对象,而且最终是通过AnnotationAwareAspectJAutoProxyCreator这个类的getEarlyBeanReference方法获取的。

但是AsyncAnnotationBeanPostProcessor并没有实现SmartInstantiationAwareBeanPostProcessor,也就是在获取早期对象这一阶段,并不会调AsyncAnnotationBeanPostProcessor处理@Async注解。

为什么@Async注解遇上循环依赖,Spring无法解决?

这里我们就拿前面的例子来说,AService加了@Async注解,AService先创建,发现引用了BService,那么BService就会去创建,当Service创建的过程中发现引用了AService,那么就会通过AnnotationAwareAspectJAutoProxyCreator 这个类实现的 getEarlyBeanReference 方法获取AService的早期引用对象,此时这个早期引用对象可能会被代理,取决于AService是否需要被代理,但是一定不是处理@Async注解的代理,原因前面也说过。

于是BService创建好之后,注入给了AService,那么AService会继续往下处理,前面说过,当初始化阶段完成之后,会调用所有的BeanPostProcessor的实现的 postProcessAfterInitialization 方法。于是就会回调依次回调 AnnotationAwareAspectJAutoProxyCreator 和 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法实现。

这段回调有两个细节:

  • AnnotationAwareAspectJAutoProxyCreator 先执行,AsyncAnnotationBeanPostProcessor 后执行,因为 AnnotationAwareAspectJAutoProxyCreator 在前面。

图片

  • AnnotationAwareAspectJAutoProxyCreator处理的结果会当入参传递给 AsyncAnnotationBeanPostProcessor,applyBeanPostProcessorsAfterInitialization方法就是这么实现的

AnnotationAwareAspectJAutoProxyCreator回调:会发现AService对象已经被早期引用了,什么都不处理,直接把对象AService给返回

AsyncAnnotationBeanPostProcessor回调:发现AService类中加了@Async注解,那么就会对AnnotationAwareAspectJAutoProxyCreator返回的对象进行动态代理,然后返回了动态代理对象。

这段回调完,是不是已经发现了问题。早期暴露出去的对象,可能是AService本身或者是AService的代理对象,而且是通过AnnotationAwareAspectJAutoProxyCreator对象实现的,但是通过AsyncAnnotationBeanPostProcessor的回调,会对AService对象进行动态代理,这就导致AService早期暴露出去的对象跟最后完全创造出来的对象不是同一个,那么肯定就不对了。同一个Bean在一个Spring中怎么能存在两个不同的对象呢,于是就会抛出BeanCurrentlyInCreationException异常,这段判断逻辑的代码如下:

if (earlySingletonExposure) {// 获取到早期暴露出去的对象Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {// 早期暴露的对象不为null,说明出现了循环依赖if (exposedObject == bean) {// 这个判断的意思就是指 postProcessAfterInitialization 回调没有进行动态代理,如果没有那么就将早期暴露出去的对象赋值给最终暴露(生成)出去的对象,// 这样就实现了早期暴露出去的对象和最终生成的对象是同一个了// 但是一旦 postProcessAfterInitialization 回调生成了动态代理 ,那么就不会走这,也就是加了@Aysnc注解,是不会走这的exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {// allowRawInjectionDespiteWrapping 默认是falseString[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {//抛出异常throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] 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 " +"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");}}}
}

所以,之所以@Async注解遇上循环依赖,Spring无法解决,是因为@Aysnc注解会使得最终创建出来的Bean,跟早期暴露出去的Bean不是同一个对象,所以就会报错。

出现循环依赖异常之后如何解决?

解决这个问题的方法很多

1、调整对象间的依赖关系,从根本上杜绝循环依赖,没有循环依赖,就没有早期暴露这么一说,那么就不会出现问题

2、不使用@Async注解,可以自己通过线程池实现异步,这样没有@Async注解,就不会在最后生成代理对象,也就不会导致跟早期暴露出去的对象不一样

3、可以在循环依赖注入的字段上加@Lazy注解

@Component
public class AService {@Resource@Lazyprivate BService bService;@Asyncpublic void save() {}
}

4、从上面的那段判断抛异常的源码注释可以看出,当allowRawInjectionDespiteWrapping为true的时候,就不会走那个else if,也就不会抛出异常,所以可以通过将allowRawInjectionDespiteWrapping设置成true来解决报错的问题,代码如下:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {((DefaultListableBeanFactory) beanFactory).setAllowRawInjectionDespiteWrapping(true);}}

虽然这样设置能解决报错的问题,但是并不推荐,因为这样设置就允许早期注入的对象和最终创建出来的对象是不一样,并且可能会导致最终生成的对象没有被动态代理。

相关文章:

  • 『亚马逊云科技产品测评』活动征文|AWS 域名注册、启动与连接 EC2 新实例、端口开放详细教程
  • Ubuntu 22.04.3编译AOSP13刷机
  • R语言如何实现多元线性回归
  • HTML新手入门笔记整理:块元素和行内元素
  • Django之importlib模块
  • angular 实现模块共享
  • WebSocket了解
  • ErphpdownV16.21插件 安装教程和插件下载
  • 初次使用vs code时go模块安装失败的解决办法
  • SASS的导入文件详细教程
  • 基于OPC UA 的运动控制读书笔记(1)
  • HCIA-RS基础:动态路由协议基础
  • 支持Arm CCA的TF-A威胁模型
  • 系列九、声明式事务(xml方式)
  • pop链反序列化 [MRCTF2020]Ezpop1
  • [nginx文档翻译系列] 控制nginx
  • 77. Combinations
  • javascript面向对象之创建对象
  • Java超时控制的实现
  • Java小白进阶笔记(3)-初级面向对象
  • java正则表式的使用
  • Spring核心 Bean的高级装配
  • Web标准制定过程
  • 反思总结然后整装待发
  • 高度不固定时垂直居中
  • 如何解决微信端直接跳WAP端
  • 使用Gradle第一次构建Java程序
  • 云栖大讲堂Java基础入门(三)- 阿里巴巴Java开发手册介绍
  • ​​​​​​​​​​​​​​Γ函数
  • ​马来语翻译中文去哪比较好?
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • #控制台大学课堂点名问题_课堂随机点名
  • (二)springcloud实战之config配置中心
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (推荐)叮当——中文语音对话机器人
  • (原創) 如何動態建立二維陣列(多維陣列)? (.NET) (C#)
  • (转)Android学习系列(31)--App自动化之使用Ant编译项目多渠道打包
  • (总结)Linux下的暴力密码在线破解工具Hydra详解
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .NET NPOI导出Excel详解
  • .NET 分布式技术比较
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .NET/C# 异常处理:写一个空的 try 块代码,而把重要代码写到 finally 中(Constrained Execution Regions)
  • /usr/bin/python: can't decompress data; zlib not available 的异常处理
  • @TableId注解详细介绍 mybaits 实体类主键注解
  • [ IOS ] iOS-控制器View的创建和生命周期
  • [Angular] 笔记 20:NgContent
  • [BZOJ 4598][Sdoi2016]模式字符串
  • [C++提高编程](三):STL初识
  • [cocos creator]EditBox,editing-return事件,清空输入框
  • [codevs 1515]跳 【解题报告】
  • [ExtJS5学习笔记]第三十节 sencha extjs 5表格gridpanel分组汇总
  • [E单调栈] lc2487. 从链表中移除节点(单调栈+递归+反转链表+多思路)
  • [js] 正则表达式
  • [LeetCode 687]最长同值路径