使用@Async异步注解导致该Bean在循环依赖时启动报BeanCurrentlyInCreationException异常的根本原因分析,以及提供解决方案【享学Spring】

每篇一句
面试造飞机,工作拧螺丝。工作中你只需要知道那些调用命令怎么使用就行,但背后的逻辑你有必要去了解

前言
今天在自己工程中使用@Async的时候,碰到了一个问题:Spring循环依赖(circular reference)问题。
或许刚说到这,有的小伙伴就会大惊失色了。Spring不是解决了循环依赖问题吗,它是支持循环依赖的呀?怎么会呢?

不可否认,在这之前我也是这么坚信的,而且每次使用得也屡试不爽。倘若你目前也和我有一样坚挺的想法,那么相信本文能让你大有收货~~。

不得不提,关于@Async的使用姿势,请参阅:
【小家Spring】Spring异步处理@Async的使用以及原理、源码分析(@EnableAsync)
关于Spring Bean的循环依赖问题,请参阅:
【小家Spring】一文告诉你Spring是如何利用"三级缓存"巧妙解决Bean的循环依赖问题的

我通过实验总结出,出现使用@Async导致循环依赖问题的必要条件:

已开启@EnableAsync的支持
@Async注解所在的Bean被循环依赖了
背景
若你是一个有经验的程序员,那你在开发中必然碰到过这种现象:事务不生效。

关于事务不生效方面的原因,可参考:【小家java】Spring事务不生效的原因大解读

本文场景的背景也一样,我想调用本类的异步方法(标注有@Async注解),很显然我知道为了让于@Async生效,我把自己依赖进来,然后通过service接口来调用,代码如下:

@Service
public class HelloServiceImpl implements HelloService {
@Autowired
private HelloService helloService;

@Override
public Object hello(Integer id) {
System.out.println("线程名称:" + Thread.currentThread().getName());
helloService.fun1(); // 使用接口方式调用,而不是this
return "service hello";
}

@Async
@Override
public void fun1() {
System.out.println("线程名称:" + Thread.currentThread().getName());
}
}

 

 

此种做法首先是Spring中一个典型的循环依赖场景:自己依赖自己。本以为能够像解决事务不生效问题一样依旧屡试不爽,但没想到非常的不给面子,启动即报错:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'helloServiceImpl': Bean with name 'helloServiceImpl' has been injected into other beans [helloServiceImpl] 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:622)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
...

这里说明一下,为什么有小伙伴跟我说:我使用@Async即使本类方法调用也从来木有遇到这个错误啊?难道它不常见?
为此经过我的一番调查,包括看一些同事、小伙伴的代码发现:并不是使用@Async没有启动报错,而是他本类调用的时候直接调用的方法,这样@Async是不生效的但小伙伴却全然不知而已。

至于@Async没生效这种问题为何没报出来???甚至过了很久很久都没人发现和关注??
其实道理很简单,它和事务不生效不一样,@Async若没生效99%情况下都不会影响到业务的正常进行,因为它不会影响数据正确性,只会影响到性能(无非就是异步变同步呗,这是兼容的)。
但是呢,我期望的是作为一个技术人,还是能够有一定的技术敏感性。能够迅速帮助自己或者你身边同事定位到这个问题,这或许是你可以出彩的资本吧~

我们知道事务不生效和@Async不生效的根本原因都是同一个:直接调用了本类方法而非接口方法/代理对象方法。
解决这类不生效问题的方案一般我们都有两种:

自己注入自己,然后再调用接口方法(当然此处的一个变种是使用编程方式形如:AInterface a = applicationContext.getBean(AInterface.class);这样子手动获取也是可行的~~~本文不讨论这种比较直接简单的方式)
使用AopContext.currentProxy();方式
本文就讲解采取方式一自己注入自己的方案解决带来了更多问题,使用AopContext.currentProxy();方式会在紧邻的下篇博文里详解~

注意:自己注入自己是能够完美解决事务不生效问题。如题,本文旨在讲解解决@Async的问题~~~

有的小伙伴肯定会说:让不调用本类的@Async方法不就可以了;让不产生循环依赖不就可以了;这都是解决方案啊~
其实你说的没毛病,但我我想说:理想的设计当然是不建议循环依赖的。但在真实的业务开发中循环依赖是100%避免不了的,同样本类方法的互调也同样是避免不了的~

关于@Async的使用和原理,有兴趣的可以先补补课:
【小家Spring】Spring异步处理@Async的使用以及原理、源码分析(@EnableAsync)

