深入学习 Java 序列化

时间:2019-02-17
本文章向大家介绍深入学习 Java 序列化,主要包括深入学习 Java 序列化使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

对于Java的序列化,一直只知道只需要实现Serializbale这个接口就可以了,具体内部实现一直不是很了解,正好这次在重复造RPC的轮子的时候涉及到序列化问题,就抽时间看了下 Java序列化的底层实现,这篇文章算是这次的学习小结吧。

第一部分:啥?

Java序列化是指把Java对象保存为二进制字节码的过程,Java反序列化是指把二进制码重新转换成Java对象的过程。

那么为什么需要序列化呢?

第一种情况是:一般情况下Java对象的声明周期都比Java虚拟机的要短,实际应用中我们希望在JVM停止运行之后能够持久化指定的对象,这时候就需要把对象进行序列化之后保存。

第二种情况是:需要把Java对象通过网络进行传输的时候。因为数据只能够以二进制的形式在网络中进行传输,因此当把对象通过网络发送出去之前需要先序列化成二进制数据,在接收端读到二进制数据之后反序列化成Java对象。

第二部分:谁?

本部分以序列化到文件为例讲解Java序列化的基本用法。

package com.beautyboss.slogen;
 
import java.io.*;
 
/**
* Author : Slogen
* AddTime : 17/4/30
*/
public class SerializableTest {
	public static void main(String[] args) throws Exception {
		FileOutputStream fos = new FileOutputStream("temp.out");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		TestObject testObject = new TestObject();
		oos.writeObject(testObject);
		oos.flush();
		oos.close();
	 
		FileInputStream fis = new FileInputStream("temp.out");
		ObjectInputStream ois = new ObjectInputStream(fis);
		TestObject deTest = (TestObject) ois.readObject();
		System.out.println(deTest.testValue);
		System.out.println(deTest.parentValue);
		System.out.println(deTest.innerObject.innerValue);
	}
}
 
class Parent implements Serializable {
 
	private static final long serialVersionUID = -4963266899668807475L;
	 
	public int parentValue = 100;
}
 
class InnerObject implements Serializable {
 
	private static final long serialVersionUID = 5704957411985783570L;
	 
	public int innerValue = 200;
}
 
class TestObject extends Parent implements Serializable {
 
	private static final long serialVersionUID = -3186721026267206914L;
	 
	public int testValue = 300;
	 
	public InnerObject innerObject = new InnerObject();
}

程序执行完用vim打开temp.out文件,可以看到

0000000: aced 0005 7372 0020 636f 6d2e 6265 6175 ....sr. com.beau
0000010: 7479 626f 7373 2e73 6c6f 6765 6e2e 5465 tyboss.slogen.Te
0000020: 7374 4f62 6a65 6374 d3c6 7e1c 4f13 2afe stObject..~.O.*.
0000030: 0200 0249 0009 7465 7374 5661 6c75 654c ...I..testValueL
0000040: 000b 696e 6e65 724f 626a 6563 7474 0023 ..innerObjectt.#
0000050: 4c63 6f6d 2f62 6561 7574 7962 6f73 732f Lcom/beautyboss/
0000060: 736c 6f67 656e 2f49 6e6e 6572 4f62 6a65 slogen/InnerObje
0000070: 6374 3b78 7200 1c63 6f6d 2e62 6561 7574 ct;xr..com.beaut
0000080: 7962 6f73 732e 736c 6f67 656e 2e50 6172 yboss.slogen.Par
0000090: 656e 74bb 1eef 0d1f c950 cd02 0001 4900 ent......P....I.
00000a0: 0b70 6172 656e 7456 616c 7565 7870 0000 .parentValuexp..
00000b0: 0064 0000 012c 7372 0021 636f 6d2e 6265 .d...,sr.!com.be
00000c0: 6175 7479 626f 7373 2e73 6c6f 6765 6e2e autyboss.slogen.
00000d0: 496e 6e65 724f 626a 6563 744f 2c14 8a40 InnerObjectO,..@
00000e0: 24fb 1202 0001 4900 0a69 6e6e 6572 5661 $.....I..innerVa
00000f0: 6c75 6578 7000 0000 c8 luexp....

第三部分:为什么?

调用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()之后究竟做了什么?temp.out文件中的二进制分别代表什么意思?

别急,且听我娓娓道来。

1. ObjectStreamClass类

官方文档对这个类的介绍如下

Serialization’s descriptor for classes. It contains the name and serialVersionUID of the class. The ObjectStreamClass for a specific class loaded in this Java VM can be found/created using the lookup method.

