前言

通过前两篇文章的叙述,相信能勾起你对 Bean Validation 的兴趣。那么本文就站在一个使用者的角度来看,要使用 Bean Validation 完成校验的话我们应该掌握哪些接口和接口方法呢?

版本约定

  • Bean Validation 版本:2.0.2
  • Hibernate Validator 版本:6.1.5.Final

    正文

Bean Validation 属于 Jakarta EE 标准技术,这里只分析标准接口和方法,不关心具体实现(不管是 Hibernate 实现,还是 Apache 实现),后面会有专门的文章介绍 Hibernate Validator 的一些扩展实现。

为了方便下面做示例讲解,对一些简单、公用的方法抽取如下:

  1. public class ValidatorUtil {
  2. public static ValidatorFactory getValidatorFactory() {
  3. return Validation.buildDefaultValidatorFactory();
  4. }
  5. public static Validator getValidator() {
  6. return obtainValidatorFactory().getValidator();
  7. }
  8. public static ExecutableValidator getExecutableValidator() {
  9. return obtainValidator().forExecutables();
  10. }
  11. public static <T> void printViolations(Set<ConstraintViolation<T>> violations) {
  12. violations.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
  13. }
  14. }

Validator

校验器接口:校验的入口,可实现对 Java Bean、属性、方法、构造器等完成校验。

  1. public interface Validator {
  2. ...
  3. }

Validator 是使用者接触得最多的一个接口,当然也是最重要的,因此下面对其每个方法做出解释和使用示例。

validate:校验 Java Bean

  1. <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);

验证 Java Bean 对象上的所有约束,包括属性和类上的约束,示例如下:

  1. @ScriptAssert(script = "_this.name==_this.fullName", lang = "javascript")
  2. @Data
  3. public class User {
  4. @NotNull
  5. private String name;
  6. @Length(min = 20)
  7. @NotNull
  8. private String fullName;
  9. }
  10. @Test
  11. public void test5() {
  12. User user = new User();
  13. user.setName("YourBatman");
  14. Set<ConstraintViolation<User>> result = ValidatorUtil.getValidator().validate(user);
  15. ValidatorUtil.printViolations(result);
  16. }

@ScriptAssert 是 Hibernate Validator 提供的一个脚本约束注解,可以实现垮字段逻辑校验,功能强大,后面详解。

运行程序,控制台输出:

  1. 执行脚本表达式"_this.name==_this.fullName"没有返回期望结果: User(name=YourBatman, fullName=null)
  2. fullName 不能为null: null

符合预期。值得注意的是:针对 fullName 中的 @Length 约束来说,null 是合法的,所以不会有相应日志输出。

validateProperty:校验指定属性

  1. <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups);

校验 Java Bean 中的某个属性上的所有约束,示例如下:

  1. @Test
  2. public void test6() {
  3. User user = new User();
  4. user.setFullName("YourBatman");
  5. Set<ConstraintViolation<User>> result = ValidatorUtil.getValidator().validateProperty(user, "fullName");
  6. ValidatorUtil.printViolations(result);
  7. }

运行程序,控制台输出:

  1. fullName 长度需要在202147483647之间: YourBatman

validateValue:校验 value 值

校验 value 值,是否符合指定属性上的所有约束。可理解为:若我把这个 value 值赋值给这个属性,是否合法?

  1. <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType,
  2. String propertyName,
  3. Object value,
  4. Class<?>... groups);

这个校验方法比较特殊:不用先存在对象实例,直接校验某个值是否满足某个属性的所有约束,所以它可以做事前校验判断,还是挺好用的。示例如下:

  1. @Test
  2. public void test7() {
  3. Set<ConstraintViolation<User>> result = ValidatorUtil.obtainValidator().validateValue(User.class, "fullName", "A哥");
  4. ValidatorUtil.printViolations(result);
  5. }