自己依赖自己方案带来的问题分析
说明:所有示例,都默认@EnableAsync已经开启~ 所以示例代码中不再特别标注

自己依赖自己这种方式是一种典型的使用循环依赖方式来解决问题,大多数情况下它是一个非常好的解决方案。
比如本例若要解决@Async本类调用问题,我们的代码会这么来写:

@Service
public class HelloServiceImpl implements HelloService {

@Autowired
private HelloService helloService;

@Transactional
@Override
public Object hello(Integer id) {
System.out.println("线程名称:" + Thread.currentThread().getName());
// fun1(); // 这样书写@Async肯定不生效~
helloService.fun1(); //调用接口方法
return "service hello";
}

@Async
@Override
public void fun1() {
System.out.println("线程名称:" + Thread.currentThread().getName());
}
}

 

本以为像解决事务问题一样,像这样写是肯定完美解决问题的。但奈何带来了新问题,启动即报错:

报错信息如上~~~
BeanCurrentlyInCreationException这个异常类型小伙伴们应该并不陌生,在循环依赖那篇文章中(请参阅相关阅读)有讲述到:文章里有提醒小伙伴们关注报错的日志,有朝一日肯定会碰面,没想到来得这么快~

对如上异常信息,我大致翻译如下:

创建名为“helloServiceImpl”的bean时出错:名为“helloServiceImpl”的bean已作为循环引用的一部分注入到其原始版本中的其他bean[helloServiceImpl]中,
**但最终已被包装**。这意味着其他bean不使用bean的最终版本。

问题定位
本着先定位问题才能解决问题的原则,找到问题的根本原因成为了我现在最需要做的事。从报错信息的描述可以看出,根本原因是helloServiceImpl最终被包装(代理),所以被使用的bean并不是最终的版本,所以Spring的自检机制报错了~~~

说明:Spring管理的Bean都是单例的,所以Spring默认需要保证所有使用此Bean的地方都指向的是同一个地址,也就是最终版本的Bean,否则可能就乱套了,Spring也提供了这样的自检机制~

上面文字叙述有点苍白,相信小伙伴们看着也是一脸懵逼、二脸继续懵逼吧。下面通过示例代码分析看看结果。

为了更好的说明问题,此处不用自己依赖自己来表述(因为名字相同容易混淆不方便说明问题),而以下面A、B两个类的形式说明:

@Service
public class A implements AInterface {
@Autowired
private BInterface b;
@Async
@Override
public void funA() {
}
}

@Service
public class B implements BInterface {
@Autowired
private AInterface a;
@Override
public void funB() {
a.funA();
}
}

 

如上示例代码启动时会报错:(示例代码模仿成功)

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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:622)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
...

下面是重点,来跟踪一下源码,定位此问题:

protected Object doCreateBean( ... ){
...
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
...

 

// populateBean这一句特别的关键,它需要给A的属性赋值,所以此处会去实例化B~~
// 而B我们从上可以看到它就是个普通的Bean(并不需要创建代理对象),实例化完成之后,继续给他的属性A赋值,而此时它会去拿到A的早期引用
// 也就在此处在给B的属性a赋值的时候,会执行到上面放进去的Bean A流程中的getEarlyBeanReference()方法 从而拿到A的早期引用~~
// 执行A的getEarlyBeanReference()方法的时候,会执行自动代理创建器,但是由于A没有标注事务,所以最终不会创建代理,so B合格属性引用会是A的**原始对象**
// 需要注意的是:@Async的代理对象不是在getEarlyBeanReference()中创建的,是在postProcessAfterInitialization创建的代理
// 从这我们也可以看出@Async的代理它默认并不支持你去循环引用,因为它并没有把代理对象的早期引用提供出来~~~(注意这点和自动代理创建器的区别~)

// 结论:此处给A的依赖属性字段B赋值为了B的实例(因为B不需要创建代理,所以就是原始对象)
// 而此处实例B里面依赖的A注入的仍旧为Bean A的普通实例对象(注意 是原始对象非代理对象) 注:此时exposedObject也依旧为原始对象
populateBean(beanName, mbd, instanceWrapper);

// 标注有@Async的Bean的代理对象在此处会被生成~~~ 参照类:AsyncAnnotationBeanPostProcessor
// 所以此句执行完成后 exposedObject就会是个代理对象而非原始对象了
exposedObject = initializeBean(beanName, exposedObject, mbd);

...
// 这里是报错的重点~~~
if (earlySingletonExposure) {
// 上面说了A被B循环依赖进去了,所以此时A是被放进了二级缓存的,所以此处earlySingletonReference 是A的原始对象的引用
// (这也就解释了为何我说:如果A没有被循环依赖,是不会报错不会有问题的 因为若没有循环依赖earlySingletonReference =null后面就直接return了)
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
// 上面分析了exposedObject 是被@Aysnc代理过的对象, 而bean是原始对象 所以此处不相等 走else逻辑
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
// allowRawInjectionDespiteWrapping 标注是否允许此Bean的原始类型被注入到其它Bean里面,即使自己最终会被包装(代理)
// 默认是false表示不允许,如果改为true表示允许,就不会报错啦。这是我们后面讲的决方案的其中一个方案~~~
// 另外dependentBeanMap记录着每个Bean它所依赖的Bean的Map~~~~
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
// 我们的Bean A依赖于B,so此处值为["b"]
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);

// 对所有的依赖进行一一检查~    比如此处B就会有问题
// “b”它经过removeSingletonIfCreatedForTypeCheckOnly最终返返回false 因为alreadyCreated里面已经有它了表示B已经完全创建完成了~~~
// 而b都完成了,所以属性a也赋值完成儿聊 但是B里面引用的a和主流程我这个A竟然不相等,那肯定就有问题(说明不是最终的)~~~
// so最终会被加入到actualDependentBeans里面去,表示A真正的依赖~~~
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.");
}
}
}
}
...
}

 


