設(shè)計模式(11)動態(tài)代理 JDK VS CGLIB面試必問

在上一篇文章我們介紹了代理模式逗嫡,靜態(tài)的伙单,本期我們介紹動態(tài)代理庄吼,動態(tài)代理的應(yīng)用也非常廣泛,也是在很多面試場合中必問的一個點颤芬,希望讀完本文,你將有所收獲巷挥。
原創(chuàng)聲明:未經(jīng)授權(quán)初厚,不得轉(zhuǎn)載衫生,侵權(quán)必究毡惜,轉(zhuǎn)載前請與作者取得聯(lián)系泳叠。

何謂動態(tài)代理

普通代理模式乌庶,代理類Proxy的Java代碼在JVM運行時就已經(jīng)確定了透敌,也就是在編碼編譯階段就確定了Proxy類的代碼背率。而動態(tài)代理是指在JVM運行過程中,動態(tài)的創(chuàng)建一個類的代理類桨仿,并實例化代理對象钞支。因為實際的代理類是在運行時創(chuàng)建的茫蛹,所以我們稱這個Java技術(shù)為:動態(tài)代理。

動態(tài)代理的應(yīng)用場景

動態(tài)代理的應(yīng)用場景有很多烁挟,例如久負盛名的RPC框架婴洼,在客戶端都會利用動態(tài)代理生成一個服務(wù)端接口的代理類來代理接口的調(diào)用執(zhí)行。同時Spring中的AOP就是一個動態(tài)代理的典型應(yīng)用信夫,有了動態(tài)代理窃蹋,媽媽再也不用擔心我的切面編程了。

實現(xiàn)動態(tài)代理的兩種方式

Java中有兩種生成動態(tài)代理的方式:

  • 基于JDK實現(xiàn)的動態(tài)代理静稻。
  • 基于CGLIB類庫實現(xiàn)的動態(tài)代理警没。

兩者的區(qū)別將在后文介紹。

JDK動態(tài)代理

在java的類庫中振湾,java.util.reflect.Proxy類就是其用來實現(xiàn)動態(tài)代理的頂層類杀迹。可以通過Proxy類的靜態(tài)方法Proxy.newProxyInstance()方法動態(tài)的創(chuàng)建一個類的代理類押搪,并實例化树酪。由它創(chuàng)建的代理類都是Proxy類的子類浅碾。

在看JDK動態(tài)代理的示例代碼之前,讓我們先來看看其UML類圖续语,從全局上進行一個理解垂谢。

JDK動態(tài)代理UML類圖


因為Proxy類是JDK為你創(chuàng)建的,所以你需要有辦法告訴Proxy類你要做什么疮茄,讓Proxy類代理你滥朱。但你不能像普通代理模式那樣,將被代理類通過組合的方式放到Proxy類中力试,那么要放到哪呢徙邻?放到InvocationHandler的實現(xiàn)類中,讓InvocationHandler的實現(xiàn)類來響應(yīng)代理的方法調(diào)用畸裳。

JDK動態(tài)代理實現(xiàn)步驟
(1)創(chuàng)建被代理對象的接口類缰犁。
(2)創(chuàng)建具體被代理對象接口的實現(xiàn)類。
(3)創(chuàng)建一個InvocationHandler的實現(xiàn)類怖糊,并持有被代理對象的引用帅容。然后在invoke方法中利用反射調(diào)用被代理對象的方法。
(4)利用Proxy.newProxyInstance方法創(chuàng)建代理對象蓬抄,利用代理對象實現(xiàn)真實對象方法的調(diào)用丰嘉。

JDK動態(tài)代理實現(xiàn)示例代碼

創(chuàng)建被代理對象的接口類Subject

public interface Subject {
    void request();
}

創(chuàng)建Subject接口的實現(xiàn)類:簡單打印一句輸出

public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("request invoke");
    }
}

創(chuàng)建一個InvocationHandler的實現(xiàn)類

public class ConcreteInvocationHandler implements InvocationHandler {

    private Subject subject;

    public ConcreteInvocationHandler(Subject subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        return method.invoke(subject, args);
    }
}

客戶端測試類

