Java序列化深入理解

1 序列化

1.1 基本概念理解

Java 对象序列化用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态。
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。

实际上,序列化的思想是 冻结 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 解冻 状态,重新获得可用的 Java 对象。所有这些事情的发生有点像是魔术,这要归功于 ObjectInputStream/ObjectOutputStream 类、完全保真的元数据以及程序员愿意用Serializable 标识接口标记他们的类,从而 参与 这个过程
序列化的实现:将需要被序列化的类实现Serializable接口,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流

Serialization(序列化)是一种将对象以一连串的字节描述的过程;deserialization(反序列化)是一种将这些字节重建成一个对象的过程

1.2 串行序列化特点

1.2.1 序列化允许重构

序列化允许一定数量的类变种,甚至重构之后也是如此,ObjectInputStream 仍可以很好地将其读出来。
Java Object Serialization 规范可以自动管理的关键任务是:

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

取决于所需的向后兼容程度,转换字段形式(从非static 转换为 static 或从非 transient 转换为 transient)或者删除字段需要额外的消息传递。

1.2.2 序列化并不安全

Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。这对于安全性有着不良影响。
例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。幸运的是,序列化允许 hook 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable对象上提供一个writeObject方法来做到这一点。

假设 类中的敏感数据是 age 字段。我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(您可以开发更安全的算法,当前这个算法只是作为一个例子。)
为了 hook 序列化过程,我们将在类上实现一个 writeObject 方法;为了 hook 反序列化过程,我们将在同一个类上实现一个readObject 方法。

public class SerialEnty  implements Serializable {

    private static final long serialVersionUID = 1L;

    private int age;
    private String name;



    public SerialEnty(int age, String name) {

        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private void writeObject(java.io.ObjectOutputStream stream)throws java.io.IOException {
    // "Encrypt"/obscure the sensitive data
    age = age << 2;
    stream.defaultWriteObject();
    }

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

    // "Decrypt"/de-obscure the sensitive data
    age = age >> 2;
    }
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerialEnty hello = new SerialEnty(2, "hello");
        System.out.println(hello);
        ByteArrayOutputStream bos = new ByteArrayOutputStream ();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(hello);
        oos.flush();
        bos.flush();

        InputStream is = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(is);
        SerialEnty se = (SerialEnty)ois.readObject();
        System.out.println(se);
        System.out.println(JSON.toJSON(se));

        ois.close();
        is.close();

        oos.close();
        bos.close();
    }

1.2.3 序列化的数据可以被签名和密封

上一个技巧假设想模糊化序列化数据,而不是对其加密或者确保它不被修改。当然,通过使用 writeObjectreadObject 可以实现密码加密和签名管理,但其实还有更好的方式。
如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObjectjava.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理。
结合使用这两种对象,便可以轻松地对序列化数据进行密封和签名,而不必强调关于数字签名验证或加密的细节。

1.2.4 序列化允许将代理放在流中

很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。

打包和解包代理
writeReplacereadResolve 方法使 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。

1.2.5 串行化的继承

如果某个类能够被串行化,其子类也可以被串行化。
如果该类有父类,则分两种情况来考虑,如果该父类已经实现了可串行化接口。则其父类的相应字段及属性的处理和该类相同,即:父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
如果该类的父类没有实现可串行化接口,则该类的父类所有的字段属性将不会串行化。
对于父类的处理,如果父类没有实现串行化接口,则其必须有默认的构造函数(即没有参数的构造函数,如果只声明有参构造会报错),否则编译的时候就会报错。在反串行化的时候,默认构造函数会被调用。但是若把父类标记为可以串行化,则在反串行化的时候,其默认构造函数不会被调用。这是为什么呢?这是因为Java 对串行化的对象进行反串行化的时候,直接从流里获取其对象数据来生成一个对象实例,而不是通过其构造函数来完成

注意 :当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化

1.2.6 static和transient

声明为statictransient类型的成员数据不能被串行化。因为static代表类的状态, transient代表对象的临时数据
序列化会忽略静态变量,即序列化不保存静态变量的状态。静态成员属于类级别的,所以不能序列化。即 序列化的是对象的状态不是类的状态
这里的不能序列化的意思,是序列化信息中不包含这个静态成员域

1.2.7 序列化前和序列化后的对象的关系

==还是equal? or 是浅复制还是深复制?
答案:深复制,反序列化还原后的对象地址与原来的的地址不同
序列化前后对象的地址不同了,但是内容是一样的,而且对象中包含的引用也相同。换句话说,通过序列化操作,我们可以实现对任何可Serializable对象的深度复制(deep copy)——这意味着我们复制的是整个对象网,而不仅仅是基本对象及其引用。对于同一流的对象,他们的地址是相同,说明他们是同一个对象,但是与其他流的对象地址却不相同。也就说,只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个

1.2.8 相关的类和接口

