专业的JAVA编程教程与资源

网站首页 > java教程 正文

一文了解java序列化的方方面面(什么是java序列化,如何实现java序列化?)

temp10 2024-10-08 18:06:01 java教程 12 ℃ 0 评论

序列化&反序列化

序列化:即将对象转换为字节序列的过程;Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。

反序列化:即将字节序列转换回对象的过程。转换后的对象包含了转换前的对象的状态。

一文了解java序列化的方方面面(什么是java序列化,如何实现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接口就完事了,要考虑的事情有很多。如何编写合适的序列化代码是一个值得考虑的问题。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表