序列化原理(一):从源码理解Serializable

前言

就在昨天和同事聊天聊起了序列化,我们熟知并且使用最方便的就是Serializable。

那么为什么要序列化呢?
有些朋友会说:序列化主要是为了数据持久化。

我们都知道Serializable是一个空接口,不需要我们实现任何的方法,设置可以不设置serialVersionUID,那他存在的意义是什么呢,直接让所有的类都可以序列化不是更简单吗,带着这样的思考,我开始研究Serializable的相关的源码。

正文

首先我们看一下Serializable的注释,非常的长,我简单的概括为一下几点:

  1. 慎用Serializable
  2. Serializable接口仅仅是为了标识哪些类可以被序列化。
  3. 如果想要序列化一个类,必须要实现Serializable接口,包括他的属性。
  4. 如果父类实现了Serializable,那么他的子类也可以被序列化。
  5. 子类只可以序列化父类的可见属性,例如public,protected,或者其他情况,并且必须提供一个无参构造方法,否则会在运行时报错。
  6. serialVersionUID可以理解为版本号,如果发生了变化,会抛出InvalidClassException。
  7. 强烈建议手动设置serialVersionUID,兼容类发生的变化,如果没有手动设置serialVersionUID,会根据系统算法默认生成一个serialVersionUID。
  8. 系统生成的serialVersionUID会因为各种原因发生变化,例如类的属性或方法变化,SDK版本的变化等等,所以请注意处理异常。
  9. 强烈建议使用JSON,简洁性,可读性,效率都会有所提高。

通过注释,我们可以理解Serializable的作用是为了帮助我们标识哪些类的实例可以被序列化,并且提供了serialVersionUID作为版本号帮助我们兼容类的改变。

之前我们提到了一个问题:如果去掉Serializable,所有的类默认可以直接序列化不行吗?

我个人是这么理解的:

1、从设计模式的六大准则之一,单一职责原则。一个类做越少的事就越好管理,如果所有的类都可以直接序列化,当我们想知道程序中到底序列化了哪些类,就没有办法进行查找和筛选。
2、Java只有单继承,导致很多情况下不够灵活,接口的出现大大弥补的了这一空缺,通过接口进行类型区分是非常方便的做法。
3、最关键的注释已经写的很清楚了,使用Serializable进行序列化是一个非常谨慎的行为(各种异常),限制类的序列化是为了保证程序的安全运行。

经过思考后,我们对Serializable的起源有了新的升华,接下来我们看看Serializable的具体使用源码。

ObjectOutputStream

ObjectOutputStream通过IO流的方式把对象写入到指定的位置,例如以下代码:

// 写入Student对象到指定路径的文件中
ObjectOutputStream(FileOutputStream(path))
                .apply {
                    writeObject(Student("1", "zhangsan"))
                    flush()
                    close()
                }

我们先看看文件中到底写了什么东西:


在这里插入图片描述

这是从内存读出出来的Student对象相关的信息,我们可以看到Class类名,属性类型和名称以及对应的值。

接下来我们看看ObjectOutputStream是怎么具体写入文件的,因为源码非常的多,我们只截取部分关键的代码。写入对象到IO流肯定要看writeObject方法,根据代码定位最终我们会在writeObject0()方法中看到第一个关键代码:

//  你会看到通过对象的Class创建了ObjectStreamClass
// ObjectStreamClass会保存类相关的信息,包括获取serialVersionUID
// 之后也会调用ObjectStreamClass的write方法进行写入
ObjectStreamClass desc = ObjectStreamClass.lookup(cl, true);
...
// 此处代码判断对象的类型是否可以被序列化
// Class
if (obj instanceof Class) {
         writeClass((Class) obj, unshared);
} 
// ObjectStreamClass
else if (obj instanceof ObjectStreamClass) {
         writeClassDesc((ObjectStreamClass) obj, unshared);
} 
// String
else 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);
} 
// Serializable实现序列化接口
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());
         }
}

以刚才Student为例,他的类型是Object(而不是Class),如果我们没有实现Serializable接口,这里就会抛出异常。所以这里会执行writeOrdinaryObject方法,在writeOrdinaryObject方法中,就会调用ObjectStreamClass的写入方法:

private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        ...
        try {
            desc.checkSerialize();
            // 开始标记位
            bout.writeByte(TC_OBJECT);
            // 调用ObjectStreamClass的写入方法
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);
            // 判断是否实现了Externalizable接口,我们没有实现,所以不会走这里
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                // 写入序列化数据
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

