1. 程式人生 > 實用技巧 >序列化框架的選型和比對

序列化框架的選型和比對

序列化通訊

將物件轉換為位元組陣列,方便在網路中進行物件的傳輸。在網路通訊中,不同的計算機進行相互通訊主要的方式就是將資料流從一臺機器傳輸給另外一臺計算機,常見的傳輸協議包括了TCP,UDP,HTTP等,網路io的方式主要包括有了aio,bio,nio三種方式。

當客戶端將需要請求的資料封裝好了之後就需要進行轉換為二進位制格式再轉換為流進行傳輸,當服務端接收到流之後再將資料解析為二進位制格式的內容,再按照約定好的協議進行處理解析。最常見的場景就是rpc遠端呼叫的時候,對傳送資料和接收資料時候的處理。

下邊我們來一一介紹一下現在比較常見的幾款序列化技術框架。

jdk序列化

jdk自身便帶有序列化的功能,Java序列化API允許我們將一個物件轉換為流,並通過網路傳送,或將其存入檔案或資料庫以便未來使用,反序列化則是將物件流轉換為實際程式中使用的Java物件的過程。

先來看看實際的程式碼案例

首先我們建立一個基礎的測試Person類

package com.sise.test;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author linhao
 * @date 2019/8/15
 * @Version V1.0
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Person implements
Serializable { private static final long serialVersionUID = 3829252771168681281L; private Integer id; private String username; private String tel; }

如果某些特殊欄位不希望被序列化該如何處理?

這裡面如果有相應的屬性不希望被序列化操作的話,可以使用transient關鍵字進行修飾,例如希望tel屬性不希望被序列化,可以改成這樣:

  private transient String tel;

這樣的話,該物件在反序列化出來結果之後,相應的屬性就會為null值。

為什麼要定義serialVersionUID?

序列化操作時,系統會把當前類宣告的serialVersionUID寫入到序列化檔案中,用於反序列化時系統會去檢測檔案中的serialVersionUID,判斷它是否與當前類的serialVersionUID一致,如果一致就說明序列化類的版本與當前類版本是一樣的,可以反序列化成功,否則失敗。

如果沒有定義serialVersionUID時

當實現當前類沒有顯式地定義一個serialVersionUID變數時候,Java序列化機制會根據編譯的Class自動生成一個serialVersionUID作序列化版本比較用,這種情況下,如果類資訊進行修改,會導致反序列化時serialVersionUID與原先值無法match,反序列化失敗。

通過jdk提升的序列化對其進行相應的序列化和反序列化的程式碼案例

package com.sise.test.jdk;


import com.sise.test.Person;

import java.io.IOException;

/**
 * @author idea
 * @date 2019/8/15
 * @Version V1.0
 */
public class SerializationTest {

    /**
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        long begin = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            String fileName = "test-person.txt";
            Person person = new Person();
            person.setId(1);
            person.setTel("99562352");
            person.setUsername("idea");
            SerializationUtil.serialize(person, fileName);
            Person newPerson = (Person) SerializationUtil.deserialize(fileName);
        }
        long end = System.currentTimeMillis();
        System.out.println("耗時:" + (end - begin));
    }
}

jdk序列化的缺點

1、無法跨語言

這一缺點幾乎是致命傷害,對於跨程序的服務呼叫,通常都需要考慮到不同語言的相互呼叫時候的相容性,而這一點對於jdk序列化操作來說卻無法做到。這是因為jdk序列化操作時是使用了java語言內部的私有協議,在對其他語言進行反序列化的時候會有嚴重的阻礙。

2、序列化之後的碼流過大

jdk進行序列化編碼之後產生的位元組陣列過大,佔用的儲存記憶體空間也較高,這就導致了相應的流在網路傳輸的時候頻寬佔用較高,效能相比較為低下的情況。

Hessian序列化框架

Hessian是一款支援多種語言進行序列化操作的框架技術,同時在進行序列化之後產生的碼流也較小,處理資料的效能方面遠超於java內建的jdk序列化方式。

相關的程式碼案例:

package com.sise.test.hessian;

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sise.test.Person;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * @author idea
 * @date 2019/8/15
 * @Version V1.0
 */
public class HessianTest {

    /**
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            Person person = new Person();
            person.setId(1);
            person.setUsername("idea");
            person.setTel("99562352");
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            HessianOutput ho = new HessianOutput(os);
            ho.writeObject(person);
            byte[] userByte = os.toByteArray();
            ByteArrayInputStream is = new ByteArrayInputStream(userByte);
            //Hessian的反序列化讀取物件
            HessianInput hi = new HessianInput(is);
            Person newPerson = (Person) hi.readObject();
        }
        long end = System.currentTimeMillis();
        System.out.println("耗時:" + (end - begin));
    }
}

Hessian的原始碼裡面,核心主要還是com.caucho.hessian.io裡面的程式碼,AbstractSerializer是Hessian裡面的核心序列化類,當我們仔細檢視原始碼的時候就會發現hessian提供了許多種序列化和反序列化的類進行不同型別資料的處理。(我使用的是hessian4.0,因此相應的類會多很多)

在SerializerFactory裡面有getSerializer和getDefaultSerializer的函式,專門用於提取這些序列化和反序列化的工具類,這樣可以避免在使用該工具類的時候又要重新例項化,這些工具類都會被儲存到不同的ConcurrentHashMap裡面去。

ps:對於hessian3.0時候的Serializer/Derializer實現功能沒有考慮到對於異常資訊進行序列化處理,因此如果遇到相應問題的朋友可以考慮將hessian的版本提升到3.1.5以上。

Kryo序列化技術

Kryo是一種非常成熟的序列化實現,已經在Twitter、Groupon、 Yahoo以及多個著名開源專案(如Hive、Storm)中廣泛的使用,它的效能在各個方面都比hessian2要優秀些,因此dubbo後期也開始漸漸引入了使用Kryo進行序列化的方式。

對於kryo的使用,我們來看看相應程式碼:

首先我們引入相應的依賴:

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

然後就是基礎的序列化和反序列化程式碼操作了

package com.sise.test.kryo;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.sise.test.Person;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;

/**
 * @author idea
 * @date 2019/8/15
 * @Version V1.0
 */
public class KryoTest {

