序列化原理(一):從源碼理解Serializable

前言

就在昨天和同事聊天聊起了序列化,我們熟知並且使用最方便的就是Serializable。

那麼爲什麼要序列化呢?
有些朋友會說:序列化主要是爲了數據持久化。

我們都知道Serializable是一個空接口,不需要我們實現任何的方法,設置可以不設置serialVersionUID,那他存在的意義是什麼呢,直接讓所有的類都可以序列化不是更簡單嗎,帶着這樣的思考,我開始研究Serializable的相關的源碼。

正文

首先我們看一下Serializable的註釋,非常的長,我簡單的概括爲一下幾點:

  1. 慎用Serializable
  2. Serializable接口僅僅是爲了標識哪些類可以被序列化。
  3. 如果想要序列化一個類,必須要實現Serializable接口,包括他的屬性。
  4. 如果父類實現了Serializable,那麼他的子類也可以被序列化。
  5. 子類只可以序列化父類的可見屬性,例如public,protected,或者其他情況,並且必須提供一個無參構造方法,否則會在運行時報錯。
  6. serialVersionUID可以理解爲版本號,如果發生了變化,會拋出InvalidClassException。
  7. 強烈建議手動設置serialVersionUID,兼容類發生的變化,如果沒有手動設置serialVersionUID,會根據系統算法默認生成一個serialVersionUID。
  8. 系統生成的serialVersionUID會因爲各種原因發生變化,例如類的屬性或方法變化,SDK版本的變化等等,所以請注意處理異常。
  9. 強烈建議使用JSON,簡潔性,可讀性,效率都會有所提高。

通過註釋,我們可以理解Serializable的作用是爲了幫助我們標識哪些類的實例可以被序列化,並且提供了serialVersionUID作爲版本號幫助我們兼容類的改變。

之前我們提到了一個問題:如果去掉Serializable,所有的類默認可以直接序列化不行嗎?

我個人是這麼理解的:

1、從設計模式的六大準則之一,單一職責原則。一個類做越少的事就越好管理,如果所有的類都可以直接序列化,當我們想知道程序中到底序列化了哪些類,就沒有辦法進行查找和篩選。
2、Java只有單繼承,導致很多情況下不夠靈活,接口的出現大大彌補的了這一空缺,通過接口進行類型區分是非常方便的做法。
3、最關鍵的註釋已經寫的很清楚了,使用Serializable進行序列化是一個非常謹慎的行爲(各種異常),限制類的序列化是爲了保證程序的安全運行。

經過思考後,我們對Serializable的起源有了新的昇華,接下來我們看看Serializable的具體使用源碼。

ObjectOutputStream

ObjectOutputStream通過IO流的方式把對象寫入到指定的位置,例如以下代碼:

// 寫入Student對象到指定路徑的文件中
ObjectOutputStream(FileOutputStream(path))
                .apply {
                    writeObject(Student("1", "zhangsan"))
                    flush()
                    close()
                }

我們先看看文件中到底寫了什麼東西:
在這裏插入圖片描述
這是從內存讀出出來的Student對象相關的信息,我們可以看到Class類名,屬性類型和名稱以及對應的值。

接下來我們看看ObjectOutputStream是怎麼具體寫入文件的,因爲源碼非常的多,我們只截取部分關鍵的代碼。寫入對象到IO流肯定要看writeObject方法,根據代碼定位最終我們會在writeObject0()方法中看到第一個關鍵代碼:

//  你會看到通過對象的Class創建了ObjectStreamClass
// ObjectStreamClass會保存類相關的信息,包括獲取serialVersionUID
// 之後也會調用ObjectStreamClass的write方法進行寫入
ObjectStreamClass desc = ObjectStreamClass.lookup(cl, true);
...
// 此處代碼判斷對象的類型是否可以被序列化
// Class
if (obj instanceof Class) {
         writeClass((Class) obj, unshared);
} 
// ObjectStreamClass
else if (obj instanceof ObjectStreamClass) {
         writeClassDesc((ObjectStreamClass) obj, unshared);
} 
// String
else if (obj instanceof String) {
         writeString((String) obj, unshared);
 }
// 數組
 else if (cl.isArray()) {
         writeArray(obj, desc, unshared);
 }
// 枚舉
 else if (obj instanceof Enum) {
         writeEnum((Enum<?>) obj, desc, unshared);
} 
// Serializable實現序列化接口
else if (obj instanceof Serializable) {
         writeOrdinaryObject(obj, desc, unshared);
}
// 其他情況都會拋出異常
 else {
         if (extendedDebugInfo) {
                 throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
         }
}