java.io包中提供的涉及对象的串行化的类与接口有ObjectOutput接口、ObjectOutputStream类、ObjectInput接口、ObjectInputStream类。
ObjectOutput接口:它继承DataOutput接口并且支持对象的串行化,其内的writeObject()方法实现存储一个对象。
ObjectInput接口:它继承DataInput接口并且支持对象的串行化,其内的readObject()方法实现读取一个对象。
ObjectOutputStream类:它继承OutputStream类并且实现ObjectOutput接口。利用该类来实现将对象存储(调用ObjectOutput接口中的writeObject()方法)。ObjectInputStream类:它继承InputStream类并且实现ObjectInput接口。利用该类来实现读取一个对象(调用ObjectInput接口中的readObject()方法)。

2 serialVersionUID

2.1 serialVersionUID作用

serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException

2.2 添加方式

serialVersionUID有两种显示的生成方式:

  • 一是默认的1L,比如:private static final long serialVersionUID = 1L;
  • 二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:private static final long serialVersionUID = xxxxL;

当一个类实现了Serializable接口,如果没有显示的定义serialVersionUID

  • Eclipse:
    Eclipse会提供相应的提醒。面对这种情况,我们只需要在Eclipse中点击类中warning图标一下,Eclipse就会自动给定两种生成的方式。如果不想定义,在Eclipse的设置中也可以把它关掉的,设置如下:Window ==> Preferences ==> Java ==> Compiler ==> Error/Warnings ==> Potential programming problems
    Serializable class without serialVersionUIDwarning改成ignore即可
  • IntellijIdea:
    IntellijIdea中没有相关提示,就需要相关设置:File ==> Settings ==> Editor ==> Inspections ==> Java ==> Serialization issus 或者搜索框中输入serialVersionUID关键字 ==> 勾选Serializable class without serialVersionUID,就完成了IntellijIdea设置
    使用时把光标放在类名上,按Alt+Enter键,这个时候可以看到Add serialVersionUID field提示信息,选中后就可以生成了

当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够 兼容先前版本 ,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化

2.3 相关案例说明

下面用代码说明一下serialVersionUID在应用中常见的几种情况。
序列化实体类

import java.io.Serializable;
public class Person implements Serializable
{
    private static final long serialVersionUID = 1234567890L;
    public int id;
    public String name;
 
    public Person(int id, String name)
    {
        this.id = id;
        this.name = name;
    }
 
    public String toString()
    {
        return "Person: " + id + " " + name;
    }
}

序列化功能:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
 
public class SerialTest
{
 
    public static void main(String[] args) throws IOException
    {
        Person person = new Person(1234, "wang");
        System.out.println("Person Serial" + person);
        FileOutputStream fos = new FileOutputStream("Person.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(person);
        oos.flush();
        oos.close();
    }
}

反序列化功能:

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class DeserialTest
{
    public static void main(String[] args) throws IOException, ClassNotFoundException
    {        
        Person person;
        FileInputStream fis = new FileInputStream("Person.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        person = (Person) ois.readObject();
        ois.close();
        System.out.println("Person Deserial" + person);
    } 
}
  • 情况一:假设Person类序列化之后,从A端传输到B端,然后在B端进行反序列化。在序列化Person和反序列化Person的时候,A端B端都需要存在一个相同的类。如果两处的serialVersionUID不一致,会产生什么错误呢?
    【答案】可以利用上面的代码做个试验来验证:
    先执行测试类SerialTest,生成序列化文件,代表A端序列化后的文件,然后修改serialVersion值,再执行测试类DeserialTest,代表B端使用不同serialVersion的类去反序列化,结果报错:
Exception in thread "main" java.io.InvalidClassException: test.Person; local class incompatible: stream classdesc serialVersionUID = 1234567890, local class serialVersionUID = 123456789
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:560)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1580)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1493)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1729)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1326)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:348)
    at test.DeserialTest.main(DeserialTest.java:15)
  • 情况二:假设两处serialVersionUID一致,如果A端增加一个字段,然后序列化,而B端不变,然后反序列化,会是什么情况呢?
    【答案】新增 public int age; 执行SerialTest,生成序列化文件,代表A端。删除 public int age,反序列化,代表B端,最后的结果为:执行序列化,反序列化正常,但是A端增加的字段丢失(被B端忽略)。
  • 情况三:假设两处serialVersionUID一致,如果B端减少一个字段,A端不变,会是什么情况呢?
    【答案】序列化,反序列化正常,B端字段少于A端,A端多的字段值丢失(被B端忽略)。
  • 情况四:假设两处serialVersionUID一致,如果B端增加一个字段,A端不变,会是什么情况呢?
    验证过程如下:
    先执行SerialTest,然后在实体类Person增加一个字段age,如下所示,再执行测试类DeserialTest.
    【答案】序列化,反序列化正常,B端新增加的int字段被赋予了默认值0
    最后通过下面的图片,总结一下上面的几种情况
    在这里插入图片描述
posted @ 2021-10-30 12:22  上善若泪  阅读(338)  评论(0编辑  收藏  举报