《HR黑話大全》:那些殘忍的潛臺詞!你知道哪些?面試必懂。

前言

記一次「帶套路」的面試——我是如何在面試別人 Spring 事務時「套路」對方的。
前幾天和一個上家公司的同事聊了一會,他說最近在面試別人的時候,套路了面試者一波,我以爲頗有趣,就分享給你們!

與這個面試者聊了一下子,咦,發現他水平還能夠,我心裏有點兒喜出望外,終於遇到一個「合格」的「陪聊者」了,我要用 Spring 事務「好好套路」他一下。java

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

 他:都加到 Service 層。  面試

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

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

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

 他:是的。  編程

我:與本身寫代碼來開啓和提交事務相比,(先給他來個小的套路),這種經過註解來使用事務的方式叫什麼? 

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

我:若是把寫代碼那種叫編程式事務,那與之相對的應該是什麼式事務? 

他:哦,聲明式事務。  網絡

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

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

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

他:是經過代理實現的。  ide

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

他:(想了一下子),我沒有想到什麼好例子。 測試

我:(開始聊會天),我看你老家離這還挺遠的,你通常都何時回去啊? 

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

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

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

我:(引子),如今有了高鐵,出行確實方便了不少。那你知道之前沒有高鐵、沒有12306的時候,人們都是怎麼買票的嗎? 

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

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

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

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

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

我:從廣義講算是代理。但有兩點須要注意: 
  • 一是,代售點賣的也是售票大廳的票,它本身是沒有票的,它只是行使售票大廳的權利。 
  • 二是,它能夠有屬於本身的行爲特徵,好比不須要排隊啊,每張硬座票收5元手續費啊等等。 咱們平時聽到的中間商/代理商,其實都差很少是一回事兒。 

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

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

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

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

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

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

他:a標註解了,確定有事務,b沒有註解,因此沒有事務。 

我:嗯,是這樣的。(開始套路),如今來作個簡單的修改,在方法b裏調用方法a,其它保持不變,此時再調用方法b,會有事務嗎? 

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

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

 他:沒有。 

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

 他:這個不知道。 

我:經過代售點的例子咱們應該知道,全部的代理都具備如下特色: 
  • 1.代理是一個空殼,它背後纔是真正的老闆。 
  • 2.代理能夠行使老闆的權力,因此它看起來「很像」老闆,除非仔細查看,不然不易區分。  
  • 3.代理本身能夠按需加進去一些行爲特徵,除非仔細查看,不然老闆都不必定知道這些。  

那咱們回到程序世界,使用接口和類再套一下上面的特色: 

  • 1.代理類是一個空殼(或外觀),它背後纔是真正的類,一般稱爲目標類。由此得出代理類要包含目標類。 
  • 2.對目標類和代理類的使用方式是同樣的,甚至你都不知道它是代理類。由此得出代理類和目標類的類型要兼容,對外接口一致。因此目標類實現的接口,代理類也要實現。 
  • 3.代理類在把執行流程代理給目標類的過程當中,能夠添加一些行爲代碼,如開啓事務、提交事務等。
他:經你這麼一分析啊,我知道該怎麼寫代碼了,應該是這樣的,請仔細看下代碼,雖然很簡單:
  • //接口
    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("<- commit Tx here in Proxy");
    }
    //目標類中此方法沒有註解,只作簡單的調用
    @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 <- commit Tx here in Proxy ------- //沒有註解的方法調用,是沒有事務的 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("<- commit Tx in Proxy");
	}
	@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方法上的事務註解才起做用。並且必須在代理類外部調用才行,若是直接在目標類裏面調用,事務照樣不起做用。 

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

相關文章
相關標籤/搜索