小心異常(Exception)帶來的風險(2) (轉)

amyz發表於2007-11-28
小心異常(Exception)帶來的風險(2) (轉)[@more@]

不要捕獲泛型異常

在複雜的中,經常會有一些特定的程式碼塊時會丟擲多種不同異常的方法。動態裝入一個類和例項化一個都可能會產生幾個不同的異常,包括ClassNotFoundException, InstantiationException, IllegalAccessException, 和 ClassCastException。
一個繁忙的員在遇到這種情況時可能簡單的把方法包在一個只會捕獲泛型異常Exception的try/catch塊,而不是新增四個不同的catch塊到try塊後面(看下面的程式碼清單3)。這看起來似乎無可置否,卻會產生一些無意識的副面效果。例如,如果className()是null,那麼Class.forName()將會丟擲一個NullPointerException異常並在這個方法中被捕獲。在這種情況下,catch塊將捕獲此異常雖然它從沒打算去捕獲這樣一個異常,只是因為NullPointerException是RuntimeException的一個子類,而且RuntimeException又是Exception的一個子類。所以一個普通的catch(Exception e)將會捕獲所有RuntimeException的子類,包括NullPointerException, IndexOutOfBoundsException, 和ArrayStoreException。通常,一個程式設計師並不打算去捕獲這些異常。
在程式碼清單3中,null的className會導致一個NullPointerException異常產生,它告訴在呼叫的方法中類名無效。
程式碼清單3

public SomeInterface buildInstance(String className) {
SomeInterface impl = null;
try {
Class clazz = Class.forName(className);
impl = (SomeInterface)clazz.newInstance();
}
catch (Exception e) {
log.error("Error creating class: " + className);
}

return impl;
}


另一個使用泛型捕獲子句的結果是限制日誌記錄,因為catch不知道到底那一個特殊的異常被捕獲。有些程式設計師在面對這種問題的時候,採取新增檢測的手段去檢視異常的型別(程式碼清單4),而這正好與使用catch塊的目的相背離。
程式碼清單4

catch (Exception e) {
if (e instanceof ClassNotFoundException) {
log.error("Invalid class name: " + className + ", " + e.toString());
}
else {
log.error("Cannot create class: " + className + ", " + e.toString());
}
}


程式碼清單5提供一種完整的捕獲特殊異常的例子,一些程式設計師可能會對它感趣。運算子instanceof不是必須的因為這個特殊的異常自會被捕獲。每一個被檢查的異常(ClassNotFoundException, InstantiationException, IllegalAccessException) 會被捕獲和處理。對於一個類裝入正確,但是卻沒有實現SomeInterface介面這種特殊情況會產生一個ClassCastException異常,這個異常也會被查證。
程式碼清單5

public SomeInterface buildInstance(String className) {
SomeInterface impl = null;
try {
Class clazz = Class.forName(className);
impl = (SomeInterface)clazz.newInstance();
}
catch (ClassNotFoundException e) {
log.error("Invalid class name: " + className + ", " + e.toString());
}
catch (InstantiationException e) {
log.error("Cannot create class: " + className + ", " + e.toString());
}
catch (IllegalAccessException e) {
log.error("Cannot create class: " + className + ", " + e.toString());
}
catch (ClassCastException e) {
log.error("Invalid class type, " + className
+ " does not implement " + SomeInterface.class.getName());
}

return impl;
}


在某些情況下,更好的方法是重新丟擲一個已知的異常(或者叫建立一個新的異常)而不是試圖去在當前這個方法中處理。這允許呼叫方法透過放置這個異常到一個已知的上下文中去處理這種錯誤情形。

下面的程式碼清單6提供了一個buildInterface()方法的替換版本。如果在裝入和例項化類時發生問題,這個版本會丟擲一個ClassNotFoundException異常。在這個例子中,呼叫方法會確保得到一個正確的例項化物件或者是一個異常。這樣呼叫方法就不需要去檢查返回的物件是否為空了。
注意這個例子使用了 1.4的方法來建立一個已經被另外的異常封裝的新的異常,以便儲存原始的堆疊跟蹤資訊。否則,堆疊跟蹤將指明方法buildInstance()是引起異常的源,而不是潛在的由newInstance()丟擲的異常。
程式碼清單6

