Spring Validation 参数校验的使用

发布于 2024-10-04 07:25:13 字数 12284 浏览 4 评论 0

Valid 和 Validated 的区别

 ValidValidated
提供者JSR-303 规范Spring
是否支持分组不支持支持
标注位置METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USETYPE, METHOD, PARAMETER
嵌套校验支持不支持

引入依赖

如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。
如果 spring-boot 版本大于 2.3.x,则需要手动引入依赖:
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.0.1.Final</version>
</dependency>

预定义对象的说明

接口统一返回 ReturnResult 定义

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * Return Result
 *
 */
@Data
@Accessors(chain = true)
public class ReturnResult<T> {
  private int code;
  private String message;
  private T data;

  public boolean ok() {
    return this.code == 0;
  }

  public static <T> Result<T> success() {
    return new Result<T>().setCode(0).setMessage("SUCCESS");
  }

  public static <T> Result<T> success(T data) {
    return new Result<T>().setCode(0).setMessage("SUCCESS").setData(data);
  }

  public static <T> Result<T> failure() {
    return new Result<T>().setCode(-1).setMessage("FAILURE");
  }

  public static <T> Result<T> failure(int code, String msg) {
    return new Result<T>().setCode(code).setMessage(msg);
  }

  public static <T> Result<T> failure(int code, String msg, T data) {
    return new Result<T>().setCode(-1).setMessage("FAILURE").setData(data);
  }
}

ErrorCode

/**
 * Error Code
 *
 */
public final class ErrorCode {
  /**
   * Normal
   */
  public static final int NORMAL = 200;
  /**
   * Request error
   */
  public static final int REQUEST_ERROR = 400;
  /**
   * Server refuse request
   */
  public static final int SERVER_REFUSE_REQUEST = 403;
  /**
   * Server internal error
   */
  public static final int SERVER_INTERNAL_ERROR = 500;
  /**
   * Argument valid failure
   */
  public static final int ARGUMENT_VALID_FAILURE = 1001;

}

常用参数校验

限制说明
@Null限制只能为 null
@NotNull 
@AssertFalse 
@AssertTrue 
@DecimalMax(value) 
@DecimalMin(value) 
@Digits(integer,fraction)限制必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction
@Future 
@Max(value) 
@Min(value) 
@Pattern(value)必须符合指定的正则表达式
@Size(max,min) 
@NotEmpty 
@NotBlank 
@Email 

RequestBody 校验

/**
 * RequestBody 参数校验
 * 校验失败会抛出 MethodArgumentNotValidException 异常
 *
 */
@RequestMapping("/api/user")
@RestController
public class UserController {

  /**
   * RequestBody 参数校验
   * 使用 @Valid 和 @Validated 都可以
   */
  @PostMapping("/save/1")
  public ReturnResult saveUser(@RequestBody @Validated UserDTO userDTO) {
    return ReturnResult.success();
  }

  @PostMapping("/save/2")
  public ReturnResult save2User(@RequestBody @Valid UserDTO userDTO) {
    return ReturnResult.success();
  }
}

RequestParam / PathVariable 校验

/**
 * RequestMapping / PathVariable 参数校验
 * 校验失败会抛出 ConstraintViolationException 异常
 * 
 * 此时必须在 Controller 上标注 @Validated 注解,并在入参上声明约束注解
 */
@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
  /**
   * 路径变量
   * 添加约束注解 @Min
   */
  @GetMapping("{userId}")
  public ReturnResult detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
    // 校验通过,才会执行业务逻辑处理
  }

  /**
   * 查询参数
   * 添加约束注解 @Length @NotNull
   */
  @GetMapping("getByAccount")
  public ReturnResult getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
    // 校验通过,才会执行业务逻辑处理
  }
}

全局异常处理
在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。

/**
 * 统一异常处理
 *
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
  /**
   * 参数校验错误的异常处理
   */
  @ExceptionHandler({MethodArgumentNotValidException.class})
  @ResponseStatus(HttpStatus.OK)
  @ResponseBody
  public Result 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();
    return ReturnResult.failure(ErrorCode.ARGUMENT_VALID_FAILURE, msg);
  }

  @ExceptionHandler({ConstraintViolationException.class})
  @ResponseStatus(HttpStatus.OK)
  @ResponseBody
  public Result handleConstraintViolationException(ConstraintViolationException ex) {
    return ReturnResult.failure(ErrorCode.ARGUMENT_VALID_FAILURE, ex.getMessage());
  }

  /**
   * 未知异常处理
   * @param e Exception
   * @return
   */
  @ExceptionHandler(Exception.class)
  @ResponseBody
  public ResponseEntity handlerException(Exception e){
    log.error(e.getMessage(),e);
    StringBuffer errorMsg = new StringBuffer();
    errorMsg.append(e.getMessage());
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.setContentType(MediaType.APPLICATION_JSON);
    ResponseEntity<ReturnData> returnDataResponseEntity = new ResponseEntity<>(new ReturnData(ReturnData.FAIL_CODE, errorMsg.toString(), null, null), httpHeaders, HttpStatus.OK);
    return returnDataResponseEntity;
  }
}

