《Object Serialization Stream Protocol/对象序列化流协议》总结

2023-01-03 14:21:49 浏览数 (1)

0、写在前面

本文主要是《Object Serialization Stream Protocol》一文的翻译,然后对序列化格式进行了一些总结

1、概述

​stream 格式满足以下设计目标:

  • 结构紧凑,便于高效阅读;
  • 允许仅使用流的结构和格式而不需要深入了解 stream,这种情况不需要调用调用任何类的代码;
  • 只允许 stream 对数据进行访问和操作;

2、stream元素

​表示 stream 中的对象需要的基本结构。对象的每个属性都需要表示:它的类和类中的字段,这些数据会被写入而且之后会被类中特定的方法读取。stream 中对象的表示可以用语法来描述。null objects, new objects, classes, arrays, strings以及对stream 中已有的任何对象的反向引用,都有特殊的表示。写入 stream 的每个对象都分配了一个Handle,用于引用回该对象。从0x7E0000开始按顺序分配Handle。当 stream 被重置时,句柄在0x7E0000处重新启动。

类对象由以下对象表示:

  • 它的ObjectStreamClass对象

非动态代理类的ObjectStreamClass对象由以下表达式表示:

  • 可兼容类的SUID
  • 一组指示类的各种属性的标志,例如该类是否定义了 writeObject 方法,以及该类是否可序列化、是否是可外部或者是否是枚举类型
  • 可序列化字段的数量
  • 默认情况下,对于类的字段数组和对象字段来说,字段的类型要作为字符串被包含,并且必须按照 Java 虚拟机规范中的规定,采用“字段描述符”格式(例如,Ljava/lang/Object;
  • annotateClass 方法写入的可选块数据(Data-Block)记录或对象
  • 其超类型的 ObjectStreamClass(如果超类不可序列化,则为 null)

动态代理类的ObjectStreamClass对象由以下表达式表示:

  • 动态代理类实现的接口数
  • 动态代理类实现的所有接口的名称,这些接口通过调用Class的getInterfaces方法的返回结果进行排序列出;
  • 可选的块数据(Data-Block)记录或由AnnotationProxyClass方法编写的对象
  • 超类对应的java.lang.reflect.ProxyObjectStreamClass

​ 字符串对象的表示由长度信息和MUTF-8编码的字符串内容组成。MUTF-8编码与Java虚拟机和Java.io.DataInput以及DataOutput接口中使用的编码相同;它在表示补充字符空字符方面与标准UTF-8不同。长度信息的形式取决于MUTF-8编码中字符串的长度。如果给定字符串的MUTF-8编码长度小于65536字节,则该长度将写入表示无符号16位整数的2字节。从Java2平台标准版v1.3开始,如果MUTF-8编码中的字符串长度为65536字节或更多,那么该长度将以表示有符号64位整数的8字节表示。序列化 stream 中字符串前面的类型码用于表明写入字符串的格式。

数组由以下内容表示:

  • 他们的ObjectStreamClass对象
  • 元素的数量。
  • 值的顺序。值的类型在数组的类型中是隐式的。例如,字节数组的值是byte类型的。

枚举常量由以下表达式表示:

  • 常量的基本枚举类型的 ObjectStreamClass 对象
  • 常量的名称字符串

stream 中的新对象New objects)由以下表示:

  • 所有对象类的派生类信息;
  • 对象的每一个可序列化类的数据,从它的顶级父类开始写入。针对 Stream 中的每一个类的信息包含以下内容: -- 一个类中的可序列化字段信息;

-- 如果这个类包含writeObject/readObject方法,有可能出现通过writeObject方法写入的可选对象或者基础类型的数据块(Data-Block)记录,跟着使用endDataBlock的代码;

​ 类写入的所有原始数据都被缓冲并包装在数据块记录中,无论数据是在 writeObject 方法内写入 stream 还是从 writeObject 方法外部直接写入 Stream。 此数据只能通过相应的 readObject 方法读取或直接从 stream 中读取。writeObject方法写入的对象终止任何以前的数据块记录,并视情况作为常规对象或空引用或反向引用写入。数据块记录允许错误恢复丢弃任何可选数据。当从类内调用时,stream 可以丢弃任何数据或对象,直到endBlockData

3、Stream 协议版本

