0%

Java对象序列化流协议梳理

序言

浮云游子意,落日故人情。

协议原文Object Serialization Stream Protocol

stream流元素

stream,流元素,就是用来表示流中的对象。

现在在我们流中的每一个对象都需要表示,比如对象的类和类中的field字段。

流中对象的表示可以用语法来描述:

1
2
3
4
5
6
7
null objects
new objects
classes
arrays
strings
handles (句柄,流中对象的方向引用)
这个句柄从 0x7E0000 开始按顺序分配句柄。当流重置时,句柄在 0x7E0000 处重新启动。

stream中对象基本结构:

类对象由的ObjectStreamClass对象表示

非动态代理类对象的ObjectStreamClass由以下成分组成:

  • 兼容类的流唯一标识符 (SUID)
  • 一组指示类的各种属性的标志,例如该类是否定义了writeObject方法,以及该类是否可序列化、可外部化或枚举类型
  • 可序列化字段的个数
  • 默认情况下,对于类的字段数组和对象字段来说,字段的类型要作为字符串被包含,字段描述符格式Ljava/lang/Object;
  • annotateClass方法写入的可选数据块
  • 该对象父类的ObjectStreamClass ,如果父类不可序列化,该字段则为null

动态代理类对象的ObjectStreamClass由以下成分组成:

  • 动态代理类实现的接口数量
  • 动态代理类实现的所有接口名称,这些借口通过调用Class的getINterfaces方法的返回结果进行排序列出
  • annotateProxyClass 方法写入的可选块数据记录或对象
  • 父类对应的ObjectStreamClassjava.lang.reflect.Proxy

数组对象由以下成分组成:

  • 他们的 ObjectStreamClass对象。
  • 元素的数量。
  • 值的序列。值的类型在数组的类型中是隐式的。例如,字节数组的值是byte类型。

Enum枚举类型:

  • 常量的基本枚举类型的ObjectStreamClass对象。
  • 常量的名称字符串。

流中的新对象new Objects由以下成组成:

  • 所有对象类的派生类信息

  • 对象的每一个可序列化类的数据,从最上面的父类开始写入。

    对于每个类,流包含以下内容:

    • 可序列化字段:
    • 如果类有writeObject/readObject方法,那么有可能出现通过writeObject方法写入的可选对象或者基础类型的数据块Data-Block,跟着使用endDataBlock方法

备注:

所有由类写入的原始数据都被缓冲并包裹在块数据记录中,无论数据是在writeObject方法中写入流中,还是在writeObject方法之外直接写入流中。这些数据只能被相应的readObject方法读取或直接从流中读取。由writeObject方法写入的对象会终止之前的任何块数据记录,并根据情况被写成普通对象或空或反向引用。块数据记录允许错误恢复以丢弃任何可选数据。

当从一个类中调用时,流可以丢弃任何数据或对象,直到遇到endBlockData。

序列化流格式

现在我们可以在对象流中随便抓一个stream。自顶向下解析它。

stream

1
2
stream:
magic version contents

每个stream对象都是由三部分组成:

  • magic : 魔数 STREAM_MAGIC 常量类型 表示内容类型
  • version:jdk版本号 STREAM_VERSION 常量类型
  • contents :流对象内容

STREAM_MAGICSTREAM_VERSION等常量值都在ObjectStreamConstants接口中定义。

contents

1
2
3
contents:
content
contents content

类似CFL,流中contents可以由一个content组成,也可以由多个contents组成

content

这里以一个content为例:

1
2
3
content:
object
blockdata

一个content可以是一个对象(object),也可以是一个块数据(blockdata)。block在下面会详细说。

object

1
2
3
4
5
6
7
8
9
10
11
object:
newObject
newClass
newArray
newString
newEnum
newClassDesc
prevObject
nullReference
exception
TC_RESET

对象序列化流中的”对象”与Java中的对象概念有些不一样。对象序列化流中的”对象”分为上面那些种,最常见的是newObject、newString、newClassDesc。

  • newClassDesc表示ObjectStreamClass类的对象,可以简单理解为类的描述符。

  • newClass表示Class类对象,如person.class对象,就是Class类的一个实例对象。

  • newObject表示一个普通的对象,如果一个对象不是其他几种类型的对象(如newString、newClassDesc、newClass等),就归到newObject,如person对象。

