实现了序列化的类,在序列化和反序列化时,由 writeObject 方法把对象写入磁盘,实例的创建是由readObject方法来完成的,因此,每次反序列化出来的对象的地址值都不一样,为了解决这个问题,可以编写 readResolve() 方法,或者用枚举代替实例类。有没有其他的办法呢?我们知道,执行 readObject 时,外部可能会通过伪字节流和内部盗用域来攻击程序,它就是伪造一个字节流,通过 readObject 读取,来改变对象的成员变量的值。鉴于此,我们今天提出的方法是代理。
public class Period implements Serializable {
private static final long serialVersionUID = 234567895125541122L;
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (null == start || null == end || start.after(end)) {
throw new IllegalArgumentException("时间数据错误");
}
this.start = start;
this.end = end;
}
@Override
public String toString() {
return "开始时间:" + start + " , 结束时间:" + end;
}
/**
* 序列化外围类时,虚拟机会转掉这个方法,序列化了一个内部的代理类对象!
*/
private Object writeReplace() {
System.out.println(" writeReplace()方法!");
return new SerializabtionProxy(this);
}
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Proxy request!");
}
/**
* 序列化代理类,序列化时会将这个内部类进行序列化!
*/
private static class SerializabtionProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
SerializabtionProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
/**
* 反序列化这个类时,虚拟机会调用这个方法
*/
private Object readResolve() {
System.out.println("进入SerializabtionProxy 的 readResolve()方法,将返回Period对象!");
// 这里进行保护性拷贝!
return new Period(new Date(start.getTime()), new Date(end.getTime()));
}
}
}
序列化Period时, 会调用调用 writeReplace() 生成一个 SerializabtionProxy 对象, 然后对此对象进行序列化 ,注意,不是对Period类对象进行序列化, 而是 SerializabtionProxy对象,因为我们编写了 writeReplace() 方法,同时也编写了 readObject() 方法,并在里面抛出了一个异常,所以如果有恶意的字节流攻击时,就会执行 readObject(),并马上抛出异常来保护程序;而我们自己执行序列化时,则会执行 writeReplace() 方法,而不会执行 readObject() 方法,所以就保证了安全性。
反序列化时, 会调用 SerializabtionProxy 的 readResolve() 方法生成一个 Period 对象, 然后返回此对象的保护性拷贝的类,由此,就得到了反序列化后的 Period 对象。由此可以见,Period 的序列化和反序列化都是通过内部的代理类实现的,并非直接把自身给序列化的。
下面写个代码演示一下。
private static void test() {
try {
Period period = new Period(new Date(), new Date());
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("E:/test.txt"));
out.writeObject(period);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("E:/test.txt"));
Period period2 = (Period) in.readObject();
in.close();
System.out.println("序列化前后是否是同一个对象 " + (period == period2));
} catch (Exception e) {
e.printStackTrace();
}
}
打印出来的日志为
writeReplace()方法!
进入SerializabtionProxy 的 readResolve()方法,将返回Period对象!
序列化前后是否是同一个对象 false
再看看序列化的文本里面的内容
sr Bcom.example.cn.desigin.utils.ObjectTest$Period$SerializabtionProxy L endt Ljava/util/Date;L startq ~ xpsr java.util.Datehj?KYt xpw g黪鲌xsq ~ w g黪鲌x
发现对应的上,此方法可行。
序列化代理模式有两个局限性。它不能与可以被客户端扩展的类兼容。它也不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理。这种模式可用,但要遵守它的规则。