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