天天看點

漫談JAVA序列化

一、使用場景

對象序列化用來将對象編碼成位元組流(序列化),并從位元組流編碼中重新建構對象(反序列化)。一旦對象被序列化後,它的編碼就可以從一台正在運作的虛拟機上被傳輸到另一台虛拟機上,或者被存儲到磁盤上,以供後續反序列化使用。實際場景:在網絡上傳輸的對象必須要序列化,比如RMI(遠端方法調用);或需要将記憶體中的對象存儲在檔案中。

本文适合細細品嘗,猴急想學用法的小夥伴請直接跳到第三章

二、序列化的危害(勸退)

其實看到能看到這篇文章你已經誤入歧途了。早在Java中新增序列化這項功能時就被認為是有風險的,而且在使用過程中弊大于利。如果你在開發一個新的系統,請在閱讀本章之後自行學習其他跨平台結構化資料表示法來替代Java的序列化,比如JSON(基于文本的序列化方法,人類可以閱讀)或Protobuf(基于二進制,計算機閱讀;當然你想讀的話,它也提供了文本表示法)。如果你正在維護一個使用了序列化的系統或者不得不使用Java序列化,那麼請在遵循本文建議的情況下謹慎使用。

下面具體講一下Java序列化的危害。

  1. 一旦一個類實作了Serializable接口,就大大降低了改變這個類的靈活性。

    假如你現在做的功能是用戶端收到服務端序列化後的對象後将其反序列化,然後展現給使用者。第一版代碼運作得很好,突然有一天你想為序列化對象添加一個新的屬性或者新的功能,那麼用戶端就無法再反序列化這個對象了。如果隻有一個用戶端在使用,好,将用戶端的對象資訊和反序列化邏輯也改了;可是如果你釋出的是一個SDK或者一個被廣泛使用的工具類,有成千上萬的使用者在使用你暴露出的API,當你釋出一個修改了序列化對象資訊的新版本後所有用到你接口的地方都将無法反序列化(後文會将如何避免這種不相容)。是以你在釋出一個需要被序列化的類時,一定要花時間設計它,讓它盡可能地可擴充。

  2. 将不被信任的流進行反序列化可能導緻遠端代碼執行、拒絕服務以及一系列其他攻擊

    假如你實作的功能是服務端收到用戶端序列化的對象後将其反序列化,問題也是一樣(這不是廢話嘛)。但是會引起更加嚴重的問題。假設這樣一個場景,服務端需要反序列化的類中有一個屬性,其類型是HashSet。此時,惡意使用者為你精心設計了一個位元組流,他執行了下面的方法來包裝這個HashSet。這是一個100層嵌套的HashSet,每一層的Set中都包含兩個元素(第一層隻有一個),一個字元串元素"shutdown"和一個Set元素。服務端在反序列化時需要計算每個元素的散列碼,這就會導緻hashCode()函數被調用 2 100 2^{100} 2100次,這很容易就造成了一次拒絕服務攻擊。當然還有針對序列化的其他攻擊方式,這裡就不列舉了。

Set<Object> safeSet = new HashSet();
Set<Object> customerSet = safeSet;
public static byte[] unSafeFunction() {
	for(int i = 0;i < 100;i++) {
		Set<Object> tmp = new HashSet();
		tmp.put("shutdown");
		customerSet.put(tmp);
		customerSet= tmp;
	}
	return serialize(safeSet);
}
           

是以,永遠不要反序列化不被信任的資料。Java官方安全編碼指導方針中提出:“對不信任資料的反序列化,從本質上來說是危險的,應該予以避免。”

3. 反序列化會使對象的限制關系受到破壞

反序列化時并不是通過構造函數來執行個體化對象,而是直接通過流資訊重構出來的(通過readObject()方法)。如果這樣講大家沒什麼值觀感受的化,我直接上代碼。有一個表示成年人的類Adult,如果年齡小于18歲,執行個體化時将會抛出IllegalArgumentException。但是惡意使用者卻可以構造出年齡小于18歲的Adult對象流給你,反序列化後你就得到了小于18歲的成年人。再次申明,反序列化時并沒有調用構造函數,并沒有走構造函數中的邏輯。關于如何避免這個問題,會在後文提及。

