先來看一段程式碼:
@Service
@Slf4j
public class AopTestService {
public String name = "真的嗎";
@Retryable
public void test(){
// 模擬業務操作
log.debug("name:{}", this.name);
// 模擬外部操作,失敗重試
}
}
很簡單的程式碼,然後在另一個類中進行呼叫
public void test(){
testService.test();
log.info("name:{}", testService.name);
}
問題也很簡單,以上程式碼列印輸出什麼?
如果沒能看出來,不妨先來看(笑)看(笑)我是怎樣觸發一個簡單的BUG。
以上程式碼肯定是不規範的。
正常應該是類裡定義為一個private
私有變數,然後提供getter/setter
方法供外部存取。
像這種將變數直接為定義public
,在外部類直接存取的情況,正常情況下我是寫不出來。
但是,話說某天,活急了,一個類寫了上千行程式碼,肯定得想把公共程式碼提取出來,將程式碼根據業務拆分。
原始類中有一個private
的成員變數,在該類內部方法中存取。由於部份程式碼拆分到其它類當中,該變數需要在外部被存取,我一時偷懶,就將該變數的存取級別由private改為public
。
省略業務程式碼,大概就變成了上面一開頭的範例程式碼。
習以為常的,我以為這樣就能存取了。
但我卻被啪啪打臉了。
因為我在方法加了個@Retryable
註解。
retryable是什麼?
由於一些網路,外部介面等不可預知的問題,我們的程式或者介面會失敗,這時就需要重試機制,比如延時1S後重試、間隔不斷增加重試等,自己去實現這些功能的話,顯得笨重而不優雅,所以spring官方實現了retryable模組。
這裡可以略過它的原理,只需知道它是使用了動態代理+AOP。
這個註解需給AopTestService
生成代理類。而動態代理是不能代理屬性的。所以在另一個類當中,使用AopTestService
的代理類不能直接存取目標類的成員變數。
嚴格意義來說,這還不算BUG,因為在偵錯階段就立馬發現了,但我確實沒能一眼看出來。
能夠一眼看出問題所在的大佬,請喝茶。
現在我們知道,動態代理類只能代理方法而不能代理屬性。但是話語是蒼白的,我們還是要有直接的證據。
最表象的原因,直接Debug截圖可以觀察到,aopTestService
由cglib
生成了代理類。在這個代理類裡value
值為null
。
再通過反編譯動態代理生成的程式碼,可以看到只有方法的定義,沒有父類別變數的定義。
cglib都可以,為什麼spring不可以呢?
再深入一點。我們可以在原始碼中斷點,看看cglib究竟如何沒有代理屬性。
在spring-aop模組中查詢類ObjenesisCglibAopProxy
,從名字當中就可以看出來,spring的動態代理全用了Objenesis
+cglib
。
在這個類中的createProxyClassAndInstance
方法斷點,在srping boot啟動的時候,可以觀察到:
可以看到這裡使用了Objenesis
範例化了AopTestService
代理物件。如果Objenesis
範例失敗,再通過預設構造方法進行範例。
因為沒有呼叫構造方法,所以spring生成動態代理類的時候沒能保留父類別的屬性。
從以上的程式碼和註釋當中也可以推測得出,它是一個可以繞過構造方法範例物件的一個工具。
為什麼需要繞過構造方法範例物件?
這又分為spring
和非spring
。
非srping下確實有這樣的場景,比如
構造器需要引數
構造器有side effects
構造器會拋異常
因此,在類庫中經常會有類必須擁有一個預設構造器的限制。Objenesis
通過繞開物件範例構造器來克服這個限制。
至於為什麼spring要使用Objenesis
繞過構造方法,那就是另一個問題了。
又比如這個知乎問題,看起來看是在釣魚,也有人認為是好問題,不曉得是不是反竄。
我覺得這位大佬說得很好
這位大佬說到最核心的點:
private標記內部程式碼,外部不應使用,並配合get/set使程式碼可控。
在一個系統裡,多人共同作業,從業人員,程式碼品質良莠不齊的情況下,程式碼可控是多麼的重要。
不僅僅是@Retryable
才會導致上面失效的場景,其它只要涉及到動態代理和AOP的都會導致失效。
比如最常見的事務,@Transcational
。
常見的面試經,導致spring事務失效的場景有哪些?
這12種場景,除卻自身的原因比如不支援事務,未被spring納入管理等,其它諸如方法存取許可權,final方法,內部呼叫等等都跟動態管理和AOP有關。
- springboot2.0以後動態代理使用cglib。cglib從名字
Code Generation Library
上來看就是一個程式碼生成的東西,它是要重寫該類,而private方法,final方法均無法被重寫。所以事務會失效。
private String value = "hello world";
@Transactional
public void proxy(ApplicationContext applicationContext) {
log.info(this.value);
}
public fianl void noProxy(ApplicationContext applicationContext) {
Object obj = applicationContext.getBean(this.getClass());
proxy(applicationContext);
}
以上範例程式碼中,通過在啟動main方法中設定
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "目錄");
將生成的動態代理類輸出到目錄中。
再反編譯過後,可以看到final
修改的方法沒有在這裡面,證明final方法
沒有被代理到。
- 方法內部呼叫。如果同類中,一個非事務方法呼叫另一個事務方法,預設使用的是this物件,非動態代理類的目標物件呼叫,所以會失效。
注意以上兩點,這是考點。
在上面的範例程式碼的基礎上簡單改一下。兩個事務方法,其中一個是final方法。
@Service
@Slf4j
public class AopTestService {
private String value = "hello world";
@Transcational
public void proxy(ApplicationContext applicationContext) {
Object obj = applicationContext.getBean(this.getClass());
boolean bool1 = AopUtils.isAopProxy(obj);
boolean bool2 = AopUtils.isAopProxy(this);
log.info("bool1:{},bool2:{},value:{}", bool1, bool2, value);
}
@Transcational
public final void noProxy(ApplicationContext applicationContext) {
Object obj = applicationContext.getBean(this.getClass());
boolean bool1 = AopUtils.isAopProxy(obj);
boolean bool2 = AopUtils.isAopProxy(this);
log.info("bool1:{},bool2:{},value:{}", bool1, bool2, value);
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
請問上面兩個方法分別輸出什麼?為什麼?
我們來捋一捋。
首先,兩個方法都加上了@Transcational註解,所以類AopTestService
和兩個方法都應該被代理。
然後noProxy
方法因為被final
修改,無法被重寫,所以最終noProxy
不會被代理。
當方法可以被代理的時候,代理物件使用的是目標物件來呼叫目標方法,所以'proxy'方法可以存取value
。
當noProxy
方法沒有被代理的時候,同時類AopTestService
卻被代理了,所以只能拿代理類來呼叫目標方法。而代理類是無法代理屬性的。所以這裡無法存取value
。
1.當代理類發現呼叫的方法可以代理的時候,就使用
目標物件
進行呼叫
這一點從下圖可以看出,最終invoke的傳入的是target目標物件,而是代理物件。
點選進去可以更明顯的看到,使用的是代理物件內部的目標物件
2.當代理類發現呼叫的方法無法代理的時候,就使用
代理物件
進行呼叫
這一點就更好理解了。假設我在controller層呼叫該service類方法,AopTestService
物件為代理物件,因該noProxy
沒有被代理,因此走的就是最普通正常的使用該代理物件直接呼叫。
bool1:true,bool2:false,value:hello world
noProxy
方法輸出:
bool1:true,bool2:true,value:null
從proxy
方法列印出來第1個布林值是true
,第2個布林值是false
,也可以反過來佐證上面的說法。
就是Object obj = applicationContext.getBean(this.getClass())
直接獲取spring ioc視窗裡的物件是代理的物件(true),
而執行到當前呼叫的卻是目標物件而非代理物件(false)。
但是,又一個問題來了,為什麼在自己的類裡面存取內部變數value會獲取到null
?
有點奇怪是吧?
因此,我給官方提了個issue:
https://github.com/spring-projects/spring-framework/issues/30102
但是,後來一想,這確實只是spring(非cglib)的一個feature
,而不是bug。
因為既然方法是final的,代表方法事務已然不生效了,在這種情況下,方法內部獲取不到類的內部變數屬於事務不生效引發的次生問題。
它本身是由於不規範的寫法導致的,因此我認為不能算是bug。
其實寫到這裡,這個不成熟的ussue有了回覆,大概看了一下,可能是我渣渣英語,沒有表述清楚,回覆其實就是把我問題的描述重複了一下,大概是就這麼設計的意思。
java的private關鍵字本身是很有意義的,同時也是防止bug的利器。
如果面試官再問到你spring事務失效的原因,除了12個場景以外,你或許還可以結合本文引申出來其它的內容,引導話題。