Java反序列化基础

一 概述

Java序列化是指把Java对象转换为Java对象的过程。字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。

序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。

常见序列化反序列化协议

1. XML&SOAP

XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议

  1. JSON
  2. Protobuf

二 序列化实现

注意:只有实现了Serializable或者externalizable接口的类的对象才能被序列化为字节序列(否则抛出异常)

Serializable是一个空接口(所以只是用来标记)

1
2
public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

Serializable 接口的基本使用

通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。

Serializable接口特点

1. 序列化类属性没用实现Serialzable 那么在序列化时就回报错
1
2
3
4
5
6
7
8
9
10
public class Person implements Serializable {
private String name;
private int age;

/*
其中 Color 类也需要是实现序列化接口
*/
private Color color;
//若不实现序列化接口,则Person在序列化时也会报错
}
  1. 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。

Animal 是父类,它没有实现 Serilizable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Animal {
private String color;

public Animal() {//没有无参构造将会报错
System.out.println("调用 Animal 无参构造");
}

public Animal(String color) {
this.color = color;

System.out.println("调用 Animal 有 color 参数的构造");
}

@Override
public String toString() {
return "Animal{" +
"color='" + color + '\'' +
'}';
}
}

BlackCat 是 Animal 的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;

public BlackCat() {
super();
System.out.println("调用黑猫的无参构造");
}

public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("调用黑猫有 color 参数的构造");
}

@Override
public String toString() {
return "BlackCat{" +
"name='" + name + '\'' +super.toString() +'\'' +
'}';
}
}

SuperMain 测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SuperMain {
private static final String FILE_PATH = "./super.bin";

public static void main(String[] args) throws Exception {
serializeAnimal();
deserializeAnimal();
}

private static void serializeAnimal() throws Exception {
BlackCat black = new BlackCat("black", "我是黑猫");
System.out.println("序列化前:"+black.toString());
System.out.println("=================开始序列化================");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(black);
oos.flush();
oos.close();
}

private static void deserializeAnimal() throws Exception {
System.out.println("=================开始反序列化================");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
BlackCat black = (BlackCat) ois.readObject();
ois.close();
System.out.println(black);
}
}

输出结果

1
2
3
4
5
6
7
调用 Animal 有 color 参数的构造
调用黑猫有 color 参数的构造
序列化前:BlackCat{name='我是黑猫'Animal{color='black'}'}
=================开始序列化================
=================开始反序列化================
调用 Animal 无参构造
BlackCat{name='我是黑猫'Animal{color='null'}'}

从上面的执行结果来看,如果要序列化的对象的父类 Animal 没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。

  1. 实现Serializable接口的子类也是可以被序列化的
  2. 静态成员变量不能被序列化
  3. transient标识的对象成员变量不参与序列化(在属性前加关键字transient,序列化时,就不会序列化到指定位置)

操作文件流的类

1. ObjectOutputStream代表对象输出流:

它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中

  1. ObjectInputStream代表对象输入流:

它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

三 序列化ID

在进行序列化时,有一个serialVersionUID 。这就是序列化ID
1
private static final long serialVersionUID = 1L;

这个序列化ID起着关键的作用,它决定着是否能够成功反序列化!java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。

即序列化ID是为了保证成功进行反序列化

如何生成UID

1. 使用 AS plugin 插件就可以生成 2. 在JDK中,可以利用 JDK 的 bin 目录下的 serialver 工具产生这个serialVersionUID,对于 Student.class,执行命令:serialver com.example.seriable.Student
1
2
➜  classes git:(master) ✗ /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/bin/serialver com.example.seriable.Student 
com.example.seriable.Student: private static final long serialVersionUID = -6840182814363029482L;//这个就是工具生成的 SerialVersionUID 值了

使用 AS plugin 的方式应该底层也是使用到这个 JDK 工具去生成的 SerialVersionUID 值,测试结果来看这两个生成的值是一样的。

serialVersionUID 的兼容性问题是什么?

具体的兼容性问题如下:
1
2
java.io.InvalidClassException: com.example.seriable.Student; local class incompatible: stream classdesc
serialVersionUID = -926212341182608815, local class serialVersionUID = -6840182814363029482

关于这个异常,它是属于兼容问题异常,是发生在反序列化阶段,检测到 serialVersionUID 不一致导致的。具体的分析如下:

1
2
序列化时使用的 serialVersionUID = -926212341182608815L,如果期间属性被修改了,如果 serialVersionUID 发生改变 -6840182814363029482 ,那么
反序列化时就会出现类不兼容问题。

四 Java的序列化步骤与数据结构分析

序列化算法一般会按步骤做如下事情:

将对象实例相关的类元数据输出。

递归地输出类的超类描述直到不再有超类。

类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。

从上至下递归输出实例的数据

writeObject原理分析

ObjectOutputStream 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);//A
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;//B
writeStreamHeader();//C
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}

A bout:用于写入一些类元数据还有对象中基本数据类型的值,在下面会分析。

B enableOverride :false 表示不支持重写序列化过程,如果为 true ,那么需要重写 writeObjectOverride 方法。这个一般不用管它。

C writeStreamHeader() 写入头信息,具体看下面分析。

ObjectOUtStream#writeObject(obj);