可以看到ObjectStreamClass这个是类的序列化描述符,这个类可以描述需要被序列化的类的元数据,包括被序列化的类的名字以及序列号。可以通过lookup()方法来查找/创建在这个JVM中加载的特定的ObjectStreamClass对象。

2. 序列化:writeObject()

在调用wroteObject()进行序列化之前会先调用ObjectOutputStream的构造函数生成一个ObjectOutputStream对象,构造函数如下:

public ObjectOutputStream(OutputStream out) throws IOException {
    verifySubclass();
    // bout表示底层的字节数据容器
    bout = new BlockDataOutputStream(out);
    handles = new HandleTable(10, (float) 3.00);
    subs = new ReplaceTable(10, (float) 3.00);
    enableOverride = false;
    writeStreamHeader(); // 写入文件头
    bout.setBlockDataMode(true); // flush数据
    if (extendedDebugInfo) {
        debugInfoStack = new DebugTraceInfoStack();
    } else {
        debugInfoStack = null;
    }
}

构造函数中首先会把bout对绑定到底层的字节数据容器,接着会调用writeStreamHeader()方法,该方法实现如下:

protected void writeStreamHeader() throws IOException {
    bout.writeShort(STREAM_MAGIC);
    bout.writeShort(STREAM_VERSION);
}

在writeStreamHeader()方法中首先会往底层字节容器中写入表示序列化的Magic Number以及版本号,定义为

/**
 * Magic number that is written to the stream header.
 */
final static short STREAM_MAGIC = (short)0xaced;
 
/**
 * Version number that is written to the stream header.
 */
final static short STREAM_VERSION = 5;

接下来会调用writeObject()方法进行序列化,实现如下:

public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        // 调用writeObject0()方法序列化
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}

正常情况下会调用writeObject0()进行序列化操作,该方法实现如下:

private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    // 一些省略代码
    try {
        // 一些省略代码
        Object orig = obj;
        // 获取要序列化的对象的Class对象
        Class cl = obj.getClass();
        ObjectStreamClass desc;
        for (;;) {
            Class repCl;
            // 创建描述cl的ObjectStreamClass对象
            desc = ObjectStreamClass.lookup(cl, true);
            // 其他省略代码
        }
        // 一些省略代码
        // 根据实际的类型进行不同的写入操作
        // 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) {
            // 被序列化对象实现了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);
    }
}

从代码里面可以看到,程序会

  1. 生成一个描述被序列化对象的类的类元信息的ObjectStreamClass对象。
  2. 根据传入的需要序列化的对象的实际类型进行不同的序列化操作。从代码里面可以很明显的看到,对于String类型、数组类型和Enum可以直接进行序列化。如果被序列化对象实现了Serializable对象,则会调用writeOrdinaryObject()方法进行序列化。

这里可以解释一个问题:Serializbale接口是个空的接口,并没有定义任何方法,为什么需要序列化的接口只要实现Serializbale接口就能够进行序列化。

答案是:Serializable接口这是一个标识,告诉程序所有实现了”我”的对象都需要进行序列化。

因此,序列化过程接下来会执行到writeOrdinaryObject()这个方法中,该方法实现如下:

private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared) throws IOException
{
    if (extendedDebugInfo) {
        debugInfoStack.push(
            (depth == 1 ? "root " : "") + "object (class \"" +
            obj.getClass().getName() + "\", " + obj.toString() + ")");
    }
    try {
        desc.checkSerialize();
 
        bout.writeByte(TC_OBJECT); // 写入Object标志位
        writeClassDesc(desc, false); // 写入类元数据
        handles.assign(unshared ? null : obj);
        if (desc.isExternalizable() && !desc.isProxy()) {
            writeExternalData((Externalizable) obj);
        } else {
            writeSerialData(obj, desc); // 写入被序列化的对象的实例数据
        }
    } finally {
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }
}

在这个方法中首先会往底层字节容器中写入TC_OBJECT,表示这是一个新的Object

/**
 * new Object.
 */
final static byte TC_OBJECT = (byte)0x73;

接下来会调用writeClassDesc()方法写入被序列化对象的类的类元数据,writeClassDesc()方法实现如下:

private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
    throws IOException
{
    int handle;
    if (desc == null) {
        // 如果desc为null
        writeNull();
    } else if (!unshared && (handle = handles.lookup(desc)) != -1) {
        writeHandle(handle);
    } else if (desc.isProxy()) {
        writeProxyDesc(desc, unshared);
    } else {
        writeNonProxyDesc(desc, unshared);
    }
}

在这个方法中会先判断传入的desc是否为null,如果为null则调用writeNull()方法

