Dynamic Proxy Cglib Spring AOP JUnit Mock
最近在写一个 Spring Controller 的 JUnit 单元测试时,需要将一个 Mock 对象塞入到 Controller 的私有成员变量中,发现怎么都塞不成功,这才引发了这篇探索如何访问和修改被动态代理对象的私有变量。
为了理解直观,下文会有不少截图,先介绍下这个项目中几个类:
EventController
我们可以直观确认注入在 JUnit 中的 eventController 实际上就是被 Spring CGLIB 字节码增强过的一个动态代理类,如下图。为表述方便后文会用
来代表图中实际的动态代理类名
- EventControllerProxy
- EventController$EnhancerBySpringCGLIB$3c1bcb52
Junit 中的代理类
带大家解读一下这张图的要点:
a. AopUtils.isAopProxy 可以判断一个对象是否是 Spring AOP 代理对象;判断依据就是或者 JdkDynamicProxy 或者 CglibProxy;
b. Spring AOP 代理类都默认实现了 Advised 接口,通过其接口方法 getTargetSource().getTarget() 可以获取到真正被代理的目标对象。
开涛博客中提到了如何从 CALLBACK 中抽丝剥茧找到目标对象,虽然不如图中简单优雅,但是对于理解代理类的构造很有好处,推荐大家看看: http://jinnianshilongnian.iteye.com/blog/1613222
c. 可以看到 EventController 的代理对象和目标对象是两个独立个体 (@后的 id 不同),这个容易理解。而对象内部的变量也是完全不同的,EventControllerProxy 里的 meProducer 是通过 PrivateAccessor 塞入的 mock 对象,EventController 里的是通过 Autowired 注入的配置完整的对象。另外,目标对象中定义的三种修饰符的 xxxField 变量,在 Proxy 里都是 null,也就是说 Field 都没有继承过来。要理解它必须学懂两个知识点:动态代理原理和 Spring 动态代理机制
关于动态代理的底层实现不展开,大家阅读下方两篇即可。从方便理解本案例来说,大家只要明白 "动态代理类" 是继承自 "被代理类" 的一个子类,且 "拦截的" 或者说 "代理的" 只是 Method 而不是 Field 就足够了。
Reference 1: Understanding proxy usage in Spring Reference 2: 占小狼 - cglib 动态代理
而说到 Spring 动态代理 Bean 的实现机制,无非是有接口的类使用 Jdk 动态代理,无接口的类使用 CGLIB,当然你可以选择强制使用 CGLIB。下方的引用链接有个关键说明:"被代理对象的构造器会被执行两次",也就是被代理的目标对象会实例化一次,代理对象作为目标对象的子类也会实例化一次。这样就可以解释上图中的情形了,Spring 先初始化好目标对象 Bean,并将其依赖树全部注入完毕,然后通过 AOP 生成动态代理类 wrap 目标对象进行方法拦截,所以目标对象里的属性对于代理类来说都是透明的,只要目标对象自己了解就行。用对象由数据和行为构成来说明的话,数据都在目标对象里,代理类不关心数据只关心行为。
Reference 3: Spring Proxying mechanisms
Proxy
上文出现的不一致情况,是因为错误的讲 mock 对象塞入到代理对象中去了,如下:
- PrivateAccessor.setField(EventControllerProxy, "meProducer", mockObj);
而这个值并不能在真正的目标对象执行中被 mock,所以我们需要想办法找到真正的目标对象才能塞入 mock, 如下图,o2, o3 都可以获取到真正的目标对象私有成员变量 meProducer。如何塞入就不用在细说了吧,目标对象都有了随便你怎么反射改变量咯。
Target Object.png
图中注释掉的 o3 实现会报错,大家可以自己去看看是为什么。 提示线索:方法定义 Field.get(Obj) 不是 Field.get(Class)。
Bean 父类:
- public class AbstractBean {
- protected String id1;
- protected Long id2;
- }
SampleBean :
- public class SampleBean extends AbstractBean {
- public String str;
- private Map map;
- private List list;
- private Long lng;
- ......
- getter setter
- ......
- }
CGLIBTest:
- public class CglibTest {
- @Test
- public void testCglib() {
- SampleBean sampleBean = new SampleBean();
- sampleBean.setStr("test2");
- sampleBean.setLng(1L);
- System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/Nicholas/cglib/");
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(sampleBean.getClass());
- enhancer.setCallback(new MethodInterceptor() {
- @Override
- public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
- if (method.getDeclaringClass() != Object.class
- && method.getReturnType() == Long.class) {
- return "Long";
- }
- Object result = methodProxy.invokeSuper(o, objects);
- return result;
- }
- });
- SampleBean proxy = (SampleBean) enhancer.create();
- System.out.println("str:" + proxy.str);
- try { //getLong被转换成了String,必报错
- System.out.println("getLng():" + proxy.getLng());
- Assert.assertTrue(false);
- } catch (Exception e) {
- Assert.assertTrue(true);
- }
- System.out.println(ArrayUtils.toString(proxy.getClass().getDeclaredFields()));
- System.out.println(ArrayUtils.toString(proxy.getClass().getSuperclass().getDeclaredFields()));
- System.out.println(ArrayUtils.toString(proxy.getClass().getSuperclass().getSuperclass().getDeclaredFields()));
- }
- }
全文总结一下:
1)JUnit 对 Spring 类进行 mock 注入的时候,若发现怎么都塞不进去,请先确认该类是否已经被代理。可以使用 AopUtils 来判断;
2)对动态代理类的 Field 进行修改无法影响到真正被代理的目标对象内的 Field,不管是 public 还是 private,都没用;
3)对目标对象 Field 的修改,除了上文提到的找到目标对象,然后反射修改这个方法;亦可以在目标对象中暴露 getter setter 方法,这样即使通过动态代理类来 setObj(), 实际上最终还是调用的目标对象的 setObj(),一样可以达到修改目标对象 Field 的效果。这个大家可以自行去试验,当然后者是目标对象的代码没有那么简洁优雅,并不推荐,但是它背后的原理希望大家读完本文已然可以理解。
来源: http://www.jianshu.com/p/7aa172ed1bce