effectiveJava
enum

Effective Java Item89 - 用Enum實現物件控制

這篇是Effective Java - For instance control, prefer enum over readResolve章節的讀書筆記 本篇的程式碼來自於原書內容

在看這篇文章之前 強烈建議先看過Item3

Item89: 對於實例控制 枚舉優先於readResolve

Singleton中說明了很多實現單例的方法 大多數都是利用private constructor來實現

public class Elvis {
  public static final Elvis INSTANCE = new Elvis();

  private Elvis() {
  }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm outta here!");
  }
}

今天如果你想要序列化這個應該要是Singleton的類別 如果只是加上implements Serializable 那他就再也不是Singleton

就如我們所說 readObject其實也可以看做一個constructor 只是input argument是byte stream 不論readObject是預設的還是你自行定義的都一樣 你反序列化後拼出來的那個東西就是個新的instance

該怎麼辦呢

readResolve

對於一個正在被反序列化的物件 如果他的class定義了readResolve 那麼在反序列化完之後 就會call readResolve

對於一個正在被反序列化的對象 如果他的類定義了一個readResolve 那麼在反序列化之後 新建對象上的readResolve方法就會被調用

然後該方法返回的對象引用將會被返回 取代新建的對象

public class Elvis implements Serializable{
  public static final Elvis INSTANCE = new Elvis();

  private Elvis() {
  }

  private Object readResolve() {
    return INSTANCE;
  }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm outta here!");
  }
}

所以你只要在readResolve裡面回傳當初那個唯一的instance就搞定 剛創出來的物件就那麼隨風而去 煙消雲散

Alt text

既然如此 那這個class就不應該有需要被傳輸的instance variable(反正到最後你反序列化完之後 也是return原本的singleton object) 所以如果你依賴readResolve進行實例控制 所有instance variable應該要是transient

不然的話可能會遭受到下列的攻擊

攻擊例子

如果一個你想支援序列化的Singleton包含了一個非transient的對象 favoriteSongs

public class Elvis implements Serializable {
  public static final Elvis INSTANCE = new Elvis();

  private Elvis() {
  }

  private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };

  public void printFavorites() {
    System.out.println(Arrays.toString(favoriteSongs));
  }

  private Object readResolve() throws ObjectStreamException {
    return INSTANCE;
  }
}

那麼就很有機會可以做一個壞事 我們可以偷偷寫一個壞Class 這個Class也有一個instance variable 也有一個readResolve

public class ElvisStealer implements Serializable {
  static Elvis impersonator;
  private Elvis payload;

  private Object readResolve() {
    // Save a reference to the "unresolved" Elvis instance
    impersonator = payload;

    // Return an object of correct type for favorites field
    return new String[] { "A Fool Such as I" };
  }

  private static final long serialVersionUID = 0;
}

這樣子的話 我們如果偷偷改了一個傳輸中的byte stream 變成這樣 Alt text

的話 就會產生兩個不同的Elvis instance了

public static void main(String[] args) {
  Elvis elvis = (Elvis) deserialize(serializedForm);
  Elvis impersonator = ElvisStealer.impersonator;

  elvis.printFavorites();
  impersonator.printFavorites();
}
[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]

YAY! 皆大歡喜 又過了和平的一天 上面的code我相信所有唸過這本書的九成都沒看懂就這樣帶過 總之知道結論就好 不要有non-transient的變數

但若想要變強 就得要參加死亡行軍

Alt text

愛上地獄的男人們

要讀懂這個section要先完全通透序列化基本知識深入解析序列化byte stream 不然沒有讀下去的必要

要看懂這個攻擊 鐵定是要認真讀一下這個byte stream Alt text ??? Alt text

Alt text

注意malicious的那一個block 原本那個block應該要開始寫favoriteSongs 也就是個長度是2的String Array 分別是”Hound Dog”跟”Heartbreak Hotel”

可是卻被竄改成了ElvisStealer的instance

現在你再重新回想一下序列化Elvis的順序

1.寫下Elvis的Class descriptor

2.如果Elvis有parent class 就一路寫class descriptor上去(在這個例子 沒有parent class)

3.開始寫non-transient的variable(別忘了static variable不寫)

4.這個variable的Class是個我沒看過的Class(ElvisStealer) 那再開始寫ElvisStealer的class descriptor

5.寫完後 再來寫這個instance的value

好戲登場

記不記得我們說readResolve會在反序列化完之後被call 而且readResolve回傳的就是最終反序列化的結果

我們用一點變數來輔助說明

1.假設我們想把剛剛那個byte stream反序列化成一個Elvis的object A 這個object的reference是refA

2.Elvis已存在一個singleton object I

然後我們再反序列化Elvis的時候(此時refA已經存起來了 71007e0002) 因為看到了新的Class 所以當反序列化Elvis到一半的時候 我們先反序列化ElvisStealer 然後當ElvisStealer的Class descriptor寫完時 我們assign 變數payload的值是refA

Alt text

值assign完了之後 ElvisStealer的readResolve會先被call

private Object readResolve() {
  // Save a reference to the "unresolved" Elvis instance
  impersonator = payload;

  // Return an object of correct type for favorites field
  return new String[] { "A Fool Such as I" };
}

他把他手上的payload變數assign給他的static variable impersonator 然後return了 一個Array of String {“A Fool Such as I”} 所以refA被偷偷地記在了ElvisStealer裡面 但是對於Elvis來說 覺得它的favoriteSongs是{“A Fool Such as I”} 因為這個Array of String就是那一整個malicious block反序列化的結果

然後所有byte stream讀完後 開始跑Elvis的readResolve 那就簡單了 剛剛辛苦了老半天的A沒有人要 對於Elvis來說煙消雲散了(至少Elvis以為煙消雲散) 直接回傳那個singleton object I

這下好了 看回主要程式

public static void main(String[] args) {
  Elvis elvis = (Elvis) deserialize(serializedForm);
  Elvis impersonator = ElvisStealer.impersonator;

  elvis.printFavorites();//print I.favoriteSongs
  impersonator.printFavorites();//print refA.favoriteSongs
}
[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]

這樣就打破了Singleton的規範 elvis變數和impersonator應該要是同一個物件 有著相同的行為才對

所以結論是 當你要序列化一個Singleton而且要用readResolve的話 不要有non-transient的變數

這樣 你看懂了嗎

所以

最好的實現Singleton方式 還是用enum

public enum Elvis {
  INSTANCE;
  private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };

  public void printFavorites() {
    System.out.println(Arrays.toString(favoriteSongs));
  }
}

總結

可以用enum實現Singleton就用enum 直接幫你處理好序列化的問題 但如果你無法用enum 但你又需要序列化和instance control 那就要提供一個readResolve並且所有instance variable都要是non-transient