問題起因
在做一個需求的時候,發現原來的程式碼邏輯都是基於模板+泛型的設計模式,模板用於規整邏輯處理流程,泛型用來轉換引數和選取實現類。聽上去是不是很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
關注我的公眾號一起交流吧!