为什么需要统一异常处理:

  • 在一个系统中出异常是无法避免的,特别是非业务异常。我在在写业务代码时如果还要去处理非业务异常(如:空指针、数组越界、JDBC异常等)是相当繁琐的,而且不能保证这种异常全部捕获处理了,因为这种非业务异常可能出现在每一行代码里。其次在写业务代码时如果处理非业务异常会导致开发人员思维的分散,对业务的场景的思考严谨性造成影响,最后的结果就是业务代码的bug率上升导致功能不稳定
  • 上面经说到系统异常不可避免,那么当系统出现异常之后如果我们没有处理异常(打印错误日志、返回可读的错误消息给前端)的话会导致:异常日志丢失、前端我法给用户操作失败的提示(UI交互的问题)。所以代码里没有对异常做处理时为了避免上面的问题需要一个兜底的方案统一处理异常

异常类型

非业务异常

非业务异常:是指由第三方框架库和JVM抛出的异常,下面列出了以一些常见的非业务异常。

  • 空指针异常 NullPointerException
  • 算术异常类 ArithmeticExecption
  • 数组下标越界异常 ArrayIndexOutOfBoundsException
  • 输入输出异常 IOException
  • 类型强制转换异常 ClassCastException
  • 数据库操作异常 SQLException

业务异常

业务异常:是指不是框架、第三方库或JVM抛出的异常,是开发人员写业务代码时抛出的异常,下面列出了一些需要抛出业务异常的场景和异常类型。

  • 接口请求参数校验错误时(如:非空、数据长度、数据格式…)
  • 出现业务处理的逻辑错误时(如:用户购买商品无库存的时候)
  • 用户无权限操作时
  • 处理业务时调用的第三方系统报错时
  • …其他各种业务场景

统一异常处理的实现

统一响应格式

一般来讲系统报出的异常信息是需要传递给前端展示提示用户操作失败。为了到达当系统出异常之后前端总是会提示相应的错误提示,所以异常的响应数据的结构最好是一致的(让前端的处理可以统一)。
前面已经讲解了RESTful API规范,这里的异常信息响应也是需要遵守这个规范的,下面列出了不同场景异常的返回数据格式。

统一的异常响应格式如下:

{
    "timestamp": "Date | 请求处理的时间",
    "status": "int | HTTP响应状态码",
    "path": "String | 请求处理路径"
    "message": "String | 给前端显示的消息",
    "error": "String | 服务端错误消息",
    "exception": "String | 异常类型(包点类名称),不一定会有这个字段(如: HTTP响应状态码为404时)",
}

错误响应示例

  • 请求参数错误时,相应状态码-400

    {
      "timestamp": "2019-08-15 13:18:30",
      "error": "fileSource: 不能为空",
      "status": 400,
      "exception": "javax.validation.ConstraintViolationException",
      "message": "请求参数校验失败",
      "path": "/api/file/upload",
      "validMessageList": [
          {
              "entityName": "UploadFileReq",
              "filed": "fileSource",
              "value": "null",
              "errorMessage": "不能为空",
              "code": "NotBlank"
          }
      ]
    }
  • 业务处理发生业务异常时,相应状态码-400

    {
      "timestamp": "2019-08-15 13:12:03",
      "error": "业务异常",
      "status": 400,
      "exception": "org.clever.common.exception.BusinessException",
      "message": "当前请求并非上传文件的请求",
      "path": "/api/file/upload"
    }
  • 用户未登录时,相应状态码-401

    {
      "timestamp": 1565846483857,
      "error": "Full authentication is required to access this resource",
      "status": 401,
      "exception": "org.springframework.security.authentication.InsufficientAuthenticationException",
      "message": "您还未登录,请先登录",
      "path": "/api/file/upload"
    }
  • 用户无权限时,相应状态码-403

    {
      "message": "没有访问权限",
      "timestamp": 1565847642580
    }
  • 资源不存在时,相应状态码-404

    {
      "timestamp": "2019-08-15 13:41:05",
      "status": 404,
      "error": "Not Found",
      "message": "No message available",
      "path": "/api/file/123"
    }
  • 意外的系统异常,相应状态码-500

    {
      "timestamp": "2019-08-15 13:44:34",
      "error": "错误123456",
      "status": 500,
      "exception": "java.lang.RuntimeException",
      "message": "服务器内部错误",
      "path": "/api/file/upload"
    }

前端对响应数据的处理

根据上面定义的异常返回的数据格式,前端应该先读取HTTP相应状态码判断请求操作是否成功(200才算成功),如失败了需要读取返回数据的message字段进行展示。当遇到401和403时可以根据需要做页面跳转的操作。遇到400而且validMessageList字段有值时说明时请求参数校验失败,可以根据需要对提交表单对应字段做提示(返回数据中有具体字段和具体错误,filederrorMessage字段)

SpringBoot统一异常处理

SpringBoot本身就有统一异常拦截的功能,使用@ExceptionHandler注解配置来处理异常拦截逻辑。目前我自定义了一个业务异常类型BusinessException来抛出业务异常。对于我统一处理异常的代码逻辑如下,后续可能还需要进行细化,目前大部分场景完全够用了。

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 获取请求参数校验错误信息
     *
     * @return 请求参数校验没有错误返回null
     */
    private List<ValidMessage> getValidMessages(BindingResult bindingResult) {
        if (!bindingResult.hasErrors()) {
            return null;
        }
        List<ValidMessage> validMessageList = new ArrayList<>();
        List<FieldError> validError = bindingResult.getFieldErrors();
        for (FieldError fieldError : validError) {
            validMessageList.add(new ValidMessage(fieldError));
        }
        return validMessageList;
    }

    private List<ValidMessage> getValidMessages(Set<? extends ConstraintViolation> constraintViolations) {
        List<ValidMessage> validMessageList = new ArrayList<>();
        for (ConstraintViolation violation : constraintViolations) {
            ValidMessage validMessage = new ValidMessage();
            validMessage.setEntityName(violation.getRootBeanClass().getSimpleName());
            validMessage.setFiled(violation.getPropertyPath().toString());
            validMessage.setValue(violation.getInvalidValue() == null ? "null" : violation.getInvalidValue().toString());
            validMessage.setErrorMessage(violation.getMessage());
            validMessage.setCode(violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName());
            validMessageList.add(validMessage);
        }
        return validMessageList;
    }

    /**
     * 创建默认的异常信息
     */
    private ErrorResponse newErrorResponse(HttpServletRequest request, HttpServletResponse response, Throwable e) {
        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setPath(request.getRequestURI());
        errorResponse.setException(e.getClass().getName());
        errorResponse.setError(e.getMessage());
        errorResponse.setMessage("服务器内部错误");
        errorResponse.setStatus(response.getStatus());
        errorResponse.setTimestamp(new Date());
        return errorResponse;
    }

    /**
     * 数据主键重复
     */
    @ResponseBody
    @ExceptionHandler(value = DuplicateKeyException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, DuplicateKeyException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setMessage("保存数据失败,数据已经存在");
        return errorResponse;
    }

    /**
     * 解析Excel文件异常
     */
    @ResponseBody
    @ExceptionHandler(value = ExcelAnalysisException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, ExcelAnalysisException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setMessage(e.getCause().getMessage());
        return errorResponse;
    }

    /**
     * 文件上传大小超过配置的最大值
     */
    @ResponseBody
    @ExceptionHandler(value = MaxUploadSizeExceededException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, MaxUploadSizeExceededException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setMessage("上传文件大小超限");
        return errorResponse;
    }

    /**
     * 数据校验异常
     */
    @ResponseBody
    @ExceptionHandler(value = ConstraintViolationException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, ConstraintViolationException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setValidMessageList(getValidMessages(e.getConstraintViolations()));
        errorResponse.setMessage("请求参数校验失败");
        return errorResponse;
    }

    /**
     * 数据校验异常
     */
    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, MethodArgumentNotValidException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setValidMessageList(getValidMessages(e.getBindingResult()));
        errorResponse.setMessage("请求参数校验失败");
        return errorResponse;
    }

    /**
     * 数据校验异常
     */
    @ResponseBody
    @ExceptionHandler(value = BindException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, BindException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setValidMessageList(getValidMessages(e.getBindingResult()));
        errorResponse.setMessage("请求参数校验失败");
        return errorResponse;
    }

    /**
     * 请求参数转换异常
     */
    @ResponseBody
    @ExceptionHandler(value = HttpMessageConversionException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, HttpMessageConversionException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setMessage("请求参数转换异常");
        return errorResponse;
    }

    /**
     * 请求参数校验异常
     */
    @ResponseBody
    @ExceptionHandler(value = ValidationException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, ValidationException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setMessage("请求参数校验异常");
        return errorResponse;
    }

    /**
     * 业务异常处理方法<br/>
     */
    @ResponseBody
    @ExceptionHandler(value = BusinessException.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, BusinessException e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        if (e.getStatus() != null) {
            response.setStatus(e.getStatus());
        } else {
            response.setStatus(HttpStatus.BAD_REQUEST.value());
        }
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setError("业务异常");
        errorResponse.setMessage(e.getMessage());
        return errorResponse;
    }

    /**
     * 默认的异常处理方法<br/>
     */
    @ResponseBody
    @ExceptionHandler(value = Throwable.class)
    protected ErrorResponse defaultErrorHandler(HttpServletRequest request, HttpServletResponse response, Throwable e) {
        log.debug("[ExceptionHandler]-全局的异常处理  ", e);
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        ErrorResponse errorResponse = newErrorResponse(request, response, e);
        errorResponse.setMessage("服务器内部错误");
        return errorResponse;
    }
}
文档更新时间: 2019-08-15 14:25   作者:lizw