Serializable 之 SerialVersionUID

Serializable 之 SerialVersionUID

本文不讲基本概念问题,如有需要 请另行查阅其他资料

最近在开发过程中遇到了InvalidClassException,也是基础不牢的缘故,导致不能快速的发现本质问题,进行有效的处理。于是侥幸的处理完这个问题之后,就想着好好的深入研究一下(也不算很深入,只是阅读了一下源码)。

正文

在Java中,存在Serializable接口,对它的实现 无需override什么方法,JVM会在底层帮你实现(其实也不是很低层,也在Java中,但是不在本文讨论范围内),不过需要关注的是一个属性 – serialVersionUID(简称SUID),本文将对这个属性进行简单的讲解。

场景

  • 定义:class Something implement Serializable – 未定义serialVersionUID
  • 操作:
    • output: 实例化Something对象,并持久化至文件中 – ObjectOutputStream.writeObject(Something)
    • input: 从文件中读取Something对象 – ObjectInputStream.readObject(byte[])
  • 异常:
    • 如果在output操作进行之后,对Something进行了部分的修改,然后再进行Input操作,将会抛出一个异常:InvalidClassException(“local class incompatible: stream classdesc serialVersionUID = $suid, local class serialVersionUID = ${osc.getSerialVersionUID()}”);

相关知识介绍

ObjectStreamClass
Serialization’s descriptor for classes. It contains the name and serialVersionUID of the class. The ObjectStreamClass for a specific class loaded in this Java VM can be found/created using the lookup method.
原文中可以理解成:每个Class(对象)在序列化时,都会伴随着这样一个关联的ObjectStreamClass对象,该对象中记录了Class的名字和serialVersionUID。

问题及源码

所有源码都进行了裁剪,如愿查看全部,请自行查阅

由于serialVersionUID属性 是Optional,那ObjectStreamClass是如何获取到Class的serialVersionUID?

/**
  * Returns explicit serial version UID value declared by given class, or
  * null if none.
  */
  private static Long getDeclaredSUID(Class<?> cl) {
      try {
      	  // 反射,获取SUID属性,并严格检查static final
          Field f = cl.getDeclaredField("serialVersionUID");
          int mask = Modifier.STATIC | Modifier.FINAL;
          // 这块的逻辑不是很清楚,但是debug的结果发现,一般是mask = 26,f.getModifiers() = 24
          if ((f.getModifiers() & mask) == mask) {
              f.setAccessible(true);
              // 用于对SUID的定义long进行检查,但是存在一个问题
              // 首先,所有的异常将会被和谐
              // 其次对SUID的定义中,使用Long而不是long,将也会抛出异常
              // 即SUID的定义必须为static fianl long
              return Long.valueOf(f.getLong(null));
          }
      } catch (Exception ex) {}
      return null;
  }

那么,当Something中没有定义serialVersionUID属性呢?ObjectStreamClass又该如何是好?

/**
 * Return the serialVersionUID for this class.  The serialVersionUID
 * defines a set of classes all with the same name that have evolved from a
 * common root class and agree to be serialized and deserialized using a
 * common format.  NonSerializable classes have a serialVersionUID of 0L.
 *
 * @return  the SUID of the class described by this descriptor
 */
public long getSerialVersionUID() {
    // REMIND: synchronize instead of relying on volatile?
    if (suid == null) {
        suid = AccessController.doPrivileged(
            new PrivilegedAction<Long>() {
                public Long run() {
                	// 其中computeDefaultSUID的内容比较复杂
                	// 简单的说:SUID与(类 & 接口 & 方法 & 属性 基本上是所有)的名称和修饰符都有关系
                    return computeDefaultSUID(cl);
                    // 一下为computeDefaultSUID的部分源码
                    // if (!Serializable.class.isAssignableFrom(cl) || Proxy.isProxyClass(cl)) 
                    // { return 0L; }
                    // 正如该方法的注释所说:NonSerializable classes have a serialVersionUID of 0L.
                }
            }
        );
    }
    return suid.longValue();
}

