1.transient 介紹
Java中的transient關鍵字,transient是短暫的意思。對於transient 修飾的成員變數,在類的例項物件的序列化處理過程中會被忽略。 因此,transient變數不會貫穿物件的序列化和反序列化,生命週期僅存於呼叫者的記憶體中而不會寫到磁碟裡進行持久化。
(1)序列化
Java中物件的序列化指的是將物件轉換成以位元組序列的形式來表示,這些位元組序列包含了物件的資料和資訊,一個序列化後的物件可以被寫到資料庫或檔案中,也可用於網路傳輸。一般地,當我們使用快取cache(記憶體空間不夠有可能會本地儲存到硬碟)或遠端呼叫rpc(網路傳輸)的時候,經常需要讓實體類實現Serializable介面,目的就是為了讓其可序列化。當然,序列化後的最終目的是為了反序列化,恢復成原先的Java物件例項。所以序列化後的位元組序列都是可以恢復成Java物件的,這個過程就是反序列化。
(2)為什麼要用transient關鍵字?
在持久化物件時,對於一些特殊的資料成員(如使用者的密碼,銀行卡號等),我們不想用序列化機制來儲存它。為了在一個特定物件的一個成員變數上關閉序列化,可以在這個成員變數前加上關鍵字transient。
(3)transient的作用
transient是Java語言的關鍵字,用來表示一個成員變數不是該物件序列化的一部分。當一個物件被序列化的時候,transient型變數的值不包括在序列化的結果中。而非transient型的變數是被包括進去的。 注意static修飾的靜態變數天然就是不可序
2. transient 使用總結
(1)一旦變數被transient修飾,變數將不再是物件持久化的一部分,該變數內容在序列化後無法被訪問。
(2) transient關鍵字只能修飾變數,而不能修飾方法和類。注意,本地變數是不能被transient關鍵字修飾的。變數如果是使用者自定義類變數,則該類需要實現Serializable介面。
(3)一個靜態變數不管是否被transient修飾,均不能被序列化(如果反序列化後類中static變數還有值,則值為當前JVM中對應static變數的值)。序列化儲存的是物件狀態,靜態變數儲存的是類狀態,因此序列化並不儲存靜態變數。
3. 序列化及反序列化
- 序列化
是指將Java物件儲存為二進位制位元組碼的過程。
- 反序列化
將二進位制位元組碼重新轉成Java物件的過程。
(1)為什麼序列化
- 我們知道,一般Java物件的生命週期比Java虛擬機器短,而實際的開發中,我們需要
在Jvm停止後能夠繼續持有物件,這個時候就需要用到序列化技術將物件持久到磁碟或資料庫。
- 在多個專案進行RPC呼叫的,需要在網路上傳輸JavaBean物件。我們知道資料只能以二進位制的
形式才能在網路上進行傳輸。所以也需要用到序列化技術。
(2)序列化的底層原理
1. 程式入口:
writeObject(obj)
Student stu1 = new Student(1001, "jack", "play"); Student stu2 = new Student(1002, "tom", "sleep"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\stu.dat")); oos.writeObject(stu1); oos.writeObject(stu2); oos.close();
- 序列化
- 在呼叫writeObject()方法之前,會先呼叫ObjectOutputStream的建構函式,生成
一個ObjectOutputStream物件。
public ObjectOutputStream(OutputStream out) throws IOException { verifySubclass(); // bout是底層的資料位元組容器 bout = new BlockDataOutputStream(out); handles = new HandleTable(10, (float) 3.00); subs = new ReplaceTable(10, (float) 3.00); enableOverride = false; // 寫入序列化檔案頭 writeStreamHeader(); // 設定檔案快取重新整理配置 bout.setBlockDataMode(true); if (extendedDebugInfo) { debugInfoStack = new DebugTraceInfoStack(); } else { debugInfoStack = null; } }
writeStreamHeader的方法內容如下:
protected void writeStreamHeader() throws IOException { bout.writeShort(STREAM_MAGIC); bout.writeShort(STREAM_MAGIC); }
3. 呼叫writeObject()方法進行具體的序列化寫入
public final void writeObject(Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return; } try { writeObject0(obj, false); } catch (IOException ex) { if (depth == 0) { writeFatalException(ex); } throw ex; } }
- writeObject0的具體內容
private void writeObject0(Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false); depth++; try { // handle previously written and non-replaceable objects int h; if ((obj = subs.lookup(obj)) == null) { writeNull(); return; } else if (!unshared && (h = handles.lookup(obj)) != -1) { writeHandle(h); return; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } // check for replacement object Object orig = obj; // 需要序列的物件的Class物件 Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { // 提示:跳過檢查string和陣列 // REMIND: skip this check for strings/arrays? Class<?> repCl; // 建立描述c1的ObjectStreamClass物件 desc = ObjectStreamClass.lookup(cl, true); if (!desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { break; } cl = repCl; } if (enableReplace) { Object rep = replaceObject(obj); if (rep != obj && rep != null) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true); } obj = rep; } // if object replaced, run through original checks a second time if (obj != orig) { subs.assign(orig, obj); if (obj == null) { writeNull(); return; } else if (!unshared && (h = handles.lookup(obj)) != -1) { writeHandle(h); return; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } } // remaining cases // 根據實際要寫入的型別,進行不同的寫入操作 // 由此可以看出String、Array、Enum是直接寫入操作的 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); } else if (obj instanceof Serializable) { // 實現序列化介面的都會執行下面的方法 // 從這裡也可以看出Serializable是一個標記介面,其本身並沒有什麼意義 writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } } finally { depth--; bout.setBlockDataMode(oldMode); } }
從上面可以看出主要做了兩件事
- 建立了ObjectStreamClass物件
- 根據實際要寫入的型別,進行不同的寫入操作
writeOrdinaryObject()
為什麼說序列化並不安全
因為序列化的物件資料轉換為二進位制,並且完全可逆。但是在RMI呼叫時
所有private欄位的資料都以明文二進位制的形式出現在網路的套接字上,這顯然是不安全的
解決方案:
- 1、 序列化Hook化(移位和復位)
- 2、 序列資料加密和簽名
- 3、 利用transient的特性解決
- 4、 打包和解包代理
4. transient 在序列化底層的應用
static和transient修飾的欄位不能被序列化。
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++) { 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]); }