到現在為止,還沒有看到什麼新鮮的或令人興奮的事情,但是這是一個很好的出發點。我們將使用 Person 來發現您可能不 知道的關於 Java 對象序列化 的 5 件事。
1. 序列化允許重構
序列化允許一定數量的類變種,甚至重構之後也是如此,ObjectInputStream 仍可以很好地將其讀出來。Java Object Serialization 規范可以自動管理的關鍵任務是:
取決於所需的向後兼容程度,轉換字段形式(從非 static 轉換為 static 或從非 transient 轉換為 transIEnt)或者刪除字段需要額外的消息傳遞。
重構序列化類
既然已經知道序列化允許重構,我們來看看當把新字段添加到 Person 類中時,會發生什麼事情。
如清單 3 所示,PersonV2 在原先 Person 類的基礎上引入一個表示性別的新字段。
清單 3. 將新字段添加到序列化的 Person 中
- enum Gender
- {
- MALE, FEMALE
- }
- public class Person
- implements Java.io.Serializable
- {
- public Person(String fn, String ln, int a, Gender g)
- {
- this.firstName = fn; this.lastName = ln; this.age = a; this.gender = g;
- }
- public String getFirstName() { return firstName; }
- public String getLastName() { return lastName; }
- public Gender getGender() { return gender; }
- public int getAge() { return age; }
- public Person getSpouse() { return spouse; }
- public void setFirstName(String value) { firstName = value; }
- public void setLastName(String value) { lastName = value; }
- public void setGender(Gender value) { gender = value; }
- public void setAge(int value) { age = value; }
- public void setSpouse(Person value) { spouse = value; }
- public String toString()
- {
- return "[Person: firstName=" + firstName +
- " lastName=" + lastName +
- " gender=" + gender +
- " age=" + age +
- " spouse=" + spouse.getFirstName() +
- "]";
- }
- private String firstName;
- private String lastName;
- private int age;
- private Person spouse;
- private Gender gender;
- }
序列化使用一個 hash,該 hash 是根據給定源文件中幾乎所有東西 — 方法名稱、字段名稱、字段類型、訪問修改方法等 — 計算出來的,序列化將該 hash 值與序列化流中的 hash 值相比較。
為了使 Java 運行時相信兩種類型實際上是一樣的,第二版和隨後版本的 Person 必須與第一版有相同的序列化版本 hash(存儲為 private static final serialVersionUID 字段)。因此,我們需要 serialVersionUID 字段,它是通過對原始(或 V1)版本的 Person 類運行 JDK serialver 命令計算出的。
一旦有了 Person 的 serialVersionUID,不僅可以從原始對象 Person 的序列化數據創建 PersonV2 對象(當出現新字段時,新字段被設為缺省值,最常見的是“null”),還可以反過來做:即從 PersonV2 的數據通過反序列化得到 Person,這毫不奇怪。
2. 序列化並不安全
讓 Java 開發人員詫異並感到不快的是,序列化二進制格式完全編寫在文檔中,並且完全可逆。實際上,只需將二進制序列化流的內容轉儲到控制台,就足以看清類是什麼樣子,以及它包含什麼內容。
這對於安全性有著不良影響。例如,當通過 RMI 進行遠程方法調用時,通過連接發送的對象中的任何 private 字段幾乎都是以明文的方式出現在套接字流中,這顯然容易招致哪怕最簡單的安全問題。
幸運的是,序列化允許 “hook” 序列化過程,並在序列化之前和反序列化之後保護(或模糊化)字段數據。可以通過在 Serializable 對象上提供一個 writeObject 方法來做到這一點。
模糊化序列化數據
假設 Person 類中的敏感數據是 age 字段。畢竟,女士忌談年齡。我們可以在序列化之前模糊化該數據,將數位循環左移一位,然後在反序列化之後復位。(您可以開發更安全的算法,當前這個算法只是作為一個例子。)
為了 “hook” 序列化過程,我們將在 Person 上實現一個 writeObject 方法;為了 “hook” 反序列化過程,我們將在同一個類上實現一個 readObject 方法。重要的是這兩個方法的細節要正確 — 如果訪問修改方法、參數或名稱不同於清單 4 中的內容,那麼代碼將不被察覺地失敗,Person 的 age 將暴露。
清單 4. 模糊化序列化數據
- public class Person
- implements Java.io.Serializable
- {
- public Person(String fn, String ln, int a)
- {
- this.firstName = fn; this.lastName = ln; this.age = a;
- }
- public String getFirstName() { return firstName; }
- public String getLastName() { return lastName; }
- public int getAge() { return age; }
- public Person getSpouse() { return spouse; }
- public void setFirstName(String value) { firstName = value; }
- public void setLastName(String value) { lastName = value; }
- public void setAge(int value) { age = value; }
- public void setSpouse(Person value) { spouse = value; }
- private void writeObject(Java.io.ObjectOutputStream stream)
- throws Java.io.IOException
- {
- // "Encrypt"/obscure the sensitive data
- age = age << 2;
- stream.defaultWriteObject();
- }
- private void readObject(Java.io.ObjectInputStream stream)
- throws Java.io.IOException, ClassNotFoundException
- {
- stream.defaultReadObject();
- // "Decrypt"/de-obscure the sensitive data
- age = age << 2;
- }
- public String toString()
- {
- return "[Person: firstName=" + firstName +
- " lastName=" + lastName +
- " age=" + age +
- " spouse=" + (spouse!=null ? spouse.getFirstName() : "[null]") +
- "]";
- }
- private String firstName;
- private String lastName;
- private int age;
- private Person spouse;
- }
如果需要查看被模糊化的數據,總是可以查看序列化數據流/文件。而且,由於該格式被完全文檔化,即使不能訪問類本身,也仍可以讀取序列化流中的內容。