前几天看到2016年的一个很有意思的故障回顾,有哥们在底层HSF服务的返回值上加了一个字段,秉承“加字段”吧一定是安全的。”这种惯性思维直接上线,上线后发现这个接口的成功率直接降为0,下游服务抛出类似下面java.io.InvalidClassException的异常栈:com.taobao.query.TestSerializable;localclassincompatible:streamclassdescserialVersionUID=-7165097063094245447,localclassserialVersionUID=6678378625230229450看到这个栈,一些老司机可能已经反应过来了,我们看看这个异常是怎么产生的streams:RecoveringobjectsfromIOstreams序列化机制允许将序列化的Java对象转换为字节序列n存储在磁盘上或通过网络传输以供以后恢复原始对象。序列化机制使得对象独立于程序的运行而存在。想要有序列化能力,就必须实现Serializable接口,就像下面的例子:版本一致性,如果传输的字节流中的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序列化,而是阿里修改的hessianlite,是dubboRPC默认开启的序列化方式。3、JSON序列化:目前有两种实现方式,一种是阿里的fastjson库,一种是使用dubbo实现的简单的json库,但是其实现不是特别成熟,json文本序列化的性能一般不如好象上面两个二进制序列化。4、Java序列化:主要是通过JDK自带的Java序列化实现的,性能并不理想。从那个帖子可以看出,当时HSF服务提供的集群设置的序列化方式是java序列化,而不是像现在默认的hessian2。如果在RPC中使用Java序列化,那么下面三个坑一定要注意不要踩到类实现指定了Serializable接口,却没有指定serialVersionUID。我们在之前的文章中提到过,如果实现Serializable的类没有指定serialVersionUID,编译器在编译时会根据类名、接口名、成员方法和属性生成64位版本。Hash字段,决定了这个类在序列化时一定不能向前兼容。上一篇的失败就是踩了这个坑。让我们在本地模拟这种情况:如果我们首先有一个像Student这样的类FileOutputStreamfileOut=newFileOutputStream("/tmp/student.ser");ObjectOutputStreamout=newObjectOutputStream(fileOut);out.writeObject(student);out.close();fileOut.close();System.out.printf("Serializeddataissasavedin/tmp/student.ser");}catch(IOExceptioni){i.printStackTrace();}}然后在Student类中添加一个字段publicclassStudentimplementsSerializable{privatestaticintstartId=1000;privateintid;//注意我们这里添加了一个字段属性privateStringname;publicStudent(){id=startId++;}}我们再解码一下,发现程序会抛出异常:java.io.InvalidClassException:com.idealism.base.Student;io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)在java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)atjava.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)atjava.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)atjava.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)在java.io.ObjectInputStream.readObject(ObjectInputStream.java:501)在java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)在com.idealism.base.SerializableTest.deserialize(SerializableTest.java:34)atcom。唯心主义。base.SerializableTest.main(SerializableTest.java:9)其实这里我们已经完全模拟了上一篇文章中的失败。根本原因是RPC参数实现了Serializable接口,但是没有指定serialVersionUID。编译器会使用类名、接口名、成员方法和属性等生成一个64位的哈希字段。服务器类升级时,服务器发送给客户端的字节流中的serialVersionUID发生变化。因此,当客户端反序列化时,检查serialVersionUID字段时,发现有变化,判断为异常父类实现Serializable接口并指定serialVersionUID,子类不指定serialVersionUID。让我们稍微改变一下前面例子中的Student类。看起来是这样的:publicclassBaseimplementsSerializable{privatestaticfinallongserialVersionUID=218886242758597651L;privateDategmtCreate;}如果我们按照之前的讨论在本地进行序列化和反序列化,程序还是会抛出异常:java.io.InvalidClassException:com.idealism.base.Student;localclassincompatible:streamclassdescserialVersionUID=1049562984784675762,localclassserialVersionUID=7566357243685852874atjava.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)atjava.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2001)atjava.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1848)atjava.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2158)atjava.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)atjava.io.ObjectInputStream.readObject(ObjectInputStream.java:501)atjava.io。ObjectInputStream.readObject(ObjectInputStream.java:459)atcom.idealism.base.SerializableTest.deserialize(SerializableTest.java:34)atcom.idealism.base.SerializableTest.main(SerializableTest.java:9)当我们设计类时,我们公开属性放到基类里,这个经验指南在这种情况下还是不对,而且这种情况比上一种更隐蔽。问题主要是IDEA插件生成的serialVersionUID的修饰符是private的,导致该字段在子类中是不可见的,而子类中的serialVersionUID仍然是编译器自动生成的.当然,你可以将父类中的serialVersionUID改为非private来解决这个问题,但我还是建议每个需要序列化的类都显式指定serialVersionUID的值。如果序列化遇到类之间的组合或继承关系,Java会按照以下规则处理:当一个对象的实例变量引用其他对象时,在序列化该对象时,被引用的对象也被序列化,不管它是否被引用实现与否如果实现了Serializable接口,如果子类实现了Serializable,则在序列化时只会对子类进行序列化,不会对父类中的属性进行序列化。如果父类实现了Serializable,那么在序列化时子类和父类都会被序列化。本例中提到的异常场景还有一点需要注意:如果类的实例中存在静态变量,则修改的属性不会被序列化和反序列化。《阿里巴巴开发规约》类中有一个枚举值。二方库实例可以定义枚举类型,参数可以使用枚举类型,但接口返回值不允许使用枚举类型或包含枚举类型的POJO对象。解释:由于升级导致双方枚举类不相同,接口解析和类反序列化异常。这里之所以有这样的限制,是因为Java对枚举的序列化和反序列化使用了完全不同的方法。战略。序列化后的结果只包含枚举的名称,不包含枚举的具体定义。反序列化时,客户端从序列化结果中读取枚举的名称,然后根据Local枚举定义调用java.lang.Enum#valueOf获取具体的枚举值。我们还是以之前的代码为例:publicclassStudentimplementsSerializable{privatestaticfinallongserialVersionUID=2528736437985230667L;privatestaticintstartId=1000;privateintid;privateStringname;//新增字段,校服尺码,其类型为枚举privateSchoolUniformSizeEnumschoolid++Size;Public}如果是一个新的枚举值校服尺码publicenumSchoolUniformSizeEnum{SMALL,MEDIUM,LARGE}已添加到学生类别中。如果此时服务端已经升级了这个枚举,但是客户端的二方包中仍然只有三个值:publicenumSchoolUniformSizeEnum{SMALL,MEDIUM,LARGE,OVERSIZED}如果服务端有返回这个新枚举的逻辑给客户端的值:privatestaticvoidserialize(){try{Studentstudent=newStudent();//服务端升级了枚举student.setSchoolUniformSize(SchoolUniformSizeEnum.OVERSIZED);FileOutputStreamfileOut=newFileOutputStream("/tmp/student.ser");ObjectOutputStreamout=newObjectOutputStream(fileOut);out.writeObject(student);out.close();fileOut.close();System.out.printf("Serializeddataissasavedin/tmp/student.ser");}catch(IOExceptioni){i.printStackTrace();}}由于客户端的二方包还没有升级,当客户端读取这个新的字节流并进行序列化转换时,会因为找不到对应的枚举值而抛出异常。java.io.InvalidObjectException:enumconstantOVERSIZEDdoesnotexistinclasscom.idealism.base.SchoolUniformSizeEnumatjava.io.ObjectInputStream.readEnum(ObjectInputStream.java:2130)atjava.io.ObjectInputStream.readObject0(ObjectInputStream.java:1659)atjava.Readio.ObjectInputStream.ObjectFids(java:2403)atjava.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2327)atjava.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2185)atjava.io.ObjectInputStream.readObject0(ObjectInputStream.java:1665)atjava.io。ObjectInputStream.readObject(ObjectInputStream.java:501)在java.io.ObjectInputStream.readObject(ObjectInputStream.java:459)在com.idealism.base.SerializableTest.deserialize(SerializableTest.java:36)在com.idealism.base.SerializableTest。main(SerializableTest.java:9)2016年的错,值得我们回顾吗?看到这里,可能有小伙伴会想,我这辈子不可能修改Dubbo的序列化方式,让他hessian2走到最后,我不得不承认,确实是这样。序列化光如果仅限于RPC的场景,就有点窄了。以阿里为例,其分布式缓存中间件Tair的写接口可接受的入参是一个Serializable。幸运的是,我们通常使用String作为key来往缓存中塞东西,但是万一真的有人用了如果你创建了一个实现了Serializable的类,而你没有指定serialVersionUID,那你就是新手,直接踩坑。所以,当你遇到连载的时候,需要仔细检查自己是否踩过文中所列的三个坑。
