Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java
正如在条目 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方法添加到外围类中:spa
// 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
方法:code
// 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方法时,请考虑序列化代理模式。 使用重要不变性来健壮序列化对象时,这种模式多是最简单方法。