序列化协议对比(Java / JSON / ProtoBuf / Hessian)

做分布式系统,序列化是绕不过去的坎。你的对象怎么变成字节流?字节流怎么变回对象?这就是序列化要解决的问题。

我见过太多团队在这上面踩坑:用 Java 序列化做微服务通信,结果带宽跑满、CPU 飙升;或者用了 JSON 做高性能场景,结果 GC 频繁。

今天我们就来把常见的序列化方案讲清楚,帮你在不同场景下做出正确的选择。

一、序列化基础概念

1.1 什么是序列化

序列化就是对象到字节序列的转换:

// 序列化:对象 → 字节
User user = new User("张三", 25);
byte[] bytes = serializer.serialize(user);  // 序列化

// 反序列化:字节 → 对象
User restored = (User) serializer.deserialize(bytes);  // 反序列化

1.2 序列化的应用场景

场景说明
网络通信对象在网络中传输前要序列化
缓存Redis/Memcached 存储对象要序列化
Session 存储分布式 Session 要序列化
RMI/远程调用远程方法调用要序列化参数和返回值

1.3 衡量指标

指标说明
序列化速度序列化耗时
反序列化速度反序列化耗时
序列化大小序列化后的字节数
可读性人类能否直接阅读
跨语言支持能否跨语言解析

二、Java 原生序列化

2.1 使用方式

// 实现 Serializable 接口
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    // getter/setter
}

// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(user);
byte[] bytes = baos.toByteArray();

// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
User restored = (User) ois.readObject();

2.2 优点

  • JDK 内置,不需要引入依赖
  • 实现简单
  • 支持复杂对象图
  • 支持对象循环引用

2.3 ❌ 缺点:性能差

Java 序列化有严重的性能问题:

// Java 序列化的问题:
// 1. 序列化后数据量大(包含类名、方法名、类型信息)
// 2. 序列化速度慢
// 3. 不安全(反序列化漏洞 CVE-2017-10388 等)
// 4. 不能跨语言

对比测试:

序列化 10000 次 User 对象:

Java Serializable:  序列化 50ms    反序列化 80ms    大小 500 bytes
JSON (Jackson):     序列化 20ms    反序列化 15ms    大小 50 bytes
ProtoBuf:          序列化 5ms     反序列化 3ms      大小 20 bytes

2.4 ❌ 安全问题

Java 序列化有反序列化漏洞:

// 反序列化攻击示例
// 攻击者构造恶意序列化数据,执行任意代码
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject();  // 可能执行恶意代码

Spring、Kryo 都受到过反序列化漏洞影响。

⚠️

生产环境中,禁止直接反序列化不可信来源的数据。如果必须,要有白名单校验机制。

三、JSON 序列化

3.1 Jackson 使用

ObjectMapper mapper = new ObjectMapper();

// 序列化
User user = new User("张三", 25);
String json = mapper.writeValueAsString(user);
// {"name":"张三","age":25}

// 反序列化
User restored = mapper.readValue(json, User.class);

3.2 FastJSON 使用

// FastJSON
String json = JSON.toJSONString(user);
User restored = JSON.parseObject(json, User.class);

3.3 优点

  • 可读性强:JSON 文本人类可以直接阅读和调试
  • 跨语言:所有语言都支持 JSON
  • 格式简单:易于理解和维护

3.4 缺点

// JSON 的问题:
// 1. 序列化后比二进制大(字符串格式)
// 2. 序列化速度比二进制慢
// 3. 无法表示复杂 Java 类型(如 LocalDateTime、BigDecimal)
// 4. 缺少类型信息,反序列化时需要类型信息
// 问题示例:LocalDateTime 序列化
LocalDateTime now = LocalDateTime.now();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(now);
// 输出:{"dayOfYear":100,"dayOfWeek":"TUESDAY",...}
// 反序列化回来变成 LinkedHashMap 而不是 LocalDateTime!

四、Protocol Buffers

4.1 proto 文件定义

// user.proto
syntax = "proto3";

package user;

option java_package = "com.example.pb";
option java_outer_classname = "UserProto";

message User {
    string name = 1;
    int32 age = 2;
    repeated string tags = 3;  // repeated 表示列表
}

4.2 生成 Java 代码

# 安装 protoc 编译器
# macOS: brew install protobuf
# Ubuntu: apt install protobuf-compiler

# 生成 Java 代码
protoc --java_out=src/main/java user.proto

生成后的代码:

public final class UserProto {
    public static final class User extends 
        com.google.protobuf.GeneratedMessageV3 implements UserOrBuilder {
        
        private String name_ = "";
        private int age_ = 0;
        private com.google.protobuf.LazyStringArrayList tags_ = 
            com.google.protobuf.LazyStringArrayList.EMPTY;
        
        // getter/setter/Builder...
    }
}

4.3 使用示例

// 构建消息
UserProto.User user = UserProto.User.newBuilder()
    .setName("张三")
    .setAge(25)
    .addTags("Java")
    .addTags("架构")
    .build();

// 序列化
byte[] bytes = user.toByteArray();

// 反序列化
UserProto.User restored = UserProto.User.parseFrom(bytes);

// 访问字段
System.out.println(restored.getName());  // "张三"

4.4 优点

优点说明
体积小使用字段编号而不是字段名,如 1 而不是 "name"
速度快序列化是纯二进制操作
类型安全编译时检查类型
跨语言支持 10+ 语言
向后兼容可以新增字段,不影响旧代码

