《Effective Java》——學習筆記(序列化)

序列化

第74條:謹慎地實現Serializable接口

實現Serializable接口而付出的最大代價是,一旦一個類被髮布,就大大降低了“改變這個類的實現”的靈活性,並且以後又要改變這個類的內部表示法,結果可能導致序列化形式的不兼容

序列化會使類的演變受到限制,這種限制的一個例子與流的唯一標識符有關,通常它也被稱爲序列版本UID。如果沒有在一個名爲serialVersionUID的私有靜態final的long域中顯式地指定該標識號,系統就會自動地根據這個類來調用一個複雜的運算過程,從而在運行時產生該標識號。這個自動產生的值會受到類名稱、它所實現的接口的名稱、以及所有公有的和受保護的成員的名稱所影響,如果通過任何方式改變了這些信息,自動產生的序列版本UID也會發生變化,在運行時導致InvalidClassException異常

實現Serializable的第二個代價是,它增加了出現Bug和安全漏洞的可能性。序列化機制是一種語言之外的對象創建機制,依靠默認的反序列化機制,很容易使對象的約束關係遭到破壞,以及遭受到非法訪問

實現Serializable的第三個代價是,隨着類發行新的版本,相關的測試負擔也增加了,因爲要檢查是否可以“在新版本中序列化一個實例,然後在舊版本中反序列化”,反之亦然

爲了繼承而設計的類應該儘可能少地去實現Serializable接口,用戶的接口也應該儘可能少地繼承Serializable接口,如果一個類或者接口存在的目的主要是爲了參與到某個框架中,該框架要求所有的參與者都必須實現Serializable接口,那麼,對於這個類或者接口來說,實現或者擴展Serializable接口就是非常有意義的

在“允許子類實現Serializable接口”或“禁止子類實現Serializable接口”兩者之間的一個折衷設計方案是,提供一個可訪問的無參構造器,這種設計方案允許(但不要求)子類實現Serializable接口

public abstract class AbstractFoo {
    private int x, y;  // Our state

    // This enum and field are used to track initialization
    private enum State { NEW, INITIALIZING, INITIALIZED };
    private final AtomicReference<State> init =
        new AtomicReference<State>(State.NEW);

    public AbstractFoo(int x, int y) { initialize(x, y); }

    // This constructor and the following method allow
    // subclass's readObject method to initialize our state.
    protected AbstractFoo() { }

    protected final void initialize(int x, int y) {
        if (!init.compareAndSet(State.NEW, State.INITIALIZING))
            throw new IllegalStateException(
                "Already initialized");
        this.x = x;
        this.y = y;
        // Do anything else the original constructor did
        init.set(State.INITIALIZED);
    }

     // These methods provide access to internal state so it can
     // be manually serialized by subclass's writeObject method.
    protected final int getX() { checkInit(); return x; }
    protected final int getY() { checkInit(); return y; }
    // Must call from all public and protected instance methods
    private void checkInit() {
        if (init.get() != State.INITIALIZED)
            throw new IllegalStateException("Uninitialized");
    }
    // Remainder omitted
}

AbstractFoo中所有公有的和受保護的實例方法在開始做任何其他工作之前都必須先調用checkInit,這樣可以確保如果有編寫不好的子類沒有初始化實例,該方法調用就可以快速而乾淨地失敗

public class Foo extends AbstractFoo implements Serializable {
    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();

        // Manually deserialize and initialize superclass state
        int x = s.readInt();
        int y = s.readInt();
        initialize(x, y);
    }

    private void writeObject(ObjectOutputStream s)
            throws IOException {
        s.defaultWriteObject();

        // Manually serialize superclass state
        s.writeInt(getX());
        s.writeInt(getY());
    }

    // Constructor does not use the fancy mechanism
    public Foo(int x, int y) { super(x, y); }

    private static final long serialVersionUID = 1856835860954L;
}

第75條:考慮使用自定義的序列化形式

如果沒有先認真考慮默認的序列化形式是否合適,則不要貿然接受,如果確定了默認的序列化形式是合適的,通常還必須提供一個readObject方法以保證約束關係和安全性

自定義的序列化示例如下:

public final class StringList implements Serializable {
    // transient修飾符表明這個實例域將從一個類的默認序列化形式中省略掉
    private transient int size   = 0;
    private transient Entry head = null;

    // No longer Serializable!
    private static class Entry {
        String data;
        Entry  next;
        Entry  previous;
    }

    // Appends the specified string to the list
    public final void add(String s) {
        // Implementation omitted
    }

    /**
     * Serialize this {@code StringList} instance.
     *
     * @serialData The size of the list (the number of strings
     * it contains) is emitted ({@code int}), followed by all of
     * its elements (each a {@code String}), in the proper
     * sequence.
     */
    private void writeObject(ObjectOutputStream s)
            throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (Entry e = head; e != null; e = e.next)
            s.writeObject(e.data);
    }

    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int numElements = s.readInt();

        // Read in all elements and insert them in list
        for (int i = 0; i < numElements; i++)
            add((String) s.readObject());
    }

    private static final long serialVersionUID = 93248094385L;
    // Remainder omitted
}

注意,writeObject方法和readObject方法的首要任務都應該是調用defaultWriteObject和defaultReadObject,這樣得到的序列化形式允許在以後的發行版本中增加非transient的實例域,並且還能保持向前或者向後兼容性

如果使用默認的序列化形式,並且把一個或者多個域標記爲transient,則當一個實例被反序列化時,這些域將被初始化爲它們的默認值

