序列化方案
- Java RMI採用的是Java序列化
- Spring Cloud採用的是JSON序列化
- Dubbo雖然兼容Java序列化,但默認使用的是Hessian序列化
Java序列化
原理java
Serializable數組
- JDK提供了輸入流對象ObjectInputStream和輸出流對象ObjectOutputStream
- 它們只能對實現了Serializable接口的類的對象進行序列化和反序列化
// 只能對實現了Serializable接口的類的對象進行序列化 // java.io.NotSerializableException: java.lang.Object ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH)); oos.writeObject(new Object()); oos.close();
transient安全
- ObjectOutputStream的默認序列化方式,僅對對象的非transient的實例變量進行序列化
- 不會序列化對象的transient的實例變量,也不會序列化靜態變量
@Getter public class A implements Serializable { private transient int f1 = 1; private int f2 = 2; @Getter private static final int f3 = 3; } // 序列化 // 僅對對象的非transient的實例變量進行序列化 A a1 = new A(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH)); oos.writeObject(a1); oos.close(); // 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH)); A a2 = (A) ois.readObject(); log.info("f1={}, f2={}, f3={}", a2.getF1(), a2.getF2(), a2.getF3()); // f1=0, f2=2, f3=3 ois.close();
serialVersionUID網絡
- 在實現了Serializable接口的類的對象中,會生成一個serialVersionUID的版本號
- 在反序列化過程當中用來驗證序列化對象是否加載了反序列化的類
- 若是是具備相同類名的不一樣版本號的類,在反序列化中是沒法獲取對象的
@Data @AllArgsConstructor public class B implements Serializable { private static final long serialVersionUID = 1L; private int id; } @Test public void test3() throws Exception { // 序列化 B b1 = new B(1); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH)); oos.writeObject(b1); oos.close(); } @Test public void test4() throws Exception { // 若是先將B的serialVersionUID修改成1,直接反序列化磁盤上的文件,會報異常 // java.io.InvalidClassException: xxx.B; local class incompatible: stream classdesc serialVersionUID = 0, local class serialVersionUID = 1 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH)); B b2 = (B) ois.readObject(); ois.close(); }
writeObject/readObject數據結構
具體實現序列化和反序列化的是writeObject和readObject架構
@Data @AllArgsConstructor public class Student implements Serializable { private long id; private int age; private String name; // 只序列化部分字段 private void writeObject(ObjectOutputStream outputStream) throws IOException { outputStream.writeLong(id); outputStream.writeObject(name); } // 按序列化的順序進行反序列化 private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { id = inputStream.readLong(); name = (String) inputStream.readObject(); } } Student s1 = new Student(1, 12, "Bob"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH)); oos.writeObject(s1); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH)); Student s2 = (Student) ois.readObject(); log.info("s2={}", s2); // s2=Student(id=1, age=0, name=Bob) ois.close();
writeReplace/readResolve框架
- writeReplace:用在序列化以前替換序列化對象
- readResolve:用在反序列化以後對返回對象進行處理
// 反序列化會經過反射調用無參構造器返回一個新對象,破壞單例模式 // 能夠經過readResolve()來解決 public class Singleton1 implements Serializable { private static final Singleton1 SINGLETON_1 = new Singleton1(); private Singleton1() { } public static Singleton1 getInstance() { return SINGLETON_1; } } Singleton1 s1 = Singleton1.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH)); oos.writeObject(s1); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH)); Singleton1 s2 = (Singleton1) ois.readObject(); log.info("{}", s1 == s2); // false ois.close();
public class Singleton2 implements Serializable { private static final Singleton2 SINGLETON_2 = new Singleton2(); private Singleton2() { } public static Singleton2 getInstance() { return SINGLETON_2; } public Object writeRepalce() { // 序列化以前,無需替換 return this; } private Object readResolve() { // 反序列化以後,直接返回單例 return getInstance(); } } Singleton2 s1 = Singleton2.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH)); oos.writeObject(s1); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH)); Singleton2 s2 = (Singleton2) ois.readObject(); log.info("{}", s1 == s2); // true ois.close();
缺陷
沒法跨語言工具
Java序列化只適用於基於Java語言實現的框架性能
易被攻擊
1.Java序列化是不安全的學習
- Java官網:對不信任數據的反序列化,本質上來講是危險的,應該予以迴避
2.ObjectInputStream.readObject()
- 將類路徑上幾乎全部實現了Serializable接口的對象都實例化!!
- 這意味着:在反序列化字節流的過程當中,該方法能夠執行任意類型的代碼,很是危險
3.對於須要長時間進行反序列化的對象,不須要執行任何代碼,也能夠發起一次攻擊
- 攻擊者能夠建立循環對象鏈,而後將序列化後的對象傳輸到程序中進行反序列化
- 這會致使haseCode方法被調用的次數呈次方爆發式增加,從而引起棧溢出異常
4.不少序列化協議都制定了一套數據結構來保存和獲取對象,如JSON序列化、ProtocolBuf
- 它們只支持一些基本類型和數組類型,能夠避免反序列化建立一些不肯定的實例
int itCount = 27; Set root = new HashSet(); Set s1 = root; Set s2 = new HashSet(); for (int i = 0; i < itCount; i++) { Set t1 = new HashSet(); Set t2 = new HashSet(); t1.add("foo"); // 使t2不等於t1 s1.add(t1); s1.add(t2); s2.add(t1); s2.add(t2); s1 = t1; s2 = t2; } ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH)); oos.writeObject(root); oos.close(); long start = System.currentTimeMillis(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH)); ois.readObject(); log.info("take : {}", System.currentTimeMillis() - start); ois.close(); // itCount - take // 25 - 3460 // 26 - 7346 // 27 - 11161
序列化後的流太大
1.序列化後的二進制流大小能體現序列化的能力
2.序列化後的二進制數組越大,佔用的存儲空間就越多,存儲硬件的成本就越高
- 若是進行網絡傳輸,則佔用的帶寬就越多,影響到系統的吞吐量
3.Java序列化使用ObjectOutputStream來實現對象轉二進制編碼,能夠對比BIO中的 ByteBuffer實現的二進制編碼
@Data class User implements Serializable { private String userName; private String password; } User user = new User(); user.setUserName("test"); user.setPassword("test"); // ObjectOutputStream ByteArrayOutputStream os = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(user); log.info("{}", os.toByteArray().length); // 107 // NIO ByteBuffer 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(); log.info("{}", byteBuffer.remaining()); // 16
序列化速度慢
- 序列化速度是體現序列化性能的重要指標
- 若是序列化的速度慢,就會影響網絡通訊的效率,從而增長系統的響應時間
int count = 10_0000; User user = new User(); user.setUserName("test"); user.setPassword("test"); // ObjectOutputStream long t1 = System.currentTimeMillis(); for (int i = 0; i < count; i++) { ByteArrayOutputStream os = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(user); oos.flush(); oos.close(); byte[] bytes = os.toByteArray(); os.close(); } long t2 = System.currentTimeMillis(); log.info("{}", t2 - t1); // 731 // NIO ByteBuffer long t3 = System.currentTimeMillis(); for (int i = 0; i < count; 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 t4 = System.currentTimeMillis(); log.info("{}", t4 - t3); // 182
ProtoBuf
- ProtoBuf是由Google推出且支持多語言的序列化框架
- 在序列化框架性能測試報告中,ProtoBuf不管編解碼耗時,仍是二進制流壓縮大小,都表現很好
- ProtoBuf以一個.proto後綴的文件爲基礎,該文件描述了字段以及字段類型,經過工具能夠生成不一樣語言的數據結構文件
- 在序列化該數據對象的時候,ProtoBuf經過.proto文件描述來生成Protocol Buffers格式的編碼
存儲格式
- Protocol Buffers是一種輕便高效的結構化數據存儲格式
- Protocol Buffers使用T-L-V(標識-長度-字段值)的數據格式來存儲數據
- T表明字段的正數序列(tag)
- Protocol Buffers將對象中的字段與正數序列對應起來,對應關係的信息是由生成的代碼來保證的
- 在序列化的時候用整數值來代替字段名稱,傳輸流量就能夠大幅縮減
- L表明Value的字節長度,通常也只佔用一個字節
- V表明字段值通過編碼後的值
- 這種格式不須要分隔符,也不須要空格,同時減小了冗餘字段名
編碼方式
1.ProtoBuf定義了一套本身的編碼方式,幾乎能夠映射Java/Python等語言的全部基礎數據類型
2.不一樣的編碼方式能夠對應不一樣的數據類型,還能採用不一樣的存儲格式
3.對於Varint編碼的數據,因爲數據佔用的存儲空間是固定的,所以不須要存儲字節長度length,存儲方式採用T-V
4.Varint編碼是一種變長的編碼方式,每一個數據類型一個字節的最後一位是標誌位(msb)
- 0表示當前字節已是最後一個字節
- 1表示後面還有一個字節
5.對於int32類型的數字,通常須要4個字節表示,若是採用Varint編碼,對於很小的int類型數字,用1個字節就能表示
- 對於大部分整數類型數據來講,通常都是小於256,因此這樣能起到很好的數據壓縮效果
編解碼
- ProtoBuf不只壓縮存儲數據的效果好,並且編解碼的性能也是很好的
- ProtoBuf的編碼和解碼過程結合.proto文件格式,加上Protocol Buffers獨特的編碼格式
- 只須要簡單的數據運算以及位移等操做就能夠完成編碼和解碼
我是小架,須要Java學習進階架構資料。加個人交流羣
772300343 便可領取!
咱們下篇文章見!
感謝!