深入理解RPC之動(dòng)態(tài)代理篇(轉(zhuǎn))

提到 JAVA 中的動(dòng)態(tài)代理耙旦,大多數(shù)人都不會(huì)對(duì) JDK 動(dòng)態(tài)代理感到陌生,Proxy萝究,InvocationHandler 等類都是 J2SE 中的基礎(chǔ)概念免都。動(dòng)態(tài)代理發(fā)生在服務(wù)調(diào)用方/客戶端,RPC 框架需要解決的一個(gè)問(wèn)題是:像調(diào)用本地接口一樣調(diào)用遠(yuǎn)程的接口帆竹。于是如何組裝數(shù)據(jù)報(bào)文绕娘,經(jīng)過(guò)網(wǎng)絡(luò)傳輸發(fā)送至服務(wù)提供方,屏蔽遠(yuǎn)程接口調(diào)用的細(xì)節(jié)栽连,便是動(dòng)態(tài)代理需要做的工作了险领。RPC 框架中的代理層往往是單獨(dú)的一層,以方便替換代理方式(如 motan 代理層位于com.weibo.api.motan.proxy 秒紧,dubbo代理層位于 com.alibaba.dubbo.common.bytecode )绢陌。

實(shí)現(xiàn)動(dòng)態(tài)代理的方案有下列幾種:

  • jdk 動(dòng)態(tài)代理
  • cglib 動(dòng)態(tài)代理
  • javassist 動(dòng)態(tài)代理
  • ASM 字節(jié)碼
  • javassist 字節(jié)碼

其中 cglib 底層實(shí)現(xiàn)依賴于 ASM,javassist 自成一派熔恢。由于 ASM 和 javassist 需要程序員直接操作字節(jié)碼脐湾,導(dǎo)致使用門檻相對(duì)較高,但實(shí)際上他們的應(yīng)用是非常廣泛的叙淌,如 Hibernate 底層使用了 javassist(默認(rèn))和 cglib秤掌,Spring 使用了 cglib 和 jdk 動(dòng)態(tài)代理。

RPC 框架無(wú)論選擇何種代理技術(shù)鹰霍,所需要完成的任務(wù)其實(shí)是固定的闻鉴,不外乎‘整理報(bào)文’,‘確認(rèn)網(wǎng)絡(luò)位置’茂洒,‘序列化’,'網(wǎng)絡(luò)傳輸'孟岛,‘反序列化’,'返回結(jié)果'…

技術(shù)選型的影響因素

框架中使用何種動(dòng)態(tài)代理技術(shù)获黔,影響因素也不少蚀苛。

性能

從早期 dubbo 的作者梁飛的博客 http://javatar.iteye.com/blog/814426 中可以得知 dubbo 選擇使用 javassist 作為動(dòng)態(tài)代理方案主要考慮的因素是性能

從其博客的測(cè)試結(jié)果來(lái)看 javassist > cglib > jdk 玷氏。但實(shí)際上他的測(cè)試過(guò)程稍微有點(diǎn)瑕疵:在 cglib 和 jdk 代理對(duì)象調(diào)用時(shí)堵未,走的是反射調(diào)用,而在 javassist 生成的代理對(duì)象調(diào)用時(shí)盏触,走的是直接調(diào)用(可以先閱讀下梁飛大大的博客)渗蟹。這意味著 cglib 和 jdk 慢的原因并不是由動(dòng)態(tài)代理產(chǎn)生的块饺,而是由反射調(diào)用產(chǎn)生的(順帶一提,很多人認(rèn)為 jdk 動(dòng)態(tài)代理的原理是反射雌芽,其實(shí)它的底層也是使用的字節(jié)碼技術(shù))授艰。而最終我的測(cè)試結(jié)果,結(jié)論如下: javassist ≈ cglib > jdk 世落。javassist 和 cglib 的效率基本持平 淮腾,而他們兩者的執(zhí)行效率基本可以達(dá)到 jdk 動(dòng)態(tài)代理的2倍(這取決于測(cè)試的機(jī)器以及 jdk 的版本,jdk1.8 相較于 jdk1.6 動(dòng)態(tài)代理技術(shù)有了質(zhì)的提升屉佳,所以并不是傳聞中的那樣:cglib 比 jdk 快 10倍)谷朝。文末會(huì)給出我的測(cè)試代碼。

