Java Kryo,Protostuff,Hessian序列化方式對比

前言

前段時間在寫RPC框架的時候用到瞭Kryo、Hessian、Protostuff三種序列化方式。但是當時因為急於實現功能,就隻是簡單的的看瞭一下如何使用這三種序列化方式,並沒有去深入研究各自的特性,以及優點和缺點。知道現在就將RPC框架寫完瞭之後,才有時間靜下心來對三種方式做一個對比,總結。

Kryo、Hessain、Protostuff都是第三方開源的序列化/反序列化框架,要瞭解其各自的特性,我們首先需要知道序列化/反序列化是什麼:

序列化:就是將對象轉化成字節序列的過程。

反序列化:就是講字節序列轉化成對象的過程。

seriallization 序列化 : 將對象轉化為便於傳輸的格式, 常見的序列化格式:二進制格式,字節數組,json字符串,xml字符串。

deseriallization 反序列化:將序列化的數據恢復為對象的過程

如果對序列化相關概念還不是很清楚的同學可以參照美團技術團隊的序列化和反序列化

筆記參考圖:

image-20220702131833644

性能對比

前期準備

  • 我們先創建一個新的Maven項目
  • 然後導入依賴
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.2</version>
    <scope>test</scope>
</dependency>
<!-- 代碼簡化 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>
<!--kryo-->
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo-shaded</artifactId>
    <version>4.0.2</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.10</version>
</dependency>
<!--protostuff-->
<dependency>
    <groupId>io.protostuff</groupId>
    <artifactId>protostuff-core</artifactId>
    <version>1.7.2</version>
</dependency>
<dependency>
    <groupId>io.protostuff</groupId>
    <artifactId>protostuff-runtime</artifactId>
    <version>1.7.2</version>
</dependency>
<!--hessian2-->
<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.62</version>
</dependency>

工具類:

kryo

package cuit.pymjl.utils;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
/**
 * @author Pymjl
 * @version 1.0
 * @date 2022/4/18 20:07
 **/
@SuppressWarnings("all")
public class KryoUtils {
    private static final String DEFAULT_ENCODING = "UTF-8";
​
    //每個線程的 Kryo 實例
    private static final ThreadLocal<Kryo> KRYO_LOCAL = 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 KRYO_LOCAL.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);
        }
    }
}

Hessian

package cuit.pymjl.utils;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * @author Pymjl
 * @version 1.0
 * @date 2022/7/2 12:39
 **/
