java序列化导致的问题

问题描述

最近在做一个需求的迭代过程中,遇到了一个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串解析成需要的对象。
 
最后分享一个快速生成serialVersionUID的教程:
 
posted @ 2022-06-20 21:09  黑木爷  阅读(315)  评论(0编辑  收藏  举报