背景
遇到問題:在進行Spring單元測試編寫時,發現被測方法是一個私有方法,無法直接通過注入物件呼叫
解決思路:首先想到通過反射獲取該私有方法的訪問許可權,並傳入注入物件,最終呼叫物件的私有方法。
出現的異常
執行時丟擲空指標異常
定位問題
- 點選異常程式碼行打上斷點,debug除錯
- 通過檢視變數值發現roleMapper為空,從而導致空指標
- 而roleMapper是傳入this物件的屬性,因此,問題來自傳入的物件
分析問題
- 通過分析this物件,可以發現它是一個被Cglib代理後的例項,由此可知,該類方法上必定有@Transactional事務註解或AOP註解修飾,從而被SpringCglib代理
- 檢視cglib原理:
動態生成一個要代理類的子類,子類重寫要代理的類的所有不是final的方法。在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯。它比使用java反射的JDK動態代理要快。
其中重要的一點,代理類是被代理類的子類,回想關於Java中的繼承,有一條很重要的特性就是:
- 子類擁有父類非 private 的屬性、方法。
- 此時,嘗試修改私有方法變成public,發現this物件恢復正常,由此鎖定代理類和私有方法出現問題
- 通過搜尋cglib代理類私有方法發現原因:
- 由此可知,此處注入的cglib代理物件中不包含private方法!
- 那為啥同樣傳入的代理物件,呼叫public方法就成功,而呼叫private方法就失敗呢?
- 如果是私有方法,那麼在代理類中,不會包含這個方法。此時通過Method.invoke()來呼叫目標方法,傳入的例項物件是userController的代理類,而這個代理類中的userService為NULL,所以,執行的時候,才會看到userService沒有注入,導致空指標異常。
- 如果是公共方法,在代理類中,就有它的子類實現,則會先呼叫到代理類的攔截器MethodInterceptor。攔截器負責鏈式呼叫AOP方法和目標方法。在攔截器執行過程中,又呼叫了方法。但不同的是,此時傳入的例項物件並不是代理類,而是代理類的目標物件。
結論:可以發現代理類正常情況下,執行到原方法時是通過代理的目標物件(即原始物件)來執行,而當代理類發現沒有代理對應的private方法時,則直接通過代理物件(即上文的this)執行目標方法。
解決方法
既然我們需要的是隻原始物件執行私有方法,只要通過代理類獲取原始的目標物件即可。
// 由於cglib類是通過繼承代理,無法代理私有方法,因此無法通過原始物件執行方法
if (AopUtils.isCglibProxy(menuService)) {
// 如果是cglib代理物件,則轉為原始物件
menuService = (MenuServiceImpl)AopProxyUtils.getSingletonTarget(menuService);
}
此時得到的物件即為原始物件,bug成功消滅!
參考文章: