蹲厕所的熊

benjaminwhx

深入探讨Java序列化

2018-06-05 作者: 吴海旭


  1. 1、简介
  2. 2、实现序列化
    1. Serializable
    2. Externalizable
  3. 3、序列化允许重构类
  4. 4、谨慎的实现Serializable接口
    1. 父类谨慎实现Serializable接口
    2. 子类谨慎实现Serializable接口
    3. 小结
  5. 5、自定义序列化格式
  6. 6、序列化并不安全
    1. 自定义加密算法
    2. 使用SealedObject
    3. Validation
    4. 使用序列化代理模式
  7. 7、静态变量的序列化
  8. 8、总结

1、简介

Java 对象序列化是 JDK 1.1 中引入的一组开创性特性之一,用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态。

实际上,序列化的思想是 “冻结” 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 “解冻” 状态,重新获得可用的 Java 对象。所有这些状态的转换和传输都是依靠ObjectInputStream/ObjectOutputStream类才得以完成,我们要做的仅仅是在类上加上 Serializable 标识接口就可以了,其他的JDK会帮我们去完成。

2、实现序列化

Java序列化规范中指出,只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。

public interface Serializable {}

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

Serializable

先来说说Serializable,下面是一个实现了Serializable接口的Person类。

@Data
@RequiredArgsConstructor
public class Person implements Serializable {
    @NonNull private String firstName;
    @NonNull private String lastName;
    @NonNull private int age;
    Person spouse;
}

接着调用序列化和反序列化方法来测试是否能够成功。

public class SerializableUtils {

    private static final String DISK_PATH = "/Users/benjamin/Desktop/Person.out";

    public static void serializePerson(Person p) throws IOException {
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File(DISK_PATH)));
        oo.writeObject(p);
        System.out.println("Person序列化成功");
        oo.close();
    }

    public static Person deserializePerson() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(DISK_PATH)));
        Person person = (Person) ois.readObject();
        System.out.println("Person反序列化成功");
        return person;
    }
}

public class SerializableTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person ted = new Person("Ted", "Neward", 39);
        Person charl = new Person("Charlotte", "Neward", 38);
        ted.setSpouse(charl);
        charl.setSpouse(ted);

        SerializableUtils.serializePerson(ted);

        Person ted2 = SerializableUtils.deserializePerson();
        System.out.println(ted2);
    }
}

方法很简单,也的确在我们指定的 DISK_PATH 路径下生成了一个序列化文件,打开后的内容是这样的:

aced 0005 7372 001b 636f 6d2e 6769 7468
7562 2e73 6572 6961 6c69 7a65 2e50 6572
736f 6e3e 57fd 5102 2827 9a02 0004 4900
0361 6765 4c00 0966 6972 7374 4e61 6d65
7400 124c 6a61 7661 2f6c 616e 672f 5374
7269 6e67 3b4c 0008 6c61 7374 4e61 6d65
7100 7e00 014c 0006 7370 6f75 7365 7400
1d4c 636f 6d2f 6769 7468 7562 2f73 6572
6961 6c69 7a65 2f50 6572 736f 6e3b 7870
0000 0027 7400 0354 6564 7400 064e 6577
6172 6473 7100 7e00 0000 0000 2674 0009
4368 6172 6c6f 7474 6571 007e 0005 7100
7e00 03

本章暂不对序列化的机制做深入探讨,该机制将会在后续的文章中讲解。

Externalizable

说完了Serializable,我们再来看看Externalizable。把刚刚的Person类改造一下:

@Data
@RequiredArgsConstructor
@NoArgsConstructor
public class Person implements Externalizable {
    @NonNull private String firstName;
    @NonNull private String lastName;
    @NonNull private int age;
    Person spouse;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Person writeExternal");
        // 如果我们不将字段的值写入的话,那么在反序列化的时候,就不会得到这些值。
        out.writeObject(firstName);
        out.writeObject(lastName);
        out.writeInt(age);
        out.writeObject(spouse);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("Person readExternal");
        // 在反序列化时,需要进行值的初始化,否则只是调用默认构造函数,得不到其他字段的值。
        firstName = (String) in.readObject();
        lastName = (String) in.readObject();
        age = in.readInt();
        spouse = (Person) in.readObject();
    }
}

