[bug]spring專案通過反射測試私有方法時,注入物件異常

shimmernight發表於2021-09-04

背景

遇到問題:在進行Spring單元測試編寫時,發現被測方法是一個私有方法,無法直接通過注入物件呼叫
解決思路:首先想到通過反射獲取該私有方法的訪問許可權,並傳入注入物件,最終呼叫物件的私有方法。

出現的異常

執行時丟擲空指標異常
image

定位問題

  1. 點選異常程式碼行打上斷點,debug除錯
    image
  2. 通過檢視變數值發現roleMapper為空,從而導致空指標
  3. 而roleMapper是傳入this物件的屬性,因此,問題來自傳入的物件

分析問題

  1. 通過分析this物件,可以發現它是一個被Cglib代理後的例項,由此可知,該類方法上必定有@Transactional事務註解或AOP註解修飾,從而被SpringCglib代理
    image
  2. 檢視cglib原理:

動態生成一個要代理類的子類,子類重寫要代理的類的所有不是final的方法。在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯。它比使用java反射的JDK動態代理要快。

其中重要的一點,代理類是被代理類的子類,回想關於Java中的繼承,有一條很重要的特性就是:

  • 子類擁有父類非 private 的屬性、方法。
  1. 此時,嘗試修改私有方法變成public,發現this物件恢復正常,由此鎖定代理類和私有方法出現問題
    image
  2. 通過搜尋cglib代理類私有方法發現原因:
    image
    image
  3. 由此可知,此處注入的cglib代理物件中不包含private方法!
  4. 那為啥同樣傳入的代理物件,呼叫public方法就成功,而呼叫private方法就失敗呢?
  1. 如果是私有方法,那麼在代理類中,不會包含這個方法。此時通過Method.invoke()來呼叫目標方法,傳入的例項物件是userController的代理類,而這個代理類中的userService為NULL,所以,執行的時候,才會看到userService沒有注入,導致空指標異常。
  2. 如果是公共方法,在代理類中,就有它的子類實現,則會先呼叫到代理類的攔截器MethodInterceptor。攔截器負責鏈式呼叫AOP方法和目標方法。在攔截器執行過程中,又呼叫了方法。但不同的是,此時傳入的例項物件並不是代理類,而是代理類的目標物件。

結論:可以發現代理類正常情況下,執行到原方法時是通過代理的目標物件(即原始物件)來執行,而當代理類發現沒有代理對應的private方法時,則直接通過代理物件(即上文的this)執行目標方法。

解決方法

既然我們需要的是隻原始物件執行私有方法,只要通過代理類獲取原始的目標物件即可。

// 由於cglib類是通過繼承代理,無法代理私有方法,因此無法通過原始物件執行方法
if (AopUtils.isCglibProxy(menuService)) {
    // 如果是cglib代理物件,則轉為原始物件
    menuService = (MenuServiceImpl)AopProxyUtils.getSingletonTarget(menuService);
}

此時得到的物件即為原始物件,bug成功消滅!
image

參考文章:

相關文章