运行程序,控制台输出:

  1. fullName 长度需要在202147483647之间: A

若程序改为:.validateValue(User.class, "fullName", "YourBatman-YourBatman");,再次运行程序,控制台将不再输出错误信息(字符串长度超过 20,符合约束要求)。

getConstraintsForClass:获取 Class 类型描述信息

  1. BeanDescriptor getConstraintsForClass(Class<?> clazz);

这个 clazz 可以是类或者接口类型。BeanDescriptor:描述受约束的 Java Bean 和与其关联的约束。示例如下:

  1. @Test
  2. public void test8() {
  3. BeanDescriptor beanDescriptor = obtainValidator().getConstraintsForClass(User.class);
  4. System.out.println("此类是否需要校验:" + beanDescriptor.isBeanConstrained());
  5. // 获取属性、方法、构造器的约束
  6. Set<PropertyDescriptor> constrainedProperties = beanDescriptor.getConstrainedProperties();
  7. Set<MethodDescriptor> constrainedMethods = beanDescriptor.getConstrainedMethods(MethodType.GETTER);
  8. Set<ConstructorDescriptor> constrainedConstructors = beanDescriptor.getConstrainedConstructors();
  9. System.out.println("需要校验的属性:" + constrainedProperties);
  10. System.out.println("需要校验的方法:" + constrainedMethods);
  11. System.out.println("需要校验的构造器:" + constrainedConstructors);
  12. PropertyDescriptor fullNameDesc = beanDescriptor.getConstraintsForProperty("fullName");
  13. System.out.println(fullNameDesc);
  14. System.out.println("fullName属性的约束注解个数:"fullNameDesc.getConstraintDescriptors().size());
  15. }

运行程序,控制台输出:

  1. 此类是否需要校验:true
  2. 需要校验的属性:[PropertyDescriptorImpl{propertyName=name, cascaded=false}, PropertyDescriptorImpl{propertyName=fullName, cascaded=false}]
  3. 需要校验的方法:[]
  4. 需要校验的构造器:[]
  5. PropertyDescriptorImpl{propertyName=fullName, cascaded=false}
  6. fullName属性的约束注解个数:2

forExecutables:获得 Executable 校验器

  1. @since 1.1
  2. ExecutableValidator forExecutables();

Validator 接口在 Bean Validation 1.0 版本就提供了,它只能校验 Java Bean,对于方法、构造器的参数、返回值等校验还无能为力。

在 Bean Validation 1.1 版本提供了 ExecutableValidator 接口用来解决这类需求,可以通过调用 Validator 的 forExecutables 方法获得实例。

关于 ExecutableValidator 的具体使用参考上篇文章:Bean Validation 声明式校验方法的参数和返回值

ConstraintViolation

约束违反详情。此对象保存了违反约束的上下文以及描述消息。

  1. // <T>:root bean
  2. public interface ConstraintViolation<T> {
  3. }

它保存着执行完所有约束后(Java Bean 约束、方法约束等等)的结果,提供了访问结果的方法,比较简单:

  1. // 已经插值(interpolated)的消息
  2. String getMessage();
  3. // 未插值的消息模版(里面变量还未替换,若存在的话)
  4. String getMessageTemplate();
  5. // 从rootBean开始的属性路径。如:parent.fullName
  6. Path getPropertyPath();
  7. // 告诉是哪个约束没有通过(的详情)
  8. ConstraintDescriptor<?> getConstraintDescriptor();

只有违反的约束才会生成此对象,违反一个约束生成一个实例。

ValidatorContext

校验器上下文,根据此上下文创建 Validator 实例。不同的上下文可以创建出不同实例(这里的不同指的是内部组件不同),满足各种个性化的定制需求。

