簡述
用Java在開發系統的時候,Exception的處理往往是比較復雜的。如何處理開發中遇到的Exception,如何將合理的異常信息呈現給客戶是開發人員必須要考慮的問題。
關於Exception的處理的文章在很多地方都可以看到,本文除了做一個總結之外,還將結合Design by Contract,JDK 1.4引入的assertion,以及如何用Spring的AOP處理Exception做進一步的探討。
Exception的分類
從JDK的API中我們可以看到,Java把異常分為了Error和Exception兩大類,在Exception中又分為checked exception和runtime exception。從系統開發的角度上,我們可以把exception分為:
· JVM異常。這種異常我們不應該捕捉,因為它的出現意味著一些比較嚴重的錯誤,比如OutOfMemoryError,StackOverflowError等;
· 系統異常。大多數情況下,系統異常以RuntimeException的形式出現,比如NullPointerException, ArrayOutOfBoundsException等,這時往往意味著我們的程序裡面出現了Bug;還有一種情況,例如我們沒有辦法通過JNDI找到某個資源,也應該屬於系統異常。系統異常的主要特點是,當我們遇到這種異常的時候,我們沒有合適的辦法處理,或者說我們不能已一個合理的方式告訴最終用戶系統出現了什麼錯誤。很難想象用戶看到一個NPE,並在界面上看到一堆stack trace是什麼感覺。這種異常應該在單元測試以及集成測試的時候被檢測到,在發布的時候應該盡可能不出現這樣的問題;
· 應用異常。這種異常是由我們的系統。這些異常的出現對用戶來說,可能是因為某個驗證沒有通過,某個操作的步驟出現錯誤等,比如插入數據庫的時候出現主鍵重復的情況等。總之,這些異常的信息可以通過一個用戶看的懂的方式顯示給用戶。
Design By Contract(DBC)
我們暫時把Exception的處理放在一邊,先看看Design by Contract的概念。
對於任何一個軟件系統來說,一個重要的目標就是可靠性,即正確性和健壯性。系統的正確性主要看這個系統是不是符合Specificatoin,健壯性主要是指當遇到Specification沒有涉及的情況,即異常情況的時候能不能以一種合理的方式解決。
DBC的主要思想是一個類和它的客戶程序之間有一個合同;客戶程序必須保證調用這個類之前某些前提條件必須滿足(Precondition),而這個類必須保證在被調用之後的某些屬性和狀態是正確的(Postcondidtion/Class Invariants)。如果能有一種方式能夠讓編譯器檢查這些Precondition和Postcondition是否正確,這個合同是否被滿足,那麼出現的錯誤可以被立即捕獲。
例如,一個類需要一個setMonth( int month )的方法,我們一般的實現方法大致如下:
public void setMonth( int month )
{
if( month > 0 && month < 13 )
{
throw new IllegalArgumentException( “” );
}
this.month = month;
}
但是按照DBC的概念,應該由客戶代碼,而不是setMonth方法保證傳進來的參數是一個正確的數值,而setMonth應該保證當方法執行之後,month的值被正確設置,同時保證該類處在一個正確的狀態。所以setMonth中對於參數month的驗證就不在這裡出現了。從這個例子中可以看到,DBC的引入對不同類的責任有一個明確的劃分,原來的代碼裡面setMonth的責任現在被轉移到了客戶代碼裡面。
目前,編程語言中對DBC支持較好的是Eiffel,而在JDK1.4中引入的assertion則為Java在DBC方面提供了一些支持。
Java Assertion
Java雖然不直接支持DBC,但我們可以利用JDK1.4提供的assertion功能做一些這方面的工作。下面簡單介紹assertion的用法,詳細的信息請參見sun網站上的資料(http://Java.sun.com/J2SE/1.4.2/docs/guide/lang/assert.Html)。
Assert有兩種用法
· assert BooleanExpression和
· assert BooleanExpression : DetailMessage
系統運行的時候如果檢查到BooleanExpression是false,那麼就是拋出一個AssertionError。DetailMessage如果提供,會通過AssertionError的構造函數傳進去。
由於assertion是在JDK1.4引入的,為了編譯包含有assertion應用的Java程序,我們需要在Javac中打開開關source=”1.4”,如在ant的build文件中,我們一般這麼寫:
而在IDE環境中注意做相應的變化。
在缺省情況下,assertion在運行時是被禁用的,我們通過開關-ea和-da來打開和關閉assertion的應用,如:
Java –ea SomeClass
有了assertion的幫助,我們在上面的assertion代碼中做如下的改變(注意:請參見Sun的assertion文檔,這裡僅僅示例assertion的用法,並沒有考慮assertion的best practice中的某些原則):
public void setMonth( int month )
{
assert month > 0 && month < 13;
this.month = month;
//assert the state of this class in valid.
}
在開發之前,我們和客戶代碼先簽訂合同,我們這裡在調用方法之前先檢查precondition是不是已經滿足,如果沒有滿足,我們就直接拋出AssertionError,因為我們合同裡面已經做了約定,輸入參數的正確性應該由客戶保證(第一個assertion不應該出錯),而我們這裡應該保證方法被調用之後的postcondition和class invariants的正確性。
引入了assertion,我們一般在開發過程中應該打開-ea開關,這樣可以在單元測試的時候能夠捕捉到相當數量的自己的bug,從而保證系統的健壯。
Java Assertion Best Practice
Sun的assertion使用文檔上對什麼時候使用assertion做了指導,對其中的一點,我們存在一定的疑問,認為Sun的提法稍顯武斷(誰這麼膽大包天,竟敢質問Sun的偉大J).
[Do not use assertions to check the parameters of a public method.]
在我們實際開發的過程中,我們認為,Sun這裡提到的public method應該理解為不同模塊之間的接口,而不是單純意義上的Java類的公用方法。假設一個小組提供一個底層支持模塊給其他小組使用,而這個小組內部也分了很多層次,那麼這裡的Public method我們認為應該指提供給其他模塊/組使用的API,而內部的某些方法,雖然是公用的,仍然可以使用assertion,因為這個合同屬於是內部合同。所以我們認為這裡的public method應該指存在合同關系的兩個模塊之間接口的public method。
另外,assertion的使用在概念會和數據驗證出現一定的重疊,這個我們需要根據實際的情況決定什麼時候使用assertion。
Exception的處理
回到正題,根據上面的討論,對於三種Exception來說,JVM的Exception我們在系統實現上不予考慮,對於一部分系統異常來說,我們可以使用assertion解決掉一部分由於系統的bug引起的異常,對於其余的Exception要正確處理,而處理的標准則是一方面能夠給最終用戶提供有意義的出錯信息,另外一方面,由於盡管我們做了認真的測試,我們仍不能保證系統在發布之後不會出現任何問題,因此需要考慮能夠有合理的方法准確定位錯誤,為開發人員糾錯提供依據。
在實際的應用中,關於Exception類的設計我們可以借鑒一下Spring中關於對Exception的設計。通過考察Spring DAO支持中Exception的設計我們可以發現以下幾個特點:
1. 所有的Exception都來自一個跟節點;
2. 包裝如SQLException等異常,使錯誤信息沒有丟失;
3. 采用RuntimeException,而不是checked exception。
在學習Spring的過程中,剛開始沒有完全理解采用RuntimeException的好處,但通過對其AOP支持的理解,的確覺得采用RuntimeException有一定的道理:
1. 我們不用在一個方法後面聲明這個方法需要throws XXXException;
2. 在調用這個方法的時候也不同一定要try…catch段了;
3. EJB的CMT中,也是靠拋出一個RuntimeException,EJBException,來通知容易回滾事務,Spring用AOP管理事務的方法和它有了相通的地方(雖然Spring的事務管理中沒有規定必須要拋出RumtimeException才能回歸事務)
使用RuntimeException並沒有限制我們在開發過程中一定不捕獲這些異常,如果一個系統中,上層模塊需要捕獲下層模塊拋出的異常,然後在其中增加信息,RuntimeException並沒有限制我們這麼做,相反,因為開發人員必須更加了解底層模塊的情況,而不是借助現在方便的開發工具如Eclipse或者JBuilder自動生成try…catch代碼段,使得我們的系統可能出錯的機會得到一定程度的降低。
AOP中的throws advice
這裡把這個題目加進來是因為AOP可能會給我們的設計帶來一些跟以往設計不同的地方,所以這裡提供一個Spring的throws advice的實例來看看AOP會給我們帶來一些什麼樣的變化。
假設我們的系統需要記錄詳細的出錯信息日志,按照以往的想法,我們會在系統中所有拋出異常的地方加上記錄日志的代碼;但是引入了AOP之後,我們可以將出錯和記錄日志兩個模塊完全解耦,通過Spring配置的方法將兩個模塊結合在一起。
請看下面的代碼:
//定義一個Service,
Public interface ItestService {
Void DOSomething();
}
Public class TestServiceImpl implements ITestService {
Public void DOSomehting() {
Throw new RuntimeException(…);
}
}
//定義throws advice
Public class LogAdvice implements ThrowsAdvice {
Public void afterThrowing( RuntimeException ex ) throws Throwable
{
//Log the error information
}
}
//Spring的配置文件
在系統運行的時候,當TestService的方法拋出異常的時候,LogAdvice中的afterThrowing方法就會被調用;同時這個advisor並不吃掉這個異常,而是繼續拋出去,從而不會影響我們原來的流程。從中我們看到,兩個模塊之間的耦合從原來的代碼中轉移到了配置文件中,因此我們的設計可以充分利用AOP給我們帶了的優勢。
參考文檔:
1. http://www.javaworld.com/Javaworld/jw-02-2002/jw-0215-dbcproxy-p2.Html
2. http://Java.sun.com/J2SE/1.4.2/docs/guide/lang/assert.Html
3. http://www.springframework.org/docs/reference/index.Html