分组校验
为了区分业务场景,对于不同场景下的数据验证规则可能不一样(例如新增时可以不用传递 ID,而修改时必须传递 ID),可以使用分组校验。

/**
 * 分组校验
 *
 */
@Data
public class UserGroupValidDTO {

  @NotNull(groups = Update.class)
  @Min(value = 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 {
  }
}

Controller 实现:

/**
 * 分组校验
 *
 */
@RestController
@RequestMapping("/api/user_group_valid")
public class UserGroupValidController {

  @PostMapping("/save")
  public Result saveUser(@RequestBody @Validated(UserGroupValidDTO.Save.class) UserGroupValidDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.success();
  }

  @PostMapping("/update")
  public Result updateUser(@RequestBody @Validated(UserGroupValidDTO.Update.class) UserGroupValidDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.success();
  }
}

嵌套校验
上面的校验主要是针对基本类型进行了校验,如果 DTO 中包含了自定义的实体类,就需要用到嵌套校验。

/**
 * 嵌套校验
 * DTO 中的某个字段也是一个对象,这种情况下,可以使用嵌套校验
 *
 */
@Data
public class UserNestedValidDTO {
  @Min(value = 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;

  /**
   * 此时 DTO 类的对应字段必须标记 @Valid 注解
   */
  @Valid
  @NotNull(groups = {Save.class, Update.class})
  private Job job;

  @Data
  public static class Job {

    @NotNull(groups = {Update.class})
    @Min(value = 1, groups = Update.class)
    private Long jobId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String jobName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String position;
  }

  /**
   * 保存的时候校验分组
   */
  public interface Save {
  }

  /**
   * 更新的时候校验分组
   */
  public interface Update {
  }
}

Controller 实现:

/**
 * 嵌套校验
 *
 */
@RestController
@RequestMapping("/api/user_nested_valid")
public class UserNestedValidController {

  @PostMapping("/save")
  public Result saveUser(@RequestBody @Validated(UserNestedValidDTO.Save.class) UserNestedValidDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.success();
  }

  @PostMapping("/update")
  public Result updateUser(@RequestBody @Validated(UserNestedValidDTO.Update.class) UserNestedValidDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.success();
  }
}

集合校验
如果请求体直接传递了 json 数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用 java.util.Collection 下的 list 或者 set 来接收数据,参数校验并不会生效(单个数组可以使用)!我们可以使用自定义 list 集合来接收参数:

/**
 * 包装 List 类型,并声明 @Valid 注解
 * @param <E>
 */
@Data
public class ValidationList<E> implements List<E> {

  @Delegate // @Delegate 是 lombok 注解
  @Valid // 一定要加 @Valid 注解
  public List<E> list = new ArrayList<>();

  // 一定要记得重写 toString 方法
  @Override
  public String toString() {
    return list.toString();
  }
}

Controller

/**
 * 集合校验
 *
 */
@RestController
@RequestMapping("/api/valid_list")
public class ValidListController {

  @PostMapping("/saveList")
  public Result saveList(@RequestBody @Validated(UserGroupValidDTO.Save.class) ValidationList<UserGroupValidDTO> userList) {
    // 校验通过,才会执行业务逻辑处理
    return Result.success();
  }
}

编程式校验
上面都是通过注解来进行校验,也可以使用编程的方式进行校验:

/**
 * 编程式校验参数
 *
 */
@RequestMapping("/api/valid_with_code")
@RestController
public class ValidWithCodeController {
  @Autowired
  private javax.validation.Validator globalValidator;

  /**
   * 编程式校验
   */
  @PostMapping("/saveWithCodingValidate")
  public Result saveWithCodingValidate(@RequestBody UserGroupValidDTO userGroupValidDTO) {
    Set<ConstraintViolation<UserGroupValidDTO>> validate = globalValidator.validate(userGroupValidDTO, UserGroupValidDTO.Save.class);
    // 如果校验通过,validate 为空;否则,validate 包含未校验通过项
    if (validate.isEmpty()) {
      // 校验通过,才会执行业务逻辑处理

    } else {
      for (ConstraintViolation<UserGroupValidDTO> userGroupValidDTOConstraintViolation : validate) {
        // 校验失败,做其它逻辑
        System.out.println(userGroupValidDTOConstraintViolation);
        // throw new RuntimeException();
      }
    }
    return Result.success();
  }
}

配置快速失败

/**
 * Web 配置
 *
 * @author zrg
 * @date 2021/5/17 16:11
 */
@Configuration
public class WebConfig {
  /**
   * 校验参数时只要出现校验失败的情况,就立即抛出对应的异常,结束校验,不再进行后续的校验
   *
   * @return
   */
  @Bean
  public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
        .configure()
        .failFast(true)
        .buildValidatorFactory();
    return validatorFactory.getValidator();
  }

  @Bean
  public MethodValidationPostProcessor methodValidationPostProcessor() {
    MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
    methodValidationPostProcessor.setValidator(validator());
    return methodValidationPostProcessor;
  }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

回眸一遍

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

我们的影子

文章 0 评论 0

素年丶

文章 0 评论 0

南笙

文章 0 评论 0

18215568913

文章 0 评论 0

qq_xk7Ean

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文