问题背景:

统一异常处理在WEB开发中可不是一个新颖的问题,然而,根据项目的实际情况,用的恰到好处,是可以在项目中省去大量冗余代码的。在以spring/springMVC做IOC容器的web项目中,常见的统一异常处理不外乎如下三种方式:
springMVC处理异常的3种方式:

(1) 使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver;

(2) 实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; 比如目前项目中就采用了这种方式,下面是项目中用于统一异常处理的类的代码:

public class CustomSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver {

private Log log = LogFactory.getLog(this.getClass());

    @ResponseBody
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        LogUtils.logException(ex);
        String viewName = determineViewName(ex, request);
        if (viewName == null && HttpRequestUtils.isAnsynJsonRequest(request)){
            Writer writer = null;
            try {
                response.setContentType("application/json");
                response.setCharacterEncoding("UTF-8");
                response.setHeader("Cache-Control", "no-cache");

                Map<String, Object> hashMap = new HashMap<String, Object>();
                hashMap.put("success", false);
                String exMessage = ex.getMessage();
                if ((ex instanceof BaseRuntimeException) && exMessage != null){
                    exMessage = exMessage.replaceAll("\"", "");
                } else {
                    exMessage = "系统异常";
                }
                writer = response.getWriter();
                hashMap.put("message", exMessage);
                String result = JsonUtils.object2Json(hashMap);
                writer.write(result);
            } catch (IOException e) {
                e.printStackTrace();
            }finally{
                if (writer != null){
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return new ModelAndView();
        } else {
            // Apply HTTP status code for error views, if specified.
            // Only apply it if we're processing a top-level request.
            Integer statusCode = determineStatusCode(request, viewName);
            if (statusCode != null) {
                applyStatusCodeIfPossible(request, response, statusCode);
                return getModelAndView(viewName, ex, request);
            }
        }

        return null;
    }

(3) 使用@ExceptionHandler注解实现异常处理;

然而,在项目中,最常见的处理方式就是在项目报出异常的时候跳转到一个友好的提示页面,以此规避页面上打印出大量异常堆栈信息导致用户看不懂、并且还影响体验的问题。
然而,上述处理方式对于在前端大量使用Ajax的情况下作用及其有限。Web项目中用户在进行了某项操作,或者后台进行了相关的参数校验后,往往需要返回提示信息给用户,告诉用户操作是否成功。 所以,在大多数场景下,项目发生异常(一般是业务异常),或者参数校验不成功时候,我们更希望的是这些出错的信息以数据的形式(比如json/xml数据)返回。然后web前端根据这些返回的提示信息弹出模态框或其他方式给予用户友好的提示。而不是直接跳转到某个或某几个特定的页面。

目前的解决方式:
然而,正如之前所说,目前本人所接触的项目采用的统一异常处理是上述提到的第(2)种方式。然而,web前端又使用了大量的ajax技术和后台交互。Web前端传入的参数在后端校验不成功,为了给予用户提示,是采用如下代码段所示的方式:


ActionResult result = new ActionResult(true);

        if (null == condition || null == condition.getBeginTime() || null == condition.getEndTime()) {
            result.setSuccess(false);
            result.setMessage("上传的参数错误");
        } else if (condition.getBeginTime().after(condition.getEndTime())) {
            result.setSuccess(false);
            result.setMessage("开始时间不能大于结束时间");
        } else if (PmsDateTimeUtil.gtMonthBetween(condition.getBeginTime(), condition.getEndTime(), PmsConstants.MAX_MONTH_QUERY)) {
            result.setSuccess(false);
            result.setMessage(String.format("查询时间不能超过%s个月", PmsConstants.MAX_MONTH_QUERY));
        }
        return result;

上述代码段中,类ActionResult封装了参数校验不通过的错误信息,web前端调用一旦参数校验不通过,就会返回一个ActionResult类的实例对象,该对象会被转为json字符串然后返回给前端。比如,前端传入的开始时间大于结束时间,后端返回给前端的提示信息是如下这个样子的json字符串:

{ 
“msg”: ”开始时间不能大于结束时间”, 
“data”: “null”,
“success” : “false” 
}

使用ExceptionHandler实现统一异常处理:
事实上,使用ExceptionHandler注解实现统一异常处理,也可以实现当抛出异常后后端返回异常信息字符串(json/xml)而不是跳转到某个特定页面的功能。使用统一异常处理的好处是能够将异常信息统一捕捉并组装成固定格式的数据返回,我想在ajax回调处理中好处可多了, 回调得到的数据因为格式统一,前端可以很方便的通过某种控件进行呈现或友好提示 。 虽然也可以手动在Controller层的方法返回的结果中添加异常信息,但是只会徒增代码量,却不能使我们更好的专注于业务逻辑。
项目中的使用示例如下:

1、增加BaseExceptionHandleAction类,并在类中同时使用@ExceptionHandler和@ResponseBody注解声明异常处理,示例代码如下:


public class BaseExceptionHandleAction {

    /** 基于@ExceptionHandler异常处理 */
    @ExceptionHandler
    @ResponseBody
    public Map<String, Object>  handleAndReturnData(HttpServletRequest request, HttpServletResponse response, Exception ex) {

        Map<String, Object> data = new HashMap<String, Object>();
        if(ex instanceof BusinessException) {
            BusinessException e = (BusinessException)ex;
            data.put("code", e.getCode());
        }
        data.put("msg", ex.getMessage());
        data.put("success", false);
        data.put("data", null);
        return data;
    }
}

2、在项目中,我们可以使所有需要统一异常处理的Controller都继承该类,如下所示,我们写了一个Controller,名字叫ExceptionTestController, 该类继承于BaseExceptionHandleAction:作为示例,本人写了一个名为test的方法以演示统一异常处理。


@Controller
public class ExceptionTestController extends BaseExceptionHandleAction {

    @RequestMapping(value = "/exceptionTest", method = RequestMethod.GET)
    public void  test(HttpServletRequest request, HttpServletResponse response, Condition condition) {

        // 此处可能还有大量代码,略

        if (null == condition || null == condition.getBeginTime() || null == condition.getEndTime()) {
            throw new BusinessException("上传的参数错误");
        }

        if (condition.getBeginTime().after(condition.getEndTime())) {
            throw new BusinessException("开始时间不能大于结束时间");
        }

        if (PmsDateTimeUtil.gtMonthBetween(condition.getBeginTime(), condition.getEndTime(),
                PmsConstants.MAX_MONTH_QUERY)) {
            throw new BusinessException(String.format("查询时间不能超过%s个月", PmsConstants.MAX_MONTH_QUERY));
        }

        // 此处可能还有大量代码,略
    }

}

然后,假如前端传入的参数开始时间小于结束时间,前端收到的相应字符串如下:

{ 
“msg “: "开始时间不能大于结束时间"
“data” : “null”, 
“success” : “false” 
}

两种方式的比较:
在上述的示例中,本人讲述了两种返回提示信息给web前端的方式。第一种方式每次都创建一个对象用于封装提示信息,上述阐述中封装错误信息的对象是ActionResult。第二种方式则是巧妙的利用了java的异常处理机制。

虽然两种方式都达到了同样的效果,然而,第一种方式需要主动封装错误信息并返回给前端,导致代码量剧增。由于历史原因,目前项目中也存在同样的问题。 并且,很容易在代码中写出大量if else这样的语句,代码显得不够优雅。 第二种方式则不需要手动组装结果,重复代码量少了,代码也显得足够优雅。并且,可以使开发人员更专注的去处理相关业务逻辑。 另外,相比于第一中方式,第二种方式返回提示信息是即时的,参数不合法立即返回。第一种方式则需要等到return 语句返回的时候才返回提示信息,很可能参数已经出错了,代因为开发人员的粗心大意,导致代码还是走了一段无用的业务逻辑才返回,所以业务延迟大大增加,有没有起到实际的作用。最后,试想一下,在一个业务量庞大的项目中,利用框架或语言本身尽可能降低编码的复杂度和代码量意味着什么?新人容易上手?容易维护 ? 我想,都有吧。

简单总结
使用@ExceptionHandler进行统一异常处理的好处已经在上面有所阐述,作为示例,上述的代码略显简单,要想让统一异常处理机制更加健壮和可靠,需要开发人员进一步的完善。但需要注意的是,在spring/springMVC中使用上述机制,需要@ExceptionHandler和@ResponseBody两个注解同时使用。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