【RPC】动态代理:面向接口编程,屏蔽RPC处理流程

一、背景

如果我问你,你知道动态代理吗? 你可能会如数家珍般地告诉我动态代理的作用以及好处。那我现在接着问你,你在项目中用过动态代理吗?这时候可能有些人就会犹豫了。那我再换一个方式问你,你在项目中有实现过统一拦截的功能吗?比如授权认证、性能统计等等。你可能立马就会想到,我实现过呀,并且我知道可以用 Spring 的 AOP 功能来实现。

没错,进一步再想,在 Spring AOP 里面我们是怎么实现统一拦截的效果呢?并且是在我们不需要改动原有代码的前提下,还能实现非业务逻辑跟业务逻辑的解耦。这里的核心就是采用动态代理技术,通过对字节码进行增强,在方法调用的时候进行拦截,以便于在方法调用前后,增加我们需要的额外处理逻辑。

那话说回来,动态代理跟 RPC 又有什么关系呢?

二、远程调用的魔法

我说个具体的场景,你可能就明白了。

在项目中,当我们要使用 RPC 的时候,我们一般的做法是先找服务提供方要接口,通过 Maven 或者其他的工具把接口依赖到我们项目中。我们在编写业务逻辑的时候,如果要调用提供方的接口,我们就只需要通过依赖注入的方式把接口注入到项目中就行了,然后在代码里面直接调用接口的方法 。

我们都知道,接口里并不会包含真实的业务逻辑,业务逻辑都在服务提供方应用里,但我们通过调用接口方法,确实拿到了想要的结果,是不是感觉有点神奇呢?想一下,在 RPC 里面,我们是怎么完成这个魔术的。

这里面用到的核心技术就是前面说的动态代理。RPC 会自动给接口生成一个代理类,当我们在项目中注入接口的时候,运行过程中实际绑定的是这个接口生成的代理类。这样在接口方法被调用的时候,它实际上是被生成代理类拦截到了,这样我们就可以在生成的代理类里面,加入远程调用逻辑。

通过这种“偷梁换柱”的手法,就可以帮用户屏蔽远程调用的细节,实现像调用本地一样地调用远程的体验,整体流程如下图所示:

三、实现原理

动态代理在 RPC 里面的作用,就像是个魔术。现在我不妨给你揭秘一下,我们一起看看这是怎么实现的。之后,学以致用自然就不难了。

一起看下下面的流程图,具体代码细节你可以对照着 JDK 的源码看(上文中有类和方法,可以直接定位),我是按照 1.7.X 版本梳理的。

JDK 动态代理的核心是InvocationHandler 接口。这里提供一个 InvocationHandler 的Demo 实现,代码如下:

public class DemoInvokerHandler implements InvocationHandler {
 
    private Object target; // 真正的业务对象,也就是RealSubject对象
 
    public DemoInvokerHandler(Object target) { // 构造方法
 
        this.target = target;
 
    }
 
    public Object invoke(Object proxy, Method method, Object[] args)
 
             throws Throwable {
 
        // ...在执行业务方法之前的预处理...
 
        Object result = method.invoke(target, args);
 
        // ...在执行业务方法之后的后置处理...
 
        return result;
 
    }
 
    public Object getProxy() {
 
        // 创建代理对象
 
        return Proxy.newProxyInstance(Thread.currentThread()
 
            .getContextClassLoader(),
 
                target.getClass().getInterfaces(), this);
 
    }
 
}

接下来,我们可以创建一个 main() 方法来模拟上层调用者,创建并使用动态代理:

public class Main {
 
    public static void main(String[] args) {
 
        Subject subject = new RealSubject();
 
        DemoInvokerHandler invokerHandler = 
 
            new DemoInvokerHandler(subject);
 
        // 获取代理对象
 
        Subject proxy = (Subject) invokerHandler.getProxy();
 
        // 调用代理对象的方法,它会调用DemoInvokerHandler.invoke()方法
 
        proxy.operation();
 
    }
 
}

对于需要相同代理逻辑的业务类,只需要提供一个 InvocationHandler 接口实现类即可。在 Java 运行的过程中,JDK会为每个 RealSubject 类动态生成相应的代理类并加载到 JVM 中,然后创建对应的代理实例对象,返回给上层调用者。

