transient的作用及序列化

沉澱所有的痛發表於2021-02-19

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();     
  1. 序列化
  • 在呼叫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]);
    }

  

相關文章