接下来也是按照这个顺序解析:

newClassDesc

表示是一个类描述符

1
2
3
newClassDesc:
TC_CLASSDESC className serialVersionUID newHandle classDescInfo
TC_PROXYCLASSDESC newHandle proxyClassDescInfo

这里列出来了两种类描述符种类:

  • 一般正常的类描述符(主流)

    1
    TC_CLASSDESC className serialVersionUID newHandle classDescInfo

    TC_CLASSDESC:类描述符的开始标志

    className:类名

    serialVersionUID:序列化ID

    newHandleL:新的引用

    classDescInfo:类信息

  • 动态代理类的类描述符

newObject

表示该部分是一个新的对象。

1
2
newObject:
TC_OBJECT classDesc newHandle classdata[] // data for each class

TC_OBJECT:常量,表示接下来是一个一个序列化Object的开始标志。

classDesc:当前这个对象的类描述符,里面存放的是类信息,字段信息。

newHandle:当前这个对象的引用句柄。

classData[]:这个对象对应的每一个Class的相关数据信息。这部分在下面会详细说

newClass

表示该部分是一个新的Class类型的对象。

1
2
newClass:
TC_CLASS classDesc newHandle

TC_CLASS:类型标记,表示接下来是一个序列化Class类型的对象。

classDesc:表示这个Class对象的类描述符。

newHandle: 新的引用。

classDesc

表示一个对象的类描述符

1
2
3
4
5
classDesc:
newClassDesc
nullReference
(ClassDesc)prevObject // an object required to be of type
// ClassDesc

newClassDesc:对象的类描述符

nullReference:空引用