依賴

motan默認(rèn)的實(shí)現(xiàn)是jdk動(dòng)態(tài)代理武花,代理方案支持SPI擴(kuò)展圆凰,可以自行擴(kuò)展其他實(shí)現(xiàn)方式。
使用jdk做為默認(rèn)体箕,主要是減少core包依賴专钉,性能不是唯一考慮因素。另外使用字節(jié)碼方式j(luò)avaassist性能比較優(yōu)秀累铅,動(dòng)態(tài)代理模式下jdk性能也不會(huì)差多少跃须。
-- rayzhang0603(motan貢獻(xiàn)者)

motan 選擇使用 jdk 動(dòng)態(tài)代理,原因主要有兩個(gè):減少 motan-core 的依賴争群,方便回怜。至于擴(kuò)展性大年,dubbo 并沒(méi)有預(yù)留出動(dòng)態(tài)代理的擴(kuò)展接口换薄,而是寫死了 bytecode ,這點(diǎn)上 motan 做的較好翔试。

易用性

從 dubbo 和 motan 的源碼中便可以直觀的看出兩者的差距了轻要,dubbo 為了使用 javassist 技術(shù)花費(fèi)不少的精力,而 motan 使用 jdk 動(dòng)態(tài)代理只用了一個(gè)類垦缅。dubbo 的設(shè)計(jì)者為了追求極致的性能而做出的工作是值得肯定的冲泥,motan 也預(yù)留了擴(kuò)展機(jī)制,兩者各有千秋壁涎。

動(dòng)態(tài)代理入門指南

為了方便對(duì)比幾種動(dòng)態(tài)代理技術(shù)凡恍,先準(zhǔn)備一個(gè)統(tǒng)一接口。

public interface BookApi {
    void sell();
}

JDK動(dòng)態(tài)代理

private static BookApi createJdkDynamicProxy(final BookApi delegate) {
        BookApi jdkProxy = (BookApi) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{BookApi.class}, new JdkHandler(delegate));
        return jdkProxy;
}

private static class JdkHandler implements InvocationHandler {

        final Object delegate;

        JdkHandler(Object delegate) {
            this.delegate = delegate;
        }

        @Override
        public Object invoke(Object object, Method method, Object[] objects)
                throws Throwable {
            //添加代理邏輯<1>
            if(method.getName().equals("sell")){
                System.out.print("");
            }
            return null;
//            return method.invoke(delegate, objects);
        }

<1> 在真正的 RPC 調(diào)用中 怔球,需要填充‘整理報(bào)文’嚼酝,‘確認(rèn)網(wǎng)絡(luò)位置’,‘序列化’,'網(wǎng)絡(luò)傳輸'竟坛,‘反序列化’闽巩,'返回結(jié)果'等邏輯钧舌。

Cglib動(dòng)態(tài)代理

private static BookApi createCglibDynamicProxy(final BookApi delegate) throws Exception {
        Enhancer enhancer = new Enhancer();
        enhancer.setCallback(new CglibInterceptor(delegate));
        enhancer.setInterfaces(new Class[]{BookApi.class});
        BookApi cglibProxy = (BookApi) enhancer.create();
        return cglibProxy;
    }

    private static class CglibInterceptor implements MethodInterceptor {

        final Object delegate;

        CglibInterceptor(Object delegate) {
            this.delegate = delegate;
        }

