物件序列化是 Java 分散式系統、快取、持久化的基石,但預設機制暗藏效能、安全、相容性風險。本文系統梳理序列化協議、Serializable/Externalizable 兩大介面、serialVersionUID 版本控制、自訂序列化策略、常見陷阱與現代替代方案。
ObjectOutputStream.writeObject() / ObjectInputStream.readObject()。| 策略 | 介面 | 特點 | 適用場景 |
|---|---|---|---|
| 預設序列化 | implements Serializable | 零代碼,JVM 自動遞歸序列化非 transient 欄位 | 簡單 DTO、內部快取、不跨版本部署 |
| 自訂序列化 | implements Serializable + private writeObject/readObject | 保留預設機制,可加密、壓縮、補欄位、驗證 | 需控制敏感欄位、處理版本演進、優化效能 |
| 完全控制 | implements Externalizable | 必須實作 writeExternal/readExternal,需 public 無參建構子 | 高效能、大物件圖、跨語言、特殊協定 |
// 強烈建議顯式宣告,避免編譯器產生不穩定的隱式 UID
private static final long serialVersionUID = 1L;
InvalidClassException。transient:例項欄位,跳過序列化,反序列化後為預設值 (null/0/false)。static:類別欄位,屬於 Class 物件,不屬於例項,天然不序列化。public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // 敏感欄位不落盤
private transient String fullName; // 衍生欄位,反序列化後重算
// 預設序列化輔助:先執行預設邏輯,再處理額外邏輯
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// 可選:加密、寫入額外資料
out.writeObject(encrypt(password));
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// 還原 transient 欄位
this.password = decrypt((String) in.readObject());
this.fullName = username.toUpperCase(); // 衍生欄位重算
}
// 序列化代理模式:防止單例破壞、不可變物件被竄改
private Object writeReplace() throws ObjectStreamException {
return new SerializationProxy(this);
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidClassException("舊版本資料不相容");
}
}
public class BigData implements Externalizable {
private static final long serialVersionUID = 1L;
private List<Record> records = new ArrayList<>();
public BigData() {} // 必須有 public 無參建構子
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(records.size());
for (Record r : records) {
out.writeUTF(r.getKey());
out.writeLong(r.getValue());
}
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
int size = in.readInt();
records = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
records.add(new Record(in.readUTF(), in.readLong()));
}
}
}
transient + 自訂 writeObject 加密 |
| 缺乏驗證 | 惡意資料導致不變量破壞、邏輯漏洞 | readObject 中驗證、拋 InvalidObjectException |
| 物件圖過深導致 StackOverflow / OOM | 遞歸遍歷大圖消耗棧/堆 | 改用 Externalizable 迭代寫入、或改用 JSON/Protobuf |
| 類別演進破壞二進制相容 | 新增欄位、修改型別導致反序列化失敗 | 穩定 serialVersionUID、提供 readObject 兼容舊版 |
ObjectInputFilter JDK 9+)。ObjectInputStream ois = new ObjectInputStream(inputStream) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
// 僅允許自家包名
if (!desc.getName().startsWith("com.mycompany.")) {
throw new InvalidClassException("未授權的類別: " + desc.getName());
}
return super.resolveClass(desc);
}
};
// 或使用全域 Filter (JDK 9+)
ObjectInputFilter.Config.setSerialFilter(info -> {
if (info.serialClass() != null && !info.serialClass().getName().startsWith("com.mycompany.")) {
return ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.ALLOWED;
});
Java 原生序列化雖然內建方便,但預設機制不安全、效能低、版本脆弱。生產環境請遵循:
serialVersionUID**。transient + 自訂加密序列化**。readObject 驗證不變量**。ObjectInputFilter 防範反序列化攻擊**。掌握這些核心原則,即能在享受序列化便利的同時,守住安全、效能與演進底線。