SpringBoot教程(九) | SpringBoot统一异常处理

SpringBoot教程(九) | SpringBoot统一异常处理

异常大家应该都很清楚,我们的项目总是不可避免的出现异常,那么应该如何优雅的进行异常处理使我们需要关注的一个问题,合理的异常封装既可以方便前端的处理,也能够简化后端的开发。

一般情况下,我们应该在我们的项目中,根据不同的异常场景,定义不同的异常类型,然后不同的异常类型,返回不同的状态码,然后和前端约定好,根据不同的状态码,做不同的展现。

SpringBoot中为我们提供一个统一的异常处理类,也是利用了AOP的思想,我们可以向外抛出各种类型的异常,然后在这个统一的处理类中,针对每一种不同类型的异常,做不同的数据封装,返回给前端。

代码编写:主要就是通过一个 @ControolerAdvice注解,实现对所有请求的拦截,很像AOP。

(注意,下面的代码仅供展示,如果大家直接粘贴,可能需要引入一些三方jar包才行)

java复制代码@RestControllerAdvice
@Order(1)
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    public GlobalExceptionHandler() {
    }

    @ExceptionHandler({ParamException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class, BindException.class, HttpMessageNotReadableException.class, MissingServletRequestPartException.class, MissingServletRequestParameterException.class, MultipartException.class})
    public Result<?> paramsExceptionHandler(HttpServletRequest request, Exception e) {
        String msg;
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException ex = (MethodArgumentNotValidException)e;
            msg = this.handlerErrors(ex.getBindingResult());
        } else if (e instanceof BindException) {
            BindException ex = (BindException)e;
            msg = this.handlerErrors(ex.getBindingResult());
        } else if (e instanceof ConstraintViolationException) {
            ConstraintViolationException ex = (ConstraintViolationException)e;
            Optional<ConstraintViolation<?>> first = ex.getConstraintViolations().stream().findFirst();
            msg = (String)first.map(ConstraintViolation::getMessage).get();
        } else {
            msg = e.getMessage();
        }

        Result<?> result = Result.error(ResultCode.PARAM_ERROR.getCode(), msg);
        return this.printLogAndReturn(request, result, e);
    }

    private String handlerErrors(BindingResult bindingResult) {
        List<FieldError> errors = bindingResult.getFieldErrors();
        FieldError error = (FieldError)errors.get(0);
        return error.getDefaultMessage();
    }

    @ExceptionHandler({BizException.class})
    public Result<?> bizExceptionHandler(HttpServletRequest request, BizException e) {
        Result<?> result = Result.error(e.getCode() == null ? ResultCode.BIZ_ERROR.getCode() : e.getCode(), e.getMessage());
        return this.printLogAndReturn(request, result, e);
    }

    @ExceptionHandler({HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class})
    public Result<?> httpRequestMethodNotSupportedExceptionHandler(HttpServletRequest request, Exception e) {
        Result<?> result = Result.error(ResultCode.REQ_MODE_NOT_SUPPORTED);
        return this.printLogAndReturn(request, result, e);
    }

    @ExceptionHandler({JSONException.class})
    public Result<?> jsonExceptionHandler(HttpServletRequest request, Exception e) {
        Result<?> result = Result.error(ResultCode.JSON_FORMAT_ERROR);
        return this.printLogAndReturn(request, result, e);
    }

    @ExceptionHandler({DataAccessException.class})
    public Result<?> sqlExceptionHandler(HttpServletRequest request, Exception e) {
        Result<?> result = Result.error(ResultCode.SQL_ERROR);
        return this.printLogAndReturn(request, result, e);
    }

    @ExceptionHandler({Exception.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> exceptionHandler(HttpServletRequest request, Exception e) {
        Result<?> result = Result.error(ResultCode.SYS_ERROR);
        return this.printLogAndReturn(request, result, e);
    }

    private Result<?> printLogAndReturn(HttpServletRequest request, Result<?> result, Exception e) {
        String requestUrl = request.getRequestURL().toString() + (StringUtil.isEmpty(request.getQueryString()) ? "" : "?" + request.getQueryString());
        log.error("<-异常返回-> 请求接口:{} | 异常时间:{} | 异常结果:{}", new Object[]{requestUrl, System.currentTimeMillis(), JSON.toJSONString(result)});
        log.error("<--异常堆栈信息-->");
        log.error(Throwables.getStackTraceAsString(e));
        return result;
    }
}

@ExceptionHandler 标识对哪种异常进行拦截。这里可以有我们自己定义的异常。当我们在业务代码中有一些异常处理的时候,我们可以根据具体的业务场景,将其抛出为我们自己定义的异常,然后在统一的异常处理类中,根据不同的异常类型,返回我们统一封装的结果。

大家可以看看上面的代码,对于所有的错误都封装成了Result对象,并且打印了异常的信息。

好的,接下来我们来写一个案例。首先把前面的统一结果的封装加入到项目中

  1. 在exception 自定义一个业务异常类
java复制代码public class BizException extends RuntimeException {
    private Integer code;

    public BizException() {
    }

    public BizException(String message) {
        super(message);
    }

    public BizException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public BizException(ResultCode resultCode) {
        super(resultCode.getMsg());
        this.code = resultCode.getCode();
    }

    public BizException(String message, Throwable cause) {
        super(message, cause);
    }

    public BizException(int code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public BizException(ResultCode resultCode, Throwable cause) {
        super(resultCode.getMsg(), cause);
        this.code = resultCode.getCode();
    }

    public Integer getCode() {
        return this.code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}
  1. 然后在把刚才的异常处理类也加到exception包下。
java复制代码@RestControllerAdvice
@Order(1)
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    public GlobalExceptionHandler() {
    }


    private String handlerErrors(BindingResult bindingResult) {
        List<FieldError> errors = bindingResult.getFieldErrors();
        FieldError error = (FieldError)errors.get(0);
        return error.getDefaultMessage();
    }

    @ExceptionHandler({BizException.class})
    public Result<?> bizExceptionHandler(HttpServletRequest request, BizException e) {
        Result<?> result = Result.error(e.getCode() == null ? ResultCode.BIZ_ERROR.getCode() : e.getCode(), e.getMessage());
        return this.printLogAndReturn(request, result, e);
    }

    @ExceptionHandler({HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class})
    public Result<?> httpRequestMethodNotSupportedExceptionHandler(HttpServletRequest request, Exception e) {
        Result<?> result = Result.error(ResultCode.REQ_MODE_NOT_SUPPORTED);
        return this.printLogAndReturn(request, result, e);
    }


    @ExceptionHandler({Exception.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> exceptionHandler(HttpServletRequest request, Exception e) {
        Result<?> result = Result.error(ResultCode.SYS_ERROR);
        return this.printLogAndReturn(request, result, e);
    }

    private Result<?> printLogAndReturn(HttpServletRequest request, Result<?> result, Exception e) {
        ObjectMapper mapper = new ObjectMapper();

        String requestUrl = request.getRequestURL().toString() + (!StringUtils.hasLength(request.getQueryString()) ? "" : "?" + request.getQueryString());
        try {
            log.error("<-异常返回-> 请求接口:{} | 异常时间:{} | 异常结果:{}", new Object[]{requestUrl, System.currentTimeMillis(), mapper.writeValueAsString(result)});

        } catch (JsonProcessingException jsonProcessingException) {
            jsonProcessingException.printStackTrace();
        }
        log.error("<--异常堆栈信息-->");
        StringWriter stringWriter = new StringWriter();
        e.printStackTrace(new PrintWriter(stringWriter));
        log.error(stringWriter.toString());
        return result;
    }
}

接下来我们就可以在程序中使用。

  1. 开发一个Controller进行测试,直接抛出异常
java复制代码@RestController
public class ThirdExceptionController {

    @GetMapping("exception")
    public User second(){
        System.out.println(1);
        throw new BizException(ResultCode.BIZ_ERROR.getCode(), "用户名密码错误");
    }


}

记得传token,因为有我们的拦截器。

SpringBoot教程(九) | SpringBoot统一异常处理_第1张图片

异常成功按照我们想要的格式返回了。当然我们可以在抛出异常的时候,自己的定义我们的code和message, 其实还可以和Assert联合使用,让代码更加的优雅。

我们同时修改一下我们之前的拦截器,之前的拦截器在拦截token的时候,如果没传token就直接返回false这种方式不是很友好,因为在浏览器上看到就是一个空白页。http请求不会继续执行,我们可以在这里不返会false,而是直接封装一个我们自己定义的异常。

java复制代码@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 核心业务逻辑,判断是否登录等
        String token = request.getHeader("token");
        // 正常token是的登录后签发的,前端携带过来
        if(!StringUtils.hasLength(token)){
            throw new BizException(9001, "token不能为空");
        }
        return true;
    }

浏览器验证效果:

SpringBoot教程(九) | SpringBoot统一异常处理_第2张图片 好了关于异常的处理我们就讲解到这里,希望对你有帮助。

另: 配套项目代码已托管中gitCode: gitcode.net/lsqingfeng/…

你可能感兴趣的:(spring,boot,状态模式,后端)