Effective Java Item89 - 用Enum實現物件控制
October 22, 2017這篇是Effective Java - For instance control, prefer enum over readResolve章節的讀書筆記 本篇的程式碼來自於原書內容
在看這篇文章之前 強烈建議先看過Item3
Item89: 對於實例控制 枚舉優先於readResolve
在Singleton中說明了很多實現單例的方法 大多數都是利用private constructor來實現
今天如果你想要序列化這個應該要是Singleton的類別 如果只是加上implements Serializable 那他就再也不是Singleton
就如我們所說 readObject其實也可以看做一個constructor 只是input argument是byte stream 不論readObject是預設的還是你自行定義的都一樣 你反序列化後拼出來的那個東西就是個新的instance
該怎麼辦呢
readResolve
對於一個正在被反序列化的物件 如果他的class定義了readResolve 那麼在反序列化完之後 就會call readResolve
對於一個正在被反序列化的對象 如果他的類定義了一個readResolve 那麼在反序列化之後 新建對象上的readResolve方法就會被調用
然後該方法返回的對象引用將會被返回 取代新建的對象
所以你只要在readResolve裡面回傳當初那個唯一的instance就搞定 剛創出來的物件就那麼隨風而去 煙消雲散
既然如此 那這個class就不應該有需要被傳輸的instance variable(反正到最後你反序列化完之後 也是return原本的singleton object) 所以如果你依賴readResolve進行實例控制 所有instance variable應該要是transient
不然的話可能會遭受到下列的攻擊
攻擊例子
如果一個你想支援序列化的Singleton包含了一個非transient的對象 favoriteSongs
那麼就很有機會可以做一個壞事 我們可以偷偷寫一個壞Class 這個Class也有一個instance variable 也有一個readResolve
這樣子的話 我們如果偷偷改了一個傳輸中的byte stream 變成這樣
的話 就會產生兩個不同的Elvis instance了
YAY! 皆大歡喜 又過了和平的一天 上面的code我相信所有唸過這本書的九成都沒看懂就這樣帶過 總之知道結論就好 不要有non-transient的變數
但若想要變強 就得要參加死亡行軍
愛上地獄的男人們
要讀懂這個section要先完全通透序列化基本知識和深入解析序列化byte stream 不然沒有讀下去的必要
要看懂這個攻擊 鐵定是要認真讀一下這個byte stream ???
注意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
值assign完了之後 ElvisStealer的readResolve會先被call
他把他手上的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
這下好了 看回主要程式
這樣就打破了Singleton的規範 elvis變數和impersonator應該要是同一個物件 有著相同的行為才對
所以結論是 當你要序列化一個Singleton而且要用readResolve的話 不要有non-transient的變數
這樣 你看懂了嗎
所以
最好的實現Singleton方式 還是用enum
總結
可以用enum實現Singleton就用enum 直接幫你處理好序列化的問題 但如果你無法用enum 但你又需要序列化和instance control 那就要提供一個readResolve並且所有instance variable都要是non-transient