Kryo官方文档

https://blog.csdn.net/fanjunjaden/article/details/72823866

Kryo是一种快速高效的Java对象图(Object graph)序列化框架。 该项目的目标是速度、效率和易于使用的API。 当对象需要持久化时,无论是用于文件、数据库还是通过网络,该项目都很有用。

Kryo还可以执行自动深层浅层的复制/克隆。这是从对象直接复制到对象,而不是object -> bytes -> object。

GitHub地址:https://github.com/EsotericSoftware/kryo

Installation(安装)

 Kryo JAR 可在发布页面和 Maven Central 上找到。 Kryo 的最新快照,包括 master 的快照构建,都在 Sonatype Repository 中。

Integration with Maven(与maven集成)

要使用Kryo的官方版本,请在 pom.xml 中使用以下代码段

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

如果您因为在类路径中使用了不同版本的 asm 而遇到问题,可以使用包含这个 asm 版本的 kryo-shaded jar,并将其重新放置在不同的包中:

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

如果要测试 Kryo 的最新快照,请在 pom.xml 中使用以下代码片段

<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.2</version>
</dependency>

快速开始

官网中一个类库的使用

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import java.io.*;

public class HelloKryo {
   static public void main (String[] args) throws Exception {
      Kryo kryo = new Kryo();
      kryo.register(SomeClass.class);

      SomeClass object = new SomeClass();
      object.value = "Hello Kryo!";

      Output output = new Output(new FileOutputStream("file.bin"));
      kryo.writeObject(output, object);
      output.close();

      Input input = new Input(new FileInputStream("file.bin"));
      SomeClass object2 = kryo.readObject(input, SomeClass.class);
      input.close();   
   }
   static public class SomeClass {
      String value;
   }
}

Kryo  自动执行序列化。  Output 和 Input 处理缓冲字节,并可选地冲向流(stream);

IO

Kryo 使用OutPut和Input类完成数据输入和输出。 .但是这两个类不是线程安全的。

Output 类是将数据写入字节数组 buffer 的 OutputStream。如果需要字节数组,则可以直接获取和使用该 buffer 。如果已经给定了OutputStream,当 buffer 满时,它将刷新 bytes 到 stream。Output有许多方法可以有效地将基本类型和字符串写为 bytes 。它提供类似于DataOutputStream,BufferedOutputStream,FilterOutputStream 和 ByteArrayOutputStream 类的功能。

在写入 OutputStream 时 buffer 后,请确保调用 flush() 或 close(),以便将缓冲的字节写入底层流。

Input 类是从字节数组缓冲区读取数据的 InputStream 类。如果需要从字节数组中读取,可以直接设置该缓冲区 (buffer) 。如果给定了InputStream,当缓冲区用尽时,它将从流中填充缓冲区。Input 有许多方法可以高效地从 bytes 读取基本类型和字符串。它提供类似于DataInputStream,BufferedInputStream,FilterInputStream 和 ByteArrayInputStream 类的功能。

如果要读写字节数组 (bytes)以外的对象,只需提供相应的 InputStream 或 OutputStream 即可。 

Serializers(序列化) 

Kryo 是一个序列化框架。它不强制要求 schema,也不关心读写的是什么数据。这些是留给序列化器本身的工作。默认提供的序列化器(Serializers)以不同的方式读写数据。可以部分或全部替换掉他们去满足特定的需求。默认提供的 serializers 可以读取和写入大多数对象,此外写入一个新的 serializer 也很容易。 Serializer 抽象类定义了从对象到 bytes 和从 bytes 到对象的方法。
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 有两种可以实现的方法。 write() 将该对象写入字节 (bytes)。 read() 创建对象的新实例,并用输入数据填充对象。

Kryo 实例可用于写入和读取嵌套对象。如果 Kryo 用于读取 read() 中的嵌套对象,且如果嵌套对象可以引用父对象,则必须先调用 kryo.reference() 引用父对象。如果嵌套对象不能引用父对象,或 Kryo 不被用于嵌套对象,或者没有使用引用,则没有必要调用 kryo.reference() 。如果嵌套对象可以使用相同的serializer,那么 serializer 必须是可重入的。

代码不应该直接使用 serializers,而应该使用 Kryo 的读写方法。这将使 Kryo 来协调序列化并处理诸如引用和空对象的特征。

默认情况下,serializers 不需要处理为空的对象。 Kryo 框架将根据需要写一个字节,以表示 null 或非 null。如果一个 serializers 想要更高效地自己来处理 nulls,可以设置 Serializer#setAcceptsNull(true)。这个设置也可以在已知类型的所有实例永不为空时,来避免写入空标识字节。 

