转载

《Effective Java》Second Edition 第11章 序列化

第11章 序列化

序列化技术为远程通信提供了标准的线路级(wire-level)对象表示法,也为JavaBeans组件结构提供了标准的持久化数据格式。

第74条:谨慎地实现Serializable接口

  1. 如果一个类实现了Serializable接口,它的字节流编码(或者说序列化形式)就变成了它的导出的API的一部分。一旦这个类被广泛使用,往往必须永远支持这种序列化形式,就好像你必须要支持导出的API的所有其他部分一样。如果你不努力设计一种自定义的序列化形式,而仅仅接受了默认的序列化形式,这种序列化形式将被永远地束缚在该类最初的内部表示法上。换句话说,如果你接受了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API的一部分,这不符合“最低限度地访问域”的实践准则,从而它就失去了作为信息隐藏工具的有效性。
  2. 序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符(stream unique identifier)有关,通常它也被称为序列版本UID。每个可序列化的类都有一个唯一标识号与它相关联。如果你没有在一个名为serialVersionUID的私有静态final的long域中显式地指定该标识号,系统就会自动地根据这个类来调用一个复杂的运算过程,从而在运行时产生该标识号。这个自动产生的值会受到类名称、它所实现的接口的名称、以及所有公有的和受保护的成员的名称所影响。如果你通过任何方式改变了这些信息,比如,增加了一个不很重要的工具方法,自动产生的序列版本UID也会发生变化。因此,如果你没有声明一个显式的序列版本UID,兼容性将会遭到破坏,在运行时导致InvalidClassException异常。
  3. 因为反序列化机制中没有显式的构造器,所以你很容易忘记要确保:反序列化过程必须也要保证所有“由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。
  4. 根据经验,比如Date和BigInteger这样的值类应该实现Serializable,大多数的集合类也应该如此。代表活动实体的类,比如线程池(thread pool),一般不应该实现Serializable。
  5. 为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口。
  6. 在为了继承而设计的类中,真正实现了Serializable接口的有Throwable类、Component和HttpServlet抽象类。因为Throwable类实现了Serializable接口,所以RMI的异常可以从服务器端传到客户端。Component实现了Serializable接口,因此GUI可以被发送、保存和恢复。HttpServlet实现了Serializable接口,因此会话状态(session state)可以被缓存。
  7. 下面的转换增加了一个受保护的无参构造器,和一个初始化方法。初始化方法与正常的构造器具有相同的参数,并且也建立起同样的约束关系。注意保存对象状态(x和y)的变量不能是final的,因为它们是由initialize方法设置的:
// Nonserializable stateful class allowing serializable subclass
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。这样可以确保如果有编写不好的子类没有初始化实例,该方法调用就可以快速而干净地失败。注意init域是一个原子引用(atomic reference)。在遇到特定的情况时,确保对象的完整性是很有必要的。如果没有这样的防范机制,万一有个线程要在某一个实例上调用initialize,而另一个线程又要企图使用这个实例,第二个线程就有可能看到这个实例处于不一致的状态。这种模式利用compareAndSet方法来操作枚举的原子引用,这是一个很好的线程安全状态机(thread-safe state machine)的通用实现。如果有了这样的机制做保证,实现一个可序列化的子类就非常简单明了:

// Serializable subclass of nonserializable stateful class
public class Foo extends AbstractFoo implements Seriablizable {
    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.defaulteWriteObject();

        // 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;
}

内部类(inner class)不应该实现Serializable。它们使用编译器产生的合成域(synthetic field)来保存指向外围实例(enclosing instance)的引用,以及保存来自外围作用域的局部变量的值。“这些域如何对应到类定义中”并没有明确的规定,就好像没有指定匿名类和局部类的名称一样。因此,内部类的默认序列化形式是定义不清楚的。然而,静态成员类(static member class)却可以实现Serializable接口。

第75条:考虑使用自定义的序列化形式

  1. 换句话说,默认的序列化形式描述了该对象内部所包含的数据,以及每一个可以从这个对象到达的其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。
  2. 如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。
  3. 即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。
// Awful candidate for default serialized form
public final class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }

    ... // Remainder omitted
}

从逻辑意义上讲,这个类表示了一个字符串序列。但是从物理意义上讲,它把该序列表示成一个双向链表。如果你接受了默认的序列化形式,该序列化形式将不遗余力地镜像出(mirror)链表中的所有项,以及这些项之间的所有双向链接。

它使这个类的导出API永远地束缚在该类的内部表示法上。在上面的例子中,私有的String.Entry类变成了公有API的一部分。如果在将来的版本中,内部表示法发生了变化,StringList类仍将需要接受链表形式的输入,并产生链表形式的输出。

它会消耗过多的空间。在上面的例子中,序列化形式既表示了链表中的每个项,也表示了所有的链接关系,这是不必要的。这些链表项以及链接只不过是实现细节,不值得记录在序列化形式中。

对于StringList类,合理的序列化形式可以非常简单,只需先包含链表中字符串的数目,然后紧跟着这些字符串即可。这样就构成了StringList所表示的逻辑数据,与它的物理表示细节脱离。

// StringList with a reasonable custom serialized form
public final class StringList implements Serializable {
    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) { ... }

    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());
    }
    ... // Remainder omitted
}

如果某一个实例将在未来的版本中被序列化,然后在前一个版本中被反序列化,那么,后增加的域将被忽略掉。如果旧版本的readObject方法没有调用defaultReadObject,反序列化过程将失败,引发StreamCorruptedException异常。

因此,每一个可以被标记为transient的实例域都应该做上这样的标记。这包括那些冗余的域,即它们的值可以根据其他“基本数据域”计算而得到的域,比如缓存起来的散列值。它也包括那些“其值依赖于JVM的某一次运行”的域,比如一个long域代表了一个指向本地数据结构的指针。

如果你正在使用一种自定义的序列化形式,大多数实例域,或者所有的实例域则都应该被标记为transient,就像上面例子中的StringList那样。

只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能使用默认的序列化形式;否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。

第76条:保护性地编写readObject方法

  1. 为了修正这个问题,你可以为Period提供一个readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化之后的对象的有效性。如果有效性检查失败,readObject方法就抛出一个InvalidObjectException异常,使反序列化过程不能成功地完成。
  2. 当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的。因此,对于每个可序列化的不可变类,如果它包含了私有的可变组件,那么在它的readObject方法中,必须要对这些组件进行保护性拷贝。
  3. 有一个简单的“石蕊”测试,可以用来确定默认的readObject方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非transient的域,并且无论参数的值是什么,都是不进行检查就可以保存到相应的域中的。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的readObject方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern)。
  4. 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
  5. 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。

第77条:对于实例控制,枚举类型优先于readResolve

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

    public void leaveTheBuilding() { ... }
}

如果这个类的声明中加上了“implements Serializable”的字样,它就不再是一个Singleton。无论该类使用了默认的序列化形式,还是自定义的序列化形式,都没有关系;也跟它是否提供了显式的readObject方法无关。任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

事实上,如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域都必须声明为transient的。否则,那种破釜沉舟式的攻击者,就有可能在readResolve方法被运行之前,保护指向反序列化对象的引用。

如果反过来,你将一个可序列化的实例受控的类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例。JVM对此提供了保障,这一点你可以确信无疑。从你这方面来讲,并不需要特别注意什么。以下是把Elvis写成枚举的例子:

// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

总而言之,你应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控(instance-controlled)的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient的。

第78条:考虑用序列化代理代替序列化实例

  1. 序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理(serialization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。从设计的角度来看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现Serializable接口。
// 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
}

接下来,将下面的writeReplace方法添加到外围类中。通过序列化代理,这个方法可以被逐字地复制到任何类中:

// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
    return new SerializationProxy(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创建外围类的一个实例,这正是该模式的魅力之所在。它极大地消除了序列化机制中语言本身之外的特征,因为反序列化实例是利用与任何其他实例相同的构造器、静态工厂和方法而创建的。这样你就不必单独确保被反序列化的实例一定要遵守类的约束条件。如果该类的静态工厂或者构造器建立了这些约束条件,并且它的实例方法在维持着这些约束条件,你就可以确信序列化也会维持这些约束条件。

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

总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。

原文地址:http://www.jsdblog.com/article/63  

正文到此结束
Loading...