当前关注:Spring:全面拥抱 Jakarta Bean Validation 规范
“我正在参加「掘金·启航计划」”
随着 JSR-303 、 JSR-349 和 JSR-380 提案的相继问世,Bean Validation 规范已经从初出茅庐的 1.0 版本发展到渐入佳境的 2.0 版本。在 Eclipse 基金会接管 Java EE 之后,Bean Validation 规范成为了 Jakarta EE 的一部分,Jakarta Bean Validation 自然也就成为 Bean Validation 的新标准,目前 Jakarta Bean Validation 最新版为 3.0。Jakarta Bean Validation 目前由 Hibernate 实现,Apache BVal 感觉有些掉队了。
【资料图】
Jakarta Bean Validation 2.0 在本质上是套壳版的 Bean Validation 2.0,因为前者只是将 GAV 坐标由 javax.validation:javax.validation-api 更新为 jakarta.validation:jakarta.validation-api ;而 Jakarta Bean Validation 3.0 在 Jakarta Bean Validation 2.0 的基础上,彻底将包命名空间迁移到 jakarta.validation ,而不再是 javax.validation 。
在 Jakarta Bean Validation 规范中,有一些核心 API 需要大家熟悉,如下:
Validator ,用于校验 常规 Java Bean,同时支持分组校验;分组校验有时候很有必要,比如用户名在创建时不允许为空,但在更新时用户名可以为空。 ExecutableValidator ,用于校验方法参数与方法返回值,同样支持分组校验。方法参数和方法返回值往往并不是一个 常规 Java Bean,可能是一种容器,比如:List、Map 和 Optional 等;Java 8 针对 ElementType 新增了一个 TYPE_USE 枚举实例,这让容器元素 (container elements) 的校验变得简单,Jakarta Bean Validation API 中内置的注解式约束的头上均有 TYPE_USE 的身影。 ConstraintValidator ,如果 Jakarta Bean Validation API 中内置的注解式约束不能满足实际的需求,则需要自定义注解式约束,同时还需要为自定义约束指定校验器,这个校验器需要实现 ConstraintValidator 接口。 ValueExtractor ,容器并不仅仅指的是 JDK 类库中的 List、Map 和 Set 等,也可以是一些包装类,比如 ResponseEntity ;如果要想校验 ResponseEntity 容器中的 body,那么就需要通过实现 ValueExtractor 接口来自定义一个容器元素抽取器,然后通过 Configuration 的 addValueExtractor() 方法注册自定义 ValueExtractor。早在 Spring 2.X 版本中,Bean Validation 的雏形就已显现,核心接口为 org.springframework.validation.Validator 。Spring 自家的 Validator API 设计的比较简陋,而且需要开发人员编写数量繁多的 Validator 实现类,这与 Jakarta Bean Validation 所推崇的 注解式约束 (Constraints) 相比,简直毫无胜算可言。尽管在 Spring MVC 中依然可以看到 Spring Validator API 的身影,其实最终也是将校验请求转发到 Jakarta Bean Validation 中去的,这部分内容会是本文的重点。
1 Spring Validator API
Spring 从 3.0 版本开始全面拥抱 Jakarta Bean Validation 规范以实现自我救赎。
在 Spring Framework 中, Validator 是对 Bean Validation 的顶级抽象接口,它有两个直系子类,分别是 SmartValidator 和 NoOpValidator ,SmartValidator 具备分组校验的能力,其 validate() 方法中第三个参数 Object... validationHints 就是和分组校验相关的,而 NoOpValidator 是一个空的实现。
java复制代码package org.springframework.validation;public interface Validator { boolean supports(Class>clazz); void validate(Object target, Errors errors);}public interface SmartValidator extends Validator { void validate(Object target, Errors errors, Object... validationHints); default void validateValue( Class>targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { throw new IllegalArgumentException(\"Cannot validate individual value for \" + targetType); }}
SpringValidatorAdapter 不仅实现了 SmartValidator 接口,同时也实现了 jakarta.validation.Validator 接口,结合其 Adapter 后缀,相信大家一定猜到了 SpringValidatorAdapter 的作用,那就是将 Bean Validation 请求转发到 Jakarta Bean Validation 实现方中去。主要内容如下。
java复制代码package org.springframework.validation.beanvalidation;public class SpringValidatorAdapter implements SmartValidator, jakarta.validation.Validator { private jakarta.validation.Validator targetValidator; //--------------------------------------------------------------------- // Implementation of Spring Validator interface //--------------------------------------------------------------------- @Override public boolean supports(Class>clazz) { return (this.targetValidator != null); } @Override public void validate(Object target, Errors errors) { if (this.targetValidator != null) { processConstraintViolations(this.targetValidator.validate(target), errors); } } @Override public void validate(Object target, Errors errors, Object... validationHints) { if (this.targetValidator != null) { processConstraintViolations( this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); } } //--------------------------------------------------------------------- // Implementation of JSR-303 Validator interface //--------------------------------------------------------------------- @Override public Set>validate(T object, Class>... groups) { Assert.state(this.targetValidator != null, \"No target Validator set\"); return this.targetValidator.validate(object, groups); }}
SpringValidatorAdapter 只负责转发 Bean Validation 请求,而 LocalValidatorFactoryBean 则负责构建与配置 jakarta.validation.Validator 实例,LocalValidatorFactoryBean 继承自 SpringValidatorAdapter,并且实现了 InitializingBean 接口,在后者 afterPropertiesSet() 方法内进行构建与配置 jakarta.validation.Validator 实例,然后通过 setTargetValidator() 方法为 SpringValidatorAdapter 注入 Bean Validation 引擎。
ValidatorAdapter 是 Spring Boot 中的一个适配器,虽然只实现了 SmartValidator 接口,但它的站位更高,既能适配 LocalValidatorFactoryBean,又能适配 NoOpValidator。当 Jakarta Bean Validation API 在当前 classpath 下不存在时,那么最终适配的就是 NoOpValidator。这一点可以通过 ValidationAutoConfiguration 和 WebMvcAutoConfiguration 源码来验证,注意:在 WebMvcAutoConfiguration 头上标有 @AutoConfiguration(after = {ValidationAutoConfiguration.class}) 。
ValidationAutoConfiguration 关于 LocalValidatorFactoryBean 的声明逻辑如下,大家可以通过 defaultValidator 这一 bean 名称来手动获取该 LocalValidatorFactoryBean 实例。
java复制代码@AutoConfiguration@ConditionalOnClass(ExecutableValidator.class)@ConditionalOnResource(resources = \"classpath:META-INF/services/jakarta.validation.spi.ValidationProvider\")@Import(PrimaryDefaultValidatorPostProcessor.class)public class ValidationAutoConfiguration { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnMissingBean(jakarta.validation.Validator.class) public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext, ObjectProvidercustomizers) { LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); factoryBean.setConfigurationInitializer((configuration) ->customizers.orderedStream() .forEach((customizer) ->customizer.customize(configuration))); MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext); factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); return factoryBean; }}
WebMvcAutoConfiguration 则声明了一名为 mvcValidator 的 ValidatorAdapter 类型的 bean。
java复制代码@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class })@ConditionalOnWebApplication(type = Type.SERVLET)@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)@ImportRuntimeHints(WebResourcesRuntimeHints.class)public class WebMvcAutoConfiguration { @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(WebProperties.class) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { @Bean @Override public Validator mvcValidator() { if (!ClassUtils.isPresent(\"jakarta.validation.Validator\", getClass().getClassLoader())) { return super.mvcValidator(); } return ValidatorAdapter.get(getApplicationContext(), getValidator()); } }}
2 Spring MVC 是如何进行 Bean 校验的
在 Spring MVC 中, HandlerMethodArgumentResolver 一般会委派 HttpMessageConverter 从 HTTP 请求中解析出 HandlerMethod 所需要的方法参数值 (有了参数才能反射调用由 @RestController 注解标记的方法),然后进行 Bean Validation 操作。 RequestResponseBodyMethodProcessor 是极为重要的一个 HandlerMethodArgumentResolver 实现类,因为由 @RequestBody 标记的参数就由它解析,主体逻辑如下所示。
java复制代码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); } protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; } } } public static Object[] determineValidationHints(Annotation ann) { Class extends Annotation>annotationType = ann.annotationType(); String annotationName = annotationType.getName(); if (\"jakarta.validation.Valid\".equals(annotationName)) { return EMPTY_OBJECT_ARRAY; } Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); if (validatedAnn != null) { Object hints = validatedAnn.value(); return convertValidationHints(hints); } if (annotationType.getSimpleName().startsWith(\"Valid\")) { Object hints = AnnotationUtils.getValue(ann); return convertValidationHints(hints); } return null; } public void validate(Object... validationHints) { Object target = getTarget(); Assert.state(target != null, \"No target to validate\"); BindingResult bindingResult = getBindingResult(); // Call each validator with the same binding result for (Validator validator : getValidators()) { if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator smartValidator) { smartValidator.validate(target, bindingResult, validationHints); } else if (validator != null) { validator.validate(target, bindingResult); } } }}
validateIfApplicable() 方法即负责 Bean Validation 操作。首先通过 determineValidationHints() 方法从 MethodParameter 实例中决策是否需要进行分组校验,若是 @Valid 注解,那么无需进行分组校验,若是 @Validated 注解,则取出分组信息。然后获取 org.springframework.validation.Validator 进行校验,这里 getValidators() 方法拿到的就是 ValidatorAdapter,至于具体适配的是 LocalValidatorFactoryBean 还是 NoOpValidator,这要看是否引入 spring-boot-starter-validation 依赖了。
最后进行真正地检验操作,无论是否涉及分组校验,最后干活的肯定是 hibernate-validator 组件。 敲黑板! SmartValidator 和 Validator 这俩 Spring Validator API 在内部都是将校验请求转发到 jakarta.validation.Validator 中的 Set
分析到这里,终于知道为什么下面这种面向容器元素的校验无法生效了,如下:
java复制代码@RestController@RequestMapping(path = \"/user/v1\")public class UserController { @PostMapping() public ResponseEntitycreateUser(@Validated or @Valid @RequestBody Listusers) { return ResponseEntity.ok(\"ojbk\"); }}
但只需要像下面这番小改造,List 中的每个 user 实例都可以得到校验。
java复制代码@Validated@RestController@RequestMapping(path = \"/user/v1\")public class UserController { @PostMapping() public ResponseEntitycreateUser(@RequestBody List<@Valid User>users) { return ResponseEntity.ok(\"ojbk\"); }}
既然 List 中的每个 user 实例都可以得到校验,那说明一定是走到 ExecutableValidator 的 Set
我们继续向下探索。其实 ValidationAutoConfiguration 不仅仅是声明了一个 LocalValidatorFactoryBean,同时还声明了一个 MethodValidationPostProcessor ,如下所示。
java复制代码@AutoConfiguration@ConditionalOnClass(ExecutableValidator.class)@ConditionalOnResource(resources = \"classpath:META-INF/services/jakarta.validation.spi.ValidationProvider\")@Import(PrimaryDefaultValidatorPostProcessor.class)public class ValidationAutoConfiguration { @Bean @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, ObjectProvidervalidator, ObjectProviderexcludeFilters) { FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor( excludeFilters.orderedStream()); boolean proxyTargetClass = environment.getProperty(\"spring.aop.proxy-target-class\", Boolean.class, true); processor.setProxyTargetClass(proxyTargetClass); processor.setValidatorProvider(validator); return processor; }}
进入 MethodValidationPostProcessor 中一探究竟。
java复制代码public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean { @Override public void afterPropertiesSet() { Pointcut pointcut = new AnnotationMatchingPointcut(Validated.class, true); this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); } protected Advice createMethodValidationAdvice(Suppliervalidator) { return new MethodValidationInterceptor(validator); }}
这块主要是 Spring AOP 中的知识了。 PointcutAdvisor 是 Spring 中的切面,它由 Pointcut 和 Advice 组成,前者用于决策应该在哪一连接点附件织入切面逻辑,后者则承载了具体的切面逻辑。 AnnotationMatchingPointcut 告诉我们一个事实:只要某一个类的头上标记有 @Validated 注解,那么就应该织入切面逻辑,而切面逻辑就在 MethodValidationInterceptor 中。
java复制代码public class MethodValidationInterceptor implements MethodInterceptor { private final Suppliervalidator; @Override public Object invoke(MethodInvocation invocation) throws Throwable { // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } Class>[] groups = determineValidationGroups(invocation); // Standard Bean Validation 1.1 API ExecutableValidator execVal = this.validator.get().forExecutables(); Method methodToValidate = invocation.getMethod(); Set>result; Object target = invocation.getThis(); if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) { // Allow validation for AOP proxy without a target target = methodInvocation.getProxy(); } Assert.state(target != null, \"Target must not be null\"); try { result = execVal.validateParameters(target, 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(), target.getClass())); result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups); } if (!result.isEmpty()) { throw new ConstraintViolationException(result); } Object returnValue = invocation.proceed(); result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; }}
从上述 MethodValidationInterceptor 源码中,终于看到了 ExecutableValidator 中 validateParameters() 和 validateReturnValue() 这俩方法的身影!这也就能说通了:为什么在 UserController 头上标记一个 @Validated 注解以及在 List<@Valid User>users 中追加一个 @Valid 注解,容器元素的校验就生效的原因。
最后提一句: hibernate-validator 默认是开启 fail-fast 机制的,可以通过下面这种方式去关闭。
java复制代码@Componentpublic class FailFastValidationConfigurationCustomizer implements ValidationConfigurationCustomizer { @Override public void customize(Configuration>configuration) { configuration.addProperty(\"hibernate.validator.fail_fast\", \"true\"); }}
总结
读完本文,大家能说出 @Validated 注解与 @Valid 注解的区别吗?
原文链接:https://juejin.cn/post/7243337546825187385
标签:
银燕飞临帕米尔高原 喀什旅游业迎来腾飞新机遇
拥抱智能新变化,MAXHUB 全新赋能企业新发展
端点科技携手晶科能源,推进能源供应链数字化转型
今日聚焦!美国高校研发出锂电池"完美替代者"
每日简讯:不惧风雨,山地骑行
当前速递!湖南大学无锡半导体先进制造创新中心正式揭牌
即时:通过数字化转型实现环境可持续发展
新资讯:3GW太阳能光伏组件项目:致力于成为行业领先专家 用光伏智造引领产业发展
快看:乘联会数据显示磷酸铁锂电池装机量反超三元锂
环球热点!丰田研发新型电动汽车电池续航提升15%
- 06-17当前关注:Spring:全面拥抱 Jakarta Bean Validation 规范
- 06-17诸葛青云武侠小说八菩萨:自我重复的套路与一夫十妻的烂俗结局
- 06-17「毕业季·校长说」西南交大校长杨丹:“强国一代”以青春之功铸就强国之路
- 06-17环球观天下!喊话未来高级工程师,这所学院向你抛来“橄榄枝”
- 06-172023年值得推荐显示器,700-3000价位可选,一定有适合你的显示器 快资讯
- 06-17植物工厂让果蔬定制化种植不是梦 环球关注
- 06-17WNBA | 李梦打出加盟神秘人队最强一战 各项数据创新高
- 06-17我国中风防治有短板!王拥军等BMJ子刊发表全国注册研究
- 06-17全球信息:必要不充分和充分不必要条件 必要不充分和充分不必要
- 06-17乙女类手机游戏推荐 焦点关注
- 06-17焦点快播:荣耀Magic5发布之后荣耀Magic4会不会有降价
- 06-17阿根廷经济部贸易国务秘书马蒂亚斯·通博利尼16日宣布,今年4月和5月,阿根廷使用人民币结算的进口额达该国这两月总进口额的19%
- 06-17热资讯!手机短信连信是什么_联信是什么
- 06-17焦点关注:北京市小学入学材料审核本周末启动
- 06-17专家点评中考作文题:用“慧心”捕捉“会心”的瞬间|2023中考加油
- 06-17汉阳区紫荆花社区开展清廉端午主题活动-当前讯息
- 06-17正财逢双 当前要闻
- 06-17行业大咖齐聚中德产业园 共话汽车产业发展
- 06-17河西区推出83项“一件事一次办”场景式套餐服务
- 06-17全球微速讯:五部门联合启动2023年新能源汽车下乡活动
- 06-17无锡交通违章怎么申诉-焦点快播
- 06-17广州8大园区、13家技术贸易企业亮相上交会
- 06-17大写一二三的书写格式_大写的 ldquo 一二三 hellip 十 rdquo 怎么写_每日速递
- 06-17四川省市场监管局召开食品安全业务培训暨重点工作推进视频会_世界球精选
- 06-17当前热议!服务平台分期贷款做服务,后续没有做服务需要还款吗
- 06-17颜色战士第233章:对战暗黑五战士(上)
- 06-17北漂10年,我用天津、郑州两套大房子换北京一套“老破小” 天天快播
- 06-17吉林四平:厚植沃土,打造农机产业发展高地_微资讯
- 06-17年末产能翻倍!存储龙头拟扩产AI芯片“标配” 刚获英伟达下一代样品请求 全球热头条
- 06-172013年2月15日一颗数十吨的陨石被地球俘获_2013年2月15日俄罗斯陨石坠落事件