如何在没有接口的情况下进行 RPC 调用?

简介: 本文介绍了RPC框架中“泛化调用”的实现原理与应用场景。针对测试平台、服务网关等无法依赖接口API的场景,通过统一的GenericService接口和动态代理机制,将接口名、方法名、参数等封装为请求消息,实现无需API的RPC调用。同时,结合专属序列化插件解决参数序列化问题,并支持异步调用,提升灵活性与性能。

在 RPC 运营的过程中,让调用端在没有接口 API 的情况下发起 RPC 调用的需求,不只是一个业务方和我提过,这里我列举两个非常典型的场景例子。
场景一:我们要搭建一个统一的测试平台,可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值,在线测试自己发布的 RPC 服务。这时我们就有一个问题要解决,我们搭建统一的测试平台实际上是作为各个 RPC 服务的调用端,而在 RPC 框架的使用中,调用端是需要依赖服务提供方提供的接口 API 的,而统一测试平台不可能依赖所有服务提供方的接口 API。我们不能因为每有一个新的服务发布,就去修改平台的代码以及重新上线。这时我们就需要让调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起 RPC 调用。

场景二:我们要搭建一个轻量级的服务网关,可以让各个业务方用 HTTP 的方式,通过服务网关调用其它服务。这时就有与场景一相同的问题,服务网关要作为所有 RPC 服务的调用端,是不能依赖所有服务提供方的接口 API 的,也需要调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起 RPC 调用。

这两个场景都是我们经常会碰到的,而让调用端在没有服务提供方提供接口 API 的情况下仍然可以发起 RPC 调用的功能,在 RPC 框架中也是非常有价值的。
怎么做?
RPC 框架要实现这个功能,我们可以使用泛化调用。那什么是泛化调用呢?我们带着这个问题,先学习下如何在没有接口的情况下进行 RPC 调用。
我们先回想下我在基础篇讲过的内容,通过前面的学习我们了解到,在 RPC 调用的过程中,调用端向服务端发起请求,首先要通过动态代理,正如 第 05 讲 中我说过的,动态代理可以帮助我们屏蔽 RPC 处理流程,真正地让我们发起远程调用就像调用本地一样。
那么在 RPC 调用的过程中,既然调用端是通过动态代理向服务端发起远程调用的,那么在调用端的程序中就一定要依赖服务提供方提供的接口 API,因为调用端是通过这个接口 API 自动生成动态代理的。那如果没有接口 API 呢?我们该如何让调用端仍然能够发起 RPC 调用呢?
所谓的 RPC 调用,本质上就是调用端向服务端发送一条请求消息,服务端接收并处理,之后向调用端发送一条响应消息,调用端处理完响应消息之后,一次 RPC 调用就完成了。那是不是说我们只要能够让调用端在没有服务提供方提供接口的情况下,仍然能够向服务端发送正确的请求消息,就能够解决这个问题了呢?
没错,只要调用端将服务端需要知道的信息,如接口名、业务分组名、方法名以及参数信息等封装成请求消息发送给服务端,服务端就能够解析并处理这条请求消息,这样问题就解决了。过程如下图所示:

现在我们已经清楚了解决问题的关键,但 RPC 的调用端向服务端发送消息是需要以动态代理作为入口的,我们现在得继续想办法让调用端发送我刚才讲过的那条请求消息。
我们可以定义一个统一的接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,而 GenericService 接口的 $invoke 方法的入参就是方法名以及参数信息。
这样我们传递给服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等都可以通过调用 GenericService 代理的 $invoke 方法来传递。具体的接口定义如下:
class GenericService {
Object $invoke(String methodName, String[] paramTypes, Object[] params);

}
这个通过统一的 GenericService 接口类生成的动态代理,来实现在没有接口的情况下进行 RPC 调用的功能,我们就称之为泛化调用。
通过泛化调用功能,我们可以解决在没有服务提供方提供接口 API 的情况下进行 RPC 调用,那么这个功能是否就完美了呢?
回顾下 第 17 讲 我过的内容,RPC 框架可以通过异步的方式提升吞吐量,还有如何实现全异步的 RPC 框架,其关键点就是 RPC 框架对 CompletableFuture 的支持,那么我们的泛化调用是否也可以支持异步呢?
当然可以。我们可以给 GenericService 接口再添加一个异步方法 $asyncInvoke,方法的返回值就是 CompletableFuture,GenericService 接口的具体定义如下:

class GenericService {
Object $invoke(String methodName, String[] paramTypes, Object[] params);
CompletableFuture $asyncInvoke(String methodName, String[] paramTypes, Object[] params);
}
学到这里相信你已经对泛化调用的功能有一定的了解了,那你有没有想过这样一个问题?在没有服务提供方提供接口 API 的情况下,我们可以用泛化调用的方式实现 RPC 调用,但是如果没有服务提供方提供接口 API,我们就没法得到入参以及返回值的 Class 类,也就不能对入参对象进行正常的序列化。这时我们会面临两个问题:
问题 1:调用端不能对入参对象进行正常的序列化,那调用端、服务端在接收到请求消息后,入参对象又该如何序列化与反序列化呢?
回想下 第 07 讲,在这一讲中我讲解了如何设计可扩展的 RPC 框架,我们通过插件体系来提高 RPC 框架的可扩展性,在 RPC 框架的整体架构中就包括了序列化插件,我们可以为泛化调用提供专属的序列化插件,通过这个插件,解决泛化调用中的序列化与反序列化问题。
问题 2:调用端的入参对象(params)与返回值应该是什么类型呢?
在服务提供方提供的接口 API 中,被调用的方法的入参类型是一个对象,那么使用泛化调用功能的调用端,可以使用 Map 类型的对象,之后通过泛化调用专属的序列化方式对这个 Map 对象进行序列化,服务端收到消息后,再通过泛化调用专属的序列化方式将其反序列成对象。
总结
今天我们主要讲解了如何在没有接口的情况下进行 RPC 调用,泛化调用的功能可以实现这一目的。
这个功能的实现原理,就是 RPC 框架提供统一的泛化调用接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,通过调用 GenericService 代理的 $invoke 方法将服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等封装成请求消息,发送给服务端,实现在没有接口的情况下进行 RPC 调用的功能。
而通过泛化调用的方式发起调用,由于调用端没有服务端提供方提供的接口 API,不能正常地进行序列化与反序列化,我们可以为泛化调用提供专属的序列化插件,来解决实际问题。
课后思考
在讲解泛化调用时,我讲到服务端在收到调用端通过泛化调用的方式发送过来的请求时,会使用泛化调用专属的序列化插件实现对其进行反序列化,那么服务端是如何判定这个请求消息是通过泛化调用的方式发送过来的消息呢?
笔者认为:可以在协议中设置序列化类型,在服务端接受到后,就知道用什么插件反序列化了

相关文章
|
18小时前
|
数据采集 安全 IDE
分布式环境下如何快速定位问题?
本文探讨了RPC在分布式环境下的问题定位难点及解决方案。由于服务间依赖复杂、跨团队协作成本高,传统日志排查效率低下。为此,提出两大方法:一是通过封装详细的异常信息,包含异常类型、IP、接口名等关键字段,助力快速溯源;二是引入分布式链路跟踪,利用TraceID和SpanID还原完整调用链,实现跨服务问题精准定位,显著降低沟通与排查成本。
|
18小时前
|
测试技术
动态分组:超高效实现秒级扩缩容
通过分组实现调用方流量隔离,保障核心业务稳定。但突发流量可能导致分组扩容困难,动态分组可通过修改注册中心数据快速调整实例归属,实现弹性扩缩容,提升系统应对能力。
|
18小时前
|
运维 数据格式
如何在线上环境里兼容多种 RPC 协议?
本讲探讨了如何在线上环境兼容多种RPC协议。由于历史原因,系统中常存在多类RPC框架,导致维护成本高。为实现平滑升级,可通过支持多协议共存,利用magic number识别协议类型,动态选择解析方式,实现新老协议无缝切换,降低升级复杂度,提升系统可维护性与扩展性。
|
18小时前
|
存储 缓存 安全
One Trick Per Day
初始化Map宜用Guava指定预期大小,避免扩容;禁用Executors创建线程池,防止OOM,推荐自定义或使用Guava;Arrays.asList返回不可变列表,禁止修改操作;遍历Map优先使用entrySet或forEach;SimpleDateFormat非线程安全,建议用ThreadLocal或Java8新时间类;并发更新记录需加锁,推荐乐观锁配合version字段。
|
18小时前
|
Java 应用服务中间件 网络安全
Eclipse运行SSM/SSH项目教程
本教程介绍如何在Eclipse中配置JDK与Tomcat,导入非Maven/Maven项目,绑定服务器并运行。包含SSH/SSM框架案例及配置视频讲解,常见问题如数据库连接修改等,助你快速搭建Java Web开发环境。(238字)
|
14小时前
|
Linux Go 虚拟化
01-Docker概述
Docker是基于Go语言的开源容器化平台,实现“一次镜像,处处运行”。它将应用及依赖打包到轻量级、可移植的容器中,直接利用宿主机内核,启动快、资源占用少,支持秒级启动、单机千级容器部署,显著优于传统虚拟机。核心组件包括镜像、容器和仓库,架构采用C/S模式,通过守护进程管理容器生命周期,提升开发、测试与部署效率。
|
15小时前
|
Ubuntu Shell Linux
04-Docker常用命令
Docker常用命令简介:涵盖启动、停止、重启、状态查看及开机自启设置;支持版本、信息查询与帮助文档查看;镜像管理包括列出、搜索、下载、删除及空间清理;支持命令自动补全配置,虚悬镜像处理,后台运行Linux容器及yum依赖下载等操作。
|
14小时前
|
存储 Ubuntu Shell
05-容器命令
使用`docker run`可创建并启动容器,支持交互式(-it)或守护式(-d)运行。通过-p映射端口,-v挂载数据卷,--name指定名称,-e设置环境变量。容器可启停、删除、拷贝文件,支持日志查看、进程监控及导出导入。数据卷实现持久化与共享,commit可将容器保存为新镜像。注意权限问题可加--privileged=true解决。(238字)
|
18小时前
|
存储
详解时钟轮在 RPC 中的应用
本文介绍了时钟轮机制及其在RPC框架中的应用。针对定时任务常见的线程过多和CPU浪费问题,时钟轮通过时间槽和分层结构,减少无效扫描,提升性能。广泛应用于请求超时、启动超时、定时心跳等场景,有效优化高并发下的资源消耗。
|
15小时前
|
网络协议 关系型数据库 MySQL
07-nexus搭建docker私仓
使用Nexus搭建Docker私有仓库,需启用Docker Realm、创建文件存储及docker-hosted仓库,配置HTTP/HTTPS端口(如8881),开放防火墙。客户端需配置insecure-registry信任私仓,登录后打标签并推送镜像。支持镜像导出导入与版本管理,实现安全高效的镜像存储与分发。