手写 RPC 框架 - 个人笔记+梳理+总结+扩展点实现
前言
跟着鱼哥又做完一个项目了。 相比传统的面向业务的项目,比如商城管理系统等。本次手写框架项目深刻学习体会了开发底层框架的思路。 并且接触了许多以前偏于写应用而未能接触到的知识点
再次做个记录总结,个别实现思路可能有些糙。
个人水平有限,多多指教!
★,°:.☆( ̄▽ ̄)/$:.°★ 。
- GitHub 代码仓库: https://github.com/Jools-hzx/jools-rpc
- 示意图汇总: 示意图大汇总
阶段 00 - 导学和入门
个人笔记: 原文链接
什么是 RPC?
本身并不是一种协议,而是一种调用
常用的 RPC 协议实现:
- gRPC
- thrift
核心
- 服务消费者
- 请求处理器:根据客户端的请求参数来进行不同的处理、调用不同的服务和方法
- 服务提供者
- 本地服务注册器:记录服务和对应实现类的映射。
- 序列化 / 反序列化器
- 注册中心: Etcd / Redis / ZooKeeper
- 负载均衡:选取提供者
- 容错机制:调用失败
- 替他:
- 服务提供者节点下线,删除失效节点
- 缓存拉取的服务信息
- 合适的网络框架,或者自定义协议头、节约传输体积
- 优化扩展性:SPI机制、配置优化
| 模块 | 描述 |
|---|---|
| 服务消费者 | 请求处理器:根据客户端的请求参数进行处理,调用不同的服务和方法。 |
| 服务提供者 | 本地服务注册器:记录服务与对应实现类的映射关系。 |
| 序列化 / 反序列化器 | 提供请求与响应对象的高效序列化和反序列化支持。 |
| 注册中心 | 支持 Etcd、Redis 和 ZooKeeper 用于服务注册与发现。 |
| 负载均衡 | 选取最佳服务提供者,实现多种负载均衡策略(如轮询、一致性哈希等)。 |
| 容错机制 | 在调用失败时提供容错策略,如 FailSafe、FailFast、FailOver 等。 |
| 其他功能 | - 服务提供者节点下线:删除失效节点,保持服务列表一致性。 |
| - 缓存服务信息:本地缓存拉取的服务信息,减少注册中心访问频率。 | |
| - 优化网络传输:通过自定义协议头减少传输体积,选择合适的网络框架。 | |
| - 优化扩展性:支持 SPI 机制扩展,结合配置文件实现灵活优化。 |
阶段 01 - 开发极简的 RPC 框架
阶段成果
- 搭建项目和模块:
exp-common: 示例代码的公共依赖,包括接口、Model 等exp-consumer: 示例服务消费者代码exp-provider: 示例服务提供者代码jools-rpc-basic: RPC框架 - 简易版
exp-common 模块
| 功能 | 核心组件 |
|---|---|
| 实体类 model | User 类,返回字段值 name |
| 服务接口 Service | UserService |
| 服务方法 | getUser() 返回 User |
exp-provider 模块
| 功能 | 核心组件 |
|---|---|
| 服务实现类 | UserServiceImpl |
| 实现服务方法 | getUser |
exp-consumer 模块
| 功能 | 核心组件 |
|---|---|
| 请求客户端 | BasicProviderExample |
| 请求发送方式 | 基于 JDK 动态代理,返回代理对象通过 HTTPRequest (Hutool 工具包) 发送请求 |
| 调用服务 | UserService |
| 调用服务方法 | getUser |
| 消费方代理方式选型 | 静态代理 与 动态代理 (区别 + 分别实现方式 + 优缺点) |
jools-rpc-basic 模块
| 功能 | 核心组件 |
|---|---|
| Web 服务器 | 使用 Vert.x,可选 Tomcat 或 Netty |
| 本地服务注册器 | LocalRegistry 基于 ConcurrentHashMap + key 为服务名称 + value 为服务实现类全类名 |
| 通信请求实体类 | RpcRequest 和 RpcResponse |
RpcRequest 请求消息体,支持序列化 | 请求服务名 serviceName方法名 methodName方法参数类型 paramTypes传入实参 params |
RpcResponse 响应消息体,支持序列化 | 响应数据 data响应数据类型 datatype响应信息 msg 异常信息 exception |
| 序列化器 | JdkSerializer,基于 JDK 原生序列化方式 |
| 请求处理器 | HttpServerHandler 借助序列化器反序列化 HTTP 请求,调用本地服务注册/序列化返回响应。 |
| 动态代理处理器 | ServiceProxyFactory返回 ServiceProxy实例,实现透明调用 |
示意图
阶段 02 - 全局配置加载
个人博客积累:稀土掘金 - 好用的解析配置工具类(Hutool+SnakeYAML)
阶段成果
- 支持基于
application.properties文件加载全局配置 - 支持区分
dev,prod多环境配置文件 (扩展)工具类 SnakeYAML 支持基于.yml / .yaml格式文件加载全局配置(扩展)支持监听配置文件变更,借助Hutool.autoLoad()(扩展)配置文件支持中文
简示图
- 橙色部分为本阶段新增内容
全局配置信息设计
参考 Dubbo 官方配置
ApplicationConfig中至少含有:
- 注册中心地址:服务提供者与消费者均需指定,用于服务注册和发现。
- 服务接口:提供者指定提供的,消费者指定调用的。
- 序列化方式:双方均需指定,用于网络数据传输的序列化与反序列化。
- 网络通信协议:双方选择合适的,如 TCP、HTTP 等。
- 超时设置:双方均需设置,用于调用服务超时处理。
- 负载均衡策略:消费者指定,决定调用哪个服务提供者实例。
- 服务端线程模型:提供者指定,决定处理客户端请求方式。
解析配置文件生成配置类实例借助官方文档: Hutool - Props 工具
设计实现 - RpcConfig
- name (String):服务名称,默认值
jools-rpc - version (String) : 版本, 默认值
1.0 - serverHost (String): 主机名称,默认值
localhost - serverPort (String): 服务端口, 默认值
8888
开发实现:扩展 rpc-core模块
- 在
utils包下创建工具类ConfigUtils,使用 Hutool 的Props工具加载配置。 - 在
constant包下创建接口RpcConstant,用于存储常用配置常量的默认值:- 默认配置项前缀为
DEFAULT_CONFIG_PREFIX = "rpc"。 - 默认配置文件格式为
PROP_CONFIG_SUFFIX = ".properties"。
- 默认配置项前缀为
- 方法参数支持通过
prefix字段配合-environment字段加载多环境配置。 - 使用 ⌈双检索单例模式⌋ 确保全局配置类的唯一性。
- 支持用户自定义
application.properties文件,若未提供则使用默认配置。默认配置的值由RpcConfig中各字段的默认值决定。
(扩展) - 支持不同格式 .yml / .yaml
参考文献: SnakeYaml 工具快速入门
导入依赖配置
▼xml复制代码<!-- Yaml配置类解析--> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>2.2</version> </dependency>
SnakeYaml 工具支持:
- 直接读取
.yml / .yaml配置转换为Map - 直接读取
.yml / .yaml配置并封装成指定类型RpcConfig - 支持基于前缀
key区分配置组,RpcConfig分配前缀rpc
扩展 **ConfigUtils**:
- 接口常量支持添加
YAML_CONFIG_SUFFIX用于辨识.yaml/.yml配置后缀 - 支持基于
.yaml / .yml格式和不同enivronment加载不同环境配置
加载配置规则:
- 若用户未添加配置文件,项目内不存在
.properties配置文件,加载默认值 - 若项目内存在
.properties配置文件,加载 - 若用户已配置但是配置了多个,优先加载
.properties - 若用户无
.properties但是存在.yml; 优先加载.yml - 若用户未配置
.properties但是配置了.yaml, 加载.yaml
(扩展) - 监听配置文件,支持自动更新
参考文档
官方说明
引用
应用Hutools 工具类中 loadConfig的同时完成监听
- 修改
ConfigUtils工具类 - autoLoad() 方法源码简单分析
简介:
Hutool 的 WatchMonitor 封装了 JDK 7 的 WatchService,用于监听文件和目录的变动(如创建、更新、删除)。你可以在 Watcher 中定义处理这些变动的逻辑。例如,你可以用 WatchMonitor 来监测配置文件的变化,并自动将其加载到内存中。
支持的监听机制:
- ENTRY_MODIFY (文件修改的事件)
- ENTRY_CREATE ()文件或目录创建的事件)
- ENTRY_DELETE (文件或目录删除的事件)
- OVERFLOW (丢失的事件)
- 添加测试方法,修改配置文件后再读取,查看是否修改成功
(扩展) 配置支持中文
默认 Hutool - Props 类支持的编码为 ISO-8859-1
修改 loadConfig 方法
- 指定编码类型为
StandardCharsets.UTF_8
阶段 03 - 接口 Mock
笔记原文:原文链接 - 接口 Mock
个人博客积累:哪些工具可以实现测试 Mock
示意图
- 橙色为本阶段的扩展内容
阶段成果
- 接口 Mock 的需求分析和设计
- 支持基于配置开启接口 Mock
- 基于 JDK + JavaFaker 支持多种数据类型返回默认值
扩展- 完善 Mock 机制,基于 JavaFaker 库;官方文档:GitHub - JavaFaker
简介
- Mock 机制简介
指模拟对象,通常用于测试代码中,特别是在单元测试中,便于泡桐业务流程
- 为什么要支持
Mock
开发者能轻松调用服务接口、跑通业务流程,无需依赖真实远程服务,提升使用体验。
开发实现
RpcConfig配置项新增mock (boolean 类型), 方便开发者快速开启
▼java复制代码public class RpcConfig { .... /** * 开启接口 mock, true 表示开启; false 表示关闭 */ private boolean mock = false; }
- 借助动态代理,返回
mock代理服务MockServiceProxy,针对指定返回类型返回模拟数据 - 服务代理工厂
ServiceProxyFactory支持返回 mock 代理服务实体
▼java复制代码@SuppressWarnings("all") public static <T> T getMockProxy(Class<T> mockClass) { return (T) Proxy.newProxyInstance( mockClass.getClassLoader(), new Class[]{mockClass}, new MockServiceProxy() ); }
(扩展) - 完善 Mock 机制
基于 JavaFaker 支持 mock 更多种数据类型。
参考文档
▼xml复制代码<dependency> <groupId>com.github.javafaker</groupId> <artifactId>javafaker</artifactId> <version>1.0.2</version> </dependency>
快速入门 - 测试方法
- 测试
internet()相关 API, 模拟域名 + IP - 测试
bothify()方法,?支持替换为随机字母;#支持替换为随机数字 - 测试
address()方法,返回模拟地址数据
新增支持 Mock 的数据类型
- 增加 RpcRequest / RpcResponse
- 增加 RpcConfig
- 增加 HttpServer
阶段 04 - 序列化器与 SPI 机制
个人笔记原文:序列化器与 SPI 机制
个人博客积累:
- 设计模式 - 单例 (稀土掘金)
- 工具[序列化解析] - 序列化器实现方式 (支持 JSON、Hession、Kryo、ProtoBuf)
- 序列化难题何解?Java 中的支持 JSON、Hessian、Kryo、Protobuf 序列化器实现和应用探究 Jav - 掘金
阶段成果
- 支持多种序列化器实现方式
JDK + JSON + Kryo + Hessian实现序列化器 扩展- 实现Protobuf序列化器,支持RpcConfig默认配置和 SPI 配置优化- 基于静态内部类方法实现懒汉式 - 单例模式创建序列化工厂优化- 基于双重检验锁校验机制实现懒汉式 - 单例模式创建序列化工厂
简示图
序列化器实现方式比较
| 序列化器 | 优点 | 缺点 |
|---|---|---|
| 原生 Java Serializable | 1. 简单易用,便于Java应用中的对象持久化。 2. 兼容性好,与Java语言及框架无缝集成,操作流畅。 | 1. 性能差。 2. 调试困难。 3. 无版本控制,类结构调整易引发反序列化问题或数据不一致,而 Protobuf 具有良好的版本兼容性。 |
| JSON | 1. 易读性好,可读性强,便于人类理解和调试。 2. 跨语言支持广泛,几乎所有编程语言都有 JSON 的解析和生成库。 | 1. 序列化后的数据量较大,因为 JSON 以文本格式存储,需要额外的字符来表示键、值和结构。 2. JSON 在处理复杂数据结构和循环引用时能力较弱,可能导致性能下降或序列化失败。 |
| Hessian | 1. 二进制序列化数据量小,传输效率高。 2. 支持跨语言,适合分布式系统服务调用。 | 1. 性能较JSON略低,因为需要将对象转换为二进制格式。 2. 对象必须实现Serializable接口,限制了可序列化的对象范围。 |
| Kryo | 1. 高性能,序列化和反序列化速度快。 2. 支持循环引用和自定义序列化器,适用于复杂的对象结构。 3. 无需实现Serializable接口,可以序列化任意对象。 | 1. 不跨语言,只适用于Java。 2. 对象的序列化格式不够友好,不易读懂和调试。 |
| Protobuf | 1. 高效的二进制序列化,序列化后的数据量极小。 2. 跨语言支持,并且提供了多种语言的实现库。 3. 支持版本化和向前/向后兼容性。 | 1. 配置相对复杂,需要先定义数据结构的消息格式。 2. 对象的序列化格式不易读懂,不便于调试。 |
参考 Dubbo 配置序列化器的方式
SPI 机制
SPI (Service Provider Interface) 是 Java 中的一种机制,用于支持模块化开发和插件扩展。它允许服务提供者文件通过配置文件注册实现,系统通过反射动态加载这些实现。不修改原有代码的情况下实现解耦和增强可扩展性
实现
之前在简易版 RPC 框架内已经实现了基于 JDK 的序列化器
新建包 com.jools.joolsrpc.serializer 存放所有序列化器相关
- 实现
JsonSerializer, 基于jackson-databind - 实现
KryoSerializer,基于kryo和ThreadLocal保证每个线程有一个单独的Kryo对象实例 - 实现
HessianSerializer, 基于hessian版本4.0.66 - 接口
SerializerKeys列举所有支持的序列化器 Key
序列化器工厂实现方案一:
- 简单工厂,基于
Map存储SerializerKeys->Serializer的映射关系,默认使用 JDK。支持基于 key 查询Map返回相应的序列化器实例 - 扩展
RpcConfig, 支持配置指定序列化器
▼java复制代码public class RpcConfig { .... private String serializer = SerializerKeys.JDK; }
- 优化提供者基于工厂和配置类获取指定序列化器
▼java复制代码//动态基于 RpcConfig 配置获取序列化器 final Serializer serializer = SerializerFactory.getInstance(RpcApplication.getRpcConfig().getSerializer());
序列化器工厂实现方案二:
- 自定义 SPI 机制的扫描路径
/resources/META-INF/rpc/ - 分为
cutom子目录(用户自定义)和system子目录(配置系统自带设置) system子目录下添加SerializerKeys和该序列化器实现类全类名
▼properties复制代码jdk=com.jools.joolsrpc.serializer.impl.JdkSerializer hessian=com.jools.joolsrpc.serializer.impl.HessianSerializer json=com.jools.joolsrpc.serializer.impl.JsonSerializer kryo=com.jools.joolsrpc.serializer.impl.KryoSerializer
- 编写
SpiLoader加载器,扫描并转载自定义 SPI 机制目录下的所有配置 - 关键属性如下:
Map<String, Map<String, Class<?>>> loaderMap:接口名 -> { 配置键名[例如 SerialzerKeys 内配置] , 实现类全类名 }instanceCache: 对象实例缓存, 存储全类名->对象实例SPI_RPC_SYSTEM_DIR: 默认读取的系统目录META-INF/rpc/systemSPI_RPC_CUSTOM_DIR:默认读取的用户自定义目录META-INF/rpc/customloadClassesList: 动态加载类的列表 目前只有: Serializer.classSPI_LOAD_DIR:需要扫描的所有目录,注意顺序先扫描默认,再扫描自定义{SPI_RPC_SYSTEM_DIR, SPI_RPC_CUSTOM_DIR}
- 关键方法包含:
getInstance(Class<?> cls, String key): 根据接口类名获取其支持的所有 key 标识,再通过这些标识找到对应的实现类全名。然后使用反射创建实例,并通过instanceCache缓存来确保单例,从而提高系统吞吐量。load(Class<?> loadClass):基于接口名,扫描所有 SPI 目录,读取文件内容, 同时将其加入到loaderMap
SerializerFactory初始化时,通过SpiLoader的 load 方法加载所有序列化器实现类。之后,使用 getInstance 方法获取特定的序列化器实例。- 测试:
- 非法序列化器实现类全类名会导致反射生成实例失败
- 测试,获取非法
SerializerKeys例如aa, 会获取失败 - 测试配置相同的key, 若实现类配置得不同,自定义配置会覆盖系统配置
- 测试支持基于
.properties/.yaml/.yml配置可切换序列化器
(扩展) - 实现更多不同协议的序列化器 protobuf
参考文档
依赖
▼xml复制代码<dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.21.12</version> </dependency>
实现步骤:
- 安装
ProtoBuf编译器 - 安装
IDEA插件支持,插件名称Protobuf Generator - 配置
IDEA快速编译GenProtobuf- Tools->Configure GenProtobuf
- Protoc Path:安装 protoc 编译器的路径
- 构建
Java, 生成 Protobuf 的路径
- 实现
Serializer接口,支持基于Protobuf的序列化器,测试 - 扩展序列化器常量
SerializerKeys,添加PROTOBUF选项 \META-INF\rpc\system目录下新增protobuf=实现全类名- 测试 - 注册新序列化器
- 测试 - 切换序列化器
(扩展) - 序列化工厂修改为懒汉式单例
实现方式:
- 基于静态内部类实现
- 原理
当外部类加载时,并不会立即加载静态内部类。只有在外部类访问静态内部类的方法或成员时,静态内部类才会被加载。
在使用静态内部类实现单例模式时,单例对象是静态内部类的一个静态成员。当外部类第一次调用获取单例对象的方法时,静态内部类会被加载,并创建单例对象。这样就实现了延迟加载,即在需要时才创建单例对象。。
- 保证线程安全
静态内部类的延迟加载有助于保证线程安全,并且单例对象仅在需要时才创建,从而降低了多线程环境下的竞争条件风险。
▼java复制代码@Slf4j public class SerializerFactory { private static class SerializerFactoryHolder { private static final SerializerFactory SERIALIZER_FACTORY = new SerializerFactory(); } public static SerializerFactory getInstance() { return SerializerFactoryHolder.SERIALIZER_FACTORY; } .... }
- 测试
(扩展) - 修改 SpiLoader 用懒加载获取实例
实现方式:
- 基于双重校验锁机制实现
▼java复制代码public class SpiLoader { private SpiLoader() { log.info("Enter SpiLoader Class `private` Constructor...."); } public static SpiLoader getSpiLoaderInstance() { if (spiLoaderInstance == null) { synchronized (SpiLoader.class) { if (spiLoaderInstance == null) { spiLoaderInstance = new SpiLoader(); } } } return spiLoaderInstance; } }
阶段 05 - 实现注册中心
个人笔记原文:注册中心实现
个人博客积累:
- 工厂模式
特征、实现方法- 简单工厂 简单工厂 Simple Factory
- 工厂模式 工厂模式 Factory
- 抽象工厂模式 抽象工厂
- Etcd
常用操作 + 需求场景 (注册、监听、上线、下线)Etcd
参考文献:
Carson带你学设计模式:简单工厂模式(SimpleFactoryPattern)
品设计模式 - (创建型) 简单工厂模式 Simple Factory
《图解设计模式》 - Factory / Abstract Factory
Carson带你学设计模式:抽象工厂模式(Abstract Factory)
【创建型模式三】抽象工厂(Abstract Factory)
阶段成果
- 实现基于 Etcd 的注册中心,借助租约 (Lease)、监听 (Watch) 特性实现注册中心核心能力
简示图
新增类 - UML 图梳理
注册中心相关:
- 通过
SpiLoader机制加载注册中心配置 - 基于
RpcConfig中的RegistryConfig获取到对应的RegistryKeys内配置的注册中心类型 Registry注册中心具体实现类EtcdRegistry完成服务注册、续期、监听机制- 服务注册信息借助
ServiceMetaInfo进行封装
注册中心工作示意图
实现
Etcd 入门
GitHub 仓库: Etcd仓库
官方文档: Etcd-Doc
可视化工具: etcdkeeper
Etcd Java 客户端:主流 Java Etcd 客户端 - Jetcd
步骤
- 注册中心选型:Etcd;
Go 语言实现的、开源的、分布式 的 键值存储系统,它主要用于分布式系统中的服务发现、配置管理和分布式锁场景
- **注册中心核心能力:**数据分布式存储 + 服务注册 + 服务发现 + 心跳检测 + 服务注销 + 扩展(注册中心容错、服务消费者缓存)
- Etcd 快速入门 + 核心数据结构 + 特性 + Raft 一致性算法
- Etcd 安装
版本: 3.5.16+ 启动 + 命令行基本操作 (put + get + del) - Etcd 可视化工具安装 -
EtcdKeeper - Etcd Java 客户端安装
Jetcd+Jetcd快速入门 - Jetcd 内常用客户端梳理, 基于
io.etcd.jetcd.Client获取
| 客户端名称 | 作用 |
|---|---|
| KVClient | 操作键值对:设置值、获取值、删除值、列出目录。 |
| LeaseClient | 管理租约:创建、续约、撤销租约,为键值对分配生存时间,自动删除过期键值对。 |
| WatchClient | 监视键变化:实时监听键的变化并接收通知。 |
| ClusterClient | 管理集群:添加/移除成员,获取健康状态和成员列表,执行选举操作。 |
| AuthClient | 身份验证:管理用户、角色等权限信息,授予或撤销权限。 |
| MaintenanceClient | 维护操作:健康检查、数据备份、快照、压缩、成员维护等。 |
| LockClient | 分布式锁:创建、获取、释放锁,实现并发控制。 |
| ElectionClient | 分布式选举:创建选举、提交选票、监视选举结果。 |
存储结构设计
要点: 一个服务可能有多个服务提供者(负载均衡)
- 层级结构 (比如 Etcd)
- 列表结构 (比如:Redis 中的 List 数据结构)
代码实现
- 定义服务注册信息类
ServiceMetaInfo封装注册信息包括:服务名称 + 服务版本号 + 服务地址 + 服务分组 - 支持获取服务注册键名 - 格式:
serviceName:serviceVersion - 支持获取服务注册节点键名 - 格式:
serviceName:serviceVersion:IP:Port方便注册 - 扩充
RpcConstant和RpcRequest新增服务版本号字段serviceVersion - 新增注册中心配置类
RegistryConfig, 维护注册中心配置 - 支持全局配置
RpcConfig持有RegistryConfig实例;默认实现为Etcd - 新增注册中心接口
Registry,定义核心功能方法- 初始化
init(RegistryConfig registryConfig) - 注册服务:基于服务注册信息
ServiceMetaInfo构建注册节点信息/rpc/serviceName:serviceVersion/serviceHost:servicePort - 注销服务
unRegistry(ServiceMetaInfo serviceMetaInfo) - 服务发现
serviceDiscovery(String serviceKey)(获取服务节点列表):基于服务注册信息构建查询服务的 key/rpc/serviceName:serviceVersion - 服务销毁
destory()
- 初始化
- 基于 Etcd 实现注册中心接口
EtcdRegistry - 新增注册中心常量
RegistryKeys类,key标记注册中心类型ETCD="etcd", 默认支持etcd - 实现基于自定义 SPI 机制配合 SpiLoador 实现的简单工厂模式的
RegistryFactory - 自定义 SPI 资源目录下新增关于注册中心的实现, 默认
etcd - 扩充代理类的实现逻辑,通过配置
RpcConfig获取RegistryConfig实例构建 HTTP 向注册中心发送服务发现请求; 向服务发现结果发送请求并响应结果 - 测试 - Provider 注册服务 + 启动消费者实现 RPC 请求调用 + 成功相应结果
流程梳理
阶段 06 - 注册中心优化
个人笔记:注册中心优化
个人博客积累:
- Hutool -
CronUtil.schedule()实现心跳检测 Hutool - CronUitl - JVM 虚拟机的
ShutdownHookJVM安全/异常退出机制 - Zookeeper 入门 - 常用操作 (注册、上线、下线、监听)
- ZooKeeper 概念简介入门 ZooKeeper基础概念
- ZooKeeper 基本操作指令
- Java操纵 ZooKeeper
- 了解学习观察者模式 个人博客 - 行为型观察者模式
- 了解学习策略模式个人博客 - 行为型策略模式
- 复习
Redis: Redis基础-数据结构
阶段成果
- Etcd 注册中心实现心跳检测和续期机制
- 实现服务节点下线后清除注册信息缓存
- 添加注册中心服务信息缓存机制
- 实现基于 ZooKeeper 的注册中心
- (优化) 完善注册信息,扩展更多字段,增加:
registerTime节点注册时间startTime节点启动时间protocol服务通信协议,比如可扩展:HTTP、HTTPS、gRPC 等- serviceWeight 服务权重,用于后期实现权重轮询
- metadata 自定义元数据,支持未来扩展
- (优化) 实现支持 Redis 作为注册中心
- (优化) 构建 Etcd 集群
- (优化) 采用策略模式实现 key 监听
- (优化) 增加消费者端缓存,实现服务注册信息失效兜底策略
示意图
优化 Etcd 作为注册中心实现
实现心跳续期
思路:
- Provider 向 Etcd 注册的时候,设置
TTL,过期之后自动删除该服务信息 - Provider 定时请求
Etcd续签自己的注册信息,更新TTL
实现:
- Registry 新增
heartBeat心跳检测 - 借助 Hutool 工具类中的
CronUtil实现定时任务, 对所有查询到的注册节点信息进行重新注册
实现服务节点下线
下线方案分类
- 主动下线:服务提供者项目正常退出时,从注册中心移除注册信息。
- 被动下线:服务提供者项目异常退出,利用 Etcd 的 key 过期机制自动移除。
实现:
- 借助JVM 的 ShutdownHook : Java 虚拟机提供的机制,可让开发者在 JVM 即将关闭时执行清理工作或必要操作,如关闭数据库连接、释放资源、保存临时数据。
- 在 Registry 实例启动时
init方法内创建并注册Shutdown Hook, 实现 JVM 退出的时候主动下线
实现服务注册信息缓存
**思路:**多服务,需要基于本地 JVM Map集合实现;其中 serviceKey 作为 key;查询到的 List<ServiceMetaInfo> 作为 value
实现:
- 实现操作缓存的方法,包括: 写缓存
writeCache、读缓存readCache、清空缓存clear - 在 registry 包下新增缓存类
RegistryServiceCache - 注册中心实现
EtcdRegistry添加RegistryServiceCache字段 - 修改服务发现
serviceDiscovery方法:先读缓存,后查询更新缓存
实现监听机制
当服务注册信息发生变更的时候,需要即时更新消费端缓存。
实现:
EtcdRegistry注册中心实现类实现watch(String serviceKey)中,新建监听key的集合;可以使用ConcurrentHashSet防止并发冲突EtcdRegistry内借助 Jetcd 的watchClient监听WatchEvent- 当
Event为 DELETE 则实现清除服务缓存
问题与解决
报错 - 删除缓存失败
- 修复 - 键名添加到缓存时没有携带
/rpc/前缀watch基于 ServiceNodeKey (serviceKey/ip:port)
▼java复制代码//解析服务名称 List<ServiceMetaInfo> serviceMetaInfos = keyValueList .stream() .map((kv) -> { //监听key String key = kv.getKey().toString(StandardCharsets.UTF_8); //监控 serviceNodeKey watch(key); String value = kv.getValue().toString(StandardCharsets.UTF_8); //映射成 ServiceMetaInfo return JSONUtil.toBean(value, ServiceMetaInfo.class); }).collect(Collectors.toList());
- 监听机制清除注册服务缓存时要基于
serviceKey(/rpc/全类名:version) 作为 key 清除 - 清空缓存时需要添加
ETCD_ROOT_PATH(/rpc/)前缀,作为缓存的 key
实现 ZooKeeper 作为注册中心
参考文档
Java 操作 ZooKeeper 的客户端 Curator
参考文档
- 基于 Curator 实现注册中心的核心功能: 服务发现
serviceDiscovery+ 监听watch+ 注册registry+ 下线unRegistry+ 销毁destory - 服务注册之前, 需要借助
createServiceInstance()建造者模式将ServiceMetaInfo封装为ServiceInstance Zookeeper也实现服务注册信息缓存- 扩充 SPI 资源目录内容,新增关于
ZooKeeper的内容 - 测试支持基于
RpcConfig配置rpc.registryConfig.registryType项灵活切换注册中心
(扩展) - 服务注册信息ServiceMetaInfo添加更多字段
增加:
- registerTime 节点注册时间
- startTime 节点启动时间
- protocol 服务协议 - 明确定义服务的通信协议(如 HTTP、HTTPS、GRPC、Dubbo 等)
- serviceWeight 服务权重 - 权重字段
权重可选 0, 1, 2;后期用于负载均衡 - metadata 自定义元数据 - 支持未来扩展
实现
- 添加时间工具类
DateUtils - 在
Provider注册服务构建ServiceMetaInfo的时候设置通信协议 (默认 HTTP);注册时添加注册时间 - 基于简单工厂
RequestSender,实现支持基于protocol字段切换请求发送者;当前先实现基于 HTTP - 修改构建代理实例的 ServiceProxy, 基于请求得到的
serviceMetaInfo内的protocol字段调用相应的RequestSender
(扩展) - 实现 Redis 作为注册中心
实现步骤:
- 实现 Registry 接口,基于 Jedis 实现
RedisRegistry init()方法:基于ip + port实例化一个JedisheartBeat(): 借助 Hutool CronUtil; 支持秒级单位registry(): 基于SETNX指令实现注册,默认设置过期时间 TTL 为 30sserviceDiscovery(): 基于SCAN迭代返回基于服务名前缀查询到的所有服务节点信息;再基于GET操作基于查询详细的serviceMetaInfounRegistry(): 删除本地服务节点注册信息和缓存destory(): 清空本地服务节点注册信息和缓存 + 停止心跳检测任务CronUtil.stop()watch(): 独立线程进行订阅, 基于psubscribe; 需要打开 Redis 事件监听配置config set notify-keyspace-events Ex- 扩充 SPI 资源目录内容,新增支持
Redis作为服务注册中心; - 测试支持基于 RpcConfig 配置项基于
rpc.registryConfig.registryType灵活切换为 Redis 作注册中心
(扩展) - 搭建 Etcd 集群
参考文档
Etcd 官方 - How to Set Up a Demo Etcd Cluster
(扩展) 策略模式实现注册中心 key 监听
- 定义
WatchStrategy接口持有watch(String serviceNodeKey) - 分别实现基于 Etcd、ZooKeeper、Redis 的监听机制
(扩展) 服务注册信息失效(过期)兜底策略 - 建立消费端缓存服务信息
实现
- 新建
ConsumerServiceCache类,持有 List 集合,消费端缓存兜底的注册信息ServiceMetaInfo - 读缓存逻辑:当
serviceDiscovery()查询信息为空则尝试读消费端缓存的服务信息。如果缓存为空,返回默认服务节点信息 - 写缓存逻辑:如果
serviceDiscovery()查询不为空,则更新缓存;
扩展后示意图
可扩展点
- 实现支持更多协议的请求
HTTPS + gRPC + UDP + Dubbo等 (后续已经实现基于 TCP) - 处理逻辑 —— 如果服务注册信信息携带元数据
MetaData - 尝试搭建
ZooKeeper \ Redis集群 - 服务注册信息兜底 —— 添加更完备的兜底服务 [比如真实的 IP + port]
阶段 07 - 自定义协议
个人笔记原文:自定义协议
博客积累:
- 装饰器模式 结构型 - 装饰器模式
阶段成果
- 目标,用更少的空间传递必要的信息。基于 TCP 的更高效、简洁且灵活的RPC框架。
- 定义自定义的RPC消息体结构
ProtocolMessage。 - 开发针对该自定义消息体结构的编码器和解码器。
- 将请求处理器升级为支持TCP加自定义消息体,并通过编码/解码器来处理消息收发与解析。
- 采用Vert.x的RecordParser解决TCP传输中的半包或粘包问题。
- 利用装饰器模式实现(TcpBufferHandlerWrapper)增强客户端和服务端的消息处理能力。
- (扩展) - 优化 RequestSenderFactory,支持基于 SPI 机制加载相应协议的发射器,支持自定义扩展
参考文献
示意图
解码器 + 编码器 工作流程
自定义消息体 ProtocolMessage 类图
借助 Lombok 的 @Builder注解
消息体结构设计
- 网络传输设计
- 分析当前使用 HTTP 的劣势
- 追求性能和自定义空间,转用 TCP
- 消息结构设计
- 消息头信息
| 字段 | 数据类型 | 长度 | 作用 |
|---|---|---|---|
| messageId (唯一请求标识) | long | 64 bit / 8 个字节 | 唯一标识来追踪请求 |
| magic (魔数) | byte | 8 bit / 1 个字节 | 魔数;安全 |
| version (版本号) | byte | 8 bit / 1 个字节 | 保证请求和响应的一致性 |
| serializerType (序列化方式) | byte | 8 bit / 1 个字节 | 告诉服务端和客户端如何解析数据 |
| messageType (消息类型) | byte | 8 bit / 1 个字节 | 标识请求还是响应 |
| messageState (消息状态) | byte | 8 bit / 1 个字节 | 如果是响应,携带响应码,比如 200 |
| bodySize (请求体长度) | int | 32 bit / 4 个字节 | 标识 body 部分的长度;用于完整地获取到 body 内容信息 |
- 消息体
| 字段 | 数据类型 | 长度 |
|---|---|---|
| body | 不限制 (T) | 不限制 |
设计消息体结构图示
参考对比 Dubbo Protocol
- 解决: TCP 本身存在的 半包 / 粘包问题
- 消息头中新增字段,标记
请求体数据长度;保证能够完整地获取body内容信息 - 而消息头是固定长度的:17 个字节 (见上文中的消息结构设计)
- 因此可以通过消息头获取到应该截取的请求体长度
- 消息头中新增字段,标记
- 设计优势:不需要基于
k - v形式携带消息,不需要借助字符串作为建,而是直接按照字节截取(比如前 8 bit) 就能够获取到
开发实现
实现自定义消息体 + 协议
- 定义自定义TCP协议消息体
ProtocolMessage,包括消息头及其字段。 - 创建协议常量
ProtocolConstant,提供默认的消息字段值。 - 构建消息状态枚举
ProtocolMessageStateEnum,解析消息体内携带的messageState涵盖请求成功2xx、请求失败4xx和响应失败5xx等状态。 - 设计消息类型枚举
ProtocolMessageTypeEnum, 解析消息体携带的messageType涵盖请求 Request, Response, Heartbeat等,其中键为 Byte 类型,值为字符串。 - 设计消息序列化类型枚举
ProtocolSerializerTypeEnum,用于解析消息体携带的serializerType字节区间,其键 Byte 类型,值为字符串。 - 基于 Vert.x 框架实现 TCP 服务端
VertxTcpServer与客户端处理器VertxTcpClient。
实现基于自定义消息题的编码 / 解码器
工作示意图
- 实现消息编码器
ProtocolMessageEncoder:基于传入的ProtocolMessage实例构建字节数组的 ⌈ 消息头 + 消息体 ⌋,并根据序列化类型选择合适的序列化器进行数据序列化。 - 实现消息解码器
ProtocolMessageDecoder:依据协议定义的 17 字节长度解析消息头,注意解析顺序需要和构造顺序一致, 获取到消息体的大小解析主体内容。目前仅支持REQUEST + RESPONSE类型 - 基于 Vert.x 构建TCP请求处理器
TcpServerHandler,其功能包括:- 接收并使用
ProtocolMessageDecoder解码请求以获得RpcRequest对象,进一步获取请求的服务类名serviceName、方法名methodName等信息。 - 根据解析的服务类名
serviceName结果查找服务注册表并通过反射调用对应方法。 - 将响应结果封装成
RpcResponse并通过ProtocolMessageEncoder编码后返回给客户端。
- 接收并使用
- 修改消费者端的
ServiceProxy,支持基于 TCP 传输和解码编码 - 扩展通信协议选项,增加对TCP的支持;引入简单工厂模式以便根据协议类型(如
HTTP或TCP)获取对应的请求发送器 (HttpRequestSender或TcpRequestSender)。
解决 TCP 存在的半包粘包问题
思路:在消息头中设置请求体的长度,基于规定的长度截取消息头;在根据消息头内的长度截取消息头
- 借助
RecordParser+ 装饰器模式 RecordParser先完整获取前 17 Byte 长度的消息头结构- 再根据请求头的解析到的消息体长度
bodySize长度更改RecordParser的固定长度
阶段 08 - 负载均衡
个人笔记原文
参考文章
阶段成果
- 了解负载均衡:介绍 + 目的 + 常用的负载均衡技术
- 实现负载均衡算法:
轮询+随机+一致性 Hash+加权轮询负载均衡器
示意图
核心部件简示图
简示图
类图 - 负载均衡策略类图
代码实现
- 入门了解一致性哈希负载均衡:原理、特性及优势。
- 开发基础负载均衡器:基于
AtomicInteger实现轮询Round;基于Random实现随机Random策略。 - 实现一致性哈希负载均衡器
ConsistentHash:- 使用TreeMap存储节点。
- 节点选择规则:优先选取大于或等于请求哈希值的最近节点;若无,则返回环首节点。
- 定义
LoadBalancerKeys接口,管理负载均衡类型常量,并通过工厂模式支持SPI机制。 - 在自定义SPI资源中配置负载均衡选项,允许用户/开发者修改
RpcConfig文件来切换不同的负载均衡策略。 - 更新消费端
ServiceProxy代码以利用负载均衡器获取服务节点。 - 设置多个具有不同端口的服务提供者(Provider),并启动这些服务以测试负载均衡效果。
(扩展) - 优化一致性负载均衡器;基于 Guava 的 MurmurHash()实现
参考文献: Java 基于 TreeMap 实现一致性 Hash
选择 Guava包下的 MurmurHash()算法
- 针对每个服务节点
hash( IP + Port ) - 这意味着相同的请求会被路由到同一个虚拟节点(从而映射到同一个真实服务节点)
(扩展) - 实现 加权轮询 负载均衡算法
参考文献:各种负载均衡算法实现
**实现: **
- 目标:实现加权轮询方法
RoundWeight - 基于之前在
ServiceMetaInfo内扩充的ServiceWeight字段;默认权重为 1 - 新增
ServiceWeight接口存储权重常量 - 权重计算逻辑:
- 计算所有服务节点的权重总和
totalWeight- 每次选取服务节点之前,遍历各个节点,选取最大权重
- 之后更新被选中的节点为
currentWeight - totalWeight- 每次请求之后所有节点的权重累加上自身权重,但是总权重不变
- 这样可以防止权重高的节点连续多次被选中,为其他节点留出机会。
- 实现
LoadBalancer接口,基于权重计算逻辑实现加权轮询负载均衡器 - 每次负载均衡选取完服务节点后,需要更新各个服务节点的权重,借助
RegistryServiceUpdaterRegistryServiceUpdater借助 ⌈线程池 + CompletableFuture⌋ 实现服务节点的重新注册- 测试算法以及每轮的权重更新
阶段 09 - 重试机制
个人笔记原文:重试机制
参考文章:
参考文章 - 使用 guava-retrying 实现灵活的重试机制
阶段成果
- 了解重试机制: 重试机制触发条件 + 重试时间 + 停止重试策略 + 重试工作
- 了解常用的重试时间策略:
- 固定重试间隔 (Fix Retry Interval)
- 递增避退重试 (Increment Wait)
- 指数退避重试 (Exponential Backoff Retry)
- 随机延迟重试 (Random Delay Retry)
- 可变延迟重试 (Variable Delay Retry)
- 了解 Google 的
Guava - Retrying工具 - 代码实现: 不重试策略 + 固定时间间隔重试策略
- 优化:基于工厂模式 + SPI 机制,支持基于配置灵活切换重试策略
- (扩展)- 支持实现 ⌈间隔递增避退⌋ + ⌈间隔指数递增避退⌋ + ⌈间隔随机避退⌋
示意图
核心部件简示图

