Java序列化的這三個(gè)坑千萬(wàn)要小心
前幾天看到一個(gè)2016年挺有趣的一個(gè)故障復(fù)盤,有一哥們給底層的HSF服務(wù)返回值加了一個(gè)字段,秉承著“加字段一定是安全的”這種慣性思維就直接上線了,上線后發(fā)現(xiàn)這個(gè)接口成功率直接跌0,下游的服務(wù)拋出類似下面這個(gè)異常堆棧
- java.io.InvalidClassException:com.taobao.query.TestSerializable;
- local class incompatible: stream classdesc serialVersionUID = -7165097063094245447,local class serialVersionUID = 6678378625230229450
看到這個(gè)堆??赡苡欣纤緳C(jī)已經(jīng)反應(yīng)過(guò)來(lái)了,下面我們就看下這種異常到底是如何發(fā)生的
Java序列化與反序列化
- 序列化:將對(duì)象寫(xiě)入到IO流中
- 反序列化:從IO流中恢復(fù)對(duì)象
序列化機(jī)制允許將實(shí)現(xiàn)序列化的Java對(duì)象轉(zhuǎn)換為字節(jié)序列,這些字節(jié)序列可以保存在磁盤上,或通過(guò)網(wǎng)絡(luò)傳輸,以達(dá)到以后恢復(fù)成原來(lái)的對(duì)象。序列化機(jī)制使得對(duì)象可以脫離程序的運(yùn)行而獨(dú)立存在。
要想有序列化的能力,得實(shí)現(xiàn)Serializable接口,就像下面的這個(gè)例子一樣:
- public class SerializableTest implements Serializable {
- private static final long serialVersionUID = -3751255153289772365L;
- }
這里面一個(gè)關(guān)鍵的點(diǎn)是serialVersionUID,JVM會(huì)在運(yùn)行時(shí)判斷類的serialVersionUID來(lái)驗(yàn)證版本一致性,如果傳來(lái)的字節(jié)流中的serialVersionUID與本地相應(yīng)類的serialVersionUID相同則認(rèn)為是一致的,可以進(jìn)行反序列化,否則就會(huì)出現(xiàn)序列化版本不一致的異常。
在上面的例子中,我們通過(guò)IDEA的插件已經(jīng)自動(dòng)為SerializableTest生成了一個(gè)serialVersionUID,如果我們不指定serialVersionUID,編譯器在編譯的時(shí)候也會(huì)根據(jù)類名、接口名、成員方法及屬性等來(lái)生成一個(gè)64位的哈希字段 。
Dubbo與序列化
/dev-guide/images/dubbo-extension.jpg
圖片來(lái)源:https://dubbo.apache.org/zh/docs/v2.7/dev/design/
從Dubbo的調(diào)用鏈可以發(fā)現(xiàn)是有一個(gè)序列化節(jié)點(diǎn)的,其支持的序列化協(xié)議一共有四種:
1. dubbo序列化:阿里尚未開(kāi)發(fā)成熟的高效java序列化實(shí)現(xiàn),阿里不建議在生產(chǎn)環(huán)境使用它
2. hessian2序列化:hessian是一種跨語(yǔ)言的高效二進(jìn)制序列化方式。但這里實(shí)際不是原生的hessian2序列化,而是阿里修改過(guò)的hessian lite,它是dubbo RPC默認(rèn)啟用的序列化方式
3. json序列化:目前有兩種實(shí)現(xiàn),一種是采用的阿里的fastjson庫(kù),另一種是采用dubbo中自己實(shí)現(xiàn)的簡(jiǎn)單json庫(kù),但其實(shí)現(xiàn)都不是特別成熟,而且json這種文本序列化性能一般不如上面兩種二進(jìn)制序列化。
4. java序列化:主要是采用JDK自帶的Java序列化實(shí)現(xiàn),性能很不理想。
從那個(gè)帖子看當(dāng)時(shí)HSF服務(wù)提供集群設(shè)置的序列化方式是java序列化,而不是像現(xiàn)在一樣默認(rèn)hessian2,如果在RPC中使用了Java序列化,那下面的這三個(gè)坑一定注意不要踩
類實(shí)現(xiàn)了Serializable接口,但是卻沒(méi)有指定serialVersionUID
我們之前在文中提過(guò),如果實(shí)現(xiàn)了Serializable的類沒(méi)有指定serialVersionUID,編譯器編譯的時(shí)候會(huì)根據(jù)類名、接口名、成員方法及屬性等來(lái)生成一個(gè)64位的哈希字段,這就決定了這個(gè)類在序列化上一定不是向前兼容的,前文中的那個(gè)故障就是踩了這個(gè)坑。我們?cè)诒镜啬M一下這個(gè)case:
假如我們先有Student這樣的一個(gè)類
- public class Student implements Serializable {
- private static int startId = 1000;
- private int id;
- public Student() {
- id = startId ++;
- }
- }
我們將其序列化到磁盤:
- private static void serialize() {
- try {
- Student student = new Student();
- FileOutputStream fileOut =
- new FileOutputStream("/tmp/student.ser");
- ObjectOutputStream out = new ObjectOutputStream(fileOut);
- out.writeObject(student);
- out.close();
- fileOut.close();
- System.out.printf("Serialized data is saved in /tmp/student.ser");
- } catch (
- IOException i) {
- i.printStackTrace();
- }
- }
然后給Student類加一個(gè)字段
- public class Student implements Serializable {
- private static int startId = 1000;
- private int id;
- // 注意這里我們已經(jīng)加了一個(gè)屬性
- private String name;
- public Student() {
- id = startId ++;
- }
- }
我們?cè)偃ソ獯a,發(fā)現(xiàn)程序會(huì)拋出異常:
- java.io.InvalidClassException: com.idealism.base.Student; local class incompatible: stream classdesc serialVersionUID = -1534228028811562580, local class serialVersionUID = 630353564791955009
- at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
- at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)
- at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)
- at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)
- at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
- at com.idealism.base.SerializableTest.deserialize(SerializableTest.java:34)
- at com.idealism.base.SerializableTest.main(SerializableTest.java:9)
其實(shí)到這里我們就完整的模擬了前文中的那個(gè)故障,其根因是RPC的參數(shù)實(shí)現(xiàn)了Serializable接口,但是沒(méi)有指定serialVersionUID,編譯器會(huì)根據(jù)類名、接口名、成員方法及屬性等來(lái)生成一個(gè)64位的哈希字段,當(dāng)服務(wù)端類升級(jí)之后導(dǎo)致了服務(wù)端發(fā)送給客戶端的字節(jié)流中的serialVersionUID發(fā)生了改變,因此當(dāng)客戶端反序列化去檢查serialVersionUID字段的時(shí)候發(fā)現(xiàn)發(fā)生了變化被判定了異常。
父類實(shí)現(xiàn)了Serializable接口,并且指定了serialVersionUID但是子類沒(méi)有指定serialVersionUID
我們對(duì)前面的例子中的Student類稍微改一下
- public class Student extends Base{
- private static int startId = 1000;
- private int id;
- public Student() {
- id = startId ++;
- }
- }
其中父類長(zhǎng)這樣:
- public class Base implements Serializable {
- private static final long serialVersionUID = 218886242758597651L;
- private Date gmtCreate;
- }
如果我們按照之前的討論在本地進(jìn)行一次序列化和反序列化,程序依然拋異常:
- java.io.InvalidClassException: com.idealism.base.Student; local class incompatible: stream classdesc serialVersionUID = 1049562984784675762, local class serialVersionUID = 7566357243685852874
- at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
- at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)
- at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)
- at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)
- at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
- at com.idealism.base.SerializableTest.deserialize(SerializableTest.java:34)
- at com.idealism.base.SerializableTest.main(SerializableTest.java:9)
我們?cè)谠O(shè)計(jì)類的時(shí)候公共屬性要放到基類,這條經(jīng)驗(yàn)指導(dǎo)放到這個(gè)case中仍然不太正確,而且這個(gè)case比上一個(gè)還要隱蔽,問(wèn)題出主要是通過(guò)IDEA插件生成的serialVersionUID的修飾符是pivate導(dǎo)致這個(gè)字段在子類中不可見(jiàn),子類中的serialVersionUID仍然是編譯器自動(dòng)生成的。當(dāng)然可以把父類中serialVersionUID的改為非private來(lái)解這個(gè)問(wèn)題,不過(guò)我仍然建議每個(gè)有序列化需求的類都顯式指定serialVersionUID的值。
如果序列化遇到類之間的組合或者繼承關(guān)系,則Java按照下面的規(guī)則處理:
- 當(dāng)一個(gè)對(duì)象的實(shí)例變量引用其他對(duì)象,序列化該對(duì)象時(shí)也把引用對(duì)象進(jìn)行序列化,而不管其是否實(shí)現(xiàn)了Serializable接口
- 如果子類實(shí)現(xiàn)了Serializable,則序列化時(shí)只序列化子類,不會(huì)序列化父類中的屬性
- 如果父類實(shí)現(xiàn)了Serializable,則序列化時(shí)子類和父類都會(huì)被序列化,異常場(chǎng)景如本例所指
還有一點(diǎn)要注意:如果類的實(shí)例中有靜態(tài)變量,改屬性不會(huì)被序列化和反序列化
類中有枚舉值
《阿里巴巴開(kāi)發(fā)規(guī)約》中有這么一條:
【強(qiáng)制】二方庫(kù)例可以定義枚舉類型,參數(shù)可以使用枚舉類型,但是接口返回值不允許使用枚舉類型或者包含枚舉類型的POJO對(duì)象。
說(shuō)明:由于升級(jí)原因,導(dǎo)致雙方的枚舉類不盡相同,在接口解析,類反序列化時(shí)出現(xiàn)異常
這里會(huì)出現(xiàn)這樣一個(gè)限制的原因是Java對(duì)枚舉的序列化和反序列化采用完全不同的策略。序列化的結(jié)果中僅包含枚舉的名字,而不包含枚舉的具體定義,反序列化的時(shí)候客戶端從序列化結(jié)果中讀取枚舉的name,然后調(diào)用java.lang.Enum#valueOf根據(jù)本地的枚舉定義獲取具體的枚舉值。
我們?nèi)匀挥弥暗拇a舉例:
- public class Student implements Serializable {
- private static final long serialVersionUID = 2528736437985230667L;
- private static int startId = 1000;
- private int id;
- private String name;
- // 新增字段,校服尺碼,其類型是一個(gè)枚舉
- private SchoolUniformSizeEnum schoolUniformSize;
- public Student() {
- id = startId ++;
- }
- }
假如學(xué)生這個(gè)類中新增了一個(gè)校服尺碼的枚舉值
- public enum SchoolUniformSizeEnum {
- SMALL,
- MEDIUM,
- LARGE
- }
假如服務(wù)端此時(shí)對(duì)這個(gè)枚舉進(jìn)行了升級(jí),但是客戶端的二方包中仍然只有三個(gè)值:
- public enum SchoolUniformSizeEnum {
- SMALL,
- MEDIUM,
- LARGE,
- OVERSIZED
- }
如果服務(wù)端有邏輯給客戶端返回了這個(gè)新增的枚舉值:
- private static void serialize() {
- try {
- Student student = new Student();
- // 服務(wù)端升級(jí)了枚舉
- student.setSchoolUniformSize(SchoolUniformSizeEnum.OVERSIZED);
- FileOutputStream fileOut =
- new FileOutputStream("/tmp/student.ser");
- ObjectOutputStream out = new ObjectOutputStream(fileOut);
- out.writeObject(student);
- out.close();
- fileOut.close();
- System.out.printf("Serialized data is saved in /tmp/student.ser");
- } catch (
- IOException i) {
- i.printStackTrace();
- }
- }
因?yàn)榭蛻舳说亩桨€沒(méi)有升級(jí),所以當(dāng)客戶端讀到這個(gè)新的字節(jié)流并序列化的時(shí)候會(huì)因?yàn)檎也坏綄?duì)應(yīng)的枚舉值而拋異常。
- java.io.InvalidObjectException: enum constant OVERSIZED does not exist in class com.idealism.base.SchoolUniformSizeEnum
- at java.io.ObjectInputStream.readEnum(ObjectInputStream.java:2130)
- at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1659)
- at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2403)
- at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2327)
- at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2185)
- at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)
- at java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)
- at com.idealism.base.SerializableTest.deserialize(SerializableTest.java:36)
- at com.idealism.base.SerializableTest.main(SerializableTest.java:9)
2016年的故障還值得我們?nèi)?fù)盤嗎
看到這里可能有小伙伴覺(jué)得,我這輩子都不可能去修改Dubbo的序列化方式,就讓他hessian2到底吧,我不得不承認(rèn)確實(shí)是這樣的。如果把序列化光限制在RPC這一個(gè)場(chǎng)景,未免有些狹隘。以阿里為例,其分布式緩存中間件Tair的寫(xiě)接口可接受的入?yún)⒕褪且粋€(gè)Serializable,好在我們平常往緩存中塞東西都是以String為key的,但萬(wàn)一有前人真的用了一個(gè)實(shí)現(xiàn)了Serializable的類,并且恰好沒(méi)有指定serialVersionUID,那新來(lái)的你不就正好踩坑了么。所以在遇到序列化的地方需要仔細(xì)查看有沒(méi)有踩文章中列出來(lái)的三個(gè)坑。