Java IO篇:序列化与反序列化
1、什么是序列化:
????????兩個服務之間要傳輸一個數據對象,就需要將對象轉換成二進制流,通過網絡傳輸到對方服務,再轉換成對象,供服務方法調用。這個編碼和解碼的過程稱之為序列化和反序列化。所以序列化就是把 Java 對象變成二進制形式,本質上就是一個byte[]數組。將對象序列化之后,就可以寫入磁盤進行保存或者通過網絡中輸出給遠程服務了。反之,反序列化可以從網絡或者磁盤中讀取的字節數組,反序列化成對象,在程序中使用。
2、序列化優點:
① 永久性保存對象:將對象轉為字節流存儲到硬盤上,即使 JVM 停機,字節流還會在硬盤上等待,等待下一次 JVM 啟動時,反序列化為原來的對象,并且序列化的二進制序列能夠減少存儲空間
② 方便網絡傳輸:序列化成字節流形式的對象可以方便網絡傳輸(二進制形式),節約網絡帶寬
③ 通過序列化可以在進程間傳遞對象
3、序列化的幾種方式:
參考文章:https://www.jianshu.com/p/7298f0c559dc
3.1、Java 原生序列化:
????????Java 默認通過 Serializable 接口實現序列化,只要實現了該接口,該類就會自動實現序列化與反序列化,該接口沒有任何方法,只起標識作用。Java序列化保留了對象類的元數據(如類、成員變量、繼承類信息等),以及對象數據等,兼容性最好,但不支持跨語言,而且性能一般。
????????實現 Serializable 接口的類在每次運行時,編譯器會根據類的內部實現,包括類名、接口名、方法和屬性等自動生成一個 serialVersionUID,serialVersionUID 主要用于驗證對象在反序列化過程中,序列化對象是否加載了與序列化兼容的類,如果是具有相同類名的不同版本號的類,在反序列化中是無法獲取對象的,顯式地定義 serialVersionUID 有兩種用途:
- 在某些場合,希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有相同的 serialVersionUID;
- 在某些場合,不希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有不同的 serialVersionUID;
如果源碼改變,那么重新編譯后的 serialVersionUID 可能會發生變化,因此建議一定要顯示定義 serialVersionUID 的屬性值。
3.2、Hessian 序列化:
????????Hessian 序列化是一種支持動態類型、跨語言、基于對象傳輸的網絡協議。Java 對象序列化的二進制流可以被其他語言反序列化。 Hessian 協議具有如下特性:
- 自描述序列化類型。不依賴外部描述文件或接口定義, 用一個字節表示常用
- 基礎類型,極大縮短二進制流
- 語言無關,支持腳本語言
- 協議簡單,比 Java 原生序列化高效
????????Hessian 2.0 中序列化二進制流大小是 Java 序列化的 50%,序列化耗時是 Java 序列化的 30%,反序列化耗時是 Java 反序列化的20% 。
????????Hessian 會把復雜對象所有屬性存儲在一個 Map 中進行序列化。所以在父類、子類存在同名成員變量的情況下, Hessian 序列化時,先序列化子類 ,然后序列化父類,因此反序列化結果會導致子類同名成員變量被父類的值覆蓋。
3.3、Json 序列化:
????????JSON 是一種輕量級的數據交換格式。JSON 序列化就是將數據對象轉換為 JSON 字符串,在序列化過程中拋棄了類型信息,所以反序列化時需要提供類型信息才能準確地反序列化,相比前兩種方式,JSON 可讀性比較好,方便調試。
4、為什么不建議使用Java序列化
該部分主要參考文章:為什么我不建議你使用Java序列化 - 掘金
????????目前主流框架很少使用到 Java 序列化,比如 SpringCloud 使用的 Json 序列化,Dubbo 雖然兼容 Java 序列化,但默認使用的是 Hessian 序列化。這是為什么呢?主要是因為 JDK 默認的序列化方式存在以下一些缺陷:無法跨語言、易被攻擊、序列化的流太大、序列化性能太差等
4.1、無法跨語言:
????????Java 序列化只支持 Java 語言實現的框架,其它語言大部分都沒有使用 Java 的序列化框架,也沒有實現 Java 序列化這套協議,因此,兩個不同語言編寫的應用程序之間通信,無法使用 Java 序列化實現應用服務間傳輸對象的序列化和反序列化。
4.2、易被攻擊:
????????對象是通過在 ObjectInputStream 上調用 readObject() 方法進行反序列化的,它可以將類路徑上幾乎所有實現了 Serializable 接口的對象都實例化。這意味著,在反序列化字節流的過程中,該方法可以執行任意類型的代碼,這是非常危險的。
????????對于需要長時間進行反序列化的對象,不需要執行任何代碼,也可以發起一次攻擊。攻擊者可以創建循環對象鏈,然后將序列化后的對象傳輸到程序中反序列化,這種情況會導致 hashCode 方法被調用次數呈次方爆發式增長, 從而引發棧溢出異常。
????????序列化通常會通過網絡傳輸對象,而對象中往往有敏感數據,所以序列化常常成為黑客的攻擊點,攻擊者巧妙地利用反序列化過程構造惡意代碼,使得程序在反序列化的過程中執行任意代碼。 Java 工程中廣泛使用的 Apache Commons Collections、Jackson、fastjson 等都出現過反序列化漏洞。如何防范這種黑客攻擊呢?有些對象的敏感屬性不需要進行序列化傳輸,可以加 transient 關鍵字,避免把此屬性信息轉化為序列化的二進制流,除此之外,聲明為 static 類型的成員變量也不能要序列化。如果一定要傳遞對象的敏感屬性,可以使用對稱與非對稱加密方式獨立傳輸,再使用某個方法把屬性還原到對象中。
4.3、序列化后的流太大
????????序列化后的二進制流大小能體現序列化的性能。序列化后的二進制數組越大,占用的存儲空間就越多,存儲硬件的成本就越高。如果我們是進行網絡傳輸,則占用的帶寬就更多,這時就會影響到系統的吞吐量。
????????Java 序列化中使用了 ObjectOutputStream 來實現對象轉二進制編碼,那么這種序列化機制實現的二進制編碼完成的二進制數組大小,相比于 NIO 中的 ByteBuffer 實現的二進制編碼完成的數組大小,有沒有區別呢?
我們可以通過一個簡單的例子來驗證下:
User user = new User(); user.setUserName("test"); user.setPassword("test");ByteArrayOutputStream os =new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(os); out.writeObject(user); byte[] testByte = os.toByteArray(); System.out.print("ObjectOutputStream 字節編碼長度:" + testByte.length + "\n"); ByteBuffer byteBuffer = ByteBuffer.allocate(2048);byte[] userName = user.getUserName().getBytes(); byte[] password = user.getPassword().getBytes(); byteBuffer.putInt(userName.length); byteBuffer.put(userName); byteBuffer.putInt(password.length); byteBuffer.put(password); byteBuffer.flip(); byte[] bytes = new byte[byteBuffer.remaining()]; System.out.print("ByteBuffer 字節編碼長度:" + bytes.length+ "\n");運行結果:
ObjectOutputStream 字節編碼長度:99
ByteBuffer 字節編碼長度:16
?4.4、序列化性能太差:
????????序列化的速度也是體現序列化性能的重要指標,如果序列化的速度慢,網絡通信效率就低,從而增加系統的響應時間。我們再來通過上面這個例子,來對比下 Java 序列化與 NIO 中的 ByteBuffer 編碼的性能:
User user = new User();user.setUserName("test");user.setPassword("test");long startTime = System.currentTimeMillis();for(int i=0; i<1000; i++) {ByteArrayOutputStream os =new ByteArrayOutputStream();ObjectOutputStream out = new ObjectOutputStream(os);out.writeObject(user);out.flush();out.close();byte[] testByte = os.toByteArray();os.close();}long endTime = System.currentTimeMillis();System.out.print("ObjectOutputStream 序列化時間:" + (endTime - startTime) + "\n"); long startTime1 = System.currentTimeMillis(); for(int i=0; i<1000; i++) {ByteBuffer byteBuffer = ByteBuffer.allocate( 2048);byte[] userName = user.getUserName().getBytes();byte[] password = user.getPassword().getBytes();byteBuffer.putInt(userName.length);byteBuffer.put(userName);byteBuffer.putInt(password.length);byteBuffer.put(password);byteBuffer.flip();byte[] bytes = new byte[byteBuffer.remaining()]; } long endTime1 = System.currentTimeMillis(); System.out.print("ByteBuffer 序列化時間:" + (endTime1 - startTime1)+ "\n");運行結果:
ObjectOutputStream 序列化時間:29
ByteBuffer 序列化時間:6
總結
以上是生活随笔為你收集整理的Java IO篇:序列化与反序列化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 四层LVS与七层Nginx负载均衡的区别
- 下一篇: RocketMQ-docker镜像的制作