这里知识点避开不@Aysnc注解标注的Bean的创建代理的时机。
@EnableAsync开启时它会向容器内注入AsyncAnnotationBeanPostProcessor,它是一个BeanPostProcessor,实现了postProcessAfterInitialization方法。此处我们看代码,创建代理的动作在抽象父类AbstractAdvisingBeanPostProcessor上:

// @since 3.2 注意:@EnableAsync在Spring3.1后出现
// 继承自ProxyProcessorSupport,所以具有动态代理相关属性~ 方便创建代理对象
public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor {

// 这里会缓存所有被处理的Bean~~~ eligible:合适的
private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);

//postProcessBeforeInitialization方法什么不做~
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}

// 关键是这里。当Bean初始化完成后这里会执行,这里会决策看看要不要对此Bean创建代理对象再返回~~~
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (this.advisor == null || bean instanceof AopInfrastructureBean) {
// Ignore AOP infrastructure such as scoped proxies.
return bean;
}

// 如果此Bean已经被代理了(比如已经被事务那边给代理了~~)
if (bean instanceof Advised) {
Advised advised = (Advised) bean;

// 此处拿的是AopUtils.getTargetClass(bean)目标对象,做最终的判断
// isEligible()是否合适的判断方法 是本文最重要的一个方法,下文解释~
// 此处还有个小细节:isFrozen为false也就是还没被冻结的时候,就只向里面添加一个切面接口 并不要自己再创建代理对象了 省事
if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
// Add our local Advisor to the existing proxy's Advisor chain...
// beforeExistingAdvisors决定这该advisor最先执行还是最后执行
// 此处的advisor为:AsyncAnnotationAdvisor 它切入Class和Method标注有@Aysnc注解的地方~~~
if (this.beforeExistingAdvisors) {
advised.addAdvisor(0, this.advisor);
} else {
advised.addAdvisor(this.advisor);
}
return bean;
}
}

// 若不是代理对象,此处就要下手了~~~~isEligible() 这个方法特别重要
if (isEligible(bean, beanName)) {
// copy属性 proxyFactory.copyFrom(this); 生成一个新的ProxyFactory
ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
// 如果没有强制采用CGLIB 去探测它的接口~
if (!proxyFactory.isProxyTargetClass()) {
evaluateProxyInterfaces(bean.getClass(), proxyFactory);
}
// 添加进此切面~~ 最终为它创建一个getProxy 代理对象
proxyFactory.addAdvisor(this.advisor);
//customize交给子类复写(实际子类目前都没有复写~)
customizeProxyFactory(proxyFactory);
return proxyFactory.getProxy(getProxyClassLoader());
}

// No proxy needed.
return bean;
}