public class JDKDynamicProxyTest {
    public static void main(String[] args) {
        Subject subject = new RealSubject();
        InvocationHandler handler = new ConcreteInvocationHandler(subject);
        Subject proxy = (Subject)Proxy.newProxyInstance(RealSubject.class.getClassLoader(),
                RealSubject.class.getInterfaces(), handler);
        proxy.request();
    }
}

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)方法有三個入?yún)ⅲ?/p>

  • ClassLoader:用來創(chuàng)建并加載Proxy類的ClassLoader。
  • interfaces:被代理對象的接口列表嚷缭,這是JDK強制要求的饮亏,被代理的對象必須實現(xiàn)某一個接口。其目的是在生成Proxy類時也實現(xiàn)這些接口阅爽,做到能夠替代被代理類路幸。
  • invoactionhandler對象:用來提供Proxy轉(zhuǎn)發(fā)處理的幫助實現(xiàn)類,所有代理方法的調(diào)用都交由它調(diào)用付翁,再由它內(nèi)部實現(xiàn)對真正對象方法的調(diào)用简肴。

輸出結(jié)果:

proxy class name : com.sun.proxy.$Proxy0
request invoke

從輸出結(jié)果可以看到,生成代理類的類名為$Proxy0

這是JDK在運行時生成的字節(jié)碼類百侧。JDK生成Proxy類的字節(jié)碼是通過ProxyGenerator.generateProxyClass生成的砰识,筆者寫了個小工具生成字節(jié)碼,并輸出到文件佣渴,代碼如下:

byte[] proxyBytes = ProxyGenerator.generateProxyClass("$Proxy0",
        RealSubject.class.getInterfaces());
InputStream inputStream = new ByteArrayInputStream(proxyBytes);
FileOutputStream outputStream = new FileOutputStream("C:/$Proxy0.class");
byte[] buff = new byte[1024];
int len = 0;
while((len=inputStream.read(buff))!=-1){
    outputStream.write(buff, 0, len);
}
inputStream.close();
outputStream.close();

生成的字節(jié)碼文件辫狼,用反編譯工具看下$Proxy0的實現(xiàn):

