SpringBoot(3-Validator属性校验)
|总字数:3.7k|阅读时长:15分钟|浏览量:|
依赖
Java API 规范(JSR303)定义了 Bean 校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解如 @Email、@Length 等。Spring Validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验
如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于 2.3.x,则需要手动引入依赖:
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
|
常用注解
注解 |
返回值 |
功能 |
@AssertFalse |
Boolean, boolean |
验证注解的元素值是 false |
@AssertTrue |
Boolean, boolean |
验证注解的元素值是 true |
@NotNull |
任意类型 |
验证注解的元素值不是 null |
@Null |
任意类型 |
验证注解的元素值是 null |
@Min(value = 值) |
BigDecimal,BigInteger, byte, short, int, long,等任何 Number 或 CharSequence(存储的是数字)子类型 |
验证注解的元素值大于等于@Min 指定的 value 值 |
@Max(value = 值) |
和@Min 要求一样 |
验证注解的元素值小于等于@Max 指定的 value 值 |
@DecimalMin(value = 值) |
和@Min 要求一样 |
验证注解的元素值大于等于@ DecimalMin 指定的 value 值 |
@DecimalMax(value = 值) |
和@Min 要求一样 |
验证注解的元素值小于等于@ DecimalMax 指定的 value 值 |
@Digits(integer = 整数位数, fraction = 小数位数) |
和@Min 要求一样 |
验证注解的元素值的整数位数和小数位数上限 |
@Size(min = 下限, max = 上限) |
字符串、Collection、Map、数组等 |
验证注解的元素值的在 min 和 max(包含)指定区间之内,如字符长度、集合大小 |
@Past |
java.util.Date, java.util.Calendar; Joda Time 类库的日期类型 |
验证注解的元素值(日期类型)比当前时间早 |
@Future |
与@Past 要求一样 |
验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank |
CharSequence 子类型 |
验证注解的元素值不为空(不为 null、去除首位空格后长度为 0),不同于@NotEmpty,@NotBlank 只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min = 下限, max = 上限) |
CharSequence 子类型 |
验证注解的元素值长度在 min 和 max 区间内 |
@NotEmpty |
CharSequence 子类型、Collection、Map、数组 |
验证注解的元素值不为 null 且不为空(字符串长度不为 0、集合大小不为 0) |
@Range(min = 最小值, max = 最大值) |
BigDecimal, BigInteger, CharSequence, byte, short, int, long 等原子类型和包装类型 |
验证注解的元素值在最小值和最大值之间 |
@Email(regexp = 正则表达式, flag = 标志的模式) |
CharSequence 子类型(如 String) |
验证注解的元素值是 Email,也可以通过 regexp 和 flag 指定自定义的 email 格式 |
@Pattern(regexp = 正则表达式, flag = 标志的模式) |
String,任何 CharSequence 的子类型 |
验证注解的元素值与指定的正则表达式匹配 |
@Valid |
任何非原子类型 |
指定递归验证关联的对象;如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid 注解即可级联验证 |
使用案例
基本使用
- 在请求参数上声明校验注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Getter @Setter public class UserValid implements Serializable { @NotNull(message = "id不能为空") private Long id; @NotNull(message = "date不能为空") @DateTimeFormat(pattern = "yyyy-MM-dd") @Future(message = "只能是将来的日期") private Date date; @NotNull @DecimalMax(value = "10000.0") @DecimalMin(value = "1.0") private Double doubleValue = null; @NotNull @Max(value = 100, message = "最大值") @Min(value = 1, message = "最小值") private Integer integer; @Range(min = 1,max = 100,message = "范围") private Long range; @Email(message = "邮箱格式错误") private String email; @Size(min = 2,max = 10,message = "字符串长度在2-10") private String size; }
|
1 2 3 4 5
| @PostMapping("/save") public Result saveUser(@RequestBody @Validated UserValid userValid) { return Result.ok(); }
|
- RequestParam/PathVariable 参数校验
GET 请求一般会使用 RequestParam/PathVariable 传参。如果参数比较多(比如超过 6 个),还是推荐使用 DTO 对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在 Controller 类上标注 @Validated 注解,并在入参上声明约束注解(如 @Min 等)。如果校验失败,会抛出 ConstraintViolationException 异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @RequestMapping("/api/user") @RestController @Validated public class UserController { @GetMapping("{userId}") public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) { UserDTO userDTO = new UserDTO(); userDTO.setUserId(userId); userDTO.setAccount("11111111111111111"); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); }
@GetMapping("getByAccount") public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) { UserDTO userDTO = new UserDTO(); userDTO.setUserId(10000000000000003L); userDTO.setAccount(account); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); } }
|
多级嵌套
当实体类中字段中包含其他对象时,且该对象是需要校验时,需要在实体类字段中加上@Valid
1 2 3 4 5 6 7 8 9 10 11
| @Data public class Project {
@NotBlank(message = "Project title must be present") @Size(min = 3, max = 20, message = "Project title size not valid") private String title;
@Valid private User owner; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Data public class User {
@NotBlank(message = "User name must be present") @Size(min = 3, max = 50, message = "User name size not valid") private String name;
@NotBlank(message = "User email must be present") @Email(message = "User email format is incorrect") private String email; }
|
分组校验
在很多时候,同一个模型可能会在多处被用到,但每处的校验场景又不一定相同(如:新增用户接口、修改用户接口,参数都是 User 模型,在新增时 User 中 name 字段不能为空,userNo 字段可以为空;在修改时 User 中 name 字段可以为空,userNo 字段不能为空)。
我们可以用 groups 来实现:同一个模型在不同场景下,动态区分校验模型中的不同字段。
1 2 3 4 5
| public interface Add{ }
public interface Edit{ }
|
1 2 3 4 5 6 7 8 9
| public AjaxResult addSave(@Validated(Add.class) @RequestBody Xxxx xxxx){ return success(xxxx); }
public AjaxResult editSave(@Validated(Edit.class) @RequestBody Xxxx xxxx){ return success(xxxx); }
|
1 2 3 4 5 6 7
| @NotNull(message = "不能为空", groups = {Add.class}) private String xxxx;
@NotBlank(message = "不能为空", groups = {Add.class, Edit.class}) private String xxxx;
|
自定义注解校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
@Target({FIELD, PARAMETER}) @Retention(RUNTIME) @Documented
@Constraint(validatedBy ={JustryDengValidator.class}) public @interface ConstraintsJustryDeng { String message() default "JustryDeng : param value must contais specified value!"; String contains() default ""; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
public class JustryDengValidator implements ConstraintValidator<ConstraintsJustryDeng, String> { private String contains;
@Override public void initialize(ConstraintsJustryDeng constraintAnnotation) { System.out.println(constraintAnnotation.message()); this.contains = constraintAnnotation.contains(); }
@Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (value == null) { return false; } if (value instanceof String) { String strMessage = (String) value; return strMessage.contains(contains); } else if (value instanceof Integer) { return contains.contains(String.valueOf(value)); } return false; } }
|
编程式校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Autowired private javax.validation.Validator globalValidator;
@PostMapping("/saveWithCodingValidate") public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) { Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class); if (validate.isEmpty()) { } else { for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) { System.out.println(userDTOConstraintViolation); } } return Result.ok(); }
|
快速失败
Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启 Fali Fast 模式,一旦校验失败就立即返回。
1 2 3 4 5 6 7 8 9
| @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory(); return validatorFactory.getValidator(); }
|
异常信息
异常分类
注解校验不通过时,可能抛出的异常:
- MethodArgumentNotValidException
- ConstraintViolationException
- BindException
异常捕获
MVC 全局捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @ControllerAdvice @ResponseBody public class GlobleExceptionHandler {
@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class}) public ResponseEntity<Result<?>> handleValidatedException(Exception e) { Result<?> resp = null;
if (e instanceof MethodArgumentNotValidException) { MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e; resp = Result.error(500, ex.getBindingResult().getAllErrors().stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining("; ")) ); } else if (e instanceof ConstraintViolationException) { ConstraintViolationException ex = (ConstraintViolationException) e; resp = Result.error(500,ex.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")) ); } else if (e instanceof BindException) { BindException ex = (BindException) e; resp = Result.error(500,ex.getAllErrors().stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining("; ")) ); } return new ResponseEntity<>(resp,HttpStatus.INTERNAL_SERVER_ERROR); } }
|
接口捕获异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @RestController public class IndexController {
@Autowired private MessageSource messageSource;
@RequestMapping("/validator") public String validator(@Validated User user, BindingResult result){ if (result.hasErrors()){ StringBuffer msg=new StringBuffer(); List<FieldError> fieldErrors = result.getFieldErrors(); Locale currentLocale = LocaleContextHolder.getLocale(); for (FieldError fieldError : fieldErrors) { String errorMessage = messageSource.getMessage(fieldError, currentLocale); msg.append(fieldError.getField()+":"+errorMessage+","); } return msg.toString(); } return "验证通过"; } }
|
@Validated 与@Valid 区别
区别 |
@Valid |
@Validated |
提供者 |
JSR-303 规范 |
Spring |
是否支持分组 |
不支持 |
支持 |
标注位置 |
METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE |
TYPE, METHOD, PARAMETER |
嵌套校验 |
支持 |
不支持 |
实现原理
校验触发的时机,其实是从两个点触发,一个跟 SpringMVC 的请求处理过程息息相关,一个是跟 MethodValidationPostProcessor 相关
RequestBody 参数
在 Spring MVC 中,RequestResponseBodyMethodProcessor 是用于解析 @RequestBody 标注的参数以及处理 @ResponseBody 标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法 resolveArgument() 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional(); Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); binder.validate(validationHints); break; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public void validate(Object... validationHints) { Object target = getTarget(); Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); for (Validator validator : getValidators()) { if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) { ((SmartValidator) validator).validate(target, bindingResult, validationHints); } else if (validator != null) { validator.validate(target, bindingResult); } } }
@Override public void validate(Object target, Errors errors, Object... validationHints) { if (this.targetValidator != null) { processConstraintViolations( this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); } }
|
方法级别
上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解的校验方式,就是方法级别的参数校验。实际上,这种方式可用于任何 Spring Bean 的方法上,比如 Controller/ Service 等。其底层实现原理就是 AOP,具体来说是通过 MethodValidationPostProcessor 动态注册 AOP 切面,然后使用 MethodValidationInterceptor 对切点方法织入增强。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean { @Override public void afterPropertiesSet() { Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); }
protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class MethodValidationInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } Class<?>[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; try { result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { methodToValidate = BridgeMethodResolver.findBridgedMethod( ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass())); result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } if (!result.isEmpty()) { throw new ConstraintViolationException(result); } Object returnValue = invocation.proceed(); result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } }
|
总结
实际上,不管是 RequestBody 参数校验 还是 方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。