深入理解Java序列化机制

默认序列化方法

基本形式

基本形式非常简单,要让类支持序列化,只需要实现Serializable接口即可,该接口是一个标记接口。序列化的类的所有实例域必须都是可序列化的,也就是说,实例域为引用类型,则该类型也必须实现Serializable。


public class WriteObject {
    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
             ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {            //第一次序列化person
            // 序列化
            Person person = new Person("9龙", 23);
            // 反序列化
            Person p1 = (Person) ios.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    
}

class Person implements Serializable {
    private int age;
    private String name;
    
    public Person(String name, int age) {
        this.age = age;
        this.name = name;
    }
}

transient

用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

序列化版本号

想象一个场景,某个应用中的某个类的对象被序列化并存储,随后该类被改变——添加了字段,当应用再次运行,试图读取并反序列化之前的对象,由于应用不知道类被改变,这时候序列化就会出问题。
为了解决这个问题,Java引入了版本控制。可以给类添加一个serialVersionUID,用来标志类是否被改变,如果开发者不添加,JVM会给每个类默认添加一个UID。

自定义序列化方法

readObjectwriteObject

可以在类中增加readObjectwriteObject方法,这样一来,ObjectStream就会在序列化的实际调用自定义的序列化/反序列化方法。

static class Person implements Serializable {
    transient private Integer age = null;

    private void writeObject(ObjectOutputStream out) throws IOException {
        // 默认序列化方法
        out.defaultWriteObject();
        // 自定义序列化方法
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 默认反序列化方法
        in.defaultReadObject();
        // 自定义反序列化方法
        age = in.readInt();
    }
}

Externalizable

另一种方式是实现接口Externalizable,这种方式同样可以自定义序列化反序列化方法。但这种方式需要提供无参构造器,给ObjectStream反射调用(注意,后文也会说,Serializable是JVM直接构造对象,Externalizable是调用无参构造器,后者的优势在于可以利用无参构造器约束类)。

public interface Externalizable extends java.io.Serializable {

    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

writeReplacereadResolve

第三种方式是在类中提供writeReplacereadResolve方法,该方法会在ObjectStream序列化/反序列化才被调用,用于替换原来序列化/反序列化之后的对象。常常用来保证单例模式反序列化一致性。

public class SerializeDemo05 {
    // 其他内容略

    static class Person implements Serializable {

        // 添加此方法
        private Object readResolve() {
            return instatnce;
        }
        // 其他内容略
    }

    // 其他内容略
}

原理

你可能会好奇,这些方法都是怎么起作用但——似乎除了Externalizable之外,都无法通过多态机制来调用我们都实现。其实方法非常简单直接——ObjectStream和ObjectStreamClass中写死的,通过反射技术获得相应的方法,在合适的时候调用。可以看下以下源码。

if (externalizable) {
    cons = getExternalizableConstructor(cl);
} else {
    cons = getSerializableConstructor(cl);
    writeObjectMethod = getPrivateMethod(cl, "writeObject",
        new Class<?>[] { ObjectOutputStream.class },
        Void.TYPE);
    readObjectMethod = getPrivateMethod(cl, "readObject",
        new Class<?>[] { ObjectInputStream.class },
        Void.TYPE);
    readObjectNoDataMethod = getPrivateMethod(
        cl, "readObjectNoData", null, Void.TYPE);
    hasWriteObjectData = (writeObjectMethod != null);
}
domains = getProtectionDomains(cons, cl);
writeReplaceMethod = getInheritableMethod(
    cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
    cl, "readResolve", null, Object.class);

序列化算法原理

JVM构造对象

需要注意,实现Serializable的类,在反序列化的时候,不会调用构造方法,而是直接由JVM生成。这一点非常重要,也给类的约束在构造方法中约定,那么默认的反序列化方法就无法保证这种约束。
另外一点,Java的序列化机制需要注意的就是,它会在序列化/反序列过程中调用一系列类自身的方法,例如上面提到的readObject, writeObject, wirtReplace, readResolve等等,这给Java序列化机制带来了方便——看起来实现序列化只需要实现Serializable,毫不费力,但也给Java序列化机制带来了很多风险——这些类自身的方法,是调用方完全无法控制的。攻击者通过巧妙的设计就可以利用这个机制进行反序列化攻击。

同一对象多次序列化机制

对于同一对象,Java序列化机制只会序列化一次,所以你会看到不可思议的“意外”。以下引用自参考文章一。


public class WriteObject {
    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
             ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {            //第一次序列化person
            Person person = new Person("9龙", 23);
            oos.writeObject(person);
            System.out.println(person);            //修改name
            person.setName("海贼王");
            System.out.println(person);            //第二次序列化person
            oos.writeObject(person);            //依次反序列化出p1、p2
            Person p1 = (Person) ios.readObject();
            Person p2 = (Person) ios.readObject();
            System.out.println(p1 == p2); // true
            System.out.println(p1.getName().equals(p2.getName())); //true
            // output
            // Person{age=23, name='9龙'}
            // Person{age=23, name='海贼王'}
            // true
            // true       

            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

要理解上述结果,必须理解序列化算法:

  1. 所有保存到磁盘的对象都有一个序列化编码号
  2. 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
  3. 如果此对象已经序列化过,则直接输出编号即可。

图示:
在这里插入图片描述

缺陷

  • 无法跨语言:Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
  • 容易被攻击:对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。
  • 序列化后的流太大:Java 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,编码后的数组很大,非常影响存储和传输效率。
  • 序列化性能太差:Java 的序列化耗时比较大。序列化的速度也是体现序列化性能的重要指标,如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间。
  • 序列化编程限制
    • Java 官方的序列化一定需要实现 Serializable 接口
    • Java 官方的序列化需要关注 serialVersionUID

参考

https://juejin.cn/post/6844903848167866375#heading-9
https://dunwu.github.io/javacore/io/java-serialization.html#_5-java-%E5%BA%8F%E5%88%97%E5%8C%96%E9%97%AE%E9%A2%98
https://blog.csdn.net/Leon_cx/article/details/81517603

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值