Java序列化的机制原理(一)
引言
序列化是Java开发中不可或缺的一部分,对于大多数开发人员也是非常熟悉,使用频繁。在项目中的使用场景也很多,例如:数据持久化保持、项目内不同服务间通信、Redis缓存等。所以需要开发人员掌握这方面的知识,本文就实现Serializable接口,来进行解析序列化后的数据。
我们在将对象进行序列化后,会发现序列化后的对象文本跟类文本文件并不一样大,这是为什么呢?这是因为序列化后的文本存储方式不一样导致的。
正文
一、序列化算法一般步骤
-
将对象实例相关的类元数据输出。
-
递归地输出类的超类描述直到不再有超类。
-
类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
-
从上至下递归输出实例的数据
二、名词解释
直接类:开始输出直接被序列化的类(以下简称“直接类”,是相对于“直接类”的父类,和“直接类”中所包含的对象型属性的类而言)的描述
现在通过用二进制查看序列化后代码进行分析序列化后的数据。以下相关解释可以通过java.io.ObjectStreamConstants找到相关解释。
三、序列化后数据
image四、相关数据解释
首先可以看到代码全部都是十六进制数据,接下来就一一解读相关意义(为什么依两个十六进制的数字为分隔,这个是因为两个十六进制最大刚好代表一个字节,方便解释)
五、数据结构解析(子类及父类)
AC ED:代表使用了序列化协议。
java.io.ObjectStreamConstants.STREAM_MAGIC 对应这个常量
00 05:代表序列化协议版本。
java.io.ObjectStreamConstants.STREAM_VERSION 对应这个常量
73:代表这是一个新对象。
java.io.ObjectStreamConstants.TC_OBJECT 对应这个常量
72:代表一个新类的开始。
java.io.ObjectStreamConstants.TC_CLASSDESC 对应这个常量
00 15:代表类名的名称长度(包含全路径)。 0015转化为十进制为21,所以往后21个字节都是类的长度 (com.mfh.ser.Developer)
63 6F 6D 2E 6D 66 68 2E 73 65 72 2E 44 65 76 65 6C 6F 70 65 72
数过21个数据后,继续解析。
4A 00 23 DC 49 F3 D8 C6:代表序列化ID,如果没有指定就会随机生成一个8BYTE的ID(这个对序列化的数据非常重要,后续篇幅会讲)。
02:代表该对象支持序列化,该字节的 8 位分表代表不同的含义。
SC_EXTERNALIZABLE 0x04: 该类实现了java.io.Externalizable接口。
SC_BLOCK_DATA 0x08: Externalizable接口的writeExternal方法写入。
SC_SERIALIZABLE 0x02: 该类实现了java.io.Serializable接口。
SC_WRITE_METHOD 0x01: 该序列化类实现了writeObject方法。
SC_ENUM 0x10: 该类是枚举(enum)类型。
该标记号通过上述信息进行或运算(|)而获得。
00 02:代表该类包含的域的个数(也就是属性)在本类中也就两个属性code和men。
4c:代表其中第一个域的类型,这个域是一个引用类型的属性域,使用字符 L表示。
00 04:代表其中第一个域名称的长度,code=(63 6F 64 65)
74:代表NEW STINRG的开始
java.io.ObjectStreamConstants.TC_STRING 对应这个常量
00 12:代表第一个域类型的JVM标准对象签名的字节长度的开始。
在本例中,第一个域是String对象,这个字符串对应的字节是:4C 74 65 73 74 2F 73 65 72 69 61 6C 2F 43 6F 6E 74 61 69 6E 3B,占用18个字节。它的JVM 标准对象签名就是:Ljava.lang.String;对象签名包括前面的大写L和最后的分号;
4c:代表其中第二个域的类型,这个域是一个引用类型的属性域。
00 03:代表其中第二个域名称的长度,men=(6D 65 6E)
74:代表NEW STINRG的开始
00 11:代表第二个域类型的JVM标准对象签名的字节长度的开始。
在本例中,第二个域是Men对象,这个字符串对应的字节是:4C 63 6F 6D 2F 6D 66 68 2F 73 65 72 2F 4D 65 6E 3B,占用17个字节。它的 JVM 标准对象签名就是:Lcom/mfh/ser/Men;对象签名包括前面的大写L和最后的分号;
78:代表对象数据块结束的标志,java.io.ObjectStreamConstants.TC_ENDBLOCKDATA对应这个常量,
72:代表一个新类的开始。
00 12:同上,代表类名的名称长度,(com.mfh.ser.Person)
63 6F 6D 2E 6D 66 68 2E 73 65 72 2E 50 65 72 73 6F 6E
CC E8 3B F2 EB 4C 4A 28:同上,序列化后Id。
02:同上,代表该对象支持序列化。
00 03:同上,代表该类包含的域的个数。
49:同上,域类型,转换成十进制数字是:73,ascii码对应的字符是大写字母 I,所以对应int类型
00 03:同上,域名长度,age:61 67 65
49:同上,域类型
00 06:同上,域名长度,gender:67 65 6E 64 65 72
4C:同上,域类型
00 04:同上,域名长度 name:6E 61 6D 65
71:代表TC_REFRENCE引用java.io.ObjectStreamConstants.TC_REFRENCE对应这个常量。
00 7E 00 01:代表引用地址,上面已经出现过String类型,所以引用地址指向了他
78:代表对象数据块结束的标志
70:代表对象没有父类了
六、对对象属性的值进行解析
00 00 00 16:四个字节,所以代表int类型,转换成十进制为22,是age的值
00 00 00 01:同上,转换成十进制为1,是gender的值
74:同上,代表new string。
00 03:代表数据长度,代表字符为Jon:4A 6F 6E
,是name的值
74:同上,代表new string。
00 03:代表数据长度,代表字符为bug:62 75 67
,是code的值
73:同上,代表这是一个新对象。
72:同上,代表一个新类的开始。
00 0F:代表类名长度,代表字符为com.mfh.ser.Men:63 6F 6D 2E 6D 66 68 2E 73 65 72 2E 4D 65 6E
。
D3 79 C3 E8 16 2D 67 5B:同上,序列化id。
02:同上,代表该对象支持序列化。
00 01:同上,代表该类包含的域的个数。
5A:代表域的类型,转换成十进制数字是:90,ascii码对应的字符是大写字母 Z,所以对应boolean类型
00 08:代表域名长度,代表字符为superman73 75 70 65 72 6D 61 6E
78:同上,代表对象数据块结束的标志。
71:同上,代表TC_REFRENCE引用。
00 7E 00 03:同上,代表引用地址,其实就是父类person的引用地址。
70:代表对象没有父类了
01:代表men对象中superman的值,01转换为boolean值为true
总结
至此序列化后的数据全部解析完毕,由此我们可以看出序列化后的数据并不是我们平常看的代码那样,而是有自己的一个语法规范。了解这序列化后每一个字节的意义,同时也代表又被修改的风险,这个会在以后篇章会讲解到。
通篇看下来,并没有多少比较抽象或难以理解的东西,只不过给了一些数字赋予了特殊意义,只是语法规范的问题,就这个语法规范问题,会在下一篇文章中具体解释这个特殊字符意义和序列化基本结构。
<a href="https://gitee.com/mafuhao/serial.git">文中所用的到的代码,均在此项目中</a>