对比于实现了Serializable接口的类,实现了Externalizable接口的类除了需要实现 writeExternalreadExternal 外,还有以下几点需要注意:

  1. 需要提供一个public的无参构造函数,不然会报 java.io.InvalidClassException: com.github.serialize.Person; no valid constructor 这个异常。(本例中使用了@NoArgsConstructor注解来提供无参构造函数)
  2. writeExternalreadExternal 方法需要在内部实现序列化和反序列化的逻辑。也就是说,最后真正输出到流中的值是在 writeExternal 方法中指定的,和当前类的字段值无关。

接下来的章节都是以Serializable接口为例,Externalizable暂时不做过多讨论。

3、序列化允许重构类

序列化能够允许我们改变类,并且能够在改变后利用ObjectInputStream很好的读取出之前的类。

Java Object Serialization 规范可以自动管理的关键任务是:

  • 可以将新字段添加到类中。
  • 可以将字段从 static 改为非 static。
  • 可以将字段从 transient 改为非 transient。

还是拿刚才的Person类来举例,我们让它新增一个字段Gender用来实现性别的需求。

public enum Gender {
    MALE, FEMALE
}

@Data
@RequiredArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = 4492337677695723418L;
    @NonNull private String firstName;
    @NonNull private String lastName;
    @NonNull private int age;
    @NonNull private Gender gender;    // 添加了一个gender字段
    Person spouse;

    @Override
    public String toString() {
        return "Person{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", spouse=" + spouse.getFirstName() +
                '}';
    }
}

如果这个时候对之前序列化的文件进行反序列化就会报 serialVersionUID 不匹配的错误。

Exception in thread "main" java.io.InvalidClassException: com.github.serialize.Person; local class incompatible: 
stream classdesc serialVersionUID = -3798168053006469940, 
local class serialVersionUID = 6944708458020997104

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致。即使两个类功能代码完全一致,但是序列化 ID 不同,他们也无法相互序列化和反序列化。那么什么是 serialVersionUID 呢?它的作用又是什么呢?

serialVersionUID:字面意思上是序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量,如果不加serialVersionUID,也是可以正常进行序列化,但是如果以后项目中需要对实体类进行增减字段的话,再进行反序列化就会报错。

那么serialVersionUID是如何取值的呢?下面是jdk中获取值的源码:

// ObjectStreamClass.java
public long getSerialVersionUID() {
    // REMIND: synchronize instead of relying on volatile?
    if (suid == null) {
        suid = AccessController.doPrivileged(
            new PrivilegedAction<Long>() {
                public Long run() {
                    return computeDefaultSUID(cl);
                }
            }
        );
    }
    return suid.longValue();
}

serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定serialVersionUID,为它赋予明确的值。一旦类有了serialVersionUID,当出现新字段的时候,新字段被设为缺省值(对象为null,int为0…)。

显式地定义serialVersionUID有两种用途:

  1. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID。
  2. 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

serialVersionUID的值无所谓多少,只要保证不变就行了。你可以每次手动定义成一个值,例如 1L,也可以利用IDE工具来生成,如果你用的是Intellej Idea的话,可以让鼠标点在类名上,接着按下 ALT+Enter,就会出现下图的提示 Add 'serialVersionUID' field

有的朋友可能出不来这句提示,那需要去设置里打开了。

4、谨慎的实现Serializable接口

对于程序员而言,实现序列化只需要把相关类实现Serializable接口就可以了,所以可以见到代码里很多人什么类都去实现Serializable,也不管它需要还是不需要。实现Serializable接口有以下几个缺点:

  1. 降低了“改变该类的实现”的灵活性:如果一个类实现了Serializable接口,它的包名、类名、字段类型、字段名等会被固定。一旦今后你要对类进行改动,很有可能导致序列化格式不兼容。你要考虑新版本的类反序列化到旧版本的类,也要考虑到旧版本的类反序列化到新版本的类。这样做起来会比较困难,而且一旦考虑的不周全,很有可能会存在隐患。
  2. 增加了出现Bug和安全漏洞的可能性:一般情况下对象是通过构造函数来创建的,但是序列化提供了另外一种对象的创建机制。如果你在构造函数中对属性做了一些校验,你也必须在反序列化中进行校验,这很容易遗忘,并且反序列化的时候很容易遭到安全的问题。
  3. 随着类的不断迭代更新,增加了测试的负担:要保持之前所有版本的类序列化都能够兼容,这是一个不小的工作量,但是现在很多人都没有重视这一点,都忽略了这一点的测试。