如果在讀取整個對象狀態的任何其他方法上強制任何同步,則也必須在對象序列化上強制這種同步。因此,如果有一個線程安全的對象,它通過同步每個方法實現了它的線程安全,並且選擇使用默認的序列化形式,就要使用下列的writeObject方法:

private synchronized void writeObject(ObjectOutputStream s)
            throws IOException {
    s.defaultWriteObject();                
}

不管選擇了哪種序列化形式,都要爲自己編寫的每個可序列化的類聲明一個顯式的序列版本UID,避免序列版本UID成爲潛在的不兼容根源

private static final long serialVersionUID = randomLongValue;

第76條:保護性地編寫readObject方法

在readObject方法中首先調用defaultReadObject,然後檢查被反序列化之後的對象的有效性,如果有效性檢查失敗,就拋出一個InvalidObjectException異常,使反序列化過程不能成功地完成,這樣做的目的是避免攻擊者創建無效的實例

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();

    // Check that out invariants are satisfied
    if(start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

當一個對象被反序列化的時候,對於客戶端不應該擁有的對象引用,如果哪個域包含了這樣的對象引用,就必須要做保護性拷貝,這是非常重要的

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();

    // Defensively copy our mutable components
    start = new Date(start.getTime());
    end = new Date(end.getTime());

    // Check that out invariants are satisfied
    if(start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

注意,保護性拷貝是在有效性檢查之前進行的

有一個簡單的“石蕊”測試,可以用來確定默認的readObject方法是否被接受,測試方法:增加一個公有的構造器,其參數對應於該對象中每個非transient的域,並且無論參數的值是什麼,都是不進行檢查就可以保存到相應的域中的,如果做不到,就必須提供一個顯式的readObject方法,並且它必須執行構造器所要求的所有有效性檢查和保護性拷貝

下面的指導方針有助於編寫出更加健壯的readObject方法:

  • 對於對象引用域必須保持爲私有的類,要保護性地拷貝這些域中的每個對象,不可變類的可變組件就屬於這一類別
  • 對於任何約束條件,如果檢查失敗,則拋出一個InvalidObjectException異常,這些檢查動作應該跟在所有的保護性拷貝之後
  • 如果整個對象圖在被反序列化之後必須進行驗證,就應該使用ObjectInputValidation接口
  • 無論是直接方式還是間接方式,都不要調用類中任何可被覆蓋的方法

第77條:對於實例控制,枚舉類型優先於readResolve

任何一個readObject方法,不管是顯式的還是默認的,它都會返回一個新建的實例,這個新建的實例不同於該類初始化時創建的實例

readResolve特性允許用readObject創建的實例代替另一個實例,對於一個正在被反序列化的對象,如果它的類定義了一個readResolve方法,並且具備正確的聲明,那麼在反序列化之後,新建對象上的readResolve方法就會被調用。然後,該方法返回的對象引用將被返回,取代新建的對象。如下的readResolve方法保證了Elvis類的Singleton屬性:

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { }

    private String[] favoriteSongs =
        { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }

    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

該方法忽略了被反序列化的對象,只返回該類初始化時創建的那個特殊的Elvis實例,因此,Elvis實例的序列化形式並不需要包含任何實際的數據,所有的實例域都應該被聲明爲transient的

readResolve的可訪問性很重要,如果把readResolve方法放在一個final類上,它就應該是私有的。如果把readResolve方法放在一個非final的類上,就必須認真考慮它的可訪問性,如果readResolve方法是受保護的或者公有的,就適用於所有沒有覆蓋它的子類,如果子類沒有覆蓋它,對序列化過的子類實例進行反序列化,就會產生一個超類實例,這樣有可能導致ClassCastException

第78條:考慮用序列化代理代替序列化實例

序列化代理模式相當簡單,首先,爲可序列化的類設計一個私有的靜態嵌套類,精確地表示外圍類的實例的邏輯狀態。這個嵌套類被稱作序列化代理,它應該有一個單獨的構造器,其參數類型就是那個外圍類,這個構造器只從它的參數中複製數據:它不需要進行任何一致性檢查或者保護性拷貝,從設計的角度來看,序列化代理的默認序列化形式是外圍類最好的序列化形式。外圍類及其序列代理都必須聲明實現Serializable接口

public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    /**
     * @param  start the beginning of the period
     * @param  end the end of the period; must not precede start
     * @throws IllegalArgumentException if start is after end
     * @throws NullPointerException if start or end is null
     */
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end   = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(
                          start + " after " + end);
    }

    public Date start () { return new Date(start.getTime()); }

    public Date end ()   { return new Date(end.getTime()); }

    public String toString() { return start + " - " + end; }

    // 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 75)

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

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

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

通過writeReplace方法,產生一個SerializationProxy實例,代替外圍類的實例,有了writeReplace方法,序列化系統永遠不會產生外圍類的序列化實例,最後,在SerializationProxy類中提供一個readResolve方法,它返回一個邏輯上相當的外圍類的實例。這個方法的出現,導致序列化系統在反序列化時將序列化代理轉變回外圍類的實例

序列化代理方法可以阻止僞字節流的攻擊以及內部域的盜用攻擊,這種方法不必要知道哪些域可能受到狡猾的序列化攻擊的威脅,也不必顯式地執行有效性檢查

序列化代理模式有兩個侷限性,它不能與可以被客戶端擴展的類兼容,它也不能與對象圖中包含循環的某些類兼容:如果企圖從一個對象的序列化代理的readResolve方法內部調用這個對象中的方法,就會得到一個ClassCastException異常,因爲還沒有這個對象,只有它的序列化代理

總之,每當必須在一個不能被客戶端擴展的類上編寫readObject或者writeObject方法的時候,就應該考慮使用序列化代理模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章