从上面的方法里,已经包含了所有的写入流程:

1、写入开始标记位
2、开始写入ObjectStreamClass中的内容
3、写入序列化数据

我们看一下第二步的所有核心内容:

// 首先会执行方法writeNonProxyDesc方法
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
        throws IOException
    {
        // 描述符,表示这个一个类的描述
        bout.writeByte(TC_CLASSDESC);
        ...
        // 虽然这里有一个判断,实际上执行的还是 desc.writeNonProxy(this);
        // 应该是一个等待兼容修改的方法
        if (protocol == PROTOCOL_VERSION_1) {
            // do not invoke class descriptor write hook with old protocol
            desc.writeNonProxy(this);
        } else {
            writeClassDescriptor(desc);
        }
        ...
        // 写入结束block data
        bout.writeByte(TC_ENDBLOCKDATA);
        // 
        writeClassDesc(desc.getSuperDesc(), false);
    }

    // 写入ObjectOutputStream,此处用到了serialVersionUID
    void writeNonProxy(ObjectOutputStream out) throws IOException {
        // 写入类的名称
        out.writeUTF(name);
        // 写入SerialVersionUID
        out.writeLong(getSerialVersionUID());
        // 写入其他标记,这里就直接省略了
        ...
        // 开始写入属性
        out.writeShort(fields.length);
        for (int i = 0; i < fields.length; i++) {
            ObjectStreamField f = fields[i];
            out.writeByte(f.getTypeCode());
            out.writeUTF(f.getName());
            if (!f.isPrimitive()) {
                out.writeTypeString(f.getTypeString());
            }
        }
    }

如果我们没有设置serialVersionUID怎么办,系统会为我们自动生成serialVersionUID,具体请查看ObjectStreamClass.computeDefaultSUID(),算法中与类的名称、属性、方法都有关系,所以我们随意的修改都可能导致计算出不同的serialVersionUID。

最后我们回到之前的第三步:

// 写入序列化方法
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;
            // 如果我们在类中自己定义了writeObject,会被调用,并进入到下面的代码
            if (slotDesc.hasWriteObjectMethod()) {
                ...
            } 
            // 如果没有自定义writeObject,会进入else
            else {
                // 此方法中会把所有的属性都写入进去,属性的类型也会重新调用最开始的writeObject放方法
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

到这里我们的序列化写入就结束了,我们以示例代码序列化Student到文件,回顾一下整个流程:


序列化流程图

ObjectInputStream

弄懂了输出流,我们再分析一下输入流,之前我们已经弄懂了序列化的流程,所以我们可以推断,反序列化的过程应该是相反的。

// 读出文件出的对象
 ObjectInputStream(FileInputStream(path))
                    .apply {
                        text.text = readObject().toString()
                        close()
                    }

首先看一下readObject()方法,首先我们在readObject0()方法中看到了反序列化的类型分支:

switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }

我们写入的类型是TC_OBJECT,最终会跟踪到readOrdinaryObject()方法:

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }
        // 读取序列化中的ObjectStreamClass
        ObjectStreamClass desc = readClassDesc(false);
        ...
        // 通过反射,创建对象
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            ...
        }
        // 判断是否实现了Externalizable接口
        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } 
        // 我们并没有实现Externalizable接口,会进入到else判断
        else {
            // 读取序列化的数据
            readSerialData(obj, desc);
        }
        
        // 判断是否自定义了readObject方法,如果有会调用,如果没有直接返回obj
        // 此处省略
       ...
        return obj;
    }

上面方法是反序列化的全部流程,我重点看一下读取ObjectStreamClass的readClassDesc()方法和读取序列化数据的readSerialData()方法。

 private ObjectStreamClass readNonProxyDesc(boolean unshared)
        throws IOException
    {
       ...
        // 创建ObjectStreamClass
        ObjectStreamClass desc = new ObjectStreamClass();
        // 读取ObjectStreamClass
        ObjectStreamClass readDesc = null;
        try {
            readDesc = readClassDescriptor();
        } catch (ClassNotFoundException ex) {
            throw (IOException) new InvalidClassException(
                "failed to read class descriptor").initCause(ex);
        }
        // 各种异常处理
        ...
        //  通过读取到ObjectStreamClass初始化desc
        desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
        ...
        // 返回desc
        return desc;
    }

读取ObjectStreamClass最终会定位到readNonProxy()方法:

void readNonProxy(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        // 类名
        name = in.readUTF();
        // suid
        suid = Long.valueOf(in.readLong());
        // 各种flag
        ...
        // 循环保存属性到数组中
        for (int i = 0; i < numFields; i++) {
            char tcode = (char) in.readByte();
            String fname = in.readUTF();
            String signature = ((tcode == 'L') || (tcode == '[')) ?
                in.readTypeString() : new String(new char[] { tcode });
            try {
                fields[i] = new ObjectStreamField(fname, signature, false);
            } catch (RuntimeException e) {
               ...
            }
        }
        computeFieldOffsets();
    }

现在已经把所有的属性保存到数组中了,接下来调用initNonProxy()方法,这个方法中主要做了很多的判断,用现有的Class和读取到的Class进行对比,例如判断serialVersionUID是否一致,否则会抛出InvalidClassException异常。

if (model.serializable == osc.serializable && !cl.isArray() && suid != osc.getSerialVersionUID()) {
       throw new InvalidClassException(osc.name,
                 "local class incompatible: " +
                  "stream classdesc serialVersionUID = " + suid +
                  ", local class serialVersionUID = " +
                   osc.getSerialVersionUID());
}

经过读取ObjectStreamClass,我们已经得到了初始化对象的所有类型信息,接下来是如何把对对象的属性赋值。

private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        // 遍历继承列表,ClassDataSlot中保存的每个层级父类的信息
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            // 判断这个属性是否有值
            if (slots[i].hasData) {
                // 忽略这个属性
                if (obj == null || handles.lookupException(passHandle) != null) {
                    defaultReadFields(null, slotDesc); // skip field values
                } 
                // 是否自定义了readObject方法,此处忽略
                else if (slotDesc.hasReadObjectMethod()) {
                    ...
                }
                // 默认会进入到这里
                 else {
                    defaultReadFields(obj, slotDesc);
                }
                ...
            } 
            // 判断在无值的情况下,是否自定了readObjectNoData方法
            // 此处忽略
            else {
               ...
            }
        }
    }

// 对属性进行赋值操作
private void defaultReadFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {   
        // 异常检查等操作
        ...
        // 开始遍历类的属性列表
        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        for (int i = 0; i < objVals.length; i++) {
            ObjectStreamField f = fields[numPrimFields + i];
            // 注意此处会回到最开始的反序列化位置,完成属性值的反序列化
            objVals[i] = readObject0(f.isUnshared());
            if (f.getField() != null) {
                handles.markDependency(objHandle, passHandle);
            }
        }
        // 通过反射进行赋值
        if (obj != null) {
            desc.setObjFieldValues(obj, objVals);
        }
        passHandle = objHandle;
    }

到这里反序列化操作到此结束,我们整理一张流程图,梳理一下反序列化的整个流程:


反序列化流程图

总结

今天我们一起讨论了序列化接口Serializable的起源和设计理念,然后通过分析源码了解了Serializable的在序列化和反序列化中的作用。我们也看到了Serializable接口的不足:

  1. 太多的循环和递归遍历,读写效率确实有隐患。
  2. 不能有选择的对属性序列化和反序列,灵活性差。
  3. 兼容性差,不指定serialVersionUID很容易出现反序列的崩溃问题。

正如Serializable开头所说的:慎用Serializable,推荐使用JSON。

补充

评论里叶落清秋提到了@Transient注解,他可以在序列化中禁止某些属性被序列化,我重新查看了一遍源码,之前我只重点看了write和read的过程,所以并没有看到这个注解,重新整理发现,在ObjectStreamClass的构造函数中就已经完成了@Transient的功能,代码如下:

// 私有构造函数
if (serializable) {
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
             public Void run() {
                   ...
                   // 得到可以序列化的属性的集合
                   fields = getSerialFields(cl);
                  ...     
      }
}

private static ObjectStreamField[] getSerialFields(Class<?> cl) throws InvalidClassException
{
        ObjectStreamField[] fields;
        ...
        // 进入到此方法中
        fields = getDefaultSerialFields(cl);
        ...
        return fields;
}

private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList<ObjectStreamField> list = new ArrayList<>();
        int mask = Modifier.STATIC | Modifier.TRANSIENT;

        for (int i = 0; i < clFields.length; i++) {
            // 遍历所有的属性,过滤掉static和transient
            if ((clFields[i].getModifiers() & mask) == 0) {
                list.add(new ObjectStreamField(clFields[i], false, true));
            }
        }
        int size = list.size();
        return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
    }

经过这次整理,我们知道了Serializable是不会序列化静态属性和@Transient注解的属性,感谢叶落清秋的评论指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335