Skip to content

序列化实战

凌晨 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

问题四:升级导致反序列化失败

这是最容易踩的坑。我的经验:

  1. 始终显式声明 serialVersionUID
  2. 加字段时不要改 UID(新字段会是默认值)
  3. 删字段或改类型时必须改 UID(旧数据读不了了)
  4. 字段改名必须改 UID(UID 包含字段名)
┌─────────────────────────────────────────────────────────────────┐
│  版本升级三原则:                                                 │
│                                                                 │
│  增字段:UID 不变,新字段=默认值                                  │
│  删字段/改类型/改名字:UID 必须变                                 │
│  方法改动:UID 不变(方法不序列化)                                │
└─────────────────────────────────────────────────────────────────┘

总结

序列化实战中的核心原则:

  • 持久化:配合 BufferedOutputStream 使用,避免频繁系统调用
  • 网络传输:请求/响应对象都需要实现 Serializable
  • 敏感数据:用 transient + writeObject / readObject 加密
  • 反序列化前做类型检查,避免 ClassCastException
  • 版本升级时小心 UID 的处理

基于 VitePress 构建