Kryo的基础使用

在项目中使用Kryo来做序列化,首先我们需要引入相关的依赖,如下:

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

主要注意的是,Kryo作为一个快速序列化/反序列化工具,其使用了字节码生成机制(底层依赖了 ASM 库),因此具有比较好的运行速度。但是一般我们可能会在Spring框架中使用,而Spring底层也用了这个库 。但是如果Kryo 使用的版本比较高;而 Spring 用的版本较低; 如果 pom 里的 Kryo 和 Spring 的顺序不对的话,Kryo 就会读到低版本的 ASM ,就会出错。

这里加了个 shaded 就能解决了,如下:

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



然后我们就来看一看Kryo最最基础的使用方法,其实大致和我们使用JDK自带的序列化步骤差不多,在 对象流——ObjectOutputStream和ObjectInputSteam 中,我们就介绍了相关的步骤,这里我们就来看看使用Kryo如何实现。


这里我们还是用之前的Dog对象来进行测试,该Bean的内容如下,这里就不是必须要求实现我们的Serializable 接口啦

public class Dog {

    private String name;
    private int age;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

然后我们来看一看,如何利用Kryo来将对象写入文件之中

public class KryoTest {
    public static void main(String[] args) throws FileNotFoundException {
        Dog dog = new Dog("金毛", 2);
        File file = new File("F:\\demo\\dog.bin");

        try (Output output = new Output(new FileOutputStream(file))) {
            Kryo kryo = new Kryo();
            kryo.writeObject(output, dog);
        }
    }
}

是不是很简单,那么我们如何将刚刚写入文件中的对象在读取出来呢?这里就需要利用与Output对于的Input了,如下:

public class KryoTest {
    public static void main(String[] args) throws FileNotFoundException {
        File file = new File("F:\\demo\\dog.bin");

        try(Input input = new Input(new FileInputStream(file))){
            Kryo kryo = new Kryo();
            Dog dog = kryo.readObject(input, Dog.class);
            System.out.println(dog.toString());
        }
    }
}

其实Kryo大体有三种序列化方法,每种方式都有其优势和劣势。上述就是第一种,也是最为简单的一种,就是writeObject/readObject ,这种方法只会序列化对象实例,而不会记录对象所属类的任何信息。优势是最节省空间,劣势是在反序列化时,需要提供类作为模板才能顺利反序列。如上述,我们在反序列化时,就提供了Dog.class进行反序列化。


第二种就是使用 writeClassAndObject ,这种方法会先将对象所属类的全限定名序列化,然后再依次序列化对象实例的成员。优势是完全动态序列化,整个Kryo周期都不需要提供类信息。反序列化时使用 readClassAndObject 。这算是kryo的一个特点,可以把对象信息直接写到序列化数据里,反序列化的时候可以精确地找到原始类信息,不会出错,这意味着在写readClassAndObject 方法时,无需传入Class或Type类信息。


第三种也是使用 writeClassAndObject ,但是我们在这之前会先进行注册,事先将需要序列化的类注册给 Kryo(此时类和唯一id绑定),之后使用 writeClassAndObject 序列化时,只会序列化注册id,而不会序列化类的全限定名了,这样大大节省了空间(通常只比writeObject多一个字节)。反序列化时使用readClassAndObject 。但是特别需要注意序列化和反序列的Kryo的注册信息应当保持一致。




从上述三种方式来看的话,肯定是第三种采用类注册的writeClassAndObject 的方法最为理想,但是对类进行注册。注册行为会给每一个 Class 编一个号码,从 0 开始,然后注意序列化和反序列的Kryo的注册信息应当保持一致。而有时 Kryo 并不保证同一个 Class 每一次的注册的号码都相同(比如重启 JVM 后,用户访问资源的顺序不同,就会导致类注册的先后顺序不同);还有的情况就是同样的Class在不同的机器上注册编号任然不能保证一致,所以多机器部署时候反序列化可能会出现问题。所以kryo默认会禁止类注册,当然如果想要打开这个属性,可以通过 kryo.setRegistrationRequired(true); 打开。


还有需要注意的就是 Kryo 对循环引用的支持。References即引用,对A对象序列化时,默认情况下 Kryo会在每个成员对象第一次序列化时写入一个数字,该数字逻辑上就代表了对该成员对象的引用,如果后续有引用指向该成员对象,则直接序列化之前存入的数字即可,而不需要再次序列化对象本身。

而 “循环引用” 是指,假设有一个 JavaBean,假设是一个销售订单(SalesOrder),这个订单下面有很多子订单,比如 List<SalesOrderLine> ,而销售子订单中又有其中一个包括一个销售订单,那么这就构成了"循环引用"。Kryo 默认是支持循环引用的,当你确定不会有循环引用发生的时候,可以通过 kryo.setReferences(false); 关闭循环引用检测,从而提高一些性能。关闭后虽然序列化速度更快,但是遇到循环引用,就会报 “栈内存溢出” 错误。



如我们采用类注册的方式,我们可以注册一些常用的类,然后可以作为一个Kryo的工厂类可以用来直接使用,如下:

public class KryoFactory {

