您现在的位置是:首页 > 文章详情

如何正确使用 Bean Validation 进行数据校验

日期:2024-01-24点击:41

一、背景

在前后端开发过程中,数据校验是一项必须且常见的事,从展示层、业务逻辑层到持久层几乎每层都需要数据校验。如果在每一层中手工实现验证逻辑,既耗时又容易出错。

9001.png

为了避免重复这些验证,通常的做法是将验证逻辑直接捆绑到领域模型中,通过元数据(默认是注解)去描述模型, 生成校验代码,从而使校验从业务逻辑中剥离,提升开发效率,使开发者更专注业务逻辑本身。

0023.png

在 Spring 中,目前支持两种不同的验证方法:Spring Validation 和 JSR-303 Bean Validation,即 @Validated(org . springframework.validation.annotation.Validated)和 @Valid(javax.validation.Valid)。两者都可以通过定义模型的约束来进行数据校验,虽然两者使用类似,在很多场景下也可以相互替换,但实际上却完全不同,这些差别长久以来对我们日常使用产生了较大疑惑,本文主要梳理其中的差别、介绍 Validation 的使用及其实现原理,帮助大家在实践过程中更好使用 Validation 功能。

二、Bean Validation简介

什么是JSR?

JSR 是 Java Specification Requests 的缩写,意思是 Java 规范提案。是指向 JCP(Java Community Process) 提出新增一个标准化技术规范的正式请求,以向 Java 平台增添新的 API 和服务。JSR 已成为 Java 界的一个重要标准。

JSR-303定义的是什么标准?

JSR-303 是用于 Bean Validation 的 Java API 规范,该规范是 Jakarta EE and JavaSE 的一部分,Hibernate Validator 是 Bean Validation 的参考实现。Hibernate Validator 提供了 JSR 303 规范中所有内置 Constraint 的实现,除此之外还有一些附加的 Constraint。(最新的为 JSR-380 为 Bean Validation 3.0)453.png

常用的校验注解补充:

@NotBlank 检查约束字符串是不是 Null 还有被 Trim 的长度是否大于,只对字符串,且会去掉前后空格。

@NotEmpty 检查约束元素是否为 Null 或者是 Empty。

@Length 被检查的字符串长度是否在指定的范围内。

@Email 验证是否是邮件地址,如果为 Null,不进行验证,算通过验证。

@Range 数值返回校验。

@IdentityCardNumber 校验身份证信息。

@UniqueElements 集合唯一性校验。

@URL 验证是否是一个 URL 地址。

Spring Validation的产生背景

上文提到 Spring 支持两种不同的验证方法:Spring Validation 和 JSR-303 Bean Validation(下文使用@Validated和@Valid替代)。

为什么会同时存在两种方式?

Spring 增加 @Validated 是为了支持分组校验,即同一个对象在不同的场景下使用不同的校验形式。比如有两个步骤用于提交用户资料,后端复用的是同一个对象,第一步验证姓名,电子邮件等字段,然后在后续步骤中的其他字段中。这时候分组校验就会发挥作用。

为什么不合入到 JSR-303 中?

之所以没有将它添加到 @Valid 注释中,是因为它是使用 Java 社区过程(JSR-303)标准化的,这需要时间,而 Spring 开发者想让人们更快地使用这个功能。

@Validated 的内置自动化校验

Spring 增加 @Validated 还有另一层原因,Bean Validation 的标准做法是在程序中手工调用 Validator 或者 ExecutableValidator 进行校验,为了实现自动化,通常通过 AOP、代理等方法拦截技术来调用。而 @Validated 注解就是为了配合 Spring 进行 AOP 拦截,从而实现 Bean Validation 的自动化执行。

@Validated 和 @Valid 的区别

@Valid 是 JSR 标准 API,@Validated 扩展了 @Valid 支持分组校验且能作为 SpringBean 的 AOP 注解,在 SpringBean 初始化时实现方法层面的自动校验。最终还是使用了 JSR API 进行约束校验。

三、Bean Validation的使用

引入POM

 // 正常应该引入hibernate-validator,是JSR的参考实现 <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> </dependency> // Spring在stark中集成了,所以hibernate-validator可以不用引入 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> 

Bean层面校验

  • 变量层面约束
 public class EntryApplicationInfoCmd { /** * 用户ID */ @NotNull(message = "用户ID不为空") private Long userId; /** * 证件类型 */ @NotEmpty(message = "证件类型不为空") private String certType; } 
  • 属性层面约束

