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

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

这里知识点避开不@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容器内~


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


  1. context.getBean(A)开始创建A,A实例化完成后给A的依赖属性b开始赋值~
  2. context.getBean(B)开始创建B,B实例化完成后给B的依赖属性a开始赋值~
  3. 重点:此时因为A支持循环依赖,所以会执行A的getEarlyBeanReference方法得到它的早期引用。而执行getEarlyBeanReference()的时候因为@Async根本还没执行,所以最终返回的仍旧是原始对象的地址
  4. B完成初始化、完成属性的赋值,此时属性field持有的是Bean A原始类型的引用~
  5. 完成了A的属性的赋值(此时已持有B的实例的引用),继续执行初始化方法initializeBean(...),在此处会解析@Aysnc注解,从而生成一个代理对象,所以最终exposedObject是一个代理对象(而非原始对象)最终加入到容器里~
  6. 尴尬场面出现了:B引用的属性A是个原始对象,而此处准备return的实例A竟然是个代理对象,也就是说B引用的并非是最终对象(不是最终放进容器里的对象)
  7. 执行自检程序:由于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代理对象的~~~


image.png

但是此种情况若是正常依赖(非循环依赖)的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并不是同一个,如下图:


image.png


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


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

相关文章
|
监控 安全 Java
解决 Spring Boot 中 SecurityConfig 循环依赖问题的详解
本文详细解析了在 Spring Boot 中配置 `SecurityConfig` 时可能遇到的循环依赖问题。通过分析错误日志与代码,指出问题根源在于 `SecurityConfig` 类中不当的依赖注入方式。文章提供了多种解决方案:移除 `configureGlobal` 方法、定义 `DaoAuthenticationProvider` Bean、使用构造函数注入以及分离配置类等。此外,还讨论了 `@Lazy` 注解和允许循环引用的临时手段,并强调重构以避免循环依赖的重要性。通过合理设计 Bean 依赖关系,可确保应用稳定启动并提升代码可维护性。
980 0
|
Java Maven 微服务
微服务——SpringBoot使用归纳——Spring Boot集成 Swagger2 展现在线接口文档——Swagger2 的 maven 依赖
在项目中使用Swagger2工具时,需导入Maven依赖。尽管官方最高版本为2.8.0,但其展示效果不够理想且稳定性欠佳。实际开发中常用2.2.2版本,因其稳定且界面友好。以下是围绕2.2.2版本的Maven依赖配置,包括`springfox-swagger2`和`springfox-swagger-ui`两个模块。
612 0
|
druid Java 关系型数据库
Spring Boot与Druid升级解决方案
好的,我需要帮助用户解决他们遇到的数据库连接问题,并升级项目的依赖。首先,用户提供的错误信息是关于Spring Boot应用在初始化数据源时抛出的异常,具体是Druid连接池验证连接失败。同时,用户希望升级项目的依赖版本。
1105 10
|
监控 Java 关系型数据库
Spring Boot整合MySQL主从集群同步延迟解决方案
本文针对电商系统在Spring Boot+MyBatis架构下的典型问题(如大促时订单状态延迟、库存超卖误判及用户信息更新延迟)提出解决方案。核心内容包括动态数据源路由(强制读主库)、大事务拆分优化以及延迟感知补偿机制,配合MySQL参数调优和监控集成,有效将主从延迟控制在1秒内。实际测试表明,在10万QPS场景下,订单查询延迟显著降低,超卖误判率下降98%。
539 5
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
1514 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
|
SQL 前端开发 Java
深入分析 Spring Boot 项目开发中的常见问题与解决方案
本文深入分析了Spring Boot项目开发中的常见问题与解决方案,涵盖视图路径冲突(Circular View Path)、ECharts图表数据异常及SQL唯一约束冲突等典型场景。通过实际案例剖析问题成因,并提供具体解决方法,如优化视图解析器配置、改进数据查询逻辑以及合理使用外键约束。同时复习了Spring MVC视图解析原理与数据库完整性知识,强调细节处理和数据验证的重要性,为开发者提供实用参考。
519 0
|
安全 前端开发 Java
Spring Boot 项目中触发 Circular View Path 错误的原理与解决方案
在Spring Boot开发中,**Circular View Path**错误常因视图解析与Controller路径重名引发。当视图名称(如`login`)与请求路径相同,Spring MVC无法区分,导致无限循环调用。解决方法包括:1) 明确指定视图路径,避免重名;2) 将视图文件移至子目录;3) 确保Spring Security配置与Controller路径一致。通过合理设定视图和路径,可有效避免该问题,确保系统稳定运行。
817 0
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
625 0
|
缓存 Java 应用服务中间件
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——依赖导入和Thymeleaf相关配置
在Spring Boot中使用Thymeleaf模板,需引入依赖`spring-boot-starter-thymeleaf`,并在HTML页面标签中声明`xmlns:th=&quot;http://www.thymeleaf.org&quot;`。此外,Thymeleaf默认开启页面缓存,开发时建议关闭缓存以实时查看更新效果,配置方式为`spring.thymeleaf.cache: false`。这可避免因缓存导致页面未及时刷新的问题。
509 0
|
Java 测试技术 数据处理
Spring 异步实现原理与实战分享
全链路压测项目的宗旨就是不让用户感知这个项目的存在,因此我们不可能让用户去对其线程池进行改造的,我们需要主动去适配用户自定义的线程池。 在适配过程的过程中无非就是将线程池替换成 ttl 去解决,可通过代理或者替换 Bean 的方式实现,这方面不是本文的内容,本文主要是深入 Spring 异步实现的原理,让大家对 Spring 异步编程不再陌生!
422 0
Spring 异步实现原理与实战分享