Java EE項目中的異常處理 – JAVA編程語言程序開發技術文章

為什麼要在J2EE項目中談異常處理呢?可能許多java初學者都想說:“異常處理不就是try….catch…finally嗎?這誰都會啊!”。筆者在初學java時也是這樣認為的。如何在一個多層的j2ee項目中定義相應的異常類?在項目中的每一層如何進行異常處理?異常何時被拋出?異常何時被記錄?異常該怎麼記錄?何時需要把checked Exception轉化成unchecked Exception ,何時需要把unChecked Exception轉化成checked Exception?異常是否應該呈現到前端頁面?如何設計一個異常框架?本文將就這些問題進行探討。
1. JAVA異常處理
在面向過程式的編程語言中,我們可以通過返回值來確定方法是否正常執行。比如在一個c語言編寫的程序中,如果方法正確的執行則返回1.錯誤則返回0。在vb或delphi開發的應用程序中,出現錯誤時,我們就彈出一個消息框給用戶。
通過方法的返回值我們並不能獲得錯誤的詳細信息。可能因為方法由不同的程序員編寫,當同一類錯誤在不同的方法出現時,返回的結果和錯誤信息並不一致。
所以java語言采取瞭一個統一的異常處理機制。
什麼是異常?運行時發生的可被捕獲和處理的錯誤。
在java語言中,Exception是所有異常的父類。任何異常都擴展於Exception類。Exception就相當於一個錯誤類型。如果要定義一個新的錯誤類型就擴展一個新的Exception子類。采用異常的好處還在於可以精確的定位到導致程序出錯的源代碼位置,並獲得詳細的錯誤信息。
Java異常處理通過五個關鍵字來實現,try,catch,throw ,throws, finally。具體的異常處理結構由try….catch….finally塊來實現。try塊存放可能出現異常的java語句,catch用來捕獲發生的異常,並對異常進行處理。Finally塊用來清除程序中未釋放的資源。不管理try塊的代碼如何返回,finally塊都總是被執行。
一個典型的異常處理代碼
java 代碼
public String getPassword(String userId)throws DataAccessException{
String sql = “select password from userinfo where userid=’”+userId +”’”;
String password = null;
Connection con = null;
Statement s = null;
ResultSet rs = null;
try{
con = getConnection();//獲得數據連接
s = con.createStatement();
rs = s.executeQuery(sql);
while(rs.next()){
password = rs.getString(1);
}
rs.close();
s.close();
}
Catch(SqlException ex){
throw new DataAccessException(ex);
}
finally{
try{
if(con != null){
con.close();
}
}
Catch(SQLException sqlEx){
throw new DataAccessException(“關閉連接失敗!”,sqlEx);
}
}
return password;
}
可以看出Java的異常處理機制具有的優勢:
給錯誤進行瞭統一的分類,通過擴展Exception類或其子類來實現。從而避免瞭相同的錯誤可能在不同的方法中具有不同的錯誤信息。在不同的方法中出現相同的錯誤時,隻需要throw 相同的異常對象即可。
獲得更為詳細的錯誤信息。通過異常類,可以給異常更為詳細,對用戶更為有用的錯誤信息。以便於用戶進行跟蹤和調試程序。
把正確的返回結果與錯誤信息分離。降低瞭程序的復雜度。調用者無需要對返回結果進行更多的瞭解。
強制調用者進行異常處理,提高程序的質量。當一個方法聲明需要拋出一個異常時,那麼調用者必須使用try….catch塊對異常進行處理。當然調用者也可以讓異常繼續往上一層拋出。
2. Checked 異常 還是 unChecked 異常?
Java異常分為兩大類:checked 異常和unChecked 異常。所有繼承java.lang.Exception 的異常都屬於checked異常。所有繼承java.lang.RuntimeException的異常都屬於unChecked異常。
當一個方法去調用一個可能拋出checked異常的方法,必須通過try…catch塊對異常進行捕獲進行處理或者重新拋出。
我們看看Connection接口的createStatement()方法的聲明。
public Statement createStatement() throws SQLException;
SQLException是checked異常。當調用createStatement方法時,java強制調用者必須對SQLException進行捕獲處理。
java 代碼
public String getPassword(String userId){
try{
……
Statement s = con.createStatement();
……
Catch(SQLException sqlEx){
……
}
……
}
或者
java 代碼
public String getPassword(String userId)throws SQLException{
Statement s = con.createStatement();
}
(當然,像Connection,Satement這些資源是需要及時關閉的,這裡僅是為瞭說明checked 異常必須強制調用者進行捕獲或繼續拋出)
unChecked異常也稱為運行時異常,通常RuntimeException都表示用戶無法恢復的異常,如無法獲得數據庫連接,不能打開文件等。雖然用戶也可以像處理checked異常一樣捕獲unChecked異常。但是如果調用者並沒有去捕獲unChecked異常時,編譯器並不會強制你那麼做。
比如一個把字符轉換為整型數值的代碼如下:
java 代碼
String str = “123”;
int value = Integer.parseInt(str);
parseInt的方法簽名為:
java 代碼
public staticint parseInt(String s)throws NumberFormatException
當傳入的參數不能轉換成相應的整數時,將會拋出NumberFormatException。因為NumberFormatException擴展於RuntimeException,是unChecked異常。所以調用parseInt方法時無需要try…catch
因為java不強制調用者對unChecked異常進行捕獲或往上拋出。所以程序員總是喜歡拋出unChecked異常。或者當需要一個新的異常類時,總是習慣的從RuntimeException擴展。當你去調用它些方法時,如果沒有相應的catch塊,編譯器也總是讓你通過,同時你也根本無需要去瞭解這個方法倒底會拋出什麼異常。看起來這似乎倒是一個很好的辦法,但是這樣做卻是遠離瞭java異常處理的真實意圖。並且對調用你這個類的程序員帶來誤導,因為調用者根本不知道需要在什麼情況下處理異常。而checked異常可以明確的告訴調用者,調用這個類需要處理什麼異常。如果調用者不去處理,編譯器都會提示並且是無法編譯通過的。當然怎麼處理是由調用者自己去決定的。
所以Java推薦人們在應用代碼中應該使用checked異常。就像我們在上節提到運用異常的好外在於可以強制調用者必須對將會產生的異常進行處理。包括在《java Tutorial》等java官方文檔中都把checked異常作為標準用法。
使用checked異常,應意味著有許多的try…catch在你的代碼中。當在編寫和處理越來越多的try…catch塊之後,許多人終於開始懷疑checked異常倒底是否應該作為標準用法瞭。
甚至連大名鼎鼎的《thinking in java》的作者Bruce Eckel也改變瞭他曾經的想法。Bruce Eckel甚至主張把unChecked異常作為標準用法。並發表文章,以試驗checked異常是否應該從java中去掉。Bruce Eckel語:“當少量代碼時,checked異常無疑是十分優雅的構思,並有助於避免瞭許多潛在的錯誤。但是經驗表明,對大量代碼來說結果正好相反”
關於checked異常和unChecked異常的詳細討論可以參考
Alan Griffiths http://www.octopull.demon.co.uk/java/ExceptionalJava.html
Bruce Eckel http://www.mindView.net/Etc/Disscussions/CheckedExceptions
《java Tutorial》 http://java.sun.com/docs/books/tutorial/essential/exceptions/runtime.html
使用checked異常會帶來許多的問題。
checked異常導致瞭太多的try…catch 代碼
可能有很多checked異常對開發人員來說是無法合理地進行處理的,比如SQLException。而開發人員卻不得不去進行try…catch。當開發人員對一個checked異常無法正確的處理時,通常是簡單的把異常打印出來或者是幹脆什麼也不幹。特別是對於新手來說,過多的checked異常讓他感到無所適從。
java 代碼
try{
……
Statement s = con.createStatement();
……
Catch(SQLException sqlEx){
sqlEx.PrintStackTrace();
}
或者
try{
……
Statement s = con.createStatement();
……
Catch(SQLException sqlEx){
//什麼也不幹
}
checked異常導致瞭許多難以理解的代碼產生
當開發人員必須去捕獲一個自己無法正確處理的checked異常,通常的是重新封裝成一個新的異常後再拋出。這樣做並沒有為程序帶來任何好處。反而使代碼晚難以理解。
就像我們使用JDBC代碼那樣,需要處理非常多的try…catch.,真正有用的代碼被包含在try…catch之內。使得理解這個方法變理困難起來
checked異常導致異常被不斷的封裝成另一個類異常後再拋出
java 代碼
public void methodA()throws ExceptionA{
…..
throw new ExceptionA();
}
public void methodB()throws ExceptionB{
try{
methodA();
……
}catch(ExceptionA ex){
throw new ExceptionB(ex);
}
}
Public void methodC()throws ExceptinC{
try{
methodB();

}
catch(ExceptionB ex){
throw new ExceptionC(ex);
}
}
我們看到異常就這樣一層層無休止的被封裝和重新拋出。
checked異常導致破壞接口方法
一個接口上的一個方法已被多個類使用,當為這個方法額外添加一個checked異常時,那麼所有調用此方法的代碼都需要修改。
可見上面這些問題都是因為調用者無法正確的處理checked異常時而被迫去捕獲和處理,被迫封裝後再重新拋出。這樣十分不方便,並不能帶來任何好處。在這種情況下通常使用unChecked異常。
chekced異常並不是無一是處,checked異常比傳統編程的錯誤返回值要好用得多。通過編譯器來確保正確的處理異常比通過返回值判斷要好得多。
如果一個異常是致命的,不可恢復的。或者調用者去捕獲它沒有任何益處,使用unChecked異常。
如果一個異常是可以恢復的,可以被調用者正確處理的,使用checked異常。
在使用unChecked異常時,必須在在方法聲明中詳細的說明該方法可能會拋出的unChekced異常。由調用者自己去決定是否捕獲unChecked異常
倒底什麼時候使用checked異常,什麼時候使用unChecked異常?並沒有一個絕對的標準。但是筆者可以給出一些建議
當所有調用者必須處理這個異常,可以讓調用者進行重試操作;或者該異常相當於該方法的第二個返回值。使用checked異常。
這個異常僅是少數比較高級的調用者才能處理,一般的調用者不能正確的處理。使用unchecked異常。有能力處理的調用者可以進行高級處理,一般調用者幹脆就不處理。
這個異常是一個非常嚴重的錯誤,如數據庫連接錯誤,文件無法打開等。或者這些異常是與外部環境相關的。不是重試可以解決的。使用unchecked異常。因為這種異常一旦出現,調用者根本無法處理。
如果不能確定時,使用unchecked異常。並詳細描述可能會拋出的異常,以讓調用者決定是否進行處理。
3. 設計一個新的異常類
在設計一個新的異常類時,首先看看是否真正的需要這個異常類。一般情況下盡量不要去設計新的異常類,而是盡量使用java中已經存在的異常類。

java 代碼
IllegalArgumentException, UnsupportedOperationException 
不管是新的異常是chekced異常還是unChecked異常。我們都必須考慮異常的嵌套問題。
java 代碼
public void methodA()throws ExceptionA{
…..
throw new ExceptionA();
}
方法methodA聲明會拋出ExceptionA.
public void methodB()throws ExceptionB
methodB聲明會拋出ExceptionB,當在methodB方法中調用methodA時,ExceptionA是無法處理的,所以ExceptionA應該繼續往上拋出。一個辦法是把methodB聲明會拋出ExceptionA.但這樣已經改變瞭MethodB的方法簽名。一旦改變,則所有調用methodB的方法都要進行改變。
另一個辦法是把ExceptionA封裝成ExceptionB,然後再拋出。如果我們不把ExceptionA封裝在ExceptionB中,就丟失瞭根異常信息,使得無法跟蹤異常的原始出處。
java 代碼
public void methodB()throws ExceptionB{
try{
methodA();
……
}catch(ExceptionA ex){
throw new ExceptionB(ex);
}
}
如上面的代碼中,ExceptionB嵌套一個ExceptionA.我們暫且把ExceptionA稱為“起因異常”,因為ExceptionA導致瞭ExceptionB的產生。這樣才不使異常信息丟失。
所以我們在定義一個新的異常類時,必須提供這樣一個可以包含嵌套異常的構造函數。並有一個私有成員來保存這個“起因異常”。
java 代碼
public Class ExceptionB extends Exception{
private Throwable cause;
public ExceptionB(String msg, Throwable ex){
super(msg);
this.cause = ex;
}
public ExceptionB(String msg){
super(msg);
}
public ExceptionB(Throwable ex){
this.cause = ex;
}
}
當然,我們在調用printStackTrace方法時,需要把所有的“起因異常”的信息也同時打印出來。所以我們需要覆寫printStackTrace方法來顯示全部的異常棧跟蹤。包括嵌套異常的棧跟蹤。
java 代碼
public void printStackTrace(PrintStrean ps){
if(cause == null){
super.printStackTrace(ps);
}else{
ps.println(this);
cause.printStackTrace(ps);
}
}
一個完整的支持嵌套的checked異常類源碼如下。我們在這裡暫且把它叫做NestedException
java 代碼
public NestedException extends Exception{
private Throwable cause;
public NestedException (String msg){
super(msg);
}
public NestedException(String msg, Throwable ex){
super(msg);
This.cause = ex;
}
public Throwable getCause(){
return (this.cause ==null ?this :this.cause);
}
public getMessage(){
String message = super.getMessage();
Throwable cause = getCause();
if(cause != null){
message = message + “;nested Exception is ” + cause;
}
return message;
}
public void printStackTrace(PrintStream ps){
if(getCause == null){
super.printStackTrace(ps);
}else{
ps.println(this);
getCause().printStackTrace(ps);
}
}
public void printStackTrace(PrintWrite pw){
if(getCause() == null){
super.printStackTrace(pw);
}
else{
pw.println(this);
getCause().printStackTrace(pw);
}
}
public void printStackTrace(){
printStackTrace(System.error);
}
}
同樣要設計一個unChecked異常類也與上面一樣。隻是需要繼承RuntimeException。
4. 如何記錄異常
作為一個大型的應用系統都需要用日志文件來記錄系統的運行,以便於跟蹤和記錄系統的運行情況。系統發生的異常理所當然的需要記錄在日志系統中。
java 代碼
public String getPassword(String userId)throws NoSuchUserException{
UserInfo user = userDao.queryUserById(userId);
If(user == null){
Logger.info(“找不到該用戶信息,userId=”+userId);
throw new NoSuchUserException(“找不到該用戶信息,userId=”+userId);
}
else{
return user.getPassword();
}
}
public void sendUserPassword(String userId)throws Exception {
UserInfo user = null;
try{
user = getPassword(userId);
//……..
sendMail();
//
}catch(NoSuchUserException ex)(
logger.error(“找不到該用戶信息:”+userId+ex);
throw new Exception(ex);
}
我們註意到,一個錯誤被記錄瞭兩次.在錯誤的起源位置我們僅是以info級別進行記錄。而在sendUserPassword方法中,我們還把整個異常信息都記錄瞭。
筆者曾看到很多項目是這樣記錄異常的,不管三七二一,隻有遇到異常就把整個異常全部記錄下。如果一個異常被不斷的封裝拋出多次,那麼就被記錄瞭多次。那麼異常倒底該在什麼地方被記錄?
異常應該在最初產生的位置記錄!
如果必須捕獲一個無法正確處理的異常,僅僅是把它封裝成另外一種異常往上拋出。不必再次把已經被記錄過的異常再次記錄。
如果捕獲到一個異常,但是這個異常是可以處理的。則無需要記錄異常
java 代碼
public Date getDate(String str){
Date applyDate = null;
SimpleDateFormat format = new SimpleDateFormat(“MM/dd/yyyy”);
try{
applyDate = format.parse(applyDateStr);
}
catch(ParseException ex){
//乎略,當格式錯誤時,返回null
}
return applyDate;
}
捕獲到一個未記錄過的異常或外部系統異常時,應該記錄異常的詳細信息
java 代碼
try{
……
String sql=”select * from userinfo”;
Statement s = con.createStatement();
……
Catch(SQLException sqlEx){
Logger.error(“sql執行錯誤”+sql+sqlEx);
}
究竟在哪裡記錄異常信息,及怎麼記錄異常信息,可能是見仁見智的問題瞭。甚至有些系統讓異常類一記錄異常。當產生一個新異常對象時,異常信息就被自動記錄。
java 代碼
public class BusinessException extends Exception {
private void logTrace() {
StringBuffer buffer=new StringBuffer();
buffer.append("Business Error in Class: ");
buffer.append(getClassName());
buffer.append(",method: ");
buffer.append(getMethodName());
buffer.append(",messsage: ");
buffer.append(this.getMessage());
logger.error(buffer.toString());
}
public BusinessException(String s) {
super(s);
race();
}
這似乎看起來是十分美妙的,其實必然導致瞭異常被重復記錄。同時違反瞭“類的職責分配原則”,是一種不好的設計。記錄異常不屬於異常類的行為,記錄異常應該由專門的日志系統去做。並且異常的記錄信息是不斷變化的。我們在記錄異常同應該給更豐富些的信息。以利於我們能夠根據異常信息找到問題的根源,以解決問題。
雖然我們對記錄異常討論瞭很多,過多的強調這些反而使開發人員更為疑惑,一種好的方式是為系統提供一個異常處理框架。由框架來決定是否記錄異常和怎麼記錄異常。而不是由普通程序員去決定。但是瞭解些還是有益的。
5. J2EE項目中的異常處理
目前,J2ee項目一般都會從邏輯上分為多層。比較經典的分為三層:表示層,業務層,集成層(包括數據庫訪問和外部系統的訪問)。
J2ee項目有著其復雜性,J2ee項目的異常處理需要特別註意幾個問題。
在分佈式應用時,我們會遇到許多checked異常。所有RMI調用(包括EJB遠程接口調用)都會拋出java.rmi.RemoteException;同時RemoteException是checked異常,當我們在業務系統中進行遠程調用時,我們都需要編寫大量的代碼來處理這些checked異常。而一旦發生RemoteException這些checked異常對系統是非常嚴重的,幾乎沒有任何進行重試的可能。也就是說,當出現RemoteException這些可怕的checked異常,我們沒有任何重試的必要性,卻必須要編寫大量的try…catch代碼去處理它。一般我們都是在最底層進行RMI調用,隻要有一個RMI調用,所有上層的接口都會要求拋出RemoteException異常。因為我們處理RemoteException的方式就是把它繼續往上拋。這樣一來就破壞瞭我們業務接口。RemoteException這些J2EE系統級的異常嚴重的影響瞭我們的業務接口。我們對系統進行分層的目的就是減少系統之間的依賴,每一層的技術改變不至於影響到其它層。
java 代碼
//
public class UserSoaImplimplements UserSoa{
public UserInfo getUserInfo(String userId)throws RemoteException{
//……
遠程方法調用.
//……
}
}
public interface UserManager{
public UserInfo getUserInfo(Stirng userId)throws RemoteException;
}
同樣JDBC訪問都會拋出SQLException的checked異常。
為瞭避免系統級的checked異常對業務系統的深度侵入,我們可以為業務方法定義一個業務系統自己的異常。針對像SQLException,RemoteException這些非常嚴重的異常,我們可以新定義一個unChecked的異常,然後把SQLException,RemoteException封裝成unChecked異常後拋出。
如果這個系統級的異常是要交由上一級調用者處理的,可以新定義一個checked的業務異常,然後把系統級的異常封存裝成業務級的異常後再拋出。
一般地,我們需要定義一個unChecked異常,讓集成層接口的所有方法都聲明拋出這unChecked異常。
java 代碼
public DataAccessExceptionextends RuntimeException{
……
}
public interface UserDao{
public String getPassword(String userId)throws DataAccessException;
}
public class UserDaoImplimplements UserDAO{
public String getPassword(String userId)throws DataAccessException{
String sql = “select password from userInfo where userId= ‘”+userId+”’”;
try{

//JDBC調用
s.executeQuery(sql);

}catch(SQLException ex){
throw new DataAccessException(“數據庫查詢失敗”+sql,ex);
}
}
}
定義一個checked的業務異常,讓業務層的接口的所有方法都聲明拋出Checked異常.
java 代碼
public class BusinessExceptionextends Exception{
…..
}
public interface UserManager{
public Userinfo copyUserInfo(Userinfo user)throws BusinessException{
Userinfo newUser = null;
try{
newUser = (Userinfo)user.clone();
}catch(CloneNotSupportedException ex){
throw new BusinessException(“不支持clone方法:”+Userinfo.class.getName(),ex);
}
}
}
J2ee表示層應該是一個很薄的層,主要的功能為:獲得頁面請求,把頁面的參數組裝成POJO對象,調用相應的業務方法,然後進行頁面轉發,把相應的業務數據呈現給頁面。表示層需要註意一個問題,表示層需要對數據的合法性進行校驗,比如某些錄入域不能為空,字符長度校驗等。
J2ee從頁面所有傳給後臺的參數都是字符型的,如果要求輸入數值或日期類型的參數時,必須把字符值轉換為相應的數值或日期值。
如果表示層代碼校驗參數不合法時,應該返回到原始頁面,讓用戶重新錄入數據,並提示相關的錯誤信息。
通常把一個從頁面傳來的參數轉換為數值,我們可以看到這樣的代碼
java 代碼
ModeAndView handleRequest(HttpServletRequest request,HttpServletResponse response)throws Exception{
String ageStr = request.getParameter(“age”);
int age = Integer.parse(ageStr);
…………
String birthDayStr = request.getParameter(“birthDay”);
SimpleDateFormat format = new SimpleDateFormat(“MM/dd/yyyy”);
Date birthDay = format.parse(birthDayStr);
}
上面的代碼應該經常見到,但是當用戶從頁面錄入一個不能轉換為整型的字符或一個錯誤的日期值。
Integer.parse()方法被拋出一個NumberFormatException的unChecked異常。但是這個異常絕對不是一個致命的異常,一般當用戶在頁面的錄入域錄入的值不合法時,我們應該提示用戶進行重新錄入。但是一旦拋出unchecked異常,就沒有重試的機會瞭。像這樣的代碼造成大量的異常信息顯示到頁面。使我們的系統看起來非常的脆弱。
同樣,SimpleDateFormat.parse()方法也會拋出ParseException的unChecked異常。
這種情況我們都應該捕獲這些unChecked異常,並給提示用戶重新錄入。
java 代碼
ModeAndView handleRequest(HttpServletRequest request,HttpServletResponse response)throws Exception{
String ageStr = request.getParameter(“age”);
String birthDayStr = request.getParameter(“birthDay”);
int age = 0;
Date birthDay = null;
try{
age=Integer.parse(ageStr);
}catch(NumberFormatException ex){
error.reject(“age”,”不是合法的整數值”);
}
…………
try{
SimpleDateFormat format = new SimpleDateFormat(“MM/dd/yyyy”);
birthDay = format.parse(birthDayStr);
}catch(ParseException ex){
error.reject(“birthDay”,”不是合法的日期,請錄入’MM/dd/yyy’格式的日期”);
}
}
在表示層一定要弄清楚調用方法的是否會拋出unChecked異常,什麼情況下會拋出這些異常,並作出正確的處理。
在表示層調用系統的業務方法,一般情況下是無需要捕獲異常的。如果調用的業務方法拋出的異常相當於第二個返回值時,在這種情況下是需要捕獲

發佈留言