Simple RPC - 04 从零开始设计一个客户端(上)

news2024/9/20 12:16:28

文章目录

  • Pre
  • 设计
  • Code
    • 1. 理解Stub“桩”的实现原理
    • 2. 动态生成桩的接口 StubFactory
    • 3. 如何来实现工厂方法创建桩
      • 动态生成“桩”类的过程
        • 步骤概述
        • 代码实现
    • 技术点
      • 动态代理模式的应用
        • 动态代理的应用分析
      • 依赖倒置和SPI

在这里插入图片描述

Pre

Simple RPC - 01 框架原理及总体架构初探

Simple RPC - 02 通用高性能序列化和反序列化设计与实现

Simple RPC - 03 借助Netty实现异步网络通信


设计

  1. 回顾基础组件的实现
    • 序列化与网络传输的实现。
    • 这些组件在RPC框架中的重要性和作用。

  1. 理解客户端“桩”的实现原理
    • “桩”在RPC框架中的角色及其工作原理。
    • “代理模式”在RPC框架中应用。

  1. 设计并动态生成桩
    • 如何动态生成“桩”。
    • 如何使用动态代理机制在运行时生成和编译桩类。
    • 使用StubFactory接口以及如何实现它。

  1. 动态代理模式的应用
    • 动态代理模式在RPC框架及其他应用场景中的广泛用途。
    • 如何在实际开发中利用代理模式实现非侵入式逻辑注入。

  1. 依赖倒置原则的应用
    • 依赖倒置原则及其在解耦设计中的重要性。
    • Java中的SPI机制以及如何在RPC框架中应用它。

Code

实现RPC框架的客户端部分,尤其是其中最关键的“桩”部分。

1. 理解Stub“桩”的实现原理

“桩”是RPC框架在客户端的服务代理,它实现了与远程服务相同的方法签名,使得客户端在调用服务时,实际上是在调用“桩”提供的方法。这些方法负责将调用封装成网络请求,并将请求发送到服务端以获得结果。

在这里插入图片描述

在实现“桩”时,我们采用代理模式。这种模式为某个对象提供一个代理对象,由代理对象控制对原对象的引用。在RPC框架中,代理对象即为“桩”,而委托对象则是服务端实现具体业务逻辑的实例。

举个例子: 最常用 Spring 框架,它的核心 IOC(依赖注入)和 AOP(面向切面)机制,就是这种代理模式的一个实现。在日常开发的过程中,可以利用这种代理模式,在调用流程中动态地注入一些非侵入式业务逻辑。

这里的“非侵入”指的是,在现有的调用链中,增加一些业务逻辑,而不用去修改调用链上下游的代码。比如说,我们要监控一个方法 A 的请求耗时,普通的方式就是在方法的开始和返回这两个地方各加一条记录时间的语句,这种方法就需要修改这个方法的代码,这是一种“侵入式”的方式。

我们还可以给这个方法所在的类创建一个代理类,在这个代理类的 A 方法中,先记录开始时间,然后调用委托类的 A 方法,再记录结束时间。把这个代理类加入到调用链中,就可以实现“非侵入式”记录耗时了。同样的方式,还可以用在权限验证、风险控制、调用链跟踪等等这些场景中。


2. 动态生成桩的接口 StubFactory

为了动态生成桩,我们首先定义了一个StubFactory接口, 用于生成客户端的“桩”(即服务的代理类)。这个“桩”将负责与服务端通信,使客户端能够像调用本地方法一样调用远程服务。

 
/**
 * StubFactory接口定义了创建服务存根的工厂.
 * 它的主要作用是通过传输层和特定的服务类创建并返回一个服务存根.
 * @author artisan
 */
