Object readResolve()
这个方法会紧挨着readObject()之后被调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject()反序列化的对象将会立即丢弃。
readObject()方法在序列化单例类时尤其有用。当然,如果使用java5提供的enum来定义枚举类,则完全不用担心,程序没有任何问题。
我们从一个单例模式开始:
public final class MySingleton {
private MySingleton() { }
private static final MySingleton INSTANCE = new MySingleton();
public static MySingleton getInstance() { return INSTANCE; }
}
如果实现了序列化,那么会执行readObject方法或默认的序列化。他们都会返回一个新建的实例,也就违反了单例。
readResolve()方法正好满足需求:
public final class MySingleton {
private MySingleton() { }
private static final MySingleton INSTANCE = new MySingleton();
public static MySingleton getInstance() { return INSTANCE; }
private Object readResolve() throws ObjectStreamException {
// instead of the object we're on,
// return the class variable INSTANCE
return INSTANCE;
}
}
书中提到了如果单例实例中存在非transient对象引用,就会有被攻击的危险,例子有点难懂,我在这里简单地解释一下;
因为单例包含一个非transient对象引用域,这个域内容在Singleton的readResolve方法运行之前被反序列化,于是,攻击者截胡,把这段readResolve方法运行之前的字节流截下来(不让他运行readResolve),来个偷梁换柱,把对象引用域指向自己写的“盗用者”。
于是,把readResolve方法运行之前的字节流 换成 偷梁换柱的字节流,再让他执行一次,系统看到单例对象有个非transient对象引用域,指向“盗用者”(因为偷梁换柱了),所以系统也会序列化“盗用者”。
盗用者是这样的:
class ElvisStealer implement Serializable{
static Elvis impersonator;
private Elvis payload;
private Object readResolve(){
impersonator = payload;
return new String[]{"foolish"};
}
}
序列化盗用者“ElvisStealer”的时候,执行它的readResolve()方法,impersonator = payload;偷偷 把第二次序列化的payload 记录下来(作为攻击者,其实没有必要,只是为了展示),然后返回一个错误的对象引用。
但是枚举类型就完全不同了:
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
private String[] favoriteSongs =
{ "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
这样做完全没有后顾之忧,因为枚举enum生来就是支持序列化的,下面的官方对枚举的序列化的声明的翻译:
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum const " + enumType +"." + name);
}
总结:尽可能用枚举类型来控制实例,否则,就要必须提供readResolve方法,并确保类的所有实例域都是基本类型或transient的。