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模式打开的文件截图
序列化之后的对象

接下来按照顺序逐一解读

  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. 接着往下就是对这三个字段的逐一解读了,

    数据字段

    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 开始
        Date类型的hireDay
      5. Date类型的hireDay字段 : 类型 L (4c) , 字段名7位长度, 名称为hireDay, 字段类型为 74(字段类型以74开头), 字段类型 类名长度16, java.lang.String
        Date类型的hireDay
      6. 最后就是name字段了: 以下就是 字段 name 类型 L (4c)(String属于对象, 不属于基本类型) , 字段名4位长度, 名称为 name, 字段类型为 74, 字段类型 类名长度18, java.lang.String;
        name字段
  9. 接下来就是 类型描述信息结束的标识了

    类型描述信息结束的标识

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

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

    2. 接下来是Date类型的 hireDay, 注意选中的部分, 前面四个字符73 72 00 0e正是 一个字节长度的 对象标识, 一个字节长度的类描述符标识, 两个字节长度的 长度标识
      Date类型的 hireDay字段值
      这里还要注意的一点是, 和我上面红线标出来的不同的是

      1. 红色标识出来的是 其解析出来, 内容是 java/util/Date
      2. 而选中部分解析出来, 其内容是 java.util.Date
    3. 最后是 name 实例字段 数据
      name 实例字段 数据

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. 数据字段描述符格式中类型编码
    数据字段描述符格式中类型编码
  1. 协议文档
  2. 《Java核心技术 卷II 高级特性 第9版》 P45 - P61 (重点关注这个)
  3. 《Effective Java》第二版 P255 - P277
  4. https://www.cnblogs.com/zhukunrong/p/4868856.html
  5. 深入学习 Java 序列化

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/79463450