首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 對象序列化與反序列化深度指南:API、接口、版本兼容與安全實踐

     編輯:Java 相關知識

對象序列化是將 Java 對象轉為字節序列(持久化到磁盤或網絡傳輸),反序列化是逆過程。本文系統講解 JDK 原生序列化 API、Serializable 與 Externalizable 兩大接口機制、自定義序列化策略、serialVersionUID 版本控制、安全風險與最佳實踐。

一、 核心 API 與基礎流程

類/接口包核心方法作用
ObjectOutputStreamjava.iowriteObject(Object)對象 → 字節流(序列化)
ObjectInputStreamjava.ioreadObject()字節流 → 對象(反序列化)

基礎示例

public class SerializationDemo {
    public static void main(String[] args) throws Exception {
        // 序列化
        try (ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("user.ser"))) {
            User user = new User("Alice", 28, "secret123");
            out.writeObject(user);
            out.writeObject("附加字符串");
            out.writeInt(2024);
        }

        // 反序列化(順序必須一致)
        try (ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("user.ser"))) {
            User u = (User) in.readObject();
            String s = (String) in.readObject();
            int year = in.readInt();
            System.out.println(u); // User{name='Alice', age=28, password='null'}
            System.out.println(s); // 附加字符串
            System.out.println(year); // 2024
        }
    }
}

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;
    }
    @Override public String toString() {
        return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
    }
}

二、 Serializable 接口:默認與自定義序列化

1. 默認機制

  • 僅序列化 非 transient、非 static 的實例字段
  • 遞歸遍歷對象圖(雙向鏈表結構),對象圖複雜時性能開銷大
  • 反序列化時 不調用構造器,直接分配內存並填充字段

2. 自定義序列化:writeObject / readObject

在可序列化類中定義 private 方法,JVM 會優先調用:

private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject(); // 先執行默認序列化
    // 手動處理 transient 敏感字段:加密後寫入
    out.writeObject(encrypt(password));
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject(); // 先執行默認反序列化
    // 手動讀取並解密
    this.password = decrypt((String) in.readObject());
}

場景:敏感字段加密、衍生字段重算、補充上下文信息。

3. transient 與性能優化

  • 標記 transient:跳過序列化,節省空間與時間
  • 大對象圖中,將無需持久化的字段(緩存、臨時狀態、日誌器)標記 transient

三、 Externalizable 接口:完全掌控

Externalizable extends Serializable,強制實現兩個 public 方法:

public class UserExt implements Externalizable {
    private String name; private int age;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
        // 完全自定義格式、壓縮、加密
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = (String) in.readObject();
        this.age = in.readInt();
    }

    // 關鍵:反序列化時會先調用 無參構造器
    public UserExt() {} // 必須提供 public 無參構造器!
}

差異點:

  • 需提供 public no-arg constructor(否則拋 InvalidClassException)
  • 完全繞過默認機制,靈活性最高,但樣板代碼多
  • 適用於高性能、特殊格式(如與異語言系統交互)場景

四、 版本兼容與 serialVersionUID

1. 作用

反序列化時,JVM 比較流中的 serialVersionUID 與當前類的 serialVersionUID:

  • 一致 → 兼容,允許反序列化
  • 不一致 → 拋 InvalidClassException

2. 顯式聲明的兩大理由

  1. 跨編譯器/版本穩定:默認值由編譯器根據類結構生成,不同編譯器可能不同,導致誤判不兼容。
  2. 業務級版本控制:
    • 兼容升級(字段增減、類型兼容轉換) → 保持 UID 不變
    • 不兼容重構(字段刪除、類型不可兼容變更) → 修改 UID 強制失效舊數據
// 兼容升級示例
private static final long serialVersionUID = 1L; // 保持不變
private String newField = "default"; // 新增字段,反序列化舊數據時自動賦默認值

五、 安全風險與防禦

1. 反序列化漏洞

惡意構造的字節流在反序列化過程中觸發 readObject、readResolve 等鏈,導致 RCE(遠程代碼執行)。著名庫:Apache Commons Collections、Spring、Fastjson 等均有歷史漏洞。

2. 防禦措施

  • 不信任數據不反序列化:網絡接口嚴禁直接 readObject()
  • 使用安全框架:ObjectInputFilter (JDK 9+) 白名單過濾允許反序列化的類
  • 避免 Java 原生序列化:首選 JSON (Jackson/Gson)、Protobuf、Avro 等無代碼執行風險的格式
  • 升級依賴:及時修復已知漏洞庫版本

六、 最佳實踐總結

  1. ✅ 實現 Serializable 而非 Externalizable(除非極致性能/特殊格式)
  2. ✅ 顯式聲明 serialVersionUID
  3. ✅ 敏感字段 transient + 自定義 writeObject/readObject 加密
  4. ✅ 大對象圖優化:標記臨時字段 transient
  5. ⚠️ 網絡傳輸/持久化首選 JSON/Protobuf,避免原生序列化安全風險
  6. ⚠️ 必須用原生序列化時:啟用 ObjectInputFilter 白名單
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved