90. 考虑用序列化代理代替序列化实例

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

      例如,以第 50 条中编写不可变的 Period 类为例,它在第 88 条中被作为可序列化的。以下是一个类的序列化代理。Period 类是如此简单,以致于它的序列化代理有着与类完全相同的字段。

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

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

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

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

      以下是上述的 Period.SerializationProxyreadResolve 方法:

      正如保护性拷贝方法一样(详见 88 条),序列化代理方式可以阻止伪字节流的攻击(详见 88 条)以及内部字段的盗用攻击(详见 88 条)。与前两种方法不同,这种方法允许 Period 类的字段为 final,为了确保 Period 类是真正不可变的(详见 17 条),这一点非常重要。与前两种方法不同的还有,这种方法不需要太费心思。你不必知道哪些字段可能受到狡猾的序列化攻击的威胁,你也不必显式的执行有效性检查,作为反序列化的一部分。

      以 EnumSet 的情况为例(详见 36 条)。这个类没有公有的构造器,只有静态工厂。从客户端的角度来看,他们返回 EnumSet 实例,但是在 OpenJDK 的实现,它们返回的是两种子类之一,具体取决于底层枚举类型的大小。如果底层的枚举类型有 64 个或者少于 64 个的元素,静态工厂就返回一个 RegularEnumSet;它们就返回一个 。

      现在考虑这种情况:如果序列化一个枚举类型,它的枚举有 60 个元素,然后给这个枚举类型再增加 5 个元素,之后反序列化这个枚举集合。当它被序列化的时候,是一个 RegularEnumSet 实例,但是它一旦被反序列化,他就变成了 JunmboEnumSet 实例。实际发生的情况也正是如此,因为 EnumSet 使用序列化代理模式如果你感兴趣,可以看看如下的 EnumSet 序列化代理,它实际上就是这么简单:

      序列化代理模式有两个局限性。它不能与可以被客户端拓展的类兼容(详见 19 条)。它也不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的 readResovle 方法内部调用这个对象的方法,就会得到一个 ClassCastException 异常,因为你还没有这个对象,只有它的序列化代理。

      最后一点,序列化代理模式所增强的功能和安全性不是没有代价。在我的机器上,通过序列化代理来序列化和反序列化 Period 实例的开销,比使用保护性拷贝增加了 14%。