成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Java序列化的這三個坑千萬要小心

開發 后端
序列化機制允許將實現序列化的Java對象轉換為字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以達到以后恢復成原來的對象。序列化機制使得對象可以脫離程序的運行而獨立存在。

 [[390519]]

前幾天看到一個2016年挺有趣的一個故障復盤,有一哥們給底層的HSF服務返回值加了一個字段,秉承著“加字段一定是安全的”這種慣性思維就直接上線了,上線后發現這個接口成功率直接跌0,下游的服務拋出類似下面這個異常堆棧 

  1. java.io.InvalidClassException:com.taobao.query.TestSerializable;  
  2.  local class incompatible: stream classdesc serialVersionUID = -7165097063094245447,local class    serialVersionUID = 6678378625230229450 

看到這個堆棧可能有老司機已經反應過來了,下面我們就看下這種異常到底是如何發生的

Java序列化與反序列化

  •  序列化:將對象寫入到IO流中
  •  反序列化:從IO流中恢復對象

序列化機制允許將實現序列化的Java對象轉換為字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以達到以后恢復成原來的對象。序列化機制使得對象可以脫離程序的運行而獨立存在。

要想有序列化的能力,得實現Serializable接口,就像下面的這個例子一樣: 

  1. public class SerializableTest implements Serializable {  
  2.     private static final long serialVersionUID = -3751255153289772365L;  

這里面一個關鍵的點是serialVersionUID,JVM會在運行時判斷類的serialVersionUID來驗證版本一致性,如果傳來的字節流中的serialVersionUID與本地相應類的serialVersionUID相同則認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常。

在上面的例子中,我們通過IDEA的插件已經自動為SerializableTest生成了一個serialVersionUID,如果我們不指定serialVersionUID,編譯器在編譯的時候也會根據類名、接口名、成員方法及屬性等來生成一個64位的哈希字段 。

Dubbo與序列化

/dev-guide/images/dubbo-extension.jpg

圖片來源:https://dubbo.apache.org/zh/docs/v2.7/dev/design/

從Dubbo的調用鏈可以發現是有一個序列化節點的,其支持的序列化協議一共有四種:

    1.  dubbo序列化:阿里尚未開發成熟的高效java序列化實現,阿里不建議在生產環境使用它

    2.  hessian2序列化:hessian是一種跨語言的高效二進制序列化方式。但這里實際不是原生的hessian2序列化,而是阿里修改過的hessian lite,它是dubbo RPC默認啟用的序列化方式

    3.  json序列化:目前有兩種實現,一種是采用的阿里的fastjson庫,另一種是采用dubbo中自己實現的簡單json庫,但其實現都不是特別成熟,而且json這種文本序列化性能一般不如上面兩種二進制序列化。

    4.  java序列化:主要是采用JDK自帶的Java序列化實現,性能很不理想。

從那個帖子看當時HSF服務提供集群設置的序列化方式是java序列化,而不是像現在一樣默認hessian2,如果在RPC中使用了Java序列化,那下面的這三個坑一定注意不要踩

類實現了Serializable接口,但是卻沒有指定serialVersionUID

我們之前在文中提過,如果實現了Serializable的類沒有指定serialVersionUID,編譯器編譯的時候會根據類名、接口名、成員方法及屬性等來生成一個64位的哈希字段,這就決定了這個類在序列化上一定不是向前兼容的,前文中的那個故障就是踩了這個坑。我們在本地模擬一下這個case:

假如我們先有Student這樣的一個類 

  1. public class Student implements Serializable {  
  2.     private static int startId = 1000 
  3.     private int id;  
  4.     public Student() {  
  5.         id = startId ++;  
  6.     }  

我們將其序列化到磁盤: 

  1. private static void serialize() {  
  2.     try {  
  3.         Student student = new Student();  
  4.         FileOutputStream fileOut =  
  5.                 new FileOutputStream("/tmp/student.ser");  
  6.         ObjectOutputStream out = new ObjectOutputStream(fileOut);  
  7.         out.writeObject(student);  
  8.         out.close();  
  9.         fileOut.close();  
  10.         System.out.printf("Serialized data is saved in /tmp/student.ser");  
  11.     } catch (  
  12.             IOException i) {  
  13.         i.printStackTrace();  
  14.     }  

然后給Student類加一個字段 

  1. public class Student implements Serializable {  
  2.     private static int startId = 1000 
  3.     private int id;  
  4.   // 注意這里我們已經加了一個屬性  
  5.     private String name;  
  6.     public Student() {  
  7.         id = startId ++;  
  8.     }  

我們再去解碼,發現程序會拋出異常: 

  1. java.io.InvalidClassException: com.idealism.base.Student; local class incompatible: stream classdesc serialVersionUID = -1534228028811562580, local class serialVersionUID = 630353564791955009 
  2.  at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699) 
  3.  at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)  
  4.  at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)  
  5.  at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)  
  6.  at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)  
  7.  at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)  
  8.  at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)  
  9.  at com.idealism.base.SerializableTest.deserialize(SerializableTest.java:34)  
  10.  at com.idealism.base.SerializableTest.main(SerializableTest.java:9) 

其實到這里我們就完整的模擬了前文中的那個故障,其根因是RPC的參數實現了Serializable接口,但是沒有指定serialVersionUID,編譯器會根據類名、接口名、成員方法及屬性等來生成一個64位的哈希字段,當服務端類升級之后導致了服務端發送給客戶端的字節流中的serialVersionUID發生了改變,因此當客戶端反序列化去檢查serialVersionUID字段的時候發現發生了變化被判定了異常。

父類實現了Serializable接口,并且指定了serialVersionUID但是子類沒有指定serialVersionUID

我們對前面的例子中的Student類稍微改一下 

  1. public class Student extends Base{  
  2.     private static int startId = 1000 
  3.     private int id;  
  4.     public Student() {  
  5.         id = startId ++;  
  6.     }  

其中父類長這樣: 

  1. public class Base implements Serializable {  
  2.     private static final long serialVersionUID = 218886242758597651L 
  3.     private Date gmtCreate;  

如果我們按照之前的討論在本地進行一次序列化和反序列化,程序依然拋異常: 

  1. java.io.InvalidClassException: com.idealism.base.Student; local class incompatible: stream classdesc serialVersionUID = 1049562984784675762, local class serialVersionUID = 7566357243685852874 
  2.  at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)  
  3.  at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)  
  4.  at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)  
  5.  at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)  
  6.  at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)  
  7.  at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)  
  8.  at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)  
  9.  at com.idealism.base.SerializableTest.deserialize(SerializableTest.java:34)  
  10.  at com.idealism.base.SerializableTest.main(SerializableTest.java:9) 

我們在設計類的時候公共屬性要放到基類,這條經驗指導放到這個case中仍然不太正確,而且這個case比上一個還要隱蔽,問題出主要是通過IDEA插件生成的serialVersionUID的修飾符是pivate導致這個字段在子類中不可見,子類中的serialVersionUID仍然是編譯器自動生成的。當然可以把父類中serialVersionUID的改為非private來解這個問題,不過我仍然建議每個有序列化需求的類都顯式指定serialVersionUID的值。

如果序列化遇到類之間的組合或者繼承關系,則Java按照下面的規則處理:

  •  當一個對象的實例變量引用其他對象,序列化該對象時也把引用對象進行序列化,而不管其是否實現了Serializable接口
  •  如果子類實現了Serializable,則序列化時只序列化子類,不會序列化父類中的屬性
  •  如果父類實現了Serializable,則序列化時子類和父類都會被序列化,異常場景如本例所指

還有一點要注意:如果類的實例中有靜態變量,改屬性不會被序列化和反序列化

類中有枚舉值

《阿里巴巴開發規約》中有這么一條:

【強制】二方庫例可以定義枚舉類型,參數可以使用枚舉類型,但是接口返回值不允許使用枚舉類型或者包含枚舉類型的POJO對象。

說明:由于升級原因,導致雙方的枚舉類不盡相同,在接口解析,類反序列化時出現異常

這里會出現這樣一個限制的原因是Java對枚舉的序列化和反序列化采用完全不同的策略。序列化的結果中僅包含枚舉的名字,而不包含枚舉的具體定義,反序列化的時候客戶端從序列化結果中讀取枚舉的name,然后調用java.lang.Enum#valueOf根據本地的枚舉定義獲取具體的枚舉值。

我們仍然用之前的代碼舉例: 

  1. public class Student implements Serializable {  
  2.     private static final long serialVersionUID = 2528736437985230667L;      
  3.     private static int startId = 1000 
  4.     private int id;  
  5.     private String name;  
  6.     // 新增字段,校服尺碼,其類型是一個枚舉  
  7.     private SchoolUniformSizeEnum schoolUniformSize;  
  8.     public Student() {  
  9.         id = startId ++;  
  10.     }  

假如學生這個類中新增了一個校服尺碼的枚舉值 

  1. public enum SchoolUniformSizeEnum {  
  2.     SMALL,  
  3.     MEDIUM,  
  4.     LARGE  

假如服務端此時對這個枚舉進行了升級,但是客戶端的二方包中仍然只有三個值: 

  1. public enum SchoolUniformSizeEnum {  
  2.     SMALL,  
  3.     MEDIUM,  
  4.     LARGE,  
  5.     OVERSIZED  

如果服務端有邏輯給客戶端返回了這個新增的枚舉值: 

  1. private static void serialize() {  
  2.     try {  
  3.         Student student = new Student();  
  4.         // 服務端升級了枚舉  
  5.         student.setSchoolUniformSize(SchoolUniformSizeEnum.OVERSIZED);  
  6.         FileOutputStream fileOut =  
  7.                 new FileOutputStream("/tmp/student.ser");  
  8.         ObjectOutputStream out = new ObjectOutputStream(fileOut);  
  9.         out.writeObject(student);  
  10.         out.close();  
  11.         fileOut.close();  
  12.         System.out.printf("Serialized data is saved in /tmp/student.ser");  
  13.     } catch (  
  14.             IOException i) {  
  15.         i.printStackTrace();  
  16.     }  

因為客戶端的二方包還沒有升級,所以當客戶端讀到這個新的字節流并序列化的時候會因為找不到對應的枚舉值而拋異常。 

  1. java.io.InvalidObjectException: enum constant OVERSIZED does not exist in class com.idealism.base.SchoolUniformSizeEnum  
  2.  at java.io.ObjectInputStream.readEnum(ObjectInputStream.java:2130)  
  3.  at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1659)  
  4.  at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2403)  
  5.  at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2327)  
  6.  at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2185)  
  7.  at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)  
  8.  at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)  
  9.  at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)  
  10.  at com.idealism.base.SerializableTest.deserialize(SerializableTest.java:36)  
  11.  at com.idealism.base.SerializableTest.main(SerializableTest.java:9) 

2016年的故障還值得我們去復盤嗎

看到這里可能有小伙伴覺得,我這輩子都不可能去修改Dubbo的序列化方式,就讓他hessian2到底吧,我不得不承認確實是這樣的。如果把序列化光限制在RPC這一個場景,未免有些狹隘。以阿里為例,其分布式緩存中間件Tair的寫接口可接受的入參就是一個Serializable,好在我們平常往緩存中塞東西都是以String為key的,但萬一有前人真的用了一個實現了Serializable的類,并且恰好沒有指定serialVersionUID,那新來的你不就正好踩坑了么。所以在遇到序列化的地方需要仔細查看有沒有踩文章中列出來的三個坑。 

 

責任編輯:龐桂玉 來源: Java知音
相關推薦

2018-04-16 10:22:08

超融合基礎設施

2023-11-03 08:14:44

CSS生成器代碼

2018-03-19 10:20:23

Java序列化反序列化

2016-12-09 09:00:32

大數據風控金融

2017-01-06 10:07:39

Linuxwindowsatime

2020-10-20 06:45:48

編程高并發

2009-06-14 22:01:27

Java對象序列化反序列化

2011-06-01 15:05:02

序列化反序列化

2022-06-08 10:40:18

顯卡礦卡暴跌

2022-08-06 08:41:18

序列化反序列化Hessian

2023-09-13 07:20:57

供應商合作生產線

2024-03-27 08:21:07

reactUpdate生命周期

2021-10-15 08:32:03

RocketMQ數據結構架構

2013-03-11 13:55:03

JavaJSON

2024-09-03 08:17:59

2015-05-08 12:41:36

C++序列化反序列化庫Kapok

2017-11-02 13:15:18

Linux

2018-04-28 11:03:58

2021-04-16 09:17:39

機器學習人工智能AI

2010-03-19 15:54:21

Java Socket
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 中文字幕高清 | 婷婷久久综合 | 少妇性l交大片免费一 | 午夜小电影 | 久久精品色欧美aⅴ一区二区 | 欧美日本韩国一区二区三区 | 久久伊人在 | 国产成人av一区二区三区 | 一区二区免费在线观看 | 国产精品久久久久久久三级 | 久久久久久久久淑女av国产精品 | 自拍偷拍中文字幕 | japanhd成人 | 成人中文字幕在线 | 久久精品国产久精国产 | 日本三级做a全过程在线观看 | 亚洲欧美综合 | 久久成人激情 | 欧美黄色一区 | 玖玖综合网 | 国产日韩欧美精品一区二区三区 | 91在线资源| 精品中文字幕久久 | 91福利网址 | 亚洲视频一区二区三区四区 | 欧美日韩精品中文字幕 | 免费激情 | 亚洲一区二区三区免费视频 | 欧美国产一区二区三区 | 欧美综合久久 | 国产精品黄色 | 成人中文字幕在线 | 亚洲一区二区中文字幕 | av黄色在线 | 午夜视频导航 | 成人免费看黄网站在线观看 | 欧美a在线 | 中文字幕在线网 | 亚洲色在线视频 | 中文字幕综合 | 国产一区亚洲 |