【编码日常】如何修改动态代理的私有变量

Dynamic Proxy Cglib Spring AOP JUnit Mock

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

案发现场

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

  1. EventController:@Controller声明的普通 Controller 类,接收 Web Http 请求,该类被一个 LogInterceptor 拦截,打印 HTTP 请求和应答报文,换句话就是被AOP切面了,在Spring上下文中已经变成了一个动态代理类。
EventController
  1. MeProducer: 该类作为 EventController 中的一个非共有(private/protected)成员变量,用来生产异步消息。本案例正是要 Mock 这个对象来模拟生产异步消息时的不同行为。

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

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

DEBUG 线索

1. 基本面分析

我们可以直观确认注入在 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

2. 方案分析

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

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

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


Target Object.png

图中注释掉的o3实现会报错,大家可以自己去看看是为什么。
提示线索:方法定义Field.get(Obj) 不是Field.get(Class)。

3. CGLIB 简单测试代码

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()));
    }
}

4. 总结陈词

全文总结一下:
1)JUnit对Spring类进行mock注入的时候,若发现怎么都塞不进去,请先确认该类是否已经被代理。可以使用AopUtils来判断;
2)对动态代理类的Field进行修改无法影响到真正被代理的目标对象内的Field,不管是public还是private,都没用;
3)对目标对象Field的修改,除了上文提到的找到目标对象,然后反射修改这个方法;亦可以在目标对象中暴露getter setter方法,这样即使通过动态代理类来setObj(), 实际上最终还是调用的目标对象的setObj(),一样可以达到修改目标对象Field的效果。这个大家可以自行去试验,当然后者是目标对象的代码没有那么简洁优雅,并不推荐,但是它背后的原理希望大家读完本文已然可以理解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,360评论 6 343
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,015评论 11 349
  • 下面我们详情地讲下操作系统中的进程几个状态 进程的状态 就绪(Ready)状态 当进程已分配到除CPU以外的所有必...
    陕西搜讯阅读 16,381评论 1 10
  • 陈志鹏_201511.19_《拖延心理学》 关键词:恐惧成功行百里者半九十 今天读了《拖延心理学》的第三章,在这一...
    大鹏鹏1994阅读 330评论 0 0