前几天看到一个2016年挺有趣的一个故障复盘,有一哥们给底层的HSF服务返回值加了一个字段,秉承着“加字段一定是安全的”这种惯性思维就直接上线了,上线后发现这个接口成功率直接跌0,下游的服务抛出类似下面这个异常堆栈

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

看到这个堆栈可能有老司机已经反应过来了,下面我们就看下这种异常到底是如何发生的

Java序列化与反序列化

  • 序列化:将对象写入到IO流中

  • 反序列化:从IO流中恢复对象

序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

要想有序列化的能力,得实现Serializable接口,就像下面的这个例子一样:

public class SerializableTest implements Serializable {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这样的一个类

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类加一个字段

public class Student implements Serializable {private static int startId = 1000;private int id;// 注意这里我们已经加了一个属性private String name;public Student() {id = startId ++;}
}

我们再去解码,发现程序会抛出异常:

java.io.InvalidClassException: com.idealism.base.Student; local class incompatible: stream classdesc serialVersionUID = -1534228028811562580, local class serialVersionUID = 630353564791955009at 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)

其实到这里我们就完整的模拟了前文中的那个故障,其根因是RPC的参数实现了Serializable接口,但是没有指定serialVersionUID,编译器会根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,当服务端类升级之后导致了服务端发送给客户端的字节流中的serialVersionUID发生了改变,因此当客户端反序列化去检查serialVersionUID字段的时候发现发生了变化被判定了异常。

父类实现了Serializable接口,并且指定了serialVersionUID但是子类没有指定serialVersionUID

我们对前面的例子中的Student类稍微改一下

public class Student extends Base{private static int startId = 1000;private int id;public Student() {id = startId ++;}
}

其中父类长这样:

public class Base implements Serializable {private static final long serialVersionUID = 218886242758597651L;private Date gmtCreate;
}

如果我们按照之前的讨论在本地进行一次序列化和反序列化,程序依然抛异常:

java.io.InvalidClassException: com.idealism.base.Student; local class incompatible: stream classdesc serialVersionUID = 1049562984784675762, local class serialVersionUID = 7566357243685852874at 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)

我们在设计类的时候公共属性要放到基类,这条经验指导放到这个case中仍然不太正确,而且这个case比上一个还要隐蔽,问题出主要是通过IDEA插件生成的serialVersionUID的修饰符是pivate导致这个字段在子类中不可见,子类中的serialVersionUID仍然是编译器自动生成的。当然可以把父类中serialVersionUID的改为非private来解这个问题,不过我仍然建议每个有序列化需求的类都显式指定serialVersionUID的值。

如果序列化遇到类之间的组合或者继承关系,则Java按照下面的规则处理:

  • 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化,而不管其是否实现了Serializable接口

  • 如果子类实现了Serializable,则序列化时只序列化子类,不会序列化父类中的属性

  • 如果父类实现了Serializable,则序列化时子类和父类都会被序列化,异常场景如本例所指

还有一点要注意:如果类的实例中有静态变量,改属性不会被序列化和反序列化

类中有枚举值

《阿里巴巴开发规约》中有这么一条:

【强制】二方库例可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的POJO对象。

说明:由于升级原因,导致双方的枚举类不尽相同,在接口解析,类反序列化时出现异常

这里会出现这样一个限制的原因是Java对枚举的序列化和反序列化采用完全不同的策略。序列化的结果中仅包含枚举的名字,而不包含枚举的具体定义,反序列化的时候客户端从序列化结果中读取枚举的name,然后调用java.lang.Enum#valueOf根据本地的枚举定义获取具体的枚举值。

我们仍然用之前的代码举例:

public class Student implements Serializable {private static final long serialVersionUID = 2528736437985230667L;private static int startId = 1000;private int id;private String name;// 新增字段,校服尺码,其类型是一个枚举private SchoolUniformSizeEnum schoolUniformSize;public Student() {id = startId ++;}
}

假如学生这个类中新增了一个校服尺码的枚举值

public enum SchoolUniformSizeEnum {SMALL,MEDIUM,LARGE
}

假如服务端此时对这个枚举进行了升级,但是客户端的二方包中仍然只有三个值:

public enum SchoolUniformSizeEnum {SMALL,MEDIUM,LARGE,OVERSIZED
}

如果服务端有逻辑给客户端返回了这个新增的枚举值:

private static void serialize() {try {Student student = new Student();// 服务端升级了枚举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();}
}

因为客户端的二方包还没有升级,所以当客户端读到这个新的字节流并序列化的时候会因为找不到对应的枚举值而抛异常。

java.io.InvalidObjectException: enum constant OVERSIZED does not exist in class com.idealism.base.SchoolUniformSizeEnumat 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年的故障还值得我们去复盘吗

看到这里可能有小伙伴觉得,我这辈子都不可能去修改Dubbo的序列化方式,就让他hessian2到底吧,我不得不承认确实是这样的。如果把序列化光限制在RPC这一个场景,未免有些狭隘。以阿里为例,其分布式缓存中间件Tair的写接口可接受的入参就是一个Serializable,好在我们平常往缓存中塞东西都是以String为key的,但万一有前人真的用了一个实现了Serializable的类,并且恰好没有指定serialVersionUID,那新来的你不就正好踩坑了么。所以在遇到序列化的地方需要仔细查看有没有踩文章中列出来的三个坑。

END

推荐好文

>>【练手项目】基于SpringBoot的ERP系统,自带进销存+财务+生产功能>>分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!
>>能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!

Java序列化的这三个坑千万要小心相关推荐

  1. Java 序列化的这三个坑千万要小心

    作者 | L       责编 | 欧阳姝黎 前几天看到一个2016年挺有趣的一个故障复盘,有一哥们给底层的HSF服务返回值加了一个字段,秉承着"加字段一定是安全的"这种惯性思维就 ...

  2. java序列化3-序列化设计防坑点

    梦回毕业季 今天跟儿子视频,突然看到自己的脸好像已经圆了,原来我已经过了三十岁了,中年发福. 敲下这几个字之时,不由回想这一路走来.我本非计算机科班出身,只是临毕业时随大流学了几个月java就出来&q ...

  3. Serializable详解(1):代码验证Java序列化与反序列化

    说明:本文为Serializable详解(1),最后两段内容在翻译上出现歧义(暂时未翻译),将在后续的Serializable(2)文中补充. 介绍:本文根据JDK英文文档翻译而成,本译文并非完全按照 ...

  4. java rest 序列化_一文看懂Java序列化

    一文看懂Java序列化 简介 首先我们看一下wiki上面对于序列化的解释. 序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓 ...

  5. Java序列化(Serialization)的理解

    2019独角兽企业重金招聘Python工程师标准>>> 1.什么是序列化 Java是面向对象的编程语言,有时需要保存对象,并在下次使用时可以顺利还原该对象.由于这种需求很常见,所以J ...

  6. java 序列化概念和作用_结合代码详细解读Java序列化与反序列化概念理解

    Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?本文围绕这些问题进行了探讨. 1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列 ...

  7. Java中如何引用另一个类里的集合_【18期】Java序列化与反序列化三连问:是什么?为什么要?如何做?...

    Java序列化与反序列化是什么? Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程: 序列化:对象序列化的最主要的用处就是在传递和保存对象 ...

  8. Java序列化,看这篇就够了!

    一.序列化的含义.意义及使用场景 序列化:将对象写入到IO流中 反序列化:从IO流中恢复对象 意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输 ...

  9. Android热补丁之Robust(三)坑和解

    在前两篇文章中,分析了 Android 热补丁框架 Robust 中,几个重要的流程包括: 补丁加载过程 基础包插桩过程 补丁包自动化生成过程 本篇文章主要分析下集成过程中遇到的坑以及分析问题的思路和 ...

最新文章

  1. 手把手教你熟悉Git操作
  2. 微信小程序开发流程介绍
  3. C#中IList与List区别
  4. 2017-2018-1 《信息安全系统设计基础》课下测试错题汇总
  5. SqlServer优化:当数据量查询不是特别多,但数据库服务器的CPU资源一直100%时,如何优化?...
  6. java 判断请求为 ajax请求_Java过滤器处理Ajax请求,Java拦截器处理Ajax请求,java 判断请求是不是ajax请求...
  7. java去除重复对象_Java19-2 集合类去除重复对象
  8. 生态系统长期观测数据产品体系
  9. Qt文档阅读笔记-Qt单元测试中模拟GUI事件
  10. Atlas与面向对象的Javascript
  11. C++实用编程——随机生成迷宫算法
  12. 材料成型计算机基础,材料成型及控制工程 主干课程
  13. 如何格式化小米云服务器,互联网要点:怎么把小米手机格式化(恢复出厂设置)...
  14. [问题]Make sure that `gem install pg -v '0.17.1'` succeeds before bundling.
  15. ubuntu16.04安装Preempt RT实时内核
  16. html制作qq会员页面,QQ会员页面导航.html
  17. 文件上传,出现400 的错误问题
  18. 【Magicavoxel简易入门教程】(二) 第二章 · 自制一个NPC导出模型优化工具使用(附下载)
  19. 2019年大学生智能车大赛室外光电组+在ROS下搭建仿真模拟环境,编程控制小车完成定位导航仿真
  20. Composing Programs 2.3 Sequence - 02

热门文章

  1. “卢十瓦”疯狂预热红米Note 8 10W“快”充不会再有了
  2. 苹果iPhone XI奋起直追?直接升级四摄镜头
  3. 传聊天宝团队解散 罗永浩已退出股东行列
  4. 弯道超车时机已来 百度:中国有机会定义AI时代的用户体验标准
  5. 突然!锤子科技天猫官方旗舰店商品全线下架 店铺撤店?!
  6. IT程序人生:学会写程序能从事哪些技术岗位?
  7. 用nohup执行python程序时,print无法输出
  8. python日志模块----logging
  9. glassfish显示不了html文件,Glassfish websocket无法正常工作(示例代码)
  10. 怎么撤销定时说说_武夷山币7省线下预约火爆!名字错了怎么办,附预约问题整理...