一個由public關鍵字引發的bug

先來看一段代碼:

@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。

bug之路

以上代碼肯定是不規範的。
正常應該是類裏定義爲一個private私有變量,然後提供getter/setter方法供外部訪問。

像這種將變量直接爲定義public,在外部類直接訪問的情況,正常情況下我是寫不出來。

但是,話說某天,活急了,一個類寫了上千行代碼,肯定得想把公共代碼提取出來,將代碼根據業務拆分。
原始類中有一個private的成員變量,在該類內部方法中訪問。由於部份代碼拆分到其它類當中,該變量需要在外部被訪問,我一時偷懶,就將該變量的訪問級別由private改爲public
省略業務代碼,大概就變成了上面一開頭的示例代碼。
習以爲常的,我以爲這樣就能訪問了。
但我卻被啪啪打臉了。


正常情況下,這樣雖然代碼不規範,但確實能訪問。 爲什麼這裏確不能訪問了呢?

因爲我在方法加了個@Retryable註解。

retryable是什麼?
由於一些網絡,外部接口等不可預知的問題,我們的程序或者接口會失敗,這時就需要重試機制,比如延時1S後重試、間隔不斷增加重試等,自己去實現這些功能的話,顯得笨重而不優雅,所以spring官方實現了retryable模塊。

這裏可以略過它的原理,只需知道它是使用了動態代理+AOP。

這個註解需給AopTestService 生成代理類。而動態代理是不能代理屬性的。所以在另一個類當中,使用AopTestService 的代理類不能直接訪問目標類的成員變量。

嚴格意義來說,這還不算BUG,因爲在調試階段就立馬發現了,但我確實沒能一眼看出來。

能夠一眼看出問題所在的大佬,請喝茶。


現在我們知道,動態代理類只能代理方法而不能代理屬性。但是話語是蒼白的,我們還是要有直接的證據。
最表象的原因,直接Debug截圖可以觀察到,aopTestServicecglib生成了代理類。在這個代理類裏value值爲null

再通過反編譯動態代理生成的代碼,可以看到只有方法的定義,沒有父類變量的定義。


爲什麼spring中的動態代理不能代理屬性?


前面說到,spring動態代理只能代理方法,不能代理屬性。

cglib都可以,爲什麼spring不可以呢?

再深入一點。我們可以在源碼中斷點,看看cglib究竟如何沒有代理屬性。

在spring-aop模塊中查找類ObjenesisCglibAopProxy,從名字當中就可以看出來,spring的動態代理全用了Objenesis+cglib
在這個類中的createProxyClassAndInstance方法斷點,在srping boot啓動的時候,可以觀察到:

可以看到這裏使用了Objenesis實例化了AopTestService代理對象。如果Objenesis實例失敗,再通過默認構造方法進行實例。
因爲沒有調用構造方法,所以spring生成動態代理類的時候沒能保留父類的屬性。


所以`Objenesis`是什麼?

從以上的代碼和註釋當中也可以推測得出,它是一個可以繞過構造方法實例對象的一個工具。
爲什麼需要繞過構造方法實例對象?

這又分爲spring非spring
非srping下確實有這樣的場景,比如

構造器需要參數
構造器有side effects
構造器會拋異常

因此,在類庫中經常會有類必須擁有一個默認構造器的限制。Objenesis通過繞開對象實例構造器來克服這個限制。


至於爲什麼spring要使用Objenesis繞過構造方法,那就是另一個問題了。


java爲什麼要有private關鍵字?


這似乎是一個無厘頭的問題,但是確實有很多初學者有這個疑問。 我想了想,至少在我剛接觸java的時候沒想過這個問題。創建一個`java bean`,`private`所有變量,然後自動生成`getter/setter`幹就完了。

又比如這個知乎問題,看起來看是在釣魚,也有人認爲是好問題,不曉得是不是反竄。

我覺得這位大佬說得很好

這位大佬說到最核心的點:

private標記內部代碼,外部不應使用,並配合get/set使代碼可控。

在一個系統裏,多人協作,從業人員,代碼品質良莠不齊的情況下,代碼可控是多麼的重要。


舉一反三


不僅僅是@Retryable纔會導致上面失效的場景,其它只要涉及到動態代理和AOP的都會導致失效。

比如最常見的事務,@Transcational

常見的面試經,導致spring事務失效的場景有哪些?

這12種場景,除卻自身的原因比如不支持事務,未被spring納入管理等,其它諸如方法訪問權限,final方法,內部調用等等都跟動態管理和AOP有關。

訪問權限和final


  1. 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方法沒有被代理到。


內部調用


  1. 方法內部調用。如果同類中,一個非事務方法調用另一個事務方法,默認使用的是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沒有被代理,因此走的就是最普通正常的使用該代理對象直接調用。





所以 `proxy` 方法輸出:

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個場景以外,你或許還可以結合本文引申出來其它的內容,引導話題。

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