歡迎光臨
每天分享高質量文章

【面試】我是如何在面試別人Spring事務時“套路”對方的

來自:程式設計新說

 

“中國最好面試官”


自從上次寫了一篇“【面試】我是如何面試別人List相關知識的,深度有點長文”的文章後,有讀者專門加我微信,說我是“中國最好面試官”,這個我可受不起呀。

我只是希望把面試當作是一次交流,像朋友那樣,而不是像一場Q & A。但也有人覺得,我對應聘者“太好了”,這完全沒必要,反正最後他也不會來。

好吧,那這次我就“使點壞”,“套路”一下麵試者。

記一次“帶套路”的面試

與這個面試者聊了一會兒,咦,發現他水平還可以,我內心有點兒喜出望外,終於遇到一個“合格”的“陪聊者”了,我要用Spring事務“好好套路”他一下。

我:你在開發中,一般都把事務加到哪一層?

他:都加到Service層。

我:現在基本都是基於註解的配置了,那和事務相關的註解是哪個?

他:我不太會讀那個單詞,就是以@T開頭的那個。

我:我明白你的意思,就是@Transactional。

他:是的。

我:與自己寫程式碼來開啟和提交事務相比,(先給他來個小的套路),這種透過註解來使用事務的方式叫什麼?

他:(猶豫了兩、三秒),不知道。

我:如果把寫程式碼那種叫程式設計式事務,那與之相對的應該是什麼式事務?

他:哦,宣告式事務。

我:(先鋪墊),不加註解,沒有事務,加上註解,就有事務,可見事務和註解有莫大的關係。(開始套路),那加上註解後,到底發生了什麼變化呢,就有了事務?

他:(猶豫了幾秒鐘),不知道。

我:(哈哈,意料之中),那我換一問法,Spring宣告式事務的底層是怎麼實現的?

他:是透過代理實現的。

我:(鋪墊),代理這個詞不僅計算機裡有,現實生活中也經常見到代理,比如招華北地區總代理等等。(套路),那你能不能在生活中舉一個代理的例子?

他:(想了一會兒),我沒有想到什麼好例子。

我:(開始聊會天),我看你老家離這還挺遠的,你一般都什麼時候回去啊?

他:一般都國慶節或春節會回去。其它時間假期短就不回去了。

我:(引子),國慶節和春節,人都很多啊,票不好買吧?

他:是啊,都在網上搶高鐵票,不停地刷。

我:(引子),現在有了高鐵,出行確實方便了很多。那你知道以前沒有高鐵、沒有12306的時候,人們都是怎麼買票的嗎?

他:我雖然沒有經歷過,但是我知道。那時候春運,都在火車站售票大廳買票,人們排很長的隊,有時需要等半天,還不一定有票。

我:(切入正題),除了火車站售票大廳外,你有沒有見過在城市裡分佈的一些火車票代售點?

他:現在偶爾還能見到幾個,但都已經關門了。

我:是啊,現在都網上買票了,代售點算是被歷史拋棄了。(開始套路),那你覺得代售點算不算火車站售票大廳的代理呢?

他:火車站售票大廳可以買票,代售點也可以買票,應該算是代理吧。

我:從廣義講算是代理。但有兩點需要註意:

一是,代售點賣的也是售票大廳的票,它自己是沒有票的,它只是行使售票大廳的權利。

二是,它可以有屬於自己的行為特徵,比如不需要排隊啊,每張硬座票收5元手續費啊等等。

我們平時聽到的中間商/代理商,其實都差不多是一回事兒。

他:經你這麼一說,我明白了。

我:那我們再說回到Spring中的代理,在Spring中生成代理的方式有幾種?

他:兩種,JDK動態代理和CGLIB。

我:那它們分別用於什麼情況下?

他:JDK動態代理只能用於帶介面的,CGLIB則帶不帶介面都行。

我:(鋪墊),假如有個介面,它包含兩個方法a和b,然後有一個類實現了該介面。在該實現類裡在a上標上事務註解、b上不標,此時事務是怎樣的?

他:a標註解了,肯定有事務,b沒有註解,所以沒有事務。

我:嗯,是這樣的。(開始套路),現在來做個簡單的修改,在方法b裡呼叫方法a,其它保持不變,此時再呼叫方法b,會有事務嗎?

他:應該有吧,雖然b沒有註解,但a有啊。

我:(我需要帶帶他),假設現在你和我都不知道有沒有事務,那我們來分析分析,看能不能找出答案。你有分析思路嗎?

他:沒有。

我:行吧,那我們開始。這是一個帶介面的,那就假定使用JDK動態代理吧。從宏觀上看,就是Spring使用JDK動態代理為這個類生成了一個代理,併為標有註解的方法添加了和事務相關的程式碼,所以就具有了事務。那你知道這個代理大概會是什麼樣子的嗎?

他:這個不知道。

我:透過代售點的例子我們應該知道,所有的代理都具有以下特點:

代理是一個空殼,它背後才是真正的老闆。

代理可以行使老闆的權力,所以它看起來“很像”老闆,除非仔細檢視,否則不易區分。