    public static void main(String[] args) throws FileNotFoundException {
        Kryo kryo=new Kryo();
        Output output = new Output(new FileOutputStream("person.txt"));
        Person person=new Person();
        person.setId(1);
        person.setUsername("idea");
        kryo.writeObject(output, person);
        output.close();
        Input input = new Input(new FileInputStream("person.txt"));
        Person person1 = kryo.readObject(input, Person.class);
        input.close();
        System.out.println(person1.toString());
        assert "idea".equals(person1.getUsername());
    }
}

ps:這裡我們需要注意,Kryo不支援沒有無參建構函式的物件進行反序列化,因此如果某個物件希望使用Kryo來進行序列化操作的話,需要有相應的無參建構函式才可以。

由於Kryo不是執行緒安全,因此當我們希望使用Kryo構建的工具類時候,需要在例項化的時候注意執行緒安全的問題。程式碼案例:

package com.sise.test.kryo;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.sise.test.Person;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

/**
 * @author idea
 * @data 2019/8/17
 */
public class KryoUtils {


    public byte[] serialize(Object obj){
        Kryo kryo = kryos.get();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Output output = new Output(byteArrayOutputStream);
        kryo.writeClassAndObject(output, obj);
        output.close();
        return byteArrayOutputStream.toByteArray();
    }

    public <T> T deserialize(byte[] bytes) {
        Kryo kryo = kryos.get();
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        Input input = new Input(byteArrayInputStream);
        input.close();
        return (T) kryo.readClassAndObject(input);
    }


    private static final ThreadLocal<Kryo> kryos=new ThreadLocal<Kryo>(){
        @Override
        protected Kryo initialValue(){
            Kryo kryo=new Kryo();
            return kryo;
        }

    };


    public static void main(String[] args) {
        KryoUtils kryoUtils=new KryoUtils();
        for(int i=0;i<1000;i++){
            Person person=new Person(1,"idea");
            byte[] bytes=kryoUtils.serialize(person);
            Person newPerson=kryoUtils.deserialize(bytes);
            System.out.println(newPerson.toString());
        }
    }
}

XStream實現物件的序列化

在使用XStream進行序列化技術的實現過程中,類中的字串組成了 XML 中的元素內容,而且該物件還不需要實現 Serializable 介面。XStream不關心被序列化/反序列化的類欄位的可見性,該物件也不需要有getter/setter方法和預設的建構函式。

引入的依賴:

<dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.9</version>
        </dependency>

通過使用XStream來對物件進行序列化和反序列化操作:

package com.sise.test.xstream;


import com.sise.test.Person;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

/**
 * @author idea
 * @date 2019/8/15
 * @Version V1.0
 */
public class XStreamTest {


    private static XStream xStream;

    static {
        xStream = new XStream(new DomDriver());
        /*
         * 使用xStream.alias(String name, Class Type)為任何一個自定義類建立到類到元素的別名
         * 如果不使用別名,則生成的標籤名為類全名
         */
        xStream.alias("person", Person.class);
    }

    //xml轉java物件
    public static Object xmlToBean(String xml) {
        return xStream.fromXML(xml);
    }

    //java物件轉xml
    public static String beanToXml(Object obj) {
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + xStream.toXML(obj);
    }

    /**
     *
     * @param args
     */
    public static void main(String[] args) {
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            Person person = new Person();
            person.setId(1);
            person.setUsername("idea");
            String xml = XStreamTest.beanToXml(person);
            Person newPerson = (Person) XStreamTest.xmlToBean(xml);
        }
        long end = System.currentTimeMillis();
        System.out.println("耗時:" + (end - begin));
    }
}

google的Protobuf

google protobuf是一個靈活的、高效的用於序列化資料的協議。相比較XML和JSON格式,protobuf更小、更快、更便捷。google protobuf是跨語言的,並且自帶了一個編譯器(protoc),只需要用它進行編譯,可以編譯成Java、python、C++、C#、Go等程式碼,然後就可以直接使用,不需要再寫其他程式碼,自帶有解析的程式碼。
protobuf相對於kryo來說具有更加高效的效能和靈活性,能夠在實際使用中,當物件序列化之後新增了欄位,在反序列化出來的時候依舊可以正常使用。(這一點kryo無法支援)

不同序列化框架的總結

目前已有的序列化框架還有很多在文中沒有提到,日後假若在開發中遇到的時候可以適當的進行歸納總結,比對各種不同的序列化框架之間的特點。