序列化是分布式系统的基石。从 Java 原生序列化到 Protobuf,性能差距可达数十倍。本文拆解各序列化方案的底层逻辑和选型指南。
一、什么是序列化?
序列化 (Serialization):
对象 → 字节序列 → 存储/传输
反序列化 (Deserialization):
字节序列 → 对象
┌───────────┐ 序列化 ┌───────────┐ 反序列化 ┌───────────┐
│ Java对象 │──────────▶│ 字节流 │──────────▶│ Java对象 │
│ (内存) │ │ (网络/磁盘) │ │ (内存) │
└───────────┘ └───────────┘ └───────────┘
应用场景:
├── RPC 通信 (Dubbo/gRPC)
├── 缓存存储 (Redis Memcached)
├── 消息队列 (Kafka/RocketMQ)
├── 持久化 (数据库 BLOB)
└── 深拷贝 (对象复制)
二、Java 原生序列化(Serializable)
java
// 实现 Serializable 接口
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient: 不参与序列化
private transient String password;
// 自定义序列化逻辑
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
oos.writeObject(encrypt(password)); // 加密后写入
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.password = decrypt((String) ois.readObject());
}
private String encrypt(String s) { return Base64.encode(s); }
private String decrypt(String s) { return Base64.decode(s); }
}
// 使用
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(user);
byte[] bytes = bos.toByteArray(); // 序列化结果
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
User user2 = (User) ois.readObject(); // 反序列化
Java 序列化的致命缺陷
java
// 1. 体积大 --- 包含类信息、字段名、类描述等大量元数据
// 序列化一个 {"name":"Tom","age":20} 的对象:
// Java序列化: ~150 bytes (含类名、字段名、serialVersionUID等)
// JSON: ~25 bytes
// Protobuf: ~10 bytes
// 2. 性能差 --- 大量反射调用
// Java序列化 vs JSON vs Protobuf (相对耗时):
// Java: 10x
// JSON: 3x
// Protobuf: 1x
// 3. 安全漏洞 --- 反序列化可执行任意代码
// 致命漏洞: Apache Commons Collections 反序列化漏洞
// 攻击者构造恶意字节流 → readObject 时执行任意命令
// 4. 跨语言不兼容 --- 只有 Java 能反序列化
// 5. API 不友好 --- 无法只序列化部分字段
Java 序列化字节布局:
┌─────────────────────────────────────────────────┐
│ 魔数 (STREAM_MAGIC) 2 bytes │
│ 版本 (STREAM_VERSION) 2 bytes │
│ TC_OBJECT 标志 1 byte │
│ 类描述 (TC_CLASSDESC) │
│ ├── 类名长度 2 bytes │
│ ├── 类名 "com.example.User" ~20 bytes │
│ ├── serialVersionUID 8 bytes │
│ ├── 标志位 1 byte │
│ ├── 字段数量 2 bytes │
│ └── 字段描述 (字段名+类型) ~50 bytes │
│ 字段值 │
│ ├── "Tom" ~8 bytes │
│ └── 20 4 bytes │
│ 结束标志 │
└─────────────────────────────────────────────────┘
总计: ~100+ bytes (而实际数据只有 8 bytes)
三、JSON 序列化
java
// Jackson (最常用)
ObjectMapper mapper = new ObjectMapper();
// 序列化
String json = mapper.writeValueAsString(user);
// {"name":"Tom","age":20}
// 反序列化
User user = mapper.readValue(json, User.class);
// 高级配置
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
java
// Gson
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd")
.create();
String json = gson.toJson(user);
User user = gson.fromJson(json, User.class);
java
// Fastjson2 (高性能)
String json = JSON.toJSONString(user);
User user = JSON.parseObject(json, User.class);
JSON 序列化特点:
优点:
├── 可读性强 (文本格式)
├── 跨语言通用
├── 生态成熟 (浏览器/API)
└── 调试方便
缺点:
├── 体积较大 (字段名+引号+括号)
├── 性能中等 (文本解析)
├── 无类型信息 (反序列化需指定类型)
└── 数字精度问题 (long → JS number 丢失)
四、Protobuf(Protocol Buffers)
1. 定义 Proto 文件
protobuf
// user.proto
syntax = "proto3";
message User {
string name = 1;
int32 age = 2;
string email = 3;
repeated string tags = 4;
}
2. 生成 Java 类
bash
protoc --java_out=./src user.proto
3. 使用
java
// 构建对象
UserProto.User user = UserProto.User.newBuilder()
.setName("Tom")
.setAge(20)
.setEmail("tom@example.com")
.addTags("java")
.addTags("backend")
.build();
// 序列化
byte[] bytes = user.toByteArray();
// 反序列化
UserProto.User user2 = UserProto.User.parseFrom(bytes);
4. Protobuf 为什么快?
Protobuf 编码原理:
1. Varint 编码 (变长整数)
小数字用更少字节:
1 → 0x01 (1 byte)
300 → 0xAC 0x02 (2 bytes)
而不是固定 4 bytes
2. 字段用 Tag (field_number + wire_type) 标识
不存储字段名,用数字编号代替:
field_number=1, type=string → Tag = (1 << 3) | 2 = 0x0A
3. 字段省略
proto3 默认值字段不编码:
age=0 → 不写入字节流 (0 bytes)
4. 二进制格式
无文本解析开销
对比同一个 User:
Java序列化: ~150 bytes (含类名、字段名等元数据)
JSON: ~45 bytes (含字段名、引号)
Protobuf: ~15 bytes (仅字段编号+值)
Protobuf 编码示例:
message: { name="Tom", age=20, email="" }
字段1 (name, type=2/string): Tag=0x0A, Len=3, Value="Tom"
字段2 (age, type=0/varint): Tag=0x10, Value=20
字段3 (email, 默认值): 省略不编码
字节流: 0A 03 54 6F 6D 10 14
总计: 7 bytes!
JSON 等价: {"name":"Tom","age":20} → 23 bytes
五、各序列化方案对比
| 序列化方案 | 体积 | 性能 | 跨语言 | 可读性 | 类型安全 | 典型场景 |
|---|---|---|---|---|---|---|
| Java Serializable | 最大 | 最慢 | 仅Java | 不可读 | 强 | 不推荐 |
| JSON (Jackson) | 中 | 中 | 全平台 | 可读 | 弱 | REST API |
| Protobuf | 最小 | 最快 | 全平台 | 不可读 | 强 | gRPC/RPC |
| Kryo | 小 | 快 | 仅JVM | 不可读 | 强 | 内部RPC |
| Hessian | 较小 | 较快 | Java/C++ | 不可读 | 强 | Dubbo |
| Avro | 小 | 快 | 全平台 | 不可读 | 强 | 大数据 |
| MessagePack | 小 | 快 | 全平台 | 不可读 | 中 | 消息队列 |
性能基准测试(相对值)
序列化性能对比 (以 Protobuf 为基准):
体积 (越小越好):
Protobuf ██ 1x
Kryo ██▎ 1.2x
MessagePack █▌ 1.5x
JSON ████ 3x
Hessian █████ 4x
Java ████████████ 10x+
耗时 (越小越好):
Protobuf ██ 1x
Kryo ██▎ 1.3x
MessagePack ██▌ 1.5x
JSON ████ 3x
Hessian ████▌ 3.5x
Java ██████████ 10x
六、选型指南
序列化选型决策树:
需要跨语言?
├── 是 → 需要人类可读?
│ ├── 是 → JSON (Jackson/Gson)
│ │ 适用: REST API, 配置文件, 调试
│ └── 否 → 对性能要求极高?
│ ├── 是 → Protobuf
│ │ 适用: gRPC, 微服务内部通信
│ └── 否 → MessagePack/Avro
│ 适用: 消息队列, 大数据
│
└── 否 (仅 Java)
├── 性能敏感? → Kryo
│ 适用: 内部 RPC, 缓存
└── 兼容旧系统? → Hessian
适用: Dubbo 默认序列化
特殊场景:
大数据: Avro (Schema 内嵌, Spark/Kafka 原生支持)
极致体积: Protobuf (字段编号替代字段名)
调试友好: JSON (文本可读)
安全敏感: 避免 Java Serializable (反序列化漏洞)
七、实战:序列化中的坑
1. serialVersionUID 不一致
java
// 发送端
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
}
// 接收端修改了类
public class User implements Serializable {
private static final long serialVersionUID = 2L; // 改了!
private String name;
private int age; // 新增字段
}
// 反序列化报错: java.io.InvalidClassException - local class incompatible
2. 反序列化漏洞
java
// 致命: Java 原生反序列化可执行任意代码
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject(); // 危险! 可触发任意命令执行
// 防御:
// 1. 使用白名单反序列化 (ObjectInputFilter, JDK 9+)
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filterInfo ->
filterInfo.serialClass() == User.class
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED
);
// 2. 避免使用 Java 原生序列化,改用 JSON/Protobuf
3. JSON 中的 Long 精度问题
java
// 后端返回 Long 类型
{"id": 123456789012345678}
// ↑ JS 中超过 Number.MAX_SAFE_INTEGER
// JS 接收:
// id = 123456789012345680 (末两位丢失!)
// 解决方案:
// 1. 后端 Long 转 String 返回
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
// 2. 全局配置 (Jackson)
objectMapper.configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS, true);
4. Protobuf 兼容性
protobuf
// 向后兼容原则:
// 1. 字段编号一旦使用不能修改
// 2. 删除字段要保留 reserved
// 3. 新增字段用新的编号
message User {
reserved 3; // 字段3已删除,保留编号避免复用
string name = 1;
int32 age = 2;
string email = 4; // 新增字段用编号4
// int32 status = 3; // ❌ 错误: 不能复用已删除的编号
}
八、常见面试追问
Q1:为什么不推荐使用 Java 原生序列化?
- 性能差(反射 + 大量元数据)
- 体积大(含类名、字段名、版本号)
- 安全漏洞(反序列化 RCE)
- 跨语言不兼容
- API 不灵活
Q2:Protobuf 比 JSON 快多少?
体积上 Protobuf 比 JSON 小 3-5 倍(字段编号替代字段名 + Varint 编码)。性能上 Protobuf 比 JSON 快 3-10 倍(二进制解析 vs 文本解析)。
Q3:Kryo 和 Protobuf 怎么选?
- Kryo: 仅 Java 场景,无需写 proto 文件,API 更简单
- Protobuf: 跨语言场景,需要强类型定义,性能极致
Q4:序列化框架需要关注哪些指标?
- 序列化体积(影响网络带宽和存储)
- 序列化/反序列化耗时(影响吞吐量)
- 跨语言支持(影响架构选型)
- 向后兼容性(影响版本演进)
- 安全性(反序列化漏洞)
九、总结
序列化演进链路:
Java Serializable (慢、大、不安全、仅Java)
↓ 追求可读性
JSON (Jackson/Gson/Fastjson)
↓ 追求性能
Protobuf (二进制、跨语言、极致体积和速度)
↓ 内部场景
Kryo/Hessian (JVM 内高效序列化)
选型建议:
REST API: JSON (Jackson)
gRPC/RPC: Protobuf
内部通信: Kryo/Protobuf
大数据: Avro
消息队列: Protobuf/MessagePack
缓存: Kryo/JSON (可读性优先)
序列化的选型核心是平衡 体积、性能、跨语言、可读性 四个维度。没有银弹,只有最适合场景的选择。