面試造火箭系列,栽在了cglib和jdk動態代理

程式設計師老貓發表於2021-12-28

“喂,你好,我是XX巴巴公司的技術面試官,請問你是張小帥嗎”。聲音是從電話那頭傳來的

“是的,你好”。小帥暗喜,大廠終於找上我了。

“下面我們來進行一下電話面試吧,請先自我介紹一下吧”

“balabalabla...”小帥把之前的經歷大概描述了一下

“嗯,經歷很豐富呀,接下來我們們來聊聊技術吧,請問cglib和jdk動態代理的區別是什麼呢?”

“額(⊙o⊙)…”,張小帥蒙了,場面一度尷尬。

......

面試的事情就發生在剛才,由於第一題就栽了,後面面試官的問題,小帥基本啥信心都沒了,此時小帥心情久久不能平靜,“這就是大廠的面試麼?”,小帥喃喃自語,他萬萬沒想到的是第一題就栽了。

螢幕前的小夥伴是否心中有數呢?接下來,就跟著老貓好好溫習一下吧。


聊起動態代理,我們們還是從代理模式先著手來看看吧。

代理模式

關於代理模式,查閱比較專業的資料是這麼定義的:代理模式給某一個物件提供一個代理物件,並由代理物件控制對原物件的引用。

主要解決:在直接訪問物件時帶來的問題,比如說:要訪問的物件在遠端的機器上。在物件導向系統中,有些物件由於某些原因(比如物件建立開銷很大,或者某些操作需要安全控制,或者需要程式外的訪問),直接訪問會給使用者或者系統結構帶來很多麻煩,我們可以在訪問此物件時加上一個對此物件的訪問層。

上述概念看起來比較模糊,舉些日常的例子,比方說火車票是個目標物件,我們們要去買,那麼我們不一定非得去火車站才能買到,其實在很多代理點也能買到。再比如豬八戒去找高翠蘭結果是孫悟空變的,可以這樣理解:把高翠蘭的外貌抽象出來,高翠蘭本人和孫悟空都實現了這個介面,豬八戒訪問高翠蘭的時候看不出來這個是孫悟空,所以說孫悟空是高翠蘭代理類。

靜態代理模式

程式碼演示
下面我們們通過買火車票的案例演示一下代理模式,具體程式碼如下:
抽象介面:

/**
 * @Author: 老貓
 * @Description: 票
 */
public interface Ticket {
    void getTicket();
}

火車站實現抽象介面具備買票功能

/**
 * @Author: 老貓
 * @Description: 火車站
 */
public class RailwayStation implements Ticket {

    @Override
    public void getTicket() {
        System.out.println("買了張火車票");
    }
}

火車站代理類實現抽象介面具備買票功能

/**
 * @Author: 老貓
 * @Description: 代理類
 * @Date: 2021/12/22 5:35 下午
 */
public class RailwayAgencyProxy implements Ticket{
    private RailwayStation railwayStation;

    public RailwayAgencyProxy(RailwayStation railwayStation) {
        this.railwayStation = railwayStation;
    }

    @Override
    public void getTicket() {
        railwayStation.getTicket();
    }
}

上述其實就是靜態代理模式。

優點:

  1. 可以做到在符合開閉原則的情況下對目標物件進行功能擴充套件。
  2. 職責非常清晰,一目瞭然。

缺點:

  1. 由於在客戶端和真實主題之間增加了代理物件,因此有些型別的代理模式可能會造成請求的處理速度變慢。
  2. 實現代理模式需要額外的工作,有些代理模式的實現非常複雜。
  3. 程式碼層面來看,如果介面發生改變,代理類也會發生變更。
動態代理

有了上面的基礎,我們們正式聊聊動態代理。
上面的例子其實我們不難發現,每個代理類只能夠實現一個介面服務。那麼如果當我們的軟體工程中出現多個適用代理模式的業務型別時那麼我們們就會建立多個代理類,那麼我們如何去解決這個問題呢?其實我們的動態代理就應運而生了。

很顯然動態代理類的位元組碼在程式執行時由Java反射機制動態生成,無需我們手工編寫它的原始碼。
那我們們基於上述的案例來先看看JDK動態代理類

JDK動態代理