前面讨论的类只要实现了Serializable接口就可以进行序列化操作,这个没有问题,但是如果该类有很多子类的时候,子类具有序列化特性吗?如果该类的父类没有实现Serializable接口,能否序列化父类的字段呢?

父类谨慎实现Serializable接口

我们创建两个类,父类SuperC以及子类SubC,首先让父类实现Serializable接口,而子类不实现。

public class SuperC implements Serializable {
    private static final long serialVersionUID = -8593928627664695238L;
    int supervalue;

    public SuperC(int supervalue) {
        this.supervalue = supervalue;
    }

    @Override
    public String toString() {
        return "super: " + supervalue;
    }
}

public class SubC extends SuperC {
    int subvalue;

    public SubC(int supervalue, int subvalue) {
        super(supervalue);
        this.subvalue = subvalue;
    }

    @Override
    public String toString() {
        return super.toString() + " sub: " + subvalue;
    }
}
public class SerializableTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SubC subC = new SubC(3, 2);
        SerializableUtils.serialize(subC);
        SubC subC1 = (SubC) SerializableUtils.deserialize();
        System.out.println(subC1);
    }
}

反序列化后能够完整的得到父类和子类设置的值:3和2。

但是我们注意到子类虽然没有实现Serializable接口,但是它也没有定义 serialVersionUID ,那它会复用父类的 serialVersionUID 吗?

我们尝试着在子类SubC中增加一个字段,并直接反序列刚才已经输出的序列化文件,发现报错:

Exception in thread "main" java.io.InvalidClassException: com.github.serialize.SubC; local class incompatible: 
stream classdesc serialVersionUID = 595763834020060396, 
local class serialVersionUID = -7117325781383779182

也就是说,需要实现序列化的类一定要一定自己的 serialVersionUID 才可以保证向后兼容

通常来说,为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口。如果违反了这条规则,扩展这个类或者实现这个接口的程序员就会背上沉重的负担。然而在有些情况下违反这条规则却是合适的。例如,如果一个类或者接口存在的目的主要是为了参与到某个框架中,该框架要求所有的参与者都必须实现Serializable接口,那么,对于这个类或者接口来说,实现或者扩展Serializable接口就是非常有意义的。

在为了继承而设计的类中,真正实现了Serializable接口的有Throwable类、Component和HttpServlet抽象类。因为Throwable类实现了Serializable接口,所以RMI的异常可以从服务器端传到客户端。Component实现了Serializable接口,因为GUI可以被发送、保存和恢复。HttpServlet实现了Serializable接口,因此会话状态(session state)可以被缓存。

如果父类已经实现了Serializable接口,但是子类却不想被序列化,那我们该怎么办?我们只需要在子类中的 writeObject/readObject 方法中抛出异常 NotSerializableException 即可。

public class SubC extends SuperC {
    int subvalue;

    public SubC(int supervalue, int subvalue) {
        super(supervalue);
        this.subvalue = subvalue;
    }

    private void writeObject(java.io.ObjectOutputStream stream)
            throws java.io.IOException {
        throw new NotSerializableException("This class is not serializable");
    }

    private void readObject(java.io.ObjectInputStream stream)
            throws java.io.IOException, ClassNotFoundException {
        throw new NotSerializableException("This class is not serializable");
    }
}

子类谨慎实现Serializable接口

要为一个没有实现Serializable接口的父类,编写一个能够序列化的子类是一件很麻烦的事情。java docs中提到:

“To allow subtypes of non-serializable classes to be serialized, the subtype may assume responsibility for saving and restoring the state of the supertype's public, protected, and (if accessible) package fields. The subtype may assume this responsibility only if the class it extends has an accessible no-arg constructor to initialize the class's state. It is an error to declare a class Serializable if this is not the case. The error will be detected at runtime.

也就是说,要为一个没有实现Serializable接口的父类,编写一个能够序列化的子类要做两件事情:

  1. 父类要有一个无参的constructor。
  2. 子类要负责序列化(反序列化)父类的域。

我们将SuperC的Serializable接口去掉,而给SubC加上Serializable接口。运行后产生错误:

Exception in thread "main" java.io.InvalidClassException: com.github.serialize.SubC; no valid constructor

果真如docs中所说的一样,父类缺少无参构造函数是不行的。

接下来,按照docs中的建议我们改写这个例子:

public class SuperC {
    int supervalue;