public class Adult implements Serializable {
    private String name;
    private int age;
    private Date birthday;
    public Adult(String name, int age, Date birthday) {
        if(age < 18) {
            throw new IllegalArgumentException("未成年人!!!");
        }
        this.name = name;
        this.age = age;
        birthday = new Date(birthday.getTime());
    }
    public Date getBirthday() {
        return new Date(birthday.getTime());
    }
}
           

這裡再提一嘴,我們在為birthday屬性複制時采用了保護性拷貝,當傳入的Date對象改變時,我們的birthday屬性依然不會改變;而且沒有提供set方法,也就是說一旦birthday傳入後就是不可變的了。但是對象反序列化時通過一定的手段卻可以讓birthday屬性變得可變。扯遠了,有興趣的小夥伴自行了解吧。

三、序列化的使用方式

  1. 實作Serializable接口或Externalizable接口

    這裡先介紹Serializable。如下,僅僅實作Serializable接口,什麼都不用做就可以實作Adult對象的序列化。當然這裡使用的是預設的序列化方式

public class Adult implements Serializable {
    private String name;
    private int age;
    private Date birthday;
    public Adult(String name, int age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = new Date(birthday.getTime());
    }
}
           
  1. 進階,自定義序列化方式【writeObject()、readObject()】

    再回到剛剛的問題,我們需要保證反序列化的Adult對象年齡必須大于等于18歲。這時候就要重寫readObject()方法和writeObject方法。其中readObject方法定義對象序列化時的操作,writeObject()方法定義對象反序列化時的操作。

public class Adult implements Serializable {
    private String name;
    private int age;
    private Date birthday;
    public Adult(String name, int age, Date birthday) {
        this.name = name;
        this.age = age;
        if(this.age < 18) {
            throw new IllegalArgumentException("未成年人!!!");
        }
        this.birthday = new Date(birthday.getTime());
    }