直接看一下JDK動態代理的使用,如下程式碼塊

public class JDKDynamicProxy implements InvocationHandler {

    //被代理的物件
    private Object object;

    public JDKDynamicProxy(Object object) {
        this.object = object;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(object,args);
        return result;
    }

    //生成代理類
    public Object createProxyObj(){
        return Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(),this);
    }
}

靜態代理,動態代理呼叫如下:

public class TestProxy {
    public static void main(String[] args) {
        //靜態代理測試
        RailwayStation railwayStation = new RailwayStation();
        railwayStation.getTicket();

        RailwayAgencyProxy railwayAgencyProxy = new RailwayAgencyProxy(railwayStation);
        railwayAgencyProxy.getTicket();

        //動態代理測試
        Ticket ticket = new RailwayStation();
        JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy(ticket);
        Ticket proxyBuyTicket = (Ticket) jdkDynamicProxy.createProxyObj();
        proxyBuyTicket.getTicket();
    }
}

觀察上面動態代理以及靜態代理測試,動態代理的優勢就顯而易見了。如果我們再演示另外豬八戒和高翠蘭代理場景的時候,是不是就不用再去建立GaocuiLanProxy了,我們只需要通過JDKDynamicProxy的方式去建立代理類即可。

注意Proxy.newProxyInstance()方法接受三個引數:

  1. ClassLoader loader:指定當前目標物件使用的類載入器,獲取載入器的方法是固定的。
  2. Class<?>[] interfaces:指定目標物件實現的介面的型別,使用泛型方式確認型別
  3. InvocationHandler:指定動態處理器,執行目標物件的方法時,會觸發事件處理器的方法

通過上面的例子以及上述引數,我們不難發現JDK動態代理有這樣一個特性:
JDK動態代理是面向介面的代理模式,如果要用JDK代理的話,首先得有個介面,例如上面例子中的Ticket介面

cglib動態代理

我們們再來看一下cglib動態代理。先了解一下cglib是什麼。關於cglib,其實其官方解釋是比較少的,但是其本身是十分強大的,這也是很多人所詬病的。CGLIB(Code Generation Library)是一個開源專案!是一個強大的,高效能,高質量的Code生成類庫,它可以在執行期擴充套件Java類與實現Java介面。CGLIB是一個強大的高效能的程式碼生成包。它廣泛的被許多AOP的框架使用,例如Spring AOP為他們提供方法的interception(攔截)。CGLIB包的底層是通過使用一個小而快的位元組碼處理框架ASM,來轉換位元組碼並生成新的類。除了CGLIB包,指令碼語言例如Groovy和BeanShell,也是使用ASM來生成java的位元組碼。當然不鼓勵直接使用ASM,因為它要求你必須對JVM內部結構包括class檔案的格式和指令集都很熟悉。

接下來我們看一下用法,由於cglib並不是jdk自帶的,所以如果是maven專案的話,我們們首先需要的是引入cglib相關的pom依賴,如下:

<dependency>    
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

由於cglib代理的物件是類,這個是和JDK動態代理不一樣的地方,這個地方標紅加重點。
這樣的話,我們們同樣的代理類的話則應該如下定義,

public class Ticket {
    public void getTicket(){   
        System.out.println("買了張火車票");  
    }   
    
     final public void refundTicket(){  
        System.out.println("退了張火車票"); 
      }
 }

顯然,上面的類定義了兩個方法,一個是買火車票另外的話退火車票。

public class CglibDynamicProxy implements MethodInterceptor {    
        
    /**
     * @param o cglib生成的代理物件
     * @param method 被代理物件的方法
     * @param objects 傳入方法的引數
     * @param methodProxy 代理的方法
     * @return
     * @throws Throwable
     */   
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("execute pre");
            Object obj = methodProxy.invokeSuper(o,objects);
            System.out.println("execute after");
             return obj;
    }
}

呼叫測試入口方法呼叫