        @Override
        public Object intercept(Object object, Method method, Object[] objects,
                                MethodProxy methodProxy) throws Throwable {
            //添加代理邏輯
            if(method.getName().equals("sell")) {
                System.out.print("");
            }
            return null;
//            return methodProxy.invoke(delegate, objects);
        }
    }

和 JDK 動(dòng)態(tài)代理的操作步驟沒(méi)有太大的區(qū)別,只不過(guò)是替換了 cglib 的API而已涎跨。

需要引入 cglib 依賴:

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

Javassist字節(jié)碼

到了 javassist洼冻,稍微有點(diǎn)不同了。因?yàn)樗峭ㄟ^(guò)直接操作字節(jié)碼來(lái)生成代理對(duì)象隅很。

private static BookApi createJavassistBytecodeDynamicProxy() throws Exception {
    ClassPool mPool = new ClassPool(true);
    CtClass mCtc = mPool.makeClass(BookApi.class.getName() + "JavaassistProxy");
    mCtc.addInterface(mPool.get(BookApi.class.getName()));
    mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
    mCtc.addMethod(CtNewMethod.make(
            "public void sell() { System.out.print(\"\") ; }", mCtc));
    Class<?> pc = mCtc.toClass();
    BookApi bytecodeProxy = (BookApi) pc.newInstance();
    return bytecodeProxy;
}

需要引入 javassist 依賴:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.21.0-GA</version>
</dependency>

動(dòng)態(tài)代理測(cè)試

測(cè)試環(huán)境:window i5 8g jdk1.8 cglib3.2.5 javassist3.21.0-GA

動(dòng)態(tài)代理其實(shí)分成了兩步:代理對(duì)象的創(chuàng)建撞牢,代理對(duì)象的調(diào)用。坊間流傳的動(dòng)態(tài)代理性能對(duì)比主要指的是后者叔营;前者一般不被大家考慮普泡,如果遠(yuǎn)程Refer的對(duì)象是單例的,其只會(huì)被創(chuàng)建一次审编,而如果是原型模式撼班,多例對(duì)象的創(chuàng)建其實(shí)也是性能損耗的一個(gè)考慮因素(只不過(guò)遠(yuǎn)沒(méi)有調(diào)用占比大)。

Create JDK Proxy: 21 ms
Create CGLIB Proxy: 342 ms
Create Javassist Bytecode Proxy: 419 ms

可能出乎大家的意料垒酬,JDK 創(chuàng)建動(dòng)態(tài)代理的速度比后兩者要快10倍左右砰嘁。

下面是調(diào)用速度的測(cè)試:

case 1:
JDK Proxy invoke cost 1912 ms
CGLIB Proxy invoke cost 1015 ms
JavassistBytecode Proxy invoke cost 1280 ms
case 2:
JDK Proxy invoke cost 1747 ms
CGLIB Proxy invoke cost 1234 ms
JavassistBytecode Proxy invoke cost 1175 ms
case 3:
JDK Proxy invoke cost 2616 ms
CGLIB Proxy invoke cost 1373 ms
JavassistBytecode Proxy invoke cost 1335 ms

Jdk 的執(zhí)行速度一定會(huì)慢于 Cglib 和 Javassist,但最慢也就2倍勘究,并沒(méi)有達(dá)到數(shù)量級(jí)的差距矮湘;Cglib 和 Javassist不相上下,差距不大(測(cè)試中偶爾發(fā)現(xiàn)Cglib實(shí)行速度會(huì)比平時(shí)慢10倍口糕,不清楚是什么原因)

所以出于易用性和性能缅阳,私以為使用 Cglib 是一個(gè)很好的選擇(性能和 Javassist 持平,易用性和 Jdk 持平)景描。

反射調(diào)用

既然提到了動(dòng)態(tài)代理和 cglib 十办,順帶提一下反射調(diào)用如何加速的問(wèn)題。RPC 框架中在 Provider 服務(wù)端需要根據(jù)客戶端傳遞來(lái)的 className + method + param 來(lái)找到容器中的實(shí)際方法執(zhí)行反射調(diào)用超棺。除了反射調(diào)用外向族,還可以使用 Cglib 來(lái)加速。

JDK反射調(diào)用

