SpringBoot的参数校验之Spring Validation

前言 Java API规范 (JSR303) 定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。接下来,我们以spring-boot项目为例,介绍Spring Validation的使用。几乎涵盖你需要的SpringBoot所有操作
引入依赖 方式一:

org.hibernate hibernate-validator 6.2.3.Final

方式二:
(本文采用此方式)
org.springframework.boot spring-boot-starter-validation

实战部分 此文需要仍需引入web的依赖
org.springframework.boot spring-boot-starter-web

在web服务里通常会通过以下两种形式进行传参:
  1. POSTPUT请求,使用requestBody传递参数;
  2. GET请求,使用requestParam/PathVariable传递参数。
requestBody参数校验
POST、PUT请求一般会使用requestBody传递参数,这种情况下,后端使用 DTO 对象进行接收。只要给 DTO 对象加上@Validated注解就能实现自动参数校验。比如,有一个保存User的接口,要求userName长度是2-10,account和password字段长度是6-20。如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。
DTO 表示数据传输对象(Data Transfer Object),用于服务器和客户端之间交互传输使用的。在 spring-web 项目中可以表示用于接收请求参数的Bean对象。
  • DTO字段上声明约束注解
    import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; public class UserDTO { private Long userId; @NotNull @Length(min = 2,max = 10) private String userName; @NotNull @Length(min = 6,max = 20) private String account; @NotNull @Length(min = 6,max = 20) private String password; public Long getUserId() { return userId; }public void setUserId(Long userId) { this.userId = userId; }public String getUserName() { return userName; }public void setUserName(String userName) { this.userName = userName; }public String getAccount() { return account; }public void setAccount(String account) { this.account = account; }public String getPassword() { return password; }public void setPassword(String password) { this.password = password; } }

  • 在方法参数上声明校验注解
    这种情况下,使用@Valid和@Validated都可以。
    import java.util.HashMap; import java.util.Map; @RestController public class UserController {@PostMapping("/save") public Map saveUser(@RequestBody @Validated UserDTO userDTO){ Map res = new HashMap(); res.put("code","200"); res.put("message","ok"); return res; } }

    requestParam/PathVariable参数校验
    GET请求一般会使用requestParam/PathVariable传参。如果参数比较多 (比如超过 6 个),还是推荐使用DTO对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解 (如@Min等)。如果校验失败,会抛出ConstraintViolationException异常。代码示例如下:
    @RestController @Validated public class SelectController {@GetMapping("/{userId}") public Map detail(@PathVariable("userId") @Min(1000000000L) Long userId){ Map res = new HashMap(); res.put("code","200"); res.put("message","ok"); return res; }@GetMapping("/getByAccount") public Map getByAccount(@Length(min = 6,max = 20)@NotNull String account){ Map res = new HashMap(); res.put("code","200"); res.put("message","ok"); return res; } }

    统一异常处理
    校验失败会抛出MethodArgumentNotValidException或者ConstraintViolationException异常,我们需要通过统一异常处理来返回一个更友好的报错。
    import org.springframework.http.HttpStatus; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.validation.ConstraintViolationException; import java.util.HashMap; import java.util.Map; @RestControllerAdvice public class CommonExceptionHandler {@ExceptionHandler({MethodArgumentNotValidException.class}) @ResponseStatus(HttpStatus.OK) public Map handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); StringBuilder sb = new StringBuilder("校验失败:"); for (FieldError fieldError : bindingResult.getFieldErrors()) { sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", "); } String msg = sb.toString(); Map err = new HashMap<>(); err.put("code","-200"); err.put("message",msg); return err; }@ExceptionHandler({ConstraintViolationException.class}) @ResponseStatus(HttpStatus.OK) public Map handleConstraintViolationException(ConstraintViolationException ex) { Map err = new HashMap<>(); err.put("code","-200"); err.put("message",ex.getMessage()); return err; }}

    进阶使用分组校验
    在实际项目中,可能多个方法需要使用同一个DTO类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在DTO类的字段上加约束注解无法解决这个问题。因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。还是上面的例子,比如保存User的时候,UserId是可空的,但是更新User的时候,UserId的值必须>=10000000000000000L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:
  • 约束注解上声明适用的分组信息groups
    public class UserDTO {@Min(value = https://www.it610.com/article/10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String account; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String password; public interface Save { }public interface Update { } }

    【SpringBoot的参数校验之Spring Validation】@Validated注解上指定校验分组
    @PostMapping("/save") public Map saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) { Map res = new HashMap(); res.put("code","200"); res.put("message","ok"); return res; }@PostMapping("/update") public Map updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) { Map res = new HashMap(); res.put("code","200"); res.put("message","ok"); return res; }

    推荐阅读