以剛纔Student爲例,他的類型是Object(而不是Class),如果我們沒有實現Serializable接口,這裏就會拋出異常。所以這裏會執行writeOrdinaryObject方法,在writeOrdinaryObject方法中,就會調用ObjectStreamClass的寫入方法:

private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        ...
        try {
            desc.checkSerialize();
            // 開始標記位
            bout.writeByte(TC_OBJECT);
            // 調用ObjectStreamClass的寫入方法
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);
            // 判斷是否實現了Externalizable接口,我們沒有實現,所以不會走這裏
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
            	// 寫入序列化數據
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

從上面的方法裏,已經包含了所有的寫入流程:

1、寫入開始標記位
2、開始寫入ObjectStreamClass中的內容
3、寫入序列化數據

我們看一下第二步的所有核心內容:

// 首先會執行方法writeNonProxyDesc方法
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
        throws IOException
    {
    	// 描述符,表示這個一個類的描述
        bout.writeByte(TC_CLASSDESC);
        ...
        // 雖然這裏有一個判斷,實際上執行的還是 desc.writeNonProxy(this);
        // 應該是一個等待兼容修改的方法
        if (protocol == PROTOCOL_VERSION_1) {
            // do not invoke class descriptor write hook with old protocol
            desc.writeNonProxy(this);
        } else {
            writeClassDescriptor(desc);
        }
        ...
        // 寫入結束block data
        bout.writeByte(TC_ENDBLOCKDATA);
		// 
        writeClassDesc(desc.getSuperDesc(), false);
    }

	// 寫入ObjectOutputStream,此處用到了serialVersionUID
    void writeNonProxy(ObjectOutputStream out) throws IOException {
    	// 寫入類的名稱
        out.writeUTF(name);
        // 寫入SerialVersionUID
        out.writeLong(getSerialVersionUID());
        // 寫入其他標記,這裏就直接省略了
		...
		// 開始寫入屬性
        out.writeShort(fields.length);
        for (int i = 0; i < fields.length; i++) {
            ObjectStreamField f = fields[i];
            out.writeByte(f.getTypeCode());
            out.writeUTF(f.getName());
            if (!f.isPrimitive()) {
                out.writeTypeString(f.getTypeString());
            }
        }
    }

如果我們沒有設置serialVersionUID怎麼辦,系統會爲我們自動生成serialVersionUID,具體請查看ObjectStreamClass.computeDefaultSUID(),算法中與類的名稱、屬性、方法都有關係,所以我們隨意的修改都可能導致計算出不同的serialVersionUID。

最後我們回到之前的第三步:

// 寫入序列化方法
private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
    	// 得到繼承結構,開始遍歷
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            // 如果我們在類中自己定義了writeObject,會被調用,並進入到下面的代碼
            if (slotDesc.hasWriteObjectMethod()) {
                ...
            } 
            // 如果沒有自定義writeObject,會進入else
			else {
				// 此方法中會把所有的屬性都寫入進去,屬性的類型也會重新調用最開始的writeObject放方法
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

到這裏我們的序列化寫入就結束了,我們以示例代碼序列化Student到文件,回顧一下整個流程:
序列化流程圖

ObjectInputStream

弄懂了輸出流,我們再分析一下輸入流,之前我們已經弄懂了序列化的流程,所以我們可以推斷,反序列化的過程應該是相反的。

// 讀出文件出的對象
 ObjectInputStream(FileInputStream(path))
                    .apply {
                        text.text = readObject().toString()
                        close()
                    }

首先看一下readObject()方法,首先我們在readObject0()方法中看到了反序列化的類型分支:

switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }

我們寫入的類型是TC_OBJECT,最終會跟蹤到readOrdinaryObject()方法:

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }
		// 讀取序列化中的ObjectStreamClass
        ObjectStreamClass desc = readClassDesc(false);
        ...
		// 通過反射,創建對象
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            ...
        }
		// 判斷是否實現了Externalizable接口
        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } 
		// 我們並沒有實現Externalizable接口,會進入到else判斷
		else {
			// 讀取序列化的數據
            readSerialData(obj, desc);
        }
		
		// 判斷是否自定義了readObject方法,如果有會調用,如果沒有直接返回obj
		// 此處省略
       ...
        return obj;
    }

上面方法是反序列化的全部流程,我重點看一下讀取ObjectStreamClass的readClassDesc()方法和讀取序列化數據的readSerialData()方法。

 private ObjectStreamClass readNonProxyDesc(boolean unshared)
        throws IOException
    {
       ...
        // 創建ObjectStreamClass
        ObjectStreamClass desc = new ObjectStreamClass();
     	// 讀取ObjectStreamClass
     	ObjectStreamClass readDesc = null;
        try {
            readDesc = readClassDescriptor();
        } catch (ClassNotFoundException ex) {
            throw (IOException) new InvalidClassException(
                "failed to read class descriptor").initCause(ex);
        }
		// 各種異常處理
		...
       	//  通過讀取到ObjectStreamClass初始化desc
        desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
        ...
        // 返回desc
        return desc;
    }