Method method = serviceClass.getMethod(methodName, new Class[]{});
method.invoke(delegate, new Object[]{});

Cglib調(diào)用

FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
serviceFastMethod.invoke(delegate, new Object[]{});

但實(shí)測(cè)效果發(fā)現(xiàn) Cglib 并不一定比 JDK 反射執(zhí)行速度快棠绘,還會(huì)跟具體的方法實(shí)現(xiàn)有關(guān)(大霧)件相。

測(cè)試代碼

略長(zhǎng)…

public class Main {

    public static void main(String[] args) throws Exception {

        BookApi delegate = new BookApiImpl();
        long time = System.currentTimeMillis();
        BookApi jdkProxy = createJdkDynamicProxy(delegate);
        time = System.currentTimeMillis() - time;
        System.out.println("Create JDK Proxy: " + time + " ms");

        time = System.currentTimeMillis();
        BookApi cglibProxy = createCglibDynamicProxy(delegate);
        time = System.currentTimeMillis() - time;
        System.out.println("Create CGLIB Proxy: " + time + " ms");

        time = System.currentTimeMillis();
        BookApi javassistBytecodeProxy = createJavassistBytecodeDynamicProxy();
        time = System.currentTimeMillis() - time;
        System.out.println("Create JavassistBytecode Proxy: " + time + " ms");

        for (int i = 0; i < 10; i++) {
            jdkProxy.sell();//warm
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            jdkProxy.sell();
        }
        System.out.println("JDK Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");

        for (int i = 0; i < 10; i++) {
            cglibProxy.sell();//warm
        }
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            cglibProxy.sell();
        }
        System.out.println("CGLIB Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");

