序列化&反序列化
序列化:即将对象转换为字节序列的过程;Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
反序列化:即将字节序列转换回对象的过程。转换后的对象包含了转换前的对象的状态。
如何实现对象序列化
我们知道,在java中要实现一个对象的序列化,只需要实现Serializable接口即可。一般还有一个IDE自动生成的serialVersionUID字段。
serialVersionUID
这个字段实际上也是很重要的,如果去掉,在序列化或反序列化的过程中会根据一系列很复杂的算法生成,导致速度变慢。
transient
对于我们不想序列化的字段,可以使用transient修饰它。比如:private transient Date start;则说明start字段不参与序列化。
静态字段
静态字段也不会参与序列化。
实现序列化
通常将对象通过ObjectOutputStream写入一个文件或者在网络上传输后进行反序列化:
Period period = new Period(new Date(),new Date());System.out.println("before serilization: " + period);try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("d:/Period.b"))) { out.writeObject(period);}
实现反序列化
通过ObjectInputStream从文件读取:
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("d:/Period.b"))) { period = (Period) in.readObject(); System.out.println("after deseriliaztion: " + period);}
自定义序列化-readObject-writeObject
readObject和writeObject用于自定义序列化。比如对静态字段进行序列化就需要编写readObject和writeObject来实现序列化与反序列化。
示例:
public class Foo extends AbstractFoo implements Serializable { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // 手动反序列化并实例化父类的state int x = in.readInt(); int y = in.readInt(); initialize(x,y); } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); // 手动序列化父类的state out.writeInt(getX()); out.writeInt(getY()); } private static final long serialVersionUID = -3881436735239828405L;}
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式。
比如下面的类:
public class Name implements Serializable { private static final long serialVersionUID = 7188905641007443816L; private String lastName; private String firstName; private String middleName; public Name(String lastName, String firstName, String middleName) { this.lastName = lastName; this.firstName = firstName; this.middleName = middleName; }}
从逻辑的角度而言,一个名字包含姓、名和中间名。Name中的实例域精确的反应了它的逻辑内容。
即使你确定了使用默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。对于Name这个类而言,readObject方法必须确保firstName和lastName是非null的。
修正的版本:
public class Name implements Serializable { private static final long serialVersionUID = 7188905641007443816L; private String lastName; private String firstName; private String middleName; public Name(String lastName, String firstName, String middleName) { this.lastName = lastName; this.firstName = firstName; this.middleName = middleName; } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject();// 从该流中读取当前类的非静态和非瞬态字段。 String lastNameTemp = s.readObject().toString(); String firstNameTemp = s.readObject().toString(); String middleNameTemp = s.readObject().toString(); if (lastNameTemp == null || firstNameTemp == null) { throw new IllegalArgumentException("firstName or lastName can't be null!"); } this.lastName = lastNameTemp; this.middleName = middleNameTemp; this.firstName = firstNameTemp; }}
ps:readObject是ObjectInputStream在反序列化的时候通过反射调用的。
保护性的编写readObject方法
readObject实际上相当于另一个公有的构造器,如同其他构造器一样,也必须注意同样的所有注意事项。构造器必须检查其参数的有效性,并且在必要的时候对参数进行有效性拷贝 。
同样的,readObject也应该这么做。
不严格的说,readObject是一个“用字节流来作为唯一参数“的构造器。
所以,我们需要编写readObject方法,并对从流读取的对象的域进行合法性校验。
public class Period implements Serializable { private static final long serialVersionUID = -5896804371611599645L; private Date start; private Date end; public Period(Date start, Date end) { Date s = new Date(start.getTime()); Date e = new Date(end.getTime()); if (s.before(e)) { throw new IllegalArgumentException("start can't after end."); } this.start = s; this.end = e; } public Date getStart() { return new Date(start.getTime()); } public Date getEnd() { return new Date(end.getTime()); } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); // 保护性拷贝易变的组件,否则攻击者可能通过篡改序列化后的字节流,拿到Period的2个Date域的引用,从而进行修改,最终改变Period。 start = new Date(start.getTime()); end = new Date(end.getTime()); if (start.after(end)) { throw new InvalidObjectException(start + " after " + end); } } @Override public String toString() { return start + " - " + end; }}
上述代码,如果不对start和end进行保护性拷贝,实际上是有可能篡改Period对象的。
篡改方式:
通过伪造字节流,创建可变的Period仍是可能的,做法是:字节流以一个有效的Period开头,然后附加2个额外的引用,指向Period实例域的2个私有的Date域。
攻击者从ObjectInputStream读取Period实例,然后读取附加在其后面的“恶意编制的对象引用”。这些对象引用使得攻击者能够访问到Period对象内部的私有的Date域所引用的对象。
通过改变这些Date实例,可以改变Period实例。
篡改实例:
/** * @author Donny * @createTime 2020-05-06 16:48 * @description 可变的Period */public class MutablePeriod { private final Period period; private final Date start; private final Date end; public MutablePeriod() { /** * 如果不对Period的readObject做保护性拷贝 * 通过伪造字节流,创建可变的Period仍是可能的,做法是:字节流以一个有效的Period开头,然后附加2个额外的引用,指向Period实例域的2个私有的Date域。 * 攻击者从ObjectInputStream读取Period实例,然后读取附加在其后面的“恶意编制的对象引用”。这些对象引用使得攻击者能够访问到Period对象内部的私有的Date域所引用的对象。 * 通过改变这些Date实例,可以改变Period实例。 */ try { // 序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(baos); // 序列化一个有效的Period对象 out.writeObject(new Period(new Date(),new Date())); /** * Append rogue "previous object refs" for internal Date fields in Period. * For details,see "Java Object Serialization Specification",Section 6.4. */ byte[] ref = {0x71,0,0x7e,0,5}; // Ref #5 baos.write(ref);// start field ref[4] = 4; // Ref #4 baos.write(ref); // end field // 反序列化 ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())); period = (Period) in.readObject(); start = (Date) in.readObject(); end = (Date) in.readObject(); } catch (IOException | ClassNotFoundException e) { throw new AssertionError(e); } } public static void main(String[] args) { MutablePeriod mp = new MutablePeriod(); Period p = mp.period; Date pEnd = mp.end; pEnd.setYear(78); System.out.println(p); pEnd.setYear(69); System.out.println(p); }}
让序列化的对象和反序列化后的对象为同一对象
如果使用枚举实现的单例模式,可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象(JVM对此提供保障)。
对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。
readResolve特性允许你用readObject创建的实例代替另一个实例(反序列化的)。对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法啊,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用。然后,该方法返回的对象引用将会被返回,取代新建的对象。
public class Elvis3 implements Serializable { private static final Elvis3 INSTANCE = new Elvis3(); public static Elvis3 getInstance() { return INSTANCE; } // readResolve method to preserve singleton property private Object readResolve() { // return the one true Elvis and let the garbage collector take care of the Elvis impersonator. return INSTANCE; }}
上面的readResolve忽略了被反序列化的对象,只返回该内初始化时创建的那个特殊的INSTANCE实例 。
如果依赖readResolve对实例进行控制,则带有对象引用类型的所有的域都应该声明为transient。
如果必须编写可序列化的实例受控的类,它的实例在编译时还不知道,那么就不能通过枚举的方式来实现,只能通过通过readResolve来保证。
考虑使用序列化代理序列化实例
当你发现自己必须在一个不能被客户端扩展的类上编写readObject或writeObject方法时,就应该考虑使用序列化代理模式。要想稳健的将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。
实现方式(本例中Period为待序列化的类,SerializationProxy是序列化代理类):
1.序列化代理类与要序列化的类都实现Serializable接口;
2.序列化代理带有一个接收一个待序列化的类参数的构造器;
3.待序列化类编写writeReplace方法,并返回序列化代理对象;
4.在序列化代理类中编写readResolve方法,返回待序列化实例。
实现过程:
1.序列化
在我们序列化Period时,序列化系统发现Period中定义了writeReplace,转而使用writeReplace返回对象的序列化方式;
Period.writeReplace() ==> SerializationProxy.writeObject().也就是说序列化后存储的实际是序列化代理对象(SerializationProxy)。
2.反序列化
SerializationProxy.readObject() ==> SerializationProxy.readResolve().
反序列化的时候因为已经知道是SerializationProxy,所以直接使用它的反序列化方式。
要序列化的类:
public class Period implements Serializable { private final Date start; private final Date end; public Period(Date start, Date end) { Date s = new Date(start.getTime()); Date e = new Date(end.getTime()); if (s.after(e)) { throw new IllegalArgumentException(start + " after " + e); } this.start = s; this.end = e; } public Date getStart() { return new Date(start.getTime()); } public Date getEnd() { return new Date(end.getTime()); } @Override public String toString() { return start + " - " + end; } private Object writeReplace() { System.out.println("call Period.writeReplace()."); return new SerializationProxy(this); } private void readObject(ObjectInputStream in) throws InvalidObjectException { System.out.println("call Period.readObject()."); throw new InvalidObjectException("proxy required."); } private void writeObject(ObjectOutputStream out) throws InvalidObjectException { System.out.println("call Period.writeObject()."); throw new InvalidObjectException("proxy required."); }}
序列化代理类:
public class SerializationProxy implements Serializable { private static final long serialVersionUID = -2475499946017458008L; private final Date start; private final Date end; public SerializationProxy(Period period) { this.start = period.getStart(); this.end = period.getEnd(); } private Object readResolve() { System.out.println("call SerializationProxy.readResolve()."); return new Period(start,end); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { System.out.println("call SerializationProxy.readObject()."); in.defaultReadObject(); } private void writeObject(ObjectOutputStream out) throws IOException { System.out.println("call SerializationProxy.writeObject()."); out.defaultWriteObject(); }}
测试代码:
public class Test { public static void main(String[] args) throws IOException, ClassNotFoundException { // 对Period进行序列化和反序列化 String name = "d:/Period.d"; Period period = new Period(new Date(),new Date()); try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(name))) { out.writeObject(period); } System.out.println("start to deserilize."); try(ObjectInputStream in = new ObjectInputStream(new FileInputStream(name))) { period = (Period) in.readObject(); System.out.println(period.toString()); } }}
测试结果:
call Period.writeReplace().call SerializationProxy.writeObject().start to deserilize.call SerializationProxy.readObject().call SerializationProxy.readResolve().Thu May 07 11:00:32 CST 2020 - Thu May 07 11:00:32 CST 2020
总结
通过本文的内容,你会发现实现序列化从来都表示实现Serializable接口就完事了,要考虑的事情有很多。如何编写合适的序列化代码是一个值得考虑的问题。