    public static Kryo createKryo() {

        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);
        kryo.register(Arrays.asList("").getClass(), new ArraysAsListSerializer());
        kryo.register(GregorianCalendar.class, new GregorianCalendarSerializer());
        kryo.register(InvocationHandler.class, new JdkProxySerializer());
        kryo.register(BigDecimal.class, new DefaultSerializers.BigDecimalSerializer());
        kryo.register(BigInteger.class, new DefaultSerializers.BigIntegerSerializer());
        kryo.register(Pattern.class, new RegexSerializer());
        kryo.register(BitSet.class, new BitSetSerializer());
        kryo.register(URI.class, new URISerializer());
        kryo.register(UUID.class, new UUIDSerializer());
        UnmodifiableCollectionsSerializer.registerSerializers(kryo);
        SynchronizedCollectionsSerializer.registerSerializers(kryo);

        kryo.register(HashMap.class);
        kryo.register(ArrayList.class);
        kryo.register(LinkedList.class);
        kryo.register(HashSet.class);
        kryo.register(TreeSet.class);
        kryo.register(Hashtable.class);
        kryo.register(Date.class);
        kryo.register(Calendar.class);
        kryo.register(ConcurrentHashMap.class);
        kryo.register(SimpleDateFormat.class);
        kryo.register(GregorianCalendar.class);
        kryo.register(Vector.class);
        kryo.register(BitSet.class);
        kryo.register(StringBuffer.class);
        kryo.register(StringBuilder.class);
        kryo.register(Object.class);
        kryo.register(Object[].class);
        kryo.register(String[].class);
        kryo.register(byte[].class);
        kryo.register(char[].class);
        kryo.register(int[].class);
        kryo.register(float[].class);
        kryo.register(double[].class);

        return kryo;
    }
}

上述我们使用了UnmodifiableCollectionsSerializer 和 SynchronizedCollectionsSerializer
在这里插入图片描述
这里是因为 Kryo 在序列化时如果是不可修改的类默认是有问题的,以及默认的序列化器不支持java.util.Collections$SynchronizedRandomAccessList 这种没有无参构造函数的类,可能会抛出异常。但是使用上述的方法,我们需要引入下面的依赖

<dependency>
    <groupId>de.javakaffee</groupId>
    <artifactId>kryo-serializers</artifactId>
    <version>0.42</version>
</dependency>

然后我们在使用Kryo做序列化的时候,就可以直接使用上述的工厂类获取Kryo实例进行处理的

public class KryoSerializer {

    private static Kryo kryo = KryoFactory.createKryo();

    public static void serialize() throws IOException {
        Dog dog = new Dog("金毛", 2);
        File file = new File("F:\\demo\\dog.bin");
        try(Output output = new Output(new FileOutputStream(file))){
            kryo.writeClassAndObject(output, dog);
        }
    }

    public static void deserialize() throws FileNotFoundException {
        File file = new File("F:\\demo\\dog.bin");
        try(Input input = new Input(new FileInputStream(file))){
            Object object = kryo.readClassAndObject(input);
            Dog dog = (Dog) object;
            System.out.println(dog.toString());
        }
    }

