前置知识: Java

Java序列化

7 minIntermediate2026/6/14

序列化与反序列化

概述

序列化是将 Java 对象转换为字节流的过程,反序列化是将字节流恢复为 Java 对象的过程。序列化的核心用途是让对象可以脱离内存存在:存储到文件、通过网络传输、保存到数据库。当你需要把一个对象从一台机器发送到另一台机器,或者把对象持久化保存时,就需要序列化。

Java 原生提供了 Serializable 接口实现序列化,但这种方式存在很多问题(安全性差、无法跨语言、版本兼容困难),实际项目中更多使用 JSON、Protocol Buffers 等替代方案。理解序列化的原理和不同方案的优缺点,是选择合适方案的基础。

基础概念

什么是序列化

简单来说,序列化就是把对象变成可以存储或传输的格式。比如你有一个 User 对象,包含 name 和 age 两个字段,序列化后它可能变成一段 JSON 文本 {“name”:“Alice”,“age”:25},或者一段二进制数据。反序列化就是反过来,把这段数据还原成 User 对象。

为什么需要序列化

  • 网络传输:微服务之间传递对象,需要把对象序列化后通过网络发送
  • 持久化存储:把对象保存到文件或数据库,下次启动时恢复
  • 缓存:把对象存入 Redis 等缓存系统,需要序列化后存储
  • 跨语言通信:不同语言编写的系统之间交换数据

序列化方案的选择标准

选择序列化方案时需要考虑:是否需要跨语言(Java 序列化只有 Java 能用)、序列化后的数据大小(影响网络传输和存储成本)、序列化和反序列化的速度、是否人可读(JSON 可读,二进制格式不可读)、Schema 演化能力(字段增删时是否兼容)。

快速上手

Java 原生序列化

最简单的序列化方式,只需实现 Serializable 接口

import java.io.*;

// 实现 Serializable 接口即可序列化
public class User implements Serializable {
    // 序列化版本号,用于版本兼容检查
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    private transient String password;  // transient 关键字表示此字段不参与序列化

    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    // getter...
    public String getName() { return name; }
    public int getAge() { return age; }
    public String getPassword() { return password; }
}

序列化和反序列化操作:

import java.io.*;

// 序列化:对象 -> 字节流
User user = new User("Alice", 25, "secret123");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
    oos.writeObject(user);  // 将对象写入文件
}

// 反序列化:字节流 -> 对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
    User restored = (User) ois.readObject();  // 从文件读取对象
    System.out.println(restored.getName());     // 输出: Alice
    System.out.println(restored.getPassword()); // 输出: null(transient 字段不序列化)
}

详细用法

1. serialVersionUID 的作用

serialVersionUID 用于标识的版本。反序列化时,JVM 会比较字节流中的 serialVersionUID 和当前的 serialVersionUID,如果不一致就抛出 InvalidClassException:

public class User implements Serializable {
    // 如果不手动指定,JVM 会根据类结构自动生成
    // 但类结构变化(增删字段)后自动生成的值会改变,导致旧数据无法反序列化
    // 手动指定后,只要版本号不变,增删字段也能兼容
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    // 新增字段:旧数据反序列化时,新字段为默认值(null 或 0)
    private String email;
}

建议始终手动指定 serialVersionUID,避免结构变化导致的反序列化失败。

2. transient 关键字

transient 标记的字段不参与序列化,适合敏感数据或不需要持久化的数据:

public class Session implements Serializable {
    private String sessionId;
    private transient String password;    // 密码不序列化
    private transient Thread worker;      // 线程对象无法序列化
    private transient Socket connection;  // 网络连接无法序列化
}

注意:transient 只能用于 Java 原生序列化。使用 JSON 序列化时,transient 不起作用,需要用 @JsonIgnore 等注解。

3. 自定义序列化逻辑

通过实现 writeObject 和 readObject 方法,可以自定义序列化和反序列化的行为:

import java.io.*;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private transient String encryptedPassword;

    // 自定义序列化逻辑
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();  // 先执行默认序列化
        // 可以在这里添加额外的序列化逻辑
    }

    // 自定义反序列化逻辑
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();  // 先执行默认反序列化
        // 可以在这里恢复 transient 字段的值
        // 例如:从其他来源重新获取密码
    }
}

4. 使用 Jackson 进行 JSON 序列化

Jackson 是 Java 生态中最流行的 JSON 序列化库,可读性好、跨语言、使用广泛:

<!-- Maven 依赖 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.0</version>
</dependency>
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

public class User {
    private String name;
    private int age;

    @JsonIgnore  // JSON 序列化时忽略此字段
    private String password;

    @JsonProperty("user_email")  // JSON 中的字段名
    private String email;

    // Jackson 需要无参构造函数
    public User() {}

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    // getter 和 setter...
}

// 序列化
ObjectMapper mapper = new ObjectMapper();
User user = new User("Alice", 25, "alice@example.com");
String json = mapper.writeValueAsString(user);
// {"name":"Alice","age":25,"user_email":"alice@example.com"}

// 反序列化
User restored = mapper.readValue(json, User.class);

5. Jackson 常用配置

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.text.SimpleDateFormat;

ObjectMapper mapper = new ObjectMapper();

// 格式化输出(开发调试时使用,生产环境关闭以减少数据量)
mapper.enable(SerializationFeature.INDENT_OUTPUT);

