10.6.3 对象替换

在进行反序列化操作时,会创建出新的Java对象。从某种意义上来说,反序列化的作用相当于一个构造方法。有些Java类对于其对象实例有自己的管理逻辑,例如使用单例模式时,要求某个类在虚拟机中只有一个实例。对于这样的Java类,在反序列化时,需要对得到的对象进行处理,以保证序列化机制不会破坏Java类本身的约束,否则,每进行一次反序列化操作,都会创建一个新的对象,就破坏了单例模式要求的约束条件。在其他情况下,可能需要在序列化时使用另外一个对象来代替当前对象,原因可能是当前对象中包含了一些不需要被序列化的域,比如这些域是从另外一个域派生而来的。对于这种情况,也可以通过把域声明为transient或使用自定义的writeObject方法和readObject方法来实现。但是如果这样的逻辑比较复杂,可以考虑封装在一个Java类中。另外还可以隐藏实际的类层次结构。比如类A中的某个域引用了类B,在正常的序列化过程中,类A和类B都会出现在序列化之后的字节流中,如果希望在字节流中隐藏类B,可以用另外一个类的对象来代替类B的对象。

在序列化时进行的对象替换操作由一组对应的writeReplace方法和readResolve方法来完成。在writeReplace方法中可以根据当前对象创建一个新的对象作为替代。这个替代对象被写入到ObjectOutputStream类的对象中。同样的,在readResolve方法中,可以对读取出来的对象进行转换,把转换的结果作为反序列化的结果返回。

通过一个示例来说明,在一个订单管理系统中,使用Order类的对象来表示实际的订单。在Order类中引用了User类,用来表示该订单的客户信息。如果使用默认的序列化机制,在Order类的对象被序列化时,其引用的User类的对象也会被序列化。为了满足使用的需求,需要把Order类的对象序列化后的字节流通过网络的方式来传输。通过网络传输可能会带来一些安全隐患,比如序列化后的内容被第三方窃取,造成客户信息的泄露。比较好的做法是为Order类定义一个专门的传输格式,只包含尽可能少的信息。进行序列化时,实际上使用的是专门的传输对象。代码清单10-20给出了Order类的代码。在Order类的writeReplace方法中,从当前对象中创建出一个传输时使用的OrderTO类的对象。

代码清单10-20 序列化时进行对象替换的示例


public class Order implements Serializable{

private User user;

private String id;

public Order(String id, User user){

this.id=id;

this.user=user;

}

public String getId(){

return this.id;

}

private Object writeReplace()throws ObjectStreamException{

return new OrderTO(this);

}

}


代码清单10-21给出了OrderTO类的实现。OrderTO类本身使用了默认的序列化格式,只包含OrderTO类中表示订单编号的orderId域的内容。在调用readResolve方法时,基本的反序列化操作已经完成,orderId域已经被初始化为正确的值。在readResolve方法中通过相关的查找逻辑根据域orderId的值得到对应的Order类的对象。把Order类的对象作为readResolve方法的返回值,即反序列化的最终结果。对调用者来说,替换对象的存在是透明的。

代码清单10-21 替换对象的Java类


public class OrderTO implements Serializable{

private String orderId;

public OrderTO(Order order){

this.orderId=order.getId();

}

private Object readResolve()throws ObjectStreamException{

return OrderGateway.getOrder(orderId);

}

}


经过对象替换之后,序列化之后的字节流中只包含订单的编号,并没有其他额外的信息。因此,即便被第三方窃取,也不会出现信息泄露。