阅读 389

【原创】(译)Java 序列化魔法方法及使用示例

转载请注明出处。

英文原文地址:www.javacodegeeks.com/2019/09/jav…

翻译:福尔马林/内观

在上一篇文章 Everything You Need to Know About Java Serialization 中我们讨论了如何通过实现 Serializable 接口来激活一个类的序列化能力。如果我们的类没有实现 Serializable 接口,或者它引用了一个非可序列化的类,JVM 就会抛出 NotSerializableException 不可序列化异常。

Serializable 的所有子类都同样也可序列化,其中也包括 Externalizable 接口。所以即使我们通过 Externalizable 对序列化过程进行了定制,我们的类也仍然是可序列化的。

Serializable 是一个标记接口。它既没有字段,也没有方法。它的作用仅仅是为 JVM 提供一个标记。真正的序列化过程是由 JVM 控制的 ObjectInputStreamObjectOutputStream 类提供的。

如果我们想在正常处理流程之上添加额外的处理逻辑,该怎么做呢?比如说我们想在序列化/反序列化之前对敏感数据进行加/解密操作。Java 提供了一些额外的方法来帮助我们实现这样的目的,也就是我们即将在这篇博客讨论的主题。

writeObject 和 readObject 方法

想要定制或者添加额外的逻辑来加强序列化/反序列化的流程,需要提供 writeObjectreadObject 两个方法,签名如下:

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException
  • private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException

这些方法在 Everything You Need to Know About Java Serialization 已经详细讨论过了。

readObjectNoData 方法

Serializable 的 java docs 所述,如果我们希望当接到的序列化流不满足我们想反序列化的类的时候,能自动进行一些状态初始化,那么我们就要提供 readObjectNoData 方法,签名如下:

  • private void readObjectNoData() throws ObjectStreamException

这种情况可能出现在接收方使用了一个与发送方不同版本的类。接收方的版本多扩展了一些字段,而发送方的版本没有这些字段。还有一种可能就是序列化流被篡改了。这时,无论是恶意的流还是不完整的流,都可以用 readObjectNoData 方法来将序列化得到的对象初始化到正确的状态。

每个可序列化类都可以定义它自己的 readObjectNoData 方法。如果一个类没有定义 readObjectNoData 方法,那么当出现上述情况的时候,这些字段的值就会取缺省值。(译注:比如 null 或 0)

writeReplace 和 readResolve 方法

可序列化的类,如果想在将对象写入流时,进行一定的转换,可以提供这个特殊方法:

  • ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException

如果想在从流里读出对象的时候,进行一定的替换,则可以提供下面这个方法:

  • ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException

基本上,writePlace 方法允许开发者提供一个替代对象来取代原来将被序列化的对象。而 readResolve 方法被用来在反序列化的时候用你选择的对象来替代原本反序列化出来的对象。

writeReplace 和 readResolve 方法的一个主要用途是用来实现单例模式。我们知道反序列化每次都会创建一个新对象,常被用来做对象的深拷贝。对于要采用单例模式的情况就不好弄了。

更多信息可以参考 Java cloning and serialization on Java CloningJava Serialization 主题。

readResolve 方法会在 readObject 方法返回后被调用(相反,writeReplace 方法是在 writeObject 方法之前被调用)。readResolve 方法返回的对象会替换 ObjectInputStream.readObject 返给用户的 this 对象,并更新流里所有对该对象的反向引用。我们可以使用 writeReplace 方法来把要序列化的对象换成 null,然后在 readResolve 方法用单例的对象示例来替代反序列化的结果。(译注:这样就解决了上两段提出的单例问题)

validateObject 方法

如果我们想对我们的某些字段做校验,我们可以实现 ObjectInputValidation 接口的 validateObject 方法。

validateObject 方法会在我们在 readObject 方法里调用 ObjectInputStream.registerValidation(this, 0) 注册校验的时候被调用。它对于验证流未被篡改或实际有效很有用。

最后是对上面所有这些方法的示例代码。

public class SerializationMethodsExample {
 
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Employee emp = new Employee("Naresh Joshi", 25);
        System.out.println("Object before serialization: " + emp.toString());
 
        // Serialization
        serialize(emp);
 
        // Deserialization
        Employee deserialisedEmp = deserialize();
        System.out.println("Object after deserialization: " + deserialisedEmp.toString());
 
 
        System.out.println();
 
        // This will print false because both object are separate
        System.out.println(emp == deserialisedEmp);
 
        System.out.println();
 
        // This will print false because both `deserialisedEmp` and `emp` are pointing to same object,
        // Because we replaced de-serializing object in readResolve method by current instance
        System.out.println(Objects.equals(emp, deserialisedEmp));
    }
 
    // Serialization code
    static void serialize(Employee empObj) throws IOException {
        try (FileOutputStream fos = new FileOutputStream("data.obj");
             ObjectOutputStream oos = new ObjectOutputStream(fos))
        {
            oos.writeObject(empObj);
        }
    }
 
    // Deserialization code
    static Employee deserialize() throws IOException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream("data.obj");
             ObjectInputStream ois = new ObjectInputStream(fis))
        {
            return (Employee) ois.readObject();
        }
    }
}
 
class Employee implements Serializable, ObjectInputValidation {
    private static final long serialVersionUID = 2L;
 
    private String name;
    private int age;
 
    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    // With ObjectInputValidation interface we get a validateObject method where we can do our validations.
    @Override
    public void validateObject() {
        System.out.println("Validating age.");
 
        if (age < 18 || age > 70)
        {
            throw new IllegalArgumentException("Not a valid age to create an employee");
        }
    }
 
    // Custom serialization logic,
    // This will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization.
    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("Custom serialization logic invoked.");
        oos.defaultWriteObject(); // Calling the default serialization logic
    }
 
    // Replacing de-serializing object with this,
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("Replacing serialising object by this.");
        return this;
    }
 
    // Custom deserialization logic
    // This will allow us to have additional deserialization logic on top of the default one e.g. performing validations, decrypting object after deserialization.
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Custom deserialization logic invoked.");
 
        ois.registerValidation(this, 0); // Registering validations, So our validateObject method can be called.
 
        ois.defaultReadObject(); // Calling the default deserialization logic.
    }
 
    // Replacing de-serializing object with this,
    // It will will not give us a full proof singleton but it will stop new object creation by deserialization.
    private Object readResolve() throws ObjectStreamException {
        System.out.println("Replacing de-serializing object by this.");
        return this;
    }
 
    @Override
    public String toString() {
        return String.format("Employee {name='%s', age='%s'}", name, age);
    }
}
复制代码

你也可以在 Github 仓库上找到完整的代码,欢迎提供任何反馈。