​ 有必要对JDK1.2中的序列化 Stream 格式进行更改,该格式与JDK1.1的所有次要版本都不向后兼容。为了提供需要向后兼容的情况,Oracle 添加了一个功能,这个功能用来指示在编写序列化流时要使用哪个协议版本。ObjectOutputStream中的useProtocolVersion方法会接收一个参数以表示写入的可序列化字节流的协议版本。

流协议版本如下:

  • ObjectStreamConstants.PROTOCOL_VERSION_1:表示初始流格式。
  • ObjectStreamConstants.PROTOCOL_VERSION_2:表示新的外部数据格式。基本数据以块数据模式写入,并以TC_ENDBLOCKDATA终止。

数据块边界已标准化。以数据块模式写入的基元数据被规范化为不超过1024字节块。此更改的好处是加强了 Stream 中序列化数据格式的规范。这种变化是完全前后兼容的。

JDK1.2默认为写入PROTOCOL_VERSION_2 JDK1.1默认为写入PROTOCOL_VERSION_1 JDK1.1.7及更高版本可以读取这两个版本 JDK1.1.7之前的版本只能读取PROTOCOL_VERSION_1

4、Stream 格式的语法

下表包含流格式的语法。非终结符号以斜体显示。终结符号拥有固定的宽度。非终结符号的定义之后带了一个。这个定义之后每一行会有一个或者多个替代符号。下表描述了符号:

Notation

Meaning

(datatype)

此令牌具有指定的数据类型,例如 byte

token[n]

令牌的预定义出现次数,即数组

x0001

用十六进制表示的文字值,十六进制数字的数量反映了值的大小

<xxx>

从 Stream 中读取的值,用于指示数组的长度。

注意,符号(utf)用于指定使用2字节长度信息写入的字符串,而(long utf)用于指定使用8字节长度信息写入的字符串。

a. 语法规则

序列化 Stream 由满足 Stream 规则的任何 Stream 表示【官方文档仅有字段值分类而无具体含义,这里的具体含义来自参考中的序列化草案】

stream:

代码语言:javascript复制
             **` magic` `version` `contents`**

  整个数据流的格式,直接分成三部分,magic 表示魔数STREAM_MAGIC标记,version 表示序列化的版本STREAM_VERSIONcontents表示最终生成的序列的内容;

contents:

代码语言:javascript复制
              **`content`**
             **` contents content`**

  这一部分表示生成的二进制序列的内容部分,这些内容有可能是独立的内容【content】,也可能是多个内容的一个集合【contents】;

content:

代码语言:javascript复制
             **`object`**
             **` blockdata`**

  二进制序列独立的内容【content】有可能包含对象定义的数据【object】,也有可能包含数据块格式的数据【blockdata】,上边格式也有能blockdata在前,object在后;

object:

代码语言:javascript复制
             **` newObject`**
             **` newClass`**
             **`  newArray`**
             **` newString`**
             **`  newEnum`**
             **`  newClassDesc`**
             **`  prevObject`**
             **`  nullReference`**
             **`  exception`**
             ==`  TC_RESET`==

​ 该部分内容表示对象中包含的字节流数据,这部分数据中的元素相互间没有顺序,仅仅表示该对象中可能存在标记表示的数据;newObject表示新对象类型, newClass表示Class类型的对象,newArray表示数组对象,newString表示字符串对象,newEnum表示枚举常量,newClassDesc表示对象的类描述信息,preObject表示前边出现过的对象,nullReference表示空引用,exception表示异常对象,==TC_RESET==表示重置标记【固定值】。

newClass:

代码语言:javascript复制
              ==`TC_CLASS`==  **`classDesc` `newHandle`**

  该部分内容表示一个新的Class类型的对象,==TC_CLASS==表示类型标记,classDesc表示类描述信息,newHandle表示新的引用;

classDesc:

代码语言:javascript复制
             `  newClassDesc`
             ` nullReference`
             `  (ClassDesc)prevObject`      // 要求为ClassDesc类型的对象

  该部分表示一个对象的类描述符,newClassDesc表示新出现一个类描述符,nullReference表示空引用,prevObject表示前边出现过的对象;

superClassDesc:

代码语言:javascript复制
              **`classDesc`**    

  这部分表示父类的描述符信息,它的内容是一个classDesc,也就是上边类描述信息;

newClassDesc:

代码语言:javascript复制
              ==`TC_CLASSDESC`== **`className` `serialVersionUID` `newHandle` `classDescInfo`**
              ==`TC_PROXYCLASSDESC`== **`newHandle` `proxyClassDescInfo`**

​ 这个部分演示了类描述符中描述的两种类描述符信息:一般类描述信息,动态代理类描述信息,clsssName表示类名,serialVersionUID表示该类中定义的serialVersionUID对应的值,newHandle表示一个新的引用,classDescInfo表示类描述符本身的相关信息,proxyClassDescInfo表示动态代理类描述符本身相关的信息;

classDescInfo:

代码语言:javascript复制
              **` classDescFlags` `fields` `classAnnotation` `superClassDesc `**

​ 这一部分内容是详细的类描述信息,classDescFlags为类描述信息标记,fields表示类中所有字段的描述信息,classAnnotation表示和类相关的Annotation的描述信息,superClassDesc表示该类的父类的描述信息。

className:

代码语言:javascript复制
        *(utf)*

  类全名,以UTF-8的格式保存的字符串对应的二进制序列,描述了当前对象的类全名;

serialVersionUID:

代码语言:javascript复制
         *(long)*

​ 对应类定义中的字段serialVersionUID的信息;

classDescFlags:

代码语言:javascript复制
          *(byte)*                  // 在终端符号和常量中的定义

​ 类描述符标记,一个字节的数据,用于定义终止符和常量;

proxyClassDescInfo:

代码语言:javascript复制
          *(int)<count> proxyInterfaceName[count] classAnnotation superClassDesc*

  动态代理类的相关描述信息,<count>表示该动态代理类实现的接口总数,类型为int类型。proxyInterfaceName[count]表示所有当前动态代理类实现的接口信息,classAnnotation表示该动态代理类对应的Annotation的描述信息,superClassDesc表示当前动态代理类的父类的类描述信息;

proxyInterfaceName:

代码语言:javascript复制
         *(utf)*

​ 动态代理类的代理接口的名称,一个UTF-8格式的字符串对应的二进制序列;

fields:

代码语言:javascript复制
         *(short)<count>  fieldDesc[count]*

<count>表示该类中的字段【成员属性】的总数,数据类型为short类型。fieldDesc[count]表示一个类中所有字段的详细描述信息,字段的数量和前边的count是一致的;

fieldDesc:

代码语言:javascript复制
              **`primitiveDesc`**

objectDesc

  这个标记表示字段的描述信息,字段描述信息包括部分信息内容,primitiveDesc表示基础类型数据的描述信息,objectDesc表示对象类型数据的描述信息;

primitiveDesc:

代码语言:javascript复制
              **`prim_typecode fieldName`**

  基础类型的字段的相关描述信息,prim_typecode表示字段的类型标识,字段类型标识表示当前字段的类型,fieldName表示字段的名称,为一个字段名称组成的字符串的二进制序列;

objectDesc:

代码语言:javascript复制
              **`obj_typecode` `fieldName` `className1`**

​ 对象类型的字段的描述信息,obj_typecode表示字段的类型标识,该标识描述了对象字段对应的类信息,fieldName表示字段的名称,为一个字段名称组成的字符串的二进制序列,className1表示该成员属性的类型签名;

fieldName:

代码语言:javascript复制
          *(utf)*

​ 表字段名称字符串,该字符串由二进制序列组成且经过了 UTF-8 编码

className1:

代码语言:javascript复制
          *(String)object*             // 包含字段类型的字符串,字段描述符格式

  该对象对应的类的类全名,为一个String类型的对象描述信息;

classAnnotation:

代码语言:javascript复制
              **`endBlockData`**
              **`contents endBlockData`**      // annotateClass 编写的内容

​ 该对象所属类中的Annotation的描述信息,endBlockData为存储对象的数据块【Data-Block】的结束标记,为终止符,contents表示该类中多个内容的一个集合【contents】;

prim_typecode:

代码语言:javascript复制
             `B`       // byte
             `C`       // char
             `D`       // double
             `F`       // float
             `I`       // integer
             `J`       // long
             `S`       // short
             `Z`       // boolean

  基础类型的字段的类型标识,标识了字段所属的基础数据类型,其代码表示的类型含义如定义中的注释部分的内容;