主要为了限制 Setter 方法的只读属性。属性的 Getter 方法打注释,而不是 Setter。

 public class EntryApplicationInfoCmd { public EntryApplicationInfoCmd(Long userId, String certType) { this.userId = userId; this.certType = certType; } /** * 用户ID */ private Long userId; /** * 证件类型 */ private String certType; @NotNull public String getUserId() { return userId; } @NotEmpty public String getCertType() { return userId; } } 
  • 容器元素约束
 public class EntryApplicationInfoCmd { ... List<@NotEmpty Long> categoryList; } 
  • 类层面约束

@CategoryBrandNotEmptyRecord 是自定义类层面的约束,也可以约束在构造函数上。

 @CategoryBrandNotEmptyRecord public class EntryApplicationInfoCmd { /** * 用户ID */ @NotNull(message = "用户ID不为空") private Long userId; List<@NotEmpty Long> categoryList; } 
  • 嵌套约束

嵌套对象需要额外使用 @Valid 进行标注(@Validate 不支持,为什么?请看产生的背景)。

 public class EntryApplicationInfoCmd { /** * 主营品牌 */ @Valid @NotNull private MainBrandImagesCmd mainBrandImage; } public class MainBrandImagesCmd { /** * 品牌名称 */ @NotEmpty private String brandName;; } 
  • 手工验证Bean约束
 // 获取校验器 ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); // 进行bean层面校验 Set<ConstraintViolation<User>> violations = validator.validate(EntryApplicationInfoCmd); // 打印校验信息 for (ConstraintViolation<User> violation : violations) { log.error(violation.getMessage()); } 

