淺談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 static int 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/ExceptionalJa va.html

  Bruce Eckel http://www.mindView.net/Etc/Disscussions/CheckedEx ceptions

  《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);

  }

  }

  咱們看到異常就這樣一層層無中止的被封裝和從新拋出(本文由wwhp轉載於www.yiyeqingjiaoyou.org)。 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 String 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 UserSoaImpl implements UserSoa{

  public UserInfo getUserInfo(String userId)throws RemoteException{

  //…… 遠程方法調用.

  //……

  }

  }

  public inte***ce UserManager{

  public UserInfo getUserInfo(Stirng userId)throws RemoteException;

  }

  一樣JDBC訪問都會拋出SQLException的checked異常。

  爲了不繫統級的checked異常對業務系統的深度侵進,咱們能夠爲業務方法定義一個業務系統本身的異常。針對像SQLException,RemoteException這些很是嚴重的異常,咱們能夠新定義一個unChecked的異常,而後把SQLException,RemoteException封裝成unChecked異常後拋出。假如這個系統級的異常是要交由上一級調用者處理的,能夠新定義一個checked的業務異常,而後把系統級的異常封存裝成業務級的異常後再拋出。通常地,咱們須要定義一個unChecked異常,讓集成層接口的全部方法都聲明拋出這unChecked異常。

  java 代碼

  public DataAccessException extends RuntimeException{

  ……

  }

  public inte***ce UserDao{

  public String getPassword(String userId)throws DataAccessException;

  }

  public class UserDaoImpl implements 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 BusinessException extends Exception{

  …..

  }

  public class 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異常,什麼狀況下會拋出這些異常,並做出正確的處理。在表示層調用系統的業務方法,通常狀況下是無須要捕捉異常的。假如調用的業務方法拋出的異常至關於第二個返回值時,在這種狀況下是須要捕捉html

相關文章
相關標籤/搜索