Spring Validation 参数校验的使用
Valid 和 Validated 的区别
Valid | Validated | |
---|---|---|
提供者 | JSR-303 规范 | Spring |
是否支持分组 | 不支持 | 支持 |
标注位置 | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE, 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 | |
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 技术交流群。
上一篇: 1.2. Java 面向对象
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论