一個用於J2EE應用程序的異常處理框架

 時間:2006-06-01
作者:ShriKant Vashishtha
瀏覽次數: <script language="JavaScript" src="http://203.81.25.103/cgi-bin/beadevcount.cgi?d_id=803" type="text/JavaScript"></script> 2785
本文關鍵字:J2EEStrutsRuntimeExceptiondesign patterns最佳實踐異常處理包裝設計模式外觀模板方法

  在大多數Java項目中,大部分代碼都是樣板代碼。異常處理就屬於此類代碼。即使業務邏輯只有3到4行代碼,用於異常處理的代碼也要佔10到20行。本文將討論如何讓異常處理保持簡單和直觀,使開發人員可以專心於開發業務邏輯,而不是把時間浪費在編寫異常處理的樣板代碼上。本文還將說明用於在J2EE環境中創建和處理異常的基礎知識和指導原則,並提出了一些可以使用異常解決的業務問題。本文將使用Struts框架作爲表示實現,但該方法適用於任何表示實現。

使用checked和unchecked異常的場景

  您是否曾經想過,爲什麼要在編寫好的代碼塊周圍放置一個try-catch塊,即便明知道無法對這些異常進行什麼處理,而只滿足於把它們放在catch塊中?您可能想知道,爲什麼不能把這項工作放在一個集中的地方完成?在大多數情況下,這個地方對於J2EE應用程序來說就是一個前端控制器。換句話說,開發人員不會因爲它們而受到干擾,因爲根本不必很多地過問它們。但是,如果一個方法名稱包含一個throws子句,會出現什麼情況呢?開發人員或者必須捕捉這些異常,或者把它們放在自己的方法的throws子句中。這就是痛苦的根源!幸運的是,Java API有一類叫做unchecked exception的異常,它們不必捕捉。但是,仍然存在一個問題:根據什麼來決定哪些是checked異常,哪些是unchecked異常?下面給出一些指導原則:

  • 終端用戶無法採取有效操作的異常應該作爲unchecked異常。例如,致命的和不可恢復的異常就應該是unchecked。把XMLParseException(在解析XML文件時拋出)作爲checked異常沒有任何意義,因爲惟一能夠採取的措施就是基於異常跟蹤來解決根本問題。通過擴展java.lang.RuntimeException,可以創建自定義的unchecked異常。
  • 在應用程序中,與用戶操作相關的異常應該是checked異常。checked異常要求客戶端來捕捉它們。您可能會問,爲什麼不把所有異常都當作是unchecked。這樣做的問題在於,其中一些異常無法在正確的位置被捕捉到。這會帶來更大的問題,因爲錯誤只有在運行時才能被識別。checked異常的例子有業務確認異常、安全性異常等等。

異常拋出策略

只捕捉基本應用程序異常(假定爲BaseAppException)並在throws子句中聲明

  在大多數J2EE應用程序中,關於針對某個異常應該在哪一界面上顯示哪條錯誤消息的決策只能在表示層中做出。這會帶來另一個問題:爲什麼我們不能把這種決策放在一個公共的地方呢?在J2EE應用程序中,前端控制器就是一個進行常見處理的集中位置。

  此外,必須有一種用於傳播異常的通用機制。異常也需要以一種普適的方式得到處理。爲此,我們始終需要在控制器端捕捉基本應用程序異常BaseAppException。這意味着我們需要把BaseAppException異常(只有這個異常)放入可以拋出checked異常的每個方法的throws子句中。這裏的概念是使用多態來隱藏異常的實際實現。我們在控制器中捕捉BaseAppException,但是所拋出的特定異常實例可能是幾個派生異常類中的任意一個。藉助於這種方法,可以獲得許多異常處理方面的靈活性:

  • 不需要在throws子句中放入大量的checked異常。throws子句中只需要有一個異常。
  • 不需要再對應用程序異常使用混亂的catch塊。如果需要處理它們,一個catch塊(用於BaseAppException)就足夠了。
  • 開發人員不需要親自進行異常處理(日誌記錄以及獲取錯誤代碼)。這種抽象是由ExceptionHandler完成的,稍後本文會就此點進行討論。
  • 即使稍後把更多異常引入到方法實現中,方法名稱也不會改變,因此也不需要修改客戶端代碼,否則就會引起連鎖反應。然而,拋出的異常需要在方法的Javadoc中指定,以便讓客戶端可以看到方法約束。

  下面給出拋出checked異常的一個例子:

public void updateUser(UserDTO userDTO) 
  
throws BaseAppException{
    UserDAO userDAO 
= new UserDAO();
    UserDAO.updateUser(userDTO);
    ...
    
if(...)
        
throw new RegionNotActiveException(
            
"Selected region is not active");
}


Controller Method:
...
try{
    User user 
= new User();
    user.updateUser(userDTO);
}
catch(BaseAppException ex){
    
//ExceptionHandler is used to handle
    
//all exceptions derived from BaseAppException
}

...

  迄今爲止,我們已經說明,對於所有可能拋出checked異常並被Controller調用的方法,其throws子句中應該只包含checked異常。然而,這實際上暗示着我們在throws子句中不能包含其他任何應用程序異常。但是,如果需要基於catch塊中某種類型的異常來執行業務邏輯,那又該怎麼辦呢?要處理這類情況,方法還可以拋出一個特定異常。記住,這是一種特例,開發人員絕對不能認爲這是理所當然的。同樣,此處討論的應用程序異常應該擴展BaseAppException類。下面給出一個例子:

CustomerDAO method:
//throws CustomerNotActiveException along with
//BaseAppException
public CustomerDTO getCustomer(InputDTO inputDTO)
    
throws BaseAppException,
        CustomerNotActiveException 
{
    . . .
    
//Make a DB call to fetch the customer 
    
//details based on inputDTO
    . . .
    
// if not details found
    throw new CustomerNotActiveException(
        
"Customer is not active");
}


Client method:

//catch CustomerNotActiveException
//and continues its processing
public CustomerDTO getCustomerDetails(
  UserDTO userDTO)
    
throws BaseAppException{
    ...
    CustomerDTO custDTO 
= null;
    
try{
        
//Get customer details 
        
//from local database
        customerDAO.getCustomerFromLocalDB();
    }
catch(CustomerNotActiveException){
        ...
        
return customerDAO
            .activateCustomerDetails();
    }

}

在web應用程序層次上處理unchecked異常

  所有unchecked異常都應該在web應用程序層次上進行處理。可以在web.xml文件中配置web頁面,以便當應用程序中出現unchecked異常時,可以把這個web頁面顯示給終端用戶。

把第三方異常包裝到特定於應用程序的異常中

  當一個異常起源於另一個外部接口(組件)時,應該把它包裝到一個特定於應用程序的異常中,並進行相應處理。

  例子:

try {
    BeanUtils.copyProperties(areaDTO, areaForm);
}
 catch (Exception se) {
    
throw new CopyPropertiesException(
        
"Exception occurred while using 
            copy properties", se);
}

  這裏,CopyPropertiesException擴展了java.lang.RuntimeException,我們將會記錄它。我們捕捉的是Exception,而不是copyProperties方法可以拋出的特定checked異常,因爲對於所有這些異常來說,我們都會拋出同一個unchecked CopyPropertiesException異常。

過多異常

  您可能想知道,如果我們爲每條錯誤消息創建一個異常,異常類自身是否會溢出呢?例如,如果“Order not found”是OrderNotFoundException的一條錯誤消息,您肯定不會讓CustomerNotFoundException的錯誤消息爲“Customer not found”,理由很明顯:這兩個異常代表同樣的意義,惟一的區別在於使用它們的上下文不同。所以,如果可以在處理異常時指定上下文,我們無疑可以把這些異常合併爲一個RecordNotFoundException。下面給出一個例子:

try{
    ...
}
catch(BaseAppException ex){
    IExceptionHandler eh 
=ExceptionHandlerFactory
      .getInstance().create();
    ExceptionDTO exDto 
= eh.handleException(
        
"employee.addEmployee", userId, ex);
}

  在這裏,employee.addEmployee上下文將被附加給一個上下文敏感的異常的錯誤代碼,從而產生惟一的錯誤代碼。例如,如果RecordNotFoundException的錯誤代碼是errorcode.recordnotfound,那麼這個上下文的最終錯誤代碼將變爲errorcode.recordnotfound.employee.addEmployee,它對於這個上下文是惟一的錯誤代碼。

  然而,我們要給出一個警告:如果您準備在同一個客戶端方法中使用多個接口,而且這些接口都可以拋出RecordNotFoundException異常,那麼想要知道是哪個實體引發了這個異常就變得十分困難。如果業務接口是公共的,而且可以被各種外部客戶端使用,那麼建議只使用特定的異常,而不使用像RecordNotFoundException這樣的一般性異常。特定於上下文的異常對於基於數據庫的可恢復異常來說非常有用,因爲在這種情況下,異常類始終是相同的,不同的只有它們出現的上下文。

J2EE應用程序的異常層次結構

  正如前面討論的那樣,我們需要定義一個異常基類,叫做BaseAppException,它包含了所有應用程序異常的默認行爲。我們將把這個基類放到所有可能拋出checked異常的方法的throws子句中。應用程序的所有checked異常都應該是這個基類的子類。有多種定義錯誤處理抽象的方式。然而,其中的區別更多地是與業務類而不是與技術有關。對錯誤處理的抽象可分爲以下幾類。所有這些異常類都是從BaseAppException派生而來。

checked異常

  • 業務異常:執行業務邏輯時出現的異常。BaseBusinessException是這類異常的基類。
  • 數據庫異常:與持久化機制進行交互時拋出的異常。BaseDBException是這類異常的基類。
  • 安全性異常:執行安全性操作時出現的異常。這類異常的基類是BaseSecurityException。
  • 確認異常:在從終端用戶處獲得確認以執行某個特定任務時使用。這類異常的基類是BaseConfirmationException。

unchecked異常

  • 系統異常:有時候我們希望使用unchecked異常。例如下面的情況:不想親自處理來自第三方庫API的異常,而是希望把它們包裝在unchecked異常中,然後拋出給控制器。有時會出現配置問題,這些問題也不能由客戶端進行處理,而應該被當作unchecked異常。所有自定義的unchecked異常都應該擴展自java.lang.RuntimeException類。

表示層上的異常處理

  表示層獨自負責決定對一個異常採取什麼操作。這種決策涉及到識別拋出異常的錯誤代碼。此外,我們還需要知道在處理錯誤之後應該把錯誤消息重定向到哪一界面。

  我們需要對基於異常類型獲得錯誤代碼這個過程進行抽象。必要時還應該執行日誌記錄。我們把這種抽象稱之爲ExceptionHandler。它基於“四人幫”(Gang of Four,GOF) 外觀模式(《Design Patterns》一書中說,該模式是用於“爲子系統中的一組接口提供一個統一接口。外觀定義了一個更高級別的接口,使子系統變得更加易於使用。”),是用於處理所有派生自BaseAppException的異常的整個異常處理系統的外觀。下面給出一個在Struts Action方法中進行異常處理的例子:

try
    ... 
    DivisionDTO storeDTO 
= divisionBusinessDelegate 
      .getDivisionByNum(fromDivisionNum); 
}
catch(BaseAppException ex)
    IExceptionHandler eh 
= ExceptionHandlerFactory 
      .getInstance().create(); 
    String expContext 
= "divisionAction.searchDivision"
    ExceptionDTO exDto 
= eh.handleException( 
        expContext , userId, ex); 
    ActionErrors errors 
= new ActionErrors(); 
        errors.add(ActionErrors.GLOBAL_ERROR 
          ,
new ActionError( 
          exDto.getMessageCode())); 
    saveErrors(request, errors); 
    
return actionMapping.findForward( 
        
"SearchAdjustmentPage"); 
}

  如果更仔細地觀察我們剛剛編寫的異常處理代碼,您可能會意識到,爲每個Struts方法編寫的代碼是十分相似的,這也是一個問題。我們的目標是儘可能地去掉樣板代碼。我們需要再次對它進行抽象。

  解決方案是使用模板方法(Template Method)設計模式(引自GOF:“它用於實現一個算法的不變部分,並把可變的算法部分留給子類來實現。”)。我們需要一個包含模板方法形式算法的基類。該算法將包含用於BaseAppException的try-catch塊和對dispatchMethod方法的調用,方法實現(委託給派生類)如下面的基於Struts的Action中所示:

public abstract class BaseAppDispatchAction 
  
extends DispatchAction{
...
protected static ThreadLocal 
  expDisplayDetails 
= new ThreadLocal();

public ActionForward execute(
  ActionMapping mapping,
  ActionForm form,
  HttpServletRequest request,
  HttpServletResponse response) 
throws Exception{
    ...
    
try{
        String actionMethod 
= request
           .getParameter(mapping.getParameter());
        finalDestination 
=dispatchMethod(mapping,
          form, request, response,actionMethod);
    }
catch (BaseAppException Ex) {
        ExceptionDisplayDTO expDTO 
= 
           (ExceptionDisplayDTO)expDisplayDetails
               .get();
        IExceptionHandler expHandler 
= 
            ExceptionHandlerFactory
                .getInstance().create();
        ExceptionDTO exDto 
= expHandler
            .handleException(
                expDTO.getContext(), userId, Ex);
        ActionErrors errors 
= new ActionErrors();
        errors.add(ActionErrors.GLOBAL_ERROR, 
            
new ActionError(exDto
                .getMessageCode()));
        saveErrors(request, errors);
        
return mapping.findForward(expDTO
            .getActionForwardName());
    }
 catch(Throwable ex){
           
//log the throwable
           
//throw ex;
    }
 finally {
           expDisplayDetails.set(
null);
    }

  在Struts中,DispatchAction::dispatchMethod方法用於把請求轉發給正確的Action方法,叫做actionMethod。

  我們假定從一個HTTP請求獲得searchDivision作爲actionMethod:dispatchMethod將在BaseAppDispatchAction的派生Action類中把請求分派給searchDivision方法。在這裏,您可以看到,異常處理僅在基類中完成,而派生類則只實現Action方法。這採用了模板方法設計模式,在該模式中,異常處理部分是保持不變的,而dispatchMethod方法的實際實現(可變部分)則交由派生類完成。

  修改後的Struts Action方法如下所示:

... 
String exceptionActionForward 
= 
    
"SearchAdjustmentPage"
String exceptionContext 
= 
    
"divisionAction.searchDivision"
        
ExceptionDisplayDTO expDTO 
= 
    
new ExceptionDisplayDTO(expActionForward, 
        exceptionContext); 
expDisplayDetails.set(expDTO); 
... 
DivisionDTO divisionDTO 
=divisionBusinessDelegate 
   .getDivisionByNum(fromDivisionNum); 
...

  現在它看起來相當清晰。因爲異常處理是在一個集中的位置上(BaseAppDispatchAction)完成的,手動錯誤可能造成的影響也降至最低。

  然而,我們需要設置異常上下文和ActionForward方法的名稱,如果有異常出現,請求就會被轉發到該方法。我們將在ThreadLocal變量expDisplayDetails中設置這些內容。

  但是,爲什麼要使用java.lang.ThreadLocal變量呢?expDisplayDetails是BaseAppDispatchActiion類中的一個受保護數據成員,這也是它需要是線程安全的原因。java.lang.ThreadLocal對象在這裏便可派上用場。

異常處理程序

  在上一部分中,我們討論瞭如何對異常處理進行抽象。下面給出一些應該滿足的約束:

  • 識別異常類型並獲得相應的錯誤代碼,該錯誤代碼可用於顯示一條消息給終端用戶。
  • 記錄異常。底層的日誌記錄機制被隱藏,可以基於一些環境屬性對其進行配置。

  您可能已經注意到了,我們在表示層中捕捉的惟一異常就是BaseAppException。由於所有checked異常都是BaseAppException的子類,這意味着我們要捕捉BaseAppException的所有派生類。基於類名稱來識別錯誤代碼再容易不過了。

//exp is an object of BaseAppException
String className = exp.getClass().getName();

  可以基於異常類的名稱在一個XML文件(exceptioninfo.xml)中對錯誤代碼進行配置。下面給出異常配置的一個例子:

 

<exception name="EmployeeConfirmationException">
    
<messagecode>messagecode.laborconfirmation</messagecode>
    
<confirmationind>true</confirmationind>
    
<loggingtype>nologging</loggingtype>
</exception>

  正如您看到的那樣,我們把這個異常變爲顯式,要使用的消息代碼是messagecode.employeeconfirmation。然後,爲了實現國際化的目的,可以從ResourceBundle提取實際的消息。我們很清楚,不需要對這類異常執行日誌記錄,因爲它只是一條確認消息,而不是一個應用程序錯誤。

  讓我們看一看上下文敏感異常的一個例子:

 

<exception name="RecordNotFoundException">
    
<messagecode>messagecode.recordnotfound</messagecode>
    
<confirmationind>false</confirmationind>
    
<contextind>true</contextind>
    
<loggingtype>error</loggingtype>
</exception>

  在這裏,這個表達式的contextind爲true。在handleException方法中傳遞的上下文可用於創建惟一的錯誤代碼。例如,如果我們把order.getOrder當作一個上下文進行傳遞,結果得到的消息代碼就是異常的消息代碼和所傳遞的上下文的串聯。因此,我們獲得了一個像messagecode.recordnotfound.order.getOrder這樣的惟一消息代碼。

  對於每個異常來說,可以把exceptioninfo.xml 中的數據封裝到一個叫做ExceptionInfoDTO的數據傳輸對象(data transfer object,DTO)。現在,我們還需要一個佔位符,用於緩存這些對象,因爲我們不想在異常出現時反覆解析XML文件和創建對象。這項工作可以委託給一個叫做ExceptionInfoCache的類來完成,這個類將會在從exceptioninfo.xml文件讀取ExceptionInfoDTO對象信息之後緩存所有這些對象。

  現在您是否弄清楚了這整個過程?這種方法的核心部分是ExceptionHandler實現,該實現將使用封裝在ExceptionInfoDTO中的數據來獲取消息代碼,創建ExceptionDTO對象,然後基於在給定異常的ExceptionInfoDTO中指定的日誌記錄類型來記錄它。

  下面是ExceptionHandler實現的handleException方法:

public ExceptionDTO handleException(String userId,
      BaseAppException exp) 
{  
    ExceptionDTO exDTO 
= new ExceptionDTO();
    ExceptionInfoCache ecache 
= 
        ExceptionInfoCache.getInstance();
    ExceptionInfo exInfo 
= ecache
      .getExceptionInfo(
        ExceptionHelper.getClassName(exp));
    String loggingType 
= null;
    
if (exInfo != null{
        loggingType 
= exInfo.getLoggingType();
        exDTO.setConfirmation(exInfo
            .isConfirmation());
        exDTO.setMessageCode(exInfo
            .getMessageCode());
    }


    FileLogger logger 
= new FileLoggerFactory()
        .create();
    logger.logException(exp, loggingType);

  根據不同的業務需求,ExceptionHandler接口可以有多種實現。決定使用何種實現的任務可交由Factory來完成,特別是ExceptionHandlerFactory類。

結束語

  如果缺乏全面的異常處理策略,一些特殊的異常處理塊便可能導致出現非標準的錯誤處理和不可維護的代碼。通過使用上面的方法,便可簡化J2EE應用程序中的異常處理過程。

參考資料

原文出處:http://www.onjava.com/pub/a/onjava/2006/01/11/exception-handling-framework-for-j2ee.html

 作者簡介
  ShriKant Vashishtha 當前是印度Tata Consultancy Services Limited (TCS)公司的解決方案架構師。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章