89. 对于实例控制,枚举类型优于 readResolve
正如在第 3 条中所提到的,如果在这个类上面增加 的字样,它就不是一个单例。无论该类使用了默认的序列化形式,还是自定义的序列化形式(详见 87 条),都没有关系;也跟它是否使用了显式的 readObject
(详见 88 条)无关。任何一个 readObject
方法,不管是显式的还是默认的,都会返回一个新建的实例,这个新建的实例不同于类初始化时创建的实例。
readResolve
特性允许你用 readObject
创建的实例代替另外一个实例[Serialization, 3.7]。对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve
方法,并且具备正确的声明,那么在反序列化之后,新建对象上的 readResolve
方法就会被调用。然后,该方法返回的对象引用将会被回收,取代新建的对象。这个特性在绝大多数用法中,指向新建对象的引用不会再被保留,因此成为垃圾回收的对象。
如果 Elvis 类要实现 Serializable
接口,下面的 readResolve
方法就足以保证它的单例属性:
// readResolve for instance control - you can do better!
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
该方法忽略了被反序列化的对象,只返回类初始化创建的那个特殊的 Elvis
实例。因此 Elvis
实例的序列化形式不应该包含任何实际的数据;所有的实例字段都应该被声明为 transient
。事实上,如果依赖 readResolve
进行实例控制,带有对象引用类型的所有实例字段都必须声明为 。 否则,那种破釜沉舟式的攻击者,就有可能在 readResolve
方法运行之前,保护指向反序列化对象的引用,采用的方式类似于在第 88 条中提到的 MutablePeriod
攻击。
以下是它更详细的工作原理。首先编写一个「盗用者」类,它既有 readResolve
方法,又有实例字段,实例字段指向被序列化的单例的引用,「盗用者」就「潜伏」在其中。在序列化流中,用「盗用者」的 readResolve
方法运行时,它的实例字段仍然引用部分反序列化(并且还没有被解析)的 Singletion。
「盗用者」的 readResolve
方法从它的实例字段中将引用复制到静态字段中,以便该引用可以在 readResolve
方法运行之后被访问到。然后这个方法为它所藏身的那个域返回一个正确的类型值。如果没有这么做,当序列化系统试着将「盗用者」引用保存到这个字段时,虚拟机就会抛出 ClassCastException
。
为了更具体的说明这一点,我们以如下这个单例模式为例:
如下「盗用者」类,是根据以上描述构造的:
static Elvis impersonator;
private static final long serialVersionUID = 0;
private Elvis payload;
private Object readResolve() {
// Save a reference to the "unresolved" Elvis instance
return new String[] { "A Fool Such as I" };
}
}
这个程序会产生如下的输出,最终证明可以创建两个截然不同的 Elvis
实例(包含两种不同的音乐品味):
[Hound Dog, Heartbreak Hotel]
通过将 favoriteSongs
字段声明为 transient
,可以修复这个问题,但是最好把 Elvis
做成一个单元素的枚举类型(详见第 3 条)。就如 ElvisStealer
攻击所示范的,用 readResolve
方法防止“临时”被反序列化的实例收到攻击者的访问,这种方法十分脆弱需要万分谨慎。
如果将一个可序列化的实例受控的类编写为枚举,Java 就可以绝对保证除了所声明的常量之外,不会有其他实例,除非攻击者恶意的使用了享受特权的方法。如 AccessibleObject.setAccessible
。能够做到这一点的任何一位攻击者,已经拥有了足够的特权来执行任意的本地代码,后果不堪设想。将 Elvis 写成枚举的例子如下所示:
用 readResolve 进行实例控制并不过时。如果必须编写可序列化的实力受控的类,在编译时还不知道它的实例,你就无法将类表示成为一个枚举类型。
总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 readResolve
方法,并确保该类的所有实例化字段都被基本类型,或者是 的。