myrpc 学习笔记-004 序列化实现和 SPI 机制

项目地址 欢迎访问

https://gitee.com/longlong5/myrpc

笔记总览 myrpc 学习笔记-001 实现简易版 rpc myrpc 学习笔记-002 配置加载 myrpc 学习笔记-003 Mock 服务代理 myrpc 学习笔记-004 序列化实现和 SPI 机制 myrpc 学习笔记-005 注册中心 myrpc 学习笔记-006 自定义协议 myrpc 学习笔记-007 负载均衡 myrpc 学习笔记-008 重试机制 myrpc 学习笔记-009 容错机制 myrpc 学习笔记-0010 启动机制和注解驱动

架构图v4.0.0

  • 支持自定义序列化器
  • spi 机制
  • 实现 jdk hessian kryo json 四种序列化器

v4.0.0.png

一、序列化基础概念

  1. 序列化:将Java对象转换为字节数组,用于网络传输、持久化存储
  2. 反序列化:将字节数组还原为Java对象
  3. RPC核心作用:服务消费者和提供者之间传输请求/响应对象必须经过序列化

二、统一序列化接口

所有序列化器都实现该接口,保证统一调用规范

java
复制代码
package com.longlong.myrpc.serializer; import java.io.IOException; /** * 序列化接口 */ public interface Serializer { /** * 序列化 */ <T> byte[] serialize(T object) throws IOException; /** * 反序列化 */ <T> T deserialize(byte[] bytes, Class<T> type) throws IOException; }

三、序列化器具体实现

1. JDK 原生序列化

什么是 JDK 序列化?

Java 自带的对象持久化 / 传输机制 不需要任何第三方库,JDK 原生就支持:

  • 序列化:把内存中的 Java 对象 → 变成字节数组 byte[]
  • 反序列化:把字节数组 → 恢复成 Java 对象

作用:

  • 网络传输对象(RPC 核心)
  • 保存对象到文件/缓存
  • 深拷贝对象

使用条件

要被序列化的类 必须实现一个空接口

java
复制代码
public class User implements Serializable { }

没有实现这个接口,序列化会直接抛异常:

text
复制代码
java.io.NotSerializableException

核心 API

  1. 序列化(对象 → byte[])
java
复制代码
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(object); // 序列化
  1. 反序列化(byte[] → 对象)
java
复制代码
ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bis); Object obj = ois.readObject(); // 反序列化

序列化过程到底做了什么?

JDK 会自动把这些信息写入字节流:

  • 类全限定名(包名+类名)
  • 字段类型和值
  • 父类信息
  • 序列化 ID(serialVersionUID)
  • 所有引用对象(递归序列化)

最终变成一串二进制,用于传输或存储。

关键知识点:serialVersionUID

java
复制代码
private static final long serialVersionUID = 1L;

作用: 验证序列化和反序列化时,是不是同一个类版本

如果不写:

  • JDK 会自动根据类结构生成
  • 类一旦改了字段、方法,ID 就变了
  • 反序列化直接报错:InvalidClassException

所以规范写法 必须手动指定 serialVersionUID

哪些东西不会被序列化?

  • static 静态变量(属于类,不属于对象)
  • transient 关键字修饰的字段
java
复制代码
transient int password; // 不会被序列化

优点

  1. JDK 原生,不用引任何包
  2. 使用简单,API 傻瓜式
  3. 支持几乎所有对象(包括继承、引用、集合)
  4. 深拷贝非常方便

缺点

  1. 性能差 速度慢,比 Kryo、Hessian 慢几倍

  2. 字节码体积巨大 序列化后的数据特别长,浪费带宽

  3. 无法跨语言 C++、Go、Python 都读不懂 Java 序列化数据

  4. 有安全风险 反序列化时会执行代码,容易被攻击

  5. 兼容性差 类稍微一改,就无法反序列化

2. JSON 序列化(Jackson)

什么是 JSON 序列化?

把 Java 对象 → JSON 字符串/字节数组 把 JSON 字节/字符串 → Java 对象

特点:

  • 文本格式,人能直接看懂
  • 跨语言通用(Java/Go/Python/JS 都支持)
  • 轻量、通用、Web 环境标配
  • 不属于 JDK 原生,需要依赖(Jackson/Gson/Fastjson 等)

我这里用的是 Jackson,Spring 默认也是它。

代码核心做了什么?

java
复制代码
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

ObjectMapper 是 Jackson 的核心工具类,全局单例即可。

关键配置

java
复制代码
// 1. 自动记录类型(解决泛型、Object 反序列化) OBJECT_MAPPER.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL );

作用: JSON 本身不记录对象类型。 比如你序列化为一个 Object 类型,反序列化时 Jackson 不知道转成什么类。 开启自动类型后,JSON 里会带上类名,才能正确转回原对象。

java
复制代码
// 2. 遇到未知字段不报错 OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

作用: 服务端新增字段、客户端旧版本不会崩,兼容性更强

java
复制代码
// 3. 不序列化 null 字段,减小体积 OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);

序列化过程(对象 → JSON 字节)

java
复制代码
@Override public <T> byte[] serialize(T object) throws IOException { return OBJECT_MAPPER.writeValueAsBytes(object); }

内部流程:

  1. 遍历对象所有字段
  2. 按字段名和值拼成 JSON 结构
  3. 转成 UTF-8 字节数组
  4. 可用于网络传输

示例结果(人类可读):

json
复制代码
{ "name": "test", "age": 20, "mock": false }

反序列化过程(字节 → 对象)

java
复制代码
@Override public <T> T deserialize(byte[] bytes, Class<T> type) throws IOException { return OBJECT_MAPPER.readValue(bytes, type); }

流程:

  1. 读取字节 → 解析为 JSON
  2. 根据传入的 Class<T> 类型
  3. 反射创建对象
  4. 把 JSON 键值对塞进对象字段

JSON 序列化优点

  1. 可读性极强 抓包、调试、日志都能直接看内容。

  2. 跨语言无敌 Go、Python、JS、C++ 全都能解析 JSON,是通用协议。

  3. 兼容性好 字段增减、版本升级不容易出错。

  4. 轻量、通用 Web、HTTP、RESTful 接口标准格式。

  5. 配置灵活 日期格式化、字段别名、忽略字段、脱敏都能做。

JSON 序列化缺点

  1. 性能不如二进制序列化 比 Kryo、Hessian 慢,体积也更大。

  2. 不支持引用、循环依赖 对象 A 引用 B,B 引用 A,JSON 序列化会栈溢出。

  3. 纯 JSON 不记录类型 反序列化 Object、泛型时必须额外配置(我在代码里已经处理了)。

  4. 不支持复杂对象结构 如非标准 Java 类、内部类、动态代理类等容易出错。

3. Hessian 序列化

Hessian 是一种由 Caucho 公司开发的动态类型、紧凑、跨语言的二进制序列化协议,专为高性能 RPC 场景设计。它在 Dubbo 2.x 中曾是默认序列化方案,核心优势是速度快、体积小、对Java零侵入

定位与本质

  • 类型二进制序列化(非文本)
  • 流程对象 → 二进制流(直接生成,无中间字符串) → byte[]
  • 对比:介于 JDK(笨重)和 JSON(文本、慢)之间,兼顾性能与兼容性

原理(Hessian 2.0)

  1. 类型标签 (Type Tag) 驱动 Hessian 为每一种数据类型分配一个单字节标识符(Tag),作为字节流的开头。解码器通过识别 Tag 来解析后续数据。
  • Boolean: T (0x54) = true, F (0x46) = false
  • Null: N (0x4e)
  • Integer: 优化编码,小整数(如 0)仅占 1字节,大数用 I (0x49) + 4字节
  • String: 短字符串直接用长度字节(0x00-0x1f)+ UTF-8数据
  • Object/Map: M (0x4d) + 类型 + (key value)* + Z (结束符)
  1. 极致的紧凑编码(变长/优化)
  • 小整数优化:-16 ~ 47 直接用单字节表示,无需类型标记。
  • 字符串优化:长度 < 32 的字符串,直接用 1字节长度前缀。
  • 二进制优化:区分短二进制、长二进制,减少冗余。
  • 引用 (Ref) 机制:遇到重复/循环对象,不重复序列化,只写入引用标记 (J/K)。
  1. 对象序列化流程
  2. 写入类型信息:写入类名(如 com.xxx.User)。
  3. 遍历字段:按字段名顺序依次写入 字段名 -> 字段值
  4. 递归处理:嵌套对象继续此流程。
  5. 结束标记:写入 Z 表示对象结束。

代码(Java)

  1. Maven 依赖
xml
复制代码
<dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.66</version> </dependency>
  1. 序列化工具类
