转载: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框架的集成,总该有些原因吧。

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

教育单元测试mock框架优化之路(下)的更多相关文章

  1. 教育单元测试mock框架优化之路(上)

    转载:https://sq.163yun.com/blog/article/169561874192850944 众所周知,mock对于单元测试,尤其是基于spring容器的单元测试,是非常重要的.它 ...

  2. 教育单元测试mock框架优化之路(中)

    转载:https://sq.163yun.com/blog/article/169564470918451200 三.间接依赖的bean的mock替换 对于前面提供的@Mock,@Spy+@Injec ...

  3. 单元测试mock框架——jmockit实战

    JMockit是google code上面的一个java单元测试mock项目,她很方便地让你对单元测试中的final类,静态方法,构造方法进行mock,功能强大.项目地址在:http://jmocki ...

  4. 单元测试Mock框架Powermockito 【mockito1.X】

    <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> &l ...

  5. 新浪微博iOS客户端架构与优化之路

    新浪微博iOS客户端架构与优化之路   随着Facebook.Twitter.微博的崛起,向UGC.PGC.OGC,自媒体提供平台的内 容消费型App逐渐形成了独特的客户端架构模式.与电商和通讯工具类 ...

  6. 阿里巴巴 web前端性能优化进阶路

    Web前端性能优化WPO,相信大多数前端同学都不会陌生,在各自所负责的站点页面中,也都会或多或少的有过一定的技术实践.可以说,这个领域并不缺乏成熟技术理论和技术牛人:例如Yahoo的web站点性能优化 ...

  7. 单元测试及框架简介 --junit、jmock、mockito、powermock的简单使用

    转 单元测试及框架简介 --junit.jmock.mockito.powermock的简单使用 2013年08月28日 14:33:06 luvinahlc 阅读数:6413 标签: 测试工具单元测 ...

  8. 微博MySQL优化之路--dockone微信群分享

    微博MySQL优化之路 数据库是所有架构中不可缺少的一环,一旦数据库出现性能问题,那对整个系统都回来带灾难性的后果.并且数据库一旦出现问题,由于数据库天生有状态(分主从)带数据(一般还不小),所以出问 ...

  9. MoQ(基于.net3.5,c#3.0的mock框架)简单介绍

    我们在做单元测试的时候,常常困扰于数据的持久化问题,很多情况下我们不希望单元测试影响到数据库中的内容,而且受数据库的影响有时我们的单元测试的速度会很慢,所以我们往往希望将持久化部分隔离开,做单元测试的 ...

随机推荐

  1. vs2012\vs2013\vs2015碰到生成时报该错误:项目中不存在目标“GatherAllFilesToPublish”

    手头一个vs2010升级到vs2012后,web项目发布到本地目录时项目报错:“该项目中不存在目标“GatherAllFilesToPublish”” 通过谷歌大神的帮助,找到了解决方法.共享之. 原 ...

  2. MySQL主从复制的原理及配置方法(比较详细)

    MySQL 的数据库的高可用性的架构大概有以下几种:集群,读写分离,主备.而后面两种都是通过复制来实现的.下面将简单介绍复制的原理及配置,以及一些常见的问题 一.复制的原理 MySQL 复制基于主服务 ...

  3. 用C扩展Python2

    参考 python扩展实现方法--python与c混和编程 编写Python扩展(Extending Python with C or C++) https://docs.python.org/2.7 ...

  4. Blocks Programming Topics

    最近的工作中比较频繁的用到了Block,不在是以前当做函数指针的替代或者某些API只有Blocks形式的接口才不得已用之了,发现自己对其了解还是太浅,特别是变量的生存期,按惯例还是翻译官方文档,原文链 ...

  5. python测试开发django-21.admin后台表名称和字段显示中文

    前言 admin后台页面表名称(默认会多加一个s)和字段名称是直接显示在后台的,如果我们想设置成中文显示需加verbose_name和verbose_name_plural属性 verbose_nam ...

  6. Linux学习16-CentOS安装gitlab环境

    前言 在学习Gitlab的环境搭建之前,首先需要了解Git,Gitlab,GitHub他们三者之间的关系 Git 它是一个源代码版本控制系统,可让您在本地跟踪更改并从远程资源推送或提取更改. GitH ...

  7. Asp.Net Mvc3.0(MEF依赖注入实例)

    前言 在http://www.cnblogs.com/aehyok/p/3386650.html前面一节主要是对MEF进行简单的介绍.本节主要来介绍如何在Asp.Net Mvc3.0中使用MEF. 准 ...

  8. arcgis server 10 for java 8399根目录是404的提示取消,并跳转到 地图目录 /arcgis/rest/services下

    看了Howto: 取消ArcGIS Server 9.x for Java内置tomcat在8399端口的文件列表 http://support.esrichina-bj.cn/2009/0819/9 ...

  9. Chart/Report资源目录

    ylbtech-Chart:Chart/Report资源目录 1.Chart.js返回顶部 1-0.官网 http://www.chartjs.org 1-1.实例 http://www.chartj ...

  10. Verilog 加法器和减法器(6)

    为了减小行波进位加法器中进位传播延迟的影响,可以尝试在每一级中快速计算进位,如果能在较短时间完成计算,则可以提高加法器性能. 我们可以进行如下的推导: 设 gi=xi&yi, pi = xi ...