【原創】(譯)Java 序列化魔法方法及使用示例

福爾馬林發表於2019-09-06

轉載請註明出處。

英文原文地址:www.javacodegeeks.com/2019/09/jav…

翻譯:福爾馬林/內觀

在上一篇文章 Everything You Need to Know About Java Serialization 中我們討論瞭如何通過實現 Serializable 介面來啟用一個類的序列化能力。如果我們的類沒有實現 Serializable 介面,或者它引用了一個非可序列化的類,JVM 就會丟擲 NotSerializableException 不可序列化異常。

Serializable 的所有子類都同樣也可序列化,其中也包括 Externalizable 介面。所以即使我們通過 Externalizable 對序列化過程進行了定製,我們的類也仍然是可序列化的。

Serializable 是一個標記介面。它既沒有欄位,也沒有方法。它的作用僅僅是為 JVM 提供一個標記。真正的序列化過程是由 JVM 控制的 ObjectInputStreamObjectOutputStream 類提供的。

如果我們想在正常處理流程之上新增額外的處理邏輯,該怎麼做呢?比如說我們想在序列化/反序列化之前對敏感資料進行加/解密操作。Java 提供了一些額外的方法來幫助我們實現這樣的目的,也就是我們即將在這篇部落格討論的主題。

【原創】(譯)Java 序列化魔法方法及使用示例

writeObject 和 readObject 方法

想要定製或者新增額外的邏輯來加強序列化/反序列化的流程,需要提供 writeObjectreadObject 兩個方法,簽名如下:

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException
  • private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException

這些方法在 Everything You Need to Know About Java Serialization 已經詳細討論過了。

readObjectNoData 方法

Serializable 的 java docs 所述,如果我們希望當接到的序列化流不滿足我們想反序列化的類的時候,能自動進行一些狀態初始化,那麼我們就要提供 readObjectNoData 方法,簽名如下:

  • private void readObjectNoData() throws ObjectStreamException

這種情況可能出現在接收方使用了一個與傳送方不同版本的類。接收方的版本多擴充套件了一些欄位,而傳送方的版本沒有這些欄位。還有一種可能就是序列化流被篡改了。這時,無論是惡意的流還是不完整的流,都可以用 readObjectNoData 方法來將序列化得到的物件初始化到正確的狀態。

每個可序列化類都可以定義它自己的 readObjectNoData 方法。如果一個類沒有定義 readObjectNoData 方法,那麼當出現上述情況的時候,這些欄位的值就會取預設值。(譯註:比如 null 或 0)

writeReplace 和 readResolve 方法

可序列化的類,如果想在將物件寫入流時,進行一定的轉換,可以提供這個特殊方法:

  • ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException

如果想在從流裡讀出物件的時候,進行一定的替換,則可以提供下面這個方法:

  • ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException

基本上,writePlace 方法允許開發者提供一個替代物件來取代原來將被序列化的物件。而 readResolve 方法被用來在反序列化的時候用你選擇的物件來替代原本反序列化出來的物件。

writeReplace 和 readResolve 方法的一個主要用途是用來實現單例模式。我們知道反序列化每次都會建立一個新物件,常被用來做物件的深拷貝。對於要採用單例模式的情況就不好弄了。

更多資訊可以參考 Java cloning and serialization on Java CloningJava Serialization 主題。

readResolve 方法會在 readObject 方法返回後被呼叫(相反,writeReplace 方法是在 writeObject 方法之前被呼叫)。readResolve 方法返回的物件會替換 ObjectInputStream.readObject 返給使用者的 this 物件,並更新流裡所有對該物件的反向引用。我們可以使用 writeReplace 方法來把要序列化的物件換成 null,然後在 readResolve 方法用單例的物件示例來替代反序列化的結果。(譯註:這樣就解決了上兩段提出的單例問題)

validateObject 方法

如果我們想對我們的某些欄位做校驗,我們可以實現 ObjectInputValidation 介面的 validateObject 方法。

validateObject 方法會在我們在 readObject 方法裡呼叫 ObjectInputStream.registerValidation(this, 0) 註冊校驗的時候被呼叫。它對於驗證流未被篡改或實際有效很有用。

最後是對上面所有這些方法的示例程式碼。

public class SerializationMethodsExample {
 
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Employee emp = new Employee("Naresh Joshi", 25);
        System.out.println("Object before serialization: " + emp.toString());
 
        // Serialization
        serialize(emp);
 
        // Deserialization
        Employee deserialisedEmp = deserialize();
        System.out.println("Object after deserialization: " + deserialisedEmp.toString());
 
 
        System.out.println();
 
        // This will print false because both object are separate
        System.out.println(emp == deserialisedEmp);
 
        System.out.println();
 
        // This will print false because both `deserialisedEmp` and `emp` are pointing to same object,
        // Because we replaced de-serializing object in readResolve method by current instance
        System.out.println(Objects.equals(emp, deserialisedEmp));
    }
 
    // Serialization code
    static void serialize(Employee empObj) throws IOException {
        try (FileOutputStream fos = new FileOutputStream("data.obj");
             ObjectOutputStream oos = new ObjectOutputStream(fos))
        {
            oos.writeObject(empObj);
        }
    }
 
    // Deserialization code
    static Employee deserialize() throws IOException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream("data.obj");
             ObjectInputStream ois = new ObjectInputStream(fis))
        {
            return (Employee) ois.readObject();
        }
    }
}
 
class Employee implements Serializable, ObjectInputValidation {
    private static final long serialVersionUID = 2L;
 
    private String name;
    private int age;
 
    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    // With ObjectInputValidation interface we get a validateObject method where we can do our validations.
    @Override
    public void validateObject() {
        System.out.println("Validating age.");
 
        if (age < 18 || age > 70)
        {
            throw new IllegalArgumentException("Not a valid age to create an employee");
        }
    }
 
    // Custom serialization logic,
    // This will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization.
    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("Custom serialization logic invoked.");
        oos.defaultWriteObject(); // Calling the default serialization logic
    }
 
    // Replacing de-serializing object with this,
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("Replacing serialising object by this.");
        return this;
    }
 
    // Custom deserialization logic
    // This will allow us to have additional deserialization logic on top of the default one e.g. performing validations, decrypting object after deserialization.
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Custom deserialization logic invoked.");
 
        ois.registerValidation(this, 0); // Registering validations, So our validateObject method can be called.
 
        ois.defaultReadObject(); // Calling the default deserialization logic.
    }
 
    // Replacing de-serializing object with this,
    // It will will not give us a full proof singleton but it will stop new object creation by deserialization.
    private Object readResolve() throws ObjectStreamException {
        System.out.println("Replacing de-serializing object by this.");
        return this;
    }
 
    @Override
    public String toString() {
        return String.format("Employee {name='%s', age='%s'}", name, age);
    }
}
複製程式碼

你也可以在 Github 倉庫上找到完整的程式碼,歡迎提供任何反饋。

相關文章