【編碼日常】如何修改動態代理的私有變數

  • 2019 年 11 月 5 日
  • 筆記

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的效果。這個大家可以自行去試驗,當然後者是目標對象的程式碼沒有那麼簡潔優雅,並不推薦,但是它背後的原理希望大家讀完本文已然可以理解。