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()*來獲取。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章