第10章:序列化(Serialization)
一、导读
1.1 本章学习目标
第10章探讨 Java 序列化系统的最佳实践。序列化是将对象转换为字节流的过程,反序列化则是将字节流恢复为对象的过程。本章通过5条经验法则,帮助你正确使用序列化,避免常见的陷阱。
本章的核心学习目标包括:
- 理解序列化的风险:掌握序列化带来的安全和兼容性问题。
- 学会实现 Serializable 的正确方式:掌握
serialVersionUID的作用。 - 掌握 readObject 的防御性编程:保护对象的不变量。
- 理解序列化代理模式:用序列化代理替代默认的序列化形式。
- 了解序列化的替代方案:掌握 JSON、Protocol Buffers 等替代方案。
1.2 为什么第十章如此重要
序列化看似简单,实则暗藏危机。不正确的序列化实现会导致安全漏洞、版本兼容性问题、性能问题。随着分布式系统和微服务架构的普及,序列化的重要性愈发凸显。
1.3 核心问题引导
为什么实现 Serializable 需要慎重考虑?
serialVersionUID 的作用是什么?不指定会怎样?
如何在反序列化时保护对象的不变量?
什么是序列化代理模式?它解决了什么问题?
有哪些替代 Java 原生序列化的方案?
二、核心概念详解
10.1 谨慎地实现 Serializable(Item 85)
10.1.1 实现 Serializable 的代价
降低灵活性:一旦发布,类的序列化形式就成为 API 的一部分。
增加测试负担:需要确保序列化和反序列化的正确性。
增加安全攻击面:反序列化可能执行恶意代码。
版本兼容问题:类的变化可能导致旧版本的序列化数据无法反序列化。
// 如果不想被序列化,不要实现 Serializable
public class UserService {
// 服务类通常不需要序列化
}
// 如果必须实现,考虑使用序列化代理
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
}10.1.2 serialVersionUID 的作用
// 不指定 serialVersionUID:编译器自动生成
// 类的任何变化都可能导致 serialVersionUID 变化
// 旧版本的序列化数据无法反序列化
// 指定 serialVersionUID:保持版本兼容
public class User implements Serializable {
private static final long serialVersionUID = 42L;
private String name;
private int age;
// 新增字段不影响旧版本的反序列化
private String email; // 旧版本数据中 email 为 null
}10.2 考虑使用序列化代理(Item 90)
10.2.1 序列化代理模式
public final class Period implements Serializable {
private final Date start;
private final Date end;
// 序列化代理
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = new Date(p.start.getTime());
this.end = new Date(p.end.getTime());
}
private Object readResolve() {
return new Period(start, end); // 调用公共构造器
}
}
private Object writeReplace() {
return new SerializationProxy(this);
}
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
}序列化代理的优势:
安全性:通过 readResolve 保证不变量。
灵活性:可以改变序列化形式而不影响反序列化。
类型安全:反序列化的对象类型由代理控制。
10.3 考虑用替代方案(Item 90 补充)
Java 原生序列化有很多问题,现代应用通常使用替代方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| JSON | 可读性好,跨语言 | 不支持复杂类型 |
| Protocol Buffers | 高效,跨语言,有 schema | 需要定义 proto 文件 |
| Avro | 高效,支持 schema 演化 | 需要 schema 注册表 |
| Kryo | 高效,Java 专用 | 不支持跨语言 |
// JSON 序列化(Jackson)
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);
User user = mapper.readValue(json, User.class);
// Protocol Buffers
// 定义 user.proto
// message User {
// string name = 1;
// int32 age = 2;
// }
UserProto.User user = UserProto.User.newBuilder()
.setName("Alice")
.setAge(30)
.build();
byte[] bytes = user.toByteArray();
UserProto.User parsed = UserProto.User.parseFrom(bytes);三、实战案例
10.1 安全的序列化实现
public final class User implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final int age;
private final transient String password; // 不序列化
public User(String name, int age, String password) {
this.name = Objects.requireNonNull(name);
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
this.age = age;
this.password = Objects.requireNonNull(password);
}
// 序列化代理
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
SerializationProxy(User user) {
this.name = user.name;
this.age = user.age;
}
private Object readResolve() {
return new User(name, age, "default");
}
}
private Object writeReplace() {
return new SerializationProxy(this);
}
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
// getter
public String getName() { return name; }
public int getAge() { return age; }
}10.2 JSON 序列化最佳实践
public class JsonSerialization {
private static final ObjectMapper MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 序列化
public static String toJson(Object obj) {
try {
return MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON serialization failed", e);
}
}
// 反序列化
public static <T> T fromJson(String json, Class<T> type) {
try {
return MAPPER.readValue(json, type);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON deserialization failed", e);
}
}
// 类型安全的反序列化
public static <T> T fromJson(String json, TypeReference<T> typeRef) {
try {
return MAPPER.readValue(json, typeRef);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON deserialization failed", e);
}
}
}
// 使用
User user = new User("Alice", 30, "secret");
String json = JsonSerialization.toJson(user);
User parsed = JsonSerialization.fromJson(json, User.class);四、本章小结
第10章系统讲解了序列化的核心知识:
谨慎实现 Serializable:序列化有安全、兼容性和性能代价。
使用 serialVersionUID:保持版本兼容性。
使用序列化代理:保证反序列化的安全性。
考虑替代方案:JSON、Protocol Buffers 等通常优于 Java 原生序列化。
transient 字段:敏感数据不应该被序列化。
序列化是 Java 中最容易出问题的特性之一。记住:如果可以避免,就不要使用 Java 原生序列化。
五、思考与练习
思考题
Java 原生序列化有哪些安全问题?
readResolve 方法的作用是什么?
为什么 transient 字段不会被序列化?
JSON 和 Protocol Buffers 各有什么优缺点?
如何保证序列化数据的版本兼容性?
编程练习
实现安全的序列化:使用序列化代理模式实现一个不可变类的序列化。
JSON 序列化:使用 Jackson 实现对象的 JSON 序列化和反序列化。
版本兼容:设计一个支持版本演化的序列化方案。