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

kryo

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

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

安装Kryo

一般适用maven来Kryo包。

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

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

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

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

如果你想试用最新的特性

<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库

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抽象类提供了方法去转换对象到字节或字节到对象。

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);
    }
}

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,它可以调用Serializer#setAcceptsNull(true)。当已知一个类型的所有实例都绝不会为null时,这也可以被用来避免写入代表null的字节。

Registration

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

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

这儿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是保留的。

注册与不注册可以混用。所有基本类型、包装过的基本类型、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());

如果类没有被注册或者没有指定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);

一个类也可以使用DefaultSerializer标注

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

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

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

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

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);

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

FieldSerializer

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

同时还提供了其他一些通用目的的serializers,比如BeanSerializer, TaggedFieldSerializer, CompatibleFieldSerializer, 和 VersionFieldSerializer。Github上还有一些附加的serializers,参考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) {
      // ...
   }
}

使用标准的java序列化

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

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

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

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

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

参考连接:

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;

       // ...
    }

Reading 和 Writing

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

如果对象的实际类是未知的并且对象可能为null

    kryo.writeClassAndObject(output, object);
    // ...
    Object object = kryo.readClassAndObject(input);
    if (object instanceof SomeClass) {
       // ...
    }

如果类是已知的并且对象可能为null

    kryo.writeObjectOrNull(output, someObject);
    // ...
    SomeClass someObject = kryo.readObjectOrNull(input, SomeClass.class);

如果类是已知的并且对象不可能为null

    kryo.writeObject(output, someObject);
    // ...
    SomeClass someObject = kryo.readObject(input, SomeClass.class);

引用

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

    Kryo kryo = new Kryo();
    kryo.setReferences(false);
    // ...

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

创建对象

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

上一篇: 分布式系统开发工具包 —— Kafka的消息投递语义
下一篇: 微服务与Spring Cloud
目录