使用@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.");
        }
      }
    }
  }
  ...
}


相关文章
|
13天前
|
Java Spring
【Spring】方法注解@Bean,配置类扫描路径
@Bean方法注解,如何在同一个类下面定义多个Bean对象,配置扫描路径
140 73
|
8天前
|
Java Spring 容器
【SpringFramework】Spring IoC-基于注解的实现
本文主要记录基于Spring注解实现IoC容器和DI相关知识。
40 21
|
30天前
|
Java Nacos Sentinel
Spring Cloud Alibaba:一站式微服务解决方案
Spring Cloud Alibaba(简称SCA) 是一个基于 Spring Cloud 构建的开源微服务框架,专为解决分布式系统中的服务治理、配置管理、服务发现、消息总线等问题而设计。
236 13
Spring Cloud Alibaba:一站式微服务解决方案
|
13天前
|
存储 Java Spring
【Spring】获取Bean对象需要哪些注解
@Conntroller,@Service,@Repository,@Component,@Configuration,关于Bean对象的五个常用注解
|
13天前
|
Java Spring
【Spring配置】idea编码格式导致注解汉字无法保存
问题一:对于同一个项目,我们在使用idea的过程中,使用汉字注解完后,再打开该项目,汉字变成乱码问题二:本来a项目中,汉字注解调试好了,没有乱码了,但是创建出来的新的项目,写的注解又成乱码了。
|
16天前
|
运维 监控 Java
为何内存不够用?微服务改造启动多个Spring Boot的陷阱与解决方案
本文记录并复盘了生产环境中Spring Boot应用内存占用过高的问题及解决过程。系统上线初期运行正常,但随着业务量上升,多个Spring Boot应用共占用了64G内存中的大部分,导致应用假死。通过jps和jmap工具排查发现,原因是运维人员未设置JVM参数,导致默认配置下每个应用占用近12G内存。最终通过调整JVM参数、优化堆内存大小等措施解决了问题。建议在生产环境中合理设置JVM参数,避免资源浪费和性能问题。
46 3
|
1月前
|
安全 Java API
实现跨域请求:Spring Boot后端的解决方案
本文介绍了在Spring Boot中处理跨域请求的三种方法:使用`@CrossOrigin`注解、全局配置以及自定义过滤器。每种方法都适用于不同的场景和需求,帮助开发者灵活地解决跨域问题,确保前后端交互顺畅与安全。
|
Java 应用服务中间件 数据库连接
Spring全家桶之Spring篇深度分析(一)
Spring 框架不局限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 中受益。Spring 框架还是一个超级粘合平台,除了自己提供功能外,还提供粘合其他技术和框架的能力。
Spring全家桶之Spring篇深度分析(一)
|
3月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
265 2
|
15天前
|
Java 数据库连接 Maven
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)