Register(注册)

 当Kryo写出一个对象的实例时,首先可能需要写出一些标识对象类的东西。默认情况下,写入完整类名,然后写入该对象的字节。后续出现的同一类对象图的对象用变长的int来写(using a variable length int)。写类的名字有点低效,所以类可以事先注册:
    Kryo kryo = new Kryo();
    kryo.register(SomeClass.class);
    // ...
    Output output = ...
    SomeClass someObject = ...
    kryo.writeObject(output, someObject);

这里,SomeClass 注册到了 Kryo,它将该类与一个 int 型的 ID 相关联。当 Kryo 写出 SomeClass 的一个实例时,它会写出这个 int ID。这比写出类名更有效。在反序列化期间,注册的类必须具有序列化期间相同的 ID 。上面展示的注册方法分配下一个可用的最小整数 ID,这意味着类被注册的顺序十分重要。注册时也可以明确指定特定 ID,这样的话注册顺序就不重要了:

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

当 IDs 是小的正整数时最有效。负数不能有效地序列化。 -1 和 -2 是保留值。<<译者注:-1 和-2 有其他含义>>

可以混合使用注册和未注册的类。默认使用 ID 0-9 注册所有基本类型,基本类包装器,String 和 void。所以要小心此范围内的注册覆盖的情况。

当 Kryo#setRegistrationRequired 设置为true,可在遇到任何未注册的类时抛出异常。这能阻止应用程序使用类名字符串来序列化。

如果使用未注册的类,则应考虑使用较短的包名。


注册行为会给每一个 Class 编一个号码,从 0 开始;但是,Kryo 并不保证同一个 Class 每一次的注册的号码都相同(比如重启 JVM 后,用户访问资源的顺序不同,就会导致类注册的先后顺序不同)。

也就是说,同样的代码、同一个 Class ,在两台机器上的注册编号可能不一致;那么,一台机器序列化之后的结果,可能就无法在另一台机器上反序列化。

因此,对于多机器部署的情况,建议关闭注册,让 Kryo 记录每个类型的真实的名称

而且,注册行为需要用户对每一个类进行手动注册:即便使用者注册了 A 类型,而 A 类型内部使用了 B 类型,使用者也必须手动注册 B 类型;(甚至,即便某一个类型是 JDK 内部的类型,比如 ArrayList ,也是需要手动注册的)一个普通的业务对象,往往需要注册十几个 Class,这是十分麻烦、甚至是寸步难行的。

关闭注册行为,不能有下面的代码设置:

kryo.setRegistrationRequired(true); // 默认为false

并且要保证不要显式地注册任何一个类,例如:

kryo.register(ArrayList.class);

同时保证以上二者,才真正地关闭了注册行为。

Default serializers(默认序列化器)

 写入类标识符后,Kryo使用一个序列化器(serializer) 来写入对象的字节。当类被注册后,serializer 实例就能被确定了:
    Kryo kryo = new Kryo();
    kryo.register(SomeClass.class, new SomeSerializer());
    kryo.register(AnotherClass.class, new AnotherSerializer());

如果一个类未注册或没有指定序列化器,则会自动从映射了类和序列化器的“ 默认序列化器 (default serializers)”列表中选择一个序列化器

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

References(引用) 

默认情况下,图中每个对象从第二个开始的表象都以整数顺序存储。这种方式可以序列化相同对象和循环图的多个引用。它具有少量的开销,如果不需要,可以禁用以节省空间:

Kryo kryo = new Kryo();
kryo.setReferences(false); //支持对象循环引用(否则会栈溢出),默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置

//不强制要求注册类(注册行为无法保证多个 JVM 内同一个类的注册编号相同;而且业务系统中大量的 Class 也难以一一注册)
kryo.setRegistrationRequired(false); //默认值就是 false,添加此行的目的是为了提醒维护者,不要改变这个配置

//Fix the NPE bug when deserializing Collections.
((Kryo.DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy()).setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());

当使用 Kryo 作为嵌套对象的序列化器时,必须在 read() 中调用 kryo.reference()。有关详细信息,请参阅 Serializers。 

循环引用

举例而言,“循环引用” 是指,假设有一个 “账单” 的 Bean(比如:BillDomain),这个账单下面有很多明细(比如:private List<ItemDomain> items;),而明细类中又有一个字段引用了所属的账单(比如:private BillDomain superior;),那么这就构成了“循环引用”。

