为什么阿里巴巴要求程序员谨慎修改serialVersionUID 字段的值?
The following article is from Hollis Author Hollis
每天10:24,干货准时送达!
本文经授权转载自公众号“Hollis”
序列化是一种对象持久化的手段。普遍应用在网络传输、RMI等场景中。类通过实现 java.io.Serializable
接口以启用其序列化功能。
在我的博客中,其实已经有多篇文章介绍过序列化了,对序列化的基础知识不够了解的朋友可以参考以下几篇文章:
Java对象的序列化与反序列化
深入分析Java的序列化与反序列化
单例与序列化的那些事儿
在这几篇文章中,我分别介绍过了序列化涉及到的类和接口、如何自定义序列化策略、transient关键字和序列化的关系等,还通过学习ArrayList对序列化的实现源码深入学习了序列化。并且还拓展分析了一下序列化对单例的影响等。
但是,还有一个知识点并未展开介绍,那就是关于serialVersionUID
。这个字段到底有什么用?如果不设置会怎么样?为什么《阿里巴巴Java开发手册》中有以下规定:
【强制】序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失收:如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID值。
说明:注意:serialVersionUID不一致会抛出序列化运行时异常。
在展开本文的介绍之前,先来简单介绍一些和序列化有关的知识,内容均来自文章开头的三个文章链接中。
Serializable 和 Externalizable
Java类通过实现 java.io.Serializable
接口以启用其序列化功能。未实现此接口的类将无法进行序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。
如果读者看过Serializable
的源码,就会发现,他只是一个空的接口,里面什么东西都没有。Serializable接口没有方法或字段,仅用于标识可序列化的语义。但是,如果一个类没有实现这个接口,想要被序列化的话,就会抛出java.io.NotSerializableException
异常。
它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
在进行序列化操作时,会判断要被序列化的类是否是Enum
、Array
和Serializable
类型,如果都不是则直接抛出NotSerializableException
。
Java中还提供了Externalizable
接口,也可以实现它来提供序列化能力。
Externalizable
继承自Serializable
,该接口中定义了两个抽象方法:writeExternal()
与readExternal()
。
当使用Externalizable
接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()
与readExternal()
方法。否则所有变量的值都会变成默认值。
transient
transient
关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient
变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
自定义序列化策略
在序列化过程中,如果被序列化的类中定义了writeObject
和 readObject
方法,虚拟机会试图调用对象类里的 writeObject
和 readObject
方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream
的 defaultWriteObject
方法以及 ObjectInputStream
的 defaultReadObject
方法。
用户自定义的 writeObject
和 readObject
方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
所以,对于一些特殊字段需要定义序列化的策略的时候,可以考虑使用transient修饰,并自己重写writeObject
和 readObject
方法,如java.util.ArrayList
中就有这样的实现。
以上,就是一些读者需要掌握和和序列化有关的知识。
Serializable
外,还定义了一个serialVersionUID
那么,到底什么是serialVersionUID
呢?为什么要设置这样一个字段呢?
序列化是将对象的状态信息转换为可存储或传输的形式的过程。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。
而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是我们在代码中定义的serialVersionUID
。
我们举个例子吧,看看如果serialVersionUID
被修改了会发生什么?
public class SerializableDemo1 {
public static void main(String[] args) {
//Initializes The Object
User1 user = new User1();
user.setName("hollis");
//Write Obj to File
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(oos);
}
}
}
class User1 implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
serialVersionUID
的值改为2L
。class User1 implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后执行以下代码,把文件中的对象反序列化出来:
public class SerializableDemo2 {
public static void main(String[] args) {
//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1) ois.readObject();
System.out.println(newUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行结果如下:
java.io.InvalidClassException: com.hollis.User1; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
可以发现,以上代码抛出了一个java.io.InvalidClassException
,并且指出serialVersionUID
不一致。
这是因为,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID
与本地相应实体类的serialVersionUID
进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException
。
这也是《阿里巴巴Java开发手册》中规定,在兼容性升级中,在修改类的时候,不要修改serialVersionUID
的原因。除非是完全不兼容的两个版本。所以,serialVersionUID
其实是验证版本一致性的。
如果读者感兴趣,可以把各个版本的JDK代码都拿出来看一下,那些向下兼容的类的serialVersionUID
是没有变化过的。比如String类的serialVersionUID
一直都是-6849794470754667710L
。
但是,作者认为,这个规范其实还可以再严格一些,那就是规定:
如果一个类实现了Serializable
接口,就必须手动添加一个private static final long serialVersionUID
变量,并且设置初始值。
如果我们没有在类中明确的定义一个serialVersionUID
的话,看看会发生什么。
serialVersionUID
,将其写入文件。class User1 implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class User1 implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
java.io.InvalidClassException: com.hollis.User1; local class incompatible: stream classdesc serialVersionUID = -2986778152837257883, local class serialVersionUID = 7961728318907695402
同样,抛出了InvalidClassException
,并且指出两个serialVersionUID
不同,分别是-2986778152837257883
和7961728318907695402
。
从这里可以看出,系统自己添加了一个serialVersionUID
。
所以,一旦类实现了Serializable
,就建议明确的定义一个serialVersionUID
。不然在修改类的时候,就会发生异常。
serialVersionUID
有两种显示的生成方式: 一种是默认的1L,比如:
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = xxxxL;
后面这种方式,可以借助IDE生成,后面会介绍。
知其然,要知其所以然,我们再来看看源码,分析一下为什么serialVersionUID
改变的时候会抛异常?在没有明确定义的情况下,默认的serialVersionUID
是怎么来的?
ObjectInputStream.readObject ->
readObject0 ->
readOrdinaryObject ->
readClassDesc ->
readNonProxyDesc ->
ObjectStreamClass.initNonProxy
initNonProxy
中 ,关键代码如下:serialVersionUID
做了比较,如果发现不相等,则直接抛出异常。getSerialVersionUID
方法:public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}
在没有定义serialVersionUID
的时候,会调用computeDefaultSUID
方法,生成一个默认的serialVersionUID
。
为了确保我们不会忘记定义serialVersionUID
,可以调节一下Intellij IDEA的配置,在实现Serializable
接口后,如果没定义serialVersionUID
的话,IDEA(eclipse一样)会进行提示:
并且可以一键生成一个:
serialVersionUID
是用来验证版本一致性的。所以在做兼容性升级的时候,不要改变类中serialVersionUID
的值。
特别说明一下,由于本文标题并不完全能表达本文的全部内容,这里再强调一下:serialVersionUID 既然是验证版本一致性的,在做版本升级的时候(非兼容性升级),记得要修改这个字段的值哦,这样可以避免序列化混乱。
如果一个类实现了Serializable接口,一定要记得定义serialVersionUID
,否则会发生异常。可以在IDE中通过设置,让他帮忙提示,并且可以一键快速生成一个serialVersionUID
。
-
△想知道更多?长按/扫码关注我吧↑↑↑