public class TestCglibProxy {
    public static void main(String[] args) {
        // 代理類class檔案存入本地磁碟方便我們反編譯檢視原始碼
   System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/kdaddy/project/log");
        // 通過CGLIB動態代理獲取代理物件的過程
        Enhancer enhancer = new Enhancer();
        // 設定enhancer物件的父類
        enhancer.setSuperclass(Ticket.class);
        // 設定enhancer的回撥物件
        enhancer.setCallback(new CglibDynamicProxy());
        // 建立代理物件
        Ticket ticket = (Ticket) enhancer.create();
        // 通過代理物件呼叫目標方法
        ticket.getTicket();
        // 通過代理嘗試呼叫final物件呼叫目標方法
        ticket.refundTicket();
    }
}

執行之後我們得到的結果如下:

execute pre
買了張火車票
execute after
取消了張火車票

根據日誌的列印情況,我們很容易發現相關列印“取消了張火車票”並沒有被代理,所以我們由此可以得到一個結論cglib動態代理無法代理被final修飾的方法

上述原始碼中,我們提及將代理類class寫到了相關的磁碟中,開啟對應的目錄,我們會發現下面三個檔案

原始碼檔案Ticket$$EnhancerByCGLIB$$4e79a04a類為cglib生成的代理類,該類即成了Ticket。
我們們來慢慢看看相關的原始碼:

public class Ticket$$EnhancerByCGLIB$$4e79a04a extends Ticket implements Factory {
    private boolean CGLIB$BOUND;
    public static Object CGLIB$FACTORY_DATA;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    //攔截器
    private MethodInterceptor CGLIB$CALLBACK_0; 
    private static Object CGLIB$CALLBACK_FILTER;
    //被代理方法
    private static final Method CGLIB$getTicket$0$Method; 
    //代理方法
    private static final MethodProxy CGLIB$getTicket$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$equals$1$Method;
    private static final MethodProxy CGLIB$equals$1$Proxy;
    private static final Method CGLIB$toString$2$Method;
    private static final MethodProxy CGLIB$toString$2$Proxy;
    private static final Method CGLIB$hashCode$3$Method;
    private static final MethodProxy CGLIB$hashCode$3$Proxy;
    private static final Method CGLIB$clone$4$Method;
    private static final MethodProxy CGLIB$clone$4$Proxy;

    static void CGLIB$STATICHOOK1() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        Class var0 = Class.forName("kdaddy.com.cglibDynamic.Ticket$$EnhancerByCGLIB$$4e79a04a");
        Class var1;
        CGLIB$getTicket$0$Method = ReflectUtils.findMethods(new String[]{"getTicket", "()V"}, (var1 = Class.forName("kdaddy.com.cglibDynamic.Ticket")).getDeclaredMethods())[0];
        CGLIB$getTicket$0$Proxy = MethodProxy.create(var1, var0, "()V", "getTicket", "CGLIB$getTicket$0");
        Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
        CGLIB$equals$1$Method = var10000[0];
        CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
        CGLIB$toString$2$Method = var10000[1];
        CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
        CGLIB$hashCode$3$Method = var10000[2];
        CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
        CGLIB$clone$4$Method = var10000[3];
        CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
    }
}    

我們通過代理類的原始碼可以看到,代理類會獲得所有在父類繼承來的方法,並且會有MethodProxy與之對應,當然被final修飾的方法除外,上述原始碼中我們也確實沒有看到之前的refundTicket方法。接下來往下看。

我們們看其中一個方法的呼叫。

//代理方法(methodProxy.invokeSuper會呼叫)
final void CGLIB$getTicket$0() {
        super.getTicket();
    }

//被代理方法(methodProxy.invoke會呼叫,這就是為什麼在攔截器中呼叫methodProxy.invoke會死迴圈,一直在呼叫攔截器)
    public final void getTicket() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
             //呼叫攔截器
            var10000.intercept(this, CGLIB$getTicket$0$Method, CGLIB$emptyArgs, CGLIB$getTicket$0$Proxy);
        } else {
            super.getTicket();
        }
    }

通過上述,我們看下getTicket整個的呼叫鏈路:
呼叫getTicket()方法->呼叫攔截器->methodProxy.invokeSuper->CGLIB$getTicket$0->被代理的getTicket方法。

接下來,我們們再來看一下比較核心的MethodProxy,我們們直接看下核心:methodProxy.invokeSuper,具體原始碼如下:

 public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        try {
            this.init();
            MethodProxy.FastClassInfo fci = this.fastClassInfo;
            return fci.f2.invoke(fci.i2, obj, args);
        } catch (InvocationTargetException var4) {
            throw var4.getTargetException();
        }
    }
    
