前言
Java 序列化是將對象狀態轉換為字節序列的過程,反序列化則是逆向重建對象。它是 RMI、分布式緩存、會話持久化、消息隊列的基石。本文深度解析序列化機制、核心介面、版本控制、效能優化、安全漏洞與現代替代方案。
核心機制原理
1. 序列化流程
對象圖 (Object Graph)
↓ ObjectOutputStream.writeObject()
┌─────────────────────────────────────┐
│ 1. 寫入魔數: AC ED 00 05 │
│ 2. 寫入類描述符 (ClassDescriptor) │
│ - 類名、serialVersionUID │
│ - 字段元數據 (非 transient) │
│ 3. 遞歸寫入對象字段值 │
│ - 基本類型直接寫入 │
│ - 引用類型遞歸序列化 │
│ - 處理循環引用 (handle 機制) │
└─────────────────────────────────────┘
↓
字節序列 (可存儲、網絡傳輸)
2. 反序列化流程
字節序列
↓ ObjectInputStream.readObject()
┌─────────────────────────────────────┐
│ 1. 讀取並驗證魔數 │
│ 2. 讀取類描述符 │
│ - 本地類路徑查找對應類 │
│ - 驗證 serialVersionUID │
│ 3. 分配對象內存 (不調用構造器) │
│ 4. 遞歸讀取字段值填充 │
│ - 解析 handle 解決循環引用 │
│ 5. 調用 readObject() (若存在) │
│ 6. 返回重建對象 │
└─────────────────────────────────────┘
核心介面與類
| 介面/類 | 包 | 作用 | 關鍵方法 |
Serializable |
java.io |
標記介面,啟用默認序列化 |
無方法 |
Externalizable |
java.io |
完全自定義序列化邏輯 |
writeExternal(), readExternal() |
ObjectOutputStream |
java.io |
序列化輸出流 |
writeObject(), writeUnshared() |
ObjectInputStream |
java.io |
反序列化輸入流 |
readObject(), readUnshared() |
Serializable 默認序列化
1. 基礎實現
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 強烈建議顯式聲明
private String name;
private int age;
private transient String password; // 不序列化
private static String company; // 不序列化
// 構造器、getter/setter...
}
2. 自定義序列化鉤子方法 (私有)
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.initTransientFields();
}
private void readObjectNoData() throws ObjectStreamException {
// 處理超類無數據情況 (版本升級)
this.initTransientFields();
}
3. 替換對象模式
// 序列化時替換為代理對象
private Object writeReplace() {
return new UserProxy(this);
}
// 反序列化時替換為單例/標準實例
private Object readResolve() {
return UserRegistry.getInstance(this.name); // 單例模式保護
}
Externalizable 完全控制
1. 實現範例
public class UserExternal implements Externalizable {
private String name;
private int age;
private String password;
// 必須有無參構造器 (反序列化調用)
public UserExternal() {}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
// 自定義加密
out.writeUTF(encrypt(password));
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = in.readUTF();
this.age = in.readInt();
this.password = decrypt(in.readUTF());
}
}
2. 對比總結
| 特性 | Serializable | Externalizable |
| 實現難度 | 低 (標記介面) | 高 (需手寫邏輯) |
| 效能 | 中 (反射、元數據開銷) | 高 (無反射、緊湊格式) |
| 版本兼容 | 自動處理 (有 serialVersionUID) | 手動處理 |
| 構造器調用 | 反序列化不調用 | 調用無參構造器 |
| 適用場景 | 通用業務對象 | 高效能、特殊格式、第三方類 |
serialVersionUID 版本控制
1. 作用機制
- 反序列化時,JVM 比較流中的 SUID 與本地類的 SUID
- 不一致 → 拋出
InvalidClassException
- 未顯式聲明 → JVM 根據類結構自動計算 (極不穩定)
2. 最佳實踐
// 1. 顯式聲明,初始版本設為 1L
private static final long serialVersionUID = 1L;
// 2. 兼容性修改規則
// ✅ 兼容:添加字段、添加/移除方法、修改訪問修飾符
// ✅ 兼容:實現/移除介面、修改 static/transient 字段
// ❌ 不兼容:刪除字段、修改字段類型、修改類層次結構
// ❌ 不兼容:移除 Serializable 實現、修改 serialVersionUID
3. 版本升級策略
// V1
private static final long serialVersionUID = 1L;
private String name;
// V2: 添加 age 字段 (兼容)
private static final long serialVersionUID = 1L; // 保持不變
private String name;
private int age = 18; // 默認值
// V3: 重構 name → firstName + lastName (不兼容)
private static final long serialVersionUID = 2L; // 必須修改
private String firstName;
private String lastName;
// 使用 readObject 兼容舊版本
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
if (firstName == null && lastName == null) {
// 舊版本數據遷移
this.firstName = this.name; // 假設舊字段仍在流中
}
}
序列化安全漏洞與防範
1. 反序列化漏洞原理
攻擊者構造惡意字節流,利用類路徑中存在的「Gadget Chain」(如 Apache Commons Collections、Spring、Groovy),在 readObject() 執行過程中觸發任意代碼執行 (RCE)。
2. 防護措施
// 1. 禁用反序列化 (JDK 9+)
ObjectInputFilter filter = info -> {
if (info.serialClass() != null &&
!info.serialClass().getPackageName().startsWith("com.myapp.")) {
return ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.ALLOWED;
};
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter);
// 2. 白名單驗證 (通用)
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set ALLOWED = Set.of(
"com.myapp.model.User",
"com.myapp.model.Order",
"java.util.ArrayList",
"java.lang.String"
);
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
if (!ALLOWED.contains(desc.getName())) {
throw new InvalidClassException("Unauthorized deserialization: " + desc.getName());
}
return super.resolveClass(desc);
}
}
// 3. 使用 NotSoSerial / jdeserialize 等防護庫
// 4. 避免在類路徑引入已知 Gadget 庫 (commons-collections3 等)
3. 安全編碼規範
- ❌ 絕不反序列化不可信數據 (網絡輸入、文件上傳、Redis 鍵值)
- ✅ 使用 JSON/Protobuf/Avro 替代 Java 原生序列化進行數據交換
- ✅ 若必須使用,實施嚴格白名單過濾
- ✅ 定期掃描依賴中的已知 Gadget 鏈 (OWASP Dependency Check)
效能優化與現代替代方案
1. Java 原生序列化優化
- 實現
Externalizable 避免反射開銷
- 使用
writeUnshared/readUnshared 處理共享引用
- 壓縮字節流:
GZIPOutputStream 包裝
- 預分配緩衝區大小
2. 高效序列化框架對比
| 框架 | 格式 | 效能 | 跨語言 | Schema | 適用場景 |
| Kryo |
二進制 |
極快 |
否 |
無 |
JVM 內部、Spark、緩存 |
| Protobuf |
二進制 |
極快 |
是 |
.proto |
微服務、gRPC、持久化 |
| Avro |
二進制 |
快 |
是 |
JSON Schema |
大數據、Kafka、Hadoop |
| MessagePack |
二進制 |
快 |
是 |
無 |
通用、Redis 值序列化 |
| Jackson |
JSON |
中 |
是 |
可選 |
REST API、配置、日誌 |
| FST |
二進制 |
極快 |
否 |
無 |
JVM 高效能替代 |
3. Protobuf 實戰範例
// user.proto
syntax = "proto3";
message User {
string name = 1;
int32 age = 2;
string email = 3;
}
// Java 使用
User user = User.newBuilder()
.setName("Alice")
.setAge(25)
.setEmail("alice@example.com")
.build();
byte[] bytes = user.toByteArray(); // 序列化
User parsed = User.parseFrom(bytes); // 反序列化
總結
- Serializable:簡單易用,適合 JVM 內部臨時傳輸,**嚴禁用於不可信數據交換**
- serialVersionUID:必須顯式聲明,遵循兼容性修改規則
- transient:保護敏感字段、非序列化資源 (連接、鎖、線程)
- readObject/writeObject:自定義邏輯、版本遷移、加密解密
- readResolve/writeReplace:單例保護、代理替換
- 安全第一:白名單過濾、避免 Gadget 鏈、優先選 JSON/Protobuf
- 現代選擇:微服務用 Protobuf/gRPC,大數據用 Avro,高效能 JVM 內部用 Kryo/FST