zoukankan      html  css  js  c++  java
  • 教育单元测试mock框架优化之路(下)

    转载:https://sq.163yun.com/blog/article/169563599967031296

    四、循环依赖的解决  

         果然!

         当我将@SpyBean应用到存在有循环依赖的Bean上时,会导致如下异常:

    Bean with name userCouponService has been injected into other beans [bizOrderService,userCoupon

    TemplateService] 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.

         是的,相信英语好的同学们都已经看懂了。大致的意思是说,一部分依赖其的bean持有了其原始版本的引用(后面大家会发现其实是earlyBeanReference),而其又最终被进一步包装代理了,这样导致了后期再依赖其的beans中持有的引用与之前的不是同一个对象。所以,spring默认是禁止了这种情况的发生。(偷偷的说,其实spring内部有个开关,打开后容器将对这种不一致情况睁一只眼闭一只眼......不知道spring的本意如何。不过,掩耳盗铃对我们而言是没有意义的。)

        那么,这个问题该怎么解决呢?

        不幸的是,网上关于这个方面的文章很少。我也问过一些资深的同事,得的反馈是这个问题的确没有特别好的解决办法。建议从业务上,尽量避免循环依赖,或通过lazy-init来解决。但现实的开发工作中,由于业务复杂、开发人员编码水平参差不齐、遗留系统等原因,循环依赖往往难以真正避免。而lazy-init也不是万能药,很有可能在某个时候就被某一个非lazy bean在初始化阶段引入调用而遭到破坏。那么,如果框架可以解决,还是尽量从框架层面本身去解决吧。

         仔细想想,有一点很值得怀疑。那就是为什么在我们增加@SpyBean前,循环依赖是照样可以正常工作的,加上就不行了呢?是不是springboot test框架在做mock(spy)对象的注入时存在缺陷呢?

        追踪下userCouponService bean的整个生命周期,终于找到了问题的根源。原来是springboot test框架的SpyPostProcessor在处理bean wrapping时存在缺陷,它没有考虑循环依赖的场景。

       同样都是SmartInstantiationAwareBeanPostProcessor,spring自家的AbstractAutoProxyCreator在做代理时就会考虑这个循环依赖的处理细节。让我们先看下AbstractAutoProxyCreator相关源码:

    @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);
    }
    @Override
    public Object postProcessAfterInitialization(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;
    }

        然后,我们再对比下SpyPostProcessor的相关源码:

    @Override
    public Object getEarlyBeanReference(Object bean, String beanName)
          throws BeansException {
       return createSpyIfNecessary(bean, beanName);
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
          throws BeansException {
       if (bean instanceof FactoryBean) {
          return bean;
       }
       return createSpyIfNecessary(bean, beanName);
    }

         相信大家已经发现,SpyPostProcessor在处理上偷工减料了。合理的姿势应该是:如果一个Bean在earlyInit的阶段(getEarlyBeanReference),就生成了代理对象并交付到spring内部的集合中后,postProcessAfterInitialization阶段就不要再对bean对代理处理。因为spring的AbstractAutowireCapableBeanFactory在doCreateBean中,已经做了如下处理(注意我红色标记的部分):

    // Initialize the bean instance.
    Object exposedObject = bean;
    try {
       populateBean(beanName, mbd, instanceWrapper);
       if (exposedObject != null) {
           //看这里,如果没有限制,SpyPostProcessor.postProcessAfterInitialization会在initializeBean方法里面搞事情,导致输出的exposedObject为
           //原exposedObject的代理对象。
           exposedObject = initializeBean(beanName, exposedObject, mbd);
       }
    }
    catch (Throwable ex) {
       if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
          throw (BeanCreationException) ex;
       }
       else {
          throw new BeanCreationException(
                mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
       }
    }
    
    if (earlySingletonExposure) {
       Object earlySingletonReference = getSingleton(beanName, false);
       if (earlySingletonReference != null) {     
          //看这里。如上,一旦exposedObject被bean的spy代理,这个if分支就不成立,而走向异常检测的深渊。而我再前面也讲过,这实际上并不是为检测而检测,因为
          //它事实在破坏不同依赖bean包含当前bean引用的一致性。
          if (exposedObject == bean) {
             //再看这里。如果按照spring预设的方案,对于循环依赖的bean,都统一在getEarlyBeanReference阶段完成代理并投放到内部的earlySingletonObjects
             //容器。那么这段赋值逻辑,就能保证最终暴露出来的singleton(即交付到内部的SingletonObjects集合)的bean等同于earlySingletonReference。这样,
             //就能确保其他所有bean都依赖同一个当前bean的引用。
             exposedObject = earlySingletonReference;
          }
          else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
             //下面就是导致本章开始那段异常的校验逻辑
             String[] dependentBeans = getDependentBeans(beanName);
             Set 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 '" + 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.");
             }
          }
       }
    }

          既然问题已然明了,那解决办法也就简单了。我要做的就是扩展实现SpyPostProcessor,然后引入相似的单例检测,循环依赖导致的异常问题解决了!~让我们具体看下EduSpyPostProcessor的实现:

    @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 createSpyIfNecessary(bean, beanName);
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean == null) {
            return bean;
        }
        if (bean instanceof FactoryBean) {
            return bean;
        }
    
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (!this.earlyProxyReferences.contains(cacheKey)) {
            return createSpyIfNecessary(bean, beanName);
        }
    
        return bean;
    }

    总结:

     

         至此,我们的单元测试mock框架算告了一小个段落。我们可以:

    1. 通过@EduMock,@EduSpy,@EduInjectMocks实现单层的属性的mock替换,同时解决了待InjectMocks bean为动态代理对象的情况。

    2. 通过@AutoMock+具体MockFactoryBean的方式,实现对类似于Dubbo等通用组件的自动Mock替换。

    3. 通过@EduMockBean和@EduSpyBean的方式,基于IOC实现bean的嵌套属性的mock替换。

        不过,我们仍有一个重要关卡要过。那就是对静态、私有、final、jdk内置等方法的mock支持。可喜的是,这个方面,PowerMockito框架已经做了支持。但如何和Spring容器,以及现有的Mock注入、状态重置、管理等框架集成,还有待进一步调研和验证。毕竟springboot test框架发展到现在,也只做了mockito框架的集成,总该有些原因吧。

         路漫漫其修远兮,再慢慢求索吧!~

  • 相关阅读:
    静态检查lua语法工具luacheck
    centos7系列:
    git submodule 教程
    CENTOS 7 安装redis
    python基本语法:
    彻底理解lib和dll
    C++语言的设计与演化(空白):
    《Effective C++》 目录:
    C++进阶书籍(转)
    学习的心态(转)
  • 原文地址:https://www.cnblogs.com/ceshi2016/p/9552514.html
Copyright © 2011-2022 走看看