方法层面校验

  • 函数参数约束
 public class MerchantMainApplyQueryService { MainApplyDetailResp detail(@NotNull(message = "申请单号不能为空") Long id) { ... } } 
  • 函数返回值约束
 public class MerchantMainApplyQueryService { @NotNull @Size(min = 1) public List<@NotNull MainApplyStandDepositResp> getStanderNewDeposit(Long id) { //... } } 
  • 嵌套约束

嵌套对象需要额外使用 @Valid 进行标注(@Validate 不支持)。

 public class MerchantMainApplyQueryService { public NewEntryBrandRuleCheckApiResp brandRuleCheck(@Valid @NotNull NewEntryBrandRuleCheckRequest request) { ... } } public class NewEntryBrandRuleCheckRequest { @NotNull(message = "一级类目不能为空") private Long level1CategoryId; } 
  • 在继承中方法约束

Validation 的设计需要遵循里氏替换原则,无论何时使用类型 T,也可以使用 T 的子类型 S,而不改变程序的行为。即子类不能增加约束也不能减弱约束。

子类方法参数的约束与父类行为不一致(++错误例子++):

 // 继承的方法参数约束不能改变,否则会导致父类子类行为不一致 public interface Vehicle { void drive(@Max(75) int speedInMph); } public class Car implements Vehicle { @Override public void drive(@Max(55) int speedInMph) { //... } } 

方法的返回值可以增加约束(++正确例子++):

 // 继承的方法返回值可以增加约束 public interface Vehicle { @NotNull List<Person> getPassengers(); } public class Car implements Vehicle { @Override @Size(min = 1) public List<Person> getPassengers() { //... return null; } } 
  • 手工验证方法约束

方法层面校验使用的是 ExecutableValidator。

 // 获取校验器 ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator executableValidator = factory.getValidator().forExecutables(); // 进行方法层面校验 MerchantMainApplyQueryService service = getService(); Method method = MerchantMainApplyQueryService.class.getMethod( "getStanderNewDeposit", int.class ); Object[] parameterValues = { 80 }; Set<ConstraintViolation<Car>> violations = executableValidator.validateParameters( service, method, parameterValues ); // 打印校验信息 for (ConstraintViolation<User> violation : violations) { log.error(violation.getMessage()); } 

分组校验

不同场景复用一个 Model,采用不一样的校验方式。

 public class NewEntryMainApplyRequest { @NotNull(message = "一级类目不能为空") private Long level1CategoryId; @NotNull(message = "申请单ID不能为空", group = UpdateMerchantMainApplyCmd.class) private Long applyId; @NotEmpty(message = "审批人不能为空", group = AddMerchantMainApplyCmd.class) private String operator; } // 校验分组UpdateMerchantMainApplyCmd.class NewEntryMainApplyRequest request1 = new NewEntryMainApplyRequest( 29, null, "aaa"); Set<ConstraintViolation<NewEntryMainApplyRequest>> constraintViolations = validator.validate( request1, UpdateMerchantMainApplyCmd.class ); assertEquals("申请单ID不能为空", constraintViolations.iterator().next().getMessage()); // 校验分组AddMerchantMainApplyCmd.class NewEntryMainApplyRequest request2 = new NewEntryMainApplyRequest( 29, "12345", ""); Set<ConstraintViolation<NewEntryMainApplyRequest>> constraintViolations = validator.validate( request2, AddMerchantMainApplyCmd.class ); assertEquals("审批人不能为空", constraintViolations.iterator().next().getMessage()); 

自定义校验

自定义注解:

 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = MyConstraintValidator.class) public @interface MyConstraint { String message(); Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } 

自定义校验器:

 public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> { @Override public void initialize(MyConstraint constraintAnnotation) { } @Override public isValid isValid(Object value, ConstraintValidatorContext context) { String name = (String)value; if("xxxx".equals(name)) { return true; } return false; } } 

使用自定义约束:

 public class Test { @MyConstraint(message = "test") String name; } 

四、Bean Validation自动执行以及原理

上述 2.6 和 3.5 分别实现了 Bean 和 Method 层面的约束校验,但是每次都主动调用比较繁琐,因此 Spring 在 @RestController 的 @RequestBody 注解中内置了一些自动化校验以及在 Bean 初始化中集成了 AOP 来简化编码。

Validation的常见误解

最常见的应该就是在 RestController 中,校验 @RequestBody 指定参数的约束,使用 @Validated 或者 @Valid(++该场景下两者等价++)进行约束校验,以至于大部分人理解的 Validation 只要打个注解就可以生效,实际上这只是一种特例。很多人在使用过程中经常遇到约束校验不生效。

  • 约束校验生效

Spring-mvc 中在 @RequestBody 后面加 @Validated、@Valid 约束即可生效。

 @RestController @RequestMapping("/biz/merchant/enter") public class MerchantEnterController { @PostMapping("/application") // 使用@Validated public HttpMessageResult addOrUpdateV1(@RequestBody @Validated MerchantEnterApplicationReq req){ ... } // 使用@Valid @PostMapping("/application2") public HttpMessageResult addOrUpdate2(@RequestBody @Valid MerchantEnterApplicationReq req){ ... } } 
  • 约束校验不生效

然而下面这个约束其实是不生效的,想要生效得在 MerchantEntryServiceImpl 类目加上 @Validated 注解。

 // @Validated 不加不生效 @Service public class MerchantEntryService { public Boolean applicationAddOrUpdate(@Validated MerchantEnterApplicationReq req) { ... } public Boolean applicationAddOrUpdate2(@Valid MerchantEnterApplicationReq req) { ... } } 

那么究竟为什么会出现这种情况呢,这就需要对 Spring Validation 的注解执行原理有一定的了解。

Controller自动执行约束校验原理

在 Spring-mvc 中,有很多拦截器对 Http 请求的出入参进行解析和转换,Validation 解析和执行也是类似,其中 RequestResponseBodyMethodProcessor 是用于解析 @RequestBody 标注的参数以及处理 @ResponseBody 标注方法的返回值的。

 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); } // 类上或者方法上标注了@ResponseBody注解都行 @Override public boolean supportsReturnType(MethodParameter returnType) { return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class)); } // 这是处理入参封装校验的入口 @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); // 只有存在binderFactory才会去完成自动的绑定、校验~ if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { // 这里完成数据绑定+数据校验~~~~~(绑定的错误和校验的错误都会放进Errors里) validateIfApplicable(binder, parameter); // 若有错误消息hasErrors(),并且仅跟着的一个参数不是Errors类型,Spring MVC会主动给你抛出MethodArgumentNotValidException异常 if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } // 把错误消息放进去 证明已经校验出错误了~~~ // 后续逻辑会判断MODEL_KEY_PREFIX这个key的~~~~ if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); } ... } 