(ClassDesc)prevObject :表示前面出现过的对象(要求为ClassDesc类型的对象

superClassDesc

表示父类的描述符

1
2
superClassDesc:
classDesc

如果被序列化对象的类,如果其父类没有实现Serializable接口,这个地方就是TC_NULL,表示空对象。

如果其父类实现了实现了Serializable接口,那此处会写入其父类对应的ObjectStreamClass对象,父类描述符。

classDescInfo

表示是详细的类描述信息

1
2
classDescInfo:
classDescFlags fields classAnnotation superClassDesc

classDescFlags:类描述信息标记

fields :类中所有字段的描述信息

classAnnotation :和类相关的Annotation的描述信息

superClassDesc:该类的父类的描述信息

proxyInterface

1
2
proxyInterfaceName:
(utf)

动态代理类的代理接口的名称,一个UTF-8格式的字符串对应的二进制序列;

proxyClassDescInfo

1
2
3
proxyClassDescInfo:
(int)<count> proxyInterfaceName[count] classAnnotation
superClassDesc

动态代理类的相关描述信息

<count>表示该动态代理类实现的接口总数,类型为int类型

proxyInterfaceName[count]表示所有当前动态代理类实现的接口信息

classAnnotation表示该动态代理类对应的Annotation的描述信息

superClassDesc表示当前动态代理类的父类的类描述信息

fields

1
2
fields:
(short)<count> fieldDesc[count]

<count>表示该类中fields总数,数据类型为short类型。

fieldDesc[count]表示一个类中所有字段的详细描述信息,字段的数量和前边的count是一致的;

fieldDesc

1
2
3
fieldDesc:
primitiveDesc
objectDesc

表示fields描述信息

两部分:

  • primitiveDesc : 基础类型数据的描述符
  • objectDesc :对象数据类型的描述信息

primitiveDesc

1
2
primitiveDesc:
prim_typecode fieldName

表示8种基础类型的字段的相关描述信息。

prim_typecode :基本类型字段的类型 如下

fieldName : 字段名字

1
2
3
4
5
6
7
8
`B`       // byte
`C` // char
`D` // double
`F` // float
`I` // integer
`J` // long
`S` // short
`Z` // boolean

objectDesc

对象类型的field的描述信息。

对象类型字段 = 该成员

1
2
objectDesc:
obj_typecode fieldName className1

obj_typecode :该成员的类型 如下

fieldName:该成员的名字

className1:该对象的类全名,String,// 包含字段类型的字符串,字段描述符格式

1
2
`[`       // array
`L` // object

classAnnotation

1
2
3
classAnnotation:
endBlockData
contents endBlockData // contents written by annotateClass

该对象所属类中的Annotation的描述信息

endBlockData : 终止符 意味着存储对象的数据块[Data-Block]的结束

contents endBlockData:该类中多个content的终止

这里多说一点:

classAnotation是由ObjectOutputStream的annotateClass()方法写入的。

由于annotateClass()方法默认什么都不做。所以classAnnotations一般都是TC_ENDBLOCKDATA。

newArray

一个新的数组的描述符

1
2
newArray:
TC_ARRAY classDesc newHandle (int)<size> values[size]

TC_ARRAY :表示新的数组类型的序列化对象的开始

classDesc:这个数组的类描述符号

newHandle:针对当前数组对象的引用

(int)<size>:该数组的长度,长度为int类型

values[size]:表示当前数组每一个元素值部分的内容

newString

1
2
3
newString:
TC_STRING newHandle (utf)
TC_LONGSTRING newHandle (long-utf)

表示一个字符串类型的对象

两种类型:STRING LONGSTRING

newEnum

表示一个枚举类型的对象

1
2
newEnum:
TC_ENUM classDesc newHandle enumConstantName

TC_ENUM为枚举类型的标识,表示接下来的序列类型是枚举类型

classDesc为一个枚举类型的类描述符

newHandle为该枚举对象的引用

enumConstantName的值为调用枚举类型中的name()方法返回的枚举类型的值对应的字符串字面量

enumConstantName

1
2
enumConstantName:
(String)object

枚举常量的字符串名称字面量,本身为一个字符串。

prevObject

1
2
prevObject
TC_REFERENCE (int)handle

表示前一个对象,handle表示是前一个对象的引用。

nullReference

1
2
nullReference
TC_NULL

表示null,一般这个值表示空引用。

exception

1
2
exception:
TC_EXCEPTION reset (Throwable)object reset

表示异常

TC_EXCEPTION : 异常信息的标识符

blockdata

1
2
3
blockdata:
blockdatashort
blockdatalong

在Java序列化中,数据块存储分为种:

一种是长度为short的默认数据块方式

另外一种是长度为int的数据块方式,这种方式可存储容量大的数据;

如果我们只是往流中写入的是基本数据类型的数据,比如整数、浮点数,会在流中使用blockdata进行标记。

endBlockData

Data-Block结束的标记

1
2
3
4
5
6
7
8
blockdatashort:
TC_BLOCKDATA (unsigned byte)<size> (byte)[size]

blockdatalong:
TC_BLOCKDATALONG (int)<size> (byte)[size]

endBlockData :
TC_ENDBLOCKDATA

classdata[]

1
2
3
4
5
6
7
8
9
classdata:
nowrclass // SC_SERIALIZABLE & classDescFlag &&
// !(SC_WRITE_METHOD & classDescFlags)
wrclass objectAnnotation // SC_SERIALIZABLE & classDescFlag &&
// SC_WRITE_METHOD & classDescFlags
externalContents // SC_EXTERNALIZABLE & classDescFlag &&
// !(SC_BLOCKDATA & classDescFlags
objectAnnotation // SC_EXTERNALIZABLE & classDescFlag&&
// SC_BLOCKDATA & classDescFlags

nowrclass

1
2
nowrclass:
values // 类描述符顺序的字段

一个类中可序列化的字段的数据值,这些数据值的顺序遵循类描述符中定义的顺序;

wrclass

1
2
wrclass:
nowrclass

这部分数据的内容和上述的nowrclass部分的内容是一样的,表一个类中可序列化的字段的数据值;

externalContents

1
2
3
externalContents:         // externalContent written by 
externalContent // writeExternal in PROTOCOL_VERSION_1.
externalContents externalContent

PROTOCOL_VERSION_1中由writeExternal编写的外部内容。

这部分内容是上述的external内容的一个集合,一般这一部分只包含了使用writeExternal方法以PROTOCOL_VERSION_1的版本写入字节流的数据;

externalContent

1
2
3
externalContent:          // Only parseable by readExternal
( bytes) // primitive data 基础数据 8种
object

这部分描述的是external的相关内容

(bytes)部分的数据只能被readExternal方法读取,而且里面一般包含的数据类型是基础类型数据,object表示对象数据类型;

objectAnnotation

1
2
3
4
objectAnnotation:
endBlockData
contents endBlockData // contents written by writeObject
// or writeExternal PROTOCOL_VERSION_2.

这部分数据的内容和classAnnotation的数据结构是一致的;

表示该对象所属类中的Annotation的描述信息,endBlockData为存储对象的数据块【Data-Block】的结束标记,为终止符,contents表示该类中多个内容的一个集合【contents】;

values

针对当前对象的classDesc对应的类描述信息提供描述类型的大小和类型;

The size and types are described by the classDesc for the current object

newHandle

序列中的下一个数值将赋值给一个可序列化或者可执行反序列化的对象引用;

reset

一个已知对象的集合将会被放弃,重置该字节流;

// The set of known objects is discarded so the objects of the exception do not overlap with the previously sent objects or with objects that may be sent after the exception

终端常量标识符

java.io.ObjectStreamConstants中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final static short STREAM_MAGIC = (short)0xaced;
final static short STREAM_VERSION = 5;
final static byte TC_NULL = (byte)0x70;
final static byte TC_REFERENCE = (byte)0x71;
final static byte TC_CLASSDESC = (byte)0x72;
final static byte TC_OBJECT = (byte)0x73;
final static byte TC_STRING = (byte)0x74;
final static byte TC_ARRAY = (byte)0x75;
final static byte TC_CLASS = (byte)0x76;
final static byte TC_BLOCKDATA = (byte)0x77;
final static byte TC_ENDBLOCKDATA = (byte)0x78;
final static byte TC_RESET = (byte)0x79;
final static byte TC_BLOCKDATALONG = (byte)0x7A;
final static byte TC_EXCEPTION = (byte)0x7B;
final static byte TC_LONGSTRING = (byte) 0x7C;
final static byte TC_PROXYCLASSDESC = (byte) 0x7D;
final static byte TC_ENUM = (byte) 0x7E;
final static int baseWireHandle = 0x7E0000;

classDescFlags会用到的:

1
2
3
4
5
final static byte SC_WRITE_METHOD = 0x01; //if SC_SERIALIZABLE
final static byte SC_BLOCK_DATA = 0x08; //if SC_EXTERNALIZABLE
final static byte SC_SERIALIZABLE = 0x02;
final static byte SC_EXTERNALIZABLE = 0x04;
final static byte SC_ENUM = 0x10;

协议还说的一段话,暂时看不懂,先写上:

The flag SC_WRITE_METHOD is set if the Serializable class writing the stream had a writeObject method that may have written additional data to the stream. In this case a TC_ENDBLOCKDATA marker is always expected to terminate the data for that class.

The flag SC_BLOCKDATA is set if the Externalizable class is written into the stream using STREAM_PROTOCOL_2. By default, this is the protocol used to write Externalizable objects into the stream in JDK 1.2. JDK 1.1 writes STREAM_PROTOCOL_1.

The flag SC_SERIALIZABLE is set if the class that wrote the stream extended java.io.Serializable but not java.io.Externalizable, the class reading the stream must also extend java.io.Serializable and the default serialization mechanism is to be used.

The flag SC_EXTERNALIZABLE is set if the class that wrote the stream extended java.io.Externalizable, the class reading the data must also extend Externalizable and the data will be read using its writeExternal and readExternal methods.

The flag SC_ENUM is set if the class that wrote the stream was an enum type. The receiver’s corresponding class must also be an enum type. Data for constants of the enum type will be written and read as described in Section 1.12, “Serialization of Enum Constants“.

如果写入流的可序列化类具有writeObject方法,并且若该方法已将其他数据写入 stream ,则会设置标志SC_WRITE_METHOD。在这种情况下,TC_ENDBLOCKDATA标记总是希望终止该类的数据。

如果使用SC_BLOCKDATAExternalizable类写入 stream,则设置标志SC_BLOCKDATA。默认情况下,在JDK 1.2中将Externalizable对象写入stream的协议。JDK1.1中写入STREAM_PROTOCOL_1

如果编写 stream 的类扩展了java.io.SERIALIZABLE而不是java.io.Externalizable,那么会设置标志 SC_SERIALIZABLE,读取 stream 的类也必须扩展java.io.SERIALIZABLE,并使用默认的序列化机制。

如果编写 stream 扩展java.io.EXTERNALIZABLE的类,读取数据的类也必须扩展EXTERNALIZABLE,并且如果使用其writeExternalreadExternal方法读取数据,那么会设置标记SC_EXTERNALIZABLE

如果写入 stream 的类是枚举类型,则会设置标志SC_ENUM。接收方的对应类也必须是枚举类型。

例子

这里写一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Person implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public static void main(String[] args) throws Exception {
Person person = new Person("0range", 100);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("0rangetest.ser"));
oos.writeObject(person);
oos.close();
}
}

