长亭百川云 - 文章详情

序列化流程分析总结

技术猫屋

73

2024-07-13

0x01 写在前面

本文写的比较细,推荐复制demo代码,然后一步一步跟随笔者的分析进行debug调试,这样跟能够帮助读者理解此文。

0x02 流程分析

所谓的序列化即是一个将对象写入到IO流中的过程。序列化的步骤通常是首先创建一个ObjectOutputStream输出流,然后调用ObjectOutputStream对象的writeObject方法,按照一定格式(上面提到的)输出可序列化对象。

如下段Demo代码:

`package com.panda.alipay;``import java.io.*;``public class Main {`    `public static class Demo implements Serializable {`        `private String string;`        `transient String name = "hello";`        `public Demo(String s) {`            `this.string = s;`        `}`        `public static void main(String[] args) throws IOException {`            `Demo demo = new Demo("panda");`            `ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("panda.out"));`            `outputStream.writeObject(new Demo("panda"));`            `outputStream.close();`        `}`    `}``}`

整个代码中最关键的两行为:

`ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("panda.out"));` `outputStream.writeObject(new Demo("panda"));`

这两行其实就包括了整个序列化的流程。

首先来看ObjectOutputStreamObjectOutputStream是一个实现了ObjectOutput接口的OutputStream的子类,其类定义如下:

`public class ObjectOutputStream`    `extends InputStream implements ObjectInput, ObjectStreamConstants{`    `...``}`

当我们实例化ObjectOutputStream并传入参数后,首先调用的是ObjectOutputStream的构造方法。

ObjectOutputStream构造方法有两个,一个是public的单参数构造函数,一个是protected的无参构造函数,上述代码中我们传入了new FileOutputStream("panda.out")为参数,因此调用的是ObjectOutputStreampublic的单参数构造函,该函数内容如下:

`/**`        `* 创建写入指定输出流的ObjectOutputStream。`        `* 此构造函数将序列化流头写入底层流;`        `* 调用者可能希望立即刷新流,以确保接收ObjectInputStreams的构造函数在读取头时不会阻塞。`        `* 如果安装了安全管理器,则当重写ObjectOutputStream.putFields或ObjectOutputStream.writeUnshared方法的子类的构造函数直接或间接调用时,此构造函数将检查“enableSublassimplementation”SerializablePermission。`     `*/`    `public ObjectOutputStream(OutputStream out) throws IOException {`        `verifySubclass();`        `bout = new BlockDataOutputStream(out);`        `handles = new HandleTable(10, (float) 3.00);`        `subs = new ReplaceTable(10, (float) 3.00);`        `enableOverride = false;`        `writeStreamHeader();`        `bout.setBlockDataMode(true);`        `if (extendedDebugInfo) {`            `debugInfoStack = new DebugTraceInfoStack();`        `} else {`            `debugInfoStack = null;`        `}`    `}`

在该构造函数的开始,首先会调用verifySubclass方法处理缓存信息,要求该类(或子类)进行验证——验证是否可以在不违反安全约束的情况下构造此实例。

然后初始化bout等,实例化一个BlockDataOutputStream

思考:bout等是什么?BlockDataOutputStream是什么?为什么要在这里初始化bout成员属性?

1、bout等是什么?

bout是主类中的成员属性,除了bout还有几个成员属性,比如handles:是一个哈希表,表示从对象到引用的映射;subs:同样是一个哈希表,表示从对象到“替换对象”的一个映射关系;enableOverride:布尔型常量,用于决定在序列化Java对象时选用writeObjectOverride方法还是writeObject方法。

`/** filter stream for handling block data conversion */`    `private final BlockDataOutputStream bout;`    `/** obj -> wire handle map */`    `private final HandleTable handles;`    `/** obj -> replacement obj map */`    `private final ReplaceTable subs;``/** if true, invoke writeObjectOverride() instead of writeObject() */`    `private final boolean enableOverride;`

我们可以把bout 可以理解为一个 “容器”,它用于处理数据块转换的过滤流。

2、BlockDataOutputStream是什么?

BlockDataOutputStreamObjectOutputStream的一个重要内部类,这个类负责将缓冲区中的数据写入到字节流。该类部分内容如下:

`/*``缓冲输出流有两种模式:在默认模式下,以与DataOutputStream相同的格式输出数据;在“块数据”模式下,输出由块数据标记括起来的数据(有关详细信息,请参阅对象序列化规范)。``*/``   ``private static class BlockDataOutputStream extends OutputStream implements DataOutput`    `{`        `/** maximum data block length */`        `private static final int MAX_BLOCK_SIZE = 1024;`        `/** maximum data block header length */`        `private static final int MAX_HEADER_SIZE = 5;`        `/** (tunable) length of char buffer (for writing strings) */`        `private static final int CHAR_BUF_SIZE = 256;``   `        `/** buffer for writing general/block data */`        `private final byte[] buf = new byte[MAX_BLOCK_SIZE];`        `/** buffer for writing block data headers */`        `private final byte[] hbuf = new byte[MAX_HEADER_SIZE];`        `/** char buffer for fast string writes */`        `private final char[] cbuf = new char[CHAR_BUF_SIZE];``   `        `/** block data mode */`        `private boolean blkmode = false;`        `/** current offset into buf */`        `private int pos = 0;``   `        `/** underlying output stream */`        `private final OutputStream out;`        `/** loopback stream (for data writes that span data blocks) */`        `private final DataOutputStream dout;``   `        `/**`         `* Creates new BlockDataOutputStream on top of given underlying stream.`         `* Block data mode is turned off by default.`         `*/`        `BlockDataOutputStream(OutputStream out) {`            `this.out = out;`            `dout = new DataOutputStream(this);`        `}`    `    ......`        `}``   ``   `

可以看到,这个类的定义和主类(ObjectOutputStream)的定义有些相似,唯独不同的就是实现的接口。

其实可以理解成BlockDataOutputStream类是封装后的DataOutputStream类,并且提供了一些缓冲区及成员属性。

3、为什么要在这里初始化bout成员属性?

writeObject0方法的代码中,会主要使用到bout对象的方法setBlockDataMode关闭Data Block模式;

Data Block模式:

在JDK 1.2中,有必要修改和JDK 1.1不兼容的字节流格式;为了处理这种情况,向前兼容性是必须的,一个兼容标记将会写入到字节流中,这个兼容标记是类似PROTOCOL_VERSION的格式,ObjectOutputStream中的useProtocolVersion方法会接收一个参数以表示写入的可序列化字节流的协议版本。

使用的字节流协议版本如下:

  • ObjectStreamConstants.PROTOCOL_VERSION_1:表示最初序列化字节流的格式;

  • ObjectStreamConstants.PROTOCOL_VERSION_2:表示新的外部字节流格式,基础类型的数据将会使用数据块【Data-Block】的模式写入字节流,它以标记TC_ENDBLOCKDATA结束

数据块的边界是标准化的,使用数据块模式写入字节流的基础类型的数据通常不能超过1024字节长度,这种变化的好处是固定以及规范化序列化数据格式,有利于其向前和向后的兼容性。

JDK1.2默认使用PROTOCOL_VERSION_2``JDK1.1默认使用PROTOCOL_VERSION_1``JDK 1.1.7版本以及以上的版本可读取以上的两种版本,而JDK 1.1.7之前的版本只能读取PROTOCOL_VERSION_1版本;

详见《Object Serialization Stream Protocol》原版:

https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html

或者也可以看我翻译总结的《Object Serialization Stream Protocol/对象序列化流协议》:

https://www.cnpanda.net/talksafe/892.html

回到正题,在初始化完几个成员属性之后,调用了writeStreamHeader()方法,跟进可以发,这个方法就是用于ObjectOutputStream在实例初始化时向bout变量中写入魔术头以及版本号,如下图:

ObjectOutputStreampublic构造方法走完后,才会调用writeObject()开始写对象数据,该方法的主要代码如下:

`public final void writeObject(Object obj) throws IOException {`        `if (enableOverride) {`            `writeObjectOverride(obj);`            `return;`        `}`        `try {`            `writeObject0(obj, false);`        `} catch (IOException ex) {`            `if (depth == 0) {`                `writeFatalException(ex);`            `}`            `throw ex;`        `}`    `}`

