问题描述
最近在做一个需求的迭代过程中,遇到了一个tair(公司的一款缓存中间件,类似Redis)反序列化失败的问题,也就是把tair里缓存的值转换成对象的时候报错了。看了一下代码里tair的使用,put的时候value是对象本身,get的时候是把tair获取到的对象进行类型强制转换,类似这种:
Person person = new Person();
person.setWorkNo("123");
person.setName("张三");
tairManager.put(key, person);
//省略其他代码...
Person person2 = (Person)tairManager.get(key);
此次迭代由于业务需要,Person类需要增加一个字段,譬如年龄age。这个时候就出问题了,tair里缓存的值取出来转换成对象的时候报错,大概的意思是类型转换失败。这就很奇怪了,以前一直没问题,为什么加个字段就有问题。
原因是什么?
看了下那个类,实现了序列化接口,但没实现serialVersionUID。导致在强制类型转换过程中,一旦类出现修改,旧对象字节流反序列化过程中容易出错。这里就涉及serialVersionUID的用法了。
serialVersionUID定义和用法
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
当实现java.io.Serializable接口的实体(类)没有显式地定义一个名为serialVersionUID,类型为long的变量时,Java序列化机制会根据编译的class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID 。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行序列化和反序列化。
我们应该总是显式指定一个版本号,这样做的话我们不仅可以增强对序列化版本的控制,而且也提高了代码的可移植性。因为不同的JVM有可能使用不同的策略来计算这个版本号,那样的话同一个类在不同的JVM下也会认为是不同的版本。
如何维护serialVersionUID
- 只修改了类的方法,无需改变serialVersionUID
- 只修改了类的static变量和使用transient 修饰的实例变量,无需改变serialVersionUID
- 如果修改了实例变量的类型,例如一个变量原来是int改成了String,则反序列化会失败,需要修改serialVersionUID;如果删除了类的一些实例变量,可以兼容无需修改;如果给类增加了一些实例变量,可以兼容无需修改,只是反序列化后这些多出来的变量的值都是默认值。
原因验证
接下来我们用一个简单的demo来验证一下上面的说法
case1:
定义一个类,实现序列化接口,但不实现serialVersionUID。先进行对象序列化,然后在类中增加一个字段,再进行反序列化。
@Data
public class Person implements Serializable {
private String workNo;
private String name;
}
对象序列化:
public static void main(String[] args) throws Exception {
Person person = new Person();
person.setWorkNo("123");
person.setName("张三");
String file = "test.txt";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(person);
out.close();
System.out.println(person + "序列化成功");
}
修改类,增加一个字段:
@Data
public class Person implements Serializable {
private String workNo;
private String name;
private Integer age;
}
反序列化:
public static void main(String[] args) throws Exception {
String file = "test.txt";
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Person p2 =(Person)in.readObject();
in.close();
System.out.println(p2 + "反序列化成功");
}
运行报错:
说明serialVersionUID不匹配,无法进行反序列化。
case2:
同样的类,实现序列化接口以及serialVersionUID,先进行对象序列化,然后在类中增加一个字段,再进行反序列化。
@Data
public class Person implements Serializable {
private static final long serialVersionUID = -6302395481117489701L;
private String workNo;
private String name;
}
对象序列化:
public static void main(String[] args) throws Exception {
Person person = new Person();
person.setWorkNo("123");
person.setName("张三");
String file = "test.txt";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(person);
out.close();
System.out.println(person + "序列化成功");
}
修改类,增加一个字段:
@Data
public class Person implements Serializable {
private static final long serialVersionUID = -6302395481117489701L;
private String workNo;
private String name;
private Integer age;
}
反序列化:
public static void main(String[] args) throws Exception {
String file = "test.txt";
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Person p2 =(Person)in.readObject();
in.close();
System.out.println(p2 + "反序列化成功");
}
运行结果:
Person(workNo=123, name=张三, age=null)反序列化成功
证明了上面的说法,同时也说明了serialVersionUID的重要性。
如何规避?
回顾最初问题产生的原因,发现有2点不太规范:
1. 缓存没有设置缓存时间,这样会导致缓存里的数据永远不会过期,缓存越来越大,最终导致缓存不够用需要扩容,带来成本提升。
2. 序列化和反序列化的方式,直接对原始对象进行put和get。tair对put的对象要求实现序列化接口,但没有检查是否实现了serialVersionUID,这就带来了强制类型转换失败的隐患。不知道tair能否加个功能,检查一下缓存对象的类是否实现了serialVersionUID?另外对于使用者而言,除了直接缓存原始对象外,还可以改成缓存对象的json串,在反序列化时把json串解析成需要的对象。