Spring|因動態代理使用不注意導致的詭異現象

關注“Java藝術”一起來充電吧!

筆者在新的定時任務項目中,限定一個類只能寫一個Job,類似於寫腳本,一個Job一個腳本。對於簡單的任務我們並不約定一定要有Service層,但Job中我們可能需要將某些數據庫操作放到事務中執行,爲讓註解事務生效,我們不能直接使用this調用事務方法。

調用本類事務方法有兩種方式可以讓註解事務生效:

  • 一是通過在類中注入自己,也就是循環依賴注入;

  • 二是在需要時再從bean工廠中獲取bean


場景描述

假設現有類A,在類AmethodA方法中,先從Springbean工廠獲取到類A的實例,再調用類AmethodB方法,這樣做的目的是使事務生效。代碼如下:

@Component
public class ProxyObjFieldNpe {

    @Value("${field_value}")
    private String fieldValue;

    public void methodA() {
        if (fieldValue == null) {
            System.out.println("methodA NPE...");
        }
        // 從bean工廠取,使AOP生效
        ProxyObjFieldNpe thisRef = OnionXxlJobApplicationContent.getBean(ProxyObjFieldNpe.class);
        // ......調用某些事務方法
        thisRef.methodB();
    }

    private void methodB() {
        if (fieldValue == null) {
            System.out.println("methodB NPE...");
        }
    }

}

外部調用methodA方法:proxyObjFieldNpe.methodA();

結果輸出的是:"methodB NPE..."

爲什麼methodA方法獲取到fieldValue字段的值不爲空,而methodB方法獲取到的fieldValue卻爲空呢?這就是筆者遇到的問題。細心的朋友,你有沒有看出原因呢?

實際項目中調試的結果截圖如下:

圖中AutoCloseTimeoutOrderJob實例的字段都爲空,這些字段都是聲明自動注入的MapperService,不可能爲空。但從調試結果我們可以看出,從bean工廠獲取到的是AutoCloseTimeoutOrderJob的代理對象,並非AutoCloseTimeoutOrderJob

ProxyObjFieldNpe的例子中,我們從bean工廠獲取到的也是ProxyObjFieldNpe的代理對象,該代理對象繼承ProxyObjFieldNpe。因此,與上面截圖一樣,代理對象的字段都是NULL

外部調用ProxyObjFieldNpemethodA方法調用的是代理類的methodA方法,那爲什麼methodA方法拿到字段的值非空,而methodB方法拿到的是空值呢?

因爲methodB方法被聲明爲private了,代理類沒法重寫該方法。因此thisRef.methodB();實際調用的是代理類父類的methodB方法。methodB方法中獲取fieldValue獲取的是代理類對象的,這就是methodA方法獲取到fieldValue字段的值不爲NULL,而methodB方法獲取到fieldValue字段的值爲NULL的原因。

“methodB方法中獲取fieldValue獲取的是代理類對象的 ",這句我們稍後從字節碼層面理解。

問題:

  • 爲什麼methidB方法的訪問標誌是private,代理對象是ProxyObjFieldNpe的子類,卻能調用其父類的methidB方法?

  • 爲什麼代理對象的字段爲NULL?


爲什麼代理對象能調用父類的private方法?

因爲調用訪問標誌爲privatemethodB方法是在ProxyObjFieldNpe類的methodA方法中調用的,而不是在代理類的methodA方法中調用的,內部調用當然有訪問權限。

代理類繼承ProxyObjFieldNpe,外部調用代理類的methodA方法時,最終經過方法攔截器調用代理類父類的methodA方法,因此methodA方法中調用methodB方法實際上是在父類中調用的。

ProxyObjFieldNpemethodA方法編譯後生成的字節碼如下(部分):

   15: ldc           #6    // class com/wujiuye/test/ProxyObjFieldNpe
   17: invokestatic  #7    // Method com/wujiuye/test/OnionXxlJobApplicationContent.getBean:(Ljava/lang/Class;)Ljava/lang/Object;
   20: checkcast     #6    // class com/wujiuye/test/ProxyObjFieldNpe
   23: astore_1
   24: aload_1
   25: invokespecial #8    // Method com/wujiuye/test/ProxyObjFieldNpe.methodB:()V

偏移量爲151720三條指令是:從bean工廠獲取代理bean,並使用checkcast指令將代理對象類型強制轉爲父類類型。

偏移量爲2425兩條字節碼實現調用methodB方法,非靜態方法的第一個隱式參數爲this引用,此處傳的是代理類對象的引用,因此在methodB方法中,使用this(代理對象的引用)獲取到的字段都是空的。

爲什麼代理對象的字段爲NULL?

如果熟悉Spring Bean生命週期,那麼就不難理解。

bean的創建過程如下:

  • 1、反射創建bean;

  • 2、爲bean注入屬性;

  • 3、調用*Aware接口的方法;

  • 4、調用BeanPostProcessorpostProcessBeforeInitialization方法;

  • 5、調用初始化方法,afterPropertiesSet或自定義的初始化方法;

  • 6、調用BeanPostProcessorpostProcessAfterInitialization方法;

代理對象是在上述步驟的第六步創建的,即調用某個BeanPostProcessorpostProcessAfterInitialization方法之後,返回代理對象,如果是單例對象,則會將該對象保存到bean工廠(容器)中。也就是說,bean工廠中存儲的是代理對象。

下面兩張圖是我在項目中調試Spring代碼的截圖。(圖中的小紅點下方有個問號,這是條件斷點,只有滿足條件時纔會停在斷點處。條件的設置可右擊小紅點,在彈出框中輸出條件,條件的編寫與在代碼中添加一個if語句是一樣的。)

在調用BeanPostProcessorpostProcessAfterInitialization方法之前, bean還是原生的bean

在調用BeanPostProcessorpostProcessAfterInitialization方法之後,bean已經變成代理對象了。

因此,使用cglib生成的代理對象  (繼承方式),在父類中,通過代理對象調用父類私有方法不會報錯,但字段都是空的。

公衆號:Java藝術

掃碼關注最新動態

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章