分布式系统开发工具包 —— 基于 Kryo 的 Java 对象序列化

kryo

Kryo 是用于 Java 语言的一个快速和高效的对象图序列化框架。Kryo 项目的目的是快速、高效、方便地使用 API。当需要持久化对象的时候,不论是持久化到文件、数据库还是网络,都可以使用 Kryo。

目前 Kryo 已经到了 4.0.1 版本以上了。本文的介绍适用于 V2.0+ 以上版本。

安装 Kryo

一般适用 maven 来 Kryo 包。

使用官方版的 Kryo 的话可以引用下述配置代码

1
2
3
4
5
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.1</version>
</dependency>

如果你已经在你的 classpath 下有了不同版本的 asm 了的话,上述依赖可能会碰到问题。这时你可以使用 kyro-shaded jar 包,它自身包含了它所需版本的 asm,并且是位于在不同包里的。

1
2
3
4
5
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo-shaded</artifactId>
<version>4.0.1</version>
</dependency>

如果你想试用最新的特性

1
2
3
4
5
6
7
8
9
10
11
<repository>
<id>sonatype-snapshots</id>
<name>sonatype snapshots repo</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>

<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.1-SNAPSHOT</version>
</dependency>

开始使用 Kryo 库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.io.Input;
// ...
Kryo kryo = new Kryo();
// ...
Output output = new Output(new FileOutputStream("file.bin"));
SomeClass someObject = ...
kryo.writeObject(output, someObject);
output.close();
// ...
Input input = new Input(new FileInputStream("file.bin"));
SomeClass someObject = kryo.readObject(input, SomeClass.class);
input.close();

Kryo 类编排序列化,Output 和 Input 类处理缓存字节和刷新到流中。

IO

Output 类是一个 OutputStream,它将数据写入字节数组缓冲。如果需要一个字节数组的话,这个缓冲可以被获得并直接使用。如果这个 Output 被赋予一个 OutputStream 的话,当缓冲满了的话,它会 flush 字节到流里。Output 有许多方法用于有效地写入基本类型和 String 到字节里。它提供了类似于 DataOutputStream, BufferedOutputStream, FilterOutputStream, and ByteArrayOutputStream 的功能。

因为 Output 作了缓冲,所以在写入一个 OutputStream 时,务必在写入结束时调用 flush()或者 close()方法,以便缓冲着的字节能够被写入底层流。

Serializers

Kryo 是一个序列化框架。它不会强迫一种模式或者要求写入或读出什么样的数据,这都留给 Serializer 本身去做。默认提供的 Serializers 可以以多种方式读写。如果这无法满足特定的需求,它们可以被部分替换或整体替换。默认提供的 serializers 可以读写多种对象,但,写一个新的 serializer 也很容易。Serializer 抽象类提供了方法去转换对象到字节或字节到对象。

1
2
3
4
5
6
7
8
9
public class ColorSerializer extends Serializer<Color> {
public void write (Kryo kryo, Output output, Color object) {
output.writeInt(object.getRGB());
}

public Color read (Kryo kryo, Input input, Class<Color> type) {
return new Color(input.readInt(), true);
true}
}

Serializer 有 2 个方法可以被实现。write()方法将对象写为字节;read()创建一个对象的新实例,并且从 input 读取内容来填充它。

Kryo 实例可以被用于读写嵌入对象。如果 Kryo 在 read()中被用于读一个嵌入对象,那么:

  • 如果嵌入对象可能引用父对象的话,kryo.reference()必须被随父对象一起调用
  • 如果嵌入对象不会引用父对象、Kryo 没有被嵌入对象使用以及引用没有被使用的话,kryo.reference()可以不被随父对象一起调用
    如果嵌入对象可以使用相同的 serializer 的话,那么 serializer 必须是可重入(reentrant)的。

代码不可以直接使用 Serializers,应该使用 Kryo 的 read 和 write 方法,这使得 Kryo 能够编排序列化、处理诸如 references 和 null objects 这种特性。

默认地、serializers 没必要处理 null object。Kryo 框架会根据需要写入一个 byte 代表 null 还是非 null。如果 serializers 想要更加高效或想自己处理 null,它可以调用

1
2
3

# Registration
当 Kryo 写出一个对象的实例时,首先它可能需要写出一些东西来代表对象的 class。默认地,先写一个完全限定的 class name,然后是实例对应的字节。写 class name 有点儿不太高效,所以可以在处理之前先注册这个类。

