一、核心概念
什么是序列化与反序列化?
序列化(Serialization):是将对象转换为字节流的过程。
反序列化(Deserialization):是将字节流还原为对象的过程。
在 RPC 框架中,序列化器扮演着"翻译官"的角色,负责将请求/响应对象在网络传输前后进行格式转换。
1. 为什么需要序列化?
-
🌐 网络传输限制:网络底层只能传输二进制字节流,无法直接传输内存中的 Java 对象。
-
🗣️ 跨语言通信:不同编程语言的内存模型不同,需要统一的数据中间格式(如 JSON、XML、Protobuf)。
-
💾 持久化存储:将对象状态安全地保存到文件或数据库中。
2. 序列化器接口设计
我们定义一个顶层接口,利用泛型支持任意类型的对象转换:
java
public interface Serializer {
/**
* 序列化:将对象转换为字节数组
*/
<T> byte[] serialize(T object) throws IOException;
/**
* 反序列化:将字节数组还原为目标类型的对象
*/
<T> T deserialize(byte[] bytes, Class<T> type) throws IOException;
}
3. 序列化器在 RPC 中的数据流向
【消费者端 (Client)】
发起调用 → 封装 RpcRequest 对象
↓ (序列化)
二进制字节流
↓ (网络发送)
----------------(网络传输)----------------
↓ (网络接收)
【提供者端 (Server)】
二进制字节流
↓ (反序列化)
还原 RpcRequest 对象 → 反射执行本地方法
↓
封装 RpcResponse 对象 ← 方法返回结果
↓ (序列化)
二进制字节流
↓ (网络发送)
----------------(网络传输)----------------
↓ (网络接收)
【消费者端 (Client)】
二进制字节流
↓ (反序列化)
还原 RpcResponse 对象 → 返回业务结果
二、JDK 序列化器
1. 实现原理
利用 Java 原生的 ObjectOutputStream 和 ObjectInputStream 实现,这是 Java 开发者最熟悉的序列化方式。
2. 完整实现
java
public class JdkSerializer implements Serializer {
@Override
public <T> byte[] serialize(T object) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream)) {
objectOutputStream.writeObject(object);
}
return outputStream.toByteArray();
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> type) throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
try (ObjectInputStream objectInputStream = new ObjectInputStream(inputStream)) {
return (T) objectInputStream.readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
3. 使用强制要求
⚠️ 注意:被序列化的实体类必须实现 Serializable 接口!
java
public class User implements Serializable {
// 强烈建议显式声明版本号,防止类结构变更导致 InvalidClassException
private static final long serialVersionUID = 1L;
private String name;
private int age;
}
4. 优缺点评估
-
✅ 优点:原生支持零依赖;使用极简;完美支持复杂对象图(循环引用、继承)。
-
❌ 缺点 :序列化后的字节码极其臃肿(包含大量类元数据);基于反射性能较差;绝对的 Java 绑定,无法跨语言。
三、JSON 序列化器
1. 实现原理
借助于 Jackson 库,将对象转换为人类可读的 JSON 格式字节数组。
2. 完整实现(处理类型擦除)
java
public class JsonSerializer implements Serializer {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
public <T> byte[] serialize(T obj) throws IOException {
return OBJECT_MAPPER.writeValueAsBytes(obj);
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> classType) throws IOException {
T obj = OBJECT_MAPPER.readValue(bytes, classType);
if (obj instanceof RpcRequest) {
return handleRequest((RpcRequest) obj, classType);
}
if (obj instanceof RpcResponse) {
return handleResponse((RpcResponse) obj, classType);
}
return obj;
}
// 处理 RpcRequest 参数类型擦除问题
private <T> T handleRequest(RpcRequest rpcRequest, Class<T> type) throws IOException {
Class<?>[] parameterTypes = rpcRequest.getParameterTypes();
Object[] args = rpcRequest.getArgs();
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> clazz = parameterTypes[i];
// 发现类型不匹配(通常变成了 LinkedHashMap),则重新反序列化
if (args[i] != null && !clazz.isAssignableFrom(args[i].getClass())) {
byte[] argBytes = OBJECT_MAPPER.writeValueAsBytes(args[i]);
args[i] = OBJECT_MAPPER.readValue(argBytes, clazz);
}
}
return type.cast(rpcRequest);
}
// 处理 RpcResponse 结果类型擦除问题
private <T> T handleResponse(RpcResponse rpcResponse, Class<T> type) throws IOException {
Object data = rpcResponse.getData();
if (data != null) {
byte[] dataBytes = OBJECT_MAPPER.writeValueAsBytes(data);
rpcResponse.setData(OBJECT_MAPPER.readValue(dataBytes, rpcResponse.getDataType()));
}
return type.cast(rpcResponse);
}
}
3. 💡 避坑指南:类型擦除问题
为什么必须写 handleRequest 和 handleResponse?
Java 的泛型在编译后会被擦除。当你把 Object[] args 序列化为 JSON 时,JSON 丢失了具体的 Java 类型信息。
当反序列化回来时,Jackson 遇到 {} 会默认将其转换为 LinkedHashMap,而不是你原本的 User 对象!这会导致后续方法反射调用时报类型转换异常。我们必须根据 parameterTypes 进行二次精准转换。
4. 优缺点评估
-
✅ 优点 :数据极其直观、可读性拉满;跨语言的绝对利器。
-
❌ 缺点:报文体积大(带着大量双引号和字段名);解析耗费 CPU;不支持复杂的循环引用;需要费心处理泛型擦除。
四、Kryo 序列化器 (生产力推荐)
1. 实现原理
Kryo 是一款专为 Java 打造的高性能序列化框架,底层大量运用字节码生成技术(ASM),直接绕开了低效的反射机制。
2. 完整实现与线程安全处理
java
public class KryoSerializer implements Serializer {
/**
* ⚠️ Kryo 实例是非线程安全的!
* 必须使用 ThreadLocal 保证每个线程拥有独立的 Kryo 实例。
*/
private static final ThreadLocal<Kryo> KRYO_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
// 关闭强制注册模式,支持动态序列化任意类
kryo.setRegistrationRequired(false);
return kryo;
});
@Override
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);
KRYO_THREAD_LOCAL.get().writeObject(output, obj);
output.close();
return byteArrayOutputStream.toByteArray();
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> classType) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream);
T result = KRYO_THREAD_LOCAL.get().readObject(input, classType);
input.close();
return result;
}
}
3. Kryo 的注册模式争议
-
注册模式 (强管控):kryo.register(User.class, 1)。优点是速度极快、体积极小(用 ID 替代类名);缺点是维护成本极高,所有微服务必须严格对齐 ID。
-
非注册模式 (本项目采用):kryo.setRegistrationRequired(false)。牺牲了一丢丢体积(写入了类名),但换来了开发时的极致便利。
4. 优缺点评估
-
✅ 优点 :性能之王;序列化字节极度紧凑;支持循环引用;无需实现 Serializable。
-
❌ 缺点 :仅限 Java 语言;线程不安全必须加套子;非注册模式下存在一定的反序列化安全风险。
五、Hessian 序列化器
1. 实现原理
由 Caucho 公司开发的轻量级二进制 RPC 协议,Dubbo 早期版本的默认序列化协议就是 Hessian2。
2. 完整实现
java
public class HessianSerializer implements Serializer {
@Override
public <T> byte[] serialize(T object) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
ho.writeObject(object);
ho.flush();
return bos.toByteArray();
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> tClass) throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
HessianInput hi = new HessianInput(bis);
return (T) hi.readObject(tClass);
}
}
3. 优缺点评估
-
✅ 优点:支持跨语言通信(二进制跨平台);体积和性能较好;兼容性强。
-
❌ 缺点:性能逊色于 Kryo;社区活跃度逐渐走低。
六、四种序列化器全维度对比
1. 核心指标对比
|-------------|-------|--------|-------|------------------|
| 序列化器 | 序列化耗时 | 反序列化耗时 | 报文体积 | 综合评价 |
| JDK | 🐢 极慢 | 🐢 极慢 | 🐘 庞大 | Java 原生兜底方案 |
| JSON | 🚶 中等 | 🚶 中等 | 🐖 较大 | 跨语言、好调试 |
| Kryo | 🚀 极快 | 🚀 极快 | 🐁 极小 | 纯 Java 高并发首选 |
| Hessian | 🏃 较快 | 🏃 较快 | 🐇 较小 | 均衡型,支持跨语言 |
2. 特性支持对比
|----------|----------------|----------------|-----------------------|---------|
| 特性维度 | JDK | JSON (Jackson) | Kryo | Hessian |
| 跨语言 | ❌ 否 | ✅ 是 | ❌ 否 | ✅ 是 |
| 明文可读 | ❌ 否 | ✅ 是 | ❌ 否 | ❌ 否 |
| 循环引用 | ✅ 支持 | ❌ 容易栈溢出 | ✅ 支持 | ✅ 支持 |
| 侵入性 | 需 Serializable | 无侵入 | 无侵入 | 无侵入 |
| 线程安全 | ✅ 安全 | ✅ 安全 | ❌ (需 ThreadLocal) | ✅ 安全 |
七、如何选择序列化器?
1. 快速决策树
是否需要跨语言调用?
├─ 是 → 是否需要抓包排错、明文可视?
│ ├─ 是 → 选 JSON
│ └─ 否 → 选 Hessian (或 Protobuf)
└─ 否 → 是否对高并发、低延迟有极致要求?
├─ 是 → 选 Kryo (内部微服务首选)
└─ 否 → 选 JDK (简单测试)
2. 经典落地场景
-
企业内部 Java 微服务群 👉 Kryo。纯粹的性能压榨,省内网带宽,省 CPU。
-
BFF 层 / 对外 Open API 👉 JSON。前端/第三方可以无缝接入,排错成本极低。
-
遗留系统整合 👉 Hessian。历史包袱重,有多语言混合调用的需求。
八、在 RPC 框架中的实战装配
得益于之前设计的 SPI 机制,切换序列化器只需修改一行配置:
1. 配置文件 (application.properties)
# 一键切换底层引擎
rpc.serializer=kryo
2. 核心调用链路
java
// 1. 通过 SPI 工厂动态加载配置的序列化器
Serializer serializer = SerializerFactory.getInstance(rpcConfig.getSerializer());
// 2. 消费者:对象 -> 字节
RpcRequest rpcRequest = RpcRequest.builder()....build();
byte[] bytes = serializer.serialize(rpcRequest);
vertxTcpClient.send(bytes);
// 3. 提供者:字节 -> 对象
byte[] receiveBytes = receive();
RpcRequest request = serializer.deserialize(receiveBytes, RpcRequest.class);
(注意:网络两端必须严格保持配置一致,否则会出现反序列化魔数错误!)
九、面试/实战高频 Q&A
Q1: 面试官:为什么 Kryo 序列化器必须配合 ThreadLocal 使用?
A: Kryo 实例本身不是线程安全的。如果多个线程共享同一个 Kryo 实例,会导致内部状态(如对象引用映射表)互相覆盖,出现数据错乱甚至抛出并发异常。使用 ThreadLocal 保证了"一线程一实例",既避开了使用 synchronized 重量级锁带来的性能损耗,又完美解决了高并发下的线程安全问题。
Q2: 面试官:JSON 序列化器为什么必须特殊处理请求和响应对象?
A: 最大的坑是泛型擦除。由于 RPC 请求体中的参数列表通常被声明为顶层的基类数组(即 java.lang.Object 的数组)。当 Jackson 在反序列化时,因为它在运行时丢失了真实的泛型类型,遇到花括号 {} 就会默认兜底将其转换为 LinkedHashMap!这就导致后续利用反射调用真实方法时,由于类型不匹配(本该是 User 对象,结果传进去了 Map)而报错。因此,我们必须根据附加的 parameterTypes 元数据,手动进行二次的精准类型转换。
Q3: 面试官:不同的序列化器可以混用吗?
A: 绝对不可以。 RPC 通信的消费者(Client)和提供者(Server)必须严格约定并使用相同的序列化协议!如果 Client 用 JSON 序列化,Server 用 Kryo 去反序列化,面对完全不同的字节码魔数和结构,系统会直接抛出解析异常。
Q4: 面试官:为什么 Kryo 的性能和体积都碾压 JDK 原生序列化?
A:
体积更小:JDK 会在字节流中写入海量的类元数据(全限定类名、属性信息等),而 Kryo 采用紧凑的二进制格式,如果是注册模式,甚至直接用一个数字 ID 代替长长的类名。
速度更快 :JDK 序列化底层重度依赖低效的反射(Reflection)机制;而 Kryo 底层大量应用了字节码生成技术(ASM),直接在运行时动态生成操作字节码,直接绕开了反射的性能瓶颈。
十、核心知识点全局总结
-
序列化的本质:它是跨网络、跨进程通信的"翻译官",负责在 Java 内存对象与网络二进制字节流之间进行相互转换。
-
四大流派:
-
JDK:原生自带,简单但性能垫底,仅限 Java。
-
JSON:可读性无敌,跨语言神器,但体积偏大且有泛型擦除坑。
-
Kryo:性能极客首选,体积小速度快,注意线程安全(ThreadLocal 加持)。
-
Hessian:兼顾了二进制的高效与跨语言特性,是 Dubbo 等老牌框架的最爱。
-
-
开发痛点与解法:Kryo 必须解决多线程共享问题;JSON 必须解决泛型类型丢失(LinkedHashMap 转换异常)问题。
十一、企业级实践与避坑建议
-
生产环境绝对主力:Kryo。在纯 Java 的微服务内部网络中,Kryo 带来的 CPU 开销节省和带宽节省是极其可观的,适合高并发、大流量场景。
-
BFF 层或网关调试推荐:JSON。如果你RPC 接口需要被 Python/Go 等异构语言调用,或者正处于联调期需要频繁通过 Wireshark 抓包看报文明文,JSON 是唯一解。
-
版本兼容性红线:如果不得已使用了 JDK 序列化,必须在实体类中写死 serialVersionUID。否则一旦未来给实体类加了一个字段,旧版本消费者发来的请求就会引发 InvalidClassException,导致服务大面积瘫痪。
-
接口收口统一配置 :借助自定义的 SPI 机制,将序列化器的选择权交给统一的配置文件(如 application.properties),做到一处修改,全局生效,严防两端协议不一致的低级事故。
十二、结语
序列化器虽然只是 RPC 框架中的一个底层组件,但它直接决定了整个微服务架构的吞吐量上限(TPS)。