javaweb異常處理
為什麼要在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異常,什麼情況下會丟擲這些異常,並作出正確的處理。 在表示層呼叫系統的業務方法,一般情況下是無需要捕獲異常的。如果呼叫的業務方法丟擲的異常相當於第二個返回值時,在這種情況下是需要捕獲