【RPC 专栏】深入理解 RPC 之动态代理篇

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 【RPC 专栏】深入理解 RPC 之动态代理篇

点击上方“芋道源码”,选择“置顶公众号”

技术文章第一时间送达!

源码精品专栏

  • [**中文详细注释的开源项目**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484404&idx=1&sn=109f263e51b81ca9f270846dd16f6b3a&chksm=fa497c45cd3ef55358b09beb6e18ba04737799d3c0bc32baaa0796dc707b1275c0c555a249ba&scene=21#wechat_redirect)

  • **[Java 并发源码合集](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484341&idx=1&sn=91d6fc7e8841a0f6046e1c2f4693a537&chksm=fa497c04cd3ef512f9249a5deb305a28b68d3ba44467f13fa8c6068711540b2f3e0a6f622ae3&scene=21#wechat_redirect)**
  • [**RocketMQ 源码合集**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484334&idx=1&sn=761e2659f474f06e7db935eae26e2b03&chksm=fa497c1fcd3ef509a02890b8e9f6bddb02e714f9c7e70cfbc37cd5bd75be64855225497fd3de&scene=21#wechat_redirect)
  • [**Sharding-JDBC 源码解析合集**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484360&idx=1&sn=0dae84944d2c388fdc1bbed868ac5b99&chksm=fa497c79cd3ef56f8487dda6d53e3772e0aa9812ee66376993c3445bc94920c01a03dd4a4b8f&scene=21#wechat_redirect)
  • [**Spring MVC 和 Security 源码合集**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484380&idx=1&sn=b4e0da1a314d77dcd170a25ed1ebb4c5&chksm=fa497c6dcd3ef57bcfb69a52c594bcb72e35d9bbe89fa87601b2a6c9f266d656b1ad2a5d4da4&scene=21#wechat_redirect)

  • [**MyCAT 源码解析合集**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484377&idx=3&sn=1323ac1a4099fac49c96686e58d1960d&chksm=fa497c68cd3ef57e5c3b683f9ead89f06ea5d01947672bfff8341cff2ab0c39c03274723c49a&scene=21#wechat_redirect)
  • 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贡献者)

    使用jdk做为默认,主要是减少core包依赖,性能不是唯一考虑因素。另外使用字节码方式javaassist性能比较优秀,动态代理模式下jdk性能也不会差多少。

    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
        groupIdcglib/groupId
        artifactIdcglib/artifactId
        version3.2.5/version
    /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
        groupIdorg.javassist/groupId
        artifactIdjavassist/artifactId
        version3.21.0-GA/version
    /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

    Create CGLIB Proxy: 342 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 Proxy invoke cost 1912 ms

    JavassistBytecode Proxy invoke cost 1280 ms

    JDK Proxy invoke cost 1747 ms

    JavassistBytecode Proxy invoke cost 1175 ms

    JDK Proxy invoke cost 2616 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 篇++

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 【RPC 专栏】深入理解 RPC 之动态代理篇


     上一篇
    【RPC 专栏】深入理解 RPC 之协议篇 【RPC 专栏】深入理解 RPC 之协议篇
    点击上方“芋道源码”,选择“置顶公众号” 技术文章第一时间送达! 源码精品专栏 [**中文详细注释的开源项目**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=22474844
    2021-04-05
    下一篇 
    【RPC 专栏】深入理解 RPC 之传输篇 【RPC 专栏】深入理解 RPC 之传输篇
    点击上方“芋道源码”,选择“置顶公众号” 技术文章第一时间送达! 源码精品专栏 [**中文详细注释的开源项目**](http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=22474844
    2021-04-05