本文写的比较细,推荐复制demo代码,然后一步一步跟随笔者的分析进行debug调试,这样跟能够帮助读者理解此文。
所谓的序列化即是一个将对象写入到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"));`
这两行其实就包括了整个序列化的流程。
首先来看ObjectOutputStream
,ObjectOutputStream
是一个实现了ObjectOutput
接口的OutputStream
的子类,其类定义如下:
`public class ObjectOutputStream` `extends InputStream implements ObjectInput, ObjectStreamConstants{` `...``}`
当我们实例化ObjectOutputStream
并传入参数后,首先调用的是ObjectOutputStream
的构造方法。
ObjectOutputStream
构造方法有两个,一个是public
的单参数构造函数,一个是protected
的无参构造函数,上述代码中我们传入了new FileOutputStream("panda.out")
为参数,因此调用的是ObjectOutputStream
的public
的单参数构造函,该函数内容如下:
`/**` `* 创建写入指定输出流的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
是什么?
BlockDataOutputStream
是ObjectOutputStream
的一个重要内部类,这个类负责将缓冲区中的数据写入到字节流。该类部分内容如下:
`/*``缓冲输出流有两种模式:在默认模式下,以与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
变量中写入魔术头以及版本号,如下图:
当ObjectOutputStream
的public
构造方法走完后,才会调用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
(因为在ObjectOutputStream
的public
构造方法中已经初始化了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,需要进一步序列化处理。
然后继续跟进:
判断当前传入对象是不是特殊类型的Class
和ObjectStreamClass
,如果是,则调用writeClass
或writeClassDesc
方法并且返回;
如上图,通过检查成员属性enableReplace
的值判断当前对象是否启用了“替换(Replace
)”功能;
但实际上enableReplace
的值通常为false
我们并不会进入这一代码段,然后进入二次检查代码段:
如果对象被替换,这里会对原始对象进行二次检查,和最开始的那段代码很像,这里先将替换对象插入到subs
(替换哈希表)中,然后进行类似的判断。
以上执行都完成过后,会处理剩余对象类型:
如果传入对象为String类型,那么调用writeString
方法将数据写入字节流;
如果传入对象为Array类型,那么调用writeArray
方法将数据写入字节流;
如果传入对象为Enum类型,调用writeEnum
方法将数据写入字节流;
如果传入对象实现了Serializable
接口,调用writeOrdinaryObject
方法将数据写入字节流;
以上条件都不满足时则抛出NotSerializableException
异常信息;
对于writeString
、writeArray
、writeEnum
的方法我们就不详谈了,只以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
方法。
writeProxyDesc
与writeString
方法较为类似且不在我们本次(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);
)
最后如下图所示,会写入每一个字段的信息,这里的字段信息包含三部分内容:TypeCode
、fieldName
、fieldType
于是,这里的debug就走完了:
接着,开启Data Block
模式,然后调用annotateClass
方法,annotateClass
方法没有具体实现,如下图:
该方法是提供给子类实现的方法,通常默认情况下这个方法什么也不做,与此类似的还有ObjectInputStream
中的resolveClass
方法。
在调用annotateClass
方法完成过后,代码会关闭Data Block
模式,然后写入TC_ENDBLOCKDATA
标记(表示当前非动态代理类的描述信息的终止)
到这里,writeNonProxy
和writeClassDescriptor
流程结束,同样,也导致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
标记。
值得一提的是,这个方法有一个切换上下文环境的过程——在检测协议前,首先令curPut
和curContext
为空,检测并写入数据后,再分别令curContext
curPut
为oldContext
和oldPut
,恢复执行之前的环境。
这里留下一个思考:为什么这里要切换上下文环境?
再来看看writeSerialData
,这个方法主要向obj对象写入数据信息,比如字段值和相关引用等,写入的时候会从顶级父类从上至下递归执行;看看这个方法的详细过程:
在序列化当前对象之前,先从类描述信息中获取ClassDataSlot
信息,在得到继承结构后,开始遍历。
首先判断可序列化对象是否重写了writeObject
方法,如果重写了该方法,则先开启Data Block
模式,再调用writeObject
方法,调用结束后再关闭Data Block
模式,并且在最后追加TC_ENDBLOCKDATA
标记(表示数据块写入终止),如果没有重写该方法,则调用defaultWriteFields
方法写入当前对象中的所有字段信息,跟进defaultWriteFields
方法:
defaultWriteFields
方法负责读取 obj 对象中的字段数据(desc
),并且将字段数据写入到字节流中,具体流程如下:
首先利用checkDefaultSerialize()
检查当前对象是否是一个可序列化对象
如果该对象不可序列化,那么抛出newInvalidClassException
异常。
检查完毕后,获取该对象中所有基础类型字段的值
会进入getPrimFieldValues
方法中的getPrimFieldValues
方法:
这些基础类型字段对应类型如下所示:
获得这些基础类型字段的值后,系统会将他们写入到字节流
在写入过程结束,系统会再调用writeObject0
方法:
在这个方法里写入对象类型的字段的值,最终完成序列化操作
其大概的流程如以下调用栈
最后再通过流程图回顾一下整个序列化的流程(可以点击原文链接看高清一点的):
序列化的流程说起来简单也很简单,实际上就是几个write*
方法:writeFataException
、writeNull
、writeHandle
、writeClass
、writeProxyDesc
、writeNonProxyDesc
、writeString
、writeArray
、writeEnum
,加两个特殊的write*
方法:writeExternalData
、writeOrginaryObject
。
序列化的流程说起来也很复杂,除了各种判断检测分支,还有各种特性:如被transient
修饰的成员属性具有”不会序列化“的语义,序列化的时候会忽略、被static
修饰的成员属性隶属于类而非对象,所以它在序列化的时候同样会被忽略。
但总的来说,搞懂序列化的某个流程(走到最后的write*
)对于理解序列化机制是很有帮助的。
https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html
https://blog.csdn.net/silentbalanceyh/article/details/8294269