ITEM 90: 使用代理模式替代直接序列化

ITEM 90: CONSIDER SERIALIZATION PROXIES INSTEAD OF SERIALIZED INSTANCES
  正如在iten 85 和 item 86 中提到的以及本章所讨论的,实现 Serializable 的决定增加了出现 bug和安全问题的可能性,因为它允许使用一种语言之外的机制来创建实例,而不是使用普通的构造函数。然而,有一种技术可以大大降低这些风险。这种技术称为序列化代理模式。
  序列化代理模式相当简单。首先,设计一个私有的静态嵌套类,它可以精确地表示封闭类的实例的逻辑状态。这个嵌套类称为封闭类的序列化代理。它应该有一个构造函数,其参数类型是封闭类。这个构造函数只从它的参数复制数据:它不需要做任何一致性检查或防御性复制。按照设计,序列化代理的默认序列化形式是封装类的完美序列化形式。要实现 Serializable,必须声明封闭类及其序列化代理。
  例如,考虑在 item 50 中编写的不可变的 Period 类,并在 item 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 方法在序列化之前将封装类的实例转换为它的序列化代理。
  有了这个 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创建其实例,这就是该模式的美妙之处。它在很大程度上消除了序列化的语言外特征,因为反序列化实例是使用与其他实例相同的构造函数、静态工厂和方法创建的。这使您不必单独确保反序列化的实例遵守类的不变量。如果类的静态工厂或构造函数建立了这些不变量,并且类的实例方法维护了它们,那么您就确保了这些不变量也将通过序列化来维护。
  下面是 Period 为 Period.SerializationProxy 提供的方法:

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

  与防御性复制方法(第357页)一样,序列化代理方法可以阻止伪字节流攻击(第354页)和内部字段盗窃攻击(第356页)。与前两种方法不同,这种方法允许 Period 的字段为final,这是使 Period 类真正不可变所必需的(Item 17)。与前两种方法不同的是,这一种方法不需要太多的思考。您不必找出哪些字段可能会被不正当的序列化攻击破坏,也不必在反序列化过程中显式地执行有效性检查。
  在另一种方式中,序列化代理模式比 readObject 中的防御性复制更强大。序列化代理模式允许反序列化实例拥有与原始序列化实例不同的类。你可能不认为这在实践中有用,但它是有用的。
  考虑 EnumSet (第36项)的情况。这个类没有公共构造函数,只有静态工厂。从客户端的角度来看,它们返回 EnumSet 实例,但是在当前的 OpenJDK 实现中,它们返回两个子类中的一个,具体取决于底层枚举类型的大小。如果底层枚举类型有 64 个或更少的元素,静态工厂返回一个 RegularEnumSet;否则,它们返回一个JumboEnumSet。
  现在考虑如果你序列化 enum 集合枚举类型的六十元素,然后将五个元素添加到枚举类型,然后反序列化枚举集。这是一个 RegularEnumSetinstance 序列化时,但最好是一旦反序列化 JumboEnumSet 实例。事实上,正是这样,因为 EnumSet 使用了序列化代理模式。如果您感到好奇,这里是 EnumSet 的序列化代理。其实很简单:

// EnumSet's serialization proxy
private static class SerializationProxy > implements Serializable {
  // The element type of this enum set. 
  private final Class elementType;
  // The elements contained in this enum set. 
  private final Enum[] elements;
  SerializationProxy(EnumSet set) { 
    elementType = set.elementType;
    elements = set.toArray(new Enum[0]);
  }

  private Object readResolve() {
    EnumSet result = EnumSet.noneOf(elementType); 
    for (Enum e : elements)
      result.add((E)e); 
    return result;
  }
  
  private static final long serialVersionUID = 362491234563181265L;
}

  序列化代理模式有两个限制。它与用户可扩展的类不兼容(item 19)。而且,它与对象图包含循环的某些类不兼容:如果您试图从这样一个对象的序列化代理的 readResolve方法中调用该对象上的方法,您将得到一个 ClassCastException,因为您还没有对象,只有它的序列化代理。
  最后,序列化代理模式所增加的功能和安全性并不是免费的。在我的机器上,使用序列化代理序列化和反序列化Period实例比使用防御性复制要慢14%。
  总之,当您发现自己必须在客户端不能扩展的类上编写 readObject 或 writeObject 方法时,请考虑序列化代理模式。此模式可能是使用非平凡不变量健壮地序列化对象的最简单方法。

你可能感兴趣的:(ITEM 90: 使用代理模式替代直接序列化)