网站首页 > 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接口就完事了,要考虑的事情有很多。如何编写合适的序列化代码是一个值得考虑的问题。
猜你喜欢
- 2024-10-08 Java核心知识 基础六 JAVA序列化(创建可复用的Java对象)
- 2024-10-08 java中的序列化(JAVA中的序列化)
- 2024-10-08 Java 面试题之 序列化和反序列化的深入理解
- 2024-10-08 Oracle:Java 的序列化就是个错误,我们要删掉它!
- 2024-10-08 java序列化和反序列化实例详解(java序列化和反序列化实例详解区别)
- 2024-10-08 java对象序列化(java对象序列化到文件)
- 2024-10-08 实战案例:轻松搞定Java两种序列化机制
- 2024-10-08 java序列化机制之protobuf(快速高效跨语言)
- 2024-10-08 Java路径-40-Java序列化(未检测到java sdk环境请在配置中添加sdk安装路径)
- 2024-10-08 java序列化知多少(java序列化怎么实现)
欢迎 你 发表评论:
- 最近发表
- 标签列表
- 
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
 

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