java
复制代码
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; public class HessianSerializer { // 序列化 public static <T> byte[] serialize(T obj) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); Hessian2Output out = new Hessian2Output(bos); out.writeObject(obj); // 核心方法 out.flush(); return bos.toByteArray(); } // 反序列化 public static <T> T deserialize(byte[] bytes, Class<T> clazz) throws Exception { ByteArrayInputStream bis = new ByteArrayInputStream(bytes); Hessian2Input in = new Hessian2Input(bis); return (T) in.readObject(clazz); } // 测试 @Data // Lombok static class User { private String name; private int age; } public static void main(String[] args) throws Exception { User user = new User(); user.setName("longlong"); user.setAge(18); // 序列化 byte[] data = serialize(user); System.out.println("Hessian字节长度: " + data.length); // 通常远小于JDK // System.out.println(HexUtil.encodeHexStr(data)); // 查看16进制 // 反序列化 User newUser = deserialize(data, User.class); System.out.println("反序列化: " + newUser); } }

优缺点

优点

  1. 高性能:速度是 JDK 的 3-5 倍,体积约为 JDK 的 1/2。
  2. 零侵入:普通 JavaBean 即可,无需实现接口、无需无参构造(相对宽松)。
  3. 跨语言:支持多语言间数据互通。
  4. 容错性:类增删字段时,反序列化兼容性比 JDK 好。

缺点

  1. 不可读:二进制流,调试不如 JSON 方便。
  2. 安全漏洞:低版本(<4.0.65)存在反序列化漏洞。
  3. 泛型/复杂类型:对某些 Java 集合(如 LinkHashMap)支持需额外适配。

4. Kryo 序列化

Kryo 是什么?

Kryo 是一个高性能、轻量级、二进制的 Java 序列化框架。速度极快、体积极小,是 Java 序列化的天花板。

特点:

  • 纯二进制(没有 JSON 那样的字符串)
  • 不跨语言(只能 Java 用)
  • 线程不安全 → 必须用 ThreadLocal
  • 速度 > Hessian > JSON > JDK
  • 体积 < Hessian < JSON < JDK

优点

  1. 不写类名(用数字 ID 代替) JDK 序列化会把完整类名写进去: com.xxx.xxx.User

Kryo 只写一个 数字 ID,比如 10 体积直接暴减!

  1. 无冗余元数据 不写多余的结构描述,只写字段值。

  2. 无引用检查(可关闭) 结构极简。

  3. 直接操作字节码 速度极快。

Kryo 序列化流程

text
复制代码
Java 对象 ↓ Kryo 读取类结构 → 分配 ID ↓ 直接写入二进制(极简格式) ↓ byte[] 字节数组

代码原理

java
复制代码
// 关键:ThreadLocal 保证线程安全 private static final ThreadLocal<Kryo> KRYO_THREAD_LOCAL = ThreadLocal.withInitial(() -> { Kryo kryo = new Kryo(); // 关闭注册检查:不用提前注册类,自动处理 kryo.setRegistrationRequired(false); return kryo; });

为什么要用 ThreadLocal? Kryo 不是线程安全的! 多线程同时序列化会直接报错。 所以必须 一个线程一个 Kryo 实例

序列化(对象 → byte[])

java
复制代码
@Override public <T> byte[] serialize(T object) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); Output output = new Output(bos); // Kryo 输出流 KRYO_THREAD_LOCAL.get().writeObject(output, object); output.close(); return bos.toByteArray(); }

反序列化(byte[] → 对象)

java
复制代码
@Override public <T> T deserialize(byte[] bytes, Class<T> type) throws IOException { ByteArrayInputStream bis = new ByteArrayInputStream(bytes); Input input = new Input(bis); T result = KRYO_THREAD_LOCAL.get().readObject(input, type); input.close(); return result; }

Kryo 最关键的两个配置

  1. setRegistrationRequired(false)
  • true:必须提前注册类,否则报错(体积更小、更快)
  • false:自动处理任何类(方便,适合RPC)

我在代码中用的是 false,最通用。

  1. ThreadLocal 保证多线程安全。

Kryo 要求

必须:

  • 有无参构造函数(默认构造)

不需要:

  • 不需要实现 Serializable
  • 不需要 serialVersionUID
  • 不需要 getter/setter(能直接访问字段)
  • 不需要任何注解

比 JDK、JSON 都宽松!

Kryo 缺点

  1. 线程不安全 → 必须 ThreadLocal
  2. 不跨语言 → Go/Python 不能解析
  3. 不能序列化没有无参构造的类
  4. 复杂对象(如动态代理)可能不支持

四、SPI 实现扩展性

SPI 是什么?

SPI = Service Provider Interface(服务发现机制)

接口定义好 → 配置文件写死实现类 → 程序自动读取配置 → 动态创建对象