实现
快速入门 Guava - Retrying
RetryBuilder方法介绍:Retry + WaitStrategy + BlockStrategy + StopStrategy + AttemptTimeLimiter + RetryListener- 重试条件 -
Retry
根据执行结果 + 根据异常发生
- 等待策略 -
WaitStrategy
固定时长 + 随机等待时长 + 递增等待时长 + 指数增长时长 + 斐波那契递增等待时长 + 异常等待时长 + 组合复合时长
- 阻塞策略 -
ThreadSleepStrategy - 停止策略 - StopStrategy
永不停止 NeverStopStrategy + 指定最多重试次数 StopAfterAttemptStrategy + 指定最长重试时间 StopAfterDelaysStrategy
- 超时限制 -
AttemptTimeLimiter
不限制执行时间:NoAttemptTimeLimit
限制执行时间为固定值:FixedAttemptTimeLimit
- 重试监听器 -
RetryListener观察者模式,可以注册监听器
基于上述工具实现重试
- 开发实现 - 不重试策略:直接执行
call方法 - 开发实现 - 固定间隔重试:重试条件(异常) + 等待策略(固定间隔)+停止策略(超过最大尝试次数后停止)+ 重试监听 (输出当前重试次数)
- 新建配置重试常量
RetryStrategyKeys - 新建重试策略工厂类
RetryStrategyFactory支持通过 SPI 机制加载 - RpcConfig 内新增配置项
retryStrategy支持用户/开发者基于配置切换
(扩展) - 实现递增避退重试策略
基于 Retrying 工具集内的
- 等待策略
WaitStrategy设置为 递增等待规则incrementingWait - 停止策略
StopStrategy设置重试总数为: 4
规则设计
| 起始间隔 | 3s |
|---|---|
| 递增间隔 | 3s |
| 重试停止策略 | 重试 3 次之后 |
(扩展) - 实现指数避退重试策略
基于 Retrying 工具集内的
- 等待策略
WaitStrategy设置为 递增等待规则exponentialWait - 停止策略
StopStrategy设置重试总数为: 4
**规则设计 **
| 起始间隔 | 1s |
|---|---|
| 指数递增间隔 | 2 的幂次方 |
| 重试停止策略 | 重试 3 次之后 |
(扩展) - 实现随机延迟重试策略
基于 Retrying 工具集内的
- 等待策略
WaitStrategy设置为 递增等待规则randomWait - 停止策略
StopStrategy设置重试总数为: 4
规则设计
| 最小间隔 | 4s |
|---|---|
| 最大间隔 | 16s |
阶段 10 - 容错策略
个人笔记原文:容错机制
阶段成果
- 了解常见的容错策略
- 基于容错策略接口实现多种容错机制,支持基于 SPI 和配置文件灵活修改
- 重试策略之后启用生效后使用容错策略处理
- 容错策略实现:
Fail - Fast快速失败 (直接抛出相关异常)Fail - Safe静默处理 (仅返回 RpcResponse)
- (扩展) 实现
Fail - Back失败自动恢复 (参考 Dubbo 的本地伪装服务) - (扩展) 实现
Fail - Over故障转移 (基于 ServiceDiscovery 获取到所有服务节点后尝试访问其他服务节点)
示意图
重试策略设计 UML
负载均衡 + 重试策略 + 容错机制流程
实现
- 常见的容错策略实现:FailOver
故障转移+ FailBack失败故障恢复+ FailSafe静默处理+ FailFast快速失败 - 常见的容错工作机制有:继续重试 + 限流 + 降级 + 熔断 + 超时控制
- 定义重试机制接口
ErrorTolerantStrategy;各个容错策略实现该接口 - 实现 FailFast:
FailFastTolerantStrategy直接抛出异常 - 实现 FailSafe:
FailSafeTolerantStrategy遇到异常后返回一个相应对象RpcResponse - 添加容错配置常量
ErrorTolerantStrategyKeys列举所有支持的容错策略键名;支持基于配置RpcConfig灵活切换重试策略 - 基于简单工厂模式实现
ErrorTolerantStrategyFactory;支持根据容错策略键名返回对象; - 添加自定义 SPI 资源文件目录下,RpcConfig 新增配置项,支持基于配置切换
- 修改消费端
ServiceProxy,应用容错策略。
扩展 - 实现 FailBack 容错机制
参考文档:
服务容错 - 本地伪装
实现:
- 消费者端实现
UserService接口,作为本地的伪装服务。 - 在消费者端的
exp-consumer模块中,使用ConcurrentHashMap来维护一个本地的 Mock 服务注册中心 (LocalServiceMockRegistry)。 - 当重试策略完成后,采用 FallBack 策略来查询这个本地 Mock 服务注册中心。
- 查询成功则返回结果;如果失败,则输出错误信息。
- 将
LocalServiceMockRegistry功能优化并迁移至rpc-core模块中。 - 构建一个简单的消息队列。当重试机制失效时,启用容错机制,并把当前请求的信息放入队列尾部。
- 在启用 FailBack 容错策略的情况下,消费端从队列中取出请求信息,并再次尝试通过本地 Mock 服务注册中心进行处理。
这样处理后,既保留了原有逻辑的核心部分,又使得表述更加简洁明了。
流程梳理
(扩展) - 实现 FailOver 容错机制
思路
-
通过 serviceDiscovery 获取多个服务节点。
-
当某个节点的重试策略失败时,切换到下一个服务节点。
实现步骤
- 修改 ServiceProxy,利用
ErrorTolerantStrategy接口定义的方法参数Map<String, Object> context传递以下信息:RpcRequest:请求的服务信息selectedServiceMetaInfo: 已访问的服务节点信息serviceInfos: 所有可用的服务节点信息retryStrategy: 重试策略sender: 基于协议的请求发送者
- 实现 ErrorTolerantStrategy 接口,定义 FailOver 逻辑以尝试下一个可用的服务节点。如果请求成功,则返回响应;否则继续尝试其他节点,直到所有节点都被尝试过为止。
- 启动多个服务提供者,并将它们注册为相同名称的服务到注册中心。
(扩展) - 基于注解驱动实现 FailBack 策略
注解设计
@JRpcFailBack 设计 - 仅支持作用于属性字段 @Target(ElementType.FIELD)
serviceName: 本地伪装的服务名 (全类名)mock: 可以指定本地伪装实现类的全类名;如果未设置,默认选用第一个查询到的服务作为伪装服务
@MockScanPackage 设计
- basePackage: 指定扫描的包名,扫描注册该包下的所有伪装服务
实现
- 优化本地伪装服务注册中心,使用
ConcurrentHashMap存储服务类名和服务实现类列表。 - 启动时,Consumer扫描标记了
@MocScanPackage注解的包,并将这些类注册到LocalServiceMockRegister中。 - 扫描所有带有
@JRpcFailBack注解的字段,检查其服务是否支持本地伪装。 - 若发现带
@JRpcFailBack注解但未设置RPC框架为FailBack模式,则抛出错误。 - 测试配置正确性,配置不当时报错。
- 测试未注册伪装服务的情况。
- 验证已注册的本地伪装服务能否正常工作。
优化
- 问题分析:当前仅支持默认调用首个配置的伪装服务(按照
serviceDiscovery()查询到后存储在LocalServiceMockRegister中的顺序)。 - 优化思路:
- 扩展
@JRpcFailBack注解,添加mockServiceName字段指定本地伪装类全名。 LocalServiceMockRegistry新增bindMockService方法来绑定特定mockServiceName与指定的伪装服务类。- 在扫描
@JRpcFailBack注解时,如果mockServiceName非空,则建立服务类与指定伪装类之间的映射。 - 容错触发时,根据
serviceName查找对应的本地伪装服务类;若指定了伪装类则使用该指定类,否则使用列表中第一个服务作为默认。
- 扩展
- 修改后的
@JRpcFailBack注解 包括:
| 字段名 | 类型 | 说明 | 默认值 |
|---|---|---|---|
| serviceName | String | 服务名称(全类名); | 空字符串 |
| mockServiceName | String | 本地伪装服务名称(全类名) | 空字符串 |
| mock | boolean | 是否开启本地伪装机制 | false |
- 修改后的
@MockScanPackage
| 字段名 | 类型 | 说明 | 默认值 |
|---|---|---|---|
| basePackage | String | 指定扫描的包名 | com.jools.exp.consumer.api |
- 扩展
LocalServiceMockRegistry支持为每个服务绑定一个特定的本地伪装服务。 - 测试:
- 测试默认情况下的行为,即调用列表中的第一个服务。
- 测试指定单一服务的情况。
阶段 11 - 启动机制和注解驱动
个人笔记原文:启动机制和注解驱动
参考文献:
阶段成果
- 注解设计:
@EnableJRpc+@JRpcReference+@JRpcService - 参考 Dubbo 为服务提供者和消费者写启动类并简化代码。用三大注解(@EnableJRpc、@JRpcService、@JRpcReference)。
- 采用 Bean 监听机制,实现 BeanPostProcessor 接口,依注解执行服务注册和代理对象注入。
- 基于 SpringBoot 的 RPC starter 模块,扫描启动类 @EnableJRpc 注解,支持启动后基于注解驱动。
示意图
设计的启动器类
实现
封装启动器
- 参考 Dubbo 设计和示例。测试基于 Dubbo 启动器实现
- 注解驱动设计:主动扫描 + 监听 Bean 的加载
- 将 Provider 服务注册信息:
serviceName+implClass封装成ServiceRegisterInfo - 实现
ProviderBootstrap: 将传入的 ServiceRegisterInfo 集合注册服务实现类到本地服务中心,并封装成 ServiceMetaInfo 注册到服务注册中心。 - 简化 Provider 启动类,调用
ProviderBootstrap - 修改 Consumer 启动类,调用
ConsumerBootstrap
基于 Spring Boot 的注解驱动
- 新建基于 Spring Boot 的项目,开发注解驱动
- 参考Dubbo 中支持的三大核心注解:
@EnableDubbo+@DubboReference+@DubboService自定义类似注解
设计实现 @EnableJRpc 注解
| 注解字段名 | 数据类型 | 默认值 | 内容 |
|---|---|---|---|
| needServer | boolean | true | 是否需要启动 Web 服务器 (区分 Consumer / Provider) |
| useStarteSDK | boolean | true | 是否启用 SDK 配置 RPC 框架 |
设计实现 @JRpcService注解
| 注解字段名 | 数据类型 | 默认值 | 内容 |
|---|---|---|---|
| serviceClass | Class<?> | Void.class | 提供服务的服务接口类全类名 |
| serviceVersion | String | RpcConstant. DEFAULT_SERVICE_VERSION | 服务版本 |
设计实现 @JRpcReference 注解
| 注解字段名 | 数据类型 | 默认值 | 内容 |
|---|---|---|---|
| interfaceClas | Class<?> | Void.class | 查询的服务接口类全类名 |
| serviceVersion | String | RpcConstant. DEFAULT_SERVICE_VERSION | 服务版本 |
| retryStrategy | String | RetryStrategyKeys.fixInterval | 重试策略 |
| loadBalanceStrategy | String | LoadBalancerKeys.ROUND_ROBIN | 负载均衡策略 |
| errorTolerantStrategy | String | ErrorTolerantKeys.FAIL_FAST | 容错策略 |
| enableMock | boolean | false | 是否开启接口 Mock 测试 |
- 实现 Rpc 框架的全局启动类
RpcInitBootstrap:扫描@EnableRpc注解并解析属性, 如果needServer 为 true则需要初始化基于 Vert.x 的 TCP 处理器。 - 服务提供者启动类
RpcProviderBootstrap:实现扫描将被@JRpcService标识的类注册到本地服务中心Local Registry(后期请求调用)+ 远端服务注册中心[Etcd / ZooKeeper / Redis] (供查询) - 服务消费者启动类
RpcConsumerBootstrap: 实现 Bean 后置处理器,在 Bean 初始化后,反射获取所有属性字段。若字段有@RpcReference注解,为该属性生成代理对象后赋值。 - 给
@EnableRpc增加@Import注解,注册自定义启动类,启用加载器
(扩展) JRPC - Spring - Boot - Starter 项目支持读取 .yml/.yaml 格式文件
配置加载规则设计
- 基于传入实参后缀格式加载相应配置文件。
- 若后缀合法,按格式匹配加载,优先级为
.properties>.yaml>.yml。 - 若不传入后缀,按此顺序加载,成功一份即返回,否则加载
rpc-core模块内 RpcConfig 的默认配置。 - 若后缀非法,若 starter 模块有
application.properties则加载返回,否则加载rpc-core模块内 RpcConfig 默认配置。
思路
- 添加工具类
StarterConfigUtils,解析/resources/目录下配置文件。 - 优先级:
.properties >.yaml >.yml。用默认 RpcConfig 字段值作配置兜底。
实现
- 复用
rpc-core模块内已经由的配置解析工具 - 在 starter模块内新建配置解析类
StarterConfigUtils - 测试是否按照设计的优先级加载配置
(扩展) 区分消费端和服务端,简化消费端启动流程。
实现
- 扩展
RpcApplication,支持 key 传入区分消费与提供端,依needServer分辨消费者和提供者。 StarterConfigUtils添加initStarterRegistry()。调用RpcApplication的initRegistry()。基于可变参数,传入key = true则启用 Web 服务器并开启心跳续期机制。
(扩展) JRPC - Spring - Boot - Starter项目支持基于注解启动本地伪装服务(容错)
容错策略阶段内已经实现基于注解 @JRpcFailBack指定本地伪装。参考之前设计,优化并实现基于 SpringBoot Start 启动器的本地伪装 FailBack
注解设计
- 定义
@FailBackService注解,标记为本地伪装服务
| 字段名 | 类型 | 默认值 | 解释 |
|---|---|---|---|
| mockServiceName | String | 空字串 (以注解标记类实现的接口名) | 为mockServiceName 服务名提供本地注册服务 |
- 定义
@LocalMockScanPackage注解, 指定本地伪装扫描的包
| 字段名 | 类型 | 默认值 | 解释 |
|---|---|---|---|
| basePackage | String | com.jools.rpc.springboot.service.localmock | 扫描该包下的所有被@FailBackService 的类 |
- 定义
@FailBackReference注解,指定服务名称 + 绑定伪装服务名称
| 字段名 | 类型 | 默认值 | 解释 |
|---|---|---|---|
| bindServiceName | String | 空子串 (默认以查询到的所有伪装服务的首个服务) | 绑定的伪装服务类名 |
注解驱动逻辑
- 扫描带有
@FailBackService注解的类,并根据接口全名和注解中的mockServiceName字段将其注册到LocalServiceMockRegistry。 - 对于带有
@FailBackReference注解的字段,根据serviceName或接口名(如果serviceName为空)以及@FailBackService注解中的mockServiceName字段(如果存在),在LocalServiceMockRegistry中建立关联。 - 当重试机制触发时,从
LocalServiceMockRegistry中查询对应的模拟服务类。如果有唯一绑定,则返回该绑定;否则返回第一个找到的模拟服务类。
实现步骤
- 定义所需注解。
- 在消费者端创建多个本地模拟服务,并用
@FailBackService标记。 - 消费者启动配置中添加
@LocalMockScanPackage来指定扫描路径。 - 需要调用的服务字段上加上
@FailBackReference注解。 - 在
JRpc-SpringBoot-Starter中开发LocalMockServiceBootstrap,实现ImportBeanDefinitionRegistrar接口负责扫描并注册标记了@FailBackService的服务及其实现类至LocalServiceMockRegistry。 - 同样在
JRpc-SpringBoot-Starter中实现LocalMockReferBootstrap实现BeanPostProcessor后置处理器,用于处理@FailBackReference注解下的字段与特定或默认本地模拟服务之间的关联。 - 在
rpc-core模块内新增ErrorFailBackHandler类,用来处理失败回退消息,通过查询LocalServiceMockRegistry获取并反射调用合适的模拟服务。 - 测试:确保当未启用 FailBack 功能但使用了
@FailBackReference时能够正确抛出异常。 - 测试:验证启用了 FailBack 时是否能自动调用首个注册的本地 Mock Service。
- 测试:检查基于
@FailBackReference中bindMockServiceName属性设置能否准确绑定唯一的本地模拟服务。
(扩展) JRPC - Spring - Boot - Starter 基于注解 @RpcReference (mock字段)支持本地 Mock 测试
需求
- 通过
RpcConsumerBootstrap启动器扫描被@JRpcReference注解标记的 Bean,获取注解相关字段。 - 其中
@JRpcReference注解内含有mock()字段可配置 - 设计:如果
mock()字段,被标记为 true; 通过ServiceProxyFactory直接返回相应的 Mock 实例
实现
- 修改
RpcConsumerBootstrap支持 Consumer 端基于@JRpcReferenec注解的 mock 字段,直接注入 Mock 示例,后续调用实现接口Mock - 测试:设置
@JRpcReferenec注解enableMock字段为 true
扩展 - JRPC-Spring-Boot-Starter 支持基于 Spring Boot SDK 方式自定义全局配置
参考文档:鱼皮 API 项目- 实现一个 SpringBoot Starter SDK
实现:
- 简化后的步骤如下:
- 在
jrpc-spring-boot-starter项目模块中创建StarterRpcConfig类,映射RpcConfig。 - 为
StarterRpcConfig添加注解:@Configuration标记其为配置类,@ComponentScan用于组件扫描与自动注册Bean,@ConfigurationProperties定义.yml配置文件的前缀。 - 在
/resources/META-INF/spring.factories中添加EnableAutoConfiguration键及其值为StarterRpcConfig全类名。 - 将rpc-core模块发布到本地Maven仓库。
- 将starter模块发布到本地Maven仓库。
- 刷新Maven后,在Consumer项目的
application.yml里输入指定前缀时应出现提示。 - 测试基于SDK注册
StarterRpcConfig。
问题
如何确定是通过哪种方式配置RpcConfig
- 目前支持两种配置方法:
- 通过SDK配置
RpcConfig - 直接从
resources目录下的.properties或.yml文件加载
- 通过SDK配置
解决方案
- 在
@EnableRpc注解中增加useStarterSDK布尔字段,当设置为true时启用SDK配置。 - 修改
BootInitBootstrap启动器,实现EnvironmentAware接口,并重写setEnvironment()方法。 - 在
setEnvironment方法中注入Environment对象。 - 使用
Binder工具读取配置文件内以 "jrpc" 为前缀的属性,并绑定至StarterRpcConfig类。 - 测试 - 更新Consumer端启动类使用
@EnableJRpc(needServer = false, useStarterSDK = true)。 - 测试 - 通过SDK配置注入
RpcConfig。 - 确认 - 注入SDK配置后RPC框架正常运行。
修改后完整的 @EnableJRpc
▼java复制代码@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import({ RpcConsumerBootstrap.class, RpcProviderBootstrap.class, RpcInitBootstrap.class, LocalMockReferBootstrap.class, LocalMockServiceBootstrap.class }) public @interface EnableJRpc { /** * 需要启动 Server * * @return */ boolean needServer() default true; /** * 是否基于 SDK 加载配置 * * @return */ boolean useStarterSDK() default false; }
可选扩展点 - from 鱼皮建议
扩展点实现笔记(2025.3.2 更新):所有扩展点实现把笔记
完整可选扩展点请参考鱼皮笔记原文
(已实现) 项目支持读取 .yml /.yaml 等更多类型的配置文件
- rpc-core模块项目支持解析 yml / yaml 配置文件
- jrpc-springboot-starter模块也支持解析 yml / yaml配置文件
- jrpc-springboot-starter模块支持基于 Spring Boot SDK 方式配置
(已实现)服务注册信息缓存优化,支持区分服务 key, 而不是公用同一个缓存对象
rpc-core模块内的 RegistryServiceCache支持服务注册信息缓存
- 基于
serviceKey作为 key 区分不同服务 - 以
List作为集合存储所有服务发现得到的服务节点信息
(已实现)实现拦截器机制,服务调用前和服务调研后可以执行额外操作
参考思路:
可参考 Spring MVC 或 Servlet 的 Filter 机制,用责任链模式实现。在服务提供者处理前后、服务消费者调用前后加拦截器,用于日志校验、安全校验等。也可用 SPI 机制支持用户自定义拦截器。
- 定义 Interceptor 接口
- 实现接口类
- 在自定义 SPI 资源目录下配置
key+Interceptor 实现类全类名- RpcConfig 添加配置项
个人思路梳理:
设计一个 Interceptor 接口
RpcHandlerInterceptor,参考 Spring 生态的拦截器设计一个拒绝访问无效服务 Interceptor -
InvalidServiceNameInterceptor一个拦截非法用词取名的 Interceptor -
IllegalIpInterceptor实现
HandlerInterceptor接口
InvalidServiceNameInterceptor:如果申请失效服务名返回 false
IllegalParamInterceptor: 携带非法实参值则拒绝在 TcpServerHandler Decode 得到 ProtocolMessage 之后可以通过 body 部分获取到携带的
RpcRequest部分,并且从中获取到serviceName: 请求服务名称
params: 请求实参列表
新增
InterceptorKey枚举类: 通过 key 控制获取注册的拦截器新增
RpcConfig配置项enableInterceptor,可以配置关闭拦截器或者开启全部开启
true全部禁止
false创建
InterceptorFactory简单工厂类,支持基于 RpcConfig 配置控制行为
具体步骤参考上述给予的笔记链接
(写思路未实现) 支持消费方指定某个服务级别的负载均衡器、重试策略、容错机制
参考思路:
目前只能通过全局配置改变对所有服务的负载均衡调用规则。实现的话可能需要修改 ServiceProxy 类,让它支持传参,根据 消费端的配置来动态创建 ServiceProxy。
- 目前在 Consumer 启动器内可以基于注解为单个服务指定
个人思路梳理:
- 基于传入的 RpcRequest 信息
- 在响应后如果有异常,基于存储的本次 RpcRequest 的负载均衡、容错机制处理
- 如果没有设置,则使用配置文件设置兜底
(已实现) 服务服务消费方支持设定超时时间
参考思路:
可以通过修改 TCP 客户端请求相关的代码实现。
目前为固定值
Consumer 调用 Sender 等待 10s 后超时
个人思路梳理:
RpcConfig内新增配置项rpcRespWaitDuration用于记录等待下一个响应的最长等待时间- 支持基于 SpringBoot Starter SDK 配置或者直接修改 RpcConfig 的默认值
- RpcConstant 内新增 RpcConfig 的默认值常量
DEFAULT_RESP_WAIT_DURATION设置为 10s- TcpSender 和 HttpSender 内启动时通过配置加载好
具体步骤参考上述给予的笔记链接
(已实现) - 处理 Bean 注入问题:目前本地服务注册表存储的是 class,然后通过反射创建实例
但是如果 Bean 包含有参构造函数,或者给属性注入了其他示例,这种方式就行不通了。
参考思路:
本地注册时要放入实现类的对象实例,而不是
class类型
具体步骤参考上述给予的笔记链接
(写思路未实现) - 服务注册信息缓存增加过期时间,定时刷新缓存
参考思路:
基于
Caffeine构建缓存

