第90项:考虑用序列化代理代替序列化实例

  正如第85项和第86项中提到以及本章中所讨论的,决定实现Serializable接口,会增加bug和出现安全问题的可能性,因为它导致实例要利用语言之外的机制来创建,而不是用普通的构造器。然而,有一种方法可以极大地减少这些风险。这种方法就是序列化代理模式(serialization proxy pattern)

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

  例如,考虑第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方法添加到外围类中。通过序列化代理,这个方法可以被逐字地复制到任何类中:

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

  这个方法的存在导致序列化系统产生一个SerializationProxy实例,代替外围类的实例。换句话说,writeReplace方法在序列化之前,将外围类的实例转变成了它的序列化代理。

  有个这个writeObject方法之后,序列化系统永远不会产生外围类的序列化实例,但是攻击者有可能伪造,企图违反该类的约束条件。为了确保这种攻击无法得逞,只要在外围类中添加这个readObject方法即可:

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

  最后,在SerializationProxy类中提供一个readResolve方法,它返回一个逻辑上相当的外围类实例。这个方法的出现,导致序列化系统在反序列化时将序列化代理转换回外围类的实例。

  这个readResolve方法仅仅利用它的公有API创建外围类的一个实例,这正是该模式的美丽之所在。它极大地消除了序列化机制中语言本身之外的特征,因为反序列化实例是利用与任何其他实例相同的构造器、静态工厂和方法而创建的。这样你就不必单独确保被序列化的实例一定要遵守类的约束条件。如果该类的静态工厂或者构造器建立了这些约束条件,并且它的实例方法在维持着这些约束条件,你就可以确信序列化也会维持这些约束条件。

  以下是上述为Period.SerializationProxy提供的readResolve方法:

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

  正如保护性拷贝方法一样(原书第357页),序列化代理方法可以阻止伪字节流的攻击(原书第354页)以及内部域的盗用攻击(原书第356页)。与前两种方法不同,这种方法允许Period的域为final的,为了确保Period类真正是不可变对的(第17项),这一点很有必要。与前两种方法不同的还有,这种方法不需要太费心思。你不必知道哪些域可能受到狡猾的序列化攻击的威胁,你也不必显示地执行有效性检查,作为反序列化的一部分。

  还有另一种方法,序列化代理模式比readObject中的保护性拷贝更强大。序列化代理模式允许反序列化实例有着与原始序列化实例不同的类。你可能认为这在实际应用中没有什么作用,其实不然。

  考虑EnumSet的情况(第36项)。这个类没有公有的构造器,只有静态工厂。在当前的OpenJDK的实现中,从客户端的角度来看,它们返回EnumSet实例,但是实际上,它们是返回两种子类之一,具体取决于底层枚举类型的大小。如果底层的枚举类型有64个或少于64个元素,静态工厂就返回一个RegularEnumSet;否则,它们就返回一个JumboEnumSet。

  现在考虑这种情况:如果序列化一个枚举集合,它的枚举类型有60个元素,然后给这个枚举类型再增加5个元素,之后反序列化这个枚举集合。当它被序列化的时候,是一个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方法的时候,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。

上一篇:A - Period(kmp的next数组的应用)


下一篇:jmeter之Ramp-up Period(in seconds)