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 序列化的数据可以被签名和密封
上一个技巧假设想模糊化序列化数据,而不是对其加密或者确保它不被修改。当然,通过使用 writeObject
和 readObject
可以实现密码加密和签名管理,但其实还有更好的方式。
如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject
或 java.security.SignedObject
包装器中。两者都是可序列化的,所以将对象包装在 SealedObject
中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject
用于数据验证,并且对称密钥也必须单独管理。
结合使用这两种对象,便可以轻松地对序列化数据进行密封和签名,而不必强调关于数字签名验证或加密的细节。
1.2.4 序列化允许将代理放在流中
很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient
,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。
打包和解包代理
writeReplace
和 readResolve
方法使 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy
中,将它放入到一个流中,然后在反序列化时再进行解包。
1.2.5 串行化的继承
如果某个类能够被串行化,其子类也可以被串行化。
如果该类有父类,则分两种情况来考虑,如果该父类已经实现了可串行化接口。则其父类的相应字段及属性的处理和该类相同,即:父类实现序列化,子类自动实现序列化,不需要显式实现Serializable
接口;
如果该类的父类没有实现可串行化接口,则该类的父类所有的字段属性将不会串行化。
对于父类的处理,如果父类没有实现串行化接口,则其必须有默认的构造函数(即没有参数的构造函数
,如果只声明有参构造会报错),否则编译的时候就会报错。在反串行化的时候,默认构造函数会被调用。但是若把父类标记为可以串行化,则在反串行化的时候,其默认构造函数不会被调用。这是为什么呢?这是因为Java
对串行化的对象进行反串行化的时候,直接从流里获取其对象数据来生成一个对象实例,而不是通过其构造函数来完成
注意
:当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化
1.2.6 static和transient
声明为static
和transient
类型的成员数据不能被串行化。因为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 serialVersionUID
的warning
改成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
。
最后通过下面的图片,总结一下上面的几种情况