在數據處理中,將數據結構或者對象轉換成其餘可用的格式,並作持久化存儲或者將其發送到網絡流中,這種行爲就是序列化,反序列化則是與之相反。java
現現在流行的微服務,服務之間相互使用RPC或者HTTP進行通訊,當一發發送的消息是對象的時候,就須要對其進行序列化,不然接收方可能沒法識別(微服務架構下,各個服務使用的語言是能夠不同的),當接受方接受消息的時候就按照必定的協議反序列化成系統可識別的數據結構。shell
如今Java序列化的方式主要有兩種:一種是Java原生的序列化,會將Java對象轉換成字節流,但這種方式會有危險,後面會說到,另外一種是使用第三方結構化數據結構,例如JSON和Google Protobuf,下面我將簡單介紹一下這兩種方式。json
這種方式序列化的對象所屬的類必須實現了Serializable接口,該接口只是一個標記接口,沒有任何抽象方法,因此實現該接口的時候不須要重寫任何方法。要對Java對象作序列化,須要使用java.io.ObjectOutputStream類,它有一個writeObject方法,其參數是須要序列化的對象,要對Java對象作反序列化,須要使用java.io.ObjectInputStream類,他有一個readObject()方法,其沒有參數。下面是一個對Java對象進行序列化和反序列化的示例:網絡
ObjectOutputStream和ObjectInputStream都是BIO中面向字節流體系下的類,因此從這裏能夠推斷出Java原生的序列化是面向字節流的。數據結構
public class User {
private Long id;
private String username;
private String password;
//setter and getter
}
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String fileName = "E:\\Java_project\\effective-java\\src\\top\\yeonon\\serializable\\origin\\user.txt";
User user = new User();
user.setId(1L);
user.setUsername("yeonon");
user.setPassword("yeonon");
//序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fileName));
out.writeObject(user); //寫入
out.flush(); //刷新緩衝區
out.close();
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream(fileName));
User newUser = (User) in.readObject();
in.close();
//比較兩個對象
System.out.println(user);
System.out.println(newUser);
System.out.println(newUser.equals(user));
}
}
複製代碼
代碼使用了ObjectOutputStream和ObjectInputStream進行序列化和反序列化,代碼很是簡單,就很少說了。直接運行這個程序,應該會獲得一個java.io.NotSerializableException異常,什麼緣由呢?由於User類沒有實現Serializable接口,咱給他加上,以下所示:架構
public class User implements Serializable {
private Long id;
private String username;
private String password;
}
複製代碼
如今再次運行程序,輸出大概以下:app
top.yeonon.serializable.User@61bbe9ba
top.yeonon.serializable.User@4e50df2e
false
複製代碼
第一行輸出是序列化以前的對象,第二行輸出是通過序列化和反序列化以後的對象,從編號上來看,這兩個對象是不一樣的,第三行是使用equals方法比較兩個對象,輸出是false?爲何,這兩個對象即便不一樣,用equals方法比較應該返回true啊,由於他們的狀態字段都是同樣的?這實際上是equals方法的問題,咱們的User類沒有重寫euqals方法,因此使用的是Object類的equals方法,Object的equals方法只是簡單的比較二者引用是否相同而已,以下所示:微服務
public boolean equals(Object obj) {
return (this == obj);
}
複製代碼
因此結果會返回false,但若是咱們在User類裏重寫了equals方法,就能夠有機會讓euqlas方法返回true,關於如何正確實現euqals方法,就不是本文討論的內容了,推薦看看《Effective Java》第三版Object主題的相關章節。工具
這就完成了一次序列化和反序列化操做,同時還在目錄下建立了一個user.txt文件,該文件時一個二進制文件,裏面的內容是虛擬機可識別的字節碼,咱們能夠把這個文件傳到另外一臺電腦上,若是那臺電腦的JVM和這邊電腦的同樣,那麼就能夠直接使用ObjectInputStream讀取並反序列這個對象了(還有一個前提是那邊電腦的java程序裏存在User類)。性能
介紹完Java原生的序列化和反序列化,接下來將介紹基於第三方結構化數據結構的序列化和反序列化,主要介紹兩種格式:JSON和Google Protobuf。
JSON即 JavaScript Object Notation,是一種輕量級的數據交換語言,JSON的設計初衷是爲了JavaScript服務的,普遍應用在Web領域,但如今JSON已經不只僅用於JS和Web環境了,而變成一種獨立於語言的結構化數據格式,並且JSON是基於文本的,其內容有極高的可讀性,人類能夠簡單理解。
下面咱們使用java的一個第三方庫jackson來演示如何將Java對象轉換成JSON以及將JSON轉換成Java對象:
public class Main {
//ObjectMapper對象,jackson中全部的操做都須要經過該對象
private static final ObjectMapper objectMapper = new ObjectMapper();
public static void main(String[] args) throws IOException {
User user = new User();
user.setId(1L);
user.setUsername("yeonon");
user.setPassword("yeonon");
//序列化成json字符串
String jsonStr = objectMapper.writeValueAsString(user);
System.out.println(jsonStr);
//反序列化成Java對象
User newUser = objectMapper.readValue(jsonStr, User.class);
System.out.println(newUser);
System.out.println(user);
System.out.println(newUser.equals(user));
}
}
複製代碼
首先是建立一個ObjectMapper對象,jackson中全部的操做都須要經過該對象。而後調用writeValueAsString(Object)方法將對象轉換成String字符串的形式,即序列化,經過readValue(String, Class<?>)方法將JSON字符串轉換成Java對象,即反序列化。輸出大體以下所示:
{"id":1,"username":"yeonon","password":"yeonon"}
top.yeonon.serializable.User@675d3402
top.yeonon.serializable.User@51565ec2
false
複製代碼
須要注意的是第一行輸出,這就是JSON格式的字符表示,關於JSON的語法、格式等建議網上查找資料學習,很是簡單。另外三行和以前同樣,以前都解釋過,再次就再也不解釋了。
Jackson的功能很是豐富、強大,不只僅能序列化user這種純對象,還能夠序列化集合,只不過反序列化的時候麻煩一些而已(但仍然比較簡單),以下所示:
public class Main {
//ObjectMapper對象,jackson中全部的操做都須要經過該對象
private static final ObjectMapper objectMapper = new ObjectMapper();
public static void main(String[] args) throws IOException {
User user1 = new User(1L, "yeonon", "yeonon");
User user2 = new User(2L, "weiyanyu", "weiyanyu");
User user3 = new User(3L, "xiangjinwei", "xiangjinwei");
Map<Long, User> map = new HashMap<>();
map.put(1L, user1);
map.put(2L, user2);
map.put(3L, user3);
//序列化集合
String jsonStr = objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(map);
System.out.println(jsonStr);
//反序列化集合
JavaType javaType = objectMapper
.getTypeFactory()
.constructParametricType(Map.class, Long.class, User.class);
Map<Long, User> newMap = objectMapper.readValue(jsonStr, javaType);
newMap.forEach((k, v) -> {
System.out.println(v);
});
}
}
複製代碼
序列化和以前同樣,只不過這裏使用了writerWithDefaultPrettyPrinter來將輸出變得更加Pretty(漂亮)一些(建議不要在生產環境使用這個,由於這個佔用的空間會比較多,不如原始的緊湊)。關鍵在反序列化那,若是還像以前同樣直接調用 objectMapper.readValue(jsonStr, Map.class);會發現結果雖然是一個Map,可是裏面包含的元素鍵和值卻不是Long和User類型,而是String類型的建和List類型的的值,這顯然不是咱們想要的結果。
因此,對於集合,須要作更多額外的處理,首先產生一個JavaType對象,該對象表示將幾種類型集合在一塊兒組成一個新的類型,constructParametricType()方法接受兩個參數,第一個參數是rawType,即原始類型,在代碼中即Map,以後的表示集合裏的元素類類型,由於Map有兩種元素類型,因此傳入兩種類型,分別是Long和User,最後調用readValue()的另外一個重載形式將字符串和javaType傳入便可,此時便完成了反序列化的操做。
運行程序,輸出結果大體以下所示:
{
"1" : {
"id" : 1,
"username" : "yeonon",
"password" : "yeonon"
},
"2" : {
"id" : 2,
"username" : "weiyanyu",
"password" : "weiyanyu"
},
"3" : {
"id" : 3,
"username" : "xiangjinwei",
"password" : "xiangjinwei"
}
}
----------------------------------
User{id=1, username='yeonon', password='yeonon'}
User{id=2, username='weiyanyu', password='weiyanyu'}
User{id=3, username='xiangjinwei', password='xiangjinwei'}
複製代碼
Jackson還有不少強大的功能,若是想深刻了解,建議自行搜索資料查看學習。下面介紹另外一種結構化數據格式:Google Protobuf。
Protobuf是Google推出的序列化工具。它主要有如下幾個特色:
語言、平臺無關是由於它是基於某種協議的格式,在序列化和反序列化的時候都須要遵循這種協議,天然就能實現平臺無關了。雖然其簡潔,但並不易讀,它不像JSON、XML等基於文本的格式,而是基於二進制格式的,這也是其高性能的緣由,下圖是它和其餘工具的性能比較:
首先,須要到官網下載對應的工具,由於個人電腦操做系統是win,因此下載的是protoc-3.6.1-win32.zip這個文件。下載好以後進入到bin目錄,找到protoc.exe,這個玩意兒就是等會咱們要用的了。
準備工做作好以後,就開始着手編寫protobuf文件了,protobuf文件格式很是貼近C++,關於其格式更詳細的內容,建議到官網上去查看,官網寫得很是清楚,下面是一個示例:
syntax = "proto2";
option java_package = "top.yeonon.serializable.protobuf";
option java_outer_classname = "UserProtobuf";
message User {
required int64 id= 1;
required string name = 2;
required string password = 3;
}
複製代碼
簡單解釋一下吧:
此時就要用到protoc.exe這個可執行文件了,執行以下命令:
protoc.exe -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
複製代碼
SRC_DIR即源文件所在目錄,DST_DIR即要將生成的類放在哪一個目錄下,下面是個人測試用例:
protoc.exe -I=C:\Users\72419\Desktop\ --java_out=E:\Java_project\effective-java\src C:\Users\72419\Desktop\user.proto
複製代碼
執行完畢以後,能夠在E:\Java_project\effective-java\src目錄下看到top目錄,從這開始,就是根據以前在protobuf文件裏設置的包名繼續建立目錄了,因此,最終咱們會在E:\Java_project\effective-java\src\top\yeonon\serializable\protobuf\目錄下看到一個UserProtobuf.java文件,這就是生成的Java類了,但這不是咱們真正想要的類,咱們真正想要的應該是User類,User類其實是UserProtobuf類的一個內部類,不能直接實例化,須要經過Buidler類來構建。下面是一個簡單的使用示例:
public class Main {
public static void main(String[] args) throws InvalidProtocolBufferException {
UserProtobuf.User user = UserProtobuf.User.newBuilder()
.setId(1L)
.setName("yeonon")
.setPassword("yeonnon").build();
System.out.println(user);
//序列化成字節流
byte[] userBytes = user.toByteArray();
//反序列化
UserProtobuf.User newUser = UserProtobuf.User.parseFrom(userBytes);
System.out.println(user);
System.out.println(newUser);
System.out.println(newUser.equals(user));
}
}
複製代碼
首先使用相關的Builder來構建對象,而後經過toByteArray生成字節流,此時就能夠將這個字節流進行網絡傳輸或者持久化了。反序列化也很是簡單,調用parseFrom()方法便可。運行程序,輸出大體以下:
id: 1
name: "yeonon"
password: "yeonnon"
true
複製代碼
發現這裏返回的是true,和上面的都不同,爲何呢?由於工具再生成該User類的時候,還順便重寫了equals方法,該euals方法會比較兩個對象的字段值,這裏字段值啥的確定是同樣的,因此最終會返回true。
以上就是Protobuf的簡單使用了,實際上Protobuf遠不止這些功能,還有不少強大的功能,因爲我本身也是「現學現賣」,因此就不獻醜了,建議網上搜索資料進行深層次的學習。
單例模式的最基本要求是整個應用程序中都只存在一個實例,不管哪一種實現方法,都是圍繞整個目的來作的。而序列化可能會破壞單例模式,更準確的說是反序列化會破壞單例。爲何呢?其實從上面的介紹中,已經大概知道緣由了,咱們發現反序列化以後的對象和原對象並非同一個對象,即整個系統中存在某個類的不止一個實例,顯然破壞了單例,這也是爲何Enum類(Enum都是單例的)的readObject方法實現是直接拋出異常(在關於枚舉的那篇文章中有提到)。因此,若是要保持單例,最好不要容許其進行序列化和反序列化。
之因此要進行序列化,是由於要將對象進行持久化存儲或者進行網絡傳輸,這樣會致使系統失去對對象的控制權。例如,如今要將user對象進行序列化並經過網絡傳輸給其餘系統,若是在網絡傳輸過程當中,序列化的字節流被篡改了,並且沒有破壞其結構,那麼接受方反序列化的內容可能就和發送方發送的內容不一致,嚴重的可能會致使接收方系統崩潰。
又或者是系統接收了不知道來源的字節流,並將其反序列化,假設此時沒有遭到網絡攻擊,即字節流沒有被篡改,但反序列化的時間很是長,甚至會致使OOM或者StackOverFlow。例如若是發送放發送的是一種深層次的Map結構(即Map裏內嵌了Map),假設有n層吧,那麼接收方爲了反序列化成java對象,就不得不一層一層解開,時間複雜度會是2的n次方,即指數級別的時間複雜度,機器極可能永遠沒法執行完畢,還有極大可能致使StackOverFlow並最終引發整個系統崩潰,不少拒絕服務攻擊就是這樣乾的。值得一提的是,一些結構化的數據結構(例如JSON)能夠有效避免這種狀況。
本文簡單介紹了什麼是序列化和反序列化,還順便說了一下JSON和Protobuf的簡單使用。序列化和反序列化是有必定危險的,若是不是有必要,要儘可能避免,若是不得不使用,那麼最好使用一些結構化的數據結構,例如JSON,Protobuf等,這樣至少能夠規避一種危險(在第5小結中有講到)。