//進一步看一下FastClassInfo
private static class FastClassInfo {
        FastClass f1;//被代理類FastClass
        FastClass f2;//代理類FastClass
        int i1; //被代理類的方法簽名(index)
        int i2;//代理類的方法簽名

        private FastClassInfo() {
        }
    }

上面程式碼呼叫過程就是獲取到代理類對應的FastClass,並執行了代理方法。還記得之前生成三個class檔案嗎?Ticket$$EnhancerByCGLIB$$4e79a04a$$FastClassByCGLIB$$f000183.class就是代理類的FastClass,Ticket$$FastClassByCGLIB$$a79cabb2.class就是被代理類的FastClass。

關於FastClass
Cglib動態代理執行代理方法效率之所以比JDK的高是因為Cglib採用了FastClass機制,它的原理簡單來說就是:為代理類和被代理類各生成一個Class,這個Class會為代理類或被代理類的方法分配一個index(int型別)。
這個index當做一個入參,FastClass就可以直接定位要呼叫的方法直接進行呼叫,這樣省去了反射呼叫,所以呼叫效率比JDK動態代理通過反射呼叫高。下面我們反編譯一個FastClass看看:

public int getIndex(Signature var1) {
        String var10000 = var1.toString();
        switch(var10000.hashCode()) {
        case -80792013:
            if (var10000.equals("getTicket()V")) {
                return 0;
            }
            break;
        case 189620111:
            if (var10000.equals("cancelTicket()V")) {
                return 1;
            }
            break;
        case 1826985398:
            if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
                return 2;
            }
            break;
        case 1913648695:
            if (var10000.equals("toString()Ljava/lang/String;")) {
                return 3;
            }
            break;
        case 1984935277:
            if (var10000.equals("hashCode()I")) {
                return 4;
            }
        }

        return -1;
    }
    ...此處省略部分程式碼...
    public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
        Ticket var10000 = (Ticket)var2;
        int var10001 = var1;

        try {
            switch(var10001) {
            case 0:
                var10000.getTicket();
                return null;
            case 1:
                var10000.cancelTicket();
                return null;
            case 2:
                return new Boolean(var10000.equals(var3[0]));
            case 3:
                return var10000.toString();
            case 4:
                return new Integer(var10000.hashCode());
            }
        } catch (Throwable var4) {
            throw new InvocationTargetException(var4);
        }

        throw new IllegalArgumentException("Cannot find matching method/constructor");
    }
    

FastClass並不是跟代理類一塊生成的,而是在第一次執行MethodProxy invoke/invokeSuper時生成的並放在了快取中。

//MethodProxy invoke/invokeSuper都呼叫了init()
private void init() {
        if(this.fastClassInfo == null) {
            Object var1 = this.initLock;
            synchronized(this.initLock) {
                if(this.fastClassInfo == null) {
                    MethodProxy.CreateInfo ci = this.createInfo;
                    MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
                    fci.f1 = helper(ci, ci.c1);//如果快取中就取出,沒有就生成新的FastClass,此處感興趣的小夥伴可以仔細看一下底層原始碼,老貓此處提及一下。
                    fci.f2 = helper(ci, ci.c2);
                    fci.i1 = fci.f1.getIndex(this.sig1);//獲取方法的index
                    fci.i2 = fci.f2.getIndex(this.sig2);
                    this.fastClassInfo = fci;
                    this.createInfo = null;
                }
            }
        }

    }

總結

如果螢幕前的你也像張小帥那樣,該如何應對呢?其實大部分的答案都在上面了。總結一下
(1)JDK動態代理是實現了被代理物件的介面,Cglib是繼承了被代理物件。
(2)JDK和Cglib都是在執行期生成位元組碼,JDK是直接寫Class位元組碼,Cglib使用ASM框架寫Class位元組碼,Cglib代理實現更復雜,生成代理類比JDK效率低。
(3)JDK呼叫代理方法,是通過反射機制呼叫,Cglib是通過FastClass機制直接呼叫方法,Cglib執行效率更高。

我是老貓,更多內容,歡迎大家搜尋關注老貓的公眾號“程式設計師老貓”。

相關文章