前言
通过前两篇文章的叙述,相信能勾起你对 Bean Validation 的兴趣。那么本文就站在一个使用者的角度来看,要使用 Bean Validation 完成校验的话我们应该掌握哪些接口和接口方法呢?
版本约定
Bean Validation 属于 Jakarta EE 标准技术,这里只分析标准接口和方法,不关心具体实现(不管是 Hibernate 实现,还是 Apache 实现),后面会有专门的文章介绍 Hibernate Validator 的一些扩展实现。
为了方便下面做示例讲解,对一些简单、公用的方法抽取如下:
public class ValidatorUtil {
public static ValidatorFactory getValidatorFactory() {
return Validation.buildDefaultValidatorFactory();
}
public static Validator getValidator() {
return obtainValidatorFactory().getValidator();
}
public static ExecutableValidator getExecutableValidator() {
return obtainValidator().forExecutables();
}
public static <T> void printViolations(Set<ConstraintViolation<T>> violations) {
violations.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}
}
Validator
校验器接口:校验的入口,可实现对 Java Bean、属性、方法、构造器等完成校验。
public interface Validator {
...
}
Validator 是使用者接触得最多的一个接口,当然也是最重要的,因此下面对其每个方法做出解释和使用示例。
validate:校验 Java Bean
<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);
验证 Java Bean 对象上的所有约束,包括属性和类上的约束,示例如下:
@ScriptAssert(script = "_this.name==_this.fullName", lang = "javascript")
@Data
public class User {
@NotNull
private String name;
@Length(min = 20)
@NotNull
private String fullName;
}
@Test
public void test5() {
User user = new User();
user.setName("YourBatman");
Set<ConstraintViolation<User>> result = ValidatorUtil.getValidator().validate(user);
ValidatorUtil.printViolations(result);
}
@ScriptAssert 是 Hibernate Validator 提供的一个脚本约束注解,可以实现垮字段逻辑校验,功能强大,后面详解。
运行程序,控制台输出:
执行脚本表达式"_this.name==_this.fullName"没有返回期望结果: User(name=YourBatman, fullName=null)
fullName 不能为null: null
符合预期。值得注意的是:针对 fullName 中的 @Length 约束来说,null 是合法的,所以不会有相应日志输出。
validateProperty:校验指定属性
<T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups);
校验 Java Bean 中的某个属性上的所有约束,示例如下:
@Test
public void test6() {
User user = new User();
user.setFullName("YourBatman");
Set<ConstraintViolation<User>> result = ValidatorUtil.getValidator().validateProperty(user, "fullName");
ValidatorUtil.printViolations(result);
}
运行程序,控制台输出:
fullName 长度需要在20和2147483647之间: YourBatman
validateValue:校验 value 值
校验 value 值,是否符合指定属性上的所有约束。可理解为:若我把这个 value 值赋值给这个属性,是否合法?
<T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType,
String propertyName,
Object value,
Class<?>... groups);
这个校验方法比较特殊:不用先存在对象实例,直接校验某个值是否满足某个属性的所有约束,所以它可以做事前校验判断,还是挺好用的。示例如下:
@Test
public void test7() {
Set<ConstraintViolation<User>> result = ValidatorUtil.obtainValidator().validateValue(User.class, "fullName", "A哥");
ValidatorUtil.printViolations(result);
}
运行程序,控制台输出:
fullName 长度需要在20和2147483647之间: A哥
若程序改为:.validateValue(User.class, "fullName", "YourBatman-YourBatman");
,再次运行程序,控制台将不再输出错误信息(字符串长度超过 20,符合约束要求)。
getConstraintsForClass:获取 Class 类型描述信息
BeanDescriptor getConstraintsForClass(Class<?> clazz);
这个 clazz 可以是类或者接口类型。BeanDescriptor:描述受约束的 Java Bean 和与其关联的约束。示例如下:
@Test
public void test8() {
BeanDescriptor beanDescriptor = obtainValidator().getConstraintsForClass(User.class);
System.out.println("此类是否需要校验:" + beanDescriptor.isBeanConstrained());
// 获取属性、方法、构造器的约束
Set<PropertyDescriptor> constrainedProperties = beanDescriptor.getConstrainedProperties();
Set<MethodDescriptor> constrainedMethods = beanDescriptor.getConstrainedMethods(MethodType.GETTER);
Set<ConstructorDescriptor> constrainedConstructors = beanDescriptor.getConstrainedConstructors();
System.out.println("需要校验的属性:" + constrainedProperties);
System.out.println("需要校验的方法:" + constrainedMethods);
System.out.println("需要校验的构造器:" + constrainedConstructors);
PropertyDescriptor fullNameDesc = beanDescriptor.getConstraintsForProperty("fullName");
System.out.println(fullNameDesc);
System.out.println("fullName属性的约束注解个数:"fullNameDesc.getConstraintDescriptors().size());
}
运行程序,控制台输出:
此类是否需要校验:true
需要校验的属性:[PropertyDescriptorImpl{propertyName=name, cascaded=false}, PropertyDescriptorImpl{propertyName=fullName, cascaded=false}]
需要校验的方法:[]
需要校验的构造器:[]
PropertyDescriptorImpl{propertyName=fullName, cascaded=false}
fullName属性的约束注解个数:2
forExecutables:获得 Executable 校验器
@since 1.1
ExecutableValidator forExecutables();
Validator 接口在 Bean Validation 1.0 版本就提供了,它只能校验 Java Bean,对于方法、构造器的参数、返回值等校验还无能为力。
在 Bean Validation 1.1 版本提供了 ExecutableValidator 接口用来解决这类需求,可以通过调用 Validator 的 forExecutables 方法获得实例。
关于 ExecutableValidator 的具体使用参考上篇文章:Bean Validation 声明式校验方法的参数和返回值
ConstraintViolation
约束违反详情。此对象保存了违反约束的上下文以及描述消息。
// <T>:root bean
public interface ConstraintViolation<T> {
}
它保存着执行完所有约束后(Java Bean 约束、方法约束等等)的结果,提供了访问结果的方法,比较简单:
// 已经插值(interpolated)的消息
String getMessage();
// 未插值的消息模版(里面变量还未替换,若存在的话)
String getMessageTemplate();
// 从rootBean开始的属性路径。如:parent.fullName
Path getPropertyPath();
// 告诉是哪个约束没有通过(的详情)
ConstraintDescriptor<?> getConstraintDescriptor();
ValidatorContext
校验器上下文,根据此上下文创建 Validator 实例。不同的上下文可以创建出不同实例(这里的不同指的是内部组件不同),满足各种个性化的定制需求。
ValidatorContext 接口提供设置方法可以定制校验器的核心组件,它们就是 Validator 校验器的五大核心组件:
public interface ValidatorContext {
ValidatorContext messageInterpolator(MessageInterpolator messageInterpolator);
ValidatorContext traversableResolver(TraversableResolver traversableResolver);
ValidatorContext constraintValidatorFactory(ConstraintValidatorFactory factory);
ValidatorContext parameterNameProvider(ParameterNameProvider parameterNameProvider);
ValidatorContext clockProvider(ClockProvider clockProvider);
// @since 2.0 值提取器。
// 注意:它是add方法,属于添加哦
ValidatorContext addValueExtractor(ValueExtractor<?> extractor);
Validator getValidator();
}
可以通过这些方法设置不同的组件实现,设置好后使用 getValidator() 方法就能得到一个定制化的校验器。所以首先要得到 ValidatorContext 实例,下面介绍两种方法。
方式一:自己 new
@Test
public void test2() {
ValidatorFactoryImpl validatorFactory = (ValidatorFactoryImpl) ValidatorUtil.obtainValidatorFactory();
// 使用默认的Context上下文,并且初始化一个Validator实例
// 必须传入一个校验器工厂实例哦
ValidatorContext validatorContext = new ValidatorContextImpl(validatorFactory)
.parameterNameProvider(new DefaultParameterNameProvider())
.clockProvider(DefaultClockProvider.INSTANCE);
// 通过该上下文,生成校验器实例(注意:调用多次,生成实例是多个哟)
System.out.println(validatorContext.getValidator());
}
运行程序,控制台输出:
org.hibernate.validator.internal.engine.ValidatorImpl@1757cd72
这种是最直接的方式,不过这么使用是有缺陷的,主要体现在两个方面:
- 不够抽象,new 的方式和抽象谈不上关系;
- 强耦合了 Hibernate Validator 的类,如:org.hibernate.validator.internal.engine.ValidatorContextImpl#ValidatorContextImpl
方式二:工厂生成
使用校验器工厂(ValidatorFactory)生成 ValidatorContext 实例:
ValidatorContext usingContext();
该方法用于得到一个 ValidatorContext 实例,它具有高度抽象、与底层实现无关的特点,是推荐的获取方式,并且使用起来有流式编程的效果,如下所示:
@Test
public void test3() {
Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext()
.parameterNameProvider(new DefaultParameterNameProvider())
.clockProvider(DefaultClockProvider.INSTANCE)
.getValidator();
}
获得 Validator 实例的两种方式
方式一:工厂生成
@Test
public void test3() {
Validator validator = ValidatorUtil.obtainValidatorFactory().getValidator();
}
这种方式简单明了,对使用者友好,内部五大组件全部使用默认组件,无法使用自定义组件。
方式二:上下文生成
校验器上下文也就是 ValidatorContext,它的步骤是先得到上下文实例,然后做定制,再通过上下文实例创建出 Validator 校验器实例。
@Test
public void test3() {
Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext()
.parameterNameProvider(new DefaultParameterNameProvider())
.clockProvider(DefaultClockProvider.INSTANCE)
.getValidator();
}
这种方式可以使用自定义组件,来实现一些特殊的要求,推荐使用方式二进行初始化,对个性化扩展更友好。
总结
本文站在一个使用者的角度去看如何使用 Bean Validation,以及哪些标准接口和方法是必须掌握的,有了这些知识点在平时绝大部分 case 都能应对自如了。
要想深入理解 Bean Validation 的功能,必须深入了解 Hibernate Validator 实现,因为有些比较常用的 case 它做了很好的补充,咱们下文见。
转载
打个广告,方便的话,可以关注一下 A哥(YourBatman) 的公众号。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/mg69yu 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。