通常来说enableOverride的默认值为false(因为在ObjectOutputStreampublic构造方法中已经初始化了enableOverride = false;

然后才是进入了关键方法writeObject0进一步序列化,该方法如下(略长):

 `/**`     `* Underlying writeObject/writeUnshared implementation.`     `*/`    `private void writeObject0(Object obj, boolean unshared)`        `throws IOException`    `{`        `boolean oldMode = bout.setBlockDataMode(false);`        `depth++;`        `try {`            `// handle previously written and non-replaceable objects`            `int h;`            `if ((obj = subs.lookup(obj)) == null) {`                `writeNull();`                `return;`            `} else if (!unshared && (h = handles.lookup(obj)) != -1) {`                `writeHandle(h);`                `return;`            `} else if (obj instanceof Class) {`                `writeClass((Class) obj, unshared);`                `return;`            `} else if (obj instanceof ObjectStreamClass) {`                `writeClassDesc((ObjectStreamClass) obj, unshared);`                `return;`            `}``   `            `// check for replacement object`            `Object orig = obj;`            `Class<?> cl = obj.getClass();`            `ObjectStreamClass desc;`            `for (;;) {`                `// REMIND: skip this check for strings/arrays?`                `Class<?> repCl;`                `desc = ObjectStreamClass.lookup(cl, true);`                `if (!desc.hasWriteReplaceMethod() ||`                    `(obj = desc.invokeWriteReplace(obj)) == null ||`                    `(repCl = obj.getClass()) == cl)`                `{`                    `break;`                `}`                `cl = repCl;`            `}`            `if (enableReplace) {`                `Object rep = replaceObject(obj);`                `if (rep != obj && rep != null) {`                    `cl = rep.getClass();`                    `desc = ObjectStreamClass.lookup(cl, true);`                `}`                `obj = rep;`            `}``   `            `// if object replaced, run through original checks a second time`            `if (obj != orig) {`                `subs.assign(orig, obj);`                `if (obj == null) {`                    `writeNull();`                    `return;`                `} else if (!unshared && (h = handles.lookup(obj)) != -1) {`                    `writeHandle(h);`                    `return;`                `} else if (obj instanceof Class) {`                    `writeClass((Class) obj, unshared);`                    `return;`                `} else if (obj instanceof ObjectStreamClass) {`                    `writeClassDesc((ObjectStreamClass) obj, unshared);`                    `return;`                `}`            `}``   `            `// remaining cases`            `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());`                `}`            `}`        `} finally {`            `depth--;`            `bout.setBlockDataMode(oldMode);`        `}`    `}`

来一点一点分析。

writeObject0()方法最开始的地方:

 boolean oldMode = bout.setBlockDataMode(false);

首先代码先关闭输出流的Data Block模式,并且将原始模式赋值给变量oldMode,然后会进入以下代码块进行判断:

在上面的代码块的主要功能就是像其注释写的一样,用于处理已经处理过的不可替换的对象,这些都是不能够序列化的,其实在大多数情况下,我们的代码都不会进入这个代码块。

具体来看,代码首先会进入subs.lookup(obj)进行判断,如下图:

根据这个方法的描述——查找并返回给定对象的替换。如果找不到替换,则返回查找对象本身。

也就是说,这个方法实际上就是处理以前写入的对象和不可替换的对象。更直白点的意思,这段代码实际上做的是一个检测功能,如果检测到当前传入对象在“替换哈希表(ReplaceTable)”中无法找到,那么就调用writeNull方法。

接着继续判断当前写入方式是不是“unshared”方式,然后可以看到紧跟着的就是handles.lookup(obj),跟进去的话:

lookup方法会查找并返回与给定对象关联的handler,如果没有找到映射,则返回 -1,直白的意思就是说判断是否在“引用哈希表(HandleTable)”中找到该引用,如果有,那么调用writeHandle方法并且返回;如果没找到,那么返回-1,需要进一步序列化处理。

然后继续跟进:

判断当前传入对象是不是特殊类型的ClassObjectStreamClass,如果是,则调用writeClasswriteClassDesc方法并且返回;

如上图,通过检查成员属性enableReplace的值判断当前对象是否启用了“替换(Replace)”功能;

但实际上enableReplace的值通常为false

我们并不会进入这一代码段,然后进入二次检查代码段:

如果对象被替换,这里会对原始对象进行二次检查,和最开始的那段代码很像,这里先将替换对象插入到subs(替换哈希表)中,然后进行类似的判断。

以上执行都完成过后,会处理剩余对象类型:

如果传入对象为String类型,那么调用writeString方法将数据写入字节流;

如果传入对象为Array类型,那么调用writeArray方法将数据写入字节流;

如果传入对象为Enum类型,调用writeEnum方法将数据写入字节流;

如果传入对象实现了Serializable接口,调用writeOrdinaryObject方法将数据写入字节流;

以上条件都不满足时则抛出NotSerializableException异常信息;

对于writeStringwriteArraywriteEnum的方法我们就不详谈了,只以writeString为例简单讲下。

    `private void writeString(String str, boolean unshared) throws IOException {`        `handles.assign(unshared ? null : str);`        `long utflen = bout.getUTFLength(str);`        `if (utflen <= 0xFFFF) {`            `bout.writeByte(TC_STRING);`            `bout.writeUTF(str, utflen);`        `} else {`            `bout.writeByte(TC_LONGSTRING);`            `bout.writeLongUTF(str, utflen);`        `}`    `}`

可以看到过程如下,首先在写入String对象之前,代码会判断当前写入方式是否是unshared,如果不是unshared方式还需要在handles的对象映射中插入当前String对象;接着,代码会调用getUTFLength函数获取String字符串的长度和0xFFFF比较,如果大于该值时,表示当前String对象是一个长字符串对象,那么会先写入TC_LONGSTRING标记(表示是LONGSTRING类型数据),然后写入字符串的长度和内容;如果小于等于该值时,表示当前String对象就是一个普通的字符串对象,那么会先写入TC_STRING标记(表示是一个STRING类型对象),然后写入字符串的长度和内容;

现在我们重点来看看writeOrdinaryObject方法。

在写入obj对象之前,代码会先调用checkSerialize()检查当前对象是否是一个可序列化对象,如果不是那么会终止本次序列化并抛出newInvalidClassException()错误:

如果是一个可序列化对象,那么会开始写入TC_OBJECT标记(表示开始),随后调用writeClassDesc方法写入当前对象所属类的类描述信息,跟进去:

writeClassDesc方法主要用于判断当前的类描述符使用什么方式写入,如果传入的类描述信息是一个null引用,那么会调用writeNull方法,如果没有使用unshared方式,并且可以在handles对象池中找到传入的对象信息,那么调用writeHandle,如果传入的类是一个动态代理类,那么调用writeProxyDesc方法,如果上面三个条件都不满足,那么调用writeNonProxyDesc方法。

writeProxyDescwriteString方法较为类似且不在我们本次(demo代码)的序列化流程中,因此不做赘述。

来看看writeNonProxyDesc

首先写入TC_CLASSDESC标记(表新类描述信息的开始)信息,然后判断使用的模式是unshared模式,那么将desc所表示的类元数据信息插入到handles对象的映射表中,然后根据使用的流协议版本调用不同的write方法,如果使用的流协议是PROTOCOL_VERSION_1,那么直接调用desc成员的writeNonProxy方法,并且将当前引用this作为实参传入到writeNonProxy方法中,如果使用的不是PROTOCOL_VERSION_1协议,那么会调用当前类中的writeClassDescriptor方法。

会调用writeNonProxy方法,跟进:

先调用writeUTF方法写入类名到字节流,这里的类名是类全名,带了包名的那种(out.writeUTF(name);

再调用writeLong方法写入serialVersionUID的值到字节流(out.writeLong(getSerialVersionUID());

然后开始写入当前类中成员属性的数量信息到字节流(out.writeShort(fields.length);

最后如下图所示,会写入每一个字段的信息,这里的字段信息包含三部分内容:TypeCodefieldNamefieldType

于是,这里的debug就走完了:

接着,开启Data Block模式,然后调用annotateClass方法,annotateClass方法没有具体实现,如下图:

该方法是提供给子类实现的方法,通常默认情况下这个方法什么也不做,与此类似的还有ObjectInputStream中的resolveClass方法。

在调用annotateClass方法完成过后,代码会关闭Data Block模式,然后写入TC_ENDBLOCKDATA标记(表示当前非动态代理类的描述信息的终止)

到这里,writeNonProxywriteClassDescriptor流程结束,同样,也导致writeClassDesc流程结束,并且回到writeOrdinaryObject方法。

继续来看writeOrdinaryObject下面的代码

如果使用的模式是unshared模式,则将desc所表示的类元数据信息插入到handles对象的映射表中,最后会判断当前Java对象的序列化语义,如果当前对象不是一个动态代理类并且是实现了外部化的,则调用writeExternalData方法写入对象信息,如果当前对象是一个实现了Serializable接口的,则调用writeSerialData方法写入对象信息。

writeExternalData主要代码如下:

    `private void writeExternalData(Externalizable obj) throws IOException {`        `PutFieldImpl oldPut = curPut;`        `curPut = null;`        `if (extendedDebugInfo) {`            `debugInfoStack.push("writeExternal data");`        `}`        `SerialCallbackContext oldContext = curContext;`        `try {`            `curContext = null;`            `if (protocol == PROTOCOL_VERSION_1) {`                `obj.writeExternal(this);`            `} else {`                `bout.setBlockDataMode(true);`                `obj.writeExternal(this);`                `bout.setBlockDataMode(false);`                `bout.writeByte(TC_ENDBLOCKDATA);`            `}`        `} finally {`            `curContext = oldContext;`            `if (extendedDebugInfo) {`                `debugInfoStack.pop();`            `}`        `}`        `curPut = oldPut;`    `}`

再这个方法内会首先判断当前使用的字节流协议,如果使用的是PROTOCOL_VERSION_1协议,那么回直接调用可序列化对象中的writeExternal方法,如果使用的不是PROTOCOL_VERSION_1协议,那么会先开启Data Block模式,再调用writeExternal方法,调用完毕后再关闭Data Block模式并在该流的最后追加TC_ENDBLOCKDATA标记。

值得一提的是,这个方法有一个切换上下文环境的过程——在检测协议前,首先令curPutcurContext为空,检测并写入数据后,再分别令curContext curPutoldContextoldPut,恢复执行之前的环境。

这里留下一个思考:为什么这里要切换上下文环境?

再来看看writeSerialData,这个方法主要向obj对象写入数据信息,比如字段值和相关引用等,写入的时候会从顶级父类从上至下递归执行;看看这个方法的详细过程:

在序列化当前对象之前,先从类描述信息中获取ClassDataSlot信息,在得到继承结构后,开始遍历。

首先判断可序列化对象是否重写了writeObject方法,如果重写了该方法,则先开启Data Block模式,再调用writeObject方法,调用结束后再关闭Data Block模式,并且在最后追加TC_ENDBLOCKDATA标记(表示数据块写入终止),如果没有重写该方法,则调用defaultWriteFields方法写入当前对象中的所有字段信息,跟进defaultWriteFields方法:

defaultWriteFields方法负责读取 obj 对象中的字段数据(desc),并且将字段数据写入到字节流中,具体流程如下:

首先利用checkDefaultSerialize()检查当前对象是否是一个可序列化对象

如果该对象不可序列化,那么抛出newInvalidClassException异常。

检查完毕后,获取该对象中所有基础类型字段的值

会进入getPrimFieldValues方法中的getPrimFieldValues方法:

这些基础类型字段对应类型如下所示:

获得这些基础类型字段的值后,系统会将他们写入到字节流

在写入过程结束,系统会再调用writeObject0方法:

在这个方法里写入对象类型的字段的值,最终完成序列化操作

其大概的流程如以下调用栈

最后再通过流程图回顾一下整个序列化的流程(可以点击原文链接看高清一点的):

0x03 总结

序列化的流程说起来简单也很简单,实际上就是几个write*方法:writeFataExceptionwriteNullwriteHandlewriteClasswriteProxyDescwriteNonProxyDescwriteStringwriteArraywriteEnum,加两个特殊的write*方法:writeExternalDatawriteOrginaryObject

序列化的流程说起来也很复杂,除了各种判断检测分支,还有各种特性:如被transient修饰的成员属性具有”不会序列化“的语义,序列化的时候会忽略、被static修饰的成员属性隶属于类而非对象,所以它在序列化的时候同样会被忽略。

但总的来说,搞懂序列化的某个流程(走到最后的write*)对于理解序列化机制是很有帮助的。

0x04 参考

https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html

https://blog.csdn.net/silentbalanceyh/article/details/8294269

https://blog.csdn.net/u011315960/article/details/89963230

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2