public interface StubFactory {
    /**
     * 创建并返回指定服务类的存根.
     * 
     * @param transport 传输层对象,用于网络通信.
     * @param serviceClass 服务类的Class对象,指定了要创建的存根类型.
     * @return 返回创建的服务存根对象,类型由serviceClass参数决定.
     */
    <T> T createStub(Transport transport, Class<T> serviceClass);
}

  • Transport:用于客户端与服务端之间的数据传输,负责发送请求和接收响应。
  • Class<T> serviceClass:表示服务接口的类型,createStub 方法需要根据该类型生成相应的“桩”。
  • 返回值 TcreateStub 返回生成的“桩”实例,该实例实现了指定的服务接口。

3. 如何来实现工厂方法创建桩

我们需要动态生成一个“桩”类,这个类会实现指定的接口,并将方法调用转化为网络请求发送给服务端,然后返回服务端的响应结果。

为了简化实现和便于理解,我们在这里做了一些假设:服务接口只能有一个方法,这个方法只能有一个参数,并且参数和返回值的类型都是 String 类型。

通常情况下,类是通过编写源代码,然后由编译器编译成字节码文件(.class 文件),最后由 JVM 加载并执行的。但是在动态生成“桩”类的场景中,我们需要在运行时根据指定的接口生成对应的类。这可以通过以下两种方式来实现:

  1. 静态生成:像 gRPC 这样的框架,会在编译 IDL 文件时生成目标语言(如 Java)的源代码文件,这些文件在编译后成为“桩”类。这种方式相对简单,但缺乏灵活性,因为“桩”类必须在编译时生成,运行时无法修改。

  2. 动态生成:像 Dubbo 这样的框架,会在运行时根据接口动态生成“桩”类。这种方式更加灵活,可以根据需要在运行时生成和修改“桩”类。


动态生成“桩”类的过程

我们将采用一种更通用的方法来动态生成“桩”类:首先生成“桩”类的源代码,然后动态编译该源代码,并将其加载到 JVM 中。

步骤概述
  1. 生成源代码:根据给定的接口定义,生成“桩”类的 Java 源代码。这个类将实现指定接口,并包含将方法调用转化为 RPC 请求的逻辑。

  2. 动态编译:将生成的源代码编译成字节码(.class 文件),这个过程可以在运行时完成。

  3. 加载类:将编译生成的字节码加载到 JVM 中,使得生成的类可以在当前应用中使用。


代码实现

编写一个 DynamicStubFactory 类来实现 StubFactory 接口。这个工厂类将动态生成“桩”类的源代码、编译并加载它,然后创建并返回对应的实例。

为了减少重复代码并简化动态生成“桩”的过程,我们可以引入一个 AbstractStub 抽象类,来封装那些通用的逻辑。通过让动态生成的“桩”类继承这个抽象类,可以避免在每个生成的类中重复实现相同的逻辑


/**
 * 动态存根工厂类,用于实现存根的动态创建
 * @author artisan
 */
public class DynamicStubFactory implements StubFactory{
    /**
     * 存根源代码模板,用于生成存根类的源代码
     */
    private final static String STUB_SOURCE_TEMPLATE =
            "package com.github.liyue2008.rpc.client.stubs;\n" +
            "import com.github.liyue2008.rpc.serialize.SerializeSupport;\n" +
            "\n" +
            "public class %s extends AbstractStub implements %s {\n" +
            "    @Override\n" +
            "    public String %s(String arg) {\n" +
            "        return SerializeSupport.parse(\n" +
            "                invokeRemote(\n" +
            "                        new RpcRequest(\n" +
            "                                \"%s\",\n" +
            "                                \"%s\",\n" +
            "                                SerializeSupport.serialize(arg)\n" +
            "                        )\n" +
            "                )\n" +
            "        );\n" +
            "    }\n" +
            "}";

