Effective Java 第三版—— 90.考慮序列化代理替代序列化實例

Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java

Effective Java, Third Edition

90. 考慮序列化代理替代序列化實例

正如在條目 85和 條目86中提到並貫穿本章的討論,實現Serializable接口的決定,增長了出現bug和安全問題的可能性,由於它容許使用一種語言以外的機制來建立實例,而不是使用普通的構造方法。然而,有一種技術能夠大大下降這些風險。這種技術稱爲序列化代理模式(serialization proxy pattern)。git

序列化代理模式至關簡單。首先,設計一個私有靜態嵌套類,它簡潔地表示外圍類實例的邏輯狀態。這個嵌套類稱爲外圍類的序列化代理。它應該有一個構造方法,其參數類型是外圍類。這個構造方法只是從它的參數拷貝數據:它不須要作任何一致性檢查或防護性拷貝。按照設計,序列化代理的默認序列化形式是外圍類的最好的序列化形式。外圍類及其序列化代理都必須聲明以實現Serializable。github

例如,考慮在條目 50中編寫的不可變Period類,並在條目 88中進行序列化。如下是該類的序列化代理。 Period很是簡單,其序列化代理與該屬性具備徹底相同的屬性:安全

// Serialization proxy for Period class
private static class SerializationProxy implements Serializable {
    private final Date start;

    private final Date end;

    SerializationProxy(Period p) {
        this.start = p.start;
        this.end = p.end;
    }

    private static final long serialVersionUID =
        234098243823485285L; // Any number will do (Item  87)
}

接下來,將如下writeReplace方法添加到外圍類中。能夠將此方法逐字複製到具備序列化代理的任何類中:ui

// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
    return new SerializationProxy(this);
}

該方法在外圍類上的存在,致使序列化系統發出SerializationProxy實例,而不是外圍類的實例。換句話說,writeReplace方法在序列化以前將外圍類的實例轉換爲它的序列化代理。this

使用此writeReplace方法,序列化系統永遠不會生成外圍類的序列化實例,但攻擊者可能會構造一個實例,試圖違反類的不變性。 要確保此類攻擊失敗,只需把readObject方法添加到外圍類中:設計

// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream)
        throws InvalidObjectException {
    throw new InvalidObjectException("Proxy required");
}

最後,在SerializationProxy類上提供一個readResolve方法,該方法返回外圍類邏輯等效的實例。此方法的存在致使序列化系統在反序列化時把序列化代理轉換回外圍類的實例。代理

這個readResolve方法只使用其公共API建立了一個外圍類的實例,這就是該模式的美妙之處。它在很大程度上消除了序列化的語言外特性,由於反序列化實例是使用與任何其餘實例相同的構造方法、靜態工廠和方法建立的。這使你沒必要單獨確保反序列化的實例聽從類的不變量。若是類的靜態工廠或構造方法確立了這些不變性,而它的實例方法維護它們,那麼就確保了這些不變性也將經過序列化來維護。code

如下是Period.SerializationProxyreadResolve方法:對象

// readResolve method for Period.SerializationProxy
private Object readResolve() {
    return new Period(start, end);    // Uses public constructor
}

與防護性拷貝方法(第357頁)同樣,序列化代理方法能夠阻止僞造的字節流攻擊(條目 88,第354頁)和內部屬性盜用攻擊(條目 88, 第356頁)。 與前兩種方法不一樣,這一方法容許Period類的屬性爲final,這是Period類成爲真正不可變所必需的(條目 17)。 與以前的兩種方法不一樣,這個方法並無涉及不少想法。 不你必弄清楚哪些屬性可能會被狡猾的序列化攻擊所破壞,也沒必要顯示地進行有效性檢查,做爲反序列化的一部分。

還有另外一種方法,序列化代理模式比readObject中的防護性拷貝更爲強大。 序列化代理模式容許反序列化實例具備與最初序列化實例不一樣的類。 你可能認爲這在實踐中沒有有用,但並不是如此。

考慮EnumSet類的狀況(條目 36)。 這個類沒有公共構造方法,只有靜態工廠。 從客戶端的角度來看,它們返回EnumSet實例,但在當前的OpenJDK實現中,它們返回兩個子類中的一個,具體取決於底層枚舉類型的大小。 若是底層枚舉類型包含64個或更少的元素,則靜態工廠返回RegularEnumSet; 不然,他們返回一個JumboEnumSet

如今考慮,若是你序列化一個枚舉集合,集合枚舉類型有60個元素,而後將五個元素添加到這個枚舉類型,再反序列化枚舉集合。序列化時,這是一個RegularEnumSet實例,但一旦反序列化,最好是JumboEnumSet實例。事實上正是這樣,由於EnumSet使用序列化代理模式。若是好奇,以下是EnumSet的序列化代理。其實很簡單:

// EnumSet's serialization proxy
private static class SerializationProxy <E extends Enum<E>>
        implements Serializable {
    // The element type of this enum set.
    private final Class<E> elementType;

    // The elements contained in this enum set.
    private final Enum<?>[] elements;

    SerializationProxy(EnumSet<E> set) {
        elementType = set.elementType;
        elements = set.toArray(new Enum<?>[0]);
    }

    private Object readResolve() {
        EnumSet<E> result = EnumSet.noneOf(elementType);
        for (Enum<?> e : elements)
            result.add((E)e);
        return result;
    }

    private static final long serialVersionUID =
        362491234563181265L;
}

序列化代理模式有兩個限制。它與用戶可擴展的類不兼容(條目 19)。並且,它與一些對象圖包含循環的類不兼容:若是試圖從對象的序列化代理的readResolve方法中調用對象上的方法,獲得一個ClassCastException異常,由於你尚未對象,只有該對象的序列化代理。

最後,序列化代理模式加強的功能和安全性並非免費的。 在個人機器上,使用序列化代理序列化和反序列化Period實例,比使用防護性拷貝多出14%的昂貴開銷。

總之,只要發現本身必須在不能由客戶端擴展的類上編寫readObject或writeObject方法時,請考慮序列化代理模式。 使用重要不變性來健壯序列化對象時,這種模式多是最簡單方法。

相關文章
相關標籤/搜索