序列化实战
凌晨 2 点,你发现生产环境的 Redis 缓存全过期了,大量请求打到数据库,响应时间从 50ms 飙到 2 秒。
排查后发现:上周发布的那个「无害」的代码改动,改了一个类的字段名。
这是一个真实的序列化踩坑经历。
实战一:对象持久化
基础版:保存单个对象
java
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private int age;
// getters, setters
}
// 保存
public void saveUser(User user, String path) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream(path)))) {
oos.writeObject(user);
}
}
// 读取
public User loadUser(String path) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream(path)))) {
return (User) ois.readObject();
}
}进阶版:保存对象集合(带版本)
java
public class UserStore implements Serializable {
private static final long serialVersionUID = 1L;
private List<User> users;
private long lastModified;
public UserStore() {
this.users = new ArrayList<>();
this.lastModified = System.currentTimeMillis();
}
public void addUser(User user) {
users.add(user);
lastModified = System.currentTimeMillis();
}
}
// 保存整个存储
public void saveStore(UserStore store, String path) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream(path)))) {
oos.writeObject(store);
}
}实战二:Socket 网络传输
序列化最常见的网络应用是 RPC 框架(Dubbo、gRPC 底层)和 RMI。
java
// 服务器端:接收并处理请求
public class RequestHandler {
public void handleClient(Socket client) {
try (
ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(client.getInputStream()));
ObjectOutputStream oos = new ObjectOutputStream(
client.getOutputStream())
) {
Request request = (Request) ois.readObject();
Response response = process(request);
oos.writeObject(response);
} catch (Exception e) {
e.printStackTrace();
}
}
private Response process(Request request) {
// 处理请求
return new Response(true, "处理成功");
}
}
// 请求对象
public class Request implements Serializable {
private static final long serialVersionUID = 1L;
private String action;
private Map<String, Object> params;
}
// 响应对象
public class Response implements Serializable {
private static final long serialVersionUID = 1L;
private boolean success;
private String message;
private Object data;
}实战三:保护敏感数据
java
public class SecureUser implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // 永远不序列化
private transient String sessionToken;
private List<String> permissions; // 序列化了但加密
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(encrypt(permissions)); // 加密权限列表
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
List<String> encrypted = (List<String>) in.readObject();
this.permissions = decrypt(encrypted);
this.sessionToken = null; // 强制设为 null
}
private List<String> encrypt(List<String> data) { /* AES */ }
private List<String> decrypt(List<String> data) { /* AES */ }
}常见问题与解决方案
问题一:反序列化后类型变了
java
// 读取时用了错误的类型
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("data.obj"))) {
User user = (User) ois.readObject(); // ❌ ClassCastException 风险
}
// ✅ 正确做法:先判断类型
Object obj = ois.readObject();
if (obj instanceof User) {
User user = (User) obj;
} else if (obj instanceof List) {
List<User> users = (List<User>) obj;
}问题二:循环引用导致栈溢出
java
public class Node implements Serializable {
private String value;
private Node next; // 循环引用:NodeA.next = NodeB, NodeB.next = NodeA
// JVM 的对象表机制保证了循环引用可以正确序列化
// 但注意:超过深度的循环可能导致 StackOverflowError
}问题三:子类序列化父类未序列化
java
public class Animal { } // 没有实现 Serializable
public class Dog extends Animal implements Serializable {
private String name;
// Animal 的字段不会被序列化!
}解法:让父类也实现 Serializable。
问题四:升级导致反序列化失败
这是最容易踩的坑。我的经验:
- 始终显式声明 serialVersionUID
- 加字段时不要改 UID(新字段会是默认值)
- 删字段或改类型时必须改 UID(旧数据读不了了)
- 字段改名必须改 UID(UID 包含字段名)
┌─────────────────────────────────────────────────────────────────┐
│ 版本升级三原则: │
│ │
│ 增字段:UID 不变,新字段=默认值 │
│ 删字段/改类型/改名字:UID 必须变 │
│ 方法改动:UID 不变(方法不序列化) │
└─────────────────────────────────────────────────────────────────┘总结
序列化实战中的核心原则:
- 持久化:配合
BufferedOutputStream使用,避免频繁系统调用 - 网络传输:请求/响应对象都需要实现
Serializable - 敏感数据:用
transient+writeObject/readObject加密 - 反序列化前做类型检查,避免
ClassCastException - 版本升级时小心 UID 的处理
