首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

如何修改动态代理的私有变量

最近在写一个 Spring Controller 的 JUnit 单元测试时,需要将一个Mock对象塞入到Controller的私有成员变量中,发现怎么都塞不成功,这才引发了这篇探索如何访问和修改被动态代理对象的私有变量。

案发现场

为了理解直观,下文会有不少截图,先介绍下这个项目中几个类:

EventController:@Controller声明的普通 Controller 类,接收 Web Http 请求,该类被一个 LogInterceptor 拦截,打印 HTTP 请求和应答报文,换句话就是被AOP切面了,在Spring上下文中已经变成了一个动态代理类。

MeProducer: 该类作为 EventController 中的一个非共有(private/protected)成员变量,用来生产异步消息。本案例正是要 Mock 这个对象来模拟生产异步消息时的不同行为。

JUnit Test: 单元测试类,把 EventController 通过@Autowired 自动注入进去(此时注入的就是动态代理过的对象),然后通过对其成员变量 MeProducer 的Mock 实现不同案例的单元测试。

PrivateAccessor:单元测试常用的用于反射私有变量和私有方法的工具类,依赖 junit-addons。

DEBUG 分析

1. 基本面分析

我们可以直观确认注入在 JUnit 中的eventController 实际上就是被 Spring CGLIB 字节码增强过的一个动态代理类,如下图。为表述方便后文会用EventControllerProxy来代表图中实际的动态代理类名EventController$$EnhancerBySpringCGLIB$$3c1bcb52

带大家解读一下这张图的要点:

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 注入的配置完整的对象。另外,目标对象中定义的三种修饰符的pxxxField变量,在Proxy里都是null,也就是说Field都没有继承过来。要理解这部分必须懂两个知识点:动态代理原理 和 Spring动态代理机制

关于动态代理的底层实现不展开,文后会有示例代码。大家阅读下方两篇文章基本可以搞明白。从方便理解本案例的角度来说,大家只要明白“动态代理类”是继承自”被代理类”的一个子类,且“拦截的”或者说“代理的”只是Method而不是Field就足够了。

Ref 1: Understand proxy usage in Spring(https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring)

Ref 2: 占小狼 - cglib动态代理(https://www.jianshu.com/p/13aa63e1ac95)

而说到Spring动态代理Bean的实现机制,无非是有接口的类使用Jdk动态代理,无接口的类使用CGLIB,当然你可以选择强制使用CGLIB。下方的引用文章有个关键说明:"被代理对象的构造器会被执行两次",也就是被代理的目标对象会实例化一次,代理对象作为目标对象的子类也会实例化一次。

这样就可以解释上图中的情形了,Spring先初始化好目标对象Bean,并将其依赖树全部注入完毕,然后通过AOP生成动态代理类wrap目标对象进行方法拦截,所以目标对象里的属性对于代理类来说都是透明的。用对象由数据和行为构成来说明的话,数据都在目标对象里,代理类不关心数据只关心行为。

Ref 3: Spring proxying mechanisms(https://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch08s06.html)

2. 方案分析

上文出现的不一致情况,是因为错误的将mock对象塞入到代理对象中去了,如下:

PrivateAccessor.setField(EventControllerProxy, "meProducer", mockObj);

而这个值并不能在真正的目标对象执行中被mock,所以我们需要想办法找到真正的目标对象才能塞入mock, 如下图,o2, o3都可以获取到真正的目标对象私有成员变量meProducer。如何塞入就不用在细说了吧,目标对象都有了随便你怎么反射改变量咯。

图中注释掉的o3实现会报错,大家可以自己去看看是为什么。

提示线索:方法定义Field.get(Obj) 不是Field.get(Class)。

CGLIB 简单测试代码

AbstractBean.java

public class AbstractBean {

protected String id1;

protected Long id2;

}

SampleBean.java

public class SampleBean extends AbstractBean {

public String str;

private Map map;

private List list;

private Long lng;

......

getter

setter

......

}

CglibTest.java(代码显示格式太乱,还是用截图)

总结陈词

全文总结一下:

1)JUnit对Spring类进行mock注入的时候,若发现怎么都塞不进去,请先确认该类是否已经被代理。可以使用AopUtils来判断;

2)对动态代理类的Field进行修改无法影响到真正被代理的目标对象内的Field,不管是public还是private,都没用;

3)对目标对象Field的修改,除了上文提到的找到目标对象,然后反射修改这个方法;亦可以在目标对象中暴露getter setter方法,这样即使通过动态代理类来setObj(), 实际上最终还是调用的目标对象的setObj(),一样可以达到修改目标对象Field的效果。这个大家可以自行去试验,当然后者是目标对象的代码没有那么简洁优雅,并不推荐,但是它背后的原理希望大家读完本文已然可以理解。

END

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180102G0QU3Q00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券