解耦!不用在代码里写死 new Xxx(),想换实现直接改配置文件!

为什么要用 SPI

目前 RPC 框架里有4 种序列化器

  • JDK
  • JSON
  • Kryo
  • Hessian

不想在代码里写:

java
复制代码
if(key.equals("jdk")) new JdkSerializer(); else if(key.equals("json")) new JsonSerializer();

太蠢了!

所以这里使用 SPI

  1. 写一个配置文件,里面写
text
复制代码
jdk=com.xxx.JdkSerializer json=com.xxx.JsonSerializer kryo=com.xxx.KryoSerializer
  1. 程序自动读取这个文件

  2. 想获取哪个,直接传 key 就能拿到实例

SPI 流程

流程总览

  1. 定义接口(Serializer)
  2. 写配置文件(META-INF/rpc/...)
  3. SpiLoader 加载配置
  4. SerializerFactory 提供获取方法
  5. 使用:getSerializer("json") → 直接拿到对象

SpiLoader 代码分析

  1. 存储结构
java
复制代码
// 接口名 -> (key -> 实现类) private static final Map<String, Map<String, Class<?>>> LOAD_MAP = new ConcurrentHashMap<>(); // 实例缓存(单例,不用反复new) private static final Map<String, Object> INSTANCE_CACHE = new ConcurrentHashMap<>();

最终结构是这样:

text
复制代码
"Serializer" -> { "jdk" : JdkSerializer.class "json" : JsonSerializer.class "kryo" : KryoSerializer.class }
  1. 扫描路径 定义两个 SPI 目录:
text
复制代码
META-INF/rpc/system/ 系统默认 META-INF/rpc/custom/ 用户自定义(可覆盖系统)

这就是 自定义扩展机制

  1. load(接口.class) 核心方法
java
复制代码
SpiLoader.load(Serializer.class);

它做了 4 件事:

① 去目录下找文件 文件名 = 接口全类名 例如:

text
复制代码
META-INF/rpc/system/com.longlong.myrpc.serializer.Serializer

② 读取每一行 格式:

text
复制代码
key=实现类全类名

例如:

text
复制代码
jdk=com.longlong.myrpc.serializer.JdkSerializer json=com.longlong.myrpc.serializer.JsonSerializer

③ 把 key 和 Class 存到 Map

text
复制代码
keyClassMap.put("jdk", JdkSerializer.class);

④ 把整个结构存入 LOAD_MAP

text
复制代码
LOAD_MAP.put("Serializer", keyClassMap);
  1. getInstance(接口.class, key)
java
复制代码
Serializer serializer = SpiLoader.getInstance(Serializer.class, "json");

做了什么:

  1. 从 LOAD_MAP 拿到接口对应的 key->类
  2. 根据 key 拿到实现类
  3. 缓存里没有就 newInstance() 创建
  4. 放回缓存,保证单例
  5. 返回对象

SerializerFactory 工厂类

java
复制代码
public class SerializerFactory { static { SpiLoader.load(Serializer.class); } public static Serializer getSerializer(String key) { return SpiLoader.getInstance(Serializer.class, key); } }

作用:

  • 项目一启动就加载所有序列化器
  • 外部只需要调用 getSerializer("json")
  • 完全解耦

配置文件

文件路径:

text
复制代码
META-INF/rpc/system/com.longlong.myrpc.serializer.Serializer

内容:

text
复制代码
jdk=com.longlong.myrpc.serializer.JdkSerializer json=com.longlong.myrpc.serializer.JsonSerializer kryo=com.longlong.myrpc.serializer.KryoSerializer hessian=com.longlong.myrpc.serializer.HessianSerializer

使用方式

java
复制代码
Serializer s = SerializerFactory.getSerializer("json");

想换序列化器? 不用改代码!只改配置文件或传入不同key!

SPI 优点

  1. 解耦 接口和实现彻底分离,不再硬编码

  2. 可扩展 别人想加个序列化器,只需要:

    • 写个实现类
    • 加个配置文件 不用改源码
  3. 易替换 想从jdk换成kryo?只换key就行

  4. 统一管理 所有插件统一读取、统一获取

总结

SPI 机制 =配置文件 + 动态加载 + 工厂模式 + 单例

流程:

text
复制代码
配置文件(key=实现类) ↓ SpiLoader 读取并缓存 ↓ SerializerFactory 提供获取 ↓ getSerializer(key) → 直接拿到对象

SPI 就是“根据名字找实现”的机制,让RPC框架可以插件化扩展!

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
longlong
下载 APP