JDK 动态代理相关实现的入口是 Proxy.newProxyInstance() 这个静态方法,它的三个参数分别是加载动态生成的代理类的类加载器、业务类实现的接口和上面介绍的InvocationHandler对象。

Proxy.newProxyInstance()方法的具体实现如下:

public static Object newProxyInstance(ClassLoader loader,
     Class[] interfaces, InvocationHandler h) 
 
         throws IllegalArgumentException {
 
    final Class<?>[] intfs = interfaces.clone();
 
    // ...省略权限检查等代码
 
    Class<?> cl = getProxyClass0(loader, intfs);  // 获取代理类
 
    // ...省略try/catch代码块和相关异常处理
 
    // 获取代理类的构造方法
 
    final Constructor<?> cons = cl.getConstructor(constructorParams);
 
    final InvocationHandler ih = h;
 
    return cons.newInstance(new Object[]{h});  // 创建代理对象
 
}

通过 newProxyInstance()方法的实现可以看到,JDK 动态代理是在 getProxyClass0() 方法中完成代理类的生成和加载。getProxyClass0() 方法的具体实现如下:

private static Class getProxyClass0 (ClassLoader loader, 
        Class... interfaces) {
 
    // 边界检查,限制接口数量(略)
 
    // 如果指定的类加载器中已经创建了实现指定接口的代理类,则查找缓存;
 
    // 否则通过ProxyClassFactory创建实现指定接口的代理类
 
    return proxyClassCache.get(loader, interfaces);
 
}

proxyClassCache 是定义在 Proxy 类中的静态字段,主要用于缓存已经创建过的代理类,定义如下:

private static final WeakCache[], Class> proxyClassCache
 
     = new WeakCache<>(new KeyFactory(), 
 
           new ProxyClassFactory());

WeakCache.get() 方法会首先尝试从缓存中查找代理类,如果查找不到,则会创建 Factory 对象并调用其 get() 方法获取代理类。Factory 是 WeakCache 中的内部类,Factory.get() 方法会调用 ProxyClassFactory.apply() 方法创建并加载代理类。

ProxyClassFactory.apply() 方法首先会检测代理类需要实现的接口集合,然后确定代理类的名称,之后创建代理类并将其写入文件中,最后加载代理类,返回对应的 Class 对象用于后续的实例化代理类对象。该方法的具体实现如下:

public Class apply(ClassLoader loader, Class[] interfaces) {
 
    // ... 对interfaces集合进行一系列检测(略)
 
    // ... 选择定义代理类的包名(略)
 
    // 代理类的名称是通过包名、代理类名称前缀以及编号这三项组成的
 
    long num = nextUniqueNumber.getAndIncrement();
 
    String proxyName = proxyPkg + proxyClassNamePrefix + num;
 
    // 生成代理类,并写入文件
 
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
 
            proxyName, interfaces, accessFlags);
 
    
 
    // 加载代理类,并返回Class对象
 
    return defineClass0(loader, proxyName, proxyClassFile, 0, 
 
      proxyClassFile.length);
 
}

ProxyGenerator.generateProxyClass() 方法会按照指定的名称和接口集合生成代理类的字节码,并根据条件决定是否保存到磁盘上。该方法的具体代码如下:

public static byte[] generateProxyClass(final String name,
 
       Class[] interfaces) {
 
    ProxyGenerator gen = new ProxyGenerator(name, interfaces);
 
    // 动态生成代理类的字节码,具体生成过程不再详细介绍,感兴趣的读者可以继续分析
 
    final byte[] classFile = gen.generateClassFile();
 
    // 如果saveGeneratedFiles值为true,会将生成的代理类的字节码保存到文件中
 
    if (saveGeneratedFiles) { 
 
        java.security.AccessController.doPrivileged(
 
            new java.security.PrivilegedAction() {
 
                public Void run() {
 
                    // 省略try/catch代码块
 
                    FileOutputStream file = new FileOutputStream(
 
                        dotToSlash(name) + ".class");
 
                    file.write(classFile);
 
                    file.close();
 
                    return null;
 
                }
 
            }
 
        );
 
    }
 
    return classFile; // 返回上面生成的代理类的字节码
 
}

最后,为了清晰地看到JDK动态生成的代理类的真正定义,我们需要将上述生成的代理类的字节码进行反编译。上述示例为RealSubject生成的代理类,反编译后得到的代码如下:

