序列化流程分析总结

本文最后更新于 2021.06.13,总计 19323 字 ,阅读本文大概需要 23 ~ 100 分钟
本文已超过 40天 没有更新。如果文章内容或图片资源失效,请留言反馈,我会及时处理,谢谢!

目录

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变量中写入魔术头以及版本号,如下图:

1.png

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;

2.png

然后才是进入了关键方法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

然后会进入以下代码块进行判断:

3.png

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

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

4.png

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

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

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

5.png

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

然后继续跟进:

6.jpg

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

当以上条件都不满足的时候(不进入if),开始检查是否开启了替换对象。

7.png

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

但实际上enableReplace的值通常为false

8.png

我们并不会进入这一代码段。

然后进入二次检查代码段:

9.png

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

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

10.png

如果传入对象为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方法。

11.png

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

12.png

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

13.png

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

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

来看看writeNonProxyDesc

14.png

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

15.png

会调用writeNonProxy方法,跟进:

16.png

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

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

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

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

17.png

这里的debug就走完了:

18.png

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

19.png

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

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

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

继续来看writeOrdinaryObject下面的代码

20.png

如果使用的模式是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对象写入数据信息,比如字段值和相关引用等,写入的时候会从顶级父类从上至下递归执行;看看这个方法的详细过程:

21.png

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

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

22.png

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

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

23.png

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

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

24.png

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

25.png

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

26.png

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

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

27.png

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

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

28.png

最后再通过流程图回顾一下整个序列化的流程:

序列化流程.jpg

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

「感谢老板送来的软糖/蛋糕/布丁/牛奶/冰阔乐!」

panda

(๑>ڡ<)☆谢谢老板~

使用微信扫描二维码打赏

版权属于:

panda | 热爱安全的理想少年

本文链接:

https://cnpanda.net/sec/893.html(转载时请注明本文出处及文章链接)

添加新评论

暂无评论