    /**
     * 增加一个无参构造函数
     */
    public SuperC() {}

    public SuperC(int supervalue) {
        this.supervalue = supervalue;
    }

    @Override
    public String toString() {
        return "super: " + supervalue;
    }
}

public class SubC extends SuperC implements Serializable {
    private static final long serialVersionUID = 1L;
    int subvalue;

    public SubC(int supervalue, int subvalue) {
        super(supervalue);
        this.subvalue = subvalue;
    }

    @Override
    public String toString() {
        return super.toString() + " sub: " + subvalue;
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();   // 先序列化对象
        out.writeInt(supervalue);   // 再序列化父类的域
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        in.defaultReadObject();     // 先反序列化对象
        supervalue = in.readInt();    // 再反序列化父类的域
    }
}

运行结果证明了这种方法是正确的。在此处我们用到了writeObject/ readObject方法(这两个方法在Serializable中的作用和writeExternal/readExternal在Externalizable中的作用类似,都是用于自定义序列化格式用的,下面一章会重点说这两个方法),这对方法如果存在的话,序列化时就会被调用,以代替默认的行为。

我们在序列化时,首先调用了ObjectOutputStream的defaultWriteObject,它使用默认的序列化行为,然后序列化父类的域;反序列化的时候也一样。

小结

实现Serializable接口很简单却又要很谨慎,我们要考虑从灵活性、安全以及兼容等多维度考虑一个类是否需要实现Serializable接口。

  1. 对于为继承而设计的类时,更要谨慎得选择是否实现Serializable接口。
  2. 如果父类是可序列化的类时,子类无须实现Serializable接口,但必须提供 serialVersionUID
  3. 如果父类是可序列化的类,但是子类不想被序列化,只需要在子类的 writeObject/readObject 方法中抛出异常 NotSerializableException 即可。
  4. 如果父类不是可序列化的类,但子类是可序列化的类并且需要序列化父类的某些域时,父类需要提供无参的构造函数,子类需要自行通过 writeObject/readObject 进行序列化操作。

5、自定义序列化格式

对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据。如果你的类和上面举例的Person一样简单,也就是类的物理表示法等同于它的逻辑内容时,默认序列化形式则是一种比较有效的编码形式。

第4节里我们说过,在序列化子类时,需要使用 writeObject/readObject 对父类的域进行自定义序列化操作,除此之外,我们还有很多特定的场景需要使用自定义的序列化操作。

用一个极端的例子来说明,例如有一个类 StringList ,该类表示了一个字符串列表:

@Data
public class StringList implements Serializable {
    private static final long serialVersionUID = 2241563110216316715L;
    private int size = 0;
    private Entry head = null;

    public static class Entry implements Serializable {
        private static final long serialVersionUID = 9141853021366686370L;
        String data;
        Entry next;
        Entry previous;

        @Override
        public String toString() {
            if (this.next != null) {
                return this.data + "->" + this.next.toString();
            }
            return this.data;
        }
    }

    @Override
    public String toString() {
        return "StringList{" +
                "size=" + size +
                ", head=" + head +
                '}';
    }
}

从逻辑意义上讲,这个类表示一个字符串序列。但是从物理意义上讲,它把该序列表示成一个双向链表。如果我们使用默认的序列化形式,该序列化形式会遍历出链表中的所有项,以及这些项之间的所有双向链接。

通过这个例子我们可以得出,当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:

  1. 它使这个类的导出API永远地束缚在该类的内部表示法上:即使以后内部不使用链表作为数据结构了,也依然需要接受链表形式的输入和输出。
  2. 它会消耗过多的空间:在上面的例子中,序列化形式既表示了链表中的每个项,也表示了所有的链接关系,这是不必要的。这些链表项以及链接只不过是实现细节,不值得记录在序列化形式中。因为这样的序列化形式过于庞大,所以,把它写到磁盘中,或者在网络上发送都将非常慢。
  3. 它会消耗过多的时间:序列化逻辑并不了解对象的拓扑关系,所以它必须经过一个昂贵的图遍历(traversal)过程。在上面的例子中,沿着next引用进行遍历是非常简单的。
  4. 它会引起栈溢出:默认的序列化过程要对对象图进行一次递归遍历,即使对于中等规模的对象图,这样的操作也可能会引起栈溢出。

