public static byte[] encoder(Object ob) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(ob); byte[] result = byteArrayOutputStream.toByteArray(); objectOutputStream.close(); byteArrayOutputStream.close(); return result;}public static <T> T decoder(byte[] bytes) throws Exception { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); T object = (T) objectInputStream.readObject(); objectInputStream.close(); byteArrayInputStream.close(); return object;}由於是Java內置序列化框架,所以本身是不支持跨語言序列化與反序列化。作為Java內置序列化框架,無序引用任何外部依賴即可完成序列化任務。但是JDK Serializable在使用上相比開源框架難用許多,可以看到上面的編解碼使用非常生硬,需要藉助ByteArrayOutputStream和ByteArrayInputStream才可以完整字節的轉換。JDK Serializable中通過serialVersionUID控制序列化類的版本,如果序列化與反序列化版本不一致,則會拋出java.io.InvalidClassException異常信息,提示序列化與反序列化SUID不一致。java.io.InvalidClassException: com.yjz.serialization.java.UserInfo; local class incompatible: stream classdesc serialVersionUID = -5548195544707231683, local class serialVersionUID = -5194320341014913710上面這種情況,是由於我們沒有定義serialVersionUID,而是由JDK自動hash生成的,所以序列化與反序列化前後結果不一致。但是我們可以通過自定義serialVersionUID方式來規避掉這種情況(序列化前後都是使用定義的serialVersionUID),這樣JDK Serializable就可以支持欄位擴展了。private static final long serialVersionUID = 1L;JDK Serializable是Java自帶的序列化框架,但是在性能上其實一點不像親生的。下面測試用例是我們貫穿全文的一個測試實體。public class MessageInfo implements Serializable {
private String username; private String password; private int age; private HashMap<String,Object> params; ... public static MessageInfo buildMessage() { MessageInfo messageInfo = new MessageInfo(); messageInfo.setUsername("abcdefg"); messageInfo.setPassword("123456789"); messageInfo.setAge(27); Map<String,Object> map = new HashMap<>(); for(int i = 0; i< 20; i++) { map.put(String.valueOf(i),"a"); } return messageInfo; }}使用JDK序列化後字節大小為:432。光看這組數字也許不會感覺到什麼,之後我們會拿這個數據和其它序列化框架進行對比。
我們對該測試用例進行1000萬次序列化,然後計算時間總和:1000萬序列化耗時(ms)1000萬反序列化耗時(ms)3895296508由於JDK Serializable是Java語法原生序列化框架,所以基本都能夠支持Java數據類型和語法。
JDK8種基礎類型支持List集合類支持Set集合類支持Queue集合類支持Map映射大部分支持(WeakHashMap不支持)自定義類類型支持枚舉類型支持WeakHashMap沒有實現Serializable接口。
JDK對象為null支持沒有無參構造函數支持static內部類支持(static內部類需要實現序列化接口)非static內部類支持,但是外部類也需要實現序列化接口局部內部類支持匿名內部類支持Lambda表達式修改代碼可以支持,看注1閉包支持異常類支持Runnable runnable = () -> System.out.println("Hello");com.yjz.serialization.SerializerFunctionTest$$Lambda$1/189568618原因就是我們Runnable的Lambda並沒有實現Serializable接口。我們可以做如下修改,即可支持Lambda表達式序列化。Runnable runnable = (Runnable & Serializable) () -> System.out.println("Hello");FST(fast-serialization)是完全兼容JDK序列化協議的Java序列化框架,它在序列化速度上能達到JDK的10倍,序列化結果只有JDK的1/3。目前FST的版本為2.56,在2.17版本之後提供了對Android的支持。下面是使用FST序列化的Demo,FSTConfiguration是線程安全的,但是為了防止頻繁調用時其成為性能瓶頸,一般會使用TreadLocal為每個線程分配一個FSTConfiguration。private final ThreadLocal<FSTConfiguration> conf = ThreadLocal.withInitial(() -> { FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration(); return conf; });
public byte[] encoder(Object object) { return conf.get().asByteArray(object);}
public <T> T decoder(byte[] bytes) { Object ob = conf.get().asObject(bytes); return (T)ob;}FST同樣是針對Java而開發的序列化框架,所以也不存在跨語言特性。在易用性上,FST可以說能夠甩JDK Serializable幾條街,語法極其簡潔,FSTConfiguration封裝了大部分方法。FST通過@Version註解能夠支持新增欄位與舊的數據流兼容。對於新增的欄位都需要通過@Version註解標識,沒有版本注釋意味著版本為0。private String origiField;@Version(1)private String addField;綜合來看,FST在擴展性上面雖然支持,但是用起來還是比較繁瑣的。使用FST序列化上面的測試用例,序列化後大小為:172,相比JDK序列化的432 ,將近減少了1/3。下面我們再看序列化與反序列化的時間開銷。1000萬序列化耗時(ms)1000萬反序列化耗時(ms)1358719031我們可以優化一下FST,將循環引用判斷關閉,並且對序列化類進行餘註冊。private static final ThreadLocal<FSTConfiguration> conf = ThreadLocal.withInitial(() -> { FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration(); conf.registerClass(UserInfo.class); conf.setShareReferences(false); return conf; });1000萬序列化耗時(ms)1000萬反序列化耗時(ms)760917792可以看到序列化時間將近提升了2倍,但是通過優化後的序列化數據大小增長到了191 。FST是基於JDK序列化框架而進行開發的,所以在數據類型和語法上和Java支持性一致。
FST8種基礎類型支持List集合類支持Set集合類支持Queue集合類支持Map映射大部門支持(WeakHashMap不支持)自定義類類型支持枚舉類型支持
FST對象為null支持沒有無參構造函數支持static內部類支持(static內部類需要實現序列化接口)非static內部類支持,但是外部類也需要實現序列化接口局部內部類支持匿名內部類支持Lambda表達式修改代碼可以支持(同JDK)閉包支持異常類支持Kryo一個快速有效的Java二進位序列化框架,它依賴底層ASM庫用於字節碼生成,因此有比較好的運行速度。Kryo的目標就是提供一個序列化速度快、結果體積小、API簡單易用的序列化框架。Kryo支持自動深/淺拷貝,它是直接通過對象->對象的深度拷貝,而不是對象->字節->對象的過程。private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> { Kryo kryo = new Kryo(); kryo.setRegistrationRequired(false); return kryo; });
public static byte[] encoder(Object object) { Output output = new Output(); kryoLocal.get().writeObject(output,object); output.flush(); return output.toBytes(); }
public static <T> T decoder(byte[] bytes) { Input input = new Input(bytes); Object ob = kryoLocal.get().readClassAndObject(input); return (T) ob; }需要注意的是使用Output.writeXxx時候一定要用對應的Input.readxxx,比如Output.writeClassAndObject()要與Input.readClassAndObject()。
首先Kryo官網說自己是一款Java二進位序列化框架,其次在網上搜了一遍沒有看到Kryo的跨語言使用,只是一些文章提及了跨語言使用非常複雜,但是沒有找到其它語言的相關實現。在使用方式上Kryo提供的API也是非常簡潔易用,Input和Output封裝了你幾乎能夠想到的所有流操作。Kryo提供了豐富的靈活配置,比如自定義序列化器、設置默認序列化器等等,這些配置使用起來還是比較費勁的。Kryo默認序列化器FiledSerializer是不支持欄位擴展的,如果想要使用擴展序列化器則需要配置其它默認序列化器。private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> { Kryo kryo = new Kryo(); kryo.setRegistrationRequired(false); kryo.setDefaultSerializer(TaggedFieldSerializer.class); return kryo; });使用Kryo測試上面的測試用例,Kryo序列化後的字節大小為172 ,和FST未經優化的大小一致。時間開銷如下:1000萬序列化時間開銷(ms)1000萬反序列化時間開銷(ms)1355014315我們同樣關閉循環引用配置和預註冊序列化類,序列化後的字節大小為120,因為這時候類序列化的標識是使用的數字,而不是類全名。使用的是時間開銷如下:
1000萬序列化時間開銷(ms)1000萬反序列化時間開銷(ms)1179911584Kryo對於序列化類的基本要求就是需要含有無參構造函數,因為反序列化過程中需要使用無參構造函數創建對象。
Kryo8種基礎類型支持List集合類支持Set集合類支持Queue集合類部分支持(ArrayBlockingQueue不支持)Map映射支持自定義類類型支持枚舉類型支持
Kryo對象為null支持沒有無參構造函數不支持static內部類支持非static內部類不支持局部內部類支持匿名內部類支持Lambda表達式不支持閉包支持異常類不支持(StackOverflowError)Protocol buffer是一種語言中立、平臺無關、可擴展的序列化框架。Protocol buffer相較於前面幾種序列化框架而言,它是需要預先定義Schema的。syntax = "proto3";
option java_package = "com.yjz.serialization.protobuf3";
message MessageInfo{ string username = 1; string password = 2; int32 age = 3; map<string,string> params = 4;}protoc --java_out=./src/main/java message.protobyte[] bytes = MessageInfo.toByteArray()MessageInfo messageInfo = Message.MessageInfo.parseFrom(bytes);protobuf設計之初的目標就是能夠設計一款與語言無關的序列化框架,它目前支持了Java、Python、C++、Go、C#等,並且很多其它語言都提供了第三方包。所以在通用性上,protobuf是非常給力的。protobuf需要使用IDL來定義Schema描述文件,定義完描述文件後,我們可以直接使用protoc來直接生成序列化與反序列化代碼。所以,在使用上只需要簡單編寫描述文件,就可以使用protobuf了。可擴展性同樣是protobuf設計之初的目標之一,我們可以非常輕鬆的在.proto文件進行修改。新增欄位:對於新增欄位,我們一定要保證新增欄位要有對應的默認值,這樣才能夠與舊代碼交互。相應的新協議生成的消息,可以被舊協議解析。刪除欄位:刪除欄位需要注意的是,對應的欄位、標籤不能夠在後續更新中使用。為了避免錯誤,我們可以通過reserved規避帶哦。message userinfo{ reserved 3,7; reserved "age","sex" }protobuf在數據兼容性上也非常友好,int32、unit32、int64、unit64、bool是完全兼容的,所以我們可以根據需要修改其類型。通過上面來看,protobuf在擴展性上做了很多,能夠很友好的支持協議擴展。我們同樣使用上面的實例來進行性能測試,使用protobuf序列化後的字節大小為 192,下面是對應的時間開銷。1000萬數據序列化耗時(ms)1000萬數據反序列化耗時(ms)1423530694可以看出protobuf的反序列化性能要比FST、Kryo差一些。
Protobuf使用IDL定義Schema所以不支持定義Java方法,下面序列化變量的測試:
Protobuf8種基礎類型基本支持(無byte、shot、char)List集合類支持Set集合類支持Queue集合類支持Map映射支持自定義類類型支持枚舉類型支持註:List、Set、Queue通過protobuf repeated定義測試的。只要實現Iterable接口的類都可以使用repeated列表。
Thrift是由Facebook實現的一種高效的、支持多種語言的遠程服務調用框架,即RPC(Remote Procedure Call)。後來Facebook將Thrift開源到Apache。可以看到Thrift是一個RPC框架,但是由於Thrift提供了多語言之間的RPC服務,所以很多時候被用於序列化中。使用Thrift實現序列化主要分為三步,創建thrift IDL文件、編譯生成Java代碼、使用TSerializer和TDeserializer進行序列化和反序列化。(1)使用Thrift IDL定義thrift文件:namespace java com.yjz.serialization.thrift
struct MessageInfo{ 1: string username; 2: string password; 3: i32 age; 4: map<string,string> params;}thrift --gen java message.thrift(3)使用TSerializer和TDeserializer進行編解碼:public static byte[] encoder(MessageInfo messageInfo) throws Exception{ TSerializer serializer = new TSerializer(); return serializer.serialize(messageInfo); } public static MessageInfo decoder(byte[] bytes) throws Exception{ TDeserializer deserializer = new TDeserializer(); MessageInfo messageInfo = new MessageInfo(); deserializer.deserialize(messageInfo,bytes); return messageInfo; }Thrift和protobuf類似,都需要使用IDL定義描述文件,這是目前實現跨語言序列化/RPC的一種有效方式。Thrift目前支持 C++、Java、Python、PHP、Ruby、 Erlang、Perl、Haskell、C#、Cocoa、JavaScript、Node.js、Smalltalk、OCaml、Delphi等語言,所以可以看到Thrift具有很強的通用性。Thrift在易用性上和protobuf類似,都需要經過三步:使用IDL編寫thrift文件、編譯生成Java代碼和調用序列化與反序列化方法。protobuf在生成類中已經內置了序列化與反序列化方法,而Thrift需要單獨調用內置序列化器來進行編解碼。Thrift支持欄位擴展,在擴展欄位過程中需要注意以下問題:上面的測試用例,使用Thrift序列化後的字節大小為:257,下面是對應的序列化時間與反序列化時間開銷:1000萬序列化時間開銷(ms)1000萬反序列化時間開銷(ms)2863420722Thrift在序列化和反序列化的時間開銷總和上和protobuf差不多,protobuf在序列化時間上更佔優勢,而Thrift在反序列化上有自己的優勢。
數據類型支持:由於Thrift使用IDL來定義序列化類,所以能夠支持的數據類型就是Thrift數據類型。Thrift所能夠支持的Java數據類型:8中基礎數據類型,沒有short、char,只能使用double和String代替。
集合類型,支持List、Set、Map,不支持Queue。
自定義類類型(struct類型)。
枚舉類型。
字節數組。
Hessian是caucho公司開發的輕量級RPC(Remote Procedure Call)框架,它使用HTTP協議傳輸,使用Hessian二進位序列化。Hessian由於其支持跨語言、高效的二進位序列化協議,被經常用於序列化框架使用。Hessian序列化協議分為Hessian1.0和Hessian2.0,Hessian2.0協議對序列化過程進行了優化(優化內容待看),在性能上相較Hessian1.0有明顯提升。使用Hessian序列化非常簡單,只需要通過HessianInput和HessianOutput即可完成對象的序列化,下面是Hessian序列化的Demo:public static <T> byte[] encoder2(T obj) throws Exception{ ByteArrayOutputStream bos = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(bos); hessian2Output.writeObject(obj); return bos.toByteArray(); }
public static <T> T decoder2(byte[] bytes) throws Exception { ByteArrayInputStream bis = new ByteArrayInputStream(bytes); Hessian2Input hessian2Input = new Hessian2Input(bis); Object obj = hessian2Input.readObject(); return (T) obj; }Hessian與Protobuf、Thrift一樣,支持跨語言RPC通信。Hessian相比其它跨語言PRC框架的一個主要優勢在於,它不是採用IDL來定義數據和服務,而是通過自描述來完成服務的定義。目前Hessian已經實現了語言包括:Java、Flash/Flex、Python、C++、.Net/C#、D、Erlang、PHP、Ruby、Object-C。相較於Protobuf和Thrift,由於Hessian不需要通過IDL來定義數據和服務,對於序列化的數據只需要實現Serializable接口即可,所以使用上相比Protobuf和Thrift更加容易。Hession序列化類雖然需要實現Serializable接口,但是它並不受serialVersionUID影響,能夠輕鬆支持欄位擴展。修改欄位名稱:反序列化後新欄位名稱為null或0(受類型影響)。
新增欄位:反序列化後新增欄位為null或0(受類型影響)。
刪除欄位:能夠正常反序列化。
修改欄位類型:如果欄位類型兼容能夠正常反序列化,如果不兼容則直接拋出異常。
使用Hessian1.0協議序列化上面的測試用例,序列化結果大小為277。使用Hessian2.0序列化協議,序列化結果大小為178。
1000萬序列化時間開銷(ms)1000萬反序列化時間開銷(ms)Hessian1.05764855261Hessian2.03882317682可以看到Hessian1.0的無論在序列化後體積大小,還是在序列化、反序列化時間上都比Hessian2.0相差很遠。由於Hession使用Java自描述序列化類,所以Java原生數據類型、集合類、自定義類、枚舉等基本都能夠支持(SynchronousQueue不支持),Java語法結構也能夠很好的支持。Avro是一個數據序列化框架。它是Apache Hadoop下的一個子項目,由Doug Cutting主導Hadoop過程中開發的數據序列化框架。Avro在設計之初就用於支持數據密集型應用,很適合遠程或本地大規模數據交換和存儲。{ "namespace": "com.yjz.serialization.avro", "type": "record", "name": "MessageInfo", "fields": [ {"name": "username","type": "string"}, {"name": "password","type": "string"}, {"name": "age","type": "int"}, {"name": "params","type": {"type": "map","values": "string"} } ]}(2)使用avro-tools.jar編譯生成Java代碼(或maven編譯生成):java -jar avro-tools-1.8.2.jar compile schema src/main/resources/avro/Message.avsc ./src/main/java(3)藉助BinaryEncoder和BinaryDecoder進行編解碼:public static byte[] encoder(MessageInfo obj) throws Exception{ DatumWriter<MessageInfo> datumWriter = new SpecificDatumWriter<>(MessageInfo.class); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); BinaryEncoder binaryEncoder = EncoderFactory.get().directBinaryEncoder(outputStream,null); datumWriter.write(obj,binaryEncoder); return outputStream.toByteArray(); }
public static MessageInfo decoder(byte[] bytes) throws Exception{ DatumReader<MessageInfo> datumReader = new SpecificDatumReader<>(MessageInfo.class); BinaryDecoder binaryDecoder = DecoderFactory.get().directBinaryDecoder(new ByteArrayInputStream(bytes),null); return datumReader.read(new MessageInfo(),binaryDecoder); }Avro通過Schema定義數據結構,目前支持Java、C、C++、C#、Python、PHP和Ruby語言,所以在這些語言之間Avro具有很好的通用性。Avro對於動態語言無需生成代碼,但對於Java這類靜態語言,還是需要使用avro-tools.jar來編譯生成Java代碼。在Schema編寫上,個人感覺相比Thrift、Protobuf更加複雜。給所有field定義default值。如果某field沒有default值,以後將不能刪除該field。
如果要新增field,必須定義default值。
不能修改field type。
不能修改field name,不過可以通過增加alias解決。
使用Avro生成代碼序列化之後的結果為:111。下面是使用Avro序列化的時間開銷:
1000萬序列化時間開銷(ms)1000萬序反列化時間開銷(ms)生成Java代碼2656545383Avro需要使用Avro所支持的數據類型來編寫Schema信息,所以能夠支持的Java數據類型即為Avro所支持的數據類型。Avro支持數據類型有:基礎類型(null、boolean、int、long、float、double、bytes、string),複雜數據類型(Record、Enum、Array、Map、Union、Fixed)。Avro自動生成代碼,或者直接使用Schema,不能支持在序列化類中定義java方法。下面是從通用性上對比各個序列化框架,可以看出Protobuf在通用上是最佳的,能夠支持多種主流變成語言。序列化框架通用性JDK Serializer只適用於JavaFST只適用於JavaKryo主要適用於Java(可複雜支持跨語言)Protocol buffer支持多種語言Thrift支持多種語言Hessian支持多種語言Avro支持多種語言下面是從API使用的易用性上面來對比各個序列化框架,可以說除了JDK Serializer外的序列化框架都提供了不錯API使用方式。序列化框架易用性JDK Serializer使用語法過於生硬FST使用簡潔,FSTConfiguration提供了序列化與反序列化的方法Kryo使用簡潔,Input/Output封裝了幾乎所有能有需要的流方法Protocol buffer稍微複雜。需要編寫所需序列化類的proto文件,然後編譯生成Java代碼。但是自動生成Java類,包含了序列化與反序列化方法Thrift稍微複雜。需要編寫所需的序列化類的thrift文件,然後編譯生成Java代碼。然後通過TSerializer和TDserializer進行序列化與反序列化Hessian使用簡單,在跨語言的基礎上不需要使用IDLAvro使用較複雜。相較於Protobuf和Thrift來說,對於一些靜態語言無序生成代碼。但是對於Java來一般還需要生成代碼,並且Avro提供的API不是很友好下面是各個序列化框架的可擴展性對比,可以看到Protobuf的可擴展性是最方便、自然的。其它序列化框架都需要一些配置、註解等操作。序列化框架可擴展性JDK Serializer自定義serialVersionUID,保證序列化前後VUID一致即可FST通過@Version控制版本,新增欄位需要修改Version版本Kryo默認序列化器不支持欄位擴展,需要修改默認序列化器或自己實現序列化器Protocol buffer支持欄位擴展,只要保證新增id標識沒有使用過即可Thrift支持欄位擴展。新增欄位為required類型時,需要設置默認值Hessian支持欄位擴展Avro支持欄位擴展。注意需要為欄位設置默認值對比各個序列化框架序列化後的數據大小如下,可以看出kryo preregister(預先註冊序列化類)和Avro序列化結果都很不錯。所以,如果在序列化大小上有需求,可以選擇Kryo或Avro。下面是序列化與反序列化的時間開銷,kryo preregister和fst preregister都能提供優異的性能,其中fst pre序列化時間就最佳,而kryo pre在序列化和反序列化時間開銷上基本一致。所以,如果序列化時間是主要的考慮指標,可以選擇Kryo或FST,都能提供不錯的性能體驗。List測試內容:ArrayList、LinkedList、Stack、CopyOnWriteArrayList、Vector。
Set測試內容:HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet。
Map測試內容:HashMap、LinkedHashMap、TreeMap、WeakHashMap、ConcurrentHashMap、Hashtable。
Queue測試內容:PriorityQueue、ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue、SynchronousQueue、ArrayDeque、LinkedBlockingDeque和ConcurrentLinkedDeque。
下面根據測試總結了以上序列化框架所能支持的數據類型、語法。由於Protobuf、Thrift是IDL定義類文件,然後使用各自的編譯器生成Java代碼。IDL沒有提供定義staic內部類、非static內部類等語法,所以這些功能無法測試。阿里雲開發者社區
世界讀書日,來讀書吧
4月23日是第26個世界讀書日,阿里雲開發者社區推出「記錄閱讀之路,影響同行之人」活動,6位阿里技術人為同學們分享他們看過的好書,開發者藏經閣也推出了最受大家歡迎的電子書。
點擊「閱讀原文」,推薦曾經影響你的書,來一起讀書吧~