標籤:序列化 serializa java json 還原序列化
概念
先來點簡單的概念:
what?why?
什麼是序列化?為什麼要序列化?
答曰:將java對象轉成位元組序列,用以傳輸和儲存
where?
使用情境是什嗎?
答曰:對象的傳輸;狀態的備份,例如jvm的dump檔案;
好了,不裝*了,下面說的詳細點。其實對象的序列化主要有兩種用途:
- 把對象的位元組序列永久地儲存到硬碟上,通常存放在一個檔案中
- 在網路上傳送對象的位元組序列
在很多應用中,需要對某些對象進行序列化,讓它們離開記憶體空間,入住物理硬碟,以便長期儲存。比如最常見的是Web伺服器中的Session對象,當有 10萬使用者並發訪問,就有可能出現10萬個Session對象,記憶體可能吃不消,於是Web容器就會把一些seesion先序列化到硬碟中,等要用了,再把儲存在硬碟中的對象還原到記憶體中。
當兩個進程在進行遠程通訊時,彼此可以發送各種類型的資料。無論是何種類型的資料,都會以二進位序列的形式在網路上傳送。發送方需要把這個Java對象轉換為位元組序列,才能在網路上傳送;接收方則需要把位元組序列再恢複為Java對象。
實現方式
how?
- (需要序列化的類)實現Serializable介面。查看源碼可知Serializable是個空介面(裡面沒有任何方法),即標記介面。作用就是明確地告訴java,這個類需要序列化,不實現這個介面,那這個對象是沒法序列化和傳輸的(如果不加implements Serializable,會報錯,建議試試,直觀感受下),類似的標記介面還有cloneable。
- 建立輸入輸出資料流。首先,序列化一個對象,需要要建立某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然後將這些OutputStream封裝在一個ObjectOutputStream中。這時候,只需要調用writeObject()方法就可以將對象序列化,並將其發送給OutputStream(對象的序列化是基於位元組的,不能使用Reader和Writer等基於字元的階層)。其次,反序列的過程(即將一個序列還原成為一個對象),則需要將一個InputStream(如FileInputstream、ByteArrayInputStream等)封裝在ObjectInputStream內,然後調用readObject()即可。簡單來說即:
序列化:ObjectOutputStream.writeObject(Object)
還原序列化:ObjectInputStream.readObject()
見下面的例子:
先定義一個待序列化的對象:
package com.alibaba.serialize.common;import java.io.Serializable;/* * 不加implements Serializable,會報錯 */public class User implements Serializable{ private static final long serialVersionUID = 1L; private String username; private int age; public User(String username, int age) { this.username = username; this.age = age; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}
再寫一個序列化的例子
package com.alibaba.serialize.common;import java.io.BufferedOutputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;public class SerializeExample { public static final String out_file = "src/com/alibaba/serialize/temp.out"; public static void main(String[] args) { User user = new User("tony", 18); try { //如果這裡改成網路輸出資料流而不是檔案輸出資料流(還原序列化那裡也同樣改成網路輸入資料流),則可以在網路上傳輸 ObjectOutputStream out = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream(out_file))); out.writeObject("使用者資訊.."); out.writeObject(user); out.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } System.out.println("序列化完成。請調用還原序列化類DeserializeExample完成還原序列化!"); }}
最後再寫一個還原序列化的例子
package com.alibaba.serialize.common;import java.io.BufferedInputStream;import java.io.FileInputStream;import java.io.ObjectInputStream;public class DeserializeExample { public static void main(String[] args) throws Exception{ ObjectInputStream in = new ObjectInputStream( new BufferedInputStream(new FileInputStream(SerializeExample.out_file))); String title = (String) in.readObject(); System.out.println(title); User user = (User) in.readObject(); System.out.println("使用者姓名:"+user.getUsername()); System.out.println("使用者年齡:"+user.getAge()); }}
分別運行SerializeExample 和 DeserializeExample 可以看到對應的效果。
序列化的檔案關係
說幾點平常不注意容易忽略的地方。
- 如果一個類沒有實現Serializable介面,但是它的基類實現了,那麼這個類也是可以序列化的;
- 相反,如果一個類實現了Serializable介面,但是它的父類沒有實現,那麼這個類還是可以序列化(Object是所有類的父類),但是序列化該子類對象,然後還原序列化後輸出父類定義的某變數的數值,會發現該變數數值與序列化時的數值不同(一般為null或者其他預設值),而且這個父類裡面必須有無參的構造方法,不然子類還原序列化的時候會報錯。
見樣本:
先寫個父類
package com.alibaba.serialize.parent;import java.io.Serializable;public class CarSerialize{ private String name; private Long price; /* * 沒有這個無參建構函式會報錯,可以刪除,測試下 */ public CarSerialize() { super(); } public CarSerialize(String name, Long price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Long getPrice() { return price; } public void setPrice(Long price) { this.price = price; }}
再寫個子類
package com.alibaba.serialize.parent;import java.io.Serializable;public class BMWSerialize extends CarSerialize implements Serializable{ private String name; private Long price; public BMWSerialize(String name, Long price) { super(name, price); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Long getPrice() { return price; } public void setPrice(Long price) { this.price = price; } }
最後寫個測試類別
package com.alibaba.serialize.parent;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class SerializeTest { public static void main(String[] args) { try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj")); out.writeObject(new BMWSerialize("BMW",100L)); out.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj")); BMWSerialize bmw = (BMWSerialize) oin.readObject(); System.out.println("解密後的字串:" + bmw.getName()); oin.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }}
上述代碼中,大家可以嘗試按照我上面說的兩點,分情況進行測試。
這裡還有個需要注意的地方
同一個流的對象參考關聯性被很好地保留了下來,不同流的對象參考關聯性則無法保證匹配
這一點我感覺比較容易理解,就不寫了,有時間可以測試下。
序列化ID
思考一個問題:如果序列化之後,兩端或者版本不同,class不一致怎麼辦?
???
???
這裡就有序列化ID的概念了,serialVersionUID適用於JAVA的序列化機制。還原序列化時,如果類發生了變動,則構造器和指派陳述式不會生效。
簡單來說,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一致性的。在進行還原序列化時,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的,可以進行還原序列化,否則就會出現序列化版本不一致的異常,即是InvalidCastException(如同上面一節,你把父類的無參構造方法刪除之後,再還原序列化也會報這個錯)。
serialVersionUID有兩種顯示的產生方式:
- 一是預設的1L,比如:private static final long serialVersionUID = 1L
- 二是根據類名、介面名、成員方法及屬性等來產生一個64位的雜湊欄位,比如:
private static final long serialVersionUID = xxxxL;
再思考一個問題,如果序列化ID是一樣的,假設A端是序列化,B端是還原序列化,還原序列化之前B端序列化對象發生變化,會有幾種情況?其實邏輯還是比較容易理解的,略微總結了下,大致如下:
- B加欄位:序列化,還原序列化正常,B端新增加的int欄位被賦予了欄位類型的預設值(如0或者false)
- B刪欄位:序列化,還原序列化正常(不會報錯),B端欄位少於A端,A端多的欄位值丟失
自訂序列化
既然題目是深入分析,那當然要接著分析,哈哈~所以,再問一個問題,當對象中有敏感欄位怎麼辦?如password。是不是自己可以決定序列化方式呢?這裡解釋一下
在序列化過程中,虛擬機器會試圖調用對象類裡的 writeObject 和 readObject 方法,進行使用者自訂的序列化和還原序列化,如果沒有這樣的方法,則預設調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。使用者自訂的 writeObject 和 readObject 方法可以允許使用者控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。
基於這個原理,可以在實際應用中得到使用,用于敏感欄位的加密工作,見樣本(代碼摘自參考資料)。
先寫一個自訂序列化過程的待序列化對象。
package com.alibaba.serialize.customer;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectInputStream.GetField;import java.io.ObjectOutputStream;import java.io.ObjectOutputStream.PutField;import java.io.Serializable;public class SecurityInfo implements Serializable{ private String password = "pass"; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } private void writeObject(ObjectOutputStream out) { try { PutField putFields = out.putFields(); System.out.println("原密碼:" + password); password = "encryption";//類比加密 putFields.put("password", password); System.out.println("加密後的密碼" + password); out.writeFields(); } catch (IOException e) { e.printStackTrace(); } } private void readObject(ObjectInputStream in) { try { GetField readFields = in.readFields(); Object object = readFields.get("password", ""); System.out.println("要解密的字串:" + object.toString()); password = "pass";//類比解密,需要獲得本地的密鑰 } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }}
再寫一個測試類別
package com.alibaba.serialize.customer;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class CustomerSerialize { public static void main(String[] args) { try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj")); out.writeObject(new SecurityInfo()); out.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj")); SecurityInfo t = (SecurityInfo) oin.readObject(); System.out.println("解密後的字串:" + t.getPassword()); oin.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }}
ok!運行下看看吧。
其實說到自訂序列化,還可以通過實現java序列化另一個介面Externalizable,實現Externalizable介面就需要實現它的兩個方法,如下:
大致總結了下,Externalizable和Serializable的區別:
| 實現Serializable介面 |
實現Externalizable |
| 系統自動儲存必要資訊 |
程式員決定儲存哪些資訊 |
| Java內建支援,易於實現,只需實現該介面如何即可,無須任何代碼支援 |
僅僅提供兩個空方法,實現該介面必須為兩個空方法提供實現 |
| 效能略差 |
效能略高 |
其實雖然Externalizable效能高,但是我們一般很少在代碼裡看到,這個原因需要請假下別人,個人認為可能是自己寫不夠通用,太麻煩,而且一般敏感資訊也不會這麼傳。
幾種序列化協議比較
這裡主要介紹和對比幾種當下比較流行的序列化協議,包括XML、JSON、Protobuf、Thrift和Avro。這裡因為我用的也不多,只用過json和xml,所以,對比的話這裡列了個參考資料: http://tech.meituan.com/serialization_vs_deserialization.html
本文還參考:
http://blog.csdn.net/zhaozheng7758/article/details/7820018
感謝上述作者~
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
深入分析java序列化