直接上SerializationDumper看结果

1
$ java -jar SerializationDumper.jar  -r test.ser

image-20210716200321909

思考一个问题,如果我们Person实现了writeObject方法,会怎么样呢?

如果Person有writeObject方法,那要怎么设计呢?

先解释第二个问题,来到java.io.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
38
39
40
/**
* Writes instance data for each serializable class of given object, from
* superclass to subclass.
*/
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);
}
}
}

可以看到,如果该类有writeObject方法,那么就slotDesc.invokeWriteObject(obj, this);

跟进invokeWriteObject:

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
/**
* Invokes the writeObject method of the represented serializable class.
* Throws UnsupportedOperationException if this class descriptor is not
* associated with a class, or if the class is externalizable,
* non-serializable or does not define writeObject.
*/
void invokeWriteObject(Object obj, ObjectOutputStream out)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (writeObjectMethod != null) {
try {
writeObjectMethod.invoke(obj, new Object[]{ out });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}

这里其实需要考虑writeObjectMethod这个属性

1
2
/** class-defined writeObject method, or null if none */
private Method writeObjectMethod;

这个属性本身类型就是java.lang.reflect.Method,属于反射的作用范围。

找了一圈发现,在ObjectStreamClass类的构造函数里面就有一句:

1
2
3
4
5
6
7
8
...
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
...

所以在这里,已经判断了这个类是否存在writeObject方法,同时也寻找了readObject方法。

如果writeObejct存在的话就封装为Method,赋值给writeObjectMethod属性。

细心的你应该也能发现,这里对writeObject做了限制:

  • 参数必须为ObjectOutputStream类型
  • 返回值必须为void
  • 必须为 private
  • 非static

到这里我们解答了第二个问题,并且知道,如果你要实现writeObject必须要形式如下:

1
2
3
public void writeObject(ObjectOutputStream oos){
...
}

那现在就写一个:

image-20210716202716995

我们在writeObject方法内部只是调用defaultWriteObject()方法写入对象字段数据。

再看一遍:

image-20210716203132016

可以发现在最后面多了一块:

image-20210717101423353

并且在前面多了一块:

1
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE

表明当前对象的类是有writeObject方法的

由于annotateClass()方法默认为空,所以objectAnnotations后一般会设置TC_ENDBLOCKDATA标识;

如果我们自己的writeObject不仅仅是defaultWriteObject

image-20210717101958948

对比看区别:

image-20210717101937318

在classdata部分又多出来了一些内容,也就是写入了自定义数据,

blockdata表示下面的就是一个数据块,因为我们往里存放的是一个整数666,所以长度为int类型的长度4,contents内容就是16进制的666。

参考

panda

xz.aliyun.com

Object Serialization Stream Protocol