序列化&反序列化

序列化:即将对象转换为字节序列的过程;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接口就完事了,要考虑的事情有很多。如何编写合适的序列化代码是一个值得考虑的问题。




java那些不可序列化对象 java不序列化某个字段_System