Aop踩坑!記一次模板類呼叫注入屬性為空的問題

語言: CN / TW / HK

問題起因

在做一個需求的時候,發現原來的程式碼邏輯都是基於模板+泛型的設計模式,模板用於規整邏輯處理流程,泛型用來轉換引數和選取實現類。聽上去是不是很nice!
  • 類目錄結構

  • AbstractTestAop:頂層抽象類,定義骨架和執行順序,內部通過Autowired注入了TopClassBean的例項物件。

  • AbstractTestCglibAop:二級抽象類,繼承自AbstractTestAop,空類無實現。
  • TestCglibAopExample:具體子類,類上添加了@Component註解,空類無實現。
  • TestAopRemoteEntrance:呼叫入口,它是一個Bean。
  • TopClassBean:例項物件,內部提供一個方法用來表示被呼叫。
  • AsyncExportLogAspect:方法切面( 路徑可以自己配置,此處對切面路徑做了處理所以飄紅 )

單元測試

單測結果:

很明顯:頂層介面內部例項引用的TopClassBean物件未注入,屬性為空,導致空指標!

排查

方法debug

  1. 獲取bean

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

2. before()

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

  1. myDo()

進入到 final 修飾的 Mydo 方法的時候又由實際類切換到代理類呼叫了,這時候內部引用 topClassBean 為空,最後NPE

總結:

由上可知,cglib動態代理可以代理目標類非final和private方法,當呼叫final或者private方法時,由於目標類中不存在此方法,所以還是使用代理類進行呼叫。

下面我們可以進行原始碼debug,主要解決兩個問題:

  1. 為什麼會發生代理
  2. 代理類為啥屬性為空

原始碼debug

通常代理都是發生在Bean例項化完成之後,對成品的Bean進行代理,多發生在BeanProcess後置處理中

按照這個思路咱們開始走斷點debug:

  1. 例項化完成情況

我們發現例項化完成內部屬性是有引用值的,不等於null,所以問題不在這,往下看

2. 後置處理器

重點:從這裡我們發現Bean變成了代理物件,並且內部引用變成了null,證實了我們的猜想,由此可斷定問題出現在BeanProcess的後置處理中

  1. AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization

    跟隨斷點進入 AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization 方法檢視

發現經歷了 AbstractAutoProxyCreator#postProcessAfterInitialization 方法後就發生了代理改變,我們繼續往下

  1. AbstractAutoProxyCreator#wrapIfNecessary

    在方法中 AbstractAutoProxyCreator#wrapIfNecessary 判斷了是否存在代理,此處生成了代理物件

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

  1. 代理生成

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

  1. 代理實現

這裡我們可以很清楚的看到是使用new構造生成出來的代理類,所以例項屬性值為空就解釋的通了, 問題二解決

總結:

由於AOP切面存在,導致目標類發生代理,生成了目標子類的代理Bean,代理類是通過 objenesis.newInstance(proxyClass, enhancer.getUseCache()) 構造出來的,所以不存在相關屬性,聯絡到cglib代理原理---通過ASM位元組碼框架在執行期寫入位元組碼跳過了編譯期,可以佐證咱們的定論。

針對上面兩個問題結論如下:

  1. 由於方法切面導致目標類發生代理
  2. 代理類是在執行期通過構造new出來的,屬性值為空,所以代理類進行例項呼叫,會報NPE

我們對整個問題進行一個完整性總結:

由於AOP切面代理的原因,導致內部final方法呼叫走的代理類呼叫,代理類例項屬性為空,導致NPE。

模板頂層為抽象類,未實現介面,導致選擇cglib代理,cglib通過構造new實現代理類,內部屬性均為空,由於通過繼承實現,final和private方法無法被代理,所以當不可繼承方法被呼叫時,當前物件為代理類,否則為目標類。

解決方案

  1. 頂層實現介面,避免cglib代理
  2. 方法訪問修飾變更,可被繼承代理
  3. 手動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

關注我的公眾號一起交流吧!