    /**
     * 重寫此方法,并在此方法中定義序列化邏輯
     * 可以在此方法中做一些加密工作或其他操作
     * @param out
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(name.toUpperCase());
        out.writeInt(age + 15);
        out.writeObject(birthday);
    }

    /**
     * 重寫此方法,并在此方法中定義反序列化邏輯
     * 可以在此方法中做一些解密工作或定義一些限制關系
     * @param in
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 反序列化取對象屬性時,一定要序列化存對象屬性時的順序保持一緻
        this.name = (String) in.readObject();
        this.age = in.readInt() - 14;
        if(this.age < 18) {
            throw new IllegalArgumentException("未成年人!!!");
        }
        this.birthday = (Date) in.readObject();
    }

    @Override
    public String toString() {
        return "Adult{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                '}';
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\adult.txt"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\adult.txt"));
        Adult adult = new Adult("Mr john", 24, new Date());
        System.out.println(adult);
        oos.writeObject(adult);
        Adult adult1 = (Adult) ois.readObject();
        System.out.println(adult1);
    }
}
           

通過在writeObject()方法中限制age屬性不能小于18,如果惡意使用者再構造非法位元組流傳入,解析的時候就會直接抛出異常。此外我們還可以在writeObject()方法中定義一些加密方法,然後在readObject()方法中定義一些解密算法,讓資料傳輸變得更加安全。

  1. 為序列化類添加serialVersionUID

    自從學會了自定義序列化和反序列化方法,系統用了一段時間感覺都沒出問題,心裡美極了。突然有一天産品說需要加一個gender字段來辨別Adult對象的性别,這也太簡單了,反手就是增加一個boolean字段,空間占用少,等着被誇吧。

// true:男;false:女
private boolean gender;
           

當我們嘗試解析adult.txt時卻出問題了

public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\adult.txt"));
        Adult adult1 = (Adult) ois.readObject();
        System.out.println(adult1);
    }
           
漫談JAVA序列化

仔細一看原來是serialVersionUID不同。什麼是serialVersionUID?如果我們沒有顯示指定serialVersionUID,系統就會對這個類的結構運用一種加密的散列函數,在運作時自動産生一個辨別号。這個表示号會受到類的名稱,它實作的接口名稱,以及它的共有的和受保護的屬性名稱影響。也就是說,當我們增加了gender屬性後,目前的adult類的serialVersionUID和adult.txt中對象的serialVersionUID不同了,是以就反序列化失敗了。可是我需要維護Adult這個類啊,我怎麼知道它之前的serialVersionUID是什麼。仔細看控制台報錯,救會發現人家已經告訴我們了

什麼?你需要防患于未然,每次釋出新的序列化類時都要加上serialVersionUID,好,滿足你。

這裡提供三種辦法生成serialVersionUID:第一就是讓IDEA幫我們生成,file->settings->Inspections->serializable class without serialVersionUID打勾即可。

漫談JAVA序列化

将Adult類恢複為未添加gender字段之前的狀态,然後在類名上alt+enter即可看到提示

漫談JAVA序列化

第二就是使用serialver工具生成,用法為:serialver [-classpath類路徑] [-show] [類名稱…]。第三是自己随便定義一個serialVersionUID(不推薦)。無論如何,為需要序列化的類添加serialVersionUID是一個很好且必須的程式設計習慣,這不僅能解決一些相容問題,也會降低系統每次計算serialVersionUID時産生的開銷。

  1. 實作相容的良好程式設計習慣【defaultWriteObject()、defaultReadObject()】

    序列化規範要求我們在writeObject()方法中首先調用defaultWriteObject(),在readObject()方法中首先調用defaultReadObject()。這樣的序列化形式允許在以後的發行版中增加非瞬時(非transient,将在後面介紹)的執行個體域,并且還能保持向前或者向後相容性。如果某一個執行個體将在未來的版本中被序列化,然後在前一個版本中被反序列化,那麼,後增加的域将被忽略掉。如果舊版本的readObject()方法中沒有調用defaultReadObject(),反序列化過程将失敗,并引發StreamCorruptedException異常。

private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeObject(name.toUpperCase());
        out.writeInt(age + 15);
        out.writeObject(birthday);
        out.writeBoolean(gender);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.name = (String) in.readObject();
        this.age = in.readInt() - 14;
        if(this.age < 18) {
            throw new IllegalArgumentException("未成年人!!!");
        }
        this.birthday = (Date) in.readObject();
        in.readBoolean();
    }
           
  1. 序列化對象的非基本類型屬性也必須是可序列化的

    什麼意思?直接上代碼。現在我為Adult增加了一個Address屬性,Address的定義如下(類上面幾個注解的意思是需要Lombok為我生成getter、setter、無參構造和全參構造,如果你沒用過注解,可以手寫代碼,我這裡懶得寫了)。

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Address {
    private String country;
    private String city;
}
           

為避免一些基礎薄弱的小夥伴看到這裡已經不知道Adult類改成什麼樣子了,這裡把全部的代碼都貼上。

public class Adult implements Serializable {
    private static final long serialVersionUID = -8645812916550949669L;
    private transient String name;
    private int age;
    private Date birthday;
    private boolean gender;
    private Address address;

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

    public String getName() {
        return name;
    }

    public Adult(String name, int age, Date birthday, boolean gender, Address address) {
        this.name = name;
        this.age = age;
        if(this.age < 18) {
            throw new IllegalArgumentException("未成年人!!!");
        }
        this.birthday = new Date(birthday.getTime());
        this.gender = gender;
        this.address = address;
    }

    /**
     * 重寫此方法,并在此方法中定義序列化邏輯
     * 可以在此方法中做一些加密工作或其他操作
     * @param out
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeObject(name.toUpperCase());
        out.writeInt(age + 15);
        out.writeObject(birthday);
        out.writeBoolean(gender);
        out.writeObject(address);
    }

    /**
     * 重寫此方法,并在此方法中定義反序列化邏輯
     * 可以在此方法中做一些解密工作或定義一些限制關系
     * @param in
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.name = (String) in.readObject();
        this.age = in.readInt() - 14;
        if(this.age < 18) {
            throw new IllegalArgumentException("未成年人!!!");
        }
        this.birthday = (Date) in.readObject();
        in.readBoolean();
        this.address = (Address) in.readObject();
    }

    @Override
    public String toString() {
        return "Adult{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                ", gender=" + gender +
                ", address=" + address +
                '}';
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\adult.txt"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\adult.txt"));
        Adult adult = new Adult("Mr john", 24, new Date(), true, new Address("china", "beijing"));
        oos.writeObject(adult);
        Adult adult1 = (Adult) ois.readObject();
        System.out.println("adult:" + adult1);
    }
}
           

此時運作代碼,将收獲一個NotSerializableException

漫談JAVA序列化

這是因為Address是一個非基本類型(int、long、short等8種基本資料類型)的對象,而它又是Adult的屬性,想序列化Adult就得先序列化Address。是以,隻要Address實作Serializable接口即可。

什麼?放你的狗*!!你說String是基本類型?你說Date是基本類型??

别着急嘛

你看

漫談JAVA序列化

你看

漫談JAVA序列化

6. transient:不要序列化我

transient是幹嘛用的,不知道,先用一下試試

漫談JAVA序列化

給name屬性增加一個transient标記,然後将readObject()方法和writeObject()方法都注釋掉。運作程式

發現沒,name屬性沒了。

transient的作用就是将某個被修飾屬性從類的預設序列化形式中去掉。

即使不将readObject()方法和writeObject()方法注釋掉,但是在這兩個方法種不寫name字段的序列化和非序列化邏輯,如下

private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
//        out.writeObject(name.toUpperCase());
        out.writeInt(age + 15);
        out.writeObject(birthday);
        out.writeBoolean(gender);
        out.writeObject(address);
    }
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
//        this.name = (String) in.readObject();
        this.age = in.readInt() - 14;
        if(this.age < 18) {
            throw new IllegalArgumentException("未成年人!!!");
        }
        this.birthday = (Date) in.readObject();
        in.readBoolean();
        this.address = (Address) in.readObject();
    }
           

我們依舊能夠在結果中看到name屬性的值。為啥?因為系統預設為我們實作了name屬性的序列化和非序列化。那我怎麼去掉name屬性?當然是給name屬性加transient标記喽。

如果我在name屬性上加了transient标記,但是我在readObject()方法和writeObject()方法中定義了name屬性的序列化和非序列化邏輯,能拿到name屬性嗎?當然可以了,你告訴系統不要為我序列化name屬性,我自己序列化,你自己都序列化了,當然拿得到了。

transient的使用場景是什麼?我為什麼要使用transient?

假如你想用連結清單的形式存儲一個字元串清單

public class LinkedStringList {
    private int size = 0;
    private Entry head = null;
    
    private static class Entry implements Serializable {
        String data;
        Entry previous;
        Entry next;
    }
}
           

Entry實作了Serializable接口,那麼就相當于這個私有的靜态内部類被暴露了,以後即使你想用ArrayList存儲字元串也必須維護這個連結清單結構。而且序列化即表示了連結清單中的每個項,也表示了所有的連結關系,這是必要的,序列化過程需要周遊整個連結清單,這很容易引起堆棧溢出。

那麼此時我們就可以通過transient修飾符去掉entry屬性的預設序列化形式,自定義序列化和反序列化邏輯。

public class LinkedStringList implements Serializable {
    private static final long serialVersionUID = -6845501612330006997L;
    private transient int size = 0;
    private transient Entry head = null;
    private transient Entry tail;

    private static class Entry implements Serializable {
        String data;
        Entry previous;
        Entry next;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(size);
        for(Entry e = head.next;e != head;e = e.next) {
            out.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.size = in.readInt();
        for (int i = 0;i < size;i++) {
            add((String) in.readObject());
        }
    }

    public void addElement(String data) {
        add(data);
        size++;
    }

    private void add(String data) {
        if(head == null) {
            head = new Entry();
            tail = head;
        }
        if(StringUtils.isNotBlank(data)) {
            Entry entry = new Entry();
            entry.data = data;
            tail.next = entry;
            entry.previous = tail;
            tail = entry;
            tail.next = head;
            head.previous = tail;
        }
    }

    public void display() {
        if(head == null) {
            return;
        }
        Entry tmp = head.next;
        while(tmp != head) {
            System.out.println(tmp.data);
            tmp = tmp.next;
        }
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        LinkedStringList linkedStringList = new LinkedStringList();
        linkedStringList.addElement("hello");
        linkedStringList.addElement("world");
        linkedStringList.addElement("!");
        System.out.println("序列化之前:");
        linkedStringList.display();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\list.txt"));
        objectOutputStream.writeObject(linkedStringList);
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\list.txt"));
        LinkedStringList result = (LinkedStringList) objectInputStream.readObject();
        System.out.println("序列化之後:");
        result.display();
    }
}
           
  1. Externalizable序列化方式

    Externalizable和Serializable作用相同,隻不過Externalizable強制實作writeExternal(ObjectOutput out)和readExternal(ObjectInput in)方法自定義序列化和反序列化對象,性能上比Serializable稍高。

  2. 擴充,寫入時替換【writeReplace()】

    writeReplace()方法和writeObject()/readObject()方法是互斥的。當類中重寫writeReplace()方法時,表示就序列化這個類傳回的對象,不用管writeObject()和readObject()方法,不寫也不走預設序列化邏輯,寫了也不走自定義邏輯。

如下代碼通過writeReplace()方法将Person類序列化為一個字元串。

public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Object writeReplace() {
        return "你好" + this.name + ",你已經" + this.age + "歲了。";
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\person.txt"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\person.txt"));
        Person person = new Person("小明", 18);
        oos.writeObject(person);
        // 注意這裡,我們已經通過writeReplace()方法将Person對象序列化為一個字元串了,
        // 那麼反序列化的時候也會反序列化出一個字元串,是以我們用String類型來接收
        String str = (String) ois.readObject();
        System.out.println(str);
    }
}
           

四、序列化踩坑

  1. java序利化算法不會重複序列化同一個對象,隻會記錄已序列化對象的編号。如果将一個對象的屬性更改後再次序列化,系統并不會再次将對象轉換為位元組序列,而隻是儲存序列化編号。

    看不明白也沒關系,直接上代碼

public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\adult.txt"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\adult.txt"));
        Adult adult = new Adult("Mr john", 24, new Date(), true);
        System.out.println("adult序列化前:" + adult);
        // 寫入序列化對象
        oos.writeObject(adult);
        // 修改name屬性
        adult.setName("Mr john 666...");
        System.out.println("adult序列化前:" + adult);
        // 再次寫入序列化對象
        oos.writeObject(adult);
        // 讀取并反序列化第一次寫入的序列化對象
        Adult adult1 = (Adult) ois.readObject();
        System.out.println("adult1:" + adult1);
        // 讀取并反序列化第二次寫入的序列化對象
        Adult adult2 = (Adult) ois.readObject();
        System.out.println("adult2:" + adult2);
        System.out.println(adult1 == adult2);
        System.out.println(StringUtils.equals(adult1.getName(), adult2.getName()));

    }
           

結果為:

adult序列化前:Adult{name='Mr john', age=24, birthday=Sat Jul 17 20:10:25 CST 2021, gender=true}
adult序列化前:Adult{name='Mr john 666...', age=24, birthday=Sat Jul 17 20:10:25 CST 2021, gender=true}
adult1:Adult{name='MR JOHN', age=25, birthday=Sat Jul 17 20:10:25 CST 2021, gender=true}
adult2:Adult{name='MR JOHN', age=25, birthday=Sat Jul 17 20:10:25 CST 2021, gender=true}
true
true

           
  1. 單例類實作Serializable接口将失去其單例性【readResove()】
public class Singleton implements Serializable {
    
    private static Singleton INSTANCE = new Singleton();
    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
           

任何一個readObject()方法,不管是顯式的還是預設的,都會傳回一個建立的執行個體,這個建立的執行個體不同于該類初始化時建立的執行個體。

而readResolve特性允許你用另一個執行個體代替readObject()建立的執行個體。對于一個正在被反序列化的對象,如果它的類定義了一個readResove()方法,并且具備正确的聲明,那麼在反序列化後,建立對象上的readResolve()方法就會被調用。然後,該方法傳回的對象引用将被傳回,取代建立的對象。在這個特性的絕大多數用法中,指向建立對象的引用不需要再被保留,是以立即成為垃圾回收的對象。

是以,我們隻需要這樣做就可以讓Singleton類既能序列化又保持其單例性

public class Singleton implements Serializable {

    private static Singleton INSTANCE = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
    private Object readResolve() {
        return INSTANCE;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\singleton.txt"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\singleton.txt"));
        oos.writeObject(singleton);
        Singleton result = (Singleton) ois.readObject();
        System.out.println(result);
    }
}
           

小夥伴們可以嘗試将readResolve()方法注釋掉,再放開。觀察反序列化後的單例對象是否還是原來的對象。

簡單來說就是readResolve()方法會在readObject()方法之後被調用,在readResolve()方法中可以對readObject()反序列化出來的對象建立一個代理,在代理類中進行一些加工(比如加一些屬性限制,修改某些屬性值等)或者幹脆建立一個跟反序列化出來的對象完全不相關的全新對象傳回。

比如,明明序列化了一個Person對象,但是我卻不管它反序列化的結果,直接在readResolve()方法中傳回一個字元串。

public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Object readResolve() {
        return "你好" + this.name + ",你已經" + this.age + "歲了。";
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\person.txt"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\person.txt"));
        Person person = new Person("小明", 18);
        oos.writeObject(person);
        String str = (String) ois.readObject();
        System.out.println(str); // 你好小明,你已經18歲了。
    }
}
           

注:千萬不要以為readObject()和writeObject()是一對,readResolve()和writeReplace()是一對,前半句是對的,後半句是錯的,錯的,錯的。readResolve()并不是用來和writeResolve()配合使用的,它是用來保護性恢複對象的。writeObject()序列化的對象需要readObject()來反序列化;但是writeReplace()序列化成什麼對象,就用什麼對象接收,readResolve()并不能解析出writeReplace()方法傳回的對象,它也沒有這個功能。

四、擴充

  1. 用序列化代理代替序列化執行個體

    看這一節之前建議一定要将前面的注搞清楚,不然關于writeReplace()和readResolve()的關系(他們之間壓根沒關系)你會更加混亂。

先介紹一下下面的代碼是什麼意思。在Person類(被代理類)中建立一個PersonProxy類(靜态代理類),這個PersonProxy中的屬性和Person中的屬性一模一樣,并實作了Serializable 接口。

(a)Person類被序列化時觸發writeReplace()方法

(b)writeReplace()方法調用PersonProxy構造函數傳回一個PersonProxy對象

(c)PersonProxy對象被反序列化後會觸發readResolve()方法

(d)readResolve()方法傳回一個Person對象

最終結果就是實作了Person的序列化(其實是PersonProxy的序列化)和反序列化

public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Object writeReplace() {
        return new PersonProxy(this);
    }

    private static class PersonProxy implements Serializable {
        private final String name;
        private final int age;

        public PersonProxy(Person person) {
            this.name = person.name;
            this.age = person.age;
        }

        private Object readResolve() {
            return new Person(this.name, this.age);
        }

    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\person.txt"));
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\person.txt"));
        Person person = new Person("小明", 18);
        oos.writeObject(person);
        Person result = (Person) ois.readObject();
        System.out.println(result); // Person{name='小明', age=18}
    }
}
           

明明實作一個Serializable接口就能完成的事,我們為什麼要轉換來轉換去,搞這麼多蜜汁操作呢?

在勸退一節我們曾提到反序列化時其實用的是Java語言之外的方法進行對象執行個體化的,這會帶來非常多的風險,但是你發現沒有,現在我們确确實實是用Person類的構造函數進行執行個體化的。這正是該模式的魅力所在,它極大地消除了序列化機制中語言本身之外的特征,因為反序列化執行個體是利用與其他執行個體相同的構造器、靜态工廠和方法而建立的。這樣你就不必單獨確定被反序列化的執行個體一定要遵守類的限制條件。如果該類的靜态工廠或者構造器建立了這些限制條件,并且它的執行個體方法在維持着這些限制條件,你就可以确信序列化也會維持這些限制條件。

當然,序列化代理模式也有兩個局限性:一個是它不能與可以被用戶端擴充的類相相容;它也不能與對象圖中包含循環的某些類想相容。如果你企圖從一個對象的序列化代理的readResolve()方法内部調用這個對象中的方法,就會得到一個ClassCastException異常,因為你還沒有這個對象,隻有它的序列化代理。此外序列化代理方式的開銷稍高些。

最後,雖然前文提到過有了writeReplace()方法之後readObject()和writeObject()方法都不會被調用。但是惡意攻擊者可以通過僞造,違反類的限制條件。是以使用序列化代理類時通常重寫外圍被代理類的readObject()方法。

private void readObject() throws InvalidObjectException {
        throw new InvalidObjectException("proxy required");
    }