89. 对于实例控制,枚举类型优于 readResolve

      正如在第 3 条中所提到的,如果在这个类上面增加 的字样,它就不是一个单例。无论该类使用了默认的序列化形式,还是自定义的序列化形式(详见 87 条),都没有关系;也跟它是否使用了显式的 readObject(详见 88 条)无关。任何一个 readObject 方法,不管是显式的还是默认的,都会返回一个新建的实例,这个新建的实例不同于类初始化时创建的实例。

      readResolve 特性允许你用 readObject 创建的实例代替另外一个实例[Serialization, 3.7]。对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve 方法,并且具备正确的声明,那么在反序列化之后,新建对象上的 readResolve 方法就会被调用。然后,该方法返回的对象引用将会被回收,取代新建的对象。这个特性在绝大多数用法中,指向新建对象的引用不会再被保留,因此成为垃圾回收的对象。

      如果 Elvis 类要实现 Serializable 接口,下面的 readResolve 方法就足以保证它的单例属性:

    1. // readResolve for instance control - you can do better!
    2. private Object readResolve() {
    3. // Return the one true Elvis and let the garbage collector
    4. // take care of the Elvis impersonator.
    5. return INSTANCE;
    6. }

      该方法忽略了被反序列化的对象,只返回类初始化创建的那个特殊的 Elvis 实例。因此 Elvis 实例的序列化形式不应该包含任何实际的数据;所有的实例字段都应该被声明为 transient。事实上,如果依赖 readResolve 进行实例控制,带有对象引用类型的所有实例字段都必须声明为 。 否则,那种破釜沉舟式的攻击者,就有可能在 readResolve 方法运行之前,保护指向反序列化对象的引用,采用的方式类似于在第 88 条中提到的 MutablePeriod 攻击。

      以下是它更详细的工作原理。首先编写一个「盗用者」类,它既有 readResolve 方法,又有实例字段,实例字段指向被序列化的单例的引用,「盗用者」就「潜伏」在其中。在序列化流中,用「盗用者」的 readResolve 方法运行时,它的实例字段仍然引用部分反序列化(并且还没有被解析)的 Singletion。

      「盗用者」的 readResolve 方法从它的实例字段中将引用复制到静态字段中,以便该引用可以在 readResolve 方法运行之后被访问到。然后这个方法为它所藏身的那个域返回一个正确的类型值。如果没有这么做,当序列化系统试着将「盗用者」引用保存到这个字段时,虚拟机就会抛出 ClassCastException

      为了更具体的说明这一点,我们以如下这个单例模式为例:

      如下「盗用者」类,是根据以上描述构造的:

    1. static Elvis impersonator;
    2. private static final long serialVersionUID = 0;
    3. private Elvis payload;
    4. private Object readResolve() {
    5. // Save a reference to the "unresolved" Elvis instance
    6. return new String[] { "A Fool Such as I" };
    7. }
    8. }

      这个程序会产生如下的输出,最终证明可以创建两个截然不同的 Elvis 实例(包含两种不同的音乐品味):

    1. [Hound Dog, Heartbreak Hotel]

      通过将 favoriteSongs 字段声明为 transient,可以修复这个问题,但是最好把 Elvis 做成一个单元素的枚举类型(详见第 3 条)。就如 ElvisStealer 攻击所示范的,用 readResolve 方法防止“临时”被反序列化的实例收到攻击者的访问,这种方法十分脆弱需要万分谨慎。

      如果将一个可序列化的实例受控的类编写为枚举,Java 就可以绝对保证除了所声明的常量之外,不会有其他实例,除非攻击者恶意的使用了享受特权的方法。如 AccessibleObject.setAccessible。能够做到这一点的任何一位攻击者,已经拥有了足够的特权来执行任意的本地代码,后果不堪设想。将 Elvis 写成枚举的例子如下所示:

      用 readResolve 进行实例控制并不过时。如果必须编写可序列化的实力受控的类,在编译时还不知道它的实例,你就无法将类表示成为一个枚举类型。

      总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 readResolve 方法,并确保该类的所有实例化字段都被基本类型,或者是 的。