public class HessianUtils {
    /**
     * 序列化
     *
     * @param obj obj
     * @return {@code byte[]}
     */
    public static byte[] serialize(Object obj) {
        Hessian2Output ho = null;
        ByteArrayOutputStream baos = null;
        try {
            baos = new ByteArrayOutputStream();
            ho = new Hessian2Output(baos);
            ho.writeObject(obj);
            ho.flush();
            return baos.toByteArray();
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException("serialize failed");
        } finally {
            if (null != ho) {
                try {
                    ho.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != baos) {
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /**
     * 反序列化
     *
     * @param bytes 字節
     * @param clazz clazz
     * @return {@code T}
     */
    public static  <T> T deserialize(byte[] bytes, Class<T> clazz) {
        Hessian2Input hi = null;
        ByteArrayInputStream bais = null;
        try {
            bais = new ByteArrayInputStream(bytes);
            hi = new Hessian2Input(bais);
            Object o = hi.readObject();
            return clazz.cast(o);
        } catch (Exception ex) {
            throw new RuntimeException("deserialize failed");
        } finally {
            if (null != hi) {
                try {
                    hi.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != bais) {
                try {
                    bais.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Protostuff

package cuit.pymjl.utils;
import io.protostuff.LinkedBuffer;
import io.protostuff.ProtostuffIOUtil;
import io.protostuff.Schema;
import io.protostuff.runtime.RuntimeSchema;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * @author Pymjl
 * @version 1.0
 * @date 2022/6/28 21:00
 **/
public class ProtostuffUtils {
    /**
     * 避免每次序列化都重新申請Buffer空間
     * 這個字段表示,申請一個內存空間用戶緩存,LinkedBuffer.DEFAULT_BUFFER_SIZE表示申請瞭默認大小的空間512個字節,
     * 我們也可以使用MIN_BUFFER_SIZE,表示256個字節。
     */
    private static final LinkedBuffer BUFFER = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
    /**
     * 緩存Schema
     * 這個字段表示緩存的Schema。那這個Schema是什麼呢?就是一個組織結構,就好比是數據庫中的表、視圖等等這樣的組織機構,
     * 在這裡表示的就是序列化對象的結構。
     */
    private static final Map<Class<?>, Schema<?>> SCHEMA_CACHE = new ConcurrentHashMap<>();
​
    /**
     * 序列化方法,把指定對象序列化成字節數組
     *
     * @param obj 對象
     * @return byte[]
     */
    @SuppressWarnings("unchecked")
    public static <T> byte[] serialize(T obj) {
        Class<T> clazz = (Class<T>) obj.getClass();
        Schema<T> schema = getSchema(clazz);
        byte[] data;
        try {
            data = ProtostuffIOUtil.toByteArray(obj, schema, BUFFER);
        } finally {
            BUFFER.clear();
        }
        return data;
    }
​
    /**
     * 反序列化方法,將字節數組反序列化成指定Class類型
     *
     * @param data  字節數組
     * @param clazz 字節碼
     * @return
     */
    public static <T> T deserialize(byte[] data, Class<T> clazz) {
        Schema<T> schema = getSchema(clazz);
        T obj = schema.newMessage();
        ProtostuffIOUtil.mergeFrom(data, obj, schema);
        return obj;
    }
    @SuppressWarnings("unchecked")
    private static <T> Schema<T> getSchema(Class<T> clazz) {
        Schema<T> schema = (Schema<T>) SCHEMA_CACHE.get(clazz);
        if (schema == null) {
            schema = RuntimeSchema.getSchema(clazz);
            if (schema == null) {
                SCHEMA_CACHE.put(clazz, schema);
            }
        }
        return schema;
    }
}

創建一個實體類進行測試:

package cuit.pymjl.entity;
​
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
 * @author Pymjl
 * @version 1.0
 * @date 2022/7/2 12:32
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
    @Serial
    private static final long serialVersionUID = -91809837793898L;
​
    private String name;
    private String password;
    private int age;
    private String address;
    private String phone;
}

序列化後字節所占空間大小比較

編寫測試類:

public class MainTest {
    @Test
    void testLength() {
        Student student = new Student("pymjl", "123456", 18, "北京", "123456789");
        int kryoLength = KryoUtils.writeObjectToByteArray(student).length;
        int hessianLength = HessianUtils.serialize(student).length;
        int protostuffLength = ProtostuffUtils.serialize(student).length;
        System.out.println("kryoLength: " + kryoLength);
        System.out.println("hessianLength: " + hessianLength);
        System.out.println("protostuffLength: " + protostuffLength);
    }
}

運行截圖:

image-20220702131907826

由圖可知,Hessian序列化後字節所占的空間都顯著比其他兩種方式要大得多

其他比較

  • Hessian使用固定長度存儲int和long,而kryo使用變長的int和long保證這種基本數據類型序列化後盡量小,實際應用中,很大的數據不會經常出現。
  • Kryo進行序列化的時候,需要傳入完整類名或者利用 register() 提前將類註冊到Kryo上,其類與一個int型的ID相關聯,序列中隻存放這個ID,因此序列體積就更小,而Hessian則是將所有類字段信息都放入序列化字節數組中,直接利用字節數組進行反序列化,不需要其他參與,因為存的東西多處理速度就會慢點
  • Kryo使用不需要實現Serializable接口,Hessian則需實現
  • Kryo數據類的字段增、減,序列化和反序列化時無法兼容,而Hessian則兼容,Protostuff是隻能在末尾添加新字段才兼容
  • Kryo和Hessian使用涉及到的數據類中必須擁有無參構造函數
  • Hessian會把復雜對象的所有屬性存儲在一個Map中進行序列化。所以在父類、子類存在同名成員變量的情況下,Hessian序列化時,先序列化子類,然後序列化父類,因此反序列化結果會導致子類同名成員變量被父類的值覆蓋
  • Kryo不是線程安全的,要通過ThreadLocal或者創建Kryo線程池來保證線程安全,而Protostuff則是線程安全的
  • Protostuff和Kryo序列化的格式有相似之處,都是利用一個標記來記錄字段類型,因此序列化出來體積都比較小

小結

  優點 缺點
Kryo 速度快,序列化後體積小 跨語言支持較復雜
Hessian 默認支持跨語言 較慢
Protostuff 速度快,基於protobuf 需靜態編譯
Protostuff-Runtime 無需靜態編譯,但序列化前需預先傳入schema 不支持無默認構造函數的類,反序列化時需用戶自己初始化序列化後的對象,其隻負責將該對象進行賦值
Java 使用方便,可序列化所有類 速度慢,占空間

到此這篇關於Java Kryo,Protostuff,Hessian序列化方式對比的文章就介紹到這瞭,更多相關Java序列化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: