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

Java 物件序列化深度解析:機制、定制化與陷阱全景指南

     編輯:Java 相關知識

前言

Java 序列化是將物件狀態轉為位元組流(持久化/網絡傳輸/分散式會話),反序列化還原物件。看似簡單的 `implements Serializable` 背後,隱藏著預設機制、效能陷阱、安全漏洞、版本演進等硬核課題。本文系統梳理核心原理、四種定制化方式、單例破壞與修復、最佳實踐。

一、核心機制:預設序列化流程

1.1 必要條件

  • 類實作 `java.io.Serializable`(標記介面,無方法)。
  • 所有非 `transient`/`static` 執行個體欄位必須可序列化(基本型別、`String`、陣列、實作 `Serializable` 的物件)。

1.2 序列化圖遍歷

`ObjectOutputStream.writeObject(obj)` 觸發深度優先遍歷:

  1. 寫入物件類描述(`ObjectStreamClass`:serialVersionUID、欄位元資料)。
  2. 遞歸序列化每個非 transient 欄位引用的物件(處理循環引用、共享引用)。
  3. 寫入欄位值(基本型別直接寫,物件引用寫 handle)。

效能警示: 物件圖龐大/深層嵌套/大量集合嵌套集合 → 序列化耗時、位元組流膨脹、棧溢出風險。

1.3 關鍵點:static/transient 不參與

  • static 欄位屬於類,不屬於實例,不序列化。
  • transient 顯式排除(如敏感密碼、臨時緩存、非序列化第三方物件)。

二、四種定制化序列化策略

策略 1:transient + 自定義讀寫


public class User implements Serializable {
    private String username;
    private transient String password; // 不序列化
    private transient String encryptedPassword; // 衍生欄位

    // 序列化前:加密/轉換
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 先執行預設序列化 (username)
        this.encryptedPassword = encrypt(password); // 衍生加密欄位
        out.writeObject(encryptedPassword); // 顯式寫入
    }

    // 反序列化後:還原/解密
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 讀 username
        this.encryptedPassword = (String) in.readObject(); // 讀加密密碼
        this.password = decrypt(encryptedPassword); // 還原明文 (或保持加密)
    }
}

適用: 少數欄位需特殊處理,主體仍用預設機制。

策略 2:Externalizable 完全控制


public class UserProfile implements Externalizable {
    private String name;
    private int age;
    private Set tags = new HashSet<>();

    public UserProfile() {} // 必須有 public 無參建構子!

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

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

關鍵差異:

  • 繼承 `Externalizable` → 完全繞過預設機制,`writeObject/readObject` 失效。
  • 反序列化先調用 public 無參建構子 創建實例,再填充欄位。
  • 適用:高效能需求、跨語言二進制協定、舊版本資料遷移。

策略 3:serialVersionUID 版本控制


// 顯式宣告,避免編譯器依賴類結構生成(修改類導致 UID 變化,反序列化拋 InvalidClassException)
private static final long serialVersionUID = 1L; 

// 兼容性規則:
// 新增欄位:相容(新欄位取預設值)
// 刪除欄位:相容(舊資料忽略)
// 修改欄位型別:不相容
// 修改 serialVersionUID:強制不相容

策略 4:readResolve 保護單例/枚舉


public class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() { return INSTANCE; }

    // 反序列化時,JVM 會調用此方法,返回的物件替換新建實例
    protected Object readResolve() {
        return INSTANCE; // 確保單例唯一性
    }
}

原理: 反序列化創建新實例後,若存在 `readResolve`,其返回值直接替換新實例(新實例變垃圾)。枚舉天然支援此機制,枚舉是實現單例的最佳方式(防反射、防序列化破壞)。

三、進階主題與陷阱

3.1 繼承體系中的序列化

  • 父類實作 `Serializable` → 子類自動可序列化,父類欄位按預設序列化。
  • 父類未實作 `Serializable` → 反序列化時調用父類 無參建構子 初始化父類欄位(必須有無參建構子,否則拋 `InvalidClassException`)。

3.2 序列化代理模式

針對不可變類或複雜不變約束,引入內部私有 `SerializationProxy`:


public class Period implements Serializable {
    private final Date start;
    private final Date end;

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(new SerializationProxy(this)); // 寫入代理
    }

    private void readObject(ObjectInputStream in) throws IOException {
        throw new InvalidObjectException("Proxy required"); // 禁止直接反序列化
    }

    private Object readResolve() {
        return ((SerializationProxy) in.readObject()).resolve(); // 代理還原
    }

    private static class SerializationProxy implements Serializable {
        private final Date start, end;
        SerializationProxy(Period p) { this.start = p.start; this.end = p.end; }
        Object resolve() { return new Period(start, end); }
    }
}

3.3 安全漏洞:反序列化 RCE

  • Gadget Chain:`ObjectInputStream.readObject()` 觸發任意代碼執行(Commons Collections、Spring、Hibernate 等庫的鏈)。
  • 防禦:ObjectInputFilter (Java 9+)、白名單限制可反序列化類、避免反序列化不可信資料、使用 JSON/Protobuf 替代。

3.4 效能替代方案對比

方案優點缺點適用場景
Java 原生序列化零依賴、支援任意物件圖體積大、速度慢、不跨語言、安全風險臨時緩存、單機持久化、舊系統相容
JSON (Jackson/Gson)可讀、跨語言、生態成熟無類型資訊、效能中等REST API、配置檔、日誌
Protocol Buffers極小體積、極快、Schema 演進、跨語言需 .proto 定義、學習曲線高效能 RPC、微服務通訊、大數據存儲
Kryo / FST比原生快 5-10x、體積小、支援循環引用第三方依賴、非標準分散式快取、Spark/Kafka 內部序列化

四、最佳實踐清單

  1. 顯式宣告 `serialVersionUID`,掌控版本兼容性。
  2. 最小化序列化面:`transient` 排除非必要/敏感/非序列化欄位。
  3. 不可變類優先用序列化代理,保護不變約束。
  4. 單例/枚舉必加 `readResolve`(枚舉免寫)。
  5. 父類非 Serializable 時提供無參建構子。
  6. 生產環境禁用原生序列化不可信資料,啟用 `ObjectInputFilter`。
  7. 高吞吐/跨語言場景遷移 Protobuf/Kryo。

結語

Java 序列化是把雙刃劍:用得好是分散式系統潤滑劑,用不好是效能殺手與安全漏洞。理解「預設機制如何遍歷物件圖」、「四種定制化切入點」、「版本演進規則」、「單例保護原理」,才能在遺留系統維護、框架原理分析、安全審計中遊刃有餘。下一篇將實戰《Jackson/Protobuf 遷移指南與效能基準測試》。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved