Java序列化 - 二进制格式详解

本文主要关注的是二进制序列化后的二进制内容解读。通过解读这些看似枯燥的内容,可以让我们做到心中有底——为什么一端序列化出来的二进制流能在另外一端完整地复原?

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模式打开的文件截图
Java序列化 - 二进制格式详解_第1张图片

接下来按照顺序逐一解读

  1. 魔法数 AC ED (可在ObjectStreamConstants接口中找到)

  2. 序列化格式的版本号 00 05 (可在ObjectStreamConstants接口中找到)

  3. 接下来的 73 72 解读如下 (参见 协议文档)
    73 代表接下来读取到的将是一个对象 (final static byte TC_OBJECT = (byte)0x73;)
    72 代表该对象是一个对类的描述 (final static byte TC_CLASSDESC = (byte)0x72;)

  4. 接下来的 00 15, 指代该类描述信息的长度, 经过转换计算, 内容正好是类Employee的完整命名objectStream.Employee;

  5. 然后是ed 65 0f 78 f9 97 ff b6,这八位是用来验证该类是否被修改过的验证码. 因为我们没有在实现Serializable接口后, 添加serialVersionUID 字段, 所以JVM会自动帮助我们生成一个.

  6. 接下来就是 02, 该一个字节长度的标志信息代表了 序列化中标识类版本 ; 该数值也是可以在ObjectStreamConstants接口中找到. (final static byte SC_SERIALIZABLE = 0x02;)

  7. 继续往下就是 00 03 , 这两个字节长度的标志信息指代的是 该类型中字段的个数. 如这里所见, 正好对应了 Employee 中的三个字段.

  8. 接着往下就是对这三个字段的逐一解读了,

    Java序列化 - 二进制格式详解_第2张图片

    1. 如上所示, 以上标注出的是 double 类型的 salary 字段的解读.
      1. 44D ; 正好对应的是 double
      2. 00 06 代表该字段名称所占的长度
      3. 接下来的6字节长度的73 61 6c 61 72 79 正好是 salary 字符串的16进制版本.
    2. 接下来的 4c 00 07 68 ... 解读如下
      1. 44L, 所代表的是 对象 , 正好和 java.util.Date 匹配
      2. 00 07 依然是长度
      3. 接下来的7位 也就是 字段名 hireDay 字符的内容了.
      4. 接下来对 Date类型的字段解读如下: 即从 74 00 10 4c 开始
        Java序列化 - 二进制格式详解_第3张图片
      5. Date类型的hireDay字段 : 类型 L (4c) , 字段名7位长度, 名称为hireDay, 字段类型为 74(字段类型以74开头), 字段类型 类名长度16, java.lang.String
        Java序列化 - 二进制格式详解_第4张图片
      6. 最后就是name字段了: 以下就是 字段 name 类型 L (4c)(String属于对象, 不属于基本类型) , 字段名4位长度, 名称为 name, 字段类型为 74, 字段类型 类名长度18, java.lang.String;
        Java序列化 - 二进制格式详解_第5张图片
  9. 接下来就是 类型描述信息结束的标识了

    Java序列化 - 二进制格式详解_第6张图片

  10. 接下来就是对象信息的描述了

    1. 首先是double类型的salary, 所以 78 70之后的 40 e8 6a 00 00 00 00 00正是它的值.

    2. 接下来是Date类型的 hireDay, 注意选中的部分, 前面四个字符73 72 00 0e正是 一个字节长度的 对象标识, 一个字节长度的类描述符标识, 两个字节长度的 长度标识
      Java序列化 - 二进制格式详解_第7张图片
      这里还要注意的一点是, 和我上面红线标出来的不同的是

      1. 红色标识出来的是 其解析出来, 内容是 java/util/Date
      2. 而选中部分解析出来, 其内容是 java.util.Date
    3. 最后是 name 实例字段 数据
      Java序列化 - 二进制格式详解_第8张图片

3. 参考信息

  1. 流中的标志性字段含义
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;
  1. 数据字段描述符格式中类型编码
    Java序列化 - 二进制格式详解_第9张图片

4. Links

  1. 协议文档
  2. 《Java核心技术 卷II 高级特性 第9版》 P45 - P61 (重点关注这个)
  3. 《Effective Java》第二版 P255 - P277
  4. https://www.cnblogs.com/zhukunrong/p/4868856.html
  5. 深入学习 Java 序列化

你可能感兴趣的:(Java)