对于上面的例子,我们可以自定义序列化方式输出我们想要输出的值。(我们可以使用transient关键字来将指定的域从一个类的默认序列化形式中省略掉)

@Data
public class StringList implements Serializable {
    private static final long serialVersionUID = 2241563110216316715L;
    private transient int size = 0;
    private transient Entry head = null;

    // 不再需要实现Serializable接口
    public static class Entry {
        String data;
        Entry next;
        Entry previous;

        @Override
        public String toString() {
            if (this.next != null) {
                return this.data + "->" + this.next.toString();
            }
            return this.data;
        }
    }

    private void add(String data) {
        System.out.println("add data: " + data);
        Entry newData = new Entry();
        newData.data = data;
        if (head == null) {
            head = newData;
        } else {
            for (Entry e = head; ; e = e.next) {
                if (e.next == null) {
                    e.next = newData;
                    newData.previous = e;
                    break;
                }
            }
        }
    }

    @Override
    public String toString() {
        return "StringList{" +
                "size=" + size +
                ", head=" + head +
                '}';
    }

    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int numElements = s.readInt();
        for (int i = 0; i < numElements; ++i) {
            add((String) s.readObject());
        }
    }
}

使用测试方法测试一下,果然能够成功序列化。

public class SerializableTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        StringList stringList = new StringList();
        stringList.setSize(2);
        StringList.Entry head = new StringList.Entry();
        StringList.Entry head2 = new StringList.Entry();
        head.data = "A";
        head.previous = null;
        head.next = head2;
        head2.data = "B";
        head2.previous = head;
        head2.next = null;
        stringList.setHead(head);
        SerializableUtils.serialize(stringList);

        StringList s = (StringList) SerializableUtils.deserialize();
        System.out.println(s);
    }
}

6、序列化并不安全

看到这里的同学可能有的已经打开输出的序列化二进制文件研究半天了,可能有的都尝试改过里面的内容了。是的,只要你知道它的序列化规则,就可以知道对象中的任何private字段几乎都是以明文的方式来保存的,也就是说我们可以随意更改它的内容,这非常没有安全性。

自定义加密算法

幸运的是,序列化允许 “hook” 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable 对象上提供一个 writeObject 方法来做到这一点。

假设 Person 类中的敏感数据是 age 字段。毕竟,女士忌谈年龄。 我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(您可以开发更安全的算法,当前这个算法只是作为一个例子。)

@Data
@RequiredArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = 4492337677695723418L;
    @NonNull private String firstName;
    @NonNull private String lastName;
    @NonNull private int age;
    Person spouse;

    private void writeObject(java.io.ObjectOutputStream stream)
            throws java.io.IOException {
        // 输出前对age加密
        age = age << 2;
        stream.defaultWriteObject();
    }

    private void readObject(java.io.ObjectInputStream stream)
            throws java.io.IOException, ClassNotFoundException {
        stream.defaultReadObject();
        // 输入后对age解密
        age = age >> 2;
    }
}

使用SealedObject

上面提到的方法为我们操作序列化对象内的局部变量提供了灵活性。但一种更简单的方法就是通过javax.crypto.SealedObject类,我们可以把整个序列化的流进行加密(类似的还有java.security.SignedObject类,各位可以自己尝试)

@Data
@RequiredArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = 1260074603667133239L;
    @NonNull private String firstName;
    @NonNull private String lastName;
    @NonNull private int age;
    Person spouse;
}

public class SerializableTest {

    public static void main(String[] args) throws Exception {
        Person ted = new Person("Ted", "Neward", 39);
        Person charl = new Person("Charlotte", "Neward", 38);
        ted.setSpouse(charl);
        charl.setSpouse(ted);

        KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede");
        SecretKey key = keyGenerator.generateKey();
        Cipher cipher = Cipher.getInstance("DESede");
        cipher.init(Cipher.ENCRYPT_MODE, key);

        // init SealedObject with ted and cipher
        SealedObject so = new SealedObject(ted,cipher);

        SerializableUtils.serialize(so);

        SealedObject so2 = (SealedObject) SerializableUtils.deserialize();
        Person p = (Person) so2.getObject(key);
        System.out.println(p);
    }
}

这样的操作大大提升了序列化的安全性,因为反序列化时,必须要拿到序列化时使用的key才行。

Validation

为了安全起见,在反序列化时,我们最好对得到的数据进行校验。我们可以在 readObject 方法里进行校验,但是更好的做法是实现 ObjectInputValidation 接口,并在 validateObject 方法中进行参数的校验工作。