public final class $Proxy0 extends Proxy implements Subject {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler paramInvocationHandler) {
        super(paramInvocationHandler);
    }

    public final boolean equals(Object paramObject) {
        try {
            return ((Boolean) this.h.invoke(this, m1, 
                    new Object[]{paramObject})).booleanValue();
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    public final String toString() {
        try {
            return (String) this.h.invoke(this, m2, null);
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    public final void request() {
        try {
            this.h.invoke(this, m3, null);
            return;
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    public final int hashCode() {
        try {
            return ((Integer) this.h.invoke(this, m0, null)).intValue();
        } catch (Error | RuntimeException localError) {
            throw localError;
        } catch (Throwable localThrowable) {
            throw new UndeclaredThrowableException(localThrowable);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.misout.designpattern.subject.Subject").getMethod("request", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException localNoSuchMethodException) {
            throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
        } catch (ClassNotFoundException localClassNotFoundException) {
            throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
        }
    }
}

從生成的類可以看出,$Proxy0實現(xiàn)了Subject接口辛润,這個接口正是我們傳遞進去的被代理對象的接口列表膨处。同時還繼承了Proxy類,這驗證了前文所說生成的代理類是Proxy子類的說明。
從類的結(jié)構(gòu)可以看出真椿,JDK動態(tài)生成代理類鹃答,一定要被代理類實現(xiàn)了某個接口,否則就無法生成代理類突硝,這也就是JDK動態(tài)代理的缺陷之一测摔。
另外,被代理類可以實現(xiàn)多個接口狞换。從代理類代碼中可以看到避咆,代理類是通過InvocationHandler的invoke方法去實現(xiàn)被代理接口方法調(diào)用的舟肉。所以被代理對象實現(xiàn)了多個接口并且希望對不同接口實施不同的代理行為時修噪,應(yīng)該在ConcreteInvocationHandler類的invoke方法中,通過判斷方法名來實現(xiàn)不同的接口的代理行為路媚。

CGLIB實現(xiàn)動態(tài)代理

CGLIB是一個高性能的代碼生成類庫黄琼,被Spring廣泛應(yīng)用。其底層是通過ASM字節(jié)碼框架生成類的字節(jié)碼整慎,達到動態(tài)創(chuàng)建類的目的脏款。

CGLIB實現(xiàn)的動態(tài)代理UML類圖:

我們知道,實現(xiàn)代理有兩種方式:

  • 要么通過繼承父類裤园,并改寫父類的方法撤师,在父類方法邏輯前后增加控制邏輯實現(xiàn)代理。
  • 要么實現(xiàn)同一接口拧揽,并利用組合的方式剃盾,持有被代理的引用,然后在代理方法前后增加控制邏輯實現(xiàn)代理淤袜。

那么從CGLIB實現(xiàn)的動態(tài)代理UML類圖來看痒谴,顯然是通過繼承父類的方式進行實現(xiàn)的。這樣在父類可以代替子類铡羡,代理子類可以直接調(diào)用父類的方法進行訪問积蔚。巧妙的是,如果想對真實類增強業(yè)務(wù)邏輯烦周,進行切面編程尽爆,則可以創(chuàng)建一個方法攔截器,在其中編寫自己增強的業(yè)務(wù)邏輯代碼或訪問控制代碼读慎,然后交給代理類進行調(diào)用訪問漱贱,達到AOP的效果。

下面贪壳,我們看看通過CGLIB實現(xiàn)動態(tài)代理的步驟:
(1)創(chuàng)建被代理的目標類饱亿。
(2)創(chuàng)建一個方法攔截器類,并實現(xiàn)CGLIB的MethodInterceptor接口的intercept()方法。
(3)通過Enhancer類增強工具彪笼,創(chuàng)建目標類的代理類钻注。
(4)利用代理類進行方法調(diào)用,就像調(diào)用真實的目標類方法一樣配猫。

CGLIB動態(tài)代理實現(xiàn)示例代碼

創(chuàng)建目標類:Target:方法簡單輸出一句話

public class Target {
    public void request() {
        System.out.println("執(zhí)行目標類的方法");
    }
}

創(chuàng)建目標類的方法增強攔截器:TargetMethodInterceptor:在攔截器內(nèi)部幅恋,調(diào)用目標方法前進行前置和后置增強處理。

public class TargetMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, 
                            MethodProxy proxy) throws Throwable {
        System.out.println("方法攔截增強邏輯-前置處理執(zhí)行");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("方法攔截增強邏輯-后置處理執(zhí)行");
        return result;
    }
}

生成代理類泵肄,并測試

public class CglibDynamicProxyTest {

    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();

        // 設(shè)置生成代理類的父類class對象
        enhancer.setSuperclass(Target.class);

        // 設(shè)置增強目標類的方法攔截器
        MethodInterceptor methodInterceptor = new TargetMethodInterceptor();
        enhancer.setCallback(methodInterceptor);

        // 生成代理類并實例化
        Target proxy = (Target) enhancer.create();

        // 用代理類調(diào)用方法
        proxy.request();
    }
}

測試輸出:可以看到成功進行了業(yè)務(wù)增強的處理捆交。

方法攔截增強邏輯-前置處理執(zhí)行
執(zhí)行目標類的方法
方法攔截增強邏輯-后置處理執(zhí)行

JDK動態(tài)代理 VS CGLIB 對比

  • 字節(jié)碼創(chuàng)建方式:JDK動態(tài)代理通過JVM實現(xiàn)代理類字節(jié)碼的創(chuàng)建,cglib通過ASM創(chuàng)建字節(jié)碼腐巢。
  • JDK動態(tài)代理強制要求目標類必須實現(xiàn)了某一接口品追,否則無法進行代理。而CGLIB則要求目標類和目標方法不能是final的冯丙,因為CGLIB通過繼承的方式實現(xiàn)代理肉瓦。
  • CGLib不能對聲明為final的方法進行代理,因為是通過繼承父類的方式實現(xiàn)胃惜,如果父類是final的泞莉,那么無法繼承父類。

性能對比

性能的對比船殉,不是一個簡單的答案鲫趁,要區(qū)分JDK版本來區(qū)分,這里得出的答案是基于其他作者的測試結(jié)果得出的利虫。

JDK1.6/1.7上的對比

  • 類的創(chuàng)建速度:JDK快于CGLIB挨厚。
  • 執(zhí)行速度:JDK慢于CGLIB,大概慢2倍的關(guān)系列吼。

JDK1.8上的對比

  • 類的創(chuàng)建速度:JDK快于CGLIB幽崩。
  • 執(zhí)行速度:JDK快于CGLIB,經(jīng)過努力寞钥,JDK1.8作了性能上的優(yōu)化慌申,速度明顯比1.7提升了很多。1.8上JDK全面優(yōu)于CGLIB理郑,是不是說以后都不要用CGLIB蹄溉,這還得看具體的類情況和場景,如果沒有實現(xiàn)接口您炉,就用CGLIB柒爵,使用的場景不同。

推薦閱讀

設(shè)計模式(一)策略模式
設(shè)計模式(二)觀察者模式
設(shè)計模式(三)裝飾器模式
設(shè)計模式(四)簡單工廠模式
設(shè)計模式(五)工廠方法模式
設(shè)計模式(六)抽象工廠模式
設(shè)計模式(七)單例模式你用對了嗎
設(shè)計模式(八)適配器模式
設(shè)計模式(九)模板方法
設(shè)計模式(十)代理模式

參考

深入剖析動態(tài)代理--性能比較
Spring AOP中的JDK和CGLib動態(tài)代理哪個效率更高赚爵?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末棉胀,一起剝皮案震驚了整個濱河市法瑟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唁奢,老刑警劉巖霎挟,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異麻掸,居然都是意外死亡酥夭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門脊奋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熬北,“玉大人,你說我怎么就攤上這事诚隙⊙纫” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵最楷,是天一觀的道長整份。 經(jīng)常有香客問我,道長籽孙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任火俄,我火速辦了婚禮犯建,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瓜客。我一直安慰自己适瓦,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布谱仪。 她就那樣靜靜地躺著玻熙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疯攒。 梳的紋絲不亂的頭發(fā)上嗦随,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音敬尺,去河邊找鬼枚尼。 笑死,一個胖子當著我的面吹牛砂吞,可吹牛的內(nèi)容都是我干的署恍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼蜻直,長吁一口氣:“原來是場噩夢啊……” “哼盯质!你這毒婦竟也來了袁串?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤呼巷,失蹤者是張志新(化名)和其女友劉穎般婆,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體朵逝,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡蔚袍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了配名。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啤咽。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖渠脉,靈堂內(nèi)的尸體忽然破棺而出宇整,到底是詐尸還是另有隱情,我是刑警寧澤芋膘,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布鳞青,位于F島的核電站,受9級特大地震影響为朋,放射性物質(zhì)發(fā)生泄漏臂拓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一习寸、第九天 我趴在偏房一處隱蔽的房頂上張望胶惰。 院中可真熱鬧,春花似錦霞溪、人聲如沸孵滞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坊饶。三九已至,卻和暖如春殴蓬,著一層夾襖步出監(jiān)牢的瞬間匿级,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工科雳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留根蟹,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓糟秘,卻偏偏與公主長得像简逮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子尿赚,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容

  • https://blog.csdn.net/luanlouis/article/details/24589193 ...
    小陳阿飛閱讀 864評論 1 1
  • 目錄:1.代理模式定義&實現(xiàn)2.裝飾模式定義&實現(xiàn)3.靜態(tài)代理4.動態(tài)代理:JDK動態(tài)代理散庶、Cglib動態(tài)代理5....
    lbcBoy閱讀 1,595評論 2 3
  • 參考資料:菜鳥教程之設(shè)計模式 設(shè)計模式概述 設(shè)計模式(Design pattern)代表了最佳的實踐蕉堰,通常被有經(jīng)驗...
    Steven1997閱讀 1,172評論 1 12
  • 2017年10月10日 拍打日記 60天禪拍實修之旅第10天 。第二個5天悲龟,練習與明理兩條腿走屋讶,今天學...
    禪子明仁閱讀 420評論 0 0
  • 今年的端午節(jié)上午還在練車,吃過午飯下午五個人又坐教練車一起來到池州须教,怕交警查不敢從高速皿渗,三小時才到池州,為提前參加...
    齊帆齊閱讀 1,380評論 15 29