首頁 > Java > java教程 > spring boot計畫使用@Async註解的坑怎麼解決

spring boot計畫使用@Async註解的坑怎麼解決

王林
發布: 2023-05-12 08:28:13
轉載
1190 人瀏覽過

    背景

    前段時間,一個同事小姐姐跟我說她的專案起不來了,讓我幫忙看一下,本著助人為樂的精神,這個忙一定要去幫忙。

    於是,我在她的控制台發現瞭如下的異常訊息:

    Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans. 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.supportle.
     at org.springframework.beans.factory.support.AbseanC. AbstractAutowireCapableBeanFactory.java:602)
     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495)1405000000050000000000000000000000m. Bean$0 (AbstractBeanFactory.java:317)
     at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)

    Incm#Incurrent#Incm#Inception反應是出現了循環依賴的問題。但仔細一想,Spring不是已經解決了循環依賴的問題麼,怎麼還報這個錯。於是,我就詢問小姐姐改了什麼東西,她說在方法上加了@Async註解。

    這裡我模擬當時的程式碼,AService 和 BService 互相引用,AService的 save() 方法加了 @Async 註解。

    @Component
    public class AService {
        @Resource
        private BService bService;
        @Async
        public void save() {
        }
    }
    @Component
    public class BService {
        @Resource
        private AService aService;
    }
    登入後複製

    也就是這段程式碼會報BeanCurrentlyInCreationException異常,難道是@Async註解遇上循環依賴的時候,Spring無法解決?為了驗證這個猜想,我將@Async註解去掉之後,再次啟動項目,項目成功起來了。於是基本上可以下結論,那就是@Async註解遇上循環依賴的時候,Spring的確無法解決。

    雖然問題的原因已經找到了,但是又引出以下幾個問題:

    • @Async註解是如何運作的?

    • 為什麼@Async註解遇上循環依賴,Spring無法解決?

    • 出現循環依賴異常之後如何解決?

    @Async註解是如何運作的?

    @Async註解起作用是靠AsyncAnnotationBeanPostProcessor這個類別實現的,這個類別會處理@Async註解。 AsyncAnnotationBeanPostProcessor這個類別的物件是由@EnableAsync註解放入到Spring容器的,這也是為什麼需要使用@EnableAsync註解來啟動讓@Async註解起作用的根本原因。

    AsyncAnnotationBeanPostProcessor

    spring boot計畫使用@Async註解的坑怎麼解決

    類別系統

    這個類別實作了BeanPostProcessor 接口,實作了postProcessAfterInitialization 方法,是在其父類別AbstractAdvisingBeanPostProcessor 中實作的,也就是說當Bean的初始化階段完成之後會回呼AsyncAnnotationBeanPostProcessor 的postProcessAfterInitialization 方法。之所以會回調,是因為在Bean的生命週期中,當Bean初始化完成之後,會回呼所有的BeanPostProcessor 的postProcessAfterInitialization 方法,程式碼如下:

    @Override
    public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)throws BeansException {
        Object result = existingBean;
        for (BeanPostProcessor processor : getBeanPostProcessors()) {
             Object current = processor.postProcessAfterInitialization(result, beanName);
             if (current == null) {
                return result;
             }
             result = current;
         }
        return result;
    }
    登入後複製

    AsyncAnnotationBeanPostProcessor 對於postProcessAfterInitialrration 方法實作:#rrration 方法實作:#rr> ##此方法的主要作用是用來對方法入參的物件進行動態代理的,當入參的物件的類別加了@Async註解,那麼這個方法就會對這個物件進行動態代理,最後會回傳參對象的代理對像出去。至於如何判斷方法有沒有加@Async註解,是靠 isEligible(bean, beanName) 來判斷的。由於這段程式碼牽扯到動態代理底層的知識,這裡就不詳細展開了。

    AsyncAnnotationBeanPostProcessor作用spring boot計畫使用@Async註解的坑怎麼解決

    綜上所述,可以得到一個結論,那就是當Bean創建過程中初始化階段完成之後,會呼叫AsyncAnnotationBeanPostProcessor 的postProcessAfterInitialization 的方法,對加了@Async註解的類別的物件進行動態代理,然後傳回一個代理物件回去。

    虽然这里我们得出@Async注解的作用是依靠动态代理实现的,但是这里其实又引发了另一个问题,那就是事务注解@Transactional又或者是自定义的AOP切面,他们也都是通过动态代理实现的,为什么使用这些的时候,没见抛出循环依赖的异常?难道他们的实现跟@Async注解的实现不一样?不错,还真的不太一样,请继续往下看。

    AOP是如何实现的?

    我们都知道AOP是依靠动态代理实现的,而且是在Bean的生命周期中起作用,具体是靠 AnnotationAwareAspectJAutoProxyCreator 这个类实现的,这个类会在Bean的生命周期中去处理切面,事务注解,然后生成动态代理。这个类的对象在容器启动的时候,就会被自动注入到Spring容器中。

    AnnotationAwareAspectJAutoProxyCreator 也实现了BeanPostProcessor,也实现了 postProcessAfterInitialization 方法。

    @Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
        if (bean != null) {
           Object cacheKey = getCacheKey(bean.getClass(), beanName);
           if (!this.earlyProxyReferences.contains(cacheKey)) {
               //生成动态代理,如果需要被代理的话
               return wrapIfNecessary(bean, beanName, cacheKey);
           }
         }
        return bean;
    }
    登入後複製

    通过 wrapIfNecessary 方法就会对Bean进行动态代理,如果你的Bean需要被动态代理的话。

    spring boot計畫使用@Async註解的坑怎麼解決

    AnnotationAwareAspectJAutoProxyCreator作用

    也就说,AOP和@Async注解虽然底层都是动态代理,但是具体实现的类是不一样的。一般的AOP或者事务的动态代理是依靠 AnnotationAwareAspectJAutoProxyCreator 实现的,而@Async是依靠 AsyncAnnotationBeanPostProcessor 实现的,并且都是在初始化完成之后起作用,这也就是@Async注解和AOP之间的主要区别,也就是处理的类不一样。

    Spring是如何解决循环依赖的

    Spring在解决循环依赖的时候,是依靠三级缓存来实现的。我曾经写过一篇关于三级缓存的文章,如果有不清楚的小伙伴可以 关注微信公众号 三友的java日记,回复 循环依赖 即可获取原文链接,本文也算是这篇三级缓存文章的续作。

    简单来说,通过缓存正在创建的对象对应的ObjectFactory对象,可以获取到正在创建的对象的早期引用的对象,当出现循环依赖的时候,由于对象没创建完,就可以通过获取早期引用的对象注入就行了。

    而缓存ObjectFactory代码如下:

    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    登入後複製
    protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(singletonFactory, "Singleton factory must not be null");
        synchronized (this.singletonObjects) {
           if (!this.singletonObjects.containsKey(beanName)) {
               this.singletonFactories.put(beanName, singletonFactory);
               this.earlySingletonObjects.remove(beanName);
               this.registeredSingletons.add(beanName);
           }
        }
    }
    登入後複製

    所以缓存的ObjectFactory对象其实是一个lamda表达式,真正获取早期暴露的引用对象其实就是通过 getEarlyBeanReference 方法来实现的。

    getEarlyBeanReference 方法:

    protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
        Object exposedObject = bean;
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
                if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                   SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                   exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
                }
           }
        }
        return exposedObject;
    }
    登入後複製

    getEarlyBeanReference 实现是调用所有的 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 方法。

    而前面提到的 AnnotationAwareAspectJAutoProxyCreator 这个类就实现了 SmartInstantiationAwareBeanPostProcessor 接口,是在父类中实现的:

    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (!this.earlyProxyReferences.contains(cacheKey)) {
            this.earlyProxyReferences.add(cacheKey);
        }
        return wrapIfNecessary(bean, beanName, cacheKey);
    }
    登入後複製

    这个方法最后会调用 wrapIfNecessary 方法,前面也说过,这个方法是获取动态代理的方法,如果需要的话就会代理,比如事务注解又或者是自定义的AOP切面,在早期暴露的时候,就会完成动态代理。

    这下终于弄清楚了,早期暴露出去的原来可能是个代理对象,而且最终是通过AnnotationAwareAspectJAutoProxyCreator这个类的getEarlyBeanReference方法获取的。

    但是AsyncAnnotationBeanPostProcessor并没有实现SmartInstantiationAwareBeanPostProcessor,也就是在获取早期对象这一阶段,并不会调AsyncAnnotationBeanPostProcessor处理@Async注解。

    为什么@Async注解遇上循环依赖,Spring无法解决?

    这里我们就拿前面的例子来说,AService加了@Async注解,AService先创建,发现引用了BService,那么BService就会去创建,当Service创建的过程中发现引用了AService,那么就会通过AnnotationAwareAspectJAutoProxyCreator 这个类实现的 getEarlyBeanReference 方法获取AService的早期引用对象,此时这个早期引用对象可能会被代理,取决于AService是否需要被代理,但是一定不是处理@Async注解的代理,原因前面也说过。

    于是BService创建好之后,注入给了AService,那么AService会继续往下处理,前面说过,当初始化阶段完成之后,会调用所有的BeanPostProcessor的实现的 postProcessAfterInitialization 方法。于是就会回调依次回调 AnnotationAwareAspectJAutoProxyCreator 和 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法实现。

    这段回调有两个细节:

    • AnnotationAwareAspectJAutoProxyCreator 先执行,AsyncAnnotationBeanPostProcessor 后执行,因为 AnnotationAwareAspectJAutoProxyCreator 在前面。

    spring boot計畫使用@Async註解的坑怎麼解決

    • AnnotationAwareAspectJAutoProxyCreator处理的结果会当入参传递给 AsyncAnnotationBeanPostProcessor,applyBeanPostProcessorsAfterInitialization方法就是这么实现的

    AnnotationAwareAspectJAutoProxyCreator回调:会发现AService对象已经被早期引用了,什么都不处理,直接把对象AService给返回

    AsyncAnnotationBeanPostProcessor回调:发现AService类中加了@Async注解,那么就会对AnnotationAwareAspectJAutoProxyCreator返回的对象进行动态代理,然后返回了动态代理对象。

    这段回调完,是不是已经发现了问题。早期暴露出去的对象,可能是AService本身或者是AService的代理对象,而且是通过AnnotationAwareAspectJAutoProxyCreator对象实现的,但是通过AsyncAnnotationBeanPostProcessor的回调,会对AService对象进行动态代理,这就导致AService早期暴露出去的对象跟最后完全创造出来的对象不是同一个,那么肯定就不对了。

    同一个Bean在一个Spring中怎么能存在两个不同的对象呢,于是就会抛出BeanCurrentlyInCreationException异常,这段判断逻辑的代码如下:

    if (earlySingletonExposure) {
      // 获取到早期暴露出去的对象
      Object earlySingletonReference = getSingleton(beanName, false);
      if (earlySingletonReference != null) {
          // 早期暴露的对象不为null,说明出现了循环依赖
          if (exposedObject == bean) {
              // 这个判断的意思就是指 postProcessAfterInitialization 回调没有进行动态代理,如果没有那么就将早期暴露出去的对象赋值给最终暴露(生成)出去的对象,
              // 这样就实现了早期暴露出去的对象和最终生成的对象是同一个了
              // 但是一旦 postProcessAfterInitialization 回调生成了动态代理 ,那么就不会走这,也就是加了@Aysnc注解,是不会走这的
              exposedObject = earlySingletonReference;
          }
          else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
                   // allowRawInjectionDespiteWrapping 默认是false
                   String[] dependentBeans = getDependentBeans(beanName);
                   Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
                   for (String dependentBean : dependentBeans) {
                       if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                           actualDependentBeans.add(dependentBean);
                      }
                   }
                   if (!actualDependentBeans.isEmpty()) {
                       //抛出异常
                       throw new BeanCurrentlyInCreationException(beanName,
                               "Bean with name &#39;" + beanName + "&#39; 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 " +
                               "&#39;getBeanNamesOfType&#39; with the &#39;allowEagerInit&#39; flag turned off, for example.");
                   }
          }
       }
    }
    登入後複製

    所以,之所以@Async注解遇上循环依赖,Spring无法解决,是因为@Aysnc注解会使得最终创建出来的Bean,跟早期暴露出去的Bean不是同一个对象,所以就会报错。

    出现循环依赖异常之后如何解决?

    解决这个问题的方法很多

    1、调整对象间的依赖关系,从根本上杜绝循环依赖,没有循环依赖,就没有早期暴露这么一说,那么就不会出现问题

    2、不使用@Async注解,可以自己通过线程池实现异步,这样没有@Async注解,就不会在最后生成代理对象,导致早期暴露的出去的对象不一样

    3、可以在循环依赖注入的字段上加@Lazy注解

    @Component
    public class AService {
        @Resource
        @Lazy
        private BService bService;
        @Async
        public void save() {
        }
    }
    登入後複製

    4、从上面的那段判断抛异常的源码注释可以看出,当allowRawInjectionDespiteWrapping为true的时候,就不会走那个else if,也就不会抛出异常,所以可以通过将allowRawInjectionDespiteWrapping设置成true来解决报错的问题,代码如下:

    @Component
    public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            ((DefaultListableBeanFactory) beanFactory).setAllowRawInjectionDespiteWrapping(true);
        }
    }
    登入後複製

    虽然这样设置能解决报错的问题,但是并不推荐,因为这样设置就允许早期注入的对象和最终创建出来的对象是不一样,并且可能会导致最终生成的对象没有被动态代理。

    以上是spring boot計畫使用@Async註解的坑怎麼解決的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    相關標籤:
    來源:yisu.com
    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
    熱門教學
    更多>
    最新下載
    更多>
    網站特效
    網站源碼
    網站素材
    前端模板