【編碼日常】如何修改動態代理的私有變數
- 2019 年 11 月 5 日
- 筆記
Dynamic Proxy
Cglib
Spring AOP
JUnit Mock
最近在寫一個 Spring Controller 的 JUnit 單元測試時,需要將一個Mock對象塞入到Controller的私有成員變數中,發現怎麼都塞不成功,這才引發了這篇探索如何訪問和修改被動態代理對象的私有變數。
案發現場
為了理解直觀,下文會有不少截圖,先介紹下這個項目中幾個類:
- EventController:@Controller聲明的普通 Controller 類,接收 Web Http 請求,該類被一個 LogInterceptor 攔截,列印 HTTP 請求和應答報文,換句話就是被AOP切面了,在Spring上下文中已經變成了一個動態代理類。
EventController
- MeProducer: 該類作為 EventController 中的一個非共有(private/protected)成員變數,用來生產非同步消息。本案例正是要 Mock 這個對象來模擬生產非同步消息時的不同行為。
- JUnit Test: 單元測試類,把 EventController 通過@Autowired 自動注入進去(此時注入的就是動態代理過的對象),然後通過對其成員變數 MeProducer 的Mock 實現不同案例的單元測試。
- 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的效果。這個大家可以自行去試驗,當然後者是目標對象的程式碼沒有那麼簡潔優雅,並不推薦,但是它背後的原理希望大家讀完本文已然可以理解。