写点什么

开源一夏 | 一场由 serialVersionUID 引发的线上问题

  • 2022 年 8 月 29 日
    北京
  • 本文字数:1908 字

    阅读完需:约 6 分钟

既然提到了 serialVersionUID,那么首先需要了解什么是 serialVersionUID?

什么是 serialVersionUID

简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体(类)的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

当实现 java.io.Serializable 接口的实体(类)没有显式地定义一个名为 serialVersionUID,类型为 long 的变量时,Java 序列化机制会根据编译的 class 自动生成一个 serialVersionUID 作序列化版本比较用,这种情况下,只有同一次编译生成的 class 才会生成相同的 serialVersionUID 。

如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为 serialVersionUID,类型为 long 的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。

其中阿里社区 Java 开发规范也就这一点做了强制要求。

线上问题由来

那么我们遇到的线上问题就是实体类没有显式定义一个 serialVersionUID,而上线前后又对同一个实体类做个更改,导致上线前放在 redis 中的用户登录信息在新的代码上线后,用户再次通过 redis 用户信息反序列化用户对象判定用户是否登录时,无法匹配的对应对象的 serialVersionUID,导致报错

java.io.InvalidClassException: com.hollis.User1; local class incompatible: stream classdesc serialVersionUID = 3435758485680535869, local class serialVersionUID = 3435758486987535869L
复制代码

问题总结

鉴于遇到的问题,后续针对实体类的建议:

对于 serialVersionUID 是用来验证版本一致性的。所以在做兼容性升级的时候,不要改变类中 serialVersionUID 的值。

如果一个类实现了 Serializable 接口,一定要记得定义 serialVersionUID,否则会发生异常。之所以会发生异常,是因为反序列化过程中做了校验,并且如果没有明确定义的话,会根据类的属性自动生成一个,但是只有同一次编译生成的 class 才会生成相同的 serialVersionUID,升级后再次编译的 class 将无法与上个版本兼容而异常。

补充说明

Serializable 和 Externalizable

一个类如果想启用序列化功能,需要通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法进行序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。

查看 Serializable 接口的源代码可以看到接口是空的,没有任何的方法或字段,仅用于标示可序列化的语义,如果一个类没有实现这个接口,想要被序列化的话,就会抛出 java.io.NotSerializableException 异常。

Java 中还提供了 Externalizable 接口,也可以实现它来提供序列化能力。


Externalizable 继承自 Serializable,该接口中定义了两个抽象方法:writeExternal()与 readExternal()。


当使用 Externalizable 接口来进行序列化与反序列化的时候需要开发人员重写 writeExternal()与 readExternal()方法。否则所有变量的值都会变成默认值。

Externalizable 接口和 Serializable 接口的区别:

Externalizable 继承了 Serializable,该接口中定义了两个抽象方法:writeExternal()与 readExternal()。当使用 Externalizable 接口来进行序列化与反序列化的时候需要开发人员重写 writeExternal()与 readExternal()方法。如果没有在这两个方法中定义序列化实现细节,所以输出的内容为空。还有一点值得注意:在使用 Externalizable 进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现 Externalizable 接口的类必须要提供一个 public 的无参的构造器。

transient

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

开发环境配置

这里我使用的是 idea 开发工具,目前个人用过的 idea 开发工具版本有 2017 版和 2020 版,具体不同的版本实体类生成 serialVersionUID

idea2017 版

配置方案参考博文:https://developer.aliyun.com/article/831233

idea2020 版

目前我使用的是 2020 版,这里说一下 2020 版本的配置方案,2020 版的 idea 在插件库中已经无法搜索到插件了

这是需要通过配置来实现,点击【Settings...】>【Editor】>【Inspections】勾选图中两个复选框点击【OK】

这个时候如果你的代码实体类没有显式定义 serialVersionUID ,会提示

这时鼠标选中实体类,点击【Alt+Enter】组合键可以看到

点击红框选中内容即可生成 serialVersionUID

此博文既是记录个人日常问题处理,同时也希望对大家有所帮助


发布于: 19 小时前阅读数: 59
用户头像

让技术不再枯燥,让每一位技术人爱上技术 2022.07.22 加入

还未添加个人简介

评论

发布
暂无评论
开源一夏 | 一场由serialVersionUID 引发的线上问题_开源_六月的雨在infoQ_InfoQ写作社区