obj_typecode:

代码语言:javascript复制
              `[`       // array
              `L`       // object

  对象类型的字段的类型标识,标识了字段所属的对象类型,其代码表示类型含义如注释部分的内容;

newArray:

代码语言:javascript复制
              ==`TC_ARRAY`== **`classDesc` `newHandle (int)<size> values[size]`**

​ 创建一个新的数组的描述符,==TC_ARRAY==表示接下来的序列是一个数组,它是数组序列的开始标记,classDesc是当前这个数组的类描述符,newHandle表示针对当前数组对象的引用,<size>表示该数组的长度,长度数字为int类型,values[size]表示当前数组每一个元素的值部分的内容;

newObject:

代码语言:javascript复制
              ==`TC_OBJECT`==**` classDesc newHandle classdata[]`**      // 每个类的数据

​ 创建一个新的对象的描述符信息,==TC_OBJECT==表示接下来的序列是一个新对象,它是对象的开始标记,classDesc是当前这个对象的类描述符,newHandle表示针对当前对象的引用,classdata[]这个对象对应的每一个Class的相关数据信息;

classdata:

代码语言:javascript复制
        **`nowrclass`**                     // SC_SERIALIZABLE & classDescFlag && !(SC_WRITE_METHOD & classDescFlags)
        **`wrclass objectAnnotation`** // SC_SERIALIZABLE &classDescFlag&&SC_WRITE_METHOD&classDescFlags 
        **` externalContents`**          // SC_EXTERNALIZABLE & classDescFlag && !(SC_BLOCKDATA  & classDescFlags
        **`objectAnnotation`**          // SC_EXTERNALIZABLE & classDescFlag&& SC_BLOCKDATA & classDescFlags

​ 这一部分数据描述的是类数据中所有内容,下边有针对各种不同的类数据相关说明;

nowrclass:

代码语言:javascript复制
              **`values`**                    //  类描述符顺序的字段

  一个类中可序列化的字段的数据值,这些数据值的顺序遵循类描述符中定义的顺序;

wrclass:

代码语言:javascript复制
              **`nowrclass`**

  这部分数据的内容和上述的nowrclass部分的内容是一样的,表一个类中可序列化的字段的数据值;

objectAnnotation:

代码语言:javascript复制
              **`endBlockData`**
              **`contents endBlockData`**     // 由`writeObject`或`writeExternal PROTOCOL_VERSION_2`编写的内容。

  这部分数据的内容和classAnnotation的数据结构是一致的;表该对象所属类中的Annotation的描述信息,endBlockData为存储对象的数据块【Data-Block】的结束标记,为终止符,contents表示该类中多个内容的一个集合【contents】;

blockdata:

代码语言:javascript复制
              **`blockdatashort`**
              **`blockdatalong`**

  在Java序列化中,数据块存储分为种:一种是长度为short的默认数据块方式,另外一种是长度为int的数据块方式,这种方式可存储容量大的数据;

blockdatashort:

代码语言:javascript复制
              ==`TC_BLOCKDATA`== **`(unsigned byte)<size> (byte)[size]`**

​ 描述了长度为short的默认数据块的结构;

blockdatalong:

代码语言:javascript复制
              ==` TC_BLOCKDATALONG`== **` (int)<size> (byte)[size]`**

  描述了长度为int类型的数据块的结构;

endBlockData :

代码语言:javascript复制
              ==`TC_ENDBLOCKDATA`==

  表示数据块的结束标记,一般用于描述当前的数据块结束了或者这个对象类型的描述符已经结束了;

externalContent: // 只能由readExternal解析

代码语言:javascript复制
          *( bytes)*                // 原始数据
                `object`

  这个部分描述的是外部化的相关内容,(bytes)部分的数据只能被readExternal方法读取,而且里面一般包含的数据类型是基础类型数据,object表示对象数据类型;

externalContents: // 在PROTOCOL_VERSION_1中由writeExternal编写的外部内容。

代码语言:javascript复制
               **`externalContent`**        

externalContents externalContent

  这部分内容是上述的外部化内容的一个集合,一般这一部分只包含了使用writeExternal方法以PROTOCOL_VERSION_1的版本写入字节流的数据;