        for (int i = 0; i < 10; i++) {
            javassistBytecodeProxy.sell();//warm
        }
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            javassistBytecodeProxy.sell();
        }
        System.out.println("JavassistBytecode Proxy invoke cost " + (System.currentTimeMillis() - start) + " ms");

        Class<?> serviceClass = delegate.getClass();
        String methodName = "sell";
        for (int i = 0; i < 10; i++) {
            cglibProxy.sell();//warm
        }
        // 執(zhí)行反射調(diào)用
        for (int i = 0; i < 10; i++) {//warm
            Method method = serviceClass.getMethod(methodName, new Class[]{});
            method.invoke(delegate, new Object[]{});
        }
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            Method method = serviceClass.getMethod(methodName, new Class[]{});
            method.invoke(delegate, new Object[]{});
        }
        System.out.println("反射 invoke cost " + (System.currentTimeMillis() - start) + " ms");

        // 使用 CGLib 執(zhí)行反射調(diào)用
        for (int i = 0; i < 10; i++) {//warm
            FastClass serviceFastClass = FastClass.create(serviceClass);
            FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
            serviceFastMethod.invoke(delegate, new Object[]{});
        }
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            FastClass serviceFastClass = FastClass.create(serviceClass);
            FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});
            serviceFastMethod.invoke(delegate, new Object[]{});
        }
        System.out.println("CGLIB invoke cost " + (System.currentTimeMillis() - start) + " ms");

    }

    private static BookApi createJdkDynamicProxy(final BookApi delegate) {
        BookApi jdkProxy = (BookApi) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{BookApi.class}, new JdkHandler(delegate));
        return jdkProxy;
    }

    private static class JdkHandler implements InvocationHandler {

        final Object delegate;

        JdkHandler(Object delegate) {
            this.delegate = delegate;
        }

        @Override
        public Object invoke(Object object, Method method, Object[] objects)
                throws Throwable {
            //添加代理邏輯
            if(method.getName().equals("sell")){
                System.out.print("");
            }
            return null;
//            return method.invoke(delegate, objects);
        }
    }

    private static BookApi createCglibDynamicProxy(final BookApi delegate) throws Exception {
        Enhancer enhancer = new Enhancer();
        enhancer.setCallback(new CglibInterceptor(delegate));
        enhancer.setInterfaces(new Class[]{BookApi.class});
        BookApi cglibProxy = (BookApi) enhancer.create();
        return cglibProxy;
    }

    private static class CglibInterceptor implements MethodInterceptor {

        final Object delegate;

        CglibInterceptor(Object delegate) {
            this.delegate = delegate;
        }

        @Override
        public Object intercept(Object object, Method method, Object[] objects,
                                MethodProxy methodProxy) throws Throwable {
            //添加代理邏輯
            if(method.getName().equals("sell")) {
                System.out.print("");
            }
            return null;
//            return methodProxy.invoke(delegate, objects);
        }
    }

    private static BookApi createJavassistBytecodeDynamicProxy() throws Exception {
        ClassPool mPool = new ClassPool(true);
        CtClass mCtc = mPool.makeClass(BookApi.class.getName() + "JavaassistProxy");
        mCtc.addInterface(mPool.get(BookApi.class.getName()));
        mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
        mCtc.addMethod(CtNewMethod.make(
                "public void sell() { System.out.print(\"\") ; }", mCtc));
        Class<?> pc = mCtc.toClass();
        BookApi bytecodeProxy = (BookApi) pc.newInstance();
        return bytecodeProxy;
    }

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市氧苍,隨后出現(xiàn)的幾起案子夜矗,更是在濱河造成了極大的恐慌,老刑警劉巖让虐,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件紊撕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡澄干,警方通過(guò)查閱死者的電腦和手機(jī)逛揩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門柠傍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人辩稽,你說(shuō)我怎么就攤上這事惧笛。” “怎么了逞泄?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵患整,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我喷众,道長(zhǎng)各谚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任到千,我火速辦了婚禮昌渤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘憔四。我一直安慰自己膀息,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布了赵。 她就那樣靜靜地躺著潜支,像睡著了一般。 火紅的嫁衣襯著肌膚如雪柿汛。 梳的紋絲不亂的頭發(fā)上冗酿,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音络断,去河邊找鬼裁替。 笑死,一個(gè)胖子當(dāng)著我的面吹牛妓羊,可吹牛的內(nèi)容都是我干的胯究。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼躁绸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了臣嚣?” 一聲冷哼從身側(cè)響起净刮,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎硅则,沒(méi)想到半個(gè)月后淹父,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怎虫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年暑认,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了困介。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蘸际,死狀恐怖座哩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粮彤,我是刑警寧澤根穷,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站导坟,受9級(jí)特大地震影響屿良,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惫周,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一尘惧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧递递,春花似錦褥伴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至逊躁,卻和暖如春似踱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背稽煤。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工核芽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酵熙。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓轧简,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親匾二。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哮独,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

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

  • https://blog.csdn.net/luanlouis/article/details/24589193 ...
    小陳阿飛閱讀 856評(píng)論 1 1
  • 動(dòng)態(tài)代理的意義在于生成一個(gè)占位(又稱代理對(duì)象),來(lái)代理真實(shí)對(duì)象察藐,從而控制真實(shí)對(duì)象的訪問(wèn)皮璧。 我們首先來(lái)談?wù)勈裁词谴?..
    Haozz_1994閱讀 303評(píng)論 0 2
  • title: Jdk動(dòng)態(tài)代理原理解析 tags:代理 categories:筆記 date: 2017-06-14...
    行徑行閱讀 19,240評(píng)論 3 36
  • 空曠的咖啡館里,一定有巨大的桌子分飞,我們十來(lái)人坐在長(zhǎng)桌的兩側(cè)悴务,我坐在離她不遠(yuǎn)的地方,她接著說(shuō)譬猫,我知道有那種幸福家庭的...
    是蓉蓉吶閱讀 253評(píng)論 2 3
  • 這兩年是我變化最大的兩年讯檐,也是我的格局被慢慢打開的兩年羡疗,截止到今天,我一共加入了三個(gè)社群别洪。這三個(gè)社群都給了我質(zhì)的改...
    卓安安閱讀 441評(píng)論 10 5