// 我们发现BeanName最终其实是没有用到的~~~
// 但是子类AbstractBeanFactoryAwareAdvisingPostProcessor是用到了的 没有做什么 可以忽略~~~
protected boolean isEligible(Object bean, String beanName) {
return isEligible(bean.getClass());
}
protected boolean isEligible(Class<?> targetClass) {
// 首次进来eligible的值肯定为null~~~
Boolean eligible = this.eligibleBeans.get(targetClass);
if (eligible != null) {
return eligible;
}
// 如果根本就没有配置advisor 也就不用看了~
if (this.advisor == null) {
return false;
}

// 最关键的就是canApply这个方法,如果AsyncAnnotationAdvisor 能切进它 那这里就是true
// 本例中方法标注有@Aysnc注解,所以铁定是能被切入的 返回true继续上面方法体的内容
eligible = AopUtils.canApply(this.advisor, targetClass);
this.eligibleBeans.put(targetClass, eligible);
return eligible;
}
...
}

经此一役,根本原理是只要能被切面AsyncAnnotationAdvisor切入(即只需要类/方法有标注@Async注解即可)的Bean最终都会生成一个代理对象(若已经是代理对象里,只需要加入该切面即可了~)赋值给上面的exposedObject作为返回最终add进Spring容器内~

针对上面的步骤,为了辅助理解,我尝试总结文字描述如下:

context.getBean(A)开始创建A,A实例化完成后给A的依赖属性b开始赋值~
context.getBean(B)开始创建B,B实例化完成后给B的依赖属性a开始赋值~
重点:此时因为A支持循环依赖,所以会执行A的getEarlyBeanReference方法得到它的早期引用。而执行getEarlyBeanReference()的时候因为@Async根本还没执行,所以最终返回的仍旧是原始对象的地址
B完成初始化、完成属性的赋值,此时属性field持有的是Bean A原始类型的引用~
完成了A的属性的赋值(此时已持有B的实例的引用),继续执行初始化方法initializeBean(...),在此处会解析@Aysnc注解,从而生成一个代理对象,所以最终exposedObject是一个代理对象(而非原始对象)最终加入到容器里~
尴尬场面出现了:B引用的属性A是个原始对象,而此处准备return的实例A竟然是个代理对象,也就是说B引用的并非是最终对象(不是最终放进容器里的对象)
执行自检程序:由于allowRawInjectionDespiteWrapping默认值是false,表示不允许上面不一致的情况发生,so最终就抛错了~
此步骤是由我个人即兴总结,希望能帮助到小伙伴们理解。若有不对的地方,还请指出让帮忙我斧正

解决方案
通过上面分析,知道了问题的根本原因,现总结出解决上述新问题的解决方案,可分为下面三种方案:

把allowRawInjectionDespiteWrapping设置为true
使用@Lazy或者@ComponentScan(lazyInit = true)解决
不要让@Async的Bean参与循环依赖
1、把allowRawInjectionDespiteWrapping设置为true:

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

这样配置后,容器启动将不再报错了,但是但是但是:Bean A的@Aysnc方法将不起作用了,因为Bean B里面依赖的a是个原始对象,所以它最终没法执行异步操作(即使容器内的a是个代理对象):


需要注意的是:但此时候Spring容器里面的Bean A是Proxy代理对象的~~~

但是此种情况若是正常依赖(非循环依赖)的a,注入的是代理对象,@Async异步依旧是会生效的哦~

这种解决方式一方面没有达到真正的目的(毕竟Bean A上的@Aysnc没有生效)。

由于它只对循环依赖内的Bean受影响,所以影响范围并不是全局,因此当找不到更好办法的时候,此种这样也不失是一个不错的方案,所以我个人对此方案的态度是不建议,也不反对。

2、使用@Lazy或者@ComponentScan(lazyInit = true)解决

本处以使用@Lazy为例:(强烈不建议使用@ComponentScan(lazyInit = true)作用范围太广了,容易产生误伤)

@Service
public class B implements BInterface {
@Lazy
@Autowired
private AInterface a;

@Override
public void funB() {
System.out.println("线程名称:" + Thread.currentThread().getName());
a.funA();
}
}

注意此@Lazy注解加的位置,因为a最终会是@Async的代理对象,所以在@Autowired它的地方加
另外,若不存在循环依赖而是直接引用a,是不用加@Lazy的

只需要在Bean b的依赖属性上加上@Lazy即可。(因为是B希望依赖进来的是最终的代理对象进来,所以B加上即可,A上并不需要加)

最终的结果让人满意:启动正常,并且@Async异步效果也生效了,因此本方案我是推荐的

但是需要稍微注意的是:此种情况下B里持有A的引用和Spring容器里的A并不是同一个,如下图:


