第76条:保护性地编写readObject方法
当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的。因此,对于每个可序
列化的不可变类,如果它包含了私有的可变组件,那么在它的readObject方法中,必须要对这些组件进行保护性拷贝。
有一个简单的"石蕊"测试,可以用来确定默认的readObject方法是否可以接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非瞬时的域,并且无论参
数的值是什么,都是不进行检查就可以保存到相应的域中的。对于这样的做法,你是否会感到很舒适?如果你对这个问题不能回答"是",就必须提供一个显式的
readObject方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern,见第78条
)。
public class Period implements Serializable{
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if(this.start.compareTo(this.end) > 0){
throw new IllegalArgumentException(start + "after" + end);
}
this.start = start;
this.end = end;
}
public Date start(){
return new Date(start.getTime());
}
public Date end(){
return new Date(end.getTime());
}
@Override
public String toString(){
return "start:" + start + " , end:" + end;
}
/**
* 序列化外围类时,调用内部的静态代理类,最后其实是序列化了一个内部的代理类的参数,所以不影响外部类的实例。
* @return
*/
private Object writeReplace(){
return new SerializabtionProxy(this);
}
/**
* 如果攻击者伪造了一个字节码文件,为了确保这种攻击无法得逞,只要外围类的readObject方法直接抛异常即可。
* @param stream
* @throws InvalidObjectException
*/
private void readObject(ObjectInputStream stream) throws InvalidObjectException{
throw new InvalidObjectException("Proxy required!");
}
/**
* 序列化代理类,他精确表示了其当前外围类对象的状态!最后序列化时会将这个私有内部内进行序列化!
*/
private static class SerializabtionProxy implements Serializable{
private final Date start;
private final Date end;
SerializabtionProxy(Period p){
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID = 1L;
/**
* 反序列化这个类时,虚拟机会调用这个方法,最后返回的对象是一个Period对象!这里同样调用了Period的构造函数,
* 会进行构造函数的一些校验!
*/
private Object readResolve(){
// 这里进行保护性拷贝!
return new Period(new Date(start.getTime()), new Date(end.getTime()));
}
}
}
对于非final的可序列化的类,在readObject方法和构造器之间还有其他类似的地方。readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可
以(见第17条)。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。程序很可能会失败。
总而言之,每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这
个字节流一定代表着一个真正被序列化过的实例。虽然在本条目的例子中,类使用了默认的序列化形式,但是,所有讨论到的有可能发生的问题也同样适用于使用自定义
序列化形式的类。下面以摘要的形式给出一些指导方针,有助于编写出更加健壮的readObject方法:
1 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
2 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
3 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口。
4 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。