解决Java序列化的版本冲突
序列化机制有很多种,Java原生的序列化机制是最方便、简单、直接的序列化方法之一。尽管这种机制有很多缺点,但还是有不少项目使用。我们有一个项目,将部分对象序列化后存储在Memcached中,后来程序出了升级版本。不幸的是,序列化对象对应的类有了一些变化,新版本的程序不能反序列化之前的数据。在协助升级Memcached中的数据的过程中才发现这个问题,这时便需提前将Memcached中的数据针对新版本程序做转换。 Memcached的导出机制我已经在前面的博文中作过介绍。
什么时候Java序列化会发生冲突
Java的序列化,实际上序列化的是对象的内容,也即只包括域(含父类的域)。因此类中方法的变化不会对序列化造成影响,只有类的体系和属性会对序列化造成影响。主要的导致版本冲突的变化包括:
- 删除域
- 改变类继承体系,讲一个实例域修改为静态或将非瞬时(transient)域修改为瞬时域
- 修改域类型
- 修writeObject/readObject导致不兼容的修改
- 以及其他更多….
详细请参见官方文档。
我们的程序修改了一个属性的类型,导致新版本程序不能反序列化之前的数据。
使用Reflection解决版本冲突
最早我尝试使用动态类加载和反射(Reflection),将旧版本的classes加入到类路径中,使用旧版本的classes将数据反序列化,再通过Reflection将值设置到新版本class的实例上,再将新版本的实例反序列化回Memcached中。伪代码如下:
//Add old classes to classpath and import ***.stat.***.service.impl.AAAContext; //return: new Instance of AAAContext; //param: serialized data of old instance AAAContext private byte[] getNewInstance(byte[] serializedData) throws Exception { //get the old instance by deserializing data AAAContext oldObje = deserialize(serializedData); //load new classes from external jar files URLClassLoader clazzLoader ; Class aaaContextClazz; URL jarfile = new URL("jar", "","file:E:\\workshop\\new-lib.jar!/"); URL jarfile1 = new URL("jar", "","file:E:\\workshop\\dependencies.jar!/"); clazzLoader = new URLClassLoader(new URL[]{ jarfile, jarfile1}, null); aaaContextClazz = clazzLoader.loadClass("***.stat.***.service.impl.AAAContext"); Object oldins = aaaContextClazz.newInstance(); //get setters on the new clazz, MUST find every setter method by hand coding Method m[] = aaaContextClazz.getDeclaredMethods(); m[0].invoke(ins, oldObject.getProp1()); m[1].invoke(ins, oldObject.getProp2()); .... .... //return the serialized data of new instance return serializeObj(ins); }
其中比较特殊的是 “clazzLoader = new URLClassLoader(new URL[]{ jarfile, jarfile1}, null);” 这一行代码,因为Java类加载体系的继承方式,必须指定new URLClassLoader()的第二个参数为null,去掉parent加载器。在不指定该参数时,会使用当前类加载器作为parent加载器,于是则会先从当前的类加载器体系中去寻找,只能得到类路径(classpath)下的同名类。
这种方法因为要序列化的字段太多,实现起来不易,且需要老版本的各依赖包,工程人员实施起来麻烦,故转而采取了下面的方法。
使用内置的序列化措施解决冲突
花了点时间研究Java的序列化机制后,发现有更好的方法支持序列化数据的版本变化。其中最简单的就是添加readObject/writeObject方法,自定义实现序列化,完成新旧版本间的转换。主要有下面三步要做。
第一步:自定义要更新的字段
Java序列化机制容许我们指定哪些字段要序列化(官方文档)。将类型变化了的属性的序列化类型指定为旧版本中的类型(Long.class,新类型为String.class),以兼容老版本的序列化信息。定义如下:
//定义的序列化版本ID必须与新/旧的class一致 private static final long serialVersionUID = 3495267477129405823L; private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("id", Long.class), new ObjectStreamField("aaa_BeginTime", Date.class), new ObjectStreamField("flag", Integer.class), //略去几十个 //将类型变化了的属性的序列化类型为旧版本中的类型 new ObjectStreamField("sess_parameter", Long.class) };
第二步:定义readObject方法
定义readObject方法,读取老版本的数据,并做新旧类型转换:
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { // Read version one types ObjectInputStream.GetField fields = ois.readFields(); this.id = (Long)fields.get("id", null); this.aaa_BeginTime = (Date)fields.get("aaa_BeginTime", null); this.flag = (Integer)fields.get("flag", null); //略去几十个 //按旧版本读取,在转化为新版本的类型 Long old_param = (Long)fields.get("sess_parameter", null); this.sess_parameter = String.valueOf(old_param); }
第三步,定义writeObject方法
定义writeObject方法,以新版本class的序列化格式得到新的序列化数据:
private void writeObject(ObjectOutputStream oos) throws IOException { PutField fields = oos.putFields(); fields.put("id", this.id); fields.put("aaa_BeginTime", this.aaa_BeginTime); fields.put("RoamFlag", this.RoamFlag); //按新版本方式写入 fields.put("sess_parameter", this.sess_parameter); oos.writeFields(); }
以这种方式,只需要新增一个类就完成了转化工作。 这个类不是新版本程序中的类,实际上只是用于中间转化操作的临时代码,它以旧版本class的序列化方式读取数据,再按新版本class的序列化方式输出数据。
教训
使用默认的Java序列化机制,会使得具体的类成为数据的schema,持久化的数据受限于类的实现细节。这带来了多方面的约束。所以,如果你用Java的原生序列化,起码应该制定一个合适的Schema,并清楚类的实现细节变化可能导致数据需要特别的迁移处理。我建议选择一种别的与类的实现细节无关的序列化方案。
参考资料
- Java Serialization Specification (1.5)
- Type Changes Affecting Serialization
- Java序列化算法透析: 国人写的一篇Java序列化算法的分析,解析很透彻
- The Java serialization algorithm revealed : 一篇英文的有关序列化机制的分析文章,与前一篇参考很接近。
- Implementing Serializable : 简明扼要的概述了序列化机制
- Advanced Serialization
- Discover the secrets of the Java Serialization API
- Dynamic Class Loading and Reloading in Java
Recent Comments