前言
就在昨天和同事聊天聊起了序列化,我們熟知並且使用最方便的就是Serializable。
那麼爲什麼要序列化呢?
有些朋友會說:序列化主要是爲了數據持久化。
我們都知道Serializable是一個空接口,不需要我們實現任何的方法,設置可以不設置serialVersionUID,那他存在的意義是什麼呢,直接讓所有的類都可以序列化不是更簡單嗎,帶着這樣的思考,我開始研究Serializable的相關的源碼。
正文
首先我們看一下Serializable的註釋,非常的長,我簡單的概括爲一下幾點:
- 慎用Serializable
- Serializable接口僅僅是爲了標識哪些類可以被序列化。
- 如果想要序列化一個類,必須要實現Serializable接口,包括他的屬性。
- 如果父類實現了Serializable,那麼他的子類也可以被序列化。
- 子類只可以序列化父類的可見屬性,例如public,protected,或者其他情況,並且必須提供一個無參構造方法,否則會在運行時報錯。
- serialVersionUID可以理解爲版本號,如果發生了變化,會拋出InvalidClassException。
- 強烈建議手動設置serialVersionUID,兼容類發生的變化,如果沒有手動設置serialVersionUID,會根據系統算法默認生成一個serialVersionUID。
- 系統生成的serialVersionUID會因爲各種原因發生變化,例如類的屬性或方法變化,SDK版本的變化等等,所以請注意處理異常。
- 強烈建議使用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接口的不足:
- 太多的循環和遞歸遍歷,讀寫效率確實有隱患。
- 不能有選擇的對屬性序列化和反序列,靈活性差。
- 兼容性差,不指定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註解的屬性,感謝葉落清秋的評論指正。