“我正在参加「启航方案」”
随着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 是一个空的完成。
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 完成方中去。主要内容如下。
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 <T> Set<ConstraintViolation<T>> 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 实例。
@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,
ObjectProvider<ValidationConfigurationCustomizer> customizers) {
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。
@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
符号的参数就由它解析,主体逻辑如下所示。
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<ConstraintViolation<T>> validate(T object, Class<?>... groups)
办法中去的,但是这个办法是不支持容器元素检验的,只需 ExecutableValidator 才具有这一才能!
剖析到这里,终于知道为什么下面这种面向容器元素的校验无法收效了,如下:
@RestController
@RequestMapping(path = "/user/v1")
public class UserController {
@PostMapping()
public ResponseEntity<String> createUser(@Validated or @Valid @RequestBody List<User> users) {
return ResponseEntity.ok("ojbk");
}
}
但只需求像下面这番小改造,List 中的每个 user 实例都能够得到校验。
@Validated
@RestController
@RequestMapping(path = "/user/v1")
public class UserController {
@PostMapping()
public ResponseEntity<String> createUser(@RequestBody List<@Valid User> users) {
return ResponseEntity.ok("ojbk");
}
}
已然 List 中的每个 user 实例都能够得到校验,那说明一定是走到 ExecutableValidator 的Set<ConstraintViolation<T>> validateParameters()
办法中去了。
咱们继续向下探究。其实 ValidationAutoConfiguration 不只是是声明晰一个 LocalValidatorFactoryBean,一起还声明晰一个MethodValidationPostProcessor
,如下所示。
@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,
ObjectProvider<Validator> validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
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 中一探终究。
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(Supplier<Validator> validator) {
return new MethodValidationInterceptor(validator);
}
}
这块主要是 Spring AOP 中的知识了。PointcutAdvisor
是 Spring 中的切面,它由Pointcut
和Advice
组成,前者用于决策应该在哪一连接点附件织入切面逻辑,后者则承载了具体的切面逻辑。AnnotationMatchingPointcut
告诉咱们一个事实:只需某一个类的头上符号有@Validated
注解,那么就应该织入切面逻辑,而切面逻辑就在MethodValidationInterceptor
中。
public class MethodValidationInterceptor implements MethodInterceptor {
private final Supplier<Validator> validator;
@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<ConstraintViolation<Object>> 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
机制的,能够经过下面这种方法去开启。
@Component
public class FailFastValidationConfigurationCustomizer implements ValidationConfigurationCustomizer {
@Override
public void customize(Configuration<?> configuration) {
configuration.addProperty("hibernate.validator.fail_fast", "true");
}
}
总结
读完本文,咱们能说出@Validated
注解与@Valid
注解的区别吗?