序列化方案
- Java RMI采用的是Java序列化
- Spring Cloud采用的是JSON序列化
- Dubbo雖然兼容Java序列化,但默認使用的是Hessian序列化
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 即可領取!
我們下篇文章見!
感謝!