// 日期格式
mapper.registerModule(new JavaTimeModule());  // 支持 Java 8 日期类型
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);  // 日期输出为字符串而非时间戳

// 忽略未知字段(反序列化时 JSON 中有 Java 类没有的字段,不报错)
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// 空对象不报错
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

6. 使用 Gson 进行 JSON 序列化

Gson 是 Google 提供的 JSON 库,比 Jackson 更轻量:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

Gson gson = new GsonBuilder()
    .setPrettyPrinting()       // 格式化输出
    .serializeNulls()          // 序列化 null 值(默认忽略)
    .setDateFormat("yyyy-MM-dd")  // 日期格式
    .create();

// 序列化
User user = new User("Alice", 25);
String json = gson.toJson(user);

// 反序列化
User restored = gson.fromJson(json, User.class);

7. 使用 Protocol Buffers

Protocol Buffers(Protobuf)是 Google 开发的高效二进制序列化格式,数据量小、速度快、支持跨语言:

// user.proto
syntax = "proto3";

message User {
    string name = 1;
    int32 age = 2;
    string email = 3;
}

编译 proto 文件生成 Java 后使用:

// 构建对象
UserProto.User user = UserProto.User.newBuilder()
    .setName("Alice")
    .setAge(25)
    .setEmail("alice@example.com")
    .build();

// 序列化为字节数组
byte[] bytes = user.toByteArray();

// 反序列化
UserProto.User restored = UserProto.User.parseFrom(bytes);

常见场景

场景一:API 响应序列化

Spring Boot 中 Controller 返回的对象自动序列化为 JSON:

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)  // null 字段不输出
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.code = 200;
        response.message = "success";
        response.data = data;
        return response;
    }
}

场景二:Redis 缓存序列化

存入 Redis 的对象需要序列化,通常使用 JSON 格式以便跨语言读取:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.StringRedisTemplate;

@Service
public class CacheService {

    private final StringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper;

    // 存储对象到 Redis
    public void put(String key, Object value) {
        try {
            String json = objectMapper.writeValueAsString(value);
            redisTemplate.opsForValue().set(key, json);
        } catch (Exception e) {
            throw new RuntimeException("序列化失败", e);
        }
    }

    // 从 Redis 读取对象
    public <T> T get(String key, Class<T> type) {
        String json = redisTemplate.opsForValue().get(key);
        if (json == null) return null;
        try {
            return objectMapper.readValue(json, type);
        } catch (Exception e) {
            throw new RuntimeException("反序列化失败", e);
        }
    }
}

注意事项与常见错误

Java 原生序列化的安全风险

Java 原生反序列化存在严重的安全漏洞。攻击者可以构造恶意的序列化数据,在反序列化时执行任意代码。永远不要反序列化不可信来源的数据。如果必须使用,可以使用 JEP 290 过滤机制限制可反序列化的

循环引用导致无限递归

对象之间存在双向引用时,JSON 序列化会陷入无限递归:

public class User {
    private List<Order> orders;  // 用户有多个订单
}

public class Order {
    private User user;  // 订单属于某个用户(循环引用)
}

// 解决方案一:使用 @JsonIgnore 忽略反向引用
public class Order {
    @JsonIgnore
    private User user;
}

// 解决方案二:使用 @JsonManagedReference 和 @JsonBackReference
public class User {
    @JsonManagedReference
    private List<Order> orders;
}

public class Order {
    @JsonBackReference
    private User user;
}

日期格式问题

不同序列化方式对日期的处理不同。Java 原生序列化保存的是时间戳,JSON 序列化默认输出时间戳数字。建议统一使用 ISO 8601 格式(如 2024-01-15T10:30:00Z)。

枚举型的序列化

默认情况下枚举按名称序列化。如果按序号序列化,新增枚举值可能导致反序列化错位:

// 错误:按序号序列化,新增值会打乱顺序
public enum Status {
    ACTIVE,    // 0
    INACTIVE,  // 1
    // 新增 PENDING 后,原来存为 1 的 INACTIVE 会变成 PENDING
    PENDING    // 2
}

// 正确:始终按名称序列化
@JsonFormat(shape = JsonFormat.Shape.STRING)
public enum Status {
    ACTIVE, INACTIVE, PENDING
}

进阶用法

Kryo 高性能序列化

Kryo 是一个高性能的 Java 序列化框架,速度比 Java 原生序列化快 10 倍以上,序列化后的数据也更小:

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.io.Input;

Kryo kryo = new Kryo();
kryo.register(User.class);  // 注册类,提高性能

// 序列化
Output output = new Output(new ByteArrayOutputStream());
kryo.writeObject(output, user);
output.close();

// 反序列化
Input input = new Input(new ByteArrayInputStream(output.getBuffer()));
User restored = kryo.readObject(input, User.class);

Avro Schema 演化

Avro 支持 Schema 演化,可以在不破坏兼容性的情况下增删字段。适合需要长期存储和版本迭代的数据格式:

{
  "type": "record",
  "name": "User",
  "fields": [
    { "name": "name", "type": "string" },
    { "name": "age", "type": ["null", "int"], "default": null }
  ]
}

新增字段时提供默认值,旧数据反序列化时新字段使用默认值,不会报错。