    /**
     * 根据传输层和接口类创建存根对象
     * @param transport 传输层实现,用于远程调用
     * @param serviceClass 服务接口类
     * @param <T> 服务接口类型
     * @return T 类型的存根对象
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T> T createStub(Transport transport, Class<T> serviceClass) {
        try {
            // 填充模板,生成存根类的源代码
            String stubSimpleName = serviceClass.getSimpleName() + "Stub";
            // 服务接口的全限定名
            String classFullName = serviceClass.getName();
            // 生成的“桩”类的全限定名-->桩的类名就定义为:“接口名 + Stub”,为了避免类名冲突,我们把这些桩都统一放到固定的包 com.github.liyue2008.rpc.client.stubs 下面
            String stubFullName = "com.github.liyue2008.rpc.client.stubs." + stubSimpleName;
            // 服务接口中的方法名
            String methodName = serviceClass.getMethods()[0].getName();

            // 填充模板后的源代码字符串
            String source = String.format(STUB_SOURCE_TEMPLATE, stubSimpleName, classFullName, methodName, classFullName, methodName);
            
            // 编译源代码
            JavaStringCompiler compiler = new JavaStringCompiler();
            Map<String, byte[]> results = compiler.compile(stubSimpleName + ".java", source);

            // 加载编译好的类
            Class<?> clazz = compiler.loadClass(stubFullName, results);

            // 创建存根实例,并设置传输层实现
            ServiceStub stubInstance = (ServiceStub) clazz.newInstance();
            // 把用于网络传输的对象 transport 赋值给桩,这样桩才能与服务端进行通信。
            stubInstance.setTransport(transport);
            // 返回存根实例
            return (T) stubInstance;
        } catch (Throwable t) {
            // 将任何异常转换为运行时异常
            throw new RuntimeException(t);
        }
    }
}


  • 生成源代码:首先根据给定的服务接口,利用模板生成对应的“桩”类的源代码。这个类实现了指定的接口,并将方法调用转化为 RPC 请求。

  • 动态编译:生成的源代码被传递给 JavaStringCompiler 进行编译,编译结果是字节码(.class 文件)。

  • 加载类:编译后的字节码通过 JavaStringCompiler 加载到 JVM 中,使得这个类在当前应用中可用。

  • 创建实例:通过反射创建“桩”类的实例,并将 Transport 对象注入到该实例中。最终,这个实例被返回给调用方。


关键点总结

  • 动态生成代码:使用模板和反射机制,动态生成实现特定接口的“桩”类的源代码。

  • 动态编译:使用自定义编译器(JavaStringCompiler),在运行时编译生成的源代码,得到字节码。

  • 动态加载:将编译后的字节码加载到 JVM 中,使得生成的类可以被正常使用。

  • 反射机制:利用 Java 反射机制实例化动态生成的类,并通过接口类型进行返回,确保生成的“桩”可以在客户端代码中透明使用。

  • 简化与复用:通过继承 AbstractStub,将通用的 RPC 调用逻辑封装起来,减少重复代码,并提升可维护性。

通过这种方法,我们可以在运行时动态生成并加载“桩”类,实现对远程调用的透明化封装。这个过程虽然复杂,但它提供了极大的灵活性,允许 RPC 框架根据不同的接口动态生成和管理“桩”类。通过了解这一过程,我们可以轻松扩展和优化这个实现,以支持更多的接口方法和参数类型。


/**
 * 抽象服务存根类,实现了ServiceStub接口
 * @author artisan
 */
public abstract class AbstractStub implements ServiceStub {
    /**
     * 用于通信的传输层对象
     */
    protected Transport transport;