private void writeNull() throws IOException {
    // TC_NULL =         (byte)0x70;
    // 表示对一个Object引用的描述的结束
    bout.writeByte(TC_NULL);
}

如果不为null,则一般情况下接下来会调用writeNonProxyDesc()方法,该方法实现如下:

private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
    throws IOException{
    // TC_CLASSDESC =    (byte)0x72;
    // 表示一个新的Class描述符
    bout.writeByte(TC_CLASSDESC);
    handles.assign(unshared ? null : desc);
 
    if (protocol == PROTOCOL_VERSION_1) {
        // do not invoke class descriptor write hook with old protocol
        desc.writeNonProxy(this);
    } else {
        writeClassDescriptor(desc);
    }
 
    Class cl = desc.forClass();
    bout.setBlockDataMode(true);
    if (cl != null && isCustomSubclass()) {
        ReflectUtil.checkPackageAccess(cl);
    }
    annotateClass(cl);
    bout.setBlockDataMode(false);
    bout.writeByte(TC_ENDBLOCKDATA);
 
    writeClassDesc(desc.getSuperDesc(), false);
}

在这个方法中首先会写入一个字节的TC_CLASSDESC,这个字节表示接下来的数据是一个新的Class描述符,接着会调用writeNonProxy()方法写入实际的类元信息,writeNonProxy()实现如下:

void writeNonProxy(ObjectOutputStream out) throws IOException {
    out.writeUTF(name); // 写入类的名字
    out.writeLong(getSerialVersionUID()); // 写入类的序列号
 
    byte flags = 0;
    // 获取类的标识
    if (externalizable) {
        flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
        int protocol = out.getProtocolVersion();
        if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
            flags |= ObjectStreamConstants.SC_BLOCK_DATA;
        }
    } else if (serializable) {
        flags |= ObjectStreamConstants.SC_SERIALIZABLE;
    }
    if (hasWriteObjectData) {
        flags |= ObjectStreamConstants.SC_WRITE_METHOD;
    }
    if (isEnum) {
        flags |= ObjectStreamConstants.SC_ENUM;
    }
    out.writeByte(flags); // 写入类的flag
 
    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()) {
            // 如果不是原始类型,即是对象或者Interface
            // 则会写入表示对象或者类的类型字符串
            out.writeTypeString(f.getTypeString());
        }
    }
}

writeNonProxy()方法中会按照以下几个过程来写入数据:

  1. 调用writeUTF()方法写入对象所属类的名字,对于本例中name = com.beautyboss.slogen.TestObject.对于writeUTF()这个方法,在写入实际的数据之前会先写入name的字节数,代码如下:
void writeUTF(String s, long utflen) throws IOException {
        if (utflen > 0xFFFFL) {
            throw new UTFDataFormatException();
        }
        // 写入两个字节的s的长度
        writeShort((int) utflen);
        if (utflen == (long) s.length()) {
            writeBytes(s);
        } else {
            writeUTFBody(s);
        }
    }
  1. 接下来会调用writeLong()方法写入类的序列号UID,UID是通过getSerialVersionUID()方法来获取。

  2. 接着会判断被序列化的对象所属类的flag,并写入底层字节容器中(占用两个字节)。类的flag分为以下几类:

  • final static byte SC_EXTERNALIZABLE = 0×04;表示该类为Externalizable类,即实现了Externalizable接口。
  • final static byte SC_SERIALIZABLE = 0×02;表示该类实现了Serializable接口。
  • final static byte SC_WRITE_METHOD = 0×01;表示该类实现了Serializable接口且自定义了writeObject()方法。
  • final static byte SC_ENUM = 0×10;表示该类是个Enum类型。

对于本例中flag = 0×02表示只是Serializable类型。

  1. 第四步会依次写入被序列化对象的字段的元数据。

<1> 首先会写入被序列化对象的字段的个数,占用两个字节。本例中为2,因为TestObject类中只有两个字段,一个是int类型的testValue,一个是InnerObject类型的innerValue。

<2> 依次写入每个字段的元数据。每个单独的字段由ObjectStreamField类来表示。

  1. 写入字段的类型码,占一个字节。 类型码的映射关系如下:

  1. 调用writeUTF()方法写入每个字段的名字。注意,writeUTF()方法会先写入名字占用的字节数。

  2. 如果被写入的字段不是基本类型,则会接着调用writeTypeString()方法写入代表对象或者类的类型字符串,该方法需要一个参数,表示对应的类或者接口的字符串,最终调用的还是writeString()方法,实现如下

private void writeString(String str, boolean unshared) throws IOException {
    handles.assign(unshared ? null : str);
    long utflen = bout.getUTFLength(str);
    if (utflen <= 0xFFF