上面展示了ObjectStreamClass与SUID相关的部分内容,接着说明一下使用情况。

在ObjectOutputStream.writeObject时,会将对象对应的SUID一并序列化

/**
 * from ObjectOutputStream
 * Writes 【representation】 of given class descriptor to stream.
 */
private void writeClassDesc(ObjectStreamClass desc, boolean unshared) throws IOException {
    writeNonProxyDesc(desc, unshared);
}
// from ObjectOutputStream
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
    throws IOException {
    bout.writeByte(TC_CLASSDESC);
	desc.writeNonProxy(this);
}

/**
 * from ObjectStreamClass
 * Writes non-proxy class descriptor information to given output stream.
 */
void writeNonProxy(ObjectOutputStream out) throws IOException {
    out.writeUTF(name); // 用于反序列化时 查找对应Class,并进行load
    out.writeLong(getSerialVersionUID());
}

而在ObjectInputStream中会load对应Class,并获取其SUID,与Output写入的SUID进行对比

/**
 * from ObjectInputStream
 * Reads in and returns (possibly null) class descriptor.  Sets passHandle
 * to class descriptor's assigned handle.  If class descriptor cannot be
 * resolved to a class in the local VM, a ClassNotFoundException is
 * associated with the class descriptor's handle.
 */
private ObjectStreamClass readClassDesc(boolean unshared) throws IOException {
    return readNonProxyDesc(unshared);
}

// from ObjectInputStream
private ObjectStreamClass readNonProxyDesc(boolean unshared)
    throws IOException {
    ObjectStreamClass desc = new ObjectStreamClass();
    // 读取byte数组,并创建ObjectStreamClass(class name & SUID)对象
    ObjectStreamClass readDesc = readDesc = readClassDescriptor();

	// 通过Class.forName(readDesc.name),获取Class对象
    if ((cl = resolveClass(readDesc)) == null) 
    { resolveEx = new ClassNotFoundException("null class"); }
    
    desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
    return desc;
}

// from ObjectStreamClass
void initNonProxy(ObjectStreamClass model, Class<?> cl, 
		ClassNotFoundException resolveEx, ObjectStreamClass superDesc)
    throws InvalidClassException {
    long suid = Long.valueOf(model.getSerialVersionUID());
    osc = lookup(cl, true); // 根据Class获取对应ObjectStreamClass对象
    // 异常的来源
    if (model.serializable == osc.serializable && !cl.isArray() && suid != osc.getSerialVersionUID()) {
        throw new InvalidClassException(osc.name,
                "local class incompatible: stream classdesc serialVersionUID = " + suid +
                        ", local class serialVersionUID = " + osc.getSerialVersionUID());
    }
}

总结

在【Java自带序列化】中,对象的序列化和反序列化会关注 SUID,在序列化时写入,在反序列化时读出并对比。

个人认为,其主要目的是为了做到版本升级的兼容性(晦涩难懂)
直白的说,就是在Something A版本的时候,进行的序列化;现在升级成Something B版本,但是仍然希望可以读取以前的数据(不然数据就浪费了)。

在这种情况下,如果Something没有自定义SUID,则必然会出现InvalidClassException。于是为了实现兼容,需要在一开始的时候,在Something上定义SUID,以保障即使Something发生变化,SUID不会变化。(所以上面说的版本设计兼容性,就是SUID保持不变)。

其他

如果开始时Something A未定义SUID,并进行了序列化操作;
然来 想使用Something B(升级版)来进行反序列化。

也是有办法的:那就是将Something B的SUID设置成Something A的SUID

但是由于Something A没有自定义SUID,所以 需要获取Something A的SUID的 算法计算值(即由Java动态生成的值),可以通过*ObjectStreamClass.lookupAny(JavaBean.class).getSerialVersionUID()*来获取。

猜你喜欢

转载自blog.csdn.net/weixin_34874025/article/details/83212102