Java 序列化到底怎么选?从 Serializable 到 Protobuf 的演进与选型

序列化是分布式系统的基石。从 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 原生序列化?

  1. 性能差(反射 + 大量元数据)
  2. 体积大(含类名、字段名、版本号)
  3. 安全漏洞(反序列化 RCE)
  4. 跨语言不兼容
  5. API 不灵活

Q2:Protobuf 比 JSON 快多少?

体积上 Protobuf 比 JSON 小 3-5 倍(字段编号替代字段名 + Varint 编码)。性能上 Protobuf 比 JSON 快 3-10 倍(二进制解析 vs 文本解析)。

Q3:Kryo 和 Protobuf 怎么选?

  • Kryo: 仅 Java 场景,无需写 proto 文件,API 更简单
  • Protobuf: 跨语言场景,需要强类型定义,性能极致

Q4:序列化框架需要关注哪些指标?

  1. 序列化体积(影响网络带宽和存储)
  2. 序列化/反序列化耗时(影响吞吐量)
  3. 跨语言支持(影响架构选型)
  4. 向后兼容性(影响版本演进)
  5. 安全性(反序列化漏洞)

九、总结

复制代码
序列化演进链路:

  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 (可读性优先)

序列化的选型核心是平衡 体积、性能、跨语言、可读性 四个维度。没有银弹,只有最适合场景的选择。

相关推荐
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 登录发送验证码的测试验证与经验复盘
java·开发语言·交友
夕除1 小时前
redis--主从还是集群,内存
java·spring·mybatis
百数平台1 小时前
低代码安全技术实践:表单 / 流程 / 报表三级权限体系设计解析
java·大数据·数据库
小白学鸿蒙1 小时前
OpenHarmony 预置应用配置详解:/system/etc/app/ 下的 5 个 JSON
json·openharmony·预置应用
白露与泡影1 小时前
Spring Boot 实现验证码:Hutool-captcha、AJ-Captcha、EasyCaptcha 到底怎么选?
java·spring boot·后端
马优晨1 小时前
Java中的dto是什么
java·开发语言·java中的dto是什么·java的dto是什么·dto在java中是什么
小蒜学长1 小时前
基于Springboot+Vue的高校教师教学业绩综合管理系统的设计与开发(代码+数据库+LW)
java·数据库·spring boot·后端·教学业绩管理·三级权限
Mikko71 小时前
Spring Boot 3.5.16 自带的 Tomcat 10.1.55 中了哪些 CVE?其中一条是修上一个漏洞时带进来的
java·spring boot·后端·安全·tomcat
十年Java程序媛1 小时前
Java 内部类全解|成员 / 静态 / 匿名内部类,理清和 Lambda 的区别
java·spring boot·面试