【RPC 專欄】深入理解 RPC 之動態代理篇

技術文章第一時間送達!

源碼精品專欄

  • 中文詳細注釋的開源項目

  • Java 並發源碼合集

  • RocketMQ 源碼合集

  • Sharding-JDBC 源碼解析合集

  • Spring MVC 和 Security 源碼合集

  • MyCAT 源碼解析合集

  • RpcRequest 和 RpcResponse

  • Socket傳輸

  • Netty 傳輸

  • 同步與非同步 阻塞與非阻塞

  • 總結


  • 提到 JAVA 中的動態代理,大多數人都不會對 JDK 動態代理感到陌生,Proxy,InvocationHandler 等類都是 J2SE 中的基礎概念。動態代理髮生在服務調用方/客戶端,RPC 框架需要解決的一個問題是:像調用本地介面一樣調用遠程的介面。於是如何組裝數據報文,經過網路傳輸發送至服務提供方,屏蔽遠程介面調用的細節,便是動態代理需要做的工作了。RPC 框架中的代理層往往是單獨的一層,以方便替換代理方式(如 motan 代理層位於com.weibo.api.motan.proxy,dubbo代理層位於com.alibaba.dubbo.common.bytecode)。

    實現動態代理的方案有下列幾種:

  • jdk 動態代理

  • cglib 動態代理

  • javassist 動態代理

  • ASM 位元組碼

  • javassist 位元組碼

  • 其中 cglib 底層實現依賴於 ASM,javassist 自成一派。由於 ASM 和 javassist 需要程序員直接操作位元組碼,導致使用門檻相對較高,但實際上他們的應用是非常廣泛的,如 Hibernate 底層使用了 javassist(默認)和 cglib,Spring 使用了 cglib 和 jdk 動態代理。

    RPC 框架無論選擇何種代理技術,所需要完成的任務其實是固定的,不外乎『整理報文』,『確認網路位置』,『序列化』,』網路傳輸』,『反序列化』,』返回結果』…

    技術選型的影響因素

    框架中使用何種動態代理技術,影響因素也不少。

    性能

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

    從其博客的測試結果來看 javassist > cglib > jdk 。但實際上他的測試過程稍微有點瑕疵:在 cglib 和 jdk 代理對象調用時,走的是反射調用,而在 javassist 生成的代理對象調用時,走的是直接調用(可以先閱讀下樑飛大大的博客)。這意味著 cglib 和 jdk 慢的原因並不是由動態代理產生的,而是由反射調用產生的(順帶一提,很多人認為 jdk 動態代理的原理是反射,其實它的底層也是使用的位元組碼技術)。而最終我的測試結果,結論如下: javassist ≈ cglib > jdk 。javassist 和 cglib 的效率基本持平 ,而他們兩者的執行效率基本可以達到 jdk 動態代理的2倍(這取決於測試的機器以及 jdk 的版本,jdk1.8 相較於 jdk1.6 動態代理技術有了質的提升,所以並不是傳聞中的那樣:cglib 比 jdk 快 10倍)。文末會給出我的測試代碼。

    依賴

    motan默認的實現是jdk動態代理,代理方案支持SPI擴展,可以自行擴展其他實現方式。

    使用jdk做為默認,主要是減少core包依賴,性能不是唯一考慮因素。另外使用位元組碼方式javaassist性能比較優秀,動態代理模式下jdk性能也不會差多少。

    –rayzhang0603(motan貢獻者)

    motan 選擇使用 jdk 動態代理,原因主要有兩個:減少 motan-core 的依賴,方便。至於擴展性,dubbo 並沒有預留出動態代理的擴展介面,而是寫死了 bytecode ,這點上 motan 做的較好。

    易用性

    從 dubbo 和 motan 的源碼中便可以直觀的看出兩者的差距了,dubbo 為了使用 javassist 技術花費不少的精力,而 motan 使用 jdk 動態代理只用了一個類。dubbo 的設計者為了追求極致的性能而做出的工作是值得肯定的,motan 也預留了擴展機制,兩者各有千秋。

    動態代理入門指南

    為了方便對比幾種動態代理技術,先準備一個統一介面。

    public interface BookApi { void sell();}JDK動態代理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 調用中 ,需要填充『整理報文』,『確認網路位置』,『序列化』,』網路傳輸』,『反序列化』,』返回結果』等邏輯。

    Cglib動態代理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 動態代理的操作步驟沒有太大的區別,只不過是替換了 cglib 的API而已。

    需要引入 cglib 依賴:

    dependency> groupId>cglibgroupId> artifactId>cglibartifactId> version>3.2.5version>dependency>Javassist位元組碼

    到了 javassist,稍微有點不同了。因為它是通過直接操作位元組碼來生成代理對象。

    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.javassistgroupId> artifactId>javassistartifactId> version>3.21.0-GAversion>dependency>動態代理測試

    測試環境:window i5 8g jdk1.8 cglib3.2.5 javassist3.21.0-GA

    動態代理其實分成了兩步:代理對象的創建,代理對象的調用。坊間流傳的動態代理性能對比主要指的是後者;前者一般不被大家考慮,如果遠程Refer的對象是單例的,其只會被創建一次,而如果是原型模式,多例對象的創建其實也是性能損耗的一個考慮因素(只不過遠沒有調用佔比大)。

    Create JDK Proxy: 21 ms

    Create CGLIB Proxy: 342 ms

    Create Javassist Bytecode Proxy: 419 ms

    可能出乎大家的意料,JDK 創建動態代理的速度比後兩者要快10倍左右。

    下面是調用速度的測試:

    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 的執行速度一定會慢於 Cglib 和 Javassist,但最慢也就2倍,並沒有達到數量級的差距;Cglib 和 Javassist不相上下,差距不大(測試中偶爾發現Cglib實行速度會比平時慢10倍,不清楚是什麼原因)

    所以出於易用性和性能,私以為使用 Cglib 是一個很好的選擇(性能和 Javassist 持平,易用性和 Jdk 持平)。

    反射調用

    既然提到了動態代理和 cglib ,順帶提一下反射調用如何加速的問題。RPC 框架中在 Provider 服務端需要根據客戶端傳遞來的 className + method + param 來找到容器中的實際方法執行反射調用。除了反射調用外,還可以使用 Cglib 來加速。

    JDK反射調用Method method = serviceClass.getMethod(methodName, new Class[]{});method.invoke(delegate, new Object[]{});Cglib調用FastClass serviceFastClass = FastClass.create(serviceClass);FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, new Class[]{});serviceFastMethod.invoke(delegate, new Object[]{});

    但實測效果發現 Cglib 並不一定比 JDK 反射執行速度快,還會跟具體的方法實現有關(大霧)。

    測試代碼

    略長…

    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 } // 執行反射調用 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 執行反射調用 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; }}666. 彩蛋

    如果你對 RPC 並發感興趣,歡迎加入我的知識一起交流。

    知識星球

    目前在知識星球(https://t.zsxq.com/2VbiaEu)更新了如下Dubbo源碼解析如下:01.調試環境搭建02.項目結構一覽03.API配置(一)之應用04.API配置(二)之服務提供者05.API配置(三)之服務消費者06.屬性配置07.XML配置08.核心流程一覽...一共60篇++


    推薦閱讀:

    分散式服務框架dubbo--服務引用的過程

    TAG:理解 | 動態 | 代理 | 動態代理 | 專欄 |