1.引子
我們在開發一些網絡應用,客戶端服務器模式應用場景,需要考慮數據如何在網絡中進行傳遞;應用微服務化以後,需要考慮請求報文、響應報文如何在服務之間傳遞。
像這種跨進程的相互協作,那麼客戶端與服務器之間,服務與服務之間需要有一套彼此都能夠理解的協議。比如說我們從小要學習普通話,只要大家都講普通話,那麼不管你來自東南西北哪個省,相互交流起來暢通無阻,這裏的普通話就是協議。
有了協議,自然還需要考慮發起請求一方如何把方言,轉換成普通話,這就是編碼;接收請求一方如何把普通話,轉換成方言,這就是解碼。你看編解碼我們也就很好理解了。
對於計算機來說,它老人家只認識0和1組成的二進制數據。於是我們在開發應用的時候,需要將編程語言表達的對象,轉換成二進制數據,這就是序列化;將二進制數據,再轉換回對象,這就是反序列化。你看序列化反序列化理解起來也不難。
業界常見的編解碼框架有protobuf、thrift等,今天這篇文章我們不討論protobuf、thrift框架,我想給你分享的是
-
java編程語言,有提供序列化機制嗎?
-
爲什麼jdk明明提供了序列化機制(實現Serializable接口),我們卻不推薦使用它?
-
實際應用中,我們都是基於什麼考慮,來選擇編解碼框架的?
2.案例
2.1.如何衡量選擇編解碼框架
在引子部分我們知道了,編解碼,與序列化反序列化本質上講的是一個事情。並且留下了3個問題
-
java編程語言,有提供序列化機制嗎?
-
爲什麼jdk明明提供了序列化機制(實現Serializable接口),我們卻不推薦使用它?
-
實際應用中,我們都是基於什麼考慮,來選擇編解碼框架的?
其實3個問題,歸納起來本質上是一個問題。我們一起來嘗試找到答案。首先毫無疑問,jdk有提供序列化機制,java.io.Serializable接口我們都難以忘懷。在需要將對象持久化到文件,或者網絡上進行傳輸,都會情不自禁的讓目標對象,實現Serializable接口
來看第二個問題,既然jdk提供了序列化機制,爲什麼不推薦使用它呢?要回答這個問題,我們可以通過回答第三個問題,從而一併得到第二個問題的答案。
第三個問題是:實際應用中,如何衡量選擇合適的編解碼框架?我們需要考慮這麼幾個點
-
跨語言,既然是跨進程的應用,服務與服務之間完全有可能採用不同的編程語言實現
-
編碼碼流精簡,要在網絡上進行傳輸,編碼以後的碼流要精簡(越小越好),節省帶寬
-
高性能,要求編碼解碼速度要夠快
對於跨語言、編碼碼流精簡、編解碼性能,jdk序列化機制都不能很好的支持,這就是爲什麼我們說不推薦使用jdk序列化機制的原因和理由。下面我們通過一個實際的案例,來進一步驗證
2.2.jdk序列化機制有什麼問題
我將通過通用的二進制機制,將對象編碼爲字節數組;與通過jdk序列化機制,將對象編碼爲字節數組。進行二者在
-
碼流大小
-
編碼性能
方面的對比,從而驗證我們說jdk序列化機制存在的問題。案例代碼非常簡單,我直接貼出相關的代碼,你一看應該就能明白了。其中你需要重點關注codeC()、jdkCodeC()方法的實現
2.2.1.編碼案例實現代碼
/**
* java序列化缺點分析
* 1.不能跨語言
* 2.序列化後碼流太大
* 3.序列化性能低
* @author ThinkPad
* @version 1.0
* @date 2021/4/4 12:07
*/
public class User implements java.io.Serializable{
private int userId;
private String userName;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
//==================================================
/**
* 通用二進制機制,將User對象轉換成字節數組
* @return
*/
public byte[] codeC(){
// 創建字節緩衝區
ByteBuffer buffer = ByteBuffer.allocate(1024);
// id序列化
buffer.putInt(this.userId);
// 名稱序列化
byte[] nameBytes = this.userName.getBytes();
buffer.putInt(nameBytes.length);
buffer.put(nameBytes);
buffer.flip();
// 將buffer轉換成字節數組
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
/**
* jdk序列化機制,將User對象轉換成字節數組
* @return
*/
public byte[] jdkCodeC(){
byte[] result = null;
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
try {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
// jdk序列化
oos.writeObject(this);
oos.flush();
// 獲取序列化後的字節數組
result = bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
oos.close();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
}
2.2.2.測試代碼
2.2.2.1.碼流大小測試
public static void main(String[] args) {
// 1.創建用戶對象
User user = new User();
user.setUserId(1);
user.setUserName("Hello World!");
// 2.通用二級制機制,將User對象轉換字節數組
byte[] codeC = user.codeC();
log.info("通用二級制機制,將User對象轉換字節數組,數組長度:{}",codeC.length);
log.info("---------------------華麗麗分割線----------------------");
// 3.jdk對象序列化機制,將User對象轉換成字節數組
byte[] jdkCodeC = user.jdkCodeC();
log.info("jdk對象序列化機制,將User對象轉換成字節數組,數組長度:{}", jdkCodeC.length);
}
通用二級制機制,將User對象轉換字節數組,數組長度:20
----------------------華麗麗分割線----------------------
jdk對象序列化機制,將User對象轉換成字節數組,數組長度:111
2.2.2.2.編碼性能測試
public static void main(String[] args) {
// 1.創建用戶對象
User user = new User();
user.setUserId(1);
user.setUserName("Hello World!");
// 性能測試對比
int loop = 1000000;
long start = System.currentTimeMillis();
for(int i=0; i<loop; i++){
user.codeC();
}
long end = System.currentTimeMillis();
log.info("通用二進制機制,執行{}次數,共耗時{}毫秒",loop,(end - start));
log.info("---------------------華麗麗分割線----------------------");
long start1 = System.currentTimeMillis();
for(int i=0; i<loop; i++){
user.jdkCodeC();
}
long end1 = System.currentTimeMillis();
log.info("jdk對象序列化機制,執行{}次數,共耗時{}毫秒",loop,(end1 - start1));
}
通用二進制機制,執行1000000次數,共耗時287毫秒
---------------------華麗麗分割線----------------------
jdk對象序列化機制,執行1000000次數,共耗時1372毫秒