日拱一卒系列(聊一聊jdk序列化)

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毫秒

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章