public final class $Proxy37 
 
      extends Proxy implements Subject {  // 实现了Subject接口
 
    // 这里省略了从Object类继承下来的相关方法和属性
 
    private static Method m3;
 
    static {
 
        // 省略了try/catch代码块
 
        // 记录了operation()方法对应的Method对象
 
        m3 = Class.forName("com.xxx.Subject")
 
          .getMethod("operation", new Class[0]);
 
    }
 
    // 构造方法的参数就是我们在示例中使用的DemoInvokerHandler对象
 
    public $Proxy11(InvocationHandler var1) throws {
 
        super(var1); 
 
    }
 
    public final void operation() throws {
 
        // 省略了try/catch代码块
 
        // 调用DemoInvokerHandler对象的invoke()方法
 
        // 最终调用RealSubject对象的对应方法
 
        super.h.invoke(this, m3, (Object[]) null);
 
    }
 
}

总结一下,JDK 动态代理的实现原理是动态创建代理类并通过指定类加载器进行加载,在创建代理对象时将InvocationHandler对象作为构造参数传入。当调用代理对象时,会调用 InvocationHandler.invoke() 方法,从而执行代理逻辑,并最终调用真正业务对象的相应方法。

四、实现方法

其实在 Java 领域,除了JDK 默认的nvocationHandler能完成代理功能,我们还有很多其他的第三方框架也可以,比如像 Javassist、Byte Buddy 这样的框架。

单纯从代理功能上来看,JDK 默认的代理功能是有一定的局限性的,它要求被代理的类只能是接口。原因是因为生成的代理类会继承 Proxy 类,但Java 是不支持多重继承的。

这个限制在RPC应用场景里面还是挺要紧的,因为对于服务调用方来说,在使用RPC的时候本来就是面向接口来编程的,这个我们刚才在前面已经讨论过了。使用JDK默认的代理功能,最大的问题就是性能问题。它生成后的代理类是使用反射来完成方法调用的,而这种方式相对直接用编码调用来说,性能会降低,但好在JDK8及以上版本对反射调用的性能有很大的提升,所以还是可以期待一下的。

相对 JDK 自带的代理功能,Javassist的定位是能够操纵底层字节码,所以使用起来并不简单,要生成动态代理类恐怕是有点复杂了。但好的方面是,通过Javassist生成字节码,不需要通过反射完成方法调用,所以性能肯定是更胜一筹的。在使用中,我们要注意一个问题,通过Javassist生成一个代理类后,此 CtClass 对象会被冻结起来,不允许再修改;否则,再次生成时会报错。

Byte Buddy 则属于后起之秀,在很多优秀的项目中,像Spring、Jackson都用到了Byte Buddy来完成底层代理。相比Javassist,Byte Buddy提供了更容易操作的API,编写的代码可读性更高。更重要的是,生成的代理类执行速度比Javassist更快。

虽然以上这三种框架使用的方式相差很大,但核心原理却是差不多的,区别就只是通过什么方式生成的代理类以及在生成的代理类里面是怎么完成的方法调用。同时呢,也正是因为这些细小的差异,才导致了不同的代理框架在性能方面的表现不同。因此,我们在设计RPC框架的时候,还是需要进行一些比较的,具体你可以综合它们的优劣以及你的场景需求进行选择。

五、总结

动态代理在RPC里面的应用,虽然它只是一种具体实现的技术,但我觉得只有理解了方法调用是怎么被拦截的,才能厘清在RPC里面我们是怎么做到面向接口编程,帮助用户屏蔽RPC调用细节的,最终呈现给用户一个像调用本地一样去调用远程的编程体验。

既然动态代理是一种具体的技术框架,那就会涉及到选型。我们可以从这样三个角度去考虑:

  • 因为代理类是在运行中生成的,那么代理框架生成代理类的速度、生成代理类的字节码大小等等,都会影响到其性能——生成的字节码越小,运行所占资源就越小。

  • 还有就是我们生成的代理类,是用于接口方法请求拦截的,所以每次调用接口方法的时候,都会执行生成的代理类,这时生成的代理类的执行效率就需要很高效。

  • 最后一个是从我们的使用角度出发的,我们肯定希望选择一个使用起来很方便的代理类框架,比如我们可以考虑:API设计是否好理解、社区活跃度、还有就是依赖复杂度等等。