讀取ObjectStreamClass最終會定位到readNonProxy()方法:

void readNonProxy(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
    	// 類名
        name = in.readUTF();
        // suid
        suid = Long.valueOf(in.readLong());
        // 各種flag
        ...
      	// 循環保存屬性到數組中
        for (int i = 0; i < numFields; i++) {
            char tcode = (char) in.readByte();
            String fname = in.readUTF();
            String signature = ((tcode == 'L') || (tcode == '[')) ?
                in.readTypeString() : new String(new char[] { tcode });
            try {
                fields[i] = new ObjectStreamField(fname, signature, false);
            } catch (RuntimeException e) {
               ...
            }
        }
        computeFieldOffsets();
    }

現在已經把所有的屬性保存到數組中了,接下來調用initNonProxy()方法,這個方法中主要做了很多的判斷,用現有的Class和讀取到的Class進行對比,例如判斷serialVersionUID是否一致,否則會拋出InvalidClassException異常。

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());
}

經過讀取ObjectStreamClass,我們已經得到了初始化對象的所有類型信息,接下來是如何把對對象的屬性賦值。

private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
    	// 遍歷繼承列表,ClassDataSlot中保存的每個層級父類的信息
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            // 判斷這個屬性是否有值
            if (slots[i].hasData) {
            	// 忽略這個屬性
                if (obj == null || handles.lookupException(passHandle) != null) {
                    defaultReadFields(null, slotDesc); // skip field values
                } 
                // 是否自定義了readObject方法,此處忽略
                else if (slotDesc.hasReadObjectMethod()) {
                    ...
                }
                // 默認會進入到這裏
				 else {
                    defaultReadFields(obj, slotDesc);
                }
				...
            } 
			// 判斷在無值的情況下,是否自定了readObjectNoData方法
			// 此處忽略
			else {
               ...
            }
        }
    }

// 對屬性進行賦值操作
private void defaultReadFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {	
    	// 異常檢查等操作
    	...
		// 開始遍歷類的屬性列表
        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        for (int i = 0; i < objVals.length; i++) {
            ObjectStreamField f = fields[numPrimFields + i];
            // 注意此處會回到最開始的反序列化位置,完成屬性值的反序列化
            objVals[i] = readObject0(f.isUnshared());
            if (f.getField() != null) {
                handles.markDependency(objHandle, passHandle);
            }
        }
        // 通過反射進行賦值
        if (obj != null) {
            desc.setObjFieldValues(obj, objVals);
        }
        passHandle = objHandle;
    }

到這裏反序列化操作到此結束,我們整理一張流程圖,梳理一下反序列化的整個流程:
反序列化流程圖

總結

今天我們一起討論了序列化接口Serializable的起源和設計理念,然後通過分析源碼瞭解了Serializable的在序列化和反序列化中的作用。我們也看到了Serializable接口的不足:

  1. 太多的循環和遞歸遍歷,讀寫效率確實有隱患。
  2. 不能有選擇的對屬性序列化和反序列,靈活性差。
  3. 兼容性差,不指定serialVersionUID很容易出現反序列的崩潰問題。

正如Serializable開頭所說的:慎用Serializable,推薦使用JSON。

補充

評論裏葉落清秋提到了@Transient註解,他可以在序列化中禁止某些屬性被序列化,我重新查看了一遍源碼,之前我只重點看了write和read的過程,所以並沒有看到這個註解,重新整理髮現,在ObjectStreamClass的構造函數中就已經完成了@Transient的功能,代碼如下:

// 私有構造函數
if (serializable) {
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
             public Void run() {
                   ...
                   // 得到可以序列化的屬性的集合
                   fields = getSerialFields(cl);
                  ...     
      }
}

private static ObjectStreamField[] getSerialFields(Class<?> cl) throws InvalidClassException
{
        ObjectStreamField[] fields;
        ...
        // 進入到此方法中
        fields = getDefaultSerialFields(cl);
        ...
        return fields;
}

private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList<ObjectStreamField> list = new ArrayList<>();
        int mask = Modifier.STATIC | Modifier.TRANSIENT;

        for (int i = 0; i < clFields.length; i++) {
            // 遍歷所有的屬性,過濾掉static和transient
            if ((clFields[i].getModifiers() & mask) == 0) {
                list.add(new ObjectStreamField(clFields[i], false, true));
            }
        }
        int size = list.size();
        return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
    }

經過這次整理,我們知道了Serializable是不會序列化靜態屬性和@Transient註解的屬性,感謝葉落清秋的評論指正。

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