5.1 概述
当Java对象使用序列化将状态保存在文件中,或作为数据库中的blob时,可能出现读取数据的类的版本与写入数据的类的版本不同的情况。
版本控制引发了一些关于类身份的基本问题,包括何为兼容更改。一个兼容更改是指不影响类与其调用者之间契约的更改。
本节描述了旨在通过限制允许的更改类型并仔细选择机制来解决此问题的目标、假设和解决方案。
提出的解决方案提供了一种机制,用于通过添加字段和添加类来“自动”处理逐渐演变的类。序列化将处理版本控制,无需为每个版本实现特定于类的方法。流格式可以在不调用特定于类的方法的情况下进行遍历。
5.2 目标
目标是:
-
支持不同虚拟机中操作的同一类的不同版本之间的双向通信,方法是:
-
定义一种机制,允许Java类读取由旧版本的同一类写入的流。
-
定义一种机制,允许Java类写入供旧版本的同一类读取的流。
-
-
为持久性和RMI提供默认序列化。
-
在简单情况下表现良好并生成紧凑的流,以便RMI可以使用序列化。
-
能够识别并加载与用于写入流的确切类匹配的类。
-
保持非版本化类的开销低。
-
使用流格式,允许在不调用特定于流中保存的对象的方法的情况下遍历流。
5.3 假设
假设是:
-
版本控制仅适用于可序列化类,因为必须控制流格式以实现其目标。可外部化类将负责其自身的版本控制,这与外部格式相关联。
-
所有数据和对象必须按照它们写入的顺序从流中读取或跳过。
-
类可以单独演变,也可以与超类型和子类型一起演变。
-
类由名称标识。具有相同名称的两个类可能是不同版本或完全不同的类,只能通过比较它们的接口或比较接口的哈希来区分。
-
默认序列化不会执行任何类型转换。
-
流格式只需要支持类型更改的线性序列,而不是类型的任意分支。
5.4 谁负责流的版本控制
在类的演变过程中,演变(较新版本)类有责任维护未演变类建立的契约。这有两种形式。首先,演变类不得破坏原始版本提供的接口的现有假设,以便演变类可以替代原始版本。其次,在与原始(或先前)版本通信时,演变类必须提供足够和等效的信息,以允许早期版本继续满足未演变契约。
在这里讨论的目的是,每个类都实现并扩展其超类型定义的接口或契约。例如,类的新版本,例如foo'
,必须继续满足foo
的契约,并且可以扩展接口或修改其实现。
通过序列化对象之间的通信不是这些接口定义的契约的一部分。序列化是实现之间的私有协议。实现有责任进行足够的通信,以允许每个实现继续满足其客户端期望的契约。
5.5 兼容的Java类型演变
Java语言规范讨论了Java类在演变过程中的二进制兼容性。二进制兼容性的灵活性大部分来自于对类、接口、字段、方法等名称的符号引用的延迟绑定。
以下是序列化对象流版本控制设计的主要方面。
-
默认序列化机制将使用符号模型将流中的字段与虚拟机中相应类的字段绑定。
-
流中引用的每个类将唯一标识自身、其超类型以及写入流的每个可序列化字段的类型和名称。字段按照首先按字段名称排序的原始类型,然后按字段名称排序的对象字段进行排序。
-
每个类在流中可能包含两种类型的数据:必需数据(直接对应于对象的可序列化字段)和可选数据(由原始类型和对象的任意序列组成)。流格式定义了必需数据和可选数据在流中的出现方式,以便在必要时可以跳过整个类、必需部分或可选部分。
-
必需数据由类描述符定义的顺序中的对象字段组成。
-
可选数据写入流中,不直接对应于类的字段。类本身负责此可选信息的长度、类型和版本控制。
-
-
如果为类定义了
writeObject
/readObject
方法,则这些方法将取代默认机制来写入/读取类的状态。这些方法通过调用defaultWriteObject
来写入数据,并通过调用defaultReadObject
来读取数据。 -
每个类的流格式通过使用流唯一标识符(SUID)进行标识。默认情况下,这是类的哈希值。所有类的后续版本必须声明它们与兼容的流唯一标识符(SUID)。这可防止具有相同名称的类被错误地识别为单个类的版本。
-
ObjectOutputStream
和ObjectInputStream
的子类型可以通过使用annotateClass
方法包含其自身的信息来标识类;例如,MarshalOutputStream
嵌入了类的URL。
5.6 影响序列化的类型更改
有了这些概念,现在我们可以描述设计如何处理演变类的不同情况。这些情况描述了由某个类的某个版本写入的流。当相同版本的类读取流时,不会丢失信息或功能。流是关于原始类的唯一信息来源。其类描述虽然是原始类描述的子集,但足以将流中的数据与正在重建的类版本匹配起来。
这些描述是从读取流以重建早期或后续版本的类的角度描述的。在RPC系统的术语中,这是一个“接收者正确”的系统。写入器以最合适的形式写入其数据,接收者必须解释该信息以提取所需部分并填充不可用的部分。
5.6.1 不兼容的更改
对类的不兼容更改是指无法保持互操作性保证的更改。在演变类时可能发生的不兼容更改包括:
-
删除字段 - 如果在类中删除字段,则写入的流将不包含其值。当早期类读取流时,字段的值将设置为默认值,因为流中没有可用的值。但是,此默认值可能会损害早期版本履行其契约的能力。
-
将类上移或下移层次结构 - 这是不允许的,因为流中的数据出现在错误的顺序中。
-
将非静态字段更改为静态字段或将非瞬态字段更改为瞬态字段 - 当依赖默认序列化时,此更改等效于从类中删除字段。此类版本将不会将该数据写入流,因此早期类的字段将初始化为默认值,这可能导致类以意外方式失败。
-
更改原始字段的声明类型 - 类的每个版本都使用其声明的类型写入数据。尝试读取字段的早期版本将失败,因为流中的数据类型与字段的类型不匹配。
-
更改
writeObject
或readObject
方法,使其不再写入或读取默认字段数据,或更改使其尝试写入或读取默认字段数据,而先前版本未这样做。默认字段数据必须一致地出现或不出现在流中。 -
将类从
Serializable
更改为Externalizable
或反之是不兼容的更改,因为流将包含与可用类的实现不兼容的数据。 -
将类从非枚举类型更改为枚举类型或反之是不兼容的更改,因为流将包含与可用类的实现不兼容的数据。
-
删除
Serializable
或Externalizable
是不兼容的更改,因为写入时将不再提供旧版本类所需的字段。 -
向类添加
writeReplace
或readResolve
方法是不兼容的,如果行为会产生与类的任何旧版本不兼容的对象。
5.6.2 兼容的更改
对类的兼容更改处理如下:
-
添加字段 - 当被重建的类具有流中不存在的字段时,对象中的该字段将被初始化为其类型的默认值。如果需要类特定的初始化,该类可以提供一个readObject方法,该方法可以将字段初始化为非默认值。
-
添加类 - 流将包含流中每个对象的类型层次结构。将流中的层次结构与当前类中的层次结构进行比较可以检测到额外的类。由于流中没有信息可以用来初始化对象,因此类的字段将被初始化为默认值。
-
移除类 - 将流中的类层次结构与当前类的类层次结构进行比较可以检测到类已被删除的情况。在这种情况下,与该类对应的字段和对象将从流中读取。原始字段将被丢弃,但由已删除类引用的对象将被创建,因为它们可能在流中稍后被引用。它们将在流被垃圾回收或重置时被垃圾回收。
-
添加
writeObject
/readObject
方法 - 如果读取流的版本具有这些方法,则预期readObject
会读取默认序列化写入流的所需数据。在读取任何可选数据之前,应首先调用defaultReadObject
。预期writeObject
方法通常会调用defaultWriteObject
来写入所需数据,然后可以写入可选数据。 -
移除
writeObject
/readObject
方法 - 如果读取流的类没有这些方法,则默认序列化将读取所需数据,并丢弃可选数据。 -
添加
java.io.Serializable
- 这相当于添加类型。流中不会有此类的值,因此其字段将被初始化为默认值。支持对非可序列化类进行子类化要求类的超类具有无参构造函数,并且类本身将被初始化为默认值。如果无参构造函数不可用,则会抛出InvalidClassException
。 -
更改字段的访问权限 - 访问修饰符public、package、protected和private对序列化赋值给字段的能力没有影响。
-
将字段从静态更改为非静态或从瞬态更改为非瞬态 - 当依赖默认序列化来计算可序列化字段时,此更改等同于向类中添加字段。新字段将写入流,但较早的类将忽略该值,因为序列化不会为静态或瞬态字段赋值。
-
添加或移除记录组件 - 当被重建的记录对象具有流中不存在的记录组件时,记录类的规范构造函数将传递其类型的默认值。如果需要特定的初始化,构造函数可以将组件初始化为非默认值。未传递给规范构造函数的流字段值将被有效丢弃。
-
将类从普通类更改为记录类 - 适合从普通类转换为记录类,并依赖默认序列化的类可以更改为记录类。普通类应将
java.lang.Object
声明为其直接超类,或者在其超类中没有可序列化状态。记录类的组件的名称和类型必须与普通类的可序列化字段的名称和类型匹配。记录对象通过记录类的规范构造函数重建。如果规范构造函数抛出异常,例如在检查不变量时,则会抛出InvalidObjectException
。 -
将类从记录类更改为普通类 - 依赖默认序列化的记录类可以更改为普通类。普通类必须声明一个显式的
serialVersionUID
,其值与先前记录类的serialVersionUID
相同,或者如果先前的记录类没有显式的serialVersionUID
声明,则为0L
。普通类的可序列化字段的名称和类型必须与先前记录类的组件的名称和类型匹配。