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

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM