問題起因
在做一個需求的時候,發現原來的代碼邏輯都是基于模板+泛型的設計模式,模板用于規整邏輯處理流程,泛型用來轉換引數和選取實作類,聽上去是不是很nice!
但是在方法呼叫的時候卻突然爆出一個NPE,直接給人整蒙了!不過懵歸懵,該排查的還是需要排查的,下面我使用一個例子來模擬分析我這次的排查的程序,
tips:因為例子我直接就定義在公司的專案當中,所以很多路徑打上了馬賽克,請勿介意噢!畢竟我們主要還是學習避坑的,?( ′???` )比心
- 類目錄結構

- AbstractTestAop:頂層抽象類,定義骨架和執行順序,內部通過Autowired注入了TopClassBean的實體物件,
- AbstractTestCglibAop:二級抽象類,繼承自AbstractTestAop,空類無實作,
- TestCglibAopExample:具體子類,類上添加了@Component注解,空類無實作,
- TestAopRemoteEntrance:呼叫入口,它是一個Bean,
- TopClassBean:實體物件,內部提供一個方法用來表示被呼叫,
- AsyncExportLogAspect:方法切面(路徑可以自己配置,此處對切面路徑做了處理所以飄紅)
單元測驗

單測結果:

很明顯:頂層介面內部實體參考的TopClassBean物件未注入,屬性為空,導致空指標!
排查
方法debug
- 獲取bean

可以看到此時獲取到的Bean型別為一個代理類,繼續往下,進入到invoke方法
2. before()

可以發現進入到protected修飾的Before方法的時候由代理轉變為實際的類方法呼叫了
- myDo()

進入到final修飾的Mydo方法的時候又由實際類切換到代理類呼叫了,這時候內部參考topClassBean為空,最后NPE
總結:
由上可知,cglib動態代理可以代理目標類非final和private方法,當呼叫final或者private方法時,由于目標類中不存在此方法,所以還是使用代理類進行呼叫,
下面我們可以進行原始碼debug,主要解決兩個問題:
- 為什么會發生代理
- 代理類為啥屬性為空
原始碼debug
通常代理都是發生在Bean實體化完成之后,對成品的Bean進行代理,多發生在BeanProcess后置處理中
按照這個思路咱們開始走斷點debug:
- 實體化完成情況

我們發現實體化完成內部屬性是有參考值的,不等于null,所以問題不在這,往下看
2. 后置處理器

重點:從這里我們發現Bean變成了代理物件,并且內部參考變成了null,證實了我們的猜想,由此可斷定問題出現在BeanProcess的后置處理中
- 跟隨斷點進入
AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization方法查看

發現經歷了
AbstractAutoProxyCreator#postProcessAfterInitialization方法后就發生了代理改變,我們繼續往下
- 在方法中
AbstractAutoProxyCreator#wrapIfNecessary判斷了是否存在代理,此處生成了代理物件

在此處我們發現了因為aop切面存在,所以導致啟用了代理
問題一解決
- 代理生成

因為沒有介面,所以使用cglib代理
- 代理實作

這里我們可以很清楚的看到是使用new構造生成出來的代理類,所以實體屬性值為空就解釋的通了,
問題二解決
總結:
由于AOP切面存在,導致目標類發生代理,生成了目標子類的代理Bean,代理類是通過 objenesis.newInstance(proxyClass, enhancer.getUseCache())構造出來的,所以不存在相關屬性,聯系到cglib代理原理---通過ASM位元組碼框架在運行期寫入位元組碼跳過了編譯期,可以佐證咱們的定論,
針對上面兩個問題結論如下:
- 由于方法切面導致目標類發生代理
- 代理類是在運行期通過構造new出來的,屬性值為空,所以代理類進行實體呼叫,會報NPE
我們對整個問題進行一個完整性總結:
由于AOP切面代理的原因,導致內部final方法呼叫走的代理類呼叫,代理類實體屬性為空,導致NPE,
模板頂層為抽象類,未實作介面,導致選擇cglib代理,cglib通過構造new實作代理類,內部屬性均為空,由于通過繼承實作,final和private方法無法被代理,所以當不可繼承方法被呼叫時,當前物件為代理類,否則為目標類,
解決方案
- 頂層實作介面,避免cglib代理
- 方法訪問修飾變更,可被繼承代理
- 手動getBean,指定目標類物件呼叫
在除錯的程序還發現一個有意思的現象:
整個參考呼叫鏈的方法堆疊上只要有一個方法被代理,呼叫鏈后端的所有方法都將使用目標類呼叫,不會導致NPE,
舉個例如下:invoke(final) -> myDo1(非final) -> myDo(final),此時不會產生NPE,因為這個時候執行Mydo方法的時候仍然是目標類,
有興趣的同學可以去翻一下原始碼,一起交流

附:代理類

從代理類上面我們可以看出:
- 代理類繼承具體子類
TestCglibAopExample,所以final或者private相關方法,即Mydo()和invoke()方法代理類未提供實作,無法被代理,
獲取代理類class檔案命令,在idea啟動引數中添加
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
-Dcglib.debugLocation=/Users/xxx
關注我的公眾號一起交流吧!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/459490.html
標籤:Java