newString:

代码语言:javascript复制
              ==`TC_STRING`== **`newHandle`** *(utf)*
              ==`TC_LONGSTRING`== **`newHandle`** *(long-utf)*

  表示一个字符串类型的数据,而字符串数据同样有种类型:STRINGLONGSTRING

newEnum:

代码语言:javascript复制
              ==`TC_ENUM`== **`classDesc` `newHandle` `enumConstantName`**

​ 表示一个Enum类型的数据,==TC_ENUM==为枚举类型的标识,表示接下来的序列类型是枚举类型,classDesc为一个枚举类型的类描述符,newHandle为该枚举对象的引用,enumConstantName的值为调用枚举类型中的name()方法返回的枚举类型的值对应的字符串字面量;

enumConstantName:

代码语言:javascript复制
              `(String)object`
    枚举常量的字符串名称字面量,本身为一个字符串;

prevObject

代码语言:javascript复制
              ==`TC_REFERENCE`== `(int)handle`

​ 表示已经写入到字节流中的对象的一个对象的引用,==TC_REFERENCE==是引用标识,表接下来的数据类型是Java引用类型。

nullReference

代码语言:javascript复制
              ==`TC_NULL`==

  就一个字节长度的数据,就表示null值,一般这个值表示的是对象的空引用;

exception:

代码语言:javascript复制
              ==`TC_EXCEPTION`== **`reset` `(Throwable)object` ` reset `**

  针对异常信息的描述,==TC_EXCEPTION==为异常信息的标记,标识接下来的序列是一个异常对象;

magic:

代码语言:javascript复制
              **`STREAM_MAGIC`**

​ 魔数;

version

代码语言:javascript复制
              `STREAM_VERSION`

​ 序列化的版本信息,本文中使用的默认值是05

values: // 当前对象的ClassDec描述了大小和类型

  针对当前对象的classDesc对应的类描述信息提供描述类型的大小;

newHandle: // 序列中的下一个数字分配给被序列化或反序列化的对象

  序列中的下一个数值将赋值给一个可序列化或者可执行反序列化的对象引用;

reset: // 将丢弃已知对象集,以便异常的对象不会与以前发送的对象或异常之后可能发送的对象重叠

  一个已知对象的集合将会被放弃,重置该字节流;

b.终端符号和常数

java.io.ObjectStreamConstants中的以下符号定义了 stream 中预期的终端值和常量值。

代码语言:javascript复制
    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;

标志字节 classDescFlags 可能包括以下值:

代码语言:javascript复制
    final static byte SC_WRITE_METHOD = 0x01; //if SC_SERIALIZABLE
    final static byte SC_BLOCK_DATA = 0x08;    //if SC_EXTERNALIZABLE
    final static byte SC_SERIALIZABLE = 0x02;
    final static byte SC_EXTERNALIZABLE = 0x04;
    final static byte SC_ENUM = 0x10;

如果写入流的可序列化类具有writeObject方法,并且若该方法已将其他数据写入 stream ,则会设置标志SC_WRITE_METHOD。在这种情况下,TC_ENDBLOCKDATA标记总是希望终止该类的数据。

如果使用SC_BLOCKDATAExternalizable类写入 stream,则设置标志SC_BLOCKDATA。默认情况下,在JDK 1.2中将Externalizable对象写入stream的协议。JDK1.1中写入STREAM_PROTOCOL_1

如果编写 stream 的类扩展了java.io.SERIALIZABLE而不是java.io.Externalizable,那么会设置标志 SC_SERIALIZABLE,读取 stream 的类也必须扩展java.io.SERIALIZABLE,并使用默认的序列化机制。

如果编写 stream 扩展java.io.EXTERNALIZABLE的类,读取数据的类也必须扩展EXTERNALIZABLE,并且如果使用其writeExternalreadExternal方法读取数据,那么会设置标记SC_EXTERNALIZABLE

如果写入 stream 的类是枚举类型,则会设置标志SC_ENUM。接收方的对应类也必须是枚举类型。

最后根据上述内容,可以总结得到下图:

5、参考

官方文档:https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html

Java序列化草案:https://blog.csdn.net/silentbalanceyh/article/details/8183849

0 人点赞