J2EE应用的异常处理框架

来源:互联网 发布:微信安全域名效验出错 编辑:程序博客网 时间:2024/05/19 19:31


转自:http://www.jmatrix.org/java/translation/175.html


这是译文,推荐看原文,原文出自OnJava,地址: An Exception Handling Framework for J2EE Applications
         在大多数的Java工程中,有很大一部分的代码都是样板代码,其中异常处理就属于这一类型。尽管业务逻辑也许仅仅有三四行代码,异常处理却可能多达20行。本文将讨论如何尽可能保持异常处理简单且直接,使开发者能将精力放到业务逻辑的实现上,而不是花费大量的时间来写异常处理的样板代码。此外,本文还会给予一些在J2EE环境中创建和处理异常的准则和指导,并列举一些可以使用异常来解决的业务问题。本文使用Struts框架作为实例,但本文提出的方法同样适用于其它的情况。
1. 我们何时需要Check和Unchecked异常?
       你是否曾经怀疑,为什么尽管知道自己并不知道如何处理这些异常,而仅仅只是在catch块输出相关的logging信息,但我们还是不得不在自己写的代码块外围放置大量的try-catch语句?这令人感到厌烦。为什么不能集中打印log信息,比如大多数的J2EE应用便是在前端控制器中进行log。换句话说,你喜欢不被它们所打扰,希望能节省下处理它们的时间。如果方法签名中包含throws语句抛出异常又是什么情况?你可以选择在catch语句中捕获这些异常亦或在你的方法中使用throws语句将它们抛出。这很烦。幸运的是,Java API中有一种类别的异常叫Unchecked异常,这种异常不强制捕获。那么,我们应该根据什么标准决定那种异常是checked的,那种应该是unchecked的呢?这里有一些指南:
  • 对于终端用户无法采取任何有用的动作的异常应该是unchecked的。如,对于致命和不可恢复的异常应该采用unchecked类型。把XMLParseException设置为checked异常是没有意义的,当此异常发生时,可以采取的唯一措施也许是基于异常追踪修复根源原因。通过扩展java.lang.RuntimeException,便可以创建自定义的unchecked异常
  • 与用户在应用中采取的动作相关的异常应该是checked类型的。checked异常要求客户端去捕获它们,你可能会问,为什么我们不将所有的异常都实现为unchecked类型?如果这么做,有些异常也许就没有能在合理的位置被捕获,同时,错误只有到了运行时才能被发现,这将会引起很严重的系统缺陷。checked异常的例子有:业务验证异常、安全异常等

2. 异常抛出策略

(1)只捕获一个基本的应用异常(也即,BaseAppException)并声明在throws语句中。
       在大多数的J2EE应用中,只有针对异常在表示层能得到什么,才能决定什么样的错误信息应该显示在屏幕上。那么为什么我们不能简单的将错误信息集中放到一个通用的地方?在J2EE应用中,前端控制器是进行通用处理的中心位置。
        此外,还有一种通用的机制可以用来传播异常。异常也需要以一种一般化的方式进行处理。为了做到这个,我们总是需要在控制终端捕获这个基本的应用异常BaseAppException。这意味着我们需要且仅需要把BaseAppException放到每个可以抛出一个checked异常的方法的throws语句中。这个概念使用Java的多态特性来隐藏了异常的实际实现,我们只需要在控制器中捕获BaseAppException,具体被抛出的异常实例可以是任何BaseAppException的实现类。使用这样的方法,在异常处理中可以有很大的灵活性。
  • 首先,你不需要在方法的throws语句块中放置太多的checked异常,而只需要一个BaseAppException。
  • 应用异常的catch块不再凌乱不堪,如果我们需要处理它们,只需要一个catch块即可。
  • 你不再需要自己进行异常处理(打印错误信息、获取错误代码),将会有一个抽象层——ExceptionHandler对其进行处理,关于ExceptionHandler,后续会进行介绍。
  • 即使你在后续的开发阶段中引入更多的异常到方法的实现中,方法的签名也无需变化,也即客户端的代码也无需改变,否则,可能整个的代码会因为一个小的改动而引起连锁反应。不过,新增抛出的异常还是应该在方法的Javadoc中限定,这样方法的使用者才能明白方法的契约。

这里有一个抛出checked异常的示例:

  1. public void updateUser(UserDTO userDTO)
  2. throws BaseAppException{
  3. UserDAO userDAO = new UserDAO();
  4. UserDAO.updateUser(userDTO);
  5. ...
  6. if(...)
  7. throw new RegionNotActiveException(
  8. "Selected region is not active");
  9. }
  10.  
  11. Controller Method:
  12. ...
  13. try{
  14. User user = new User();
  15. user.updateUser(userDTO);
  16. }catch(BaseAppException ex){
  17. //ExceptionHandler is used to handle
  18. //all exceptions derived from BaseAppException
  19. }
  20. ...
         到目前为止,我们已经说明了:所有可能抛出checked异常以及被Controller调用的方法在它们的throws语句中都应该只包含BaseAppException。然而,这意味着在throws语句中无法再有其它的应用异常,但是,如果你需要根据catch块中的某个异常类型执行一些业务逻辑呢?对于这种情况,方法也可以一个抛出特定的异常。但请记住,这只是一种特殊的情况,开发者不应该认为是理所当然。同时,这里提到的特殊的异常也应该继承BaseAppException。

下面是一个实例:

  1. CustomerDAO method:
  2. //throws CustomerNotActiveException along with
  3. //BaseAppException
  4. public CustomerDTO getCustomer(InputDTO inputDTO)
  5. throws BaseAppException,
  6. CustomerNotActiveException {
  7. . . .
  8. //Make a DB call to fetch the customer
  9. //details based on inputDTO
  10. . . .
  11. // if not details found
  12. throw new CustomerNotActiveException(
  13. "Customer is not active");
  14. }
  15.  
  16. Client method:
  17.  
  18. //catch CustomerNotActiveException
  19. //and continues its processing
  20. public CustomerDTO getCustomerDetails(
  21. UserDTO userDTO)
  22. throws BaseAppException{
  23. ...
  24. CustomerDTO custDTO = null;
  25. try{
  26. //Get customer details
  27. //from local database
  28. customerDAO.getCustomerFromLocalDB();
  29. }catch(CustomerNotActiveException){
  30. ...
  31. return customerDAO
  32. .activateCustomerDetails();
  33. }
  34. }

(2) 在web应用层处理unchecked异常

        所有的unchecked异常都应该在web应用层中进行处理。无论何时,当应用中有unchecked异常发生时,可以在web.xml中配置一个web页面来向终端用户显示。

(3) 将第三方异常包装为特定于应用的异常

无论何时,一个来源于其它外部接口的异常应该被封装为特定于应用的异常并进行合适的处理。

一个实例:

  1. try {
  2. BeanUtils.copyProperties(areaDTO, areaForm);
  3. } catch (Exception se) {
  4. throw new CopyPropertiesException(
  5. "Exception occurred while using
  6. copy properties", se);
  7. }

这里,由于我们只是想进行记录,所以CopyPropertiesException继承自java.lang.RuntimeException。因为对于所有这些异常,我们都只是抛出相同的unchecked异常CopyPropertiesException,因此我们捕获Exception而不是copyProperties方法可以抛出的特定的checked异常。

(4) 太多的异常

        你也许会想,如果我们为每个错误信息都创建一个异常,会不会导致异常类泛滥。例如,如果“未找到订单”是OrderNotFoundException的错误信息,你肯定不想再为“未找到客户”创建一个CustomerNotFoundException异常,原因很明显:这两个异常代表了相同的事情,只是它们使用的上下文有所不同。如果在处理异常时,我们能指定其上下文,则可以将异常的类型减少到只有一个——RecordNotFoundException。下面是一个例子:

  1. try{
  2. ...
  3. }catch(BaseAppException ex){
  4. IExceptionHandler eh =ExceptionHandlerFactory
  5. .getInstance().create();
  6. ExceptionDTO exDto = eh.handleException(
  7. "employee.addEmployee", userId, ex);
  8. }
         在这里,为了创建一个唯一的合成错误码,employee.addEmployee上下文被添加到上下文敏感的异常的错误代码中。例如,如果RecordNotFoundException的错误码是errorcode.recordnotfound,那么此上下文的合成错误码将是errorcode.recordnotfound.employee.addEmployee,这在上下文中是唯一的。

       然而,有个警告:如果你在同一个Client端方法中使用多个可以抛出RecordNotFoundException的接口,那么将难以知道你从哪个实体中捕获这个异常。在业务接口是公有的且能被各种外部客户端使用的场景,我们推荐只使用特定的异常,而不是一个像RecordNotFoundException这样的一般接口。对于基于数据库的可恢复异常,其异常类总是一样的,不同的只是它们发生的上下文,这时,上下文特定的接口将非常有用。

(5) J2EE应用的异常层次

       正如前面讨论,我们需要定义一个基本的异常类,称为BaseAppException,其中包含了所有应用异常的默认行为。我们将这个基本异常类放到每个可能抛出unchecked异常的方法的throws语句中。应用中所有的checked异常都应该是此基类的子类。

       有许多定义错误处理抽象的方式,然而,这些不同应该是因业务场景的不同而引起,而非技术上强制。错误处理的抽象可以分为下面这些类别,所有这些异常类都继承自BaseAppException:

Checked异常

  • 业务异常:是指执行业务逻辑时发生的异常,BaseBusinessException是这类异常的基类。
  • DB异常:与持久化机制交互时发生的异常,BaseDBException是这类异常的基类。
  • 安全异常:执行安全操作时产生的异常,这类异常的基类是BaseSecurityException。
  • 确认异常:用于“为了执行某一个任务而从终端用户获取确认信息”的场景,这类异常的基类是BaseConfirmationException。

Unchecked Exception

  • 系统异常:在某一些场景,你会想要使用unchecked异常。例如,你也许不像处理来自第三方API库的异常,因此,你想要把它们包装为uncheck异常并抛给控制器。有时,也会存在一些客户端无法处理的配置问题,同样应该被抛出为unchecked异常。所有自定义的unchecked异常应该从java.lang.RuntimeException类扩展。

(6) 表现层异常处理

        当某个异常发生时,表现层是需要负责决定对应应该采取怎样的动作。所做的决定包括基于抛出的异常识别错误码。同时,我们需要知道在处理完错误后应该重定向到那个页面。

         我们需要一个抽象层来基于异常的类型获取其错误码,当需要时,它也可以打印信息,我们称这个抽象层为ExceptionHandler。基于GOF的门面模式,它是处理衍生自BaseAppException的所有异常的异常处理子系统的一个“门面”。下面是一个Struts Action方法中的异常处理例子:

  1. try{
  2. ...
  3. DivisionDTO storeDTO = divisionBusinessDelegate
  4. .getDivisionByNum(fromDivisionNum);
  5. }catch(BaseAppException ex){
  6. IExceptionHandler eh = ExceptionHandlerFactory
  7. .getInstance().create();
  8. String expContext = "divisionAction.searchDivision";
  9. ExceptionDTO exDto = eh.handleException(
  10. expContext , userId, ex);
  11. ActionErrors errors = new ActionErrors();
  12. errors.add(ActionErrors.GLOBAL_ERROR
  13. ,new ActionError(
  14. exDto.getMessageCode()));
  15. saveErrors(request, errors);
  16. return actionMapping.findForward(
  17. "SearchAdjustmentPage");
  18. }
         如果你仔细看我们刚写的异常处理代码,你也许会意识到你正在为每个struts方法写相似的代码,这是令人痛苦的事情。我们的目标是尽可能的移除模板代码,我们需要再次对其进行抽象。
         解决方案是使用“模板方法”设计模式。我们需要一个基类用于包含模板方法的算法,算法中包含对BaseAppException的try-catch块及一个对dispatchMethod方法的调用,具体实现如下面的Struts Action所示:
  1. public abstract class BaseAppDispatchAction
  2. extends DispatchAction{
  3. ...
  4. protected static ThreadLocal
  5. expDisplayDetails = new ThreadLocal();
  6.  
  7. public ActionForward execute(
  8. ActionMapping mapping,
  9. ActionForm form,
  10. HttpServletRequest request,
  11. HttpServletResponse response) throws Exception{
  12. ...
  13. try{
  14. String actionMethod = request
  15. .getParameter(mapping.getParameter());
  16. finalDestination =dispatchMethod(mapping,
  17. form, request, response,actionMethod);
  18. }catch (BaseAppException Ex) {
  19. ExceptionDisplayDTO expDTO =
  20. (ExceptionDisplayDTO)expDisplayDetails
  21. .get();
  22. IExceptionHandler expHandler =
  23. ExceptionHandlerFactory
  24. .getInstance().create();
  25. ExceptionDTO exDto = expHandler
  26. .handleException(
  27. expDTO.getContext(), userId, Ex);
  28. ActionErrors errors = new ActionErrors();
  29. errors.add(ActionErrors.GLOBAL_ERROR,
  30. new ActionError(exDto
  31. .getMessageCode()));
  32. saveErrors(request, errors);
  33. return mapping.findForward(expDTO
  34. .getActionForwardName());
  35. } catch(Throwable ex){
  36. //log the throwable
  37. //throw ex;
  38. } finally {
  39. expDisplayDetails.set(null);
  40. }

        在Struts中,DispatchAction::dispatchMethod方法用于将请求发送给合适的Action方法————actionMethod。假设在某一次HTTP类型请求中,我们的“actionMethod”方法是searchDivision,因此dispatchMethod会将请求转发给某个派生自BaseAppDispatchAction的Action类中的searchDivision方法。可以看出,我们只在基类中进行异常处理,派生类只需实现Action的方法,这很符合“模板方法”设计模式的思想:无论dispatchMethod的具体实现被推迟到那个派生类,异常处理部分都保持不变。

对先前提到的Struts Action方法采用讨论的“模板方法”进行修改后如下所示:

  1. String exceptionActionForward =
  2. "SearchAdjustmentPage";
  3. String exceptionContext =
  4. "divisionAction.searchDivision";
  5.  
  6. ExceptionDisplayDTO expDTO =
  7. new ExceptionDisplayDTO(expActionForward,
  8. exceptionContext);
  9. expDisplayDetails.set(expDTO);
  10. ...
  11. DivisionDTO divisionDTO =divisionBusinessDelegate
  12. .getDivisionByNum(fromDivisionNum);
  13. ...
        Wow!现在它看起来是如此清晰。由于将异常处理集中放到BaseAppDispatchAction中,手工错误的范围也大大减小了。
        然而,我们需要设置异常上下文及ActionForward的名称,以便异常发生时,请求能被转发到正确的地方,在这里,我们将它们保存到  ThreadLocal变量——expDisplayDetails中。
        那么为什么我们要使用java.lang.ThreadLocal变量呢?expDisplayDetails在BaseAppDispatchAction中是一个保护数据成员,因此我们需要它是线程安全的,ThreadLocal正好可以做到这点。

(7) 异常处理

前面的部分一直有提到异常处理的抽象层,作为抽象层应该满足下面这些条件:

  • 识别异常类型并且获取对应的错误代码,并基于此向终端用于显示错误信息。
  • 打印异常信息。低层的日志记录机制是透明的,我们可以基于某些环境属性对其进行配置。
        你已经知道了,在表现层,我们只捕获BaseAppException。由于我们所有的checked异常都是BaseAppException的子类,这也就意味着,我们正捕获所有BaseAppException的派生类。基于类的名称我们很容易便可识别其错误码。
  1. //exp is an object of BaseAppException
  2. String className = exp.getClass().getName();

错误码可以基于异常类的名称在XML文件中进行配置,下面是一个异常配置的例子:

  1. messagecode.laborconfirmation
  2. true
  3. nologging
         如你所见,对于这个异常我们使他非常明确,使用的消息代码是messagecode.employeeconfirmation。为了适应国际化需求,真正的消息可以从“ResourceBundle “中获取。此外,我们也明确的声明了对于这种类型的异常不需要进行日志记录,因为它仅仅是一个确认消息而不是一个应用错误。

      下面是一个上下文敏感的异常的例子:

  1. messagecode.recordnotfound
  2. false
  3. true
  4. error

 

        这里,异常的contexting属性是true。我们传递给handleException方法的上下文参数可以用于创建一个唯一的错误码,例如,如果我们传递order.getOrder作为上下文,它与异常的消息代码的连接字符串将作为合成的消息码。因此,我们得到的唯一消息码将是:messagecode.recordnotfound.order.getOrder。

         exceptioninfo.xml中每个异常的消息可以封装到一个数据传输对象(DTO)——ExceptionInfoDTO中。现在,我们需要一个占位符来缓存这些对象,因为我们不想每次有异常发生时,一遍一遍的解析XML文件并创建对应的对象。这个工作可以委托给一个ExceptionInfoCache类,它可以缓存所有解析exceptioninfo.xml而得到的ExceptionInfoDTO对象。

      这好像也没什么好大惊小怪的,是吧?这里的核心是ExceptionHandler的实现,它可以使用在ExceptionInfoDTO中封装的数据来获取消息代码、创建ExceptionDTO对象,并基于ExceptionInfoDTO中有关异常的日志配置打印日志信息。

      下面是一个ExceptionHandler实现的handleException方法:

  1. public ExceptionDTO handleException(String userId,
  2. BaseAppException exp) {
  3. ExceptionDTO exDTO = new ExceptionDTO();
  4. ExceptionInfoCache ecache =
  5. ExceptionInfoCache.getInstance();
  6. ExceptionInfo exInfo = ecache
  7. .getExceptionInfo(
  8. ExceptionHelper.getClassName(exp));
  9. String loggingType = null;
  10. if (exInfo != null) {
  11. loggingType = exInfo.getLoggingType();
  12. exDTO.setConfirmation(exInfo
  13. .isConfirmation());
  14. exDTO.setMessageCode(exInfo
  15. .getMessageCode());
  16. }
  17.  
  18. FileLogger logger = new FileLoggerFactory()
  19. .create();
  20. logger.logException(exp, loggingType);
          根据业务需求的不同,可以有多个不同的ExceptionHandler接口的具体实现。可以将决定使用哪个实现的工作委托给一个工厂类————也即ExceptionHandlerFactory类。

最后:结论

          如果没有一个全面的异常处理策略,一个特别的异常处理块将导致出现非标准的错误处理,并使得代码难以维护。使用上述的方法,可以使J2EE应用中的异常处理流水化。