@Data
@RequiredArgsConstructor
public class Person implements Serializable, ObjectInputValidation {
    private static final long serialVersionUID = 1260074603667133239L;
    @NonNull private String firstName;
    @NonNull private String lastName;
    @NonNull private int age;
    Person spouse;

    @Override
    public String toString() {
        return "Person{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                ", spouse=" + spouse.getFirstName() +
                '}';
    }

    private void readObject(java.io.ObjectInputStream stream)
            throws java.io.IOException, ClassNotFoundException {
        stream.defaultReadObject();
        stream.registerValidation(this, 0);
    }

    @Override
    public void validateObject() throws InvalidObjectException {
        if (age < 0) {
            throw new InvalidObjectException("The Deserialized object is invalid. age can't be negative.");
        }
        System.out.println("The Deserialized object is valid.");
    }
}

使用序列化代理模式

如之前说的,实现Serializable接口会增加出错和出现安全问题的可能性,因为它导致实例要利用语言之外的机制来创建,而不是使用普通的构造器。然而,有一种方法可以极大地减少这些风险,却又不同于之前的加密模式,这种方法就是序列化代理模式(serialization proxy pattern)

我们可以为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。 writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。

@Data
@RequiredArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = 4492337677695723418L;
    @NonNull private String firstName;
    @NonNull private String lastName;
    @NonNull private int age;
    Person spouse;

    // 先于writeObject调用
    private Object writeReplace()
            throws java.io.ObjectStreamException {
        PersonProxy personProxy = new PersonProxy(this);
        System.out.println("Person writeReplace personProxy:" + personProxy);
        // 使用personProxy代替Person序列化
        return personProxy;
    }
}

@Data
public class PersonProxy implements Serializable {
    private static final long serialVersionUID = 1664294754918026967L;
    private String data;

    public PersonProxy(Person person) {
        data = person.getFirstName() + "," + person.getLastName() + ","
                + person.getAge();
        if (person.getSpouse() != null) {
            Person spouse = person.getSpouse();
            data = data + "," + spouse.getFirstName() + "," + spouse.getLastName()
                    + "," + spouse.getAge();
        }
    }

    private Object readResolve()
            throws java.io.ObjectStreamException {
        String[] pieces = data.split(",");
        Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2]));
        if (pieces.length > 3) {
            result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt
                    (pieces[5])));
            result.getSpouse().setSpouse(result);
        }
        System.out.println("PersonProxy readResolve result:" + result);
        // 解包时构造Person
        return result;
    }
}

在PersonProxy中有了这个writeReplace方法之后,序列化系统永远不会产生Person的序列化实例,但是攻击者可能伪造,企图违反该类的约束条件。为了确保这种攻击无法得逞,我们只需要在Person中加入这个readObject方法即可:

private void readObject(java.io.ObjectInputStream stream)
        throws java.io.IOException, ClassNotFoundException {
    throw new InvalidObjectException("Proxy required");
}

7、静态变量的序列化

说了这么多,最后来问大家一个问题,静态变量能够被序列化吗?

我们重新写一个类 StaticTest ,定义一个静态变量 staticVar=5,将它序列化后,再把staticVar设置成10,最后反序列化后得出的对象的staticVar是5还是10呢?

@Data
public class StaticTest implements Serializable {
    private static final long serialVersionUID = 3917695071773306180L;
    public static int staticVar = 5;

    @Override
    public String toString() {
        return "StaticTest: staticVar=" + staticVar;
    }
}

public class SerializableTest {

    public static void main(String[] args) throws Exception {
        StaticTest staticTest = new StaticTest();
        SerializableUtils.serialize(staticTest);

        // 修改为10
        StaticTest.staticVar = 10;

        StaticTest staticTest1 = (StaticTest) SerializableUtils.deserialize();
        System.out.println(staticTest1);    // StaticTest: staticVar=10
    }
}

对于无法理解的读者认为,打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量

8、总结

本文主要从JDK的序列化角度教会大家如何使用序列化,以及使用序列化时需要注意的一些点。虽然默认的序列化方式无论从安全方面考虑还是兼容方面考虑,想写好绝对不是一件特别容易的事,这也是为什么甲骨文计划砍掉Java序列化功能的原因,不过认识序列化的问题对于我们以后写代码绝对有益无害。



坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论