Springboot参数校验及异常处理

目录

Springboot参数校验及相关异常处理

不同类型参数校验

实体添加校验规则

@Data
public class User {
    @NotNull(message = "name不能为空")
    private String name;
    @Range(min = 0, max = 120, message = "必须在0-120之间")
    private int age;
}

通过query string传递的参数

需要在Controller上加上@Validated注解否则不会触发校验规则

@RestController()
@RequestMapping("/validation")
//用于开启query string传参校验,实体对象检测需要单独开启
@Validated
@Slf4j
public class ValidationTestController {
  @RequestMapping("/param")
    public String testParam(@NotNull(message = "name不能为空") String name, @Range(min = 0, max = 120) int age) throws IOException {
        ...
    }
}

两种form类型的参数

对于multipart/form-data和application/x-www-form-urlencoded类型:

需要在校验实体参数前加上@Validated注解否则不会触发校验规则

@RequestMapping("/formData")
public String testFormData(@Validated User user, @RequestPart("file") MultipartFile file) throws IOException {
  ...
}
@RequestMapping("/formUrlencoded")
public String testFormUrl(@Validated User user) throws IOException {
  ...
}

json类型的参数

@RequestMapping("/json")
public String testJson1(@Validated @RequestBody User user) throws IOException {
  ...
}

参数校验相关异常处理

对于1.1场景校验失败会产生ConstraintViolationException

对于1.2场景校验失败会产生BindException

对于1.3场景校验失败会产生MethodArgumentNotValidException

全局异常处理器参考代码:

/**
 * 对于application/json类型请求需要从body中获取,需要处理流只能能读一次问题
 * 对于非application/json类型请求,可使用request.getParameterMap()获取参数,request.getParts()获取文件
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /***
     * 可能出现的未知异常
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResult<?> handle(Exception e, ServletRequest request) {
        log.error("input:{}", RequestUtils.getInputAsString(request));
        log.error(e.getMessage(), e);
        return ApiResult.error(e.getMessage());
    }

    /***
     * 参数异常:ConstraintViolationException()
     * 用于处理类似url传参[http://xxx?age=30&name=tom]请求中age和name的校验引发的异常
     * @param e
     * @return
     */
    @ExceptionHandler(value = {ConstraintViolationException.class})
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> urlParametersExceptionHandle(ConstraintViolationException e, ServletRequest request) throws JsonProcessingException {
        //可以获取到原始参数
        log.error("input:{}", RequestUtils.getInputAsString(request));
        log.error(e.getMessage(), e);
        //收集所有错误信息,可能有多个不满足条件的字段
        List<String> errorMsg = e.getConstraintViolations()
                .stream().map(s -> s.getMessage()).collect(Collectors.toList());
        return ApiResult.error(BaseResultEnum.INVALID_PARAMETER, errorMsg.toString());
    }

    /***
     * 参数异常: MethodArgumentNotValidException
     * MethodArgumentNotValidException: 用于处理请求参数为实体类时校验引发的异常 Content-Type为application/json
     * @param e
     * @return
     */
    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> jsonExceptionHandle(MethodArgumentNotValidException e, ServletRequest request) throws JsonProcessingException {
        log.error("input:{}", RequestUtils.getInputAsString(request));
        log.error(e.getMessage(), e);
        BindingResult bindingResult = e.getBindingResult();
        //收集所有错误信息
        List<String> errorMsg = bindingResult.getFieldErrors().stream()
                .map(s -> s.getDefaultMessage()).collect(Collectors.toList());
        return ApiResult.error(BaseResultEnum.INVALID_PARAMETER, errorMsg.toString());
    }

    /***
     * 参数异常: BindException
     * BindException: 用于处理请求参数为实体类时校验引发的异常 Content-Type为application/x-www-form-urlencoded或multipart/form-data
     * @param e
     * @return
     */
    @ExceptionHandler(value = {BindException.class})
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResult<?> formExceptionHandle(BindException e, ServletRequest request) throws JsonProcessingException {
        log.error("input:{}", RequestUtils.getInputAsString(request));
        log.error(e.getMessage(), e);
        BindingResult bindingResult = e.getBindingResult();
        //收集所有错误信息
        List<String> errorMsg = bindingResult.getFieldErrors().stream()
                .map(s -> s.getDefaultMessage()).collect(Collectors.toList());
        return ApiResult.error(BaseResultEnum.INVALID_PARAMETER, errorMsg.toString());

    }

    /***
     * 自定义异常:自定义异常一般不要设置为ERROR级别,因为我们用自定义的异常主要是为了辅助我们处理业务逻辑
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(value = {BusinessException.class})
    @ResponseBody
    public ApiResult<?> handleBusinessException(BusinessException e, ServletRequest request) throws JsonProcessingException {
        log.error("input:{}", RequestUtils.getInputAsString(request));
        log.warn(e.getMessage(), e);
        return ApiResult.error(BaseResultEnum.INTERNAL_ERROR, e.getMessage());
    }
}

获取校验异常时原始输入参数参考方法:

public abstract class RequestUtils {
    private static final ObjectMapper objectMapper = new ObjectMapper();

    public static String getParameterMapAsString(ServletRequest request) {
        try {
            return objectMapper.writeValueAsString(request.getParameterMap());
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public static String getBodyAsString(ServletRequest request) {
        if (request instanceof JsonRequestWrapper wrapper) {
            return new String(wrapper.getBodyAsByteArray(), StandardCharsets.UTF_8);
        }
        return "[No Wrapper]";
    }

    public static String getInputAsString(ServletRequest request) {
        if (isJsonRequest(request)) {
            return getBodyAsString(request);
        } else {
            return getParameterMapAsString(request);
        }
    }

    public static boolean isJsonRequest(ServletRequest request) {
        String contentType = request.getContentType();
        return contentType != null && contentType.contains(MediaType.APPLICATION_JSON_VALUE);
    }
}