Kryo 是支持循环引用的,只需要保证没有进行过这样的设置就可以了:

kryo.setReferences(false);

配置成 false 的话,序列化速度更快,但是遇到循环引用,就会报 “栈内存溢出” 错误

Object creation(对象创建)

 特定类型的序列化器使用 Java 代码创建该类型的新实例。序列化器如 FieldSerializer 是泛型的,用于处理创建任何类的新实例。默认情况下,如果某个类有一个无参构造方法,那么它将通过 ReflectASM 或反射来调用,否则抛出异常。如果无参构造方法是私有的,则尝试通过setAccessible用反射来访问它。这样的过程,可以使 Kryo 在不影响公共API的情况下创建类的实例。

当不能使用 ReflectASM 或反射时,可以配置 Kryo 使用 InstantiatorStrategy 来创建类的实例。Objenesis 提供 StdInstantiatorStrategy,它使用JVM 特定的 API 来创建类的实例,而不会调用任何构造方法。虽然这适用于许多 JVM,但无参构造方法的移植性更好。

    kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());

请注意,需要设计类来按下述的方式创建。如果期望调用一个类的构造函数,那么当通过这种机制创建时,它可能处于未初始化的状态。

在许多情况下,您可能希望有这样的策略:Kryo 首先尝试使用无参构造方法,如果尝试失败,再尝试使用 StdInstantiatorStrategy 作为后备方案,因为后备方案不需要调用任何构造方法。这种策略的配置可以这样表示:

kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));

然而,这样的默认行为需要一个无参构造方法。

Objenesis 还可以使用Java的内置序列化机制创建新对象。如此的话,类必须实现 java.io.Serializable,且在调用时会执行父类中的第一个无参构造方法。

    kryo.setInstantiatorStrategy(new SerializingInstantiatorStrategy());

您也可以编写自己的 InstantiatorStrategy。

要定制特定类型的创建方式,可以设置一个 ObjectInstantiator。这会覆盖 ReflectASM,反射和 InstantiatorStrategy。

    Registration registration = kryo.register(SomeClass.class);
    registration.setObjectInstantiator(...);

另外,一些序列化器提供重写的方式定制对象的创建。

    kryo.register(SomeClass.class, new FieldSerializer(kryo, SomeClass.class) {
       public Object create (Kryo kryo, Input input, Class type) {
          return new SomeClass("some constructor arguments", 1234);
       }
    }); 

Copying/cloning(复制 / 克隆) 

序列化库需要关于如何创建新实例、获取和设置值、导航对象图等的特定信息。这些几乎是复制对象所需的一些,因此 Kryo 支持自动生成深和浅的对象副本。注意 Kryo 的复制不会序列化为字节然后反转,它使用直接分配。

    Kryo kryo = new Kryo();
    SomeClass someObject = ...
    SomeClass copy1 = kryo.copy(someObject);
    SomeClass copy2 = kryo.copyShallow(someObject);

Serializer 类有一个复制方法可以完成复制工作。如果实现了特定于应用程序的序列化器而不使用复制功能时,可以忽略这些方法。 Kryo 提供的所有序列化器都支持复制。多个对同一个对象的引用和循环引用由框架自动处理。

与 read() Serializer 方法类似,必须先调用 kryo.reference(),然后才能使用 Kryo 来复制子对象。有关详细信息,请参阅序列化器。

类似于 KryoSerializable,类可以实现 KryoCopyable 进行自己的复制:

    public class SomeClass implements KryoCopyable<SomeClass> {
       // ...
    
       public SomeClass copy (Kryo kryo) {
          // Create new instance and copy values from this instance.
       }
    } 

Context(上下文) 

Kryo 有两种上下文方法。 getContext() 返回一个用于存储用户数据的 map。 由于 Kryo 实例可用于所有序列化器,因此此数据可以随时获得。 getGraphContext() 与之类似,但在每个对象图被序列化或反序列化之后被清除。 这样可以方便地管理每个对象图状态。  

Compression and encryption(压缩与加密) 

Kryo 支持流,因此在所有序列化字节上使用压缩或加密是不太必要的: 

    OutputStream outputStream = new DeflaterOutputStream(new FileOutputStream("file.bin"));
    Output output = new Output(outputStream);
    Kryo kryo = new Kryo();
    kryo.writeObject(output, object);
    output.close();

如有需要,可以使用序列化器来压缩或加密对象图字节中的一个部分字节。 例如,请参阅 DeflateSerializer 或 BlowfishSerializer。 这些序列化器包装了另一个序列化器,并对字节进行编码和解码。 