Kryo kryo = new Kryo();
kryo.register(SomeClass.class);
// …
Output output = …
SomeClass someObject = …
kryo.writeObject(output, someObject);

1
这儿 SomeClass 被注册到 Kryo 中,class name会被关联到一个整数 ID。当 Kryo 写出一个 SomeClass 的实例的时候,它将写出这个整数 ID。这可比写出 class name 高效地多,但它需要提前知晓这个将被序列化的类。上面展示的 register 方法分配下一个、最小的整数 ID,它意味着类的注册顺序是很重要的。ID 也可以被显示指定,使得顺序无关紧要。

Kryo kryo = new Kryo();
kryo.register(SomeClass.class, 10);
kryo.register(AnotherClass.class, 11);
kryo.register(YetAnotherClass.class, 12);

1
2
3
4
5
6
7
8
9
10
11
小的正整数是最高效的。负数不能被有效地序列化。-1 和 -2 是保留的。

注册与不注册可以混用。所有基本类型、包装过的基本类型、String 和 void 默认被注册为 ID 从 0-9,所以注意不要把其他的注册到这个范围内了,以防覆盖了它们。

Kryo#setRegistrationRequired 设为 true,以便在发生未注册类时抛出异常。这阻止应用程序偶然地使用了 class name 字符串。

如果使用非注册类,那么尽量使用短的包名。

# 默认 serializer

在写出了类的身份信息后,Kryo 使用 serializer 去写对象的字节。在注册类的时候,可以指定一个 serializer 实例。

Kryo kryo = new Kryo();
kryo.register(SomeClass.class, new SomeSerializer());
kryo.register(AnotherClass.class, new AnotherSerializer());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果类没有被注册或者没有指定 serializer,则会从 "默认 serializers" 列表中选择一个 serializer 出来。以下类默认有一个默认的 serializer 集。

| | | | ||
|-----------|-----------|-------|-------|----|
|boolean |Boolean |byte |Byte |char|
|Character |short |Short |int |Integer|
|long |Long |float |Float |double|
|Double |byte[] |String |BigInteger |BigDecimal|
|Collection |Date |Collections.emptyList |Collections.singleton |Map|
|StringBuilder |TreeMap |Collections.emptyMap |Collections.emptySet |KryoSerializable|
|StringBuffer |Class |Collections.singletonList |Collections.singletonMap |Currency|
|Calendar |TimeZone |Enum |EnumSet|

可以增加额外的默认 serializers

Kryo kryo = new Kryo();
kryo.addDefaultSerializer(SomeClass.class, SomeSerializer.class);
// …
Output output = …
SomeClass someObject = …
kryo.writeObject(output, someObject);

1
2

一个类也可以使用 DefaultSerializer 标注

@DefaultSerializer(SomeClassSerializer.class)
public class SomeClass {
// …
}

1
2

如果没有匹配的 serializer,那么默认地,使用 FieldSerializer。这也是可改的。

Kryo kryo = new Kryo();
kryo.setDefaultSerializer(AnotherGenericSerializer.class);

1
2

一些序列化器允许设置额外的信息,以便减少序列化后的字节数量。

Kryo kryo = new Kryo();
FieldSerializer someClassSerializer = new FieldSerializer(kryo, SomeClass.class);
CollectionSerializer listSerializer = new CollectionSerializer();
listSerializer.setElementClass(String.class, kryo.getSerializer(String.class));
listSerializer.setElementsCanBeNull(false);
someClassSerializer.getField(“list”).setClass(LinkedList.class, listSerializer);
kryo.register(SomeClass.class, someClassSerializer);
// …
SomeClass someObject = …
someObject.list = new LinkedList();
someObject.list.add(“thishitis”);
someObject.list.add(“bananas”);
kryo.writeObject(output, someObject);

1
2
3
4
5
6
7
8
9
10

在这个例子中,FieldSerializer 将被用于 SomeClass,FieldSerializer 被配置为 "list" 字段永远为 LinkedList,并且用指定的 CollectionSerializer。CollectionSerializer 被配置为所有字段都是 String,并且没有字段为 null。这让序列化器更加高效。这种情况下,list 中的每个元素将节约 2-3 个字节。

