myrpc 学习笔记-004 序列化实现和 SPI 机制
项目地址 欢迎访问
笔记总览 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 四种序列化器

一、序列化基础概念
- 序列化:将Java对象转换为字节数组,用于网络传输、持久化存储
- 反序列化:将字节数组还原为Java对象
- 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
- 序列化(对象 → byte[])
▼java复制代码ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(object); // 序列化
- 反序列化(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; // 不会被序列化
优点
- JDK 原生,不用引任何包
- 使用简单,API 傻瓜式
- 支持几乎所有对象(包括继承、引用、集合)
- 深拷贝非常方便
缺点
-
性能差 速度慢,比 Kryo、Hessian 慢几倍
-
字节码体积巨大 序列化后的数据特别长,浪费带宽
-
无法跨语言 C++、Go、Python 都读不懂 Java 序列化数据
-
有安全风险 反序列化时会执行代码,容易被攻击
-
兼容性差 类稍微一改,就无法反序列化
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); }
内部流程:
- 遍历对象所有字段
- 按字段名和值拼成 JSON 结构
- 转成 UTF-8 字节数组
- 可用于网络传输
示例结果(人类可读):
▼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); }
流程:
- 读取字节 → 解析为 JSON
- 根据传入的
Class<T>类型 - 反射创建对象
- 把 JSON 键值对塞进对象字段
JSON 序列化优点
-
可读性极强 抓包、调试、日志都能直接看内容。
-
跨语言无敌 Go、Python、JS、C++ 全都能解析 JSON,是通用协议。
-
兼容性好 字段增减、版本升级不容易出错。
-
轻量、通用 Web、HTTP、RESTful 接口标准格式。
-
配置灵活 日期格式化、字段别名、忽略字段、脱敏都能做。
JSON 序列化缺点
-
性能不如二进制序列化 比 Kryo、Hessian 慢,体积也更大。
-
不支持引用、循环依赖 对象 A 引用 B,B 引用 A,JSON 序列化会栈溢出。
-
纯 JSON 不记录类型 反序列化
Object、泛型时必须额外配置(我在代码里已经处理了)。 -
不支持复杂对象结构 如非标准 Java 类、内部类、动态代理类等容易出错。
3. Hessian 序列化
Hessian 是一种由 Caucho 公司开发的动态类型、紧凑、跨语言的二进制序列化协议,专为高性能 RPC 场景设计。它在 Dubbo 2.x 中曾是默认序列化方案,核心优势是速度快、体积小、对Java零侵入。
定位与本质
- 类型:二进制序列化(非文本)
- 流程:
对象 → 二进制流(直接生成,无中间字符串) → byte[] - 对比:介于 JDK(笨重)和 JSON(文本、慢)之间,兼顾性能与兼容性
原理(Hessian 2.0)
- 类型标签 (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(结束符)
- 极致的紧凑编码(变长/优化)
- 小整数优化:-16 ~ 47 直接用单字节表示,无需类型标记。
- 字符串优化:长度 < 32 的字符串,直接用 1字节长度前缀。
- 二进制优化:区分短二进制、长二进制,减少冗余。
- 引用 (Ref) 机制:遇到重复/循环对象,不重复序列化,只写入引用标记 (
J/K)。
- 对象序列化流程
- 写入类型信息:写入类名(如
com.xxx.User)。 - 遍历字段:按字段名顺序依次写入
字段名 -> 字段值。 - 递归处理:嵌套对象继续此流程。
- 结束标记:写入
Z表示对象结束。
代码(Java)
- Maven 依赖
▼xml复制代码<dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.66</version> </dependency>
- 序列化工具类
▼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); } }
优缺点
优点
- 高性能:速度是 JDK 的 3-5 倍,体积约为 JDK 的 1/2。
- 零侵入:普通 JavaBean 即可,无需实现接口、无需无参构造(相对宽松)。
- 跨语言:支持多语言间数据互通。
- 容错性:类增删字段时,反序列化兼容性比 JDK 好。
缺点
- 不可读:二进制流,调试不如 JSON 方便。
- 安全漏洞:低版本(<4.0.65)存在反序列化漏洞。
- 泛型/复杂类型:对某些 Java 集合(如 LinkHashMap)支持需额外适配。
4. Kryo 序列化
Kryo 是什么?
Kryo 是一个高性能、轻量级、二进制的 Java 序列化框架。速度极快、体积极小,是 Java 序列化的天花板。
特点:
- 纯二进制(没有 JSON 那样的字符串)
- 不跨语言(只能 Java 用)
- 线程不安全 → 必须用 ThreadLocal
- 速度 > Hessian > JSON > JDK
- 体积 < Hessian < JSON < JDK
优点
- 不写类名(用数字 ID 代替)
JDK 序列化会把完整类名写进去:
com.xxx.xxx.User
Kryo 只写一个 数字 ID,比如 10
体积直接暴减!
-
无冗余元数据 不写多余的结构描述,只写字段值。
-
无引用检查(可关闭) 结构极简。
-
直接操作字节码 速度极快。
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 最关键的两个配置
- setRegistrationRequired(false)
true:必须提前注册类,否则报错(体积更小、更快)false:自动处理任何类(方便,适合RPC)
我在代码中用的是 false,最通用。
- ThreadLocal 保证多线程安全。
Kryo 要求
必须:
- 有无参构造函数(默认构造)
不需要:
- 不需要实现
Serializable - 不需要
serialVersionUID - 不需要 getter/setter(能直接访问字段)
- 不需要任何注解
比 JDK、JSON 都宽松!
Kryo 缺点
- 线程不安全 → 必须 ThreadLocal
- 不跨语言 → Go/Python 不能解析
- 不能序列化没有无参构造的类
- 复杂对象(如动态代理)可能不支持
四、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:
- 写一个配置文件,里面写
▼text复制代码jdk=com.xxx.JdkSerializer json=com.xxx.JsonSerializer kryo=com.xxx.KryoSerializer
-
程序自动读取这个文件
-
想获取哪个,直接传 key 就能拿到实例
SPI 流程
流程总览
- 定义接口(Serializer)
- 写配置文件(META-INF/rpc/...)
- SpiLoader 加载配置
- SerializerFactory 提供获取方法
- 使用:getSerializer("json") → 直接拿到对象
SpiLoader 代码分析
- 存储结构
▼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 }
- 扫描路径 定义两个 SPI 目录:
▼text复制代码META-INF/rpc/system/ 系统默认 META-INF/rpc/custom/ 用户自定义(可覆盖系统)
这就是 自定义扩展机制!
- 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);
- getInstance(接口.class, key)
▼java复制代码Serializer serializer = SpiLoader.getInstance(Serializer.class, "json");
做了什么:
- 从 LOAD_MAP 拿到接口对应的
key->类 - 根据 key 拿到实现类
- 缓存里没有就 newInstance() 创建
- 放回缓存,保证单例
- 返回对象
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 优点
-
解耦 接口和实现彻底分离,不再硬编码
-
可扩展 别人想加个序列化器,只需要:
- 写个实现类
- 加个配置文件 不用改源码
-
易替换 想从jdk换成kryo?只换key就行
-
统一管理 所有插件统一读取、统一获取
总结
SPI 机制 =配置文件 + 动态加载 + 工厂模式 + 单例
流程:
▼text复制代码配置文件(key=实现类) ↓ SpiLoader 读取并缓存 ↓ SerializerFactory 提供获取 ↓ getSerializer(key) → 直接拿到对象
SPI 就是“根据名字找实现”的机制,让RPC框架可以插件化扩展!
