资讯动态

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

发布时间:2026/10/14 15:36:38 来源:尧图企业网站定制
序列化是分布式系统的基石。从 Java 原生序列化到 Protobuf性能差距可达数十倍。本文拆解各序列化方案的底层逻辑和选型指南。一、什么是序列化序列化 (Serialization): 对象 → 字节序列 → 存储/传输 反序列化 (Deserialization): 字节序列 → 对象 ┌───────────┐ 序列化 ┌───────────┐ 反序列化 ┌───────────┐ │ Java对象 │──────────▶│ 字节流 │──────────▶│ Java对象 │ │ (内存) │ │ (网络/磁盘) │ │ (内存) │ └───────────┘ └───────────┘ └───────────┘ 应用场景: ├── RPC 通信 (Dubbo/gRPC) ├── 缓存存储 (Redis Memcached) ├── 消息队列 (Kafka/RocketMQ) ├── 持久化 (数据库 BLOB) └── 深拷贝 (对象复制)二、Java 原生序列化Serializable// 实现 Serializable 接口publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID1L;privateStringname;privateintage;// transient: 不参与序列化privatetransientStringpassword;// 自定义序列化逻辑privatevoidwriteObject(ObjectOutputStreamoos)throwsIOException{oos.defaultWriteObject();oos.writeObject(encrypt(password));// 加密后写入}privatevoidreadObject(ObjectInputStreamois)throwsIOException,ClassNotFoundException{ois.defaultReadObject();this.passworddecrypt((String)ois.readObject());}privateStringencrypt(Strings){returnBase64.encode(s);}privateStringdecrypt(Strings){returnBase64.decode(s);}}// 使用ByteArrayOutputStreambosnewByteArrayOutputStream();ObjectOutputStreamoosnewObjectOutputStream(bos);oos.writeObject(user);byte[]bytesbos.toByteArray();// 序列化结果ObjectInputStreamoisnewObjectInputStream(newByteArrayInputStream(bytes));Useruser2(User)ois.readObject();// 反序列化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 序列化// Jackson (最常用)ObjectMappermappernewObjectMapper();// 序列化Stringjsonmapper.writeValueAsString(user);// {name:Tom,age:20}// 反序列化Userusermapper.readValue(json,User.class);// 高级配置mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,false);mapper.setDateFormat(newSimpleDateFormat(yyyy-MM-dd HH:mm:ss));// GsonGsongsonnewGsonBuilder().setDateFormat(yyyy-MM-dd).create();Stringjsongson.toJson(user);Userusergson.fromJson(json,User.class);// Fastjson2 (高性能)StringjsonJSON.toJSONString(user);UseruserJSON.parseObject(json,User.class);JSON 序列化特点: 优点: ├── 可读性强 (文本格式) ├── 跨语言通用 ├── 生态成熟 (浏览器/API) └── 调试方便 缺点: ├── 体积较大 (字段名引号括号) ├── 性能中等 (文本解析) ├── 无类型信息 (反序列化需指定类型) └── 数字精度问题 (long → JS number 丢失)四、ProtobufProtocol Buffers1. 定义 Proto 文件// user.proto syntax proto3; message User { string name 1; int32 age 2; string email 3; repeated string tags 4; }2. 生成 Java 类protoc--java_out./src user.proto3. 使用// 构建对象UserProto.UseruserUserProto.User.newBuilder().setName(Tom).setAge(20).setEmail(tomexample.com).addTags(java).addTags(backend).build();// 序列化byte[]bytesuser.toByteArray();// 反序列化UserProto.Useruser2UserProto.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_number1, typestring → Tag (1 3) | 2 0x0A 3. 字段省略 proto3 默认值字段不编码: age0 → 不写入字节流 (0 bytes) 4. 二进制格式 无文本解析开销 对比同一个 User: Java序列化: ~150 bytes (含类名、字段名等元数据) JSON: ~45 bytes (含字段名、引号) Protobuf: ~15 bytes (仅字段编号值)Protobuf 编码示例: message: { nameTom, age20, email } 字段1 (name, type2/string): Tag0x0A, Len3, ValueTom 字段2 (age, type0/varint): Tag0x10, Value20 字段3 (email, 默认值): 省略不编码 字节流: 0A 03 54 6F 6D 10 14 总计: 7 bytes! JSON 等价: {name:Tom,age:20} → 23 bytes五、各序列化方案对比序列化方案体积性能跨语言可读性类型安全典型场景Java Serializable最大最慢仅Java不可读强不推荐JSON (Jackson)中中全平台可读弱REST APIProtobuf最小最快全平台不可读强gRPC/RPCKryo小快仅JVM不可读强内部RPCHessian较小较快Java/C不可读强DubboAvro小快全平台不可读强大数据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 不一致// 发送端publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID1L;privateStringname;}// 接收端修改了类publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID2L;// 改了!privateStringname;privateintage;// 新增字段}// 反序列化报错: java.io.InvalidClassException - local class incompatible2. 反序列化漏洞// 致命: Java 原生反序列化可执行任意代码ObjectInputStreamoisnewObjectInputStream(inputStream);Objectobjois.readObject();// 危险! 可触发任意命令执行// 防御:// 1. 使用白名单反序列化 (ObjectInputFilter, JDK 9)ObjectInputStreamoisnewObjectInputStream(inputStream);ois.setObjectInputFilter(filterInfo-filterInfo.serialClass()User.class?ObjectInputFilter.Status.ALLOWED:ObjectInputFilter.Status.REJECTED);// 2. 避免使用 Java 原生序列化改用 JSON/Protobuf3. JSON 中的 Long 精度问题// 后端返回 Long 类型{id:123456789012345678}// ↑ JS 中超过 Number.MAX_SAFE_INTEGER// JS 接收:// id 123456789012345680 (末两位丢失!)// 解决方案:// 1. 后端 Long 转 String 返回JsonSerialize(usingToStringSerializer.class)privateLongid;// 2. 全局配置 (Jackson)objectMapper.configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS,true);4. 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 不灵活Q2Protobuf 比 JSON 快多少体积上 Protobuf 比 JSON 小 3-5 倍字段编号替代字段名 Varint 编码。性能上 Protobuf 比 JSON 快 3-10 倍二进制解析 vs 文本解析。Q3Kryo 和 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 (可读性优先)序列化的选型核心是平衡体积、性能、跨语言、可读性四个维度。没有银弹只有最适合场景的选择。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