目录
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"));
这两行其实就包括了整个序列化的流程。
首先来看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
方法并且返回;
当以上条件都不满足的时候(不进入if),开始检查是否开启了替换对象。
如上图,通过检查成员属性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
方法:
在这个方法里写入对象类型的字段的值,最终完成序列化操作
其大概的流程如以下调用栈
最后再通过流程图回顾一下整个序列化的流程:
0x03 总结
序列化的流程说起来简单也很简单,实际上就是几个write*
方法:writeFataException
、writeNull
、writeHandle
、writeClass
、writeProxyDesc
、writeNonProxyDesc
、writeString
、writeArray
、writeEnum
,加两个特殊的write*
方法:writeExternalData
、writeOrginaryObject
。
序列化的流程说起来也很复杂,除了各种判断检测分支,还有各种特性:如被transient
修饰的成员属性具有”不会序列化“的语义,序列化的时候会忽略、被static
修饰的成员属性隶属于类而非对象,所以它在序列化的时候同样会被忽略。
但总的来说,搞懂序列化的某个流程(走到最后的write*
)对于理解序列化机制是很有帮助的。
0x04 参考
https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html
https://blog.csdn.net/silentbalanceyh/article/details/8294269