代理自己可以按需加進去一些行為特徵,除非仔細檢視,否則老闆都不一定知道這些。

那我們回到程式世界,使用介面和類再套一下上面的特點:

代理類是一個空殼(或外觀),它背後才是真正的類,通常稱為標的類。由此得出代理類要包含標的類。

對標的類和代理類的使用方式是一樣的,甚至你都不知道它是代理類。由此得出代理類和標的類的型別要相容,對外介面一致。所以標的類實現的介面,代理類也要實現。

代理類在把執行流程代理給標的類的過程中,可以新增一些行為程式碼,如開啟事務、提交事務等。

他:經你這麼一分析啊,我知道該怎麼寫程式碼了,應該是這樣的,請仔細看下程式碼,雖然很簡單:

//介面
interface Service {
    void doNeedTx();

    void doNotneedTx();
}

//標的類,實現介面
class ServiceImpl implements Service {

    @Transactional
    @Override
    public void doNeedTx() {
        System.out.println("execute doNeedTx in ServiceImpl");
    }

    //no annotation here
    @Override
    public void doNotneedTx() {
        this.doNeedTx();
    }
}

//代理類,也要實現相同的介面
class ProxyByJdkDynamic implements Service {

    //包含標的物件
    private Service target;

    public ProxyByJdkDynamic(Service target) {
        this.target = target;
    }

    //標的類中此方法帶註解,進行特殊處理
    @Override
    public void doNeedTx() {
        //開啟事務
        System.out.println("-> create Tx here in Proxy");
        //呼叫標的物件的方法,該方法已在事務中了
        target.doNeedTx();
        //提交事務
        System.out.println(");
    }

    //標的類中此方法沒有註解,只做簡單的呼叫
    @Override
    public void doNotneedTx() {
        //直接呼叫標的物件方法
        target.doNotneedTx();
    }
}


我:標的類是我們自己寫的,肯定是沒有事務的。代理類是系統生成的,對帶註解的方法進行事務增強,沒有註解的方法原樣呼叫,所以事務是代理類加上去的。

那回到一開始的問題,我們呼叫的方法不帶註解,因此代理類不開事務,而是直接呼叫標的物件的方法。當進入標的物件的方法後,執行的背景關係已經變成標的物件本身了,因為標的物件的程式碼是我們自己寫的,和事務沒有半毛錢關係,此時你再呼叫帶註解的方法,照樣沒有事務,只是一個普通的方法呼叫而已。

他:所以這個問題的答案就是沒有事務。

我:這是我們分析推理的結果,究竟對不對呢,還需要驗證一下。驗證過程如下:

找一個正常可用的Spring專案,把一個@Service的介面註入到一個@Controller類裡面,進行檢測,請仔細看下程式碼:

//是否是JDK動態代理
System.out.println("isJdkDynamicProxy => " + AopUtils.isJdkDynamicProxy(exampleService));
//是否是CGLIB代理
System.out.println("isCglibProxy => " + AopUtils.isCglibProxy(exampleService));
//代理類的型別
System.out.println("proxyClass => " + exampleService.getClass());
//代理類的父類的型別
System.out.println("parentClass => " + exampleService.getClass().getSuperclass());
//代理類的父類實現的介面
System.out.println("parentClass's interfaces => " + Arrays.asList(exampleService.getClass().getSuperclass().getInterfaces()));
//代理類實現的介面
System.out.println("proxyClass's interfaces => " + Arrays.asList(exampleService.getClass().getInterfaces()));
//代理物件
System.out.println("proxy => " + exampleService);
//標的物件
System.out.println("target => " + AopProxyUtils.getSingletonTarget(exampleService));
//代理物件和標的物件是不是同一個
System.out.println("proxy == target => " + (exampleService == AopProxyUtils.getSingletonTarget(exampleService)));
//標的類的型別
System.out.println("targetClass => " + AopProxyUtils.getSingletonTarget(exampleService).getClass());
//標的類實現的介面
System.out.println("targetClass's interfaces => " + Arrays.asList(AopProxyUtils.getSingletonTarget(exampleService).getClass().getInterfaces()));

System.out.println("----------------------------------------------------");

//自己模擬的動態代理的測試
Service target = new ServiceImpl();
ProxyByJdkDynamic proxy = new ProxyByJdkDynamic(target);
proxy.doNeedTx();
System.out.println("-------");
proxy.doNotneedTx();
System.out.println("-------");

以下是輸出結果:

//是JDK動態代理
isJdkDynamicProxy => true
//不是CGLIB代理
isCglibProxy => false
//代理類的型別,帶$的
proxyClass => class com.sun.proxy.$Proxy82
//代理類的父類
parentClass => class java.lang.reflect.Proxy
代理類的父類實現的介面
parentClass's interfaces => [interface java.io.Serializable]
//代理類實現的介面,包含了標的類的介面IExampleService,還有其它的
proxyClass's interfaces => [interface org.eop.sb.example.service.IExampleService,
interface org.springframework.aop.SpringProxy,
interface org.springframework.aop.framework.Advised,
interface org.springframework.core.DecoratingProxy]
//代理物件
proxy => org.eop.sb.example.service.impl.ExampleServiceImpl@54561bc9
//標的物件
target => org.eop.sb.example.service.impl.ExampleServiceImpl@54561bc9
//代理物件和標的物件輸出的都是@54561bc9,還真有點懵逼
//進行測試後發現,其實不是同一個,只是toString()的問題
proxy == target => false
//標的類,我們自己寫的
targetClass => class org.eop.sb.example.service.impl.ExampleServiceImpl
//標的類實現的介面,我們自己寫的
targetClass's interfaces => [interface org.eop.sb.example.service.IExampleService]
----------------------------------------------------
//帶註解的方法呼叫,有事務的開啟和提交
-> create Tx here in Proxy
execute doNeedTx in ServiceImpl
-------
//沒有註解的方法呼叫,是沒有事務的
execute doNeedTx in ServiceImpl
-------