两处实例a的地址值是不一样的,容器内的是$Proxy@6914,B持有的是$Proxy@5899。

关于@Autowired和@Lazy的联合使用为何是此现象,其实@Lazy的代理对象是由ContextAnnotationAutowireCandidateResolver生成的,具体参考博文:【小家Spring】Spring依赖注入(DI)核心接口AutowireCandidateResolver深度分析,解析@Lazy、@Qualifier注解的原理

3、不要让@Async的Bean参与循环依赖
显然如果方案3如果能够解决它肯定是最优的方案。奈何它却是现实情况中最为难达到的方案。
因为在实际业务开发中像循环依赖、类内方法调用等情况并不能避免,除非重新设计、按规范改变代码结构,因此此种方案就见仁见智吧~

为何@Transactional即使循环依赖也没有问题呢?
最后回答小伙伴给我提问的这个问题:同为创建动态代理对象,同为一个注解标注在类上 / 方法上,为何@Transactional就不会出现这种启动报错呢?

其实这个问题的答案在上篇文章的后半拉已经解释了,详见
【小家Spring】一文告诉你Spring是如何利用"三级缓存"巧妙解决Bean的循环依赖问题的

虽说他俩的原理都是产生代理对象,且注解的使用方式几乎无异。so区别Spring对它哥俩的解析不同,也就是他们代理的创建的方式不同:

@Transactional使用的是自动代理创建器AbstractAutoProxyCreator,上篇文章详细描述了,它实现了getEarlyBeanReference()方法从而很好的对循环依赖提供了支持
@Async的代理创建使用的是AsyncAnnotationBeanPostProcessor单独的后置处理器实现的,它只在一处postProcessAfterInitialization()实现了对代理对象的创建,因此若出现它被循环依赖了,就会报错如上~~~
so,虽然从表象上看这两个注解的实现方式一样,但细咬其实现过程细节上,两者差异性还是非常明显的。了解了实现方式上的差异后,自然就不难理解为何有报错和有不报错了~

最后,在理解原理的基础上还需要注意如下这个case(加深理解),若是下面这种情况,其实是启动不报错且可以正常work的:

和上面示例相比:唯一区别是把@Async写在bean B上而A没有写(上面是写在bean A上而B中没有写)

@Service
public class A implements AInterface{
@Autowired
private BInterface b;
@Override
public void funA() {
}
}

@Service
public class B implements BInterface {
@Autowired
private AInterface a;
@Async // 写在B的方法上 这样B最终会被创建代理对象
@Override
public void funB() {
a.funA();
}
}

备注:若按照正常Spring容器会先初始化A,启动就肯定是不会报错的,这也就是我上面说的结论:这种情况下默认是可以work的

通过猜测也能够猜到,A和B不是对等的关系,处理结果和Bean的初始化顺序有关。

至于Spring对Bean的实例化、初始化顺序,若没有特别干预的情况下,它和类名字母排序有关~

为了说明问题,此处我人工干预先让Spring容器初始化B(此处方案为使用@DependsOn("b")):

@DependsOn("b")
@Service
public class A implements AInterface { ... }

这样干预能够保证B肯定在A之前初始化,然后启动也就会报同样错误:(当然此处报错信息是bean b):

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'b': Bean with name 'b' has been injected into other beans [a] 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:622)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
...

若技术敏感点的小伙伴发现,此处能够给我们一个解决自己依赖自己问题的另外一个思路,是否可以考虑干预一下Bean的初始化顺序来达到正常启动的目的呢?
理论上是可行的,但是在实操过程中个人不太建议这么去干(如果有更好的方案的话)~

总结
虽然Spring官方也不推荐循环依赖,但是一个是理想情况,一个现实情况,它俩是有差距和差异的。
现实使用中,特别是业务开发中循环依赖可以说是几乎避免不了的,因此知其然而知其所以然后,才能彻底的大彻大悟,遇到问题不再蒙圈。

使用AopContext.currentProxy();方式解决同类方法调用的方案,由于这种方式也是一个较大的话题,限于篇幅,且听紧邻的下文分解~

 

什么是“异步调用”?“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。

同步调用

下面通过一个简单示例来直观的理解什么是同步调用:

定义Task类,创建三个处理函数分别模拟三个执行任务的操作,操作消耗时间随机取(10秒内)

