spring-boot-starter-validation 相关的参数校验
1. 参数校验 1.1 Spring参数校验快速开始 1.1.1 导入依赖 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency >
1.1.2 使用 参数校验实操分两步
在Controller层将要校验的参数上标注@Vaild
注解
在要校验对象属性加上具体的约束
例子 :
我要校验登录的用户名和密码不能为空,那么我就在参数loginUserDTO前加上@valid
注解。
1 2 3 4 5 6 7 8 9 @RestController public class testVaildController { @Resource public LoginService loginService; @PostMapping("/login") public ResponseResult login (@RequestBody @Valid LoginUserDTO loginUserDTO) { return ResponseResult.success(loginService.login(loginUserDTO.getUserName(),loginUserDTO.getPassword())); } }
然后在LoginUserDTO类中加上具体的约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class LoginUserDTO { @NotBlank(message = "用户名不能为空") private String userName = "" ; @NotBlank(message = "密码不能为空") private String password = "" ; public String getUserName () { return this .userName; } public void setUserName (String userName) { this .userName = userName; } public String getPassword () { return this .password; } public void setPassword (String password) { this .password = password; } }
这样参数校验就可以生效了,测试接口结果如下:
1 2 3 4 5 6 { "timestamp" : "2024-05-24T09:15:58.364+00:00" , "status" : 400 , "error" : "Bad Request" , "path" : "/login" }
异常捕获
为了更有效的传递信息 以及进行结果统一返回 、日志记录 等操作,我们通常会定义异常处理器来统一处理参数校验的异常(BindException是什么见下文异常信息)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestControllerAdvice public class GlobalExceptionHandler { Logger logger = LoggerFactory.getLogger("GlobalExceptionHandler" ); @ExceptionHandler(BindException.class) public ResponseResult bindExceptionHandler (BindException e) { List<String> list = new ArrayList <>(); BindingResult bindingResult = e.getBindingResult(); for (ObjectError objectError : bindingResult.getAllErrors()) { FieldError fieldError = (FieldError) objectError; logger.error("参数 {} ,{} 校验错误:{}" , fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage()); list.add(fieldError.getDefaultMessage()); } return ResponseResult.error(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),list.toString()); } }
测试数据与结果如下 :
1 2 3 4 5 6 7 8 9 10 { "userName" : "" , "password" : "" } { "data" : null , "code" : 500 , "msg" : "[用户名不能为空, 密码不能为空]" }
1.2 常用的校验注解
控制检查
注解
说明
@NotBlank
用于字符串,字符串不能为null也不能为空
@NotEmpty
字符串同上,集合不能为空,必须有元素
@NotNull
不能为null
@Null
必须为null
数值检查
注解
说明
@DecimalMax(value)
被标注元素必须是数字,必须小于等于value
@DecimalMin(value)
被标注元素必须是数字,必须大于等于value
@Digits(integer,fraction)
被标注的元素必须为数字,其值的整数部分精度为 integer
,小数部分精度为 fraction
@Positive
被标注的元素必须为正数
@PositiveOrZero
被标注的元素必须为正数或 0
@Max(value)
被标注的元素必须小于等于指定的值
@Min(value)
被标注的元素必须大于等于指定的值
@negative
被标注的元素必须为负数
NegativeOrZero
被标注的元素必须为负数或 0
Boolean检查 (不太用的到的样子)
注解
说明
@AssertFalse
被标注的元素必须值为 false
@AssertTrue
被标注的元素必须值为 true
长度检查
注解
说明
@Size(min,max)
被标注的元素长度必须在 min
和 max
之间,可以是 String、Collection、Map、数组
日期检查
注解
说明
@Future
被标注的元素必须是一个将来的日期
@FutureOrPresent
被标注的元素必须是现在或者将来的日期
@Past
被标注的元素必须是一个过去的日期
@PastOrPresent
被标注的元素必须是现在或者过去的日期
其他
注解
说明
@Email
被标注的元素必须是电子邮箱地址
@Pattern(regexp)
被标注的元素必须符合正则表达式
1.3 @Vaild与@Vaildated 在第一步加注解的时候,可以明显的看到还有一个可能也是参数校验的注解@Validated
,把@Vaild
换成@Validated
,我们惊奇的发现参数校验也能正常的工作,接下来我们就来看看这两注解之间的联系与区别。
1.3.1 来源 一个是Spring 的,一个是javax ,了解过@Autowired
与@Resource
区别的老哥可能很快就反应过来了,就像这两个注解一样。一个是JSR规范的,一个是Spring规范的
1 2 import org.springframework.validation.annotation.Validated;import javax.validation.Valid;
1.3.2 定义区别 1 2 3 4 5 6 7 8 9 10 11 12 @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Validated { Class<?>[] value() default {}; } @Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Valid {}
差异主要有两点 :
接下来我们来结合定义区别看看它们的功能差异
1.3.3 功能差异 嵌套校验 上文校验的时候我们在类属性上加上相关的限制注解就可以了,但是如果属性是一个类的实例,我们想校验这个作为属性的实例里面的字段,我们就只能使用@Vaild
加在属性上,标注这是需要校验的属性。**@Validated
不能标注在属性上,自然也就 不支持嵌套校验**
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 56 57 58 59 60 61 62 @PostMapping("/buy") public ResponseResult buy (@RequestBody @Valid ShoppingCart shoppingCart) { return ResponseResult.success(payService.pay(shoppingCart.getUserId(),shoppingCart.getItemList())); } public class ShoppingCart { @Positive(message = "用户id必须大于0") private Long userId; @NotEmpty(message = "不能为空") @Valid private List<Item> itemList; public Long getUserId () { return userId; } public void setUserId (Long userId) { this .userId = userId; } public List<Item> getItemList () { return itemList; } public void setItemList (List<Item> itemList) { this .itemList = itemList; } } public class Item { @Positive(message = "价格必须大于0") private BigDecimal price; @NotBlank(message = "物品名不能为空") private String name; @Positive(message = "数量必须大于0") private int number; public BigDecimal getPrice () { return price; } public void setPrice (BigDecimal price) { this .price = price; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getNumber () { return number; } public void setNumber (int number) { this .number = number; } }
结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "userId" : "1" , "itemList" : [ { "price" : "142.0" , "name" : "小风车" , "number" : 1 } , { "price" : "-2" , "name" : "" , "number" : -1 } ] } { "data" : null , "code" : 500 , "msg" : "[数量必须大于0, 价格必须大于0, 物品名不能为空]" }
可以看到校验成功了,当然这里的异常捕获逻辑比较简单,具体生产环境里可以将返回具体的校验信息,进行更清晰的信息提示。
杂记 :
分组校验 上面看注解代码的时候可以明显注意到@Validated
注解里面有个value
属性。**@Valid
没有value属性,自然也就 不支持分组校验**
1 2 3 4 5 6 7 @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Validated { Class<?>[] value() default {}; }
这就和@Validated
的分组校验有关了,(@Valid
没有定义这个属性,自然也就不支持分组校验)
1 2 3 4 5 package javax.validation.groups;public interface Default {}
我们可以自定义分组来指定需要校验的时机。
1 2 3 4 5 public class Group { public interface GroupTest1 {} public interface GroupTest2 {} }
测试
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 public class LoginUserDTO { @NotBlank(message = "账户不能为空",groups = {Group.GroupTest1.class, Default.class}) private String userName = "" ; @NotBlank(message = "密码不能为空") private String password = "" ; public String getUserName () { return this .userName; } public void setUserName (String userName) { this .userName = userName; } public String getPassword () { return this .password; } public void setPassword (String password) { this .password = password; } } @PostMapping("/login") public ResponseResult login (@RequestBody @Validated(value = {Group.GroupTest2.class}) LoginUserDTO loginUserDTO) { return ResponseResult.success(loginService.login(loginUserDTO.getUserName(),loginUserDTO.getPassword())); }
结果 :
1 2 3 4 5 6 7 8 9 10 { "userName" : "" , "password" : "123456" } { "data" : "登录成功" , "code" : 200 , "msg" : "操作成功" }
2. 深入理解@Valid与@Validated 因为@Valid
与@Validated
能混合使用,我们可以大胆猜测一下,一定有一个Adapter
来承担两者的适配工作,搜搜Valid相关的Adapter,还真有一个。
2.1 SpringValidtorAdapter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class SpringValidatorAdapter implements SmartValidator , javax.validation.Validator { @Nullable private javax.validation.Validator targetValidator; @Override public void validate (Object target, Errors errors, Object... validationHints) { if (this .targetValidator != null ) { processConstraintViolations( this .targetValidator.validate(target, asValidationGroups(validationHints)), errors); } } @Override public <T> Set<ConstraintViolation<T>> validate (T object, Class<?>... groups) { Assert.state(this .targetValidator != null , "No target Validator set" ); return this .targetValidator.validate(object, groups); } }
SpringValidtorAdapter的结构
根据上面结构图和代码,可以看到SpringValidatorAdapter
实现了两个不同的validator
接口,针对其中核心方法validate()
进行了返回值的适配(有点像Runnable
和 Callable
之间的适配)
2.2 具体校验器的获取实现 2.2.1 ValidatorFactory 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public interface ValidatorFactory extends AutoCloseable { Validator getValidator () ; ValidatorContext usingContext () ; MessageInterpolator getMessageInterpolator () ; TraversableResolver getTraversableResolver () ; ConstraintValidatorFactory getConstraintValidatorFactory () ; ParameterNameProvider getParameterNameProvider () ; ClockProvider getClockProvider () ; public <T> T unwrap (Class<T> type) ; @Override public void close () ; }
ValidatorFactory
的实现可以分成两部分,hibernate
和Spring
实现。
LocalValidatorFactoryBean LocalValidatorFactoryBean
不仅是ValidatorFactory
实现,还是SpringValidAdapter
子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class LocalValidatorFactoryBean extends SpringValidatorAdapter implements ValidatorFactory , ApplicationContextAware, InitializingBean, DisposableBean { @Override @SuppressWarnings({"rawtypes", "unchecked"}) public void afterPropertiesSet () { Configuration<?> configuration; ... try { this .validatorFactory = configuration.buildValidatorFactory(); setTargetValidator(this .validatorFactory.getValidator()); } finally { closeMappingStreams(mappingStreams); } } }
ValidatorFactoryImpl 在最开始我们导入了校验starter,导入的就有org.hibernate.validator 相关的类
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 package org.hibernate.validator.internal.engine;public class ValidatorFactoryImpl implements HibernateValidatorFactory { @Override public Validator getValidator () { return createValidator( constraintValidatorManager.getDefaultConstraintValidatorFactory(), valueExtractorManager, validatorFactoryScopedContext, methodValidationConfiguration ); } Validator createValidator (ConstraintValidatorFactory constraintValidatorFactory, ValueExtractorManager valueExtractorManager, ValidatorFactoryScopedContext validatorFactoryScopedContext, MethodValidationConfiguration methodValidationConfiguration) { BeanMetaDataManager beanMetaDataManager = beanMetaDataManagers.computeIfAbsent( new BeanMetaDataManagerKey ( validatorFactoryScopedContext.getParameterNameProvider(), valueExtractorManager, methodValidationConfiguration ), key -> new BeanMetaDataManager ( constraintHelper, executableHelper, typeResolutionHelper, validatorFactoryScopedContext.getParameterNameProvider(), valueExtractorManager, validationOrderGenerator, buildDataProviders(), methodValidationConfiguration ) ); return new ValidatorImpl ( constraintValidatorFactory, beanMetaDataManager, valueExtractorManager, constraintValidatorManager, validationOrderGenerator, validatorFactoryScopedContext ); } }
2.2.2 ValidatorImpl ValidatorImpl
是Hibernate Validator 提供的唯一校验器实现,校验过程很复杂,了解是哪个类就行,感兴趣可以深度剖析
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 ValidatorImpl implements Validator , ExecutableValidator { private static final Collection<Class<?>> DEFAULT_GROUPS = Collections.<Class<?>>singletonList( Default.class ); private final transient ValidationOrderGenerator validationOrderGenerator; private final ConstraintValidatorFactory constraintValidatorFactory; ... @Override public final <T> Set<ConstraintViolation<T>> validate (T object, Class<?>... groups) { Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() ); sanityCheckGroups( groups ); @SuppressWarnings("unchecked") Class<T> rootBeanClass = (Class<T>) object.getClass(); BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass ); if ( !rootBeanMetaData.hasConstraints() ) { return Collections.emptySet(); } BaseBeanValidationContext<T> validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object ); ValidationOrder validationOrder = determineGroupValidationOrder( groups ); BeanValueContext<?, Object> valueContext = ValueContexts.getLocalExecutionContextForBean( validatorScopedContext.getParameterNameProvider(), object, validationContext.getRootBeanMetaData(), PathImpl.createRootPath() ); return validateInContext( validationContext, valueContext, validationOrder ); }
3. 异常信息 3.1 说明 参数校验失败抛出的异常时MethodArgumentNotValidException
,不过建议捕获BindException
,因为MethodArgumentNotValidException
是对BindResult
的封装,只能通过getMessage()
获取封装好的信息,不够灵活
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 public class MethodArgumentNotValidException extends BindException { private final MethodParameter parameter; public MethodArgumentNotValidException (MethodParameter parameter, BindingResult bindingResult) { super (bindingResult); this .parameter = parameter; } public final MethodParameter getParameter () { return this .parameter; } public String getMessage () { StringBuilder sb = (new StringBuilder ("Validation failed for argument [" )).append(this .parameter.getParameterIndex()).append("] in " ).append(this .parameter.getExecutable().toGenericString()); BindingResult bindingResult = this .getBindingResult(); if (bindingResult.getErrorCount() > 1 ) { sb.append(" with " ).append(bindingResult.getErrorCount()).append(" errors" ); } sb.append(": " ); Iterator var3 = bindingResult.getAllErrors().iterator(); while (var3.hasNext()) { ObjectError error = (ObjectError)var3.next(); sb.append('[' ).append(error).append("] " ); } return sb.toString(); } }
3.2 BindException 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class BindException extends Exception implements BindingResult { private final BindingResult bindingResult; public BindException (BindingResult bindingResult) { Assert.notNull(bindingResult, "BindingResult must not be null" ); this .bindingResult = bindingResult; } public BindException (Object target, String objectName) { Assert.notNull(target, "Target object must not be null" ); this .bindingResult = new BeanPropertyBindingResult (target, objectName); } }
3.2.1 BindResult BindResult是一个接口,里面没有什么特别的东西。
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 public interface BindingResult extends Errors { String MODEL_KEY_PREFIX = BindingResult.class.getName() + "." ; @Nullable Object getTarget () ; Map<String, Object> getModel () ; @Nullable Object getRawFieldValue (String field) ; @Nullable PropertyEditor findEditor (@Nullable String field, @Nullable Class<?> valueType) ; @Nullable PropertyEditorRegistry getPropertyEditorRegistry () ; String[] resolveMessageCodes(String errorCode); String[] resolveMessageCodes(String errorCode, String field); void addError (ObjectError error) ; default void recordFieldValue (String field, Class<?> type, @Nullable Object value) { } default void recordSuppressedField (String field) { } default String[] getSuppressedFields() { return new String [0 ]; } }
3.2.2 AbstractBindingResult 一般来说抽象类都是接口的基本实现,BindingResult也不例外
1 2 3 4 5 6 7 8 9 10 11 12 public abstract class AbstractBindingResult extends AbstractErrors implements BindingResult , Serializable { private final String objectName; private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver (); private final List<ObjectError> errors = new ArrayList (); private final Map<String, Class<?>> fieldTypes = new HashMap (); private final Map<String, Object> fieldValues = new HashMap (); private final Set<String> suppressedFields = new HashSet (); ... }
ObjectError
本体
1 2 3 4 5 6 7 8 public class ObjectError extends DefaultMessageSourceResolvable { private final String objectName; @Nullable private transient Object source; ... }
子类
1 2 3 4 5 6 7 8 9 10 public class FieldError extends ObjectError { private final String field; @Nullable private final Object rejectedValue; private final boolean bindingFailure; ... }
父类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class DefaultMessageSourceResolvable implements MessageSourceResolvable , Serializable { @Nullable private final String[] codes; @Nullable private final Object[] arguments; @Nullable private final String defaultMessage; ... }
Tips :可以自行打印观察值,推测存放信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @ExceptionHandler(BindException.class) public ResponseResult bindExceptionHandler (BindException e) { List<String> list = new ArrayList <>(); BindingResult bindingResult = e.getBindingResult(); for (ObjectError objectError : bindingResult.getAllErrors()) { FieldError fieldError = (FieldError) objectError; System.out.println("--------------" ); System.out.println("codes" ); Arrays.stream(fieldError.getCodes()).forEach(System.out::println); System.out.println("--------------" ); System.out.println("code" ); System.out.println(fieldError.getCode()); System.out.println("--------------" ); System.out.println("ObjectName" ); System.out.println(fieldError.getObjectName()); logger.error("参数 {} ,{} 校验错误:{}" , fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage()); list.add(fieldError.getDefaultMessage()); } return ResponseResult.error(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),list.toString()); }