4.5 ❌ 缺点

  • 需要定义 .proto 文件
  • 需要编译,修改协议要重新生成
  • 学习曲线

五、Hessian

5.1 Hessian 的特点

Hessian 是一种动态的二进制序列化协议,不需要预定义 schema:

// 使用 Hessian2
Hessian2Output out = new Hessian2Output(outputStream);
out.writeObject(user);
out.flush();

Hessian2Input in = new Hessian2Input(inputStream);
User restored = (User) in.readObject();

5.2 Spring 的集成

// Spring Boot 中配置 Hessian
@Bean
public HessianServiceExporter userServiceExporter(UserService userService) {
    HessianServiceExporter exporter = new HessianServiceExporter();
    exporter.setService(userService);
    exporter.setServiceInterface(UserService.class);
    return exporter;
}

5.3 优点

  • 动态序列化,不需要预定义
  • 跨语言(Java、C++、Python、.NET、PHP...)
  • 序列化后体积小
  • 在 Java 生态中广泛使用

5.4 缺点

  • 不如 ProtoBuf 快
  • 类型信息不够丰富
  • 存在反序列化漏洞

六、Kryo

6.1 使用示例

// Kryo 序列化
Kryo kryo = new Kryo();
kryo.register(User.class);

// 序列化
Output output = new Output(outputStream);
kryo.writeObject(output, user);
output.close();

// 反序列化
Input input = new Input(inputStream);
User restored = kryo.readObject(input, User.class);
input.close();

6.2 注册类 ID

// 为了获得最佳性能,建议注册类 ID
kryo.register(User.class, 1);
kryo.register(Order.class, 2);
kryo.register(Product.class, 3);

6.3 Kryo 序列化池

频繁创建 Kryo 实例有开销,可以使用对象池:

// 使用 KryoFactory 和 KryoPool
KryoFactory factory = new DefaultKryoFactory();
KryoPool pool = new KryoPool(factory, 10);

User user = pool.run(kryo -> {
    Output output = new Output(1024);
    kryo.writeObject(output, user);
    output.close();
    return output.toBytes();
});

七、性能对比

7.1 综合对比表

方案序列化速度反序列化速度体积跨语言学习成本
Java Serializable
JSON
ProtoBuf
Hessian
Kryo

7.2 选型决策

// 选型指南:

// 1. HTTP API,前后端分离 → JSON
// 理由:可读性强,调试方便,前后端都支持

// 2. 微服务内部通信 → ProtoBuf / Kryo
// 理由:性能高,体积小

// 3. Java to Java,缓存 → Kryo
// 理由:性能最高,Java 专用

// 4. 跨语言 RPC → ProtoBuf / Hessian
// 理由:支持多语言

// 5. Spring Remoting → Hessian
// 理由:Spring 官方集成

八、生产避坑

8.1 ❌ 错误示范:使用 Java 序列化做微服务通信

// ❌ 非常慢的代码
@RestController
public class BadController {
    @PostMapping("/user")
    public byte[] saveUser(@RequestBody User user) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(user);  // Java 序列化,慢!
        return baos.toByteArray();
    }
}

正确做法:用 JSON 或 ProtoBuf:

// ✅ 推荐:使用 JSON
@RestController
public class GoodController {
    @Autowired
    private ObjectMapper mapper;
    
    @PostMapping("/user")
    public User saveUser(@RequestBody User user) {
        return userService.save(user);
    }
}

8.2 ❌ 错误示范:没有处理序列化版本变化

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    // 后来加了新字段,但没有更新 serialVersionUID
    private String email;  // 新增
}

正确做法:显式声明 serialVersionUID,并在类结构变化时更新:

public class User implements Serializable {
    private static final long serialVersionUID = 2L;  // 改了结构要更新
}

8.3 ❌ 错误示范:序列化敏感数据未加密

// ❌ 不安全
byte[] bytes = serializer.serialize(user);
// 直接存到 Redis 或通过网络传输

正确做法:对敏感数据加密或使用 TLS:

// 或者用专门的加密序列化库

九、面试追问链

第一层:基础概念

面试官问:"什么是序列化?为什么需要序列化?"

序列化是将对象转换为字节流的过程,用于网络传输、缓存存储等。反序列化是反向过程。不同系统/语言之间通信需要序列化来编码和解码数据。

第二层:常见方案

面试官问:"你知道哪些序列化方案?"

Java Serializable(慢、重、不安全)、JSON(可读、跨语言、慢)、ProtoBuf(快、小、跨语言、需要 proto 文件)、Hessian(动态、跨语言、较快)、Kryo(Java 最快、不跨语言)。

第三层:选型决策

面试官追问:"微服务之间通信用什么序列化方案?"

如果是 HTTP JSON,通用性好;如果追求性能,用 ProtoBuf 或 Kryo。还要考虑团队能力、是否需要跨语言等因素。

第四层:安全考虑

面试官追问:"Java 序列化有什么安全问题?"

Java 反序列化漏洞,攻击者可以构造恶意序列化数据来执行任意代码。生产中不要直接反序列化不可信数据,或者使用黑名单/白名单机制。

【学习小结】

  • Java 序列化:慢、大、不安全,尽量不用
  • JSON:可读、跨语言、适中性能
  • ProtoBuf:快、小、跨语言、需要预定义
  • Hessian:动态序列化、跨语言
  • Kryo:Java 最快、不跨语言
  • 选型看场景:HTTP API 用 JSON,内部通信用 ProtoBuf/Kryo