转自:http://www.jmatrix.org/java/translation/175.html
- 对于终端用户无法采取任何有用的动作的异常应该是unchecked的。如,对于致命和不可恢复的异常应该采用unchecked类型。把XMLParseException设置为checked异常是没有意义的,当此异常发生时,可以采取的唯一措施也许是基于异常追踪修复根源原因。通过扩展java.lang.RuntimeException,便可以创建自定义的unchecked异常
- 与用户在应用中采取的动作相关的异常应该是checked类型的。checked异常要求客户端去捕获它们,你可能会问,为什么我们不将所有的异常都实现为unchecked类型?如果这么做,有些异常也许就没有能在合理的位置被捕获,同时,错误只有到了运行时才能被发现,这将会引起很严重的系统缺陷。checked异常的例子有:业务验证异常、安全异常等
2. 异常抛出策略
- 首先,你不需要在方法的throws语句块中放置太多的checked异常,而只需要一个BaseAppException。
- 应用异常的catch块不再凌乱不堪,如果我们需要处理它们,只需要一个catch块即可。
- 你不再需要自己进行异常处理(打印错误信息、获取错误代码),将会有一个抽象层——ExceptionHandler对其进行处理,关于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
- }
- ...
下面是一个实例:
- 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();
- }
- }
(2) 在web应用层处理unchecked异常
所有的unchecked异常都应该在web应用层中进行处理。无论何时,当应用中有unchecked异常发生时,可以在web.xml中配置一个web页面来向终端用户显示。
(3) 将第三方异常包装为特定于应用的异常
无论何时,一个来源于其它外部接口的异常应该被封装为特定于应用的异常并进行合适的处理。
一个实例:
- try {
- BeanUtils.copyProperties(areaDTO, areaForm);
- } catch (Exception se) {
- throw new CopyPropertiesException(
- "Exception occurred while using
- copy properties", se);
- }
这里,由于我们只是想进行记录,所以CopyPropertiesException继承自java.lang.RuntimeException。因为对于所有这些异常,我们都只是抛出相同的unchecked异常CopyPropertiesException,因此我们捕获Exception而不是copyProperties方法可以抛出的特定的checked异常。
(4) 太多的异常
你也许会想,如果我们为每个错误信息都创建一个异常,会不会导致异常类泛滥。例如,如果“未找到订单”是OrderNotFoundException的错误信息,你肯定不想再为“未找到客户”创建一个CustomerNotFoundException异常,原因很明显:这两个异常代表了相同的事情,只是它们使用的上下文有所不同。如果在处理异常时,我们能指定其上下文,则可以将异常的类型减少到只有一个——RecordNotFoundException。下面是一个例子:
- try{
- ...
- }catch(BaseAppException ex){
- IExceptionHandler eh =ExceptionHandlerFactory
- .getInstance().create();
- ExceptionDTO exDto = eh.handleException(
- "employee.addEmployee", userId, ex);
- }
然而,有个警告:如果你在同一个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方法中的异常处理例子:
- 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");
- }
- 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类型请求中,我们的“actionMethod”方法是searchDivision,因此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);
- ...
(7) 异常处理
前面的部分一直有提到异常处理的抽象层,作为抽象层应该满足下面这些条件:
- 识别异常类型并且获取对应的错误代码,并基于此向终端用于显示错误信息。
- 打印异常信息。低层的日志记录机制是透明的,我们可以基于某些环境属性对其进行配置。
- //exp is an object of BaseAppException
- String className = exp.getClass().getName();
错误码可以基于异常类的名称在XML文件中进行配置,下面是一个异常配置的例子:
- messagecode.laborconfirmation
- true
- nologging
下面是一个上下文敏感的异常的例子:
- messagecode.recordnotfound
- false
- true
- 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方法:
- 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);
最后:结论