Kryo 使用指南

1Kryo 的简介

Kryo 是一个快速序列化/反序列化工具,其使用了字节码生成机制(底层依赖了 ASM 库),因此具有比较好的运行速度。

Kryo 序列化出来的结果,是其自定义的、独有的一种格式,不再是 JSON 或者其他现有的通用格式;而且,其序列化出来的结果是二进制的(即 byte[];而 JSON 本质上是字符串 String);二进制数据显然体积更小,序列化、反序列化时的速度也更快。

Kryo 一般只用来进行序列化(然后作为缓存,或者落地到存储设备之中)、反序列化,而不用于在多个系统、甚至多种语言间进行数据交换 —— 目前 kryo 也只有 java 实现。

像 Redis 这样的存储工具,是可以安全地存储二进制数据的,所以可以直接把 Kryo 序列化出来的数据存进去。

当然,如果你希望用 String 的形式存储、传输 Kryo 序列化之后的数据,也可以通过 Base64 等编码方式来实现。但这会降低程序的运行速度,一定程度上违背了使用 kryo 的初衷。

Kryo 在使用时,需要根据使用场景进行一定的设置;如果设置不当,会导致一些严重的错误。(这些问题的原因参见第 2 节)

附件中提供了我们部门封装的 KryoUtil ,其根据分布式 Web 应用的一般场景,进行了配置及封装;可以在自己的项目里安全地使用此工具类。

2Kryo 的特点和配置的选择

2.1 支持的范围

除了常见的 JDK 类型、以及这些类型组合而来的普通 POJO,Kryo 还支持以下特殊情况:

  • 枚举;
  • 任意 Collention、数组;
  • 子类/多态(详见 2.2 节);
  • 循环引用(详见 2.5 节);
  • 内部类(详见 2.6 节);
  • 泛型对象(详见 3.2 节);
  • Builder 模式;

其中部分特性的支持,需要使用者手动设定 Kryo 的某些配置(KryoUtil 已经进行了这些配置)。

Kryo 不支持以下情况:

  • 增加或删除 Bean 中的字段;

举例来说,某一个 Bean 使用 Kryo 序列化后,结果被放到 Redis 里做了缓存,如果某次上线增加/删除了这个 Bean 中的一个字段,则缓存中的数据进行反序列化时会报错;作为缓存功能的开发者,此时应该 catch 住异常,清除这条缓存,然后返回 “缓存未命中” 信息给上层调用者。

字段顺序的变化不会导致反序列化失败。

2.2 记录类型/对多态的支持

Kryo 的一大特点是,支持把对象的类型信息,也放进序列化的结果里。

举例来说:假设我们有一个自己定义的接口 WeightList<T>,有两个实现:ArrayWeightList<T> 和 LinkedWeightList<T>;一般的 JSON 序列化工具,在默认情况下无法记录我们使用的是哪一个实现类;如果不进行特殊的配置,JSON 序列化工具在进行反序列化时会报错。

而 Kryo 将原始对象的类型信息,记录到了序列化的结果里;所以反序列的时候可以精确地找到原始的类型,不会报错。

同时,在反序列化任意对象时,也不再需要再提供 Class 信息或者 Type 信息了,代码也更为简洁、通用。(可以参考第 5 节中的例子)

如果选择记录类型信息,则使用 kryo 中的 writeClassAndObject/readClassAndObject 方法,如果选择不记录类型信息(反序列化时由调用方提供类型信息),则使用 writeObject/readObject 方法。

2.3 线程安全

Kryo 对象不是线程安全的,所以需要借用 ThreadLocal 来保证线程安全性。具体实现可以参考附件中的 KryoUtil。

如果对性能有更高要求,也可以使用 KryoPool:https://github.com/EsotericSoftware/kryo#threading

2.4 注册行为

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

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

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

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

关闭注册行为,需要保证没有进行过这样的设置:

kryo.setRegistrationRequired(true);

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

kryo.register(ArrayList.class);

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

2.5 对循环引用的支持

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

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

kryo.setReferences(false);

配置成 false 的话,序列化速度更快,但是遇到循环引用,就会报 “栈内存溢出” 错误。这有很大的风险:等你不得不支持循环引用的那一天你就会发现,你必须在代码上线的同时,清除 Redis 里已有的大量缓存(详见 2.8 节)。

2.6 内部类

Kryo 支持静态内部类,既可以是私有/包级私有的,也可以是 public 的;但是对非静态内部类的支持不够好(一般不会报错,但在有些情况下会产生错误的数据),这和不同的编译器对内部类的处理有关(可参阅 Java 内部类的语法糖机制)。同样地,Kryo 支持 Builder 模式。

Kryo 不支持匿名类,反序列化时往往会产生错误的数据(这比报错更加危险),请尽量不要使用匿名类传递数据。

2.7 序列化格式

Kryo 实际上支持任意的序列化格式,并不一定使用 Kryo 自己定义的那种特殊的格式(甚至可以为不同的 class 指定不同的序列化格式),比如使用 Java 语言自己的序列化格式(在 Kryo 中注册 JavaSerializer 即可) —— 但我们强烈建议不要这么使用,Java 语言本身的序列化方式有很多限制,比如必须要保证每一个 Bean 都实现 Serializable 接口;而系统中可能有很多 Bean 都忘了实现这个接口;这些类在编译时并不会报错,只有在运行期间、进行序列化时才会报错,这是危险的。

Kryo 默认的序列化格式没有任何限制,显然方便的多。

2.8 配置的修改

Kryo 可以通过修改配置来达到更快的速度,或者支持更多的特殊形式;但是必须注意的是,一旦改变某一个个配置,序列化出来的格式和之前的格式是完全不一样的; 也就是说,你必须在上线代码的同时,清除 Redis 里所有已有的缓存,否则那些缓存里的数据再回来进行反序列化的时候,就会报错。

3、常见问题

3.1 使用的时候报 asm 相关类的错误

Kryo 底层用了 asm 库(一个字节码生成库),Spring 底层也用了这个库 ;但是,Kryo 使用的版本比较高;而 Spring 用的版本较低; 如果 pom 里的 Kryo 和 Spring 的顺序不对的话,Kryo 就会读到低版本的 asm,就会出错。

请检查对 Kryo 的 Maven 依赖,如果 artifactId 是这样的:

<artifactId>kryo</artifactId>:

就改为:

<artifactId>kryo-shaded</artifactId>

加了个 shaded 就能解决了。在这个 shaded 的版本里,Kryo 的作者复制了一份高版本的 asm,集成到了 Kryo 内部(作者修改了 asm 类的包名,所以和原来的 asm 就不会再冲突了)。

3.2 泛型对象的反序列化

在使用常见的 JSON 库时,泛型对象不能使用 *.class 进行反序列化;比如 Gson 在反序列化 List<SomeDomain> 的时候,除了传入 JSON 字符串,还需要传入第二个参数:

new TypeToken<List<SomeDomain>>(){}.getType()

直接使用 List.class 是不行的(而 List<SomeDomain>.class 则是语法错误),这是 Java 泛型的 “擦除” 机制导致的。

而 Kryo 的 readObject 方法则没有这个问题。在上例中,向 Kryo 的 readObject 方法传入 List.class 即可;Kryo 实际上在序列化结果里记录了泛型参数的实际类型的信息,反序列化时会根据这些信息来实例化对象。

直觉上我们会觉得,不在序列化结果中包含类型信息,能减小空间的占用、提高速度;但实际上,我们发现,所谓的 “不包含类型信息”,在 Kryo 内部的实现里,仅仅是 “不包含最外层对象的类型信息” ,对象内部的子对象的类型信息依然是包含的(可能是为了支持多态问题);也就是说,“不包含类型信息” 能带来的空间节省非常有限。

如果对速度、序列化之后的数据大小没有特别极端的要求,推荐在序列化结果中包含类型信息,这样的话,反序列化时能少些一个参数,也更为通用。

4、使用 Kryo 需要添加的 Maven 依赖

<!-- Kryo -->

<dependency>

    <groupId>com.esotericsoftware</groupId>

    <artifactId>kryo-shaded</artifactId>

    <version>4.0.0</version>

</dependency>

如果使用 KryoUtil 的话,还需要以下依赖:

<!-- commons-codec -->

<dependency>

    <groupId>commons-codec</groupId>

    <artifactId>commons-codec</artifactId>

    <version>1.10</version>

</dependency>

5KryoUtil 使用示例

KryoUtil 是我们部门编写的工具类,其对 Kryo 进行了一定的封装,能够满足分布式系统的一般需求,而无需进行任何额外的配置。

除了用于获得当前线程的 kryo 实例的 getInstance() 方法之外,KryoUtil 内共有 8 个 public 方法,分为两组:

<T> byte[] writeToByteArray(T obj);

<T> String writeToString(T obj);

<T> T readFromByteArray(byte[] byteArray);

<T> T readFromString(String str);

及:

<T> byte[] writeObjectToByteArray(T obj)

<T> String writeObjectToString(T obj)

<T> T readObjectFromByteArray(byte[] byteArray, Class<T> clazz)

<T> T readObjectFromString(String str, Class<T> clazz)

其中第一组序列化的结果里包含了类型信息,第二组不包含 —— 因此,可以看到,在使用第二组方法进行反序列化的时候,需要提供原始对象的 Class 。但我们建议使用第一组方法,原因见第 3.2 节。

另外,必须注意,第一组方法和第二组方法不能混用,第一组序列化出来的结果,只能由第一组的方法进行反序列化;第二组亦然。

每组方法内,序列化的结果格式都可以选择二进制格式,或者字符串格式。具体的使用示例代码如下:

将任意对象序列化成 byte[]:

byte[] tempByteArray = KryoUtil.writeToByteArray(domainA);

//tempByteArray 就是序列化的结果,直接放到 Redis 里面即可

 

DomainA domainA1 = KryoUtil.readFromByteArray(tempByteArray);

//domainA1 就是反序列化之后的对象

如果你们的存储服务不支持二进制数据(或者说不是 “二进制安全” 的),那么也可以序列化成 String:

String tempStr = KryoUtil.writeToString(domainA);

//tempStr 就是序列化的结果

 

DomainA domainA1 = KryoUtil.readFromString(tempStr);

//domainA1 就是反序列化之后的对象

附 KryoUtil.java 如下

package com.jd.personal.hanwenyang5.util;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.apache.commons.codec.binary.Base64;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.*;

/**
 * Kryo Utils
 * <p/>
 */
public class KryoUtil {

    private static final String DEFAULT_ENCODING = "UTF-8";

    //每个线程的 Kryo 实例
    private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {
        @Override
        protected Kryo initialValue() {
            Kryo kryo = new Kryo();

            /**
             * 不要轻易改变这里的配置!更改之后,序列化的格式就会发生变化,
             * 上线的同时就必须清除 Redis 里的所有缓存,
             * 否则那些缓存再回来反序列化的时候,就会报错
             */
            //支持对象循环引用(否则会栈溢出)
            kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置

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

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

            return kryo;
        }
    };

    /**
     * 获得当前线程的 Kryo 实例
     *
     * @return 当前线程的 Kryo 实例
     */
    public static Kryo getInstance() {
        return kryoLocal.get();
    }

    //-----------------------------------------------
    //          序列化/反序列化对象,及类型信息
    //          序列化的结果里,包含类型的信息
    //          反序列化时不再需要提供类型
    //-----------------------------------------------

    /**
     * 将对象【及类型】序列化为字节数组
     *
     * @param obj 任意对象
     * @param <T> 对象的类型
     * @return 序列化后的字节数组
     */
    public static <T> byte[] writeToByteArray(T obj) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Output output = new Output(byteArrayOutputStream);

        Kryo kryo = getInstance();
        kryo.writeClassAndObject(output, obj);
        output.flush();

        return byteArrayOutputStream.toByteArray();
    }

    /**
     * 将对象【及类型】序列化为 String
     * 利用了 Base64 编码
     *
     * @param obj 任意对象
     * @param <T> 对象的类型
     * @return 序列化后的字符串
     */
    public static <T> String writeToString(T obj) {
        try {
            return new String(Base64.encodeBase64(writeToByteArray(obj)), DEFAULT_ENCODING);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * 将字节数组反序列化为原对象
     *
     * @param byteArray writeToByteArray 方法序列化后的字节数组
     * @param <T>       原对象的类型
     * @return 原对象
     */
    @SuppressWarnings("unchecked")
    public static <T> T readFromByteArray(byte[] byteArray) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
        Input input = new Input(byteArrayInputStream);

        Kryo kryo = getInstance();
        return (T) kryo.readClassAndObject(input);
    }

    /**
     * 将 String 反序列化为原对象
     * 利用了 Base64 编码
     *
     * @param str writeToString 方法序列化后的字符串
     * @param <T> 原对象的类型
     * @return 原对象
     */
    public static <T> T readFromString(String str) {
        try {
            return readFromByteArray(Base64.decodeBase64(str.getBytes(DEFAULT_ENCODING)));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    //-----------------------------------------------
    //          只序列化/反序列化对象
    //          序列化的结果里,不包含类型的信息
    //-----------------------------------------------

    /**
     * 将对象序列化为字节数组
     *
     * @param obj 任意对象
     * @param <T> 对象的类型
     * @return 序列化后的字节数组
     */
    public static <T> byte[] writeObjectToByteArray(T obj) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Output output = new Output(byteArrayOutputStream);

        Kryo kryo = getInstance();
        kryo.writeObject(output, obj);
        output.flush();

        return byteArrayOutputStream.toByteArray();
    }

    /**
     * 将对象序列化为 String
     * 利用了 Base64 编码
     *
     * @param obj 任意对象
     * @param <T> 对象的类型
     * @return 序列化后的字符串
     */
    public static <T> String writeObjectToString(T obj) {
        try {
            return new String(Base64.encodeBase64(writeObjectToByteArray(obj)), DEFAULT_ENCODING);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * 将字节数组反序列化为原对象
     *
     * @param byteArray writeToByteArray 方法序列化后的字节数组
     * @param clazz     原对象的 Class
     * @param <T>       原对象的类型
     * @return 原对象
     */
    @SuppressWarnings("unchecked")
    public static <T> T readObjectFromByteArray(byte[] byteArray, Class<T> clazz) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
        Input input = new Input(byteArrayInputStream);

        Kryo kryo = getInstance();
        return kryo.readObject(input, clazz);
    }

    /**
     * 将 String 反序列化为原对象
     * 利用了 Base64 编码
     *
     * @param str   writeToString 方法序列化后的字符串
     * @param clazz 原对象的 Class
     * @param <T>   原对象的类型
     * @return 原对象
     */
    public static <T> T readObjectFromString(String str, Class<T> clazz) {
        try {
            return readObjectFromByteArray(Base64.decodeBase64(str.getBytes(DEFAULT_ENCODING)), clazz);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }
}

  

posted @ 2017-07-05 17:52 hntyzgn 阅读(...) 评论(...) 编辑 收藏