@Slf4j
@Component
public class AsyncTasks {

public static Random random = new Random();

public void doTaskOne() throws Exception {
log.info("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
}

public void doTaskTwo() throws Exception {
log.info("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务二,耗时:" + (end - start) + "毫秒");
}

public void doTaskThree() throws Exception {
log.info("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务三,耗时:" + (end - start) + "毫秒");
}

}

在单元测试用例中,注入Task对象,并在测试用例中执行doTaskOnedoTaskTwodoTaskThree三个函数。

@Slf4j
@SpringBootTest
public class Chapter75ApplicationTests {

@Autowired
private AsyncTasks asyncTasks;

@Test
public void test() throws Exception {
asyncTasks.doTaskOne();
asyncTasks.doTaskTwo();
asyncTasks.doTaskThree();
}

}

执行单元测试,可以看到类似如下输出:

2021-09-11 23:19:12.922  INFO 92539 --- [           main] com.didispace.chapter75.AsyncTasks       : 开始做任务一
2021-09-11 23:19:17.788 INFO 92539 --- [ main] com.didispace.chapter75.AsyncTasks : 完成任务一,耗时:4865毫秒
2021-09-11 23:19:17.788 INFO 92539 --- [ main] com.didispace.chapter75.AsyncTasks : 开始做任务二
2021-09-11 23:19:24.851 INFO 92539 --- [ main] com.didispace.chapter75.AsyncTasks : 完成任务二,耗时:7063毫秒
2021-09-11 23:19:24.851 INFO 92539 --- [ main] com.didispace.chapter75.AsyncTasks : 开始做任务三
2021-09-11 23:19:26.928 INFO 92539 --- [ main] com.didispace.chapter75.AsyncTasks : 完成任务三,耗时:2076毫秒

任务一、任务二、任务三顺序的执行完了,换言之doTaskOne、doTaskTwo、doTaskThree三个函数顺序的执行完成。

异步调用

上述的同步调用虽然顺利的执行完了三个任务,但是可以看到执行时间比较长,若这三个任务本身之间不存在依赖关系,可以并发执行的话,同步调用在执行效率方面就比较差,可以考虑通过异步调用的方式来并发执行。

在Spring Boot中,我们只需要通过使用@Async注解就能简单的将原来的同步函数变为异步函数,Task类改在为如下模式:

@Slf4j
@Component
public class AsyncTasks {

public static Random random = new Random();

@Async
public void doTaskOne() throws Exception {
log.info("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
}

@Async
public void doTaskTwo() throws Exception {
log.info("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务二,耗时:" + (end - start) + "毫秒");
}

@Async
public void doTaskThree() throws Exception {
log.info("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务三,耗时:" + (end - start) + "毫秒");
}

}

为了让@Async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsync,如下所示:

@EnableAsync
@SpringBootApplication
public class Chapter75Application {

public static void main(String[] args) {
SpringApplication.run(Chapter75Application.class, args);
}

}

此时可以反复执行单元测试,您可能会遇到各种不同的结果,比如:

  • 没有任何任务相关的输出
  • 有部分任务相关的输出
  • 乱序的任务相关的输出

  • 原因是目前doTaskOnedoTaskTwodoTaskThree三个函数的时候已经是异步执行了。主程序在异步调用之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就自动结束了,导致了不完整或是没有输出任务相关内容的情况。

注:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效

异步回调

为了让doTaskOnedoTaskTwodoTaskThree能正常结束,假设我们需要统计一下三个任务并发执行共耗时多少,这就需要等到上述三个函数都完成调动之后记录时间,并计算结果。

那么我们如何判断上述三个异步调用是否已经执行完成呢?我们需要使用CompletableFuture<T>来返回异步调用的结果,就像如下方式改造doTaskOne函数:

    @Async
public CompletableFuture<String> doTaskOne() throws Exception {
log.info("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
return CompletableFuture.completedFuture("任务一完成");
}

按照如上方式改造一下其他两个异步函数之后,下面我们改造一下测试用例,让测试在等待完成三个异步调用之后来做一些其他事情。

@Test
public void test() throws Exception {
long start = System.currentTimeMillis();

CompletableFuture<String> task1 = asyncTasks.doTaskOne();
CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
CompletableFuture<String> task3 = asyncTasks.doTaskThree();

CompletableFuture.allOf(task1, task2, task3).join();

long end = System.currentTimeMillis();

log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
}

看看我们做了哪些改变:

  • 在测试用例一开始记录开始时间
  • 在调用三个异步函数的时候,返回CompletableFuture<String>类型的结果对象
  • 通过CompletableFuture.allOf(task1, task2, task3).join()实现三个异步任务都结束之前的阻塞效果
  • 三个任务都完成之后,根据结束时间 - 开始时间,计算出三个任务并发执行的总耗时。

执行一下上述的单元测试,可以看到如下结果:

2021-09-11 23:33:38.842  INFO 95891 --- [         task-3] com.didispace.chapter75.AsyncTasks       : 开始做任务三
2021-09-11 23:33:38.842 INFO 95891 --- [ task-2] com.didispace.chapter75.AsyncTasks : 开始做任务二
2021-09-11 23:33:38.842 INFO 95891 --- [ task-1] com.didispace.chapter75.AsyncTasks : 开始做任务一
2021-09-11 23:33:45.155 INFO 95891 --- [ task-2] com.didispace.chapter75.AsyncTasks : 完成任务二,耗时:6312毫秒
2021-09-11 23:33:47.308 INFO 95891 --- [ task-3] com.didispace.chapter75.AsyncTasks : 完成任务三,耗时:8465毫秒
2021-09-11 23:33:47.403 INFO 95891 --- [ task-1] com.didispace.chapter75.AsyncTasks : 完成任务一,耗时:8560毫秒
2021-09-11 23:33:47.404 INFO 95891 --- [ main] c.d.chapter75.Chapter75ApplicationTests : 任务全部完成,总耗时:8590毫秒

可以看到,通过异步调用,让任务一、二、三并发执行,有效的减少了程序的总运行时间。

本系列教程《Spring Boot 2.x基础教程》点击直达!,欢迎收藏与转发!如果学习过程中如遇困难?可以加入我们的 Spring技术交流群,参与交流与讨论,更好的学习与进步!

代码示例

本文的完整工程可以查看下面仓库中2.x目录下的chapter7-5工程:

 

通过上一篇:配置@Async异步任务的线程池的介绍,你应该已经了解到异步任务的执行背后有一个线程池来管理执行任务。为了控制异步任务的并发不影响到应用的正常运作,我们必须要对线程池做好相应的配置,防止资源的过渡使用。除了默认线程池的配置之外,还有一类场景,也是很常见的,那就是多任务情况下的线程池隔离。

什么是线程池的隔离,为什么要隔离

可能有的小伙伴还不太了解什么是线程池的隔离,为什么要隔离?。所以,我们先来看看下面的场景案例:

@RestController
public class HelloController {

@Autowired
private AsyncTasks asyncTasks;

@GetMapping("/api-1")
public String taskOne() {
CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");

CompletableFuture.allOf(task1, task2, task3).join();
return "";
}

@GetMapping("/api-2")
public String taskTwo() {
CompletableFuture<String> task1 = asyncTasks.doTaskTwo("1");
CompletableFuture<String> task2 = asyncTasks.doTaskTwo("2");
CompletableFuture<String> task3 = asyncTasks.doTaskTwo("3");

CompletableFuture.allOf(task1, task2, task3).join();
return "";
}

}

上面的代码中,有两个API接口,这两个接口的具体执行逻辑中都会把执行过程拆分为三个异步任务来实现。

好了,思考一分钟,想一下。如果这样实现,会有什么问题吗?


上面这段代码,在API请求并发不高,同时如果每个任务的处理速度也够快的时候,是没有问题的。但如果并发上来或其中某几个处理过程扯后腿了的时候。这两个提供不相干服务的接口可能会互相影响。比如:假设当前线程池配置的最大线程数有2个,这个时候/api-1接口中task1和task2处理速度很慢,阻塞了;那么此时,当用户调用api-2接口的时候,这个服务也会阻塞!

造成这种现场的原因是:默认情况下,所有用@Async创建的异步任务都是共用的一个线程池,所以当有一些异步任务碰到性能问题的时候,是会直接影响其他异步任务的。

为了解决这个问题,我们就需要对异步任务做一定的线程池隔离,让不同的异步任务互不影响。

不同异步任务配置不同线程池

下面,我们就来实际操作一下!

第一步:初始化多个线程池,比如下面这样:

@EnableAsync
@Configuration
public class TaskPoolConfig {

@Bean
public Executor taskExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(10);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("executor-1-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}

@Bean
public Executor taskExecutor2() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(10);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("executor-2-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}

注意:这里特地用executor.setThreadNamePrefix设置了线程名的前缀,这样可以方便观察后面具体执行的顺序。

第二步:创建异步任务,并指定要使用的线程池名称

@Slf4j
@Component
public class AsyncTasks {

public static Random random = new Random();

@Async("taskExecutor1")
public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {
log.info("开始任务:{}", taskNo);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
return CompletableFuture.completedFuture("任务完成");
}

@Async("taskExecutor2")
public CompletableFuture<String> doTaskTwo(String taskNo) throws Exception {
log.info("开始任务:{}", taskNo);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
return CompletableFuture.completedFuture("任务完成");
}

}

这里@Async注解中定义的taskExecutor1taskExecutor2就是线程池的名字。由于在第一步中,我们没有具体写两个线程池Bean的名称,所以默认会使用方法名,也就是taskExecutor1taskExecutor2

第三步:写个单元测试来验证下,比如下面这样:

@Slf4j
@SpringBootTest
public class Chapter77ApplicationTests {

@Autowired
private AsyncTasks asyncTasks;

@Test
public void test() throws Exception {
long start = System.currentTimeMillis();

// 线程池1
CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");

// 线程池2
CompletableFuture<String> task4 = asyncTasks.doTaskTwo("4");
CompletableFuture<String> task5 = asyncTasks.doTaskTwo("5");
CompletableFuture<String> task6 = asyncTasks.doTaskTwo("6");

// 一起执行
CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();

long end = System.currentTimeMillis();

log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
}

}

在上面的单元测试中,一共启动了6个异步任务,前三个用的是线程池1,后三个用的是线程池2。

先不执行,根据设置的核心线程2和最大线程数2,来分析一下,大概会是怎么样的执行情况?

  1. 线程池1的三个任务,task1和task2会先获得执行线程,然后task3因为没有可分配线程进入缓冲队列
  2. 线程池2的三个任务,task4和task5会先获得执行线程,然后task6因为没有可分配线程进入缓冲队列
  3. 任务task3会在task1或task2完成之后,开始执行
  4. 任务task6会在task4或task5完成之后,开始执行

分析好之后,执行下单元测试,看看是否是这样的:

2021-09-15 23:45:11.369  INFO 61670 --- [   executor-1-1] com.didispace.chapter77.AsyncTasks       : 开始任务:1
2021-09-15 23:45:11.369 INFO 61670 --- [ executor-2-2] com.didispace.chapter77.AsyncTasks : 开始任务:5
2021-09-15 23:45:11.369 INFO 61670 --- [ executor-2-1] com.didispace.chapter77.AsyncTasks : 开始任务:4
2021-09-15 23:45:11.369 INFO 61670 --- [ executor-1-2] com.didispace.chapter77.AsyncTasks : 开始任务:2
2021-09-15 23:45:15.905 INFO 61670 --- [ executor-2-1] com.didispace.chapter77.AsyncTasks : 完成任务:4,耗时:4532 毫秒
2021-09-15 23:45:15.905 INFO 61670 --- [ executor-2-1] com.didispace.chapter77.AsyncTasks : 开始任务:6
2021-09-15 23:45:18.263 INFO 61670 --- [ executor-1-2] com.didispace.chapter77.AsyncTasks : 完成任务:2,耗时:6890 毫秒
2021-09-15 23:45:18.263 INFO 61670 --- [ executor-1-2] com.didispace.chapter77.AsyncTasks : 开始任务:3
2021-09-15 23:45:18.896 INFO 61670 --- [ executor-2-2] com.didispace.chapter77.AsyncTasks : 完成任务:5,耗时:7523 毫秒
2021-09-15 23:45:19.842 INFO 61670 --- [ executor-1-2] com.didispace.chapter77.AsyncTasks : 完成任务:3,耗时:1579 毫秒
2021-09-15 23:45:20.551 INFO 61670 --- [ executor-1-1] com.didispace.chapter77.AsyncTasks : 完成任务:1,耗时:9178 毫秒
2021-09-15 23:45:24.117 INFO 61670 --- [ executor-2-1] com.didispace.chapter77.AsyncTasks : 完成任务:6,耗时:8212 毫秒
2021-09-15 23:45:24.117 INFO 61670 --- [ main] c.d.chapter77.Chapter77ApplicationTests : 任务全部完成,总耗时:12762毫秒

好了,今天的学习就到这里!如果您学习过程中如遇困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!更多Spring Boot教程可以点击直达!,欢迎收藏与转发支持!

代码示例

本文的完整工程可以查看下面仓库中2.x目录下的chapter7-7工程:

 

posted @ 2020-01-17 10:07  沧海一滴  阅读(5219)  评论(1编辑  收藏  举报