约束的校验逻辑是在 RequestResponseBodyMethodProcessor.validateIfApplicable 实现的,这里同时兼容了 @Validated 和 @Valid,所以该场景下两者是等价的。

 // 校验,如果合适的话。使用WebDataBinder,失败信息最终也都是放在它身上~ // 入参:MethodParameter parameter protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { // 拿到标注在此参数上的所有注解们(比如此处有@Valid和@RequestBody两个注解) Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { // 先看看有木有@Validated Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); // 这个里的判断是关键:可以看到标注了@Validated注解 或者注解名是以Valid打头的 都会有效哦 //注意:这里可没说必须是@Valid注解。实际上你自定义注解,名称只要一Valid开头都成~~~~~ if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { // 拿到分组group后,调用binder的validate()进行校验~~~~ // 可以看到:拿到一个合适的注解后,立马就break了~~~ // 所以若你两个主机都标注@Validated和@Valid,效果是一样滴~ Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); binder.validate(validationHints); break; } } 

binder.validate() 的实现中使用的 org.springframework.validation.Validator 的接口,该接口的实现为 SpringValidatorAdapter。

 public void validate(Object... validationHints) { Object target = getTarget(); Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); for (Validator validator : getValidators()) { // 使用的org.springframework.validation.Validator,调用SpringValidatorAdapter.validate if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) { ((SmartValidator) validator).validate(target, bindingResult, validationHints); } else if (validator != null) { validator.validate(target, bindingResult); } } } 

在 ValidatorAdapter.validate 实现中,最终调用了 javax.validation.Validator.validate,也就是说最终是调用 JSR 实现,@Validate 只是外层的包装,在这个包装中扩展的分组功能。

 public class SpringValidatorAdapter { ... private javax.validation.Validator targetValidator; @Override public void validate(Object target, Errors errors) { if (this.targetValidator != null) { processConstraintViolations( // 最终是调用JSR实现 this.targetValidator.validate(target), errors)); } } } 

++targetValidator.validate 就是 javax.validation.Validator.validate 上述 2.6 Bean 层面手工验证一致。++

Service自动执行约束校验原理

非Controller的@RequestBody注解,自动执行约束校验,是通过 MethodValidationPostProcessor 实现的,该类继承。

BeanPostProcessor, 在 Spring Bean 初始化过程中读取 @Validated 注解创建 AOP 代理(实现方式与 @Async 基本一致)。该类开头文档注解(++JSR 生效必须类层面上打上 @Spring Validated 注解++)。

 /** * <p>Target classes with such annotated methods need to be annotated with Spring's * {@link Validated} annotation at the type level, for their methods to be searched for * inline constraint annotations. Validation groups can be specified through {@code @Validated} * as well. By default, JSR-303 will validate against its default group only. */ public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean { private Class<? extends Annotation> validatedAnnotationType = Validated.class; @Nullable private Validator validator; ..... /** * 设置Validator * Set the JSR-303 Validator to delegate to for validating methods. * <p>Default is the default ValidatorFactory's default Validator. */ public void setValidator(Validator validator) { // Unwrap to the native Validator with forExecutables support if (validator instanceof LocalValidatorFactoryBean) { this.validator = ((LocalValidatorFactoryBean) validator).getValidator(); } else if (validator instanceof SpringValidatorAdapter) { this.validator = validator.unwrap(Validator.class); } else { this.validator = validator; } } /** * Create AOP advice for method validation purposes, to be applied * with a pointcut for the specified 'validated' annotation. * @param validator the JSR-303 Validator to delegate to * @return the interceptor to use (typically, but not necessarily, * a {@link MethodValidationInterceptor} or subclass thereof) * @since 4.2 */ protected Advice createMethodValidationAdvice(@Nullable Validator validator) { // 创建了方法调用时的拦截器 return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); } } 

真正执行方法调用时,会走到 MethodValidationInterceptor.invoke,进行约束校验。

 public class MethodValidationInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } // Standard Bean Validation 1.1 API ExecutableValidator execVal = this.validator.forExecutables(); ... try { // 执行约束校验 result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 // Let's try to find the bridged method on the implementation class... methodToValidate = BridgeMethodResolver.findBridgedMethod( ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass())); result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } ... return returnValue; } } 

execVal.validateParameters 就是 javax.validation.executable.ExecutableValidator.validateParameters 与上述 3.5 方法层面手工验证一致。

五、总结

1367.jpeg

参考文章:

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single

*文/洛峰

本文属得物技术原创,更多精彩文章请看:得物技术官网

未经得物技术许可严禁转载,否则依法追究法律责任!

原文链接:https://my.oschina.net/u/5783135/blog/10927175
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章