public SomeInterface buildInstance(String className)
throws ClassNotFoundException {

try {
Class clazz = Class.forName(className);
return (SomeInterface)clazz.newInstance();
}
catch (ClassNotFoundException e) {
log.error("Invalid class name: " + className + ", " + e.toString());
throw e;
}
catch (InstantiationException e) {
throw new ClassNotFoundException("Cannot create class: " + className, e);
}
catch (IllegalAccessException e) {
throw new ClassNotFoundException("Cannot create class: " + className, e);
}
catch (ClassCastException e) {
throw new ClassNotFoundException(className
+ " does not implement " + SomeInterface.class.getName(), e);
}
}


在有些情況下,這段程式碼可能無法從某種錯誤狀態恢復,這時,捕獲一個特殊的異常以使程式碼能指出某種狀態是否是可恢復的就變得很重要了。請試著以這種觀點去看程式碼清單6中類例項化的例子。

在程式碼清單7中,如果className無效,程式會返回一個預設的物件,並且丟擲一個異常以指明的操作,比如錯誤的轉型或訪問不夠。
注意:IllegalClassException是一系列的異常類,為示範的目的而在這提及(譯註:並不是Java標準庫所帶)。
程式碼清單7


public SomeInterface buildInstance(String className)
throws IllegalClassException {

SomeInterface impl = null;
try {
Class clazz = Class.forName(className);
return (SomeInterface)clazz.newInstance();
}
catch (ClassNotFoundException e) {
log.warn("Invalid class name: " + className + ", using default");
}
catch (InstantiationException e) {
log.warn("Invalid class name: " + className + ", using default");
}
catch (IllegalAccessException e) {
throw new IllegalClassException("Cannot create class: " + className, e);
}
catch (ClassCastException e) {
throw new IllegalClassException(className
+ " does not implement " + SomeInterface.class.getName(), e);
}

if (impl == null) {
impl = new DefaultImplemantation();
}

return impl;
}



什麼時候應捕獲一個泛型異常?

在某些情況下捕獲一個泛型異常是可以的,比如當它很便利且必需去捕獲一個泛型異常的時候。這種情況非常特殊,且對於大型、允許失敗的來說很重要。在程式碼清單8中,從一個請求佇列中讀取請求並順序處理。但是,當請求被處理時候如果有任何異常發生(一個BadRequestException或任何RuntimeException的子類,包括NullpointerException),則那個異常就會被while迴圈外部所捕獲。這樣任何錯誤都會引起迴圈終止並且任何剩餘的請求都不會被處理。那意味著在請求處理期間去處理一個錯誤是一種較差的方法。
程式碼清單8

public void processAllRequests() {
Request req = null;
try {
while (true) {
req = getNextRequest();
if (req != null) {
processRequest(req); // throws BadRequestException
}
else {
// Request queue is empty, must be done
break;
}
}
}
catch (BadRequestException e) {
log.error("Invalid request: " + req, e);
}
}


操作請求處理的一種較好的方法是對上述程式碼的邏輯作兩個重要的改變,首先,將try/catch塊移入請求處理的迴圈中。那樣的話任何錯誤都會在迴圈內部被捕獲和處理,並且不會引起迴圈終止。因而,迴圈會繼續處理請求,即使一個單個的請求失敗。第二,更改這個try/catch塊使它去捕獲一個泛型的異常。這樣,任何異常都在迴圈內部被捕獲,請求也得以繼續處理。請看下面程式碼清單9:
程式碼清單9:


public void processAllRequests() {
while (true) {
Request req = null;
try {
req = getNextRequest();
if (req != null) {
processRequest(req); // Throws BadRequestException
}
else {
// Request queue is empty, must be done
break;
}
}
catch (Exception e) {
log.error("Error processing request: " + req, e);
}
}
}



捕獲一個泛型異常聽起來好像直接與本篇開始的觀點相違背,確實如此。但是,這只是在非常非常特殊的的環境下。在這種情況下,捕獲一個泛型的異常以防止一個單個的異常終止整個系統執行。
在請求、事務或事件在一個迴圈中被處理的情況下,即使在處理期間有異常被丟擲,那個迴圈仍需要執行以便繼續去處理。
在程式碼清單9中,在處理迴圈中try/catch塊被認為是頂級異常管理者(the top-level exception handler),並且它需要捕獲和記錄所有在這個程式碼級別引起的異常。這樣,異常沒有被忽略,也不會丟失,並且異常不會中斷餘下需要處理的請求。

每個大型、複雜的系統都有一個頂級異常管理者(或者是每一個子系統,這取決於系統如何完成處理的)。頂級異常管理者無意去修復由異常引起的潛在問題,並且能在不終止處理的情況下捕獲和記錄這個問題。這並不是暗示所有的異常都應該在這個級別被丟擲。任何異常如果能在較低階別被處理,則就應該那麼做。如果那樣,異常處理對問題發生時的狀態邏輯就會知道的更多。但是,如果異常不能在較低階別被處理,那麼丟擲它到更高階別,這樣,所有那些不可恢復的錯誤都將在一處被處理(頂級異常管理者),而不是遍及整個系統。

不要拋擲泛型異常
出現在程式清單1中的整個問題是在程式設計師決定從cleanupEverything()方法中拋擲泛型異常開始的,如此一來程式碼變得很優美,而當一個方法丟擲6個不同異常時則會變得雜亂:方法宣告變得得以理解,呼叫方法也不得不捕獲那6個不同的異常,就像程式碼清單10那樣。
程式碼清單10

public void cleanupEverything() throws
ExceptionOne, ExceptionTwo, ExceptionThree,
ExceptionFour, ExceptionFive, ExceptionSix {

cleanupConnections();
cleanupFiles();
removeListeners();
}

public void done() {
try {
doStuff();

cleanupEverything();

oreStuff();
}
catch (ExceptionOne e1) {
// Log e1
}
catch (ExceptionTwo e2) {
// Log e2
}
catch (ExceptionThree e3) {
// Log e3
}
catch (ExceptionFour e4) {
// Log e4
}
catch (ExceptionFive e5) {
// Log e5
}
catch (ExceptionSix e6) {
// Log e6
}
}


但是,即使程式碼有點雜亂,卻很清楚。使用特殊異常可以避免兩種非常真實的問題:拋擲一個泛型Exception隱藏了潛在問題的細節,這樣也就失去了處理問題之所在的機會。更進一步說,拋擲一個泛型異常會強制任何呼叫這個的方法的程式碼要麼捕獲那個泛型異常(正像前述一樣,這種方法有問題),要麼重新拋擲那個泛型異常擴大問題範圍。
有代表性的是,當一個方法宣告它將丟擲一個泛型異常Exception時,它這樣做可能有以下兩個原因之一:一種原因是,這個方法呼叫了幾個另外的方法,而那幾個方法可能會拋擲出許多不同的異常(比如調停者或門面模式),且隱藏了異常狀態的細節。因此不論是什麼問題這個方法只是簡單的宣告它會丟擲Exception,而不建立和拋擲一層異常(封裝在較低階別的異常)。另外一種情形是,在方法例項化和拋擲泛型異常Exception(即throw new Exception())的地方,因為程式設計師認為異常事實上不應該被用來表達這種情形。

這兩方面的問題只要稍微思考和設計,就都可解決。什麼是細節?那一層異常真的應該被拋擲嗎?這個設計可能包括僅僅宣告這個方法會拋擲一些確實會發生的異常。另一個選擇是建立一層異常去封裝拋擲和宣告異常的東西。在大多數情況下,被方法拋擲的異常(或者叫一系列異常)應該儘可能的詳細。這種更詳細的異常會提供更多關於錯誤狀態的資訊,這樣就允許這種情形被處理或至少詳細的被記錄。

如果泛型Exception類被選中,那就意味著任何呼叫一個宣告瞭會丟擲Exception方法的方法,要麼必須宣告它本身也會丟擲Exception,要麼封裝這個方法呼叫在一個捕獲泛型Exception的try/catch塊中。我用這種方法在前面解釋了這個問題。
小心使用泛型異常
這篇文章探究了處理泛型異常的幾個方面:它們永遠都不要被拋擲,也不應被忽略。它們應該很少被捕獲(只在非常特殊的情況下)。它們不會提供詳細資訊以允許你去有效的處理它們,所以你不打算那樣做時你應該小心捕獲異常。

異常是Java中的一種強有力的工具。如果你正確使用,它能使你成為一個更有的程式設計師,並且縮短你的開發週期,特別是在測試和時。當異常被錯誤的使用時,在你的系統中,由於隱藏了問題的所在,你得一次又一次的重複工作。所以你要關注好你在哪和如何使用泛型異常。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-985584/,如需轉載,請註明出處,否則將追究法律責任。

相關文章