    public static void main(String[] args) throws IOException {
        serialize();
        deserialize();
    }
}

上述我们只是简单的演示写入、读取文件,一般在项目中,我们可以使用 ByteArrayOutputStream 来处理,不过需要注意的是,无论是哪种方式,我们需要明确一点,就是Output对象默认的缓存字节数并不大,实际对象超出大小的时候,系列化的时候并不会报告错误,但是系列化结果已经不完整,从而就会导致反系列化的时候失败,所以我们有时需要特意的指定更大的bufferSize,如下:
在这里插入图片描述

public class KryoSerializer {

    private static Kryo kryo = KryoFactory.createKryo();

    public static byte[] serialize(Object object) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Output output = new Output(baos, 8192)) {

            kryo.writeClassAndObject(output, object);
            output.flush();

            byte[] bytes = baos.toByteArray();
            baos.flush();

            return bytes;
        }
    }

    public static Object deserialize(byte[] bytes) throws IOException {
        try (ByteArrayInputStream bios = new ByteArrayInputStream(bytes);
             Input input = new Input(bios)) {

            return kryo.readClassAndObject(input);
        }
    }
}



上述我们说了这么久的Kryo,其实还有一个比较重要的问题,就是Kryo其本身是线程不安全的,所以我们在并发编程时,需要特别的注意这一点,一般我们有两种较为常用的方法,其中一种就是使用 Kryo 为我们提供的 Pool,其用法如下:

public class KryoTest {

    public KryoPool newKryoPool() {
        return new KryoPool.Builder(() -> {
            final Kryo kryo = new Kryo();
            kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
            return kryo;
        }).softReferences().build();
    }

    public static void main(String[] args) {
        KryoTest kryoTest = new KryoTest();
        KryoPool pool = kryoTest.newKryoPool();
        Kryo kryo = pool.borrow();
        //相关序列化与反序列化操作
        ...
        pool.release(kryo);
    }
}

上述指定的默认 StdInstantiatorStrategy ,是为了解决可能由于实例化器指定的问题而抛出空指针异常。

InstantiatorStrategy即初始化策略,默认kryo在反序列化对象时需要对象的类有一个零参数构造器,该构造器可以是private的,Kryo通过反射调用该构造器来实例化对象。如果没有这样一个构造器,就需要使用kryo.setInstantiatorStrategy(new StdInstantiatorStrategy())了,该策略通过JVM API创建对象,这会创建一个完全空的对象(即不执行任何代码中的初始化工作),如果对象在实例化时需要一些初始化操作(比如在构造代码块中执行一些计算逻辑),这种策略就不可行了。

比较好的策略是kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy())); 即先尝试通过零参数构造实例化对象,如果类中没有零参数构造器,则会使用StdInstantiatorStrategy策略。

StdInstantiatorStrategy 在创建对象时是依据JVM version信息及JVM vendor信息进行的,而不是依据Class的具体实现,其可以不调用对象的任何构造方法创建对象。




还有另一种方式就是通过我们的 ThreadLocal 来实现,有关ThreadLocal在并发编程中也进行介绍过
在这里插入图片描述


在我们实际的使用中,我们有时不仅要求将其序列化为二进制数据,可能有时我们希望得到的结果是字符串,那么我们只需要在Kryo序列化后,在使用Base64进行编码即可;在反序列化的时候,同样如此,首先先试用Base64,然后在使用Kryo进行反序列化,使用Base64我们需要引入相关的依赖,如下:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.13</version>
</dependency>

利用上述我们所介绍的,下面就是一个我们常见的Kryo应用的工具类的,如下:

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

            //支持对象循环引用(否则会栈溢出),其默认值为true
            kryo.setReferences(true);

            //默认值就是false
            kryo.setRegistrationRequired(false);

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

            return kryo;
        }
    };

    /**
     * 获得当前线程的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);
        }
    }
}

发布了286 篇原创文章 · 获赞 12 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/newbie0107/article/details/104620616