ValidatorContext 接口提供设置方法可以定制校验器的核心组件,它们就是 Validator 校验器的五大核心组件:

  1. public interface ValidatorContext {
  2. ValidatorContext messageInterpolator(MessageInterpolator messageInterpolator);
  3. ValidatorContext traversableResolver(TraversableResolver traversableResolver);
  4. ValidatorContext constraintValidatorFactory(ConstraintValidatorFactory factory);
  5. ValidatorContext parameterNameProvider(ParameterNameProvider parameterNameProvider);
  6. ValidatorContext clockProvider(ClockProvider clockProvider);
  7. // @since 2.0 值提取器。
  8. // 注意:它是add方法,属于添加哦
  9. ValidatorContext addValueExtractor(ValueExtractor<?> extractor);
  10. Validator getValidator();
  11. }

可以通过这些方法设置不同的组件实现,设置好后使用 getValidator() 方法就能得到一个定制化的校验器。所以首先要得到 ValidatorContext 实例,下面介绍两种方法。

方式一:自己 new

  1. @Test
  2. public void test2() {
  3. ValidatorFactoryImpl validatorFactory = (ValidatorFactoryImpl) ValidatorUtil.obtainValidatorFactory();
  4. // 使用默认的Context上下文,并且初始化一个Validator实例
  5. // 必须传入一个校验器工厂实例哦
  6. ValidatorContext validatorContext = new ValidatorContextImpl(validatorFactory)
  7. .parameterNameProvider(new DefaultParameterNameProvider())
  8. .clockProvider(DefaultClockProvider.INSTANCE);
  9. // 通过该上下文,生成校验器实例(注意:调用多次,生成实例是多个哟)
  10. System.out.println(validatorContext.getValidator());
  11. }

运行程序,控制台输出:

  1. org.hibernate.validator.internal.engine.ValidatorImpl@1757cd72

这种是最直接的方式,不过这么使用是有缺陷的,主要体现在两个方面:

  1. 不够抽象,new 的方式和抽象谈不上关系;
  2. 强耦合了 Hibernate Validator 的类,如:org.hibernate.validator.internal.engine.ValidatorContextImpl#ValidatorContextImpl

    方式二:工厂生成

使用校验器工厂(ValidatorFactory)生成 ValidatorContext 实例:

  1. ValidatorContext usingContext();

该方法用于得到一个 ValidatorContext 实例,它具有高度抽象、与底层实现无关的特点,是推荐的获取方式,并且使用起来有流式编程的效果,如下所示:

  1. @Test
  2. public void test3() {
  3. Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext()
  4. .parameterNameProvider(new DefaultParameterNameProvider())
  5. .clockProvider(DefaultClockProvider.INSTANCE)
  6. .getValidator();
  7. }

获得 Validator 实例的两种方式

方式一:工厂生成

  1. @Test
  2. public void test3() {
  3. Validator validator = ValidatorUtil.obtainValidatorFactory().getValidator();
  4. }

这种方式简单明了,对使用者友好,内部五大组件全部使用默认组件,无法使用自定义组件。

方式二:上下文生成

校验器上下文也就是 ValidatorContext,它的步骤是先得到上下文实例,然后做定制,再通过上下文实例创建出 Validator 校验器实例。

  1. @Test
  2. public void test3() {
  3. Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext()
  4. .parameterNameProvider(new DefaultParameterNameProvider())
  5. .clockProvider(DefaultClockProvider.INSTANCE)
  6. .getValidator();
  7. }

这种方式可以使用自定义组件,来实现一些特殊的要求,推荐使用方式二进行初始化,对个性化扩展更友好。

总结

本文站在一个使用者的角度去看如何使用 Bean Validation,以及哪些标准接口和方法是必须掌握的,有了这些知识点在平时绝大部分 case 都能应对自如了。

要想深入理解 Bean Validation 的功能,必须深入了解 Hibernate Validator 实现,因为有些比较常用的 case 它做了很好的补充,咱们下文见。

转载

打个广告,方便的话,可以关注一下 A哥(YourBatman) 的公众号。
image.png

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/mg69yu 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。