图 1:本文封面,同一个对象的三种编码形态。
同批的面试真题讲了 Java 原生序列化的机制与缺陷:能跑,但安全、兼容、体积三座大山让新系统不该再选它。那换掉它之后选什么?这篇把视角从「一个机制讲透」换成「一张坐标系做选型」:常见方案在体积、速度、跨语言、Schema 演进四个维度上各站在哪,不同业务链路该怎么落子。
先看清三条路线:文本、二进制、IDL。
所有序列化方案大体分三派。文本派以 JSON 为代表:人可读、自描述,字段名直接写在数据里。二进制派以 Kryo 为代表:把对象图编码成紧凑字节,类名、字段名尽量省掉或压缩成数字 ID。IDL 派以 Protobuf 为代表:先用 proto 文件定义 Schema、给每个字段分配编号,数据里只存编号和值,编解码靠生成的代码。三派的分歧本质上是同一个问题——元数据(这个字段叫什么、是什么类型)放在哪里:放在数据里、干脆省掉、还是放到两端的代码里。
编码本身差多少:同一个对象跑一遍。
用 Node.js 22.15.0 手工编码同一个 User{id:1, name:"A"} 做对比(node encoding-size.js):
const user = { id: 1, name: "A" };
// 1) JSON 文本
const json = Buffer.from(JSON.stringify(user), "utf8");
console.log(`JSON: ${JSON.stringify(user)} → ${json.length} 字节`);
// 2) Protobuf wire format(手工编码 message User { int32 id = 1; string name = 2; })
// 字段 1 varint: tag = (1<<3)|0 = 0x08, 值 1 → 0x01
// 字段 2 LEN: tag = (2<<3)|2 = 0x12, 长度 1, "A" = 0x41
const proto = Buffer.from([0x08, 0x01, 0x12, 0x01, 0x41]);
console.log(`Protobuf: ${proto.toString("hex").toUpperCase().match(/../g).join(" ")} → ${proto.length} 字节`);
// 3) 手工 varint 编码,对照官方文档示例:150 → 96 01
function varint(n) {
const out = [];
do {
let b = n % 128;
n = Math.floor(n / 128);
if (n > 0) b |= 0x80;
out.push(b);
} while (n > 0);
return Buffer.from(out);
}
console.log(`varint(150) = ${varint(150).toString("hex").toUpperCase().match(/../g).join(" ")}`);
console.log(`varint(1) = ${varint(1).toString("hex")}(小整数只占 1 字节)`);
实测输出:
JSON: {"id":1,"name":"A"} → 19 字节
Protobuf: 08 01 12 01 41 → 5 字节
varint(150) = 96 01
varint(1) = 01(小整数只占 1 字节)
19 对 5,差距来自两处:JSON 把字段名和引号、括号都写进了数据;Protobuf 只存字段编号和值,整数还用 varint 变长编码——每个字节的最高位是「还有后续」的标记,小整数只占一个字节。官方编码文档里 150 编码为 96 01 的示例,我们的手工实现逐字节对上了。要说明的是,这是同一个小对象上的体积形态对比,不是严谨基准测试;方案之间真正的差距在大对象和高并发下会进一步拉开。
三个方案的画像。
JSON 的优势是可读性与生态:任何语言、任何抓包和调试工具都认识它,对外 API、配置文件、日志几乎默认选它。代价是体积大、解析慢,而且 Schema 弱——字段叫什么、该是什么类型全靠两端约定,接口悄悄改字段只能靠评审和测试兜底。
Kryo 是 Java 系的二进制方案,官方定位就是「高速、小体积」的对象图序列化框架:类注册后用数字 ID 代替类名,输出远比原生序列化紧凑,编解码也快得多,适合 Java 内部链路——RPC 调用、缓存里存对象。短板是跨语言基本没戏;反序列化同样有安全面,Kryo 从 5.x 起默认要求类注册,正是把「能还原出什么类」收口到白名单里。
Protobuf 为跨语言与长期演进而生:字段编号代替字段名,每个字段带 wire type,老版本解析器遇到不认识的新字段能按类型安全跳过——这就是「加字段不破坏旧客户端」的机制基础。多语言代码生成让前端、后端、跨团队共用同一份 proto 文件。代价是要维护 IDL 与代码生成流程,数据不可读,调试得靠工具解码。
图 2:四种方案在五个维度上的选型矩阵。
选型只问三个问题。
第一,数据要给人看吗?要——配置文件、开放 API、需要肉眼排查的链路,选 JSON。第二,链路跨语言吗?纯 Java 内部且追性能,选 Kryo;跨语言服务通信,选 Protobuf。第三,数据活多久、演不演进?写一次读一次的 transient 消息,Schema 弱也能忍;要落库存上几年、字段必然增删的,Protobuf 的编号兼容机制值回票价。三个问题答完,矩阵里通常只剩一个格子。顺带一提,同批真题讲的 Java 原生序列化在这张坐标系里的位置是「仅 Java、兼容性差、体积大」,这就是它被挤出选型清单的原因。
你负责的系统里,对外接口和内部缓存分别用了哪种序列化方案?当时最看重的维度是哪一个?欢迎评论区聊聊。