l

2013年11月20日 星期三

Java的try、catch、finally(6):自己製作Suppressed Exception

Nov. 08 10:47~14:20

image

 

複  習

這系列寫了好幾集,內容又都是程式碼,還沒轉台的鄉民們應該也看到頭暈了。先整理一下之前提談過的一些Java SE 7的try statement關於清除資源的特性:

  1. 用try-with-resources來使用資源物件,在離開try statement之前,JVM會自動幫忙關閉這些資源。
  2. 如果try block或catch block丟出例外,且JVM在關閉資源時也產生例外,JVM會把這些cleanup例外加到之前產生的例外之中,稱之為suppressed exception。
  3. Try-with-resources的功能雖然解決了cleanup例外蓋掉try block或catch block所產生例外地這個問題,但是最終所丟出去的例外,到底是代表function failure或cleanup failure,其語意還是模糊不清。
  4. 藉由讓AutoCloseable介面的close() method丟出CleanupException這個使用者自訂的unchecked exception,可以讓明確區分try-with-resources的function failure與cleanup failure語意。但是,可惜Java內建的資源物件,其close() method並不會丟出CleanupException,所以在預設情況下還是語意不清。
  5. 第2點所提到的功能,只有在try-with-resources的情況下JVM會幫忙產生suppressed exception,如果是傳統的寫法,finally block所產生的例外,在Java SE 7 之後的行為依然沒變,還是會把try block或是catch block所產生的例外給「蓋台」。

複習完畢,今天的目的想要解決第4點與第5點所提到的問題。首先寫段程式確定一下第5點所描述的現象。請看以下Java SE 7的程式碼片段,在finally block中直接丟出IOException(第35行)。

螢幕快照 2013-11-08 上午11.17.35

 

在main()程式測試一下finallyBlockOverrideExceptionThrownByTryBlock() method。

螢幕快照 2013-11-08 上午11.19.21

 

結果發現,沒有任何suppressed exception,捕捉到的例外是由finally block所丟出來的IOException,原本try block所丟出的IOException已經被蓋台了。Java SE 7的finally block的行為模式和之前版本並無不同。

螢幕快照 2013-11-08 上午11.24.21

***

老師傅手工打造

接下來將會使用到MyConnection、MyInputStream、MyOutputStream這三個類別,由於要模擬Java內建的資源物件,因此這三個類別的close() method不再丟出Teddy自訂的CleanupException,而是丟出Java內建的SQLException、FileNotFoundException、IOException。

螢幕快照 2013-11-08 下午1.09.27

螢幕快照 2013-11-08 下午1.09.44

螢幕快照 2013-11-08 下午1.10.10

 

接下來先看function failure的例子,之前已經說明過try-with-resources無法清楚的區別function failure與cleanup failure兩者的語意,因此只好用傳統的finally block加上一些程式設計技巧來嘗試解決這個問題。請先看一下下圖useCleanerWithFailureException() method的程式碼。

螢幕快照 2013-11-08 下午1.13.47

 

整個設計與使用概念其實很簡單,只有三個步驟:

  1. Teddy設計了一個Cleaner類別,利用這個類別來模擬JVM把資源物件push到一個stack的動作(24~26行)。
  2. 由於try block與catch block都可能會丟出例外(在此稱之為lead exception),為了有機會可以把cleanup exception加入到lead exception裡面,因此在28~31行用一個blanket catch捕捉住除了Error以外的全部的例外。然後,把呼叫Cleaner類別的setLeadException() 方法,把捕捉到的lead exception先存起來。等一下如果在finally block發生cleanup例外,才有辦法把cleanup例外加到lead exception裡面。
  3. 最後,在finally block呼叫Cleaner類別的clear() method,執行資源釋放的動作。

接下來將會使用到MyConnection

接下來看一下執行結果,的確是捕捉到了代表function failure的IOException,而三個型態屬於CleanupException的suppressed exception也都被正確加入。

螢幕快照 2013-11-08 下午1.43.30

***

Function failure的語意正確了,接下來看一下cleanup failure的語意是否正確,先看範例程式,和剛剛function failre的例子差不多,只是現在try block不會丟出例外。

螢幕快照 2013-11-08 下午1.46.58

 

執行結果,的確是捕捉到了代表cleanup failure的CleanupException,而且suppressed exception現在變成只剩二個,也都被正確加入。

螢幕快照 2013-11-08 下午1.48.50

***

最後看一下Cleaner程式碼,運作原理剛剛已經說明過了,程式也很簡單,不再贅述。

螢幕快照 2013-11-08 下午1.53.25

螢幕快照 2013-11-08 下午1.54.05

***

例子看到這邊,鄉民們可能會覺得:有必要為了區分function failure與cleanup failure把程式寫成這個樣子嗎?直接用try-with-resources不是簡單又方便嗎?這個問題其實Teddy也沒有什麼「正確答案」。之前念書的時候有一陣子在研究例外處理,發現例外處理很複雜,其中原因很多,而程式語言沒有清楚的區分function failure與cleanup failure就是其中的原因之一。

請回想一下自己寫過的Java或C#程式,請問大家都怎麼處理close()所丟出的例外?大部分的人不是忽略它,就是反射性的印出例外訊息然後這個訊息就被掩沒在其他更多的訊息之中。到底呼叫close()發生例外的機率高不高?依據Teddy的經驗,不高。但是一旦發生卻又被有意無意忽略,則這樣的問題是很難被找出來的。

好幾年前Teddy曾經用過一個第三方廠商所寫的免費JDBC驅動程式,用來連接某個資料庫。在那個年代,有些JDBC驅動程式是需要付費的,所以Teddy找了這個免費的驅動程式來使用。剛開始用得時候都很正常,一直到上線之後,發現程式跑了一陣子就會把資料庫的connection數目全部使用完畢,造成後續的連線要求全部失敗,導致需要重新啟動資料庫。

一開始Teddy以為程式中釋放資源的程式碼沒有寫好,後來搞了好久,才發現呼叫connection物件的close() method丟出了例外,一直都沒被注意到。看起來是這個免費的JDBC驅動程式有bug,最後只好換另一個JDBC驅動程式才解決了這個問題。

可能是因為有這個經驗,所以Teddy對於cleanup failure才有自己的一點小小堅持吧挑眉質疑

***

友藏內心獨白:程式要不要這樣寫不是重點,解題思考模式可以參考一下。

沒有留言:

張貼留言