# FieldSerializer
默认地,大部分类以 FieldSerializer 结束,它本质上是做了手工编写的序列化要做的事情,不过它是自动的。FieldSerializer 直接赋值到对象的字段,如果字段是 public、protected 或者默认访问级别(package private)并且没有被标记为 final,则使用生成字节码方式以求最大速度(参考 [ReflectASM](https://github.com/EsotericSoftware/reflectasm))。对于 private 字段,则使用 setAccessible 和 cached reflection,速度也不慢。

同时还提供了其他一些通用目的的 serializers,比如 BeanSerializer, TaggedFieldSerializer, CompatibleFieldSerializer, 和 VersionFieldSerializer。Github 上还有一些附加的 serializers,参考 [kryo-serializers](https://github.com/magro/kryo-serializers)

# KryoSerializable
虽然 FieldSerializer 对于大多数类来说已经足够理想了,但有时候让类自己去做序列化更加方便。实现 KryoSerializable 接口,就可以达到目的(有点类似 JDK 中的 java.io.Externalizable)。

public class SomeClass implements KryoSerializable {
// …

public void write (Kryo kryo, Output output) {
// …
}

public void read (Kryo kryo, Input input) {
// …
}
}

1
2
3
4
# 使用标准的 java 序列化 
很罕见地,某些类不能被 Kryo 序列化。这种情况下,可替代地,可以使用 Kryo 的 JavaSerializer 提供的回退策略:使用标准的 Java Serialization。这种方式可能会和Java 序列化一样慢,但可以让你的类像 Java 序列化一样得到序列化。当然,你的类需要实现 Serializable 或者 Externalizable 接口。

如果你的类实现了 Serializable 接口,那么你可以使用 Kryo 专用的 JavaSerializer 这种 serializer:

kryo.register(SomeClass.class, new JavaSerializer());

1
2

如果你的类实现了 Externalizable 接口,那么你可以使用 Kryo 专用的 ExternalizableSerializer 这种 serializer:

kryo.register(SomeClass.class, new ExternalizableSerializer());

1
2
3
4
5
6
7
8
9
10



参考连接:

# Class Field Annotation

典型地,如果使用了 FieldSerializer,它可以自动地猜测类中的每个字段应该用哪种 serializer。但在特定情况下,你可能需要改变默认的行为,并且定制化字段该如何被序列化。

Kryo 提供了一系列注解来辅助实现这些目的。@Bind 可以用于任何字段,@CollectionBind 可以用于集合类型的字段,@MapBind 可以用于 map 类型的字段。

public class SomeClass {
   // Use a StringSerializer for this field
   @Bind(StringSerializer.class) 
   Object stringField;

   // Use a MapSerializer for this field. Keys should be serialized
   // using a StringSerializer, whereas values should be serialized
   // using IntArraySerializer
   @BindMap(
             valueSerializer = IntArraySerializer.class, 
             keySerializer = StringSerializer.class, 
             valueClass = int[].class, 
             keyClass = String.class, 
             keysCanBeNull = false) 
   Map map;

   // Use a CollectionSerializer for this field. Elements should be serialized
   // using LongArraySerializer
   @BindCollection(
             elementSerializer = LongArraySerializer.class,
             elementClass = long[].class, 
             elementsCanBeNull = false) 
   Collection collection;

   // ...
}
1
2
3
4
5
6

# Reading 和 Writing

Kryo 有三种方法用于读写对象

如果对象的实际类是未知的并且对象可能为null
kryo.writeClassAndObject(output, object);
// ...
Object object = kryo.readClassAndObject(input);
if (object instanceof SomeClass) {// ...}
1
2

如果类是已知的并且对象可能为null
kryo.writeObjectOrNull(output, someObject);
// ...
SomeClass someObject = kryo.readObjectOrNull(input, SomeClass.class);
1
2

如果类是已知的并且对象不可能为null
kryo.writeObject(output, someObject);
// ...
SomeClass someObject = kryo.readObject(input, SomeClass.class);
1
2
3
4

# 引用

默认地,图中对象的首次之后的每一次出现都是存储为整数序号的,这允许序列化相同对象的多次引用和循环引用。这需要一些成本,你可以根据需要关闭它,以节约空间。
Kryo kryo = new Kryo();
kryo.setReferences(false);
// ...

`

当编写 serializer 使用 Kryo 处理嵌套对象时,read()方法中必须调用 kryo.reference()。

创建对象

用于特定类型的 Serializers,使用 Java 代码创建那个类型的新的实例。诸如 FieldSerializer 这种 serializers 是通用的,并且必须处理创建任何类型的对象。默认地,如果类型有零参构造函数,那么它通过 ReflectASM 或反射调用,其它情况会抛出一个异常。如果零参构造函数是 private 的,将尝试通过反射利用 setAccessible 来访问。如果这是可接受的,一个私有零参构造函数是一个很好的方式来允许 Kryo 创建类实例,同时避免影响 public API。