    /**
     * 调用远程服务方法
     *
     * @param request 远程过程调用请求对象
     * @return 远程服务返回的数据
     * @throws RuntimeException 如果执行过程中出现异常
     * @throws Exception 如果远程服务返回错误
     */
    protected byte[] invokeRemote(RpcRequest request) {
        // 创建请求头,指定服务类型为RPC请求,版本为1,请求ID通过RequestIdSupport生成
        Header header = new Header(ServiceTypes.TYPE_RPC_REQUEST, 1, RequestIdSupport.next());
        // 将请求对象序列化为字节数组
        byte[] payload = SerializeSupport.serialize(request);
        // 使用请求头和负载创建一个请求命令对象
        Command requestCommand = new Command(header, payload);

        try {
            // 通过传输层发送请求命令,等待返回命令
            Command responseCommand = transport.send(requestCommand).get();
            // 解析返回命令的头信息
            ResponseHeader responseHeader = (ResponseHeader) responseCommand.getHeader();

            // 检查响应状态码,如果成功则返回响应数据
            if (responseHeader.getCode() == Code.SUCCESS.getCode()) {
                return responseCommand.getPayload();
            } else {
                // 如果响应状态码表示错误,则抛出异常
                throw new Exception(responseHeader.getError());
            }

        } catch (ExecutionException e) {
            // 如果异步执行过程中出现异常,包装后抛出RuntimeException
            throw new RuntimeException(e.getCause());
        } catch (Throwable e) {
            // 捕获所有异常和错误,包装后抛出RuntimeException
            throw new RuntimeException(e);
        }
    }

    /**
     * 设置用于通信的传输层对象
     *
     * @param transport 传输层对象
     */
    @Override
    public void setTransport(Transport transport) {
        this.transport = transport;
    }
}


技术点

动态代理模式的应用

动态代理模式 是一种设计模式,它允许我们在运行时动态地创建实现某些接口的代理对象,并定义这些代理对象的行为。相比于静态代理,它更加灵活,因为代理类并不需要在编译时定义,而是可以根据运行时的需求动态生成。

在RPC框架的设计中,动态代理模式可以在不修改源码的情况下,在调用链中注入额外的逻辑。我们利用这一模式,在桩中动态封装调用请求,并发送到服务端。这种非侵入式的设计方法使得代码更灵活、可扩展性更强。

在上述代码中,使用了 动态代理模式 来实现客户端调用远程服务的“桩”(Stub),即在运行时根据服务接口生成代理类,使得客户端可以像调用本地方法一样调用远程服务。

动态代理的应用分析

在上述代码中,DynamicStubFactory 通过动态代理模式实现了 RPC 客户端的“桩”生成。其具体应用如下:

  1. 接口与代理类

    • StubFactory 接口定义了一个通用的桩工厂方法 createStub,用来创建某个服务接口的代理对象(即“桩”)。
    • 动态生成的代理类(“桩”)会实现客户端指定的服务接口,并封装远程调用逻辑。
  2. 模板生成代理类

    • 通过字符串模板 STUB_SOURCE_TEMPLATE,生成了服务接口的代理类的 Java 源代码。这些代理类继承自 AbstractStub,并实现了指定的服务接口。
    • 在模板中,代理类的每个方法都按照统一的模式,将方法名、参数等信息封装成一个 RpcRequest 对象,通过网络传输给远程服务器,并处理返回值。
  3. 动态编译与加载

    • 使用 JavaStringCompiler 在运行时编译生成的 Java 源代码,得到字节码。
    • 将编译后的字节码动态加载到 JVM 中,生成代理类的 Class 对象。
    • 通过反射机制实例化这个代理类,并返回给客户端。
  4. 动态代理的好处