1
2
3
4
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);//①
bout.writeShort(STREAM_VERSION);//②
}

①STREAM_MAGIC 声明使用了序列化协议,bout 就是一个流,将对应的头数据写入该流中

②STREAM_VERSION 指定序列化协议版本

ObjectOUtStream#writeObject(obj);

上面是 ObjectOutStream 构造中做的事,下面来看看具体 writeObject 方法内部做了什么事?

1
2
3
4
5
6
7
8
9
10
11
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {//一般不会走这里,因为在 ObjectOutputStream 构造设置为 false 了
writeObjectOverride(obj);
return;
}
try {//代码会执行这里
writeObject0(obj, false);
} catch (IOException ex) {
...
}
}

ObjectOutStream#writeObject0()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
...
try {

Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;

//①
desc = ObjectStreamClass.lookup(cl, true);
...
//②
if (obj instanceof Class) {
writeClass((Class) obj, unshared);
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
// END Android-changed: Make Class and ObjectStreamClass replaceable.
} else if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
//③
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
}
...
}

① lookup 函数用于查找当前类的 ObjectStreamClass ,它是用于描述一个类的结构信息的,通过它就可以获取对象及其对象属性的相关信息,并且它内部持有该对象的父类的 ObjectStreamClass 实例。

② 根据 obj 的类型去执行序列化操作,如果不符合序列化要求,那么会③位置抛出NotSerializableException异常。

ObjectOutputStream#writeOrdinaryObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
...
try {
desc.checkSerialize();
//①
bout.writeByte(TC_OBJECT);
//②
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
//③
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
//④
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}

①写入类的元数据,TC_OBJECT. 声明这是一个新的对象,如果写入的是一个 String 类型的数据,那么就需要 TC_STRING 这个标识。

②writeClassDesc 方法主要作用就是自上而下(从父类写到子类,注意只会遍历那些实现了序列化接口的类)写入描述信息。该方法内部会不断的递归调用,我们只需要关系这个方法是写入描述信息就好了。

从这里可以知道,序列化过程需要额外的写入很多数据,例如描述信息,类数据等,因此序列化后占用的空间肯定会更大。

③ desc.isExternalizable() 判断需要序列化的对象是否实现了 Externalizable 接口,这个在上面已经演示过怎么使用的,在序列化过程就是在这个地方进行判断的。如果有,那么序列化的过程就会由程序员自己控制了哦,writeExternalData 方法会回调,在这里就可以愉快地编写需要序列化的数据拉。

④ writeSerialData 在没有实现 Externalizable 接口时,就执行这个方法

ObjectOutputstream#writeSerialData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
//①
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {

ObjectStreamClass slotDesc = slots[i].desc;

if (slotDesc.hasWriteObjectMethod()) {//②
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
defaultWriteFields(obj, slotDesc);//③
}
}
}

① desc.getClassDataLayout 会返回 ObjectStreamClass.ClassDataSlot[] ,我们来看看 ClassDataSlot 类,可以看到它是封装了 ObjectStreamClass 而已,所以我们就简单的认为 ① 这一步就是用于返回序列化对象及其父类的 ClassDataSlot[] 数组,我们可以从 ClassDataSlot 中获取对应 ObjectStreamClass 描述信息。

② 开始遍历返回的数组,slotDesc 这个我们就简单将其看成对一个对象的描述吧。hasWriteObjectMethod 表示的是什么呢?这个其实就是你要序列化这个对象是否有 writeObject 这个 private 方法,注意哦,这个方法并不是任何接口的方法,而是我们手动写的,读者可以参考 ArrayList 代码,它内部就有这个方法。那么这个方法的作用是什么呢?这个方法我们在上面也演示过具体的使用,它就是用于自定义序列化过程的,读者可以返回到上面看看如果使用这个 writeObject 实现自定义序列化过程的。注意:其实这个过程不像实现 Externalizable 接口那样,自己完全去自定义序列化数据。

③ defaultWriteFields 这个方法就是 JVM 自动帮我们序列化了

这个方法主要分为以下两步

  • ① 写入基本数据类型的数据
  • ②写入引用数据类型的数据,这里最终又调用到了 writeObject0() 方法,读者可以返回到上面去看看具体的实现。

readObject 原理分析

从流中读取类的描述信息 ObjectStreamClass 实例,通过这个对象就可以创建出序列化的对象。
1
2
3
4
5
6
7
8
9
10
11
ObjectStreamClass desc = readClassDesc(false);
...
Object obj;
try {
//创建对应反序列化的对象
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}

读取该对象及其对象的父类的 ObjectStreamClass信息

1
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();

然后遍历得到每一个 ObjectStreamClass 对象,将对应的属性值赋值给需要反序列化的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void defaultReadFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
bin.readFully(primVals, 0, primDataSize, false);
if (obj != null) {
desc.setPrimFieldValues(obj, primVals);
}
int objHandle = passHandle;
//从 ObjectStreamClass 中得到对象的所有 Field 信息
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
for (int i = 0; i < objVals.length; i++) {
ObjectStreamField f = fields[numPrimFields + i];
objVals[i] = readObject0(f.isUnshared());
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
if (obj != null) {
//将数据保存到对象中去
desc.setObjFieldValues(obj, objVals);
}
passHandle = objHandle;
}