Spring:全面拥抱 Jakarta Bean Validation 规范

2023-09-02 10:58:53 浏览数 (2)

随着JSR-303JSR-349JSR-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 接口来自定义一个容器元素抽取器,然后通过ConfigurationaddValueExtractor()方法注册自定义 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 的顶级抽象接口,它有两个直系子类,分别是SmartValidatorNoOpValidator,SmartValidator 具备分组校验的能力,其 validate() 方法中第三个参数Object... validationHints就是和分组校验相关的,而 NoOpValidator 是一个空的实现。

代码语言:javascript复制
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 实现方中去。主要内容如下。

代码语言:javascript复制
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。这一点可以通过ValidationAutoConfigurationWebMvcAutoConfiguration源码来验证,注意:在 WebMvcAutoConfiguration 头上标有@AutoConfiguration(after = {ValidationAutoConfiguration.class})

ValidationAutoConfiguration 关于 LocalValidatorFactoryBean 的声明逻辑如下,大家可以通过 defaultValidator 这一 bean 名称来手动获取该 LocalValidatorFactoryBean 实例。

代码语言:javascript复制
@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。

代码语言:javascript复制
@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标记的参数就由它解析,主体逻辑如下所示。

代码语言:javascript复制
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 才具备这一能力!

分析到这里,终于知道为什么下面这种面向容器元素的校验无法生效了,如下:

代码语言:javascript复制
@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 实例都可以得到校验。

代码语言:javascript复制
@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,如下所示。

代码语言:javascript复制
@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 中一探究竟。

代码语言:javascript复制
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 中的切面,它由PointcutAdvice组成,前者用于决策应该在哪一连接点附件织入切面逻辑,后者则承载了具体的切面逻辑。AnnotationMatchingPointcut告诉我们一个事实:只要某一个类的头上标记有@Validated注解,那么就应该织入切面逻辑,而切面逻辑就在MethodValidationInterceptor中。

代码语言:javascript复制
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机制的,可以通过下面这种方式去开启。

代码语言:javascript复制
@Component
public class FailFastValidationConfigurationCustomizer implements ValidationConfigurationCustomizer {
    @Override
    public void customize(Configuration<?> configuration) {
        configuration.addProperty("hibernate.validator.fail_fast", "true");
    }
}

总结

读完本文,大家能说出@Validated注解与@Valid注解的区别吗?

0 人点赞