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

邱志強發表於2022-04-20

問題起因

在做一個需求的時候,發現原來的程式碼邏輯都是基於模板+泛型的設計模式,模板用於規整邏輯處理流程,泛型用來轉換引數和選取實現類。聽上去是不是很nice!
  • 類目錄結構
    image
  • AbstractTestAop:頂層抽象類,定義骨架和執行順序,內部通過Autowired注入了TopClassBean的例項物件。
    image
  • AbstractTestCglibAop:二級抽象類,繼承自AbstractTestAop,空類無實現。
    image
  • TestCglibAopExample:具體子類,類上新增了@Component註解,空類無實現。
    image
  • TestAopRemoteEntrance:呼叫入口,它是一個Bean。
    image
  • TopClassBean:例項物件,內部提供一個方法用來表示被呼叫。
    image
  • AsyncExportLogAspect:方法切面(路徑可以自己配置,此處對切面路徑做了處理所以飄紅)
    image

單元測試

image
單測結果:
image

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

排查

方法debug

  1. 獲取bean
    image

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

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

  1. myDo()
    image

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

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

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

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

原始碼debug

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

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

  1. 例項化完成情況
    image

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

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

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

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

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

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

  1. 代理生成
    image

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

  1. 代理實現
    image

這裡我們可以很清楚的看到是使用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方法的時候仍然是目標類。
有興趣的同學可以去翻一下原始碼,一起交流

image

附:代理類

image
從代理類上面我們可以看出:

  • 代理類繼承具體子類TestCglibAopExample,所以final或者private相關方法,即Mydo()和invoke()方法代理類未提供實現,無法被代理。

獲取代理類class檔案命令,在idea啟動引數中新增
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
-Dcglib.debugLocation=/Users/xxx


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

相關文章