Chunked encoding(分块编码) 

有时先写一些数据的长度,然后再写入数据的机制是很有用的。如果数据长度不能提前知道,则需要缓冲所有数据以确定其长度,然后写入长度,再然后写入数据。这种缓冲能防止流式传输并且潜在地需要非常大的缓冲区(buffer),这并不理想。

分块编码通过使用小的缓冲区(buffer)来解决这个问题。当缓冲区已满时,其长度被写入,然后是数据。这是一个数据块的机制。当多个块时,缓冲区被清除,这样继续直到没有更多的数据写入。长度为零的块表示块的结尾。

Kryo 提供了简单的分块编码的类。 OutputChunked 用于写分块数据。它扩展了 Output,所以有方便的方法来写入数据。当 OutputChunked 缓冲区已满时,它将该块刷新到包装的 OutputStream。 endChunks() 方法用于标记一组块的结尾。

    OutputStream outputStream = new FileOutputStream("file.bin");
    OutputChunked output = new OutputChunked(outputStream, 1024);
    // Write data to output...
    output.endChunks();
    // Write more data to output...
    output.endChunks();
    // Write even more data to output...
    output.close();

如要读取分块数据,使用 InputChunked。它继承了 Input,所以有方法来读取数据。当读取时,InputChunked 将在到达一组块的末尾时作为数据的末尾。nextChunks() 方法前进到下一组块,即使当前块组中的数据并未读取完。

    InputStream outputStream = new FileInputStream("file.bin");
    InputChunked input = new InputChunked(inputStream, 1024);
    // Read data from first set of chunks...
    input.nextChunks();
    // Read data from second set of chunks...
    input.nextChunks();
    // Read data from third set of chunks...
    input.close(); 

Compatibility(兼容性) 

对于某些需求,特别是长期存储序列化后的bytes,序列化如何处理类的变化至关重要。这被称为 forward(读取较新类的序列化生成的字节)和 backword(读取由旧类序列化产生的字节)兼容性。

FieldSerializer 是最常用的 serializer。它是通用的,可以序列化大多数类而无需任何配置。它是高效的,只写字段数据,没有任何额外信息。它不支持添加,删除或更改字段类型,不会使先前的序列化字节无效。大多情况下,这可以接受,例如通过网络发送数据,但是作为长期数据存储,这不是个好方法,因为Java 类不能演化。由于 FieldSerializer 默认尝试读写非 public 字段,因此评估将被序列化的每个类是很重要的工作。

当没有指定序列化程序时,默认情况下使用 FieldSerializer。如有必要,可以使用另一种通用的序列化器:

kryo.setDefaultSerializer(TaggedFieldSerializer.class);

BeanSerializer 非常类似于 FieldSerializer,除了它使用 bean getter 和 setter 方法,而不是直接的字段访问。速度上来说,这稍慢些,但因为它使用公共 API 来配置对象,可能会更安全。

VersionFieldSerializer 扩展了 FieldSerializer,并允许字段具有 @Since(int) 注解来指示它们被添加的版本。对于特定字段,@Since 中的值不应该在创建后改变。这不如 FieldSerializer 那么灵活,后者可以处理大多数类而不需要注解,但前者提供向后兼容性。这意味着可以添加新的字段,但删除,重命名或更改任何字段的类型将使先前的序列化字节失效。与 FieldSerializer 相比,VersionFieldSerializer 具有非常少的开销(一个额外的变量)。

TaggedFieldSerializer 将 FieldSerializer 扩展为仅序列化具有 @Tag(int) 注解的字段,提供向后兼容性,从而可以添加新字段。并且它还通过setIgnoreUnknownTags(true) 提供向前兼容性,因此任何未知的字段 tags 将被忽略。 对比 VersionFieldSerializer,TaggedFieldSerializer 有两个优点:1)字段可以被重命名,2)标记有 @Deprecated 注解的字段,在读取旧字节时或写出新字节时将被忽略。尽管字段和 @Tag 注解必须保留在类中,弃用机制有效地从序列化中删除了废弃的字段。废弃的字段能被设置私有和/或重命名,这样他们不会弄乱类的信息(例如,ignored, ignored2)。基于这些原因,TaggedFieldSerializer 为类的演化提供更多的灵活性。缺点是与 VersionFieldSerializer(每个字段需额外的一个变量)相比,它具有少量额外的开销。