最后,我想再强调一下。动态代理在RPC里面,虽然看起来只是一个很小的技术点,但就是这个创新使得用户可以不用关注细节了。其实,我们在日常设计接口的时候也是一样的,我们会想尽一切办法把细节对调用方屏蔽,让调用方的接入尽可能的简单。这就好比,让你去设计一个商品发布的接口,你并不需要暴露给用户一些细节,比如,告诉他们商品数据是怎么存储的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/332427.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【RT-DETR有效改进】轻量级网络ShuffleNetV2(附代码+修改教程)

前言 大家好&#xff0c;这里是RT-DETR有效涨点专栏。 本专栏的内容为根据ultralytics版本的RT-DETR进行改进&#xff0c;内容持续更新&#xff0c;每周更新文章数量3-10篇。 专栏以ResNet18、ResNet50为基础修改版本&#xff0c;同时修改内容也支持ResNet32、ResNet101和PP…

最新ChatGPT/GPT4科研应用与AI绘图及论文高效写作

详情点击链接&#xff1a;最新ChatGPT/GPT4科研应用与AI绘图及论文高效写作 一OpenAI 1.最新大模型GPT-4 Turbo 2.最新发布的高级数据分析&#xff0c;AI画图&#xff0c;图像识别&#xff0c;文档API 3.GPT Store 4.从0到1创建自己的GPT应用 5. 模型Gemini以及大模型Clau…

详细介绍IP 地址、网络号和主机号、ABC三类、ip地址可分配问题、子网掩码、子网划分

1、 IP 地址: 网络之间互连的协议&#xff0c;是由4个字节(32位二进制)组成的逻辑上的地址。 将32位二进制进行分组&#xff0c;分成4组&#xff0c;每组8位(1个字节)。【ip地址通常使用十进制表示】ip地址分成四组之后&#xff0c;在逻辑上&#xff0c;分成网络号和主机号 2…

Educational Codeforces Round 161 (Rated for Div. 2)(A~E)

被教育咯 A - Tricky Template 题意&#xff1a; 思路&#xff1a;读题读了半天..可以发现&#xff0c;若对于第位而言&#xff0c;&#xff0c;那么c就一定与模板匹配。否则模板只需要取大写的即可。因此若所有的 &#xff0c;都有&#xff0c;那么就不能构造&#xff0c;否…

gitgud.io+Sapphire注册账号教程

gitgud.io是一个仓库&#xff0c;地址 https://gitgud.io/&#xff0c;点进去之后会看到注册页面。 意思是需要通过注册这个Sapphire账户来登录。点击右边的Sapphire&#xff0c;就跳转到Sapphire的登陆页面&#xff0c;点击创建新账号&#xff0c;就进入注册页面。&#xff0…

中仕公考:国考进面后资格复审需要准备什么?

参加国考面试的考生在资格审核阶段需要准备以下材料&#xff1a; 1、本人身份证、学生证或工作证复印件。 2、公共科目笔试准考证复印件。 3、考试报名登记表。 4、本(专)科、研究生各阶段学历、学位证书(应届毕业生没有可以暂时不提供)。 5、报名资料上填写的各类证书材料…

【webrtc】GCC 7: call模块创建的ReceiveSideCongestionController

webrtc 代码学习&#xff08;三十二&#xff09; video RTT 作用笔记 从call模块说起 call模块创建的时候&#xff0c;会创建 src\call\call.h 线程&#xff1a; 统计 const std::unique_ptr<CallStats> call_stats_;SendDelayStats &#xff1a; 发送延迟统计 const…

统计学-R语言-6.1

文章目录 前言参数估计的原理总体、样本和统计量点估计区间估计评价估计量的标准有效性 总体均值的区间估计一个总体均值的估计&#xff08;大样本&#xff09;一个总体均值的估计&#xff08;小样本估计&#xff09; 练习 前言 本篇文章将开始介绍参数估计的相关知识。 参数估…

本地安装配置禅道BUG管理系统并结合内网穿透实现公网访问管理界面

文章目录 前言1. 本地安装配置BUG管理系统2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射本地服务3. 测试公网远程访问4. 配置固定二级子域名4.1 保留一个二级子域名5.1 配置二级子域名6. 使用固定二级子域名远程 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个…

《30天自制操作系统》学习笔记(七)

