【二】Java 序列化與反序列化

參考:Java 序列化的高級認識

一、概念

把對象轉換爲字節序列的過程稱爲對象的序列化

把字節序列恢復爲對象的過程稱爲對象的反序列化

對象的序列化主要有兩種用途:

1) 把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中;

2) 在網絡上傳送對象的字節序列。

二、JDK類庫中的序列化API

java.io.ObjectOutputStream代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。

java.io.ObjectInputStream代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化爲一個對象,並將其返回。

1.只有實現了Serializable和Externalizable接口的類的對象才能被序列化

Externalizable接口繼承自 Serializable接口,實現Externalizable接口的類完全由自身來控制序列化的行爲,而僅實現Serializable接口的類可以 採用默認的序列化方式 。

例子:

package com.sid.io;

import java.io.Serializable;

public class TestModel implements Serializable {
    private static final long serialVersionUID= -1L;

    private String name;
    private int age;

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
package com.sid.io;

import java.io.*;

public class SerializableMain {
    public static void main(String[] args) throws Exception {
        serializable();
        deserializable();
    }

    public static void serializable() throws Exception{
        TestModel testModel = new TestModel();
        testModel.setAge(22);
        testModel.setName("sid");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("Serializable.txt")));
        oos.writeObject(testModel);
        oos.close();
    }

    public static void deserializable() throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Serializable.txt")));
        TestModel testModel =(TestModel) ois.readObject();
        System.out.println(testModel.getAge());
        System.out.println(testModel.getName());
        ois.close();
    }
}

結果

2.transient關鍵字修飾

不想序列化某些字段,則該字段用transient關鍵字修飾

比如:把上訴TestModel類的name字段用transient修飾

再跑一邊SerializableMain結果:

父類的序列化與 Transient 關鍵字

情境:一個子類實現了 Serializable 接口,它的父類都沒有實現 Serializable 接口,序列化該子類對象,然後反序列化後輸出父類定義的某變量的數值,該變量數值與序列化時的數值不同。

解決要想將父類對象也序列化,就需要讓父類也實現Serializable 接口。如果父類不實現的話的,就 需要有默認的無參的構造函數。在父類沒有實現 Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構造必須先有父對象,纔有子對象,反序列化也不例外。所以反序列化時,爲了構造父對象,只能調用父類的無參構造函數作爲默認的父對象。因此當我們取父對象的變量值時,它的值是調用父類無參構造函數後的值。如果你考慮到這種序列化的情況,在父類無參構造函數中對變量進行初始化,否則的話,父類變量值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。

Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。

特性使用案例

我們熟悉使用 Transient 關鍵字可以使得字段不被序列化,那麼還有別的方法嗎?根據父類對象序列化的規則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實現 Serializable 接口,父類不實現,根據父類序列化規則,父類的字段數據將不被序列化,形成類圖如圖 2 所示。

圖 2. 案例程序類圖

圖 2. 案例程序類圖

上圖中可以看出,attr1、attr2、attr3、attr5 都不會被序列化,放在父類中的好處在於當有另外一個 Child 類時,attr1、attr2、attr3 依然不會被序列化,不用重複抒寫 transient,代碼簡潔。

3.serialVersionUID有什麼用

注意看TestModel類裏面有一個serialVersionUID

字​面​意​思​上​是​序​列​化​的​版​本​號​,凡是實現Serializable接口的類都有一個表示序列化版本標識符的靜態變量

虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。雖然兩個類的功能代碼、package、類名完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。

由於實現了Serializable接口如果沒有顯示在類中定義靜態final的serialVersionUID, Java會自動給這個class進行一個摘要算法,類似於指紋算法,只要這個文件 多一個空格,得到的UID就會截然不同的。一旦SerialversionUID 跟之前不匹配,反序列化就無法成功。

所以我們需要自己定義serialVersionUID,序列化的那一方的該類用的serialVersionUID要與反序列化的那一方的該類用的serialVersionUID一樣

顯式地定義serialVersionUID有兩種用途:

       1、 在某些場合,希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有相同的serialVersionUID;
  2、 在某些場合,不希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有不同的serialVersionUID。

4.靜態變量序列化問題

public class Test implements Serializable {
 
    private static final long serialVersionUID = 1L;
 
    public static int staticVar = 5;
 
    public static void main(String[] args) {
        try {
            //初始時staticVar爲5
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("result.obj"));
            out.writeObject(new Test());
            out.close();
 
            //序列化後修改爲10
            Test.staticVar = 10;
 
            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                    "result.obj"));
            Test t = (Test) oin.readObject();
            oin.close();
             
            //再讀取,通過t.staticVar打印新的值
            System.out.println(t.staticVar);
             
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

將對象序列化後,修改靜態變量的數值,再將序列化對象讀取出來,然後通過讀取出來的對象獲得靜態變量的數值並打印出來。這個 System.out.println(t.staticVar) 語句輸出的是 10 。

原因在於序列化時,並不保存靜態變量,序列化保存的是對象的狀態,靜態變量屬於類的狀態,因此 序列化並不保存靜態變量

 5.對敏感字段加密

情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,纔可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。

解決:在序列化過程中,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。基於這個原理,可以在實際應用中得到使用,用於敏感字段的加密工作,清單 3 展示了這個過程。

清單 3. 靜態變量序列化問題代碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

private static final long serialVersionUID = 1L;

 

   private String password = "pass";

 

   public String getPassword() {

       return password;

   }

 

   public void setPassword(String password) {

       this.password = password;

   }

 

   private void writeObject(ObjectOutputStream out) {

       try {

           PutField putFields = out.putFields();

           System.out.println("原密碼:" + password);

           password = "encryption";//模擬加密

           putFields.put("password", password);

           System.out.println("加密後的密碼" + password);

           out.writeFields();

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

 

   private void readObject(ObjectInputStream in) {

       try {

           GetField readFields = in.readFields();

           Object object = readFields.get("password", "");

           System.out.println("要解密的字符串:" + object.toString());

           password = "pass";//模擬解密,需要獲得本地的密鑰

       } catch (IOException e) {

           e.printStackTrace();

       } catch (ClassNotFoundException e) {

           e.printStackTrace();

       }

 

   }

 

   public static void main(String[] args) {

       try {

           ObjectOutputStream out = new ObjectOutputStream(

                   new FileOutputStream("result.obj"));

           out.writeObject(new Test());

           out.close();

 

           ObjectInputStream oin = new ObjectInputStream(new FileInputStream(

                   "result.obj"));

           Test t = (Test) oin.readObject();

           System.out.println("解密後的字符串:" + t.getPassword());

           oin.close();

       } catch (FileNotFoundException e) {

           e.printStackTrace();

       } catch (IOException e) {

           e.printStackTrace();

       } catch (ClassNotFoundException e) {

           e.printStackTrace();

       }

   }

在清單 3 的 writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,纔可以正確的解析出密碼,確保了數據的安全。執行清單 3 後控制檯輸出如圖 3 所示。

圖 3. 數據加密演示

圖 3. 數據加密演示

特性使用案例

RMI 技術是完全基於 Java 序列化技術的,服務器端接口調用所需要的參數對象來至於客戶端,它們通過網絡相互傳輸。這就涉及 RMI 的安全傳輸的問題。一些敏感的字段,如用戶名密碼(用戶登錄時需要對密碼進行傳輸),我們希望對其進行加密,這時,就可以採用本節介紹的方法在客戶端對密碼進行加密,服務器端進行解密,確保數據傳輸的安全性。

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