序列化框架的選型和比對

  • 2019 年 10 月 3 日
  • 筆記

序列化通信

將對象轉換為位元組數組,方便在網絡中進行對象的傳輸。在網絡通信中,不同的計算機進行相互通信主要的方式就是將數據流從一台機器傳輸給另外一台計算機,常見的傳輸協議包括了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無法支持)

不同序列化框架的總結

序列化框架的選型和比對

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