请求参数校验

快速开始

从SpringBoot 2.3开始,校验包被独立成了一个starter组件(参见:validation-starter-no-longer-included-in-web-starters),所以需要引入如下依赖:

<!--校验组件-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--web组件-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

参数校验非常简单,首先在待校验字段上增加校验规则注解

public class UserVO {
    @NotNull(message = "age 不能为空")
    private Integer age;
}

然后在controller方法中添加@Validated和用于接收错误信息的BindingResult就可以了,于是有了第一版:

public String add1(@Validated UserVO userVO, BindingResult result) {
    List<FieldError> fieldErrors = result.getFieldErrors();
    if(!fieldErrors.isEmpty()){
        return fieldErrors.get(0).getDefaultMessage();
    }
    return "OK";
}

Validation 内置注解

注解
说明

@Null

被注解的元素必须是null

@NotNull

被注解的元素不能为null,可以为空字符串

@NotBlank

只能用于String上,不能为null,而且调用trim(),长度必须大于0 倍注解的String非空

@NotEmpty

不能为null,字符串长度不能为0,集合长度不能为0

@Valid和@Validated区别

区别
@Valid
@Validated

提供者

JSR-303规范

Spring

是否支持分组

不支持

支持

标注位置

METHOD,FIELD,CONSTRUCTOR,TYPE_USE

TYPE,METHOD,PARAMETER

嵌套校验

支持

不支持

Spring Validation 默认会校验完所有字段,然后才抛出异常,可以通过一些简单的配置,开启Fail Fast模式,一旦校验失败就立即返回。

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 快速失败模式
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

自定义校验

Spring Validation提供的默认注解并不能实际开发中的需要,这时我们可以自定义校验来满足我们的需求。

例如,校验ipv4地址是否合法。

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {

    // 默认错误消息
    String message() default "ipv4 格式错误";

    // 分组
    Class<?>[] groups() default {};

    // 负载
    Class<? extends Payload>[] payload() default {};
}

这样我们就可以使用@Ipv4进行参数校验了!

分组校验功能

一般我们一个模块会实现增删改查等多个功能,每个功能接口所需要检验的DTO也不太一样,我们可能选择定义多个DTO,然后对每个DTO添加不同的校验规则。但是在某些情况下两个不同的接口会复用相同的DTO,例如用户的新增和修改,参数大致相同,可复用相同的DTO,但是我们需要对新增和修改的参数校验进行区分。为此,Validation为我们提供了一个分组检验的功能。

  1. 定义一个校验分组类

public class ValidationGroups {
  public interface Insert{}
  public interface Update{}
}
@Data
public class AddCourseDto {

 @NotEmpty(message = "新增用户名不能为空", groups = {ValidationGroups.Insert.class})
 @NotEmpty(message = "修改用户名不能为空", groups = {ValidationGroups.Update.class})
 private String name;

 @NotEmpty(message = "新增用户名手机不能为空", groups = {ValidationGroups.Insert.class})
 private String mobile;
}

name属性上定义了两个分组,会在controller指定分组的时候走到不同的校验规则上,提示的信息也会不一样,并且只有在新增分组时,mobile属性才不能为空。

  1. controller中指定校验分组

@RequiredArgsConstructor
@RestController
public class CourseBaseInfoController {
    private final UserService userService;

    @PostMapping("/user")
    public CourseBaseInfoDto createUser(@RequestBody @Validated(value = ValidationGroups.Insert.class) AddUserDto addUserDto) {
        return userService.createUser(1, addUserDto);
    }
    
    @PatchMapping("/user")
    public CourseBaseInfoDto updateUser(@RequestBody @Validated(value = ValidationGroups.Update.class) AddUserDto addUserDto) {
        return userService.createUser(1, addUserDto);
    }
}

校验异常统一处理

MethodArgumentTypeMismatchException

在使用普通行参接收时,当请求传递的请求参数类型不能被接收参数的类型正确转换时,会抛出MethodArgumentTypeMismatchException异常

@RequestMapping("/api/user")
@Validated
public class UserController {
    /**
     * ✅ /api/user/query?age=1
     * ❎ /api/user/query?age=abc
     */
    @GetMapping("/query")
    public ResponseEntity<Void> test(Integer age) {
        return ResponseEntity.ok().build();
    }
}

异常捕获:

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResultData<?> parameterExceptionHandler(MethodArgumentTypeMismatchException exception, HttpServletRequest request) {
    log.error("请求参数类型转换失败! {} => {}", request.getRequestURI(), exception.getMessage());
    LinkedHashMap<String, Object> map = new LinkedHashMap<>();
    map.put("filed", exception.getName());
    String[] split = exception.getMessage().split(": ");
    if (split.length > 1) {
        map.put("value", split[1].substring(1, split[1].length() - 1));
    }
    return ResultData.fail(HttpStatus.BAD_REQUEST.value(), "参数不合法", map);
}

MissingServletRequestParameterException

在使用@RequestParam注解接收参数时,当请求缺失请求参数时,会抛出MissingServletRequestParameterException异常

/**
 * ✅ /api/user/query?age=1
 * ❎ /api/user/query
 */
@GetMapping("/code")
public ResponseEntity<Void> test(@RequestParam("age") String age) {
    return ResponseEntity.ok().build();
}

异常捕获

@ExceptionHandler(MissingServletRequestParameterException.class)
public ResultData<?> parameterMissingExceptionHandler(MissingServletRequestParameterException exception, HttpServletRequest request) {
    log.error("请求参数绑定异常! {} => {}", request.getRequestURI(), exception.getMessage());
    LinkedHashMap<String, Object> map = new LinkedHashMap<>();
    map.put("filed", exception.getParameterName());
    map.put("required", exception.getParameterType());
    return ResultData.fail(HttpStatus.BAD_REQUEST.value(), "参数不合法", map);
}

ConstraintViolationException

在对普通行参进行Validation验证时,在请求中,缺失请求参数时,会抛出ConstraintViolationException异常。要在类上添加@Validated注解才能生效。

/**
 * ✅ /api/user/query?age=1
 * ❎ /api/user/query
 */
@RequestMapping("/api/user")
@Validated
public class UserController {
    @GetMapping("/query")
    public ResponseEntity<Void> test(@NotNull age) {
        return ResponseEntity.ok().build();
    }
}

异常捕获

@ExceptionHandler(MissingServletRequestParameterException.class)
public ResultData<?> parameterMissingExceptionHandler(MissingServletRequestParameterException exception, HttpServletRequest request) {
    log.error("请求参数绑定异常! {} => {}", request.getRequestURI(), exception.getMessage());
    LinkedHashMap<String, Object> map = new LinkedHashMap<>();
    map.put("filed", exception.getParameterName());
    map.put("required", exception.getParameterType());
    return ResultData.fail(HttpStatus.BAD_REQUEST.value(), "参数不合法", map);
}

MethodArgumentNotValidException

在使用JavaBean接收参数时,当请求参数校验失败时,会抛出MethodArgumentNotValidException异常

/**
 * ✅ /api/user/query?age=1
 * ❎ /api/user/query
 */
@RequestMapping("/api/user")
@Validated
public class UserController {
    @GetMapping("/query")
    public ResponseEntity<Void> test(UserDto userDto) {
        return ResponseEntity.ok().build();
    }
}

异常捕获

public ResultData<?> parameterExceptionHandler(MethodArgumentNotValidException exception, HttpServletRequest request) {
    log.error("请求参数校验失败! {} => {}", request.getRequestURI(), exception.getMessage());
    // 获取异常信息
    BindingResult exceptions = exception.getBindingResult();
    // 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
    List<Object> list = new ArrayList<>();
    if (exceptions.hasErrors()) {
        List<FieldError> fieldErrors = exceptions.getFieldErrors();
        if (!fieldErrors.isEmpty()) {
            fieldErrors.forEach(error -> {
                String field = error.getField();
                String message = error.getDefaultMessage();
                if (message != null) {
                    list.add(Map.of("filed", field, "message", message));
                }
            });
            return ResultData.fail(HttpStatus.BAD_REQUEST.value(), "参数不合法", list);
        }
    }
    return ResultData.fail(HttpStatus.BAD_REQUEST.value(), "参数不合法", list);
}

HttpMessageNotReadableException

请求体无法被正确解析

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResultData<?> processException(HttpMessageNotReadableException exception, HttpServletRequest request) {
    log.error("请求体无法被正确解析! {}, {}", request.getRequestURI(), exception.getMessage());
    return ResultData.fail(HttpStatus.BAD_REQUEST.value(), "参数不合法");
}

UnexpectedTypeException

注解使用错,例如:将@NotBlank注解放在任何非字符串类型的字段中。

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(UnexpectedTypeException.class)
public ResultData<?> parameterExceptionHandler(UnexpectedTypeException exception, HttpServletRequest request) {
    log.error("注解使用错误! {} => {}", request.getRequestURI(), exception.getMessage());
    return ResultData.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务端错误");
}
BadRequestException.java

最后更新于

这有帮助吗?