Bean Validation规范篇----03

x33g5p2x  于2022-07-26 转载在 其他  
字(7.6k)|赞(0)|评价(0)|浏览(499)

引言

Bean Validation属于Java EE标准技术,拥有对应的JSR抽象,因此我们实际使用过程中仅需要面向标准使用即可,并不需要关心具体实现(是hibernate实现,还是apache的实现并不重要),也就是我们常说的面向接口编程。

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

  1. public abstract class ValidatorUtil {
  2. public static ValidatorFactory obtainValidatorFactory() {
  3. return Validation.buildDefaultValidatorFactory();
  4. }
  5. public static Validator obtainValidator() {
  6. return obtainValidatorFactory().getValidator();
  7. }
  8. public static ExecutableValidator obtainExecutableValidator() {
  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. //属于JSR规范接口---可以类比为日志框架中的Slf4j门面
  2. package javax.validation;
  3. public interface Validator {
  4. ...
  5. }

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

下面对Validator接口中每个方法进行介绍:

validate:校验Java Bean

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

验证Java Bean对象上的所有约束。示例如下:

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 = 5)
  7. @NotNull
  8. private String fullName;
  9. }
  10. @Test
  11. public void test5() {
  12. User user = new User();
  13. user.setName("大忽悠");
  14. user.setFullName("11");
  15. Set<ConstraintViolation<User>> result = ValidatorUtil.obtainValidator().validate(user);
  16. ValidatorUtil.printViolations(result);
  17. }

执行结果:

  1. 执行脚本表达式"_this.name==_this.fullName"没有返回期望结果: TestDataBinder.User(name=大忽悠, fullName=11)
  2. fullName 长度需要在52147483647之间: 11

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

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

校验Java Bean所有约束中的所有包括:

1、属性上的约束

2、类上的约束

validateProperty:校验指定属性

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

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

  1. @Test
  2. public void test5() {
  3. User user = new User();
  4. user.setName("大忽悠");
  5. user.setFullName("11");
  6. Set<ConstraintViolation<User>> result = ValidatorUtil.obtainValidator().validateProperty(user,"fullName");
  7. ValidatorUtil.printViolations(result);
  8. }

输出结果:

  1. fullName 长度需要在52147483647之间: 11

符合预期。它会校验属性上的所有约束,注意只是属性上的哦,其它地方的不管。

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 test5() {
  3. Set<ConstraintViolation<User>> result = ValidatorUtil.obtainValidator().validateValue(User.class,"fullName","1111");
  4. ValidatorUtil.printViolations(result);
  5. }

输出:

  1. fullName 长度需要在52147483647之间: 1111

获取Class类型描述信息

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

这个clazz可以是类or接口类型。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=fullName, cascaded=false}, PropertyDescriptorImpl{propertyName=name, cascaded=false}]
  3. 需要校验的方法:[ExecutableDescriptorImpl{name='getName'}, ExecutableDescriptorImpl{name='getFullName'}]
  4. 需要校验的构造器:[]
  5. PropertyDescriptorImpl{propertyName=fullName, cascaded=false}
  6. fullName属性的约束注解个数:2

获得Executable校验器

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

Validator这个API是1.0就提出的,它只能校验Java Bean,对于方法、构造器的参数、返回值等校验还无能为力。

这不1.1版本就提供了ExecutableValidator这个API解决这类需求,它的实例可通过调用Validator的该方法获得,非常方便。

ConstraintViolation

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

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

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

小贴士:只有违反的约束才会生成此对象哦。违反一个约束对应一个实例

  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

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

  • 不够抽象。new的方式嘛,和抽象谈不上关系
  • 强耦合了Hibernate Validator的API,如:org.hibernate.validator.internal.engine.ValidatorContextImpl#ValidatorContextImpl

方式二:工厂生成

上面即使通过自己new的方式得到ValidatorContext实例也需要传入校验器工厂,那还不如直接使用工厂生成呢。恰好ValidatorFactory也提供了对应的方法:

  1. alidatorContext usingContext();

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

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

获得Validator实例的两种姿势

在文章最后,再回头看看Validator实例获取的两种姿势。

Validator校验器接口是完成数据校验(Java Bean校验、方法校验等)最主要API,经过了上面的讲述,下面可以来个获取方式的小总结了。

方式一:工厂直接获取

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

这种方式十分简单、简约,对初学者十分的友好,入门简单,优点明显。各组件全部使用默认方式,省心。如果要挑缺点那肯定也是有的:无法满足个性化、定制化需求,说白了:无法自定义五大组件 + 值提取器的实现。

作为这么优秀的Java EE标准技术,怎么少得了对扩展的开放呢?继续方式二吧~

方式二:从上下文获取

校验器上下文也就是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. }

这种方式给与了极大的定制性,你可以任意指定核心组件实现,来达到自己的要求。

这两种方式结合起来,不就是典型的默认 + 定制扩展的搭配么?另外,Validator是线程安全的,一般来说一个应用只需要初始化一个 Validator实例即可,所以推荐使用方式二进行初始化,对个性扩展更友好。

小结

本文站在一个使用者的角度去看如何使用Bean Validation,以及哪些标准的接口API是必须掌握了,有了这些知识点在平时绝大部分case都能应对自如了。
规范接口/标准接口一般能解决绝大多数问题,这就是规范的边界,有些可为,有些不为

参考

3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸

相关文章