SpringBoot项目对返回值和异常的统一处理

205 阅读5分钟

本文主要总结了在web开发中、在进行http请求时、对结果返回值的统一封装以及常见的一些异常,如参数缺失、参数校验失败、方法类型不匹配等异常的统一处理。配合Spring-Validated的入门级使用更加。

  • Spring boot 返回值+异常统一处理
  • 参数验证常见的一些异常

统一返回值处理

在Java开发中,我们常常需要对返回结果进行统一处理、在没有进行处理时、我们一般会定义一个泛型类,如:

public class ResultData <T>{
    private String code;
    private String msg;

    private T data;

    public static <T> ResultData<T> success(){
        return new ResultData<T>("200","success",null);
    }
    public static <T> ResultData<T> success(T data){
        return new ResultData<T>("200","success",data);
    }

    public static <T> ResultData<T> fail(String code,String msg){
        return new ResultData<T>(code,msg,null);
    }
}

然后在Controller中通过如下的方式使用:

public ResultData<User> getDataDetail(@Validated @RequestBody RequestArgs requestArgs){
      User result = userService.loadData(requestArgs)
      return result ==  null ?ResultData.fail("500","服务异常")
     :ResultData.success(result);
}

基本上在每一个handler方法中都需要调用ResultData.success(result)这一行代码,为了不重复编码、可以通过实现ResponseBodyAdvice的方式去实现这一个功能。

@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if(o instanceof String){
            return objectMapper.writeValueAsString(ResultData.success(o));
        }
      	if(o instanceof ResultData ){
          return o;
        }
      	if(o instanceof PageResultData ){
          return  o;
        }
    }
}
  • ResponseBodyAdvice 是 Spring Framework 中的一个接口,允许我们在每个控制器方法的响应体被发送回客户端之前,对其进行修改或处理。
  • supports(): 判断是否需要对响应体进行处理。
  • beforeBodyWrite(): 在响应体写出之前对其进行处理。

如果是String类型的、需要json化、否则会发生重定向。如果是我们定义的ResultData或者PageResultData,则直接返回。

统一异常处理

当服务发生异常时、我们常见的是在业务代码里面进行try catch处理,Spring提供了@RestControllerAdvice 注解,该注解通常用于全局处理控制器抛出的异常,并将这些异常转换为 HTTP 响应。它是 @ControllerAdvice 注解的扩展。使用方式如下:

package com.validate.tutorial.global;

/**
 * 创建时间: 2024年09月13日 17:04
 * 文件描述:
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 功能: 捕获 404 异常
     * @param e
     * @return Object
     */
    @ExceptionHandler(value = NoHandlerFoundException.class)
    public Object handlerNoHandleFoundException(NoHandlerFoundException e) {
        log.error("请求路径不存在 {}", e.getHttpMethod());
        return ResultData.fail("404", "请求路径不存在");
    }


    /**
     * 功能: 缺少参数异常
     * @param e
     * @return Object
     */
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public Object handlerMissingServletRequestParameterException(MissingServletRequestParameterException e) {
        log.error("缺少参数 {}", e.getParameterName());
        return ResultData.fail("400", "缺少参数 ["+e.getParameterName()+"]");
    }

    /**
     * 功能: 请求方法不匹配抛出的异常
     *
     * @param e
     * @return Object
     */
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Object handlerHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        log.error("不支持的请求方式 {}", e.getMessage());
        return ResultData.fail("400", "不支持的请求方式");
    }

    /**
     * 功能: 请求参数解析异常 @RequestBody 抛出
     *
     * @param e
     * @return Object
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public Object handlerHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.error("请求参数解析异常 {}", e.getMessage());
        return ResultData.fail("400", "请求参数解析异常");
    }
    /**
     * 功能: 参数类型不匹配异常
     *
     * @param e
     * @return Object
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public Object handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        log.error("参数类型不匹配异常", e.getMessage());
        return ResultData.fail("400", "参数类型不匹配异常");
    }


    /**
     * 功能: 参数绑定异常 @Valid 单独使用抛出 如:(@Valid RequestArgs requestArgs) RequestArgs使用了@NotNull @Max @Min等注解
     * @Valid+@RequestBody也会进入此逻辑,如参数缺失 校验失败等
     * @Validate 是对 @Valid 的封装
     * @date 2024/9/15 17:09
     * @param e
     * @return Object
     */
    @ExceptionHandler(BindException.class)
    public Object handleBindException(BindException e) {
        log.error("BindException {}", e.getMessage());
        return ResultData.fail("400", "BindException");
    }



    /**
     * 功能: 参数校验异常 @Valid + @RequestBody 参数校验失败会进入此异常,如果同时写了BindException 会进入这个异常
     *
     * @date 2024/9/15 17:09
     * @param e
     * @return Object
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object handlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("MethodArgumentNotValidException {}", e.getMessage());
        return ResultData.fail("400", "参数校验异常");
    }
    /**
     * 功能: singleParameter2(@NotBlank String username, @NotNull Integer userId) 需要在类上 @Validate
     * 此时参数校验失败会进入此异常
     *
     * @param e
     * @return Object
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Object handlerConstraintViolationException(ConstraintViolationException e){
        log.error("ConstraintViolationException {}", e.getMessage());
        return ResultData.fail("400", "参数校验异常");
    }

    @ExceptionHandler(value = Exception.class)
    public Object handlerException(Exception e) {
        log.error("系统异常 {}", e.getMessage());
        return ResultData.fail("500", "系统异常");
    }

}

详细说明如下:

  1. NoHandlerFoundException:这个异常发生的场景是后端没有响应的请求接口时触发。

    @ExceptionHandler(value = NoHandlerFoundException.class)
    public Object handlerNoHandleFoundException(NoHandlerFoundException e) {
        log.error("请求路径不存在 {}", e.getHttpMethod());
        return ResultData.fail("404", "请求路径不存在");
    }
    

需要在统一异常处理时需要在配置文件中添加如下配置,才能被捕获。

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false
  1. MissingServletRequestParameterException: 参数缺失异常,如果有如下接口,参数username或者password没有传递就会触发此异常。
@GetMapping("missing_servlet_request_parameter_exception.do")
public ResultData<String> testMissingServletRequestParameterException(@RequestParam String username,
                                                                    @RequestParam String password) {
  return ResultData.success();
}

@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public Object handlerMissingServletRequestParameterException(MissingServletRequestParameterException e) {
    log.error("缺少参数 {}", e.getParameterName());
    return ResultData.fail("400", "缺少参数 ["+e.getParameterName()+"]");
}
  1. HttpRequestMethodNotSupportedException:当请求方法不匹配时,就会触发此异常,如使用@GetMapping修饰的handler方法使用post方式去访问

    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Object handlerHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        log.error("不支持的请求方式 {}", e.getMessage());
        return ResultData.fail("400", "不支持的请求方式");
    }
    
  2. HttpMessageNotReadableException:当后端使用@RequestBody修饰方法时,前端传参是使用FormData方式时,就会触发这个异常

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public Object handlerHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.error("请求参数解析异常 {}", e.getMessage());
        return ResultData.fail("400", "请求参数解析异常");
    }
    
  3. MethodArgumentTypeMismatchException:当前后端参数类型不匹配时抛出此异常。

    // 如需要一个Integer传递了一个字符串 
    @GetMapping("method_argument_type_mismatch_exception.do")
    public ResultData<String> testMethodArgumentTypeMismatchException(Integer number){
        return ResultData.success("testMethodArgumentTypeMismatchException");
    }
    
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public Object handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        log.error("参数类型不匹配异常", e.getMessage());
        return ResultData.fail("400", "参数类型不匹配异常");
    }
    
  4. BindException、MethodArgumentNotValidException: 参数绑定异常,如使用了validation第三方工具包使用@Validated修饰方法时,方法1就会触发BindException,方法2的参数校验不成功时就会触发MethodArgumentNotValidException

    public class RequestArgs {
    //@NotNull(groups = CostumerValidGroup.Curd.Update.class, message = "userId不能为null")
    //@Null(groups = CostumerValidGroup.Curd.Insert.class)
    private Integer userId;
    
    @NotBlank(message = "名称为必填项")
    private String nickName;
    
    @Email(message = "请填写正确的邮箱地址")
    private String email;
    
    @Length(message = "密码长度应在6~12位之间", min = 6, max = 12)
    @NotBlank
    private String password;
    
    }
    // controller
    @PostMapping("valid_bind_exception1.do")
    public ResultData testValidBindException1(@Validated RequestArgs requestArgs){
        log.info("BindException requestArgs = {}", requestArgs);
        return ResultData.success();
    }
    @PostMapping("valid_bind_exception2.do")
    public ResultData testValidBindException2(@Validated @RequestBody RequestArgs requestArgs){
        log.info("testValidBindException requestArgs = {}", requestArgs);
        return ResultData.success();
    }
    
    // globalHandler.java
    @ExceptionHandler(BindException.class)
    public Object handleBindException(BindException e) {
        log.error("BindException {}", e.getMessage());
        return ResultData.fail("400", "BindException");
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object handlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("MethodArgumentNotValidException {}", e.getMessage());
        return ResultData.fail("400", "参数校验异常");
    }
    
  5. ConstraintViolationException: 方法参数如(@NotBlank String username, @NotNull Integer userId) 需要在类上 @Validated此时参数校验失败会进入此异常

    @GetMapping("single_param2.do")
    public void singleParameter2(@NotBlank String username, @NotNull Integer userId) {
        log.info("singleParameter2 username = {},userId = {}", username, userId);
    }
    // 如username:""就会触发此异常。
    @ExceptionHandler(ConstraintViolationException.class)
    public Object handlerConstraintViolationException(ConstraintViolationException e){
        log.error("ConstraintViolationException {}", e.getMessage());
        return ResultData.fail("400", "参数校验异常");
    }