先体验一下编译仿真方法&#xff1a; 30天自制操作系统光盘代码在下面链接&#xff0c;但是没有编译仿真工具&#xff1a; https://gitee.com/zhanfei3000/30dayMakeOS 仿真工具在下面链接&#xff1a; https://gitee.com/909854136/nask-code-ide 这是一个集成的编译仿真工…

Docker五部曲之五:通过Docker和GitHub Action搭建个人CICD项目

文章目录 项目介绍Dockerfile解析compose.yml解析MySQL的准备工作Spring和环境变量的交互 GitHub Action解析项目测试结语 项目介绍 该项目是一个入门CICD-Demo&#xff0c;它由以下几部分组成&#xff1a; Dockerfile&#xff1a;用于构建自定义镜像compose.yml&#xff1a;…

开源免费的可私有化部署的白板excalidraw 详细部署教程

简介 excalidraw 是一款开源免费的虚拟白板&#xff0c;提供一个在线的实时协作白板工具&#xff0c;使用户能够创建简单的图形和图示。 excalidraw 的设计目标是提供一个易于使用的绘图工具&#xff0c;支持团队协作&#xff0c;同时具有跨平台和实时协作的功能。 简单易用&…

DAY04_Spring—Aop案例引入代理机制

目录 1 AOP1.1 AOP案例引入1.1.1 数据库事务说明 1.2 Spring实现事务控制1.2.1 代码结构如下1.2.2 编辑User1.2.3 编辑UserMapper/UserMapperImpl1.2.4 编辑UserService/UserServiceImpl1.2.5 编辑配置类1.2.6 编辑测试类 1.3 代码问题分析1.4 代理模式1.4.1 生活中代理案例1.4…

Gitlab添加ssh-key报500错误处理

Gitlab添加ssh-key报500错误 一、查看日志 发现Errno::Enoent(No such file or derectory -ssh): rootasu1:/home/caixin# tail -f /var/log/gitlab/gitlab-rails/production.log二、分析 根据日志提示&#xff0c;好像是缺少文件或目录&#xff0c;后面有个ssh,难首是依赖s…

【python】—— 字典

目录 &#xff08;一&#xff09;什么是字典 &#xff08;二&#xff09;字典的基本操作 2.1 创建字典 2.2 查找 key 2.3 新增/修改元素 2.4 删除元素 2.5 遍历字典元素 2.6 取出所有 key 和 value &#xff08;三&#xff09;合法的 key 类型 &#xff08;四&#xff09…

VUE组件--动态组件、组件保持存活、异步组件

动态组件 有些场景可能会需要在多个组件之间进行来回切换&#xff0c;在vue中则使用<component :is"..."> 来实现组件间的来回切换 // App.vue <template><component :is"tabComponent"></component><button click"change…

登陆提示:不支持你所在的地区,“Openai’s services are not available in your country…”

错误 登陆时提示“openai’s services are not available in your country”&#xff0c; 说明&#xff1a;Openai的服务在你的地区不可用解决&#xff1a;先清理下浏览器缓存&#xff0c;然后更换代理节点&#xff0c;开启全局模式&#xff0c;最好用欧美节点&#xff0c;或…

hyperf 二十一 数据库 模型关系

教程&#xff1a;Hyperf 一 定义关联 根据文档 一对一&#xff1a;Model::hasOne(被关联模型&#xff0c;被关联模型外键&#xff0c;本模型被关联的字段)一对多&#xff1a;Model::hasMany(被关联模型&#xff0c;被关联模型外键&#xff0c;本模型被关联的字段)反向一对多…

Docker 安装 PHP

Docker 安装 PHP 安装 PHP 镜像 方法一、docker pull php 查找 Docker Hub 上的 php 镜像: 可以通过 Sort by 查看其他版本的 php&#xff0c;默认是最新版本 php:latest。 此外&#xff0c;我们还可以用 docker search php 命令来查看可用版本&#xff1a; runoobrunoob:…

【51单片机】数码管的静态与动态显示(含消影)

数码管在现实生活里是非常常见的设备&#xff0c;例如 这些数字的显示都是数码管的应用。 目录 静态数码管&#xff1a;器件介绍&#xff1a;数码管的使用&#xff1a;译码器的使用&#xff1a;缓冲器&#xff1a; 实现原理&#xff1a;完整代码&#xff1a; 动态数码管&#…