經過測試後,發現和我們推斷的一模一樣。

他:你真是打破砂鍋問到底,把這個事情徹底弄明白了。

我:對於沒有實現介面的類,只能使用CGLIB來生成代理。(開始套路),假設有這樣一個類,它裡麵包含public方法,protected方法,private方法,package方法,final方法,static方法,我都給它們加上事務註解,哪些方法會有事務呢?

他:那我就現學現賣,事務是由代理加進去的,所以關鍵就是代理如何生成。按照上面所說的代理應該具備的特點來看,只能透過繼承的方式生成一個子類來充當代理,看起來就是這樣的:

class Target {

    @Transactional
    public void doNeedTx() {
        System.out.println("execute doNeedTx in Target");
    }

    //no annotation here
    public void doNotneedTx() {
        this.doNeedTx();
    }
}

class ProxyByCGLIB extends Target {

    private Target target;

    public ProxyByCGLIB(Target target) {
        this.target = target;
    }

    @Override
    public void doNeedTx() {
        System.out.println("-> create Tx in Proxy");
        target.doNeedTx();
        System.out.println(");
    }

    @Override
    public void doNotneedTx() {
        target.doNotneedTx();
    }
}


而且,必須在代理類裡重寫帶註解方法以新增開啟事務、提交事務的程式碼。從這個角度來說,private方法不能被繼承,final方法不能被重寫,static方法和繼承不相干,所以它們3個的事務不起作用。

public方法,protected方法可以被重寫以新增事務程式碼,對於package方法來說,如果生成的子類位於同一個包裡,就可以被重寫以新增事務程式碼。所以public方法事務肯定起作用,剩下那2個就不確定了,只能說它們有這個可能性。

我:你分析的很好,CGLIB確實是按照這種方式生成了子類作為代理,而且和父類在同一個包下。不過Spring選擇讓protected方法和package方法不支援事務,所以只有public方法支援事務。

使用和上面一樣的方法進行了測試,結果如下:

//不是JDK動態代理
isJdkDynamicProxy => false
//是CGLIB代理
isCglibProxy => true
//生成的代理類的型別,帶$$的
proxyClass => class org.eop.sb.example.service.impl.ExampleServiceImpl$$EnhancerBySpringCGLIB$$5320b86e
//代理類的父類,就是標的類
parentClass => class org.eop.sb.example.service.impl.ExampleServiceImpl
//父類實現的介面,就是我們自己寫的介面
parentClass's interfaces => [interface org.eop.sb.example.service.IExampleService]
/**代理類實現的介面,並不包含標的類的介面*/
proxyClass's interfaces => [interface org.springframework.aop.SpringProxy,
interface org.springframework.aop.framework.Advised,
interface org.springframework.cglib.proxy.Factory]
//代理物件
proxy => org.eop.sb.example.service.impl.ExampleServiceImpl@1b2702b1
//標的物件
target => org.eop.sb.example.service.impl.ExampleServiceImpl@1b2702b1
//代理物件和標的物件不是同一個
proxy == target => false
//標的類,我們自己寫的類
targetClass => class org.eop.sb.example.service.impl.ExampleServiceImpl
//標的類實現的介面
targetClass's interfaces => [interface org.eop.sb.example.service.IExampleService]

由於採用的是相同的測試程式碼,所以標的類是實現了介面的,不過這並不影響使用CGLIB來生成代理。可見,代理類確實繼承了標的類以保持和標的類的型別相容,對外介面相同。

註:只要是以代理方式實現的宣告式事務,無論是JDK動態代理,還是CGLIB直接寫位元組碼生成代理,都只有public方法上的事務註解才起作用。而且必須在代理類外部呼叫才行,如果直接在標的類裡面呼叫,事務照樣不起作用。

他:以前在網上也看到過有人說事務不生效的情況,我想,這個問題不會發生在我身上了。

後記


本文循序漸進地介紹了什麼是代理,代理具備的特徵,以及如何實現代理。它可是宣告式事務賴以存在的基石。

當然,除此之外,Spring事務還有很多其它方面的設計哲學和細節問題,後續再進行解說,也歡迎持續關註。

    贊(0)

    分享創造快樂