CompatibleFieldSerializer 扩展了 FieldSerializer 以提供向前和向后兼容性,这意味着可以添加或删除字段,而不会使先前的序列化字节无效。它不支持更改字段的类型。像 FieldSerializer 一样,它可以序列化大多数类而不需要注解。前向和后向兼容性有一些代价:在序列化中第一次遇到某个类时,会写入一个包含字段名称字符串的简单 scheme。同时,在序列化和反序列化期间,缓冲区用以执行分块编码。这种机制使得CompatibleFieldSerializer 能够忽略它不认识的字段的字节。当 Kryo 配置为使用引用时,如果某个字段被删除,可能引发CompatibleFieldSerializer 的问题。如果您的类继承层次结构包含相同的命名字段,请使用 CachedFieldNameStrategy.EXTENDED 策略。

class A {
	String a;
}

class B extends A {
	String a;
}
...
// use `EXTENDED` name strategy, otherwise serialized object can't be deserialized correctly. Attention, `EXTENDED` strategy increases the serialized footprint.
kryo.getFieldSerializerConfig().setCachedFieldNameStrategy(FieldSerializer.CachedFieldNameStrategy.EXTENDED);


可以轻松开发额外的序列化器,用于向前和向后兼容性,例如使用外部手写 schema 的序列化器。 

Interoperability(互通性) 

提供的 Kryo 序列化器默认假设将用 Java 反序列化,因此它们不会明确定义写入的格式。序列化器可以使用更容易被其他语言读取的标准格式写入,但默认情况下不提供。 

Stack size(栈大小) 

序列化器 Kryo 在序列化嵌套对象时使用调用堆栈。 Kryo 使用最小的堆栈调用,但对于极深的对象图,可能会发生堆栈溢出。这是大多数序列化库的常见问题,包括内置的 Java 序列化。可以使用 -Xss 增加堆栈大小,但请注意,这项配置作用于所有线程。JVM 中具有多个线程且巨大的堆栈大小会导致占用大量内存。 

Threading(线程)

 Kryo 不是线程安全的。每个线程都应该有自己的 Kryo,Input 和 Output 实例。此外, bytes[] Input 可能被修改,然后在反序列化期间回到初始状态,因此不应该在多线程中并发使用相同的 bytes[]。 

 

Pooling Kryo instances(池化Kryo实例) 

因为 Kryo 实例的创建/初始化是相当昂贵的,所以在多线程的情况下,您应该池化 Kryo 实例。一个非常简单的解决方案是使用 ThreadLocal 将 Kryo实例绑定到 Threads,如下所示:

// Setup ThreadLocal of Kryo instances
private static final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
	protected Kryo initialValue() {
		Kryo kryo = new Kryo();
		// configure kryo instance, customize settings
		return kryo;
	};
};

// Somewhere else, use Kryo
Kryo k = kryos.get();
...

或者您也可以使用 kryo 提供的 KryoPool。 KryoPool 允许使用 SoftReferences 保留对 Kryo 实例的引用,这样当 JVM 开始耗尽内存时,Kryo 实例就可以被 GC 回收(当然你也可以使用 ThreadLocal 和 SoftReferences)。

以下是一个示例,显示如何使用KryoPool:

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.pool.*;

KryoFactory factory = new KryoFactory() {
  public Kryo create () {
    Kryo kryo = new Kryo();
Log.set(1); //记录序列化的详情 // configure kryo instance, customize settings return kryo; } }; // Build pool with SoftReferences enabled (optional) KryoPool pool = new KryoPool.Builder(factory).softReferences().build(); Kryo kryo = pool.borrow(); // do s.th. with kryo here, and afterwards release it pool.release(kryo); // or use a callback to work with kryo - no need to borrow/release, // that's done by `run`. String value = pool.run(new KryoCallback() { public String execute(Kryo kryo) { return kryo.readObject(input, String.class); } });

Logging(日志) 

Kryo利用低开销,轻量级的MinLog日志库。可以通过以下方法之一设置日志记录级别:

    Log.ERROR();
    Log.WARN();
    Log.INFO();
    Log.DEBUG();
    Log.TRACE();

Kryo在INFO(默认)和以上级别没有记录。 DEBUG方便在开发过程中使用。调试一个特定的问题时,TRACE很好用,但是通常输出的信息太多。

MinLog支持固定的日志记录级别,这会导致javac在编译时删除低于该级别的日志记录。在Kryo发行版ZIP中,"debug"JAR启用日志记录。 "production"JAR使用NONE的固定日志记录级别,这意味着所有日志记录代码已被删除。

 
posted @ 2015-09-21 15:11  南极山  阅读(4369)  评论(0)    收藏  举报