作者 | L 责编 | 欧阳姝黎
java程序基础设施
前几天看到一个2016年挺有趣的一个故障复盘,有一哥们给底层的HSF服务返回值加了一个字段,秉承着“加字段一定是安全的”这种惯性思维就直接上线了,上线后发现这个接口成功率直接跌0,下游的服务抛出类似下面这个异常堆栈
看到这个堆栈可能有老司机已经反应过来了,下面我们就看下这种异常到底是如何发生的
Java序列化与反序列化
序列化:将对象写入到IO流中
反序列化:从IO流中恢复对象
序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
要想有序列化的能力,得实现Serializable接口,就像下面的这个例子一样:
这里面一个关键的点是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的调用链可以发现是有一个序列化节点的,其支持的序列化协议一共有四种:
dubbo序列化:阿里尚未开发成熟的高效java序列化实现,阿里不建议在生产环境使用它
hessian2序列化:hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的hessian lite,它是dubbo RPC默认启用的序列化方式
json序列化:目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,但其实现都不是特别成熟,而且json这种文本序列化性能一般不如上面两种二进制序列化。
java序列化:主要是采用JDK自带的Java序列化实现,性能很不理想。
从那个帖子看当时HSF服务提供集群设置的序列化方式是java序列化,而不是像现在一样默认hessian2,如果在RPC中使用了Java序列化,那下面的这三个坑一定注意不要踩
类实现了Serializable接口,但是却没有指定serialVersionUID
我们之前在文中提过,如果实现了Serializable的类没有指定serialVersionUID,编译器编译的时候会根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,这就决定了这个类在序列化上一定不是向前兼容的,前文中的那个故障就是踩了这个坑。我们在本地模拟一下这个case:
假如我们先有Student这样的一个类
我们将其序列化到磁盘:
然后给Student类加一个字段
我们再去解码,发现程序会抛出异常:
其实到这里我们就完整的模拟了前文中的那个故障,其根因是RPC的参数实现了Serializable接口,但是没有指定serialVersionUID,编译器会根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,当服务端类升级之后导致了服务端发送给客户端的字节流中的serialVersionUID发生了改变,因此当客户端反序列化去检查serialVersionUID字段的时候发现发生了变化被判定了异常。
父类实现了Serializable接口,并且指定了serialVersionUID但是子类没有指定serialVersionUID
我们对前面的例子中的Student类稍微改一下
其中父类长这样:
如果我们按照之前的讨论在本地进行一次序列化和反序列化,程序依然抛异常:
我们在设计类的时候公共属性要放到基类,这条经验指导放到这个case中仍然不太正确,而且这个case比上一个还要隐蔽,问题出主要是通过IDEA插件生成的serialVersionUID的修饰符是pivate导致这个字段在子类中不可见,子类中的serialVersionUID仍然是编译器自动生成的。当然可以把父类中serialVersionUID的改为非private来解这个问题,不过我仍然建议每个有序列化需求的类都显示指定serialVersionUID的值。
如果序列化遇到类之间的组合或者继承关系,则Java按照下面的规则处理:
当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化,而不管其是否实现了Serializable接口
如果子类实现了Serializable,则序列化时只序列化子类,不会序列化父类中的属性
如果父类实现了Serializable,则序列化时子类和父类都会被序列化,异常场景如本例所指
还有一点要注意:如果类的实例中有静态变量,该属性不会被序列化和反序列化
类中有枚举值
《阿里巴巴开发规约》中有这么一条:
【强制】二方库例可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的POJO对象。 说明:由于升级原因,导致双方的枚举类不尽相同,在接口解析,类反序列化时出现异常
这里会出现这样一个限制的原因是Java对枚举的序列化和反序列化采用完全不同的策略。序列化的结果中仅包含枚举的名字,而不包含枚举的具体定义,反序列化的时候客户端从序列化结果中读取枚举的name,然后调用java.lang.Enum#valueOf根据本地的枚举定义获取具体的枚举值。
我们仍然用之前的代码举例:
假如学生这个类中新增了一个校服尺码的枚举值
假如服务端此时对这个枚举进行了升级,但是客户端的二方包中仍然只有三个值:
如果服务端有逻辑给客户端返回了这个新增的枚举值:
因为客户端的二方包还没有升级,所以当客户端读到这个新的字节流并序列化的时候会因为找不到对应的枚举值而抛异常。
2016年的故障还值得我们去复盘吗
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/h6javajc/18711.html