    • 灵活性:通过动态代理,RPC 框架可以根据任意服务接口动态生成对应的代理类,而无需手动编写和维护这些代理类。
    • 封装性:客户端只需调用服务接口中的方法,具体的远程调用逻辑完全被代理类封装起来,客户端无需关心底层实现。
    • 扩展性:模板和编译机制使得动态生成的代理类可以适应不同的接口和方法,无需在编译期确定具体的代理逻辑。

依赖倒置和SPI

为了使客户端与具体的桩实现解耦,我们采用了依赖倒置原则。调用方依赖于StubFactory接口,而不直接依赖具体实现。我们还利用了Java的SPI机制,通过配置文件的方式动态加载实现类,实现了完全的解耦。

如何实现的,我们下一篇博文分解。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Postman文件上传接口测试

接口介绍 返回示例 测试步骤 1.添加一个新请求&#xff0c;修改请求名&#xff0c;填写URL&#xff0c;选择请求方式 2.将剩下的media参数放在请求body里&#xff0c;选择form-data&#xff0c;选择key右边的类型为file类型&#xff0c;就会出现选择文件的按钮Select Files&a…

继承 (上)【C++】

文章目录 继承的定义继承的语法继承权限和继承到子类后父类成员的访问限定符的变化继承到子类后父类成员的访问限定符的变化 子类继承到了父类的什么&#xff1f;继承中的作用域子类和父类之间的赋值转换子类对象可以直接赋值给父类对象&#xff0c;但是父类对象不能直接赋值给…

spring boot 接收第三方mq消息

文章目录 前言一、pom二、配置三、RabbitMQListener总结 前言 mq 用的很少&#xff0c;简单记录一下。 需求&#xff1a;数据提供方采用mq的方式推送数据&#xff0c;我方接收数据后&#xff0c;入库。 一、pom <dependency><groupId>org.springframework.boot&…

基于 Appium 的 App 爬取实战

除了运行 Appium 的基本条件外&#xff0c;还要一个日志输出库 安装&#xff1a; pip install loguru 思路分析 首先我们观察一下整个 app5 的交互流程&#xff0c;其首页分条显示了电影数据&#xff0c; 每个电影条目都包括封面&#xff0c;标题&#xff0c; 类别和评分 4…

Linux下Oracle 11g升级19c实录

1.组件信息 source /home/oracle/.bash_profile11g && sqlplus "/ as sysdba"<<EOF set line 200 col COMP_NAME for a40 select comp_name,VERSION,STATUS from dba_registry; exit; EOF COMP_NAME VERSION …

自动化之响应式Web设计:纯HTML和CSS的实现技巧

​ 大家好&#xff0c;我是程序员小羊&#xff01; 前言 响应式Web设计是一种使Web页面在各种设备和屏幕尺寸下都能良好显示的设计方法。随着移动设备的普及&#xff0c;响应式设计已经成为Web开发中的标准实践。本文将探讨如何使用纯HTML和CSS实现响应式Web设计&#xff0c;覆…

测试架构师领导力的原则

目录 一、建立信任关系 二、建立共识 三、通过关系带来安全 四、要身体力行&#xff0c;以身作则 五、适当处理风险&#xff0c;什么是鞭炮&#xff0c;什么是原子弹 测试架构师的领导力是建立在把握和执行的某些原则上---信任&#xff0c;认知&#xff0c;安全&#xff0…

Python 算法交易实验81 QTV200日常推进-重新实验SMA/EMA/RSI

说明 本次实验考虑两个点&#xff1a; 1 按照上一篇谈到的业务目标进行反推&#xff0c;有针对性的寻找策略2 worker增加计算的指标&#xff0c;重新计算之前的实验 内容 工具方面&#xff0c;感觉rabbitmq还是太慢了。看了下&#xff0c;rabbitmq主要还是面向可靠和灵活路…

【软件测试】软件系统测试方案(Word原件)

1. 引言 1.1. 编写目的 1.2. 项目背景 1.3. 读者对象 1.4. 参考资料 1.5. 术语与缩略语 2. 测试策略 2.1. 测试完成标准 2.2. 测试类型 2.2.1. 功能测试 2.2.2. 性能测试 2.2.3. 安全性与访问控制测试 2.3. 测试工具 3. 测试技术 4. 测试资源 4.1. 人员安排 4.2. 测试环境 4.2.…

Openstack 与 Ceph集群搭建(上): 规划与准备

文章目录 写在前面网络架构节点规划软件版本避坑指南 基础配置1. host配置2. 修改hostname名称3. 确保root账号能登录系统4. 配置NTP5. 配置免密登录 写在前面 近期将进行三节点的Openstack、Ceph集群混合部署&#xff0c;本人将详细记录该过程。在此之前&#xff0c;本文为Op…

逆向开发LabVIEW程序的操作与注意事项(无源代码)

1. 概述与准备工作 当手头没有源代码&#xff0c;只有LabVIEW编译后的可执行程序时&#xff0c;逆向开发的难度和复杂性大大增加。需要用到的工具、方法和策略也会有所不同。逆向工程的目标是在没有源代码的情况下重建或理解该程序的功能、结构和行为。涉及CameraLink通讯的程…

Android大脑--systemserver进程

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章&#xff0c;技术文章也可以有温度。 本文摘要 系统native进程的文章就先告一段落了&#xff0c;从这篇文章开始写Java层的文章&#xff0c;本文同样延续自述的方式来介绍systemserver进程&#xff0c;通过本文您将…

day34-nginx常用模块

## 0. 网络面试题 网络面试题: TCP三次握手 TCP四次挥手 DNS解析流程 OSI七层模型 抓包工具 tcpdump RAID级别区别 开机启动流程 如何实现不同的网段之间通信(路由器) ip route add 192.168.1.0 255.255.255.0 下一跳的地址或者接口 探测服务器开启了哪些端口(无法登录服务器…

嵌入式开发如何看芯片数据手册

不管什么芯片手册&#xff0c;它再怎么写得天花乱坠&#xff0c;本质也只是芯片的使用说明书而已。而说明书一个最显著的特点就是必须尽可能地使用通俗易懂的语句&#xff0c;向使用者交代清楚该产品的特点、功能以及使用方法。 以TMP423为例&#xff0c;这是一个测量温度的芯…

【密码学】密钥管理:①基本概念和密钥生成

密钥管理是处理密钥从产生到最终销毁的整个过程的有关问题&#xff0c;包括系统的初始化及密钥的产生、存储、备份与恢复、装入、分配、保护、更新、控制、丢失、撤销和销毁等内容。 一、密钥管理技术诞生的背景 随着计算机网络的普及和发展&#xff0c;数据传输和存储的安全问…

蓝牙音视频远程控制协议(AVRCP) command跟response介绍

零.声明 本专栏文章我们会以连载的方式持续更新&#xff0c;本专栏计划更新内容如下&#xff1a; 第一篇:蓝牙综合介绍 &#xff0c;主要介绍蓝牙的一些概念&#xff0c;产生背景&#xff0c;发展轨迹&#xff0c;市面蓝牙介绍&#xff0c;以及蓝牙开发板介绍。 第二篇:Trans…

智慧运维:数据中心可视化管理平台

图扑智慧运维数据中心可视化管理平台&#xff0c;实时监控与数据分析&#xff0c;优化资源分配&#xff0c;提升运维效率&#xff0c;确保数据中心的安全稳定运行。

Linux进程间通信——匿名管道

文章目录 进程间通信管道匿名管道匿名管道使用 进程间通信 进程设计的特点之一就是独立性&#xff0c;要避免其他东西影响自身的数据 但有时候我们需要共享数据或者传递信息&#xff0c;传统的父子进程也只能父进程传递给子进程信息 因此进程间通信还是很必要的&#xff0c;…

Apollo9.0 PNC源码学习之Planning模块—— Lattice规划(三):静态障碍物与动态障碍物ST图构建

参考文章: (1)Apollo6.0代码Lattice算法详解——Part4:计算障碍物ST/SL图 (2)自动驾驶规划理论与实践Lattice算法详解 1 计算障碍物ST/SL图 计算障碍物ST/SL图主要函数关系图: // 通过预测得到障碍物list auto ptr_prediction_querier = std::make_shared<Predict…

2024新型数字政府综合解决方案(五)

新型数字政府综合解决方案通过集成人工智能、大数据、区块链和云计算技术&#xff0c;打造了一个智能化、透明化和高效的政务服务平台&#xff0c;旨在提升政府服务的响应速度、处理效率和数据安全性。该方案实现了跨部门的数据共享与实时更新&#xff0c;通过智能化的流程自动…