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

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

前言


今天在自己工程中使用@Async的时候,碰到了一个问题:Spring循环依赖(circular reference)问题。

或许刚说到这,有的小伙伴就会大惊失色了。Spring不是解决了循环依赖问题吗,它是支持循环依赖的呀?怎么会呢?


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


不得不提,关于@Async的使用姿势,请参阅:

【小家Spring】Spring异步处理@Async的使用以及原理、源码分析(@EnableAsync)

关于Spring Bean的循环依赖问题,请参阅:

【小家Spring】一文告诉你Spring是如何利用"三级缓存"巧妙解决Bean的循环依赖问题的


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


  1. 已开启@EnableAsync的支持
  2. @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不生效的根本原因都是同一个:直接调用了本类方法而非接口方法/代理对象方法。

解决这类不生效问题的方案一般我们都有两种:


  1. 自己注入自己,然后再调用接口方法(当然此处的一个变种是使用编程方式形如:AInterface a = applicationContext.getBean(AInterface.class);这样子手动获取也是可行的~~~本文不讨论这种比较直接简单的方式)
  2. 使用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.");
        }
      }
    }
  }
  ...
}


相关文章
|
20天前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
|
20天前
|
缓存 安全 Java
Spring框架中Bean是如何加载的?从底层源码入手,详细解读Bean的创建流程
从底层源码入手,通过代码示例,追踪AnnotationConfigApplicationContext加载配置类、启动Spring容器的整个流程,并对IOC、BeanDefinition、PostProcesser等相关概念进行解释
Spring框架中Bean是如何加载的?从底层源码入手,详细解读Bean的创建流程
|
19天前
|
XML Java 数据格式
Spring IOC—基于XML配置Bean的更多内容和细节(通俗易懂)
Spring 第二节内容补充 关于Bean配置的更多内容和细节 万字详解!
116 18
Spring IOC—基于XML配置Bean的更多内容和细节(通俗易懂)
|
7天前
|
XML Java 数据格式
spring复习02,xml配置管理bean
详细讲解了Spring框架中基于XML配置文件管理bean的各种方式,包括获取bean、依赖注入、特殊值处理、属性赋值、集合类型处理、p命名空间、bean作用域及生命周期和自动装配。
spring复习02,xml配置管理bean
|
1天前
|
Java 关系型数据库 数据库连接
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第3天】Spring Boot项目中数据库连接问题可能源于配置错误或依赖缺失。YAML配置文件的格式不正确,如缩进错误,会导致解析失败;而数据库驱动不匹配、连接字符串或认证信息错误同样引发连接异常。解决方法包括检查并修正YAML格式,确认配置属性无误,以及添加正确的数据库驱动依赖。利用日志记录和异常信息分析可辅助问题排查。
22 10
|
4天前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
13 4
|
8天前
|
Java Spring 容器
Spring使用异步注解@Async正确姿势
Spring使用异步注解@Async正确姿势,异步任务,spring boot
|
7天前
|
XML Java 数据格式
spring复习03,注解配置管理bean
Spring框架中使用注解配置管理bean的方法,包括常用注解的标识组件、扫描组件、基于注解的自动装配以及使用注解后的注意事项,并提供了一个基于注解自动装配的完整示例。
spring复习03,注解配置管理bean
|
2月前
|
前端开发 小程序 Java
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
本文详细介绍了如何在SpringBoot项目中统一处理接口返回结果及全局异常。首先,通过封装`ResponseResult`类,实现了接口返回结果的规范化,包括状态码、状态信息、返回信息和数据等字段,提供了多种成功和失败的返回方法。其次,利用`@RestControllerAdvice`和`@ExceptionHandler`注解配置全局异常处理,捕获并友好地处理各种异常信息。
240 0
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
|
Java Spring
Spring常见异常笔记(8)
Spring常见异常笔记(8)
105 0
下一篇
无影云桌面