關注“Java藝術”一起來充電吧!
筆者在新的定時任務項目中,限定一個類只能寫一個Job
,類似於寫腳本,一個Job
一個腳本。對於簡單的任務我們並不約定一定要有Service
層,但在Job
中我們可能需要將某些數據庫操作放到事務中執行,爲讓註解事務生效,我們不能直接使用this
調用事務方法。
調用本類事務方法有兩種方式可以讓註解事務生效:
一是通過在類中注入自己,也就是循環依賴注入;
二是在需要時再從
bean
工廠中獲取bean
;
場景描述
假設現有類A
,在類A
的methodA
方法中,先從Spring
的bean
工廠獲取到類A
的實例,再調用類A
的methodB
方法,這樣做的目的是使事務生效。代碼如下:
@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
實例的字段都爲空,這些字段都是聲明自動注入的Mapper
與Service
,不可能爲空。但從調試結果我們可以看出,從bean
工廠獲取到的是AutoCloseTimeoutOrderJob
的代理對象,並非AutoCloseTimeoutOrderJob
。
在ProxyObjFieldNpe
的例子中,我們從bean
工廠獲取到的也是ProxyObjFieldNpe
的代理對象,該代理對象繼承ProxyObjFieldNpe
。因此,與上面截圖一樣,代理對象的字段都是NULL
。
外部調用ProxyObjFieldNpe
的methodA
方法調用的是代理類的methodA
方法,那爲什麼methodA
方法拿到字段的值非空,而methodB
方法拿到的是空值呢?
因爲methodB
方法被聲明爲private
了,代理類沒法重寫該方法。因此thisRef.methodB();
實際調用的是代理類父類的methodB
方法。methodB
方法中獲取fieldValue
獲取的是代理類對象的,這就是methodA
方法獲取到fieldValue
字段的值不爲NULL
,而methodB
方法獲取到fieldValue
字段的值爲NULL
的原因。
“methodB
方法中獲取fieldValue
獲取的是代理類對象的 ",這句我們稍後從字節碼層面理解。
問題:
爲什麼
methidB
方法的訪問標誌是private
,代理對象是ProxyObjFieldNpe
的子類,卻能調用其父類的methidB
方法?爲什麼代理對象的字段爲
NULL
?
爲什麼代理對象能調用父類的private方法?
因爲調用訪問標誌爲private
的methodB
方法是在ProxyObjFieldNpe
類的methodA
方法中調用的,而不是在代理類的methodA
方法中調用的,內部調用當然有訪問權限。
代理類繼承ProxyObjFieldNpe
,外部調用代理類的methodA
方法時,最終經過方法攔截器調用代理類父類的methodA
方法,因此methodA
方法中調用methodB
方法實際上是在父類中調用的。
ProxyObjFieldNpe
的methodA
方法編譯後生成的字節碼如下(部分):
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
偏移量爲15
、17
、20
三條指令是:從bean
工廠獲取代理bean
,並使用checkcast
指令將代理對象類型強制轉爲父類類型。
偏移量爲24
、25
兩條字節碼實現調用methodB
方法,非靜態方法的第一個隱式參數爲this
引用,此處傳的是代理類對象的引用,因此在methodB
方法中,使用this
(代理對象的引用)獲取到的字段都是空的。
爲什麼代理對象的字段爲NULL?
如果熟悉Spring Bean
生命週期,那麼就不難理解。
bean
的創建過程如下:
1、反射創建
bean
;2、爲
bean
注入屬性;3、調用
*Aware
接口的方法;4、調用
BeanPostProcessor
的postProcessBeforeInitialization
方法;5、調用初始化方法,
afterPropertiesSet
或自定義的初始化方法;6、調用
BeanPostProcessor
的postProcessAfterInitialization
方法;
代理對象是在上述步驟的第六步創建的,即調用某個BeanPostProcessor
的postProcessAfterInitialization
方法之後,返回代理對象,如果是單例對象,則會將該對象保存到bean
工廠(容器)中。也就是說,bean
工廠中存儲的是代理對象。
下面兩張圖是我在項目中調試Spring
代碼的截圖。(圖中的小紅點下方有個問號,這是條件斷點,只有滿足條件時纔會停在斷點處。條件的設置可右擊小紅點,在彈出框中輸出條件,條件的編寫與在代碼中添加一個if
語句是一樣的。)
在調用BeanPostProcessor
的postProcessAfterInitialization
方法之前, bean
還是原生的bean
。
在調用BeanPostProcessor
的postProcessAfterInitialization
方法之後,bean
已經變成代理對象了。
因此,使用cglib
生成的代理對象 (繼承方式),在父類中,通過代理對象調用父類私有方法不會報錯,但字段都是空的。
公衆號:Java藝術
掃碼關注最新動態