前言
今天在自己工程中使用@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."); } } } } ... }