本文主要关注的是二进制序列化后的二进制内容解读。通过解读这些看似枯燥的内容,可以让我们做到心中有底——为什么一端序列化出来的二进制流能在另外一端完整地复原?
1. 样例代码
代码如下, 省略不关心的部分
package objectStream;
public class Employee implements Serializable
{
private String name;
private double salary;
private Date hireDay;
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
// 略
}
public class Tester{
private static final String SAVED_PATH = "src/main/java/objectStream/employee.dat";
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 持久化到本地存储中
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(SAVED_PATH))) {
out.writeObject(obj);
}
}
2. 解读
以下是在notepad++下以hex模式打开的文件截图
接下来按照顺序逐一解读
- 魔法数
AC ED
(可在ObjectStreamConstants
接口中找到) - 序列化格式的版本号
00 05
(可在ObjectStreamConstants
接口中找到) - 接下来的
73 72
解读如下 (参见 协议文档)
73 代表接下来读取到的将是一个对象 (final static byte TC_OBJECT = (byte)0x73;
)
72 代表该对象是一个对类的描述 (final static byte TC_CLASSDESC = (byte)0x72;
) - 接下来的
00 15
, 指代该类描述信息的长度, 经过转换计算, 内容正好是类Employee
的完整命名objectStream.Employee
; - 然后是
ed 65 0f 78 f9 97 ff b6
,这八位是用来验证该类是否被修改过的验证码. 因为我们没有在实现Serializable
接口后, 添加serialVersionUID
字段, 所以JVM会自动帮助我们生成一个. - 接下来就是
02
, 该一个字节长度的标志信息代表了 序列化中标识类版本 ; 该数值也是可以在ObjectStreamConstants
接口中找到. (final static byte SC_SERIALIZABLE = 0x02;
) - 继续往下就是
00 03
, 这两个字节长度的标志信息指代的是 该类型中字段的个数. 如这里所见, 正好对应了Employee
中的三个字段. - 接着往下就是对这三个字段的逐一解读了,
- 如上所示, 以上标注出的是
double
类型的salary
字段的解读.
-
44
即D
; 正好对应的是double
-
00 06
代表该字段名称所占的长度 - 接下来的6字节长度的
73 61 6c 61 72 79
正好是 salary 字符串的16进制版本.
- 接下来的
4c 00 07 68 ...
解读如下
-
44
即L
, 所代表的是对象
, 正好和java.util.Date
匹配 -
00 07
依然是长度 - 接下来的7位 也就是 字段名 hireDay 字符的内容了.
- 接下来对 Date类型的字段解读如下: 即从
74 00 10 4c
开始 - Date类型的hireDay字段 : 类型 L (4c) , 字段名7位长度, 名称为hireDay, 字段类型为 74(字段类型以74开头), 字段类型 类名长度16, java.lang.String
- 最后就是name字段了: 以下就是 字段 name 类型
L (4c)
(String属于对象, 不属于基本类型) , 字段名4位长度, 名称为 name, 字段类型为 74, 字段类型 类名长度18,java.lang.String;
- 接下来就是 类型描述信息结束的标识了
- 接下来就是对象信息的描述了
- 首先是double类型的salary, 所以
78 70
之后的40 e8 6a 00 00 00 00 00
正是它的值. - 接下来是Date类型的 hireDay, 注意选中的部分, 前面四个字符
73 72 00 0e
正是 一个字节长度的 对象标识, 一个字节长度的类描述符标识, 两个字节长度的 长度标识
这里还要注意的一点是, 和我上面红线标出来的不同的是
- 红色标识出来的是 其解析出来, 内容是
java/util/Date
- 而选中部分解析出来, 其内容是
java.util.Date
- 最后是 name 实例字段 数据
3. 参考信息
- 流中的标志性字段含义
final static short STREAM_MAGIC = (short)0xaced;
final static short STREAM_VERSION = 5;
final static byte TC_NULL = (byte)0x70;
final static byte TC_REFERENCE = (byte)0x71;
final static byte TC_CLASSDESC = (byte)0x72;
final static byte TC_OBJECT = (byte)0x73;
final static byte TC_STRING = (byte)0x74;
final static byte TC_ARRAY = (byte)0x75;
final static byte TC_CLASS = (byte)0x76;
final static byte TC_BLOCKDATA = (byte)0x77;
final static byte TC_ENDBLOCKDATA = (byte)0x78;
final static byte TC_RESET = (byte)0x79;
final static byte TC_BLOCKDATALONG = (byte)0x7A;
final static byte TC_EXCEPTION = (byte)0x7B;
final static byte TC_LONGSTRING = (byte) 0x7C;
final static byte TC_PROXYCLASSDESC = (byte) 0x7D;
final static byte TC_ENUM = (byte) 0x7E;
final static int baseWireHandle = 0x7E0000;
- 数据字段描述符格式中类型编码