乐趣区

关于java:Java-Object-Serialization中版本号不一致的兼容处理

背景

公司有个子服务较多,交互频繁的零碎,有一些须要共享传输的对象,它们通过 Java Object Serialization 后进行交互;然而因为一些不可形容的历史起因,这些对象存在多个版本,每个版本中的属性不统一,且未设置 serialVersionUID。

这阵子在做梳理 / 对立代码的工作,打算对立这些对象的版本和固定 serialVersionUID,然而因为服务较多,上线发版时会有一段新老版本共存的期间,所以得思考这些对象序列化的兼容问题,新的对象反序列化肯定得兼容老的对象

Java Object Serialization

Java 对象序列化(Serialization)是指将 Java 中的对象转为字节流,从而能够不便的存储或在网络中传输,反序列化(Deserialization)是指将字节流转位 Java 对象

个别状况下,Java Object Serialization 指的是利用 JDK 自带的性能对对象进行序列化 / 反序列化,而不是应用其余的序列化库进行(反)序列化

Java Object Serialization 中,要求对象必须实现 java.io.Serializable 接口,根本应用形式如下:

Serialization

// Serialize today's date to a file.
FileOutputStream f = new FileOutputStream("tmp");
ObjectOutput s = new ObjectOutputStream(f);
s.writeObject("Today");
s.writeObject(new Date());
s.flush();

Deserialization

// Deserialize a string and date from a file.
FileInputStream in = new FileInputStream("tmp");
ObjectInputStream s = new ObjectInputStream(in);
String today = (String)s.readObject();
Date date = (Date)s.readObject();

serialVersionUID

private static final long serialVersionUID = 1L;

Java Object Serialization 会应用对象中的 serialVersionUID 常量属性作为该对象的版本号,进行反序列化时会校验该版本号是否统一,如果不统一会导致序列化失败,抛出 InvalidClassException 异样

默认状况下,JVM 为每一个实现了 Serializable 的接口的类生成一个 serialVersionUID(long),这个 ID 的计算规定是通过以后类信息(类名、属性等)去生成的,所以当属性有变更时这个 serialVersionUID 也肯定会产生变更

这个 serialVersionUID 的生成,和所应用的 JDK 无关,不同的 JDK 可能会生成不一样的版本号,所以最好是手动生成一个,大多数 JAVA IDE 都会提供这个生成的性能

而且思考到理论业务场景,变更属性是常有的事,如果应用主动生成的版本号很容易造成 serialVersionUID 不统一的问题,导致反序列化失败

serialVersionUID 不统一时的兼容解决

解决这个不统一也很简略,既然反序列化时应用 ObjectInputStream 来实现,那么这里自定义一个 CompatibleInputStream 继承 ObjectInputStream,并且重写 readClassDescriptor 办法即可

当遇到指标数据 Class 版本号和本地 Class 版本号不统一时,默认应用本地版本的 Class 即可

public class CompatibleInputStream extends ObjectInputStream {private static Logger logger = LoggerFactory.getLogger(CompatibleInputStream.class);
    
    public CompatibleInputStream(InputStream in) throws IOException {super(in);
    }
    
    @Override
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {ObjectStreamClass resultClassDescriptor = super.readClassDescriptor(); // initially streams descriptor
        Class localClass; // the class in the local JVM that this descriptor represents.
        try {localClass = Class.forName(resultClassDescriptor.getName()); 
        } catch (ClassNotFoundException e) {logger.error("No local class for" + resultClassDescriptor.getName(), e);
            return resultClassDescriptor;
        }
        ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass);
        if (localClassDescriptor != null) { // only if class implements serializable
            final long localSUID = localClassDescriptor.getSerialVersionUID();
            final long streamSUID = resultClassDescriptor.getSerialVersionUID();
            if (streamSUID != localSUID) { // check for serialVersionUID mismatch.
                final StringBuffer s = new StringBuffer("Overriding serialized class version mismatch:");
                s.append("local serialVersionUID =").append(localSUID);
                s.append("stream serialVersionUID =").append(streamSUID);
                Exception e = new InvalidClassException(s.toString());
                logger.error("Potentially Fatal Deserialization Operation.", e);
                resultClassDescriptor = localClassDescriptor; // Use local class descriptor for deserialization
            }
        }
        return resultClassDescriptor;
    }
}

以上要害代码摘自 https://stackoverflow.com/a/1816711/6507948

应用形式:

// Deserialize a string and date from a file.
FileInputStream in = new FileInputStream("tmp");
// 反序列化时应用下面的 CompatibleInputStream 即可
ObjectInputStream s = new CompatibleInputStream(in);
String today = (String)s.readObject();
Date date = (Date)s.readObject();

Java 中支流的对象序列化库

  • JBoss Marshalling
  • Jackson
  • Apache avro
  • CBOR
  • MessagePack
  • Amazon Ion
  • Kryo
  • FST

参考

  • Java Object Serialization Specification
  • https://stackoverflow.com/a/1816711/6507948
退出移动版