为什么要有数据校验
传统的if-else判断参数是否合法的方法存在以下几个问题:
- 需要写大量的代码来进行参数基本验证;
- 需要通过文字注释来知道每个入参的约束是什么;
- 每个程序员的参数验证方式可能不一样,参数验证抛出的异常也不一样,导致后期几乎无法维护;
如上会导致代码冗余和一些管理的问题,最好是将验证逻辑与相应域模型进行绑定。
Bean Validation是标准,它的参考实现除了Hibernate Validator
外还有Apcache BVal
。
核心API分析
Validation
它作为校验的入口,有三种方式来启动:
- 使用默认的
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
,芮然是默认的但也会有两种情况:
- 若使用xml配置一个provider,那就会使用这个provider来提供Factory;
- 若没有xml或者xml里没有配置provider,那就用默认的
ValidationProviderResolver
实现类来处理。
- 选择自定义的
ValidationProviderResolver
来跟xml配置逻辑选出一个ValidationProvider
来。 - 直接提供一个类型安全的
ValidationProvider
实现。
值得注意的是ValidatorFactory被创建后应该缓存起来再提供使用,因为它是线程安全的。
HibernateValidatorConfiguration
此接口表示配置,继承自标注接口javax.validation.Configuration
。
先看顶级接口javax.validation.Configuration
,为构建ValidatorFactory
的配置类,默认情况下它会读取配置文件META-INF/validation.xml
,Configuration提供的API方法是覆盖xml配置文件项的,若没有找到validation.xml,就会使用默认的ValidationProviderResolver
也就是:DefaultValidationProviderResolver
。
public interface Configuration<T extends Configuration<T>> {
// 调用此方法则不从validation.xml中获取配置信息
T ignoreXmlConfiguration();
// 定义使用的消息内插器,如果为null则使用默认的或者从xml中获取的,否则会覆盖从xml获取的
T messageInterpolator(MessageInterpolator interpolator);
// 确定bean验证提供程序是否可以访问属性的协定。对每个正在验证或级联的属性调用此约定。(Spring木有实现它)
// 对每个正在验证或级联的属性都会调用此约定
// Traversable: 可移动的
T traversableResolver(TraversableResolver resolver);
// 创建ConstraintValidator的工厂
// ConstraintValidator:定义逻辑以验证给定对象类型T的给定约束A。(A是个注解类型)
T constraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory);
// ParameterNameProvider:提供Constructor/Method的方法名们
T parameterNameProvider(ParameterNameProvider parameterNameProvider);
// java.time.Clock 用作判定@Future和@Past(默认取值当前时间)
// 若你希望他是个逻辑实现,提供一个它即可
// @since 2.0
T clockProvider(ClockProvider clockProvider);
// 值提取器。这是add哦~ 负责从Optional、List等这种容器里提取值~
// @since 2.0
T addValueExtractor(ValueExtractor<?> extractor);
// 加载xml文件
T addMapping(InputStream stream);
// 添加特定的属性给Provider用的。此属性等效于XML配置属性。
// 此方法通常是框架自己分析xml文件得到属性值然后放进去,调用者一般不使用(当然也可以用)
T addProperty(String name, String value);
// 下面都是get方法喽
MessageInterpolator getDefaultMessageInterpolator();
TraversableResolver getDefaultTraversableResolver();
ConstraintValidatorFactory getDefaultConstraintValidatorFactory();
ParameterNameProvider getDefaultParameterNameProvider();
ClockProvider getDefaultClockProvider();
BootstrapConfiguration getBootstrapConfiguration(); // 整个配置也可返回出去
// 上面都是工作,这个方法才是最终需要调用的:得到一个ValidatorFactory
ValidatorFactory buildValidatorFactory();
}
该接口提供了一些标准的配置项,在实际应用中都是使用Hibernate validation
。
Bean Validation标准接口的使用
Validator
工具类,获取校验器
public abstract class ValidatorUtil {
public static ValidatorFactory obtainValidatorFactory() {
return Validation.buildDefaultValidatorFactory();
}
public static Validator obtainValidator() {
return obtainValidatorFactory().getValidator();
}
public static ExecutableValidator obtainExecutableValidator() {
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);
}
validate:校验Java Bean
@Data
@ScriptAssert(script = "_this.name==_this.fullName", lang = "javascript")
public class User {
@NotBlank(message = "name can not null")
private String name;
@DecimalMax(value = "100",message = "age can more than 100")
private Integer age;
@Length(min=20 ,message = "fullName length less then 20")
@NotBlank(message = "full name can not null")
private String fullName;
}
@Test
public void test1(){
User user = new User();
user.setName("Hello");
user.setAge(200);
user.setFullName("123456");
Set<ConstraintViolation<User>> validate = ValidatorUtil.obtainValidator().validate(user);
ValidatorUtil.printViolations(validate);
}
针对@Length
约束,null是合法的,不会触发。
validateProperty 校验指定属性
@Test
public void test2(){
User user = new User();
user.setName("Hello");
user.setAge(200);
user.setFullName("123456");
// 指定校验fullName
Set<ConstraintViolation<User>> validate = ValidatorUtil.obtainValidator().validateProperty(user,"fullName");
ValidatorUtil.printViolations(validate);
// 指定校验age
Set<ConstraintViolation<User>> validateAge = ValidatorUtil.obtainValidator().validateProperty(user,"age");
ValidatorUtil.printViolations(validateAge);
}
符合预期,它会校验属性上的所有约束。
validateValue 校验value值
校验某个value值是否符合指定属性上的所有约束,可理解为,如果把这个value值赋值给这个属性是否合法。并且不用先存在对象实例,直接校验某个值是否满足某个属性的所有约束,可以做事前校验。
<T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType,
String propertyName,
Object value,
Class<?>... groups);
@Test
public void test3(){
Set<ConstraintViolation<User>> fullName = ValidatorUtil.obtainValidator().validateValue(User.class, "fullName", "123");
ValidatorUtil.printViolations(fullName);
}
获取class类型描述信息
BeanDescriptor getConstraintsForClass(Class<?> clazz);
@Test
public void test4(){
BeanDescriptor beanDescriptor = ValidatorUtil.obtainValidator().getConstraintsForClass(User.class);
System.out.println("此类是否需要校验:"+beanDescriptor.isBeanConstrained());
System.out.println("需要校验的属性:"+beanDescriptor.getConstrainedProperties());
System.out.println("需要校验的方法:"+beanDescriptor.getConstrainedMethods(MethodType.GETTER));
System.out.println("需要校验的构造器:"+beanDescriptor.getConstrainedConstructors());
PropertyDescriptor fullName = beanDescriptor.getConstraintsForProperty("fullName");
System.out.println("fullName属性的约束注解个数 " + fullName.getConstraintDescriptors().size());
}
获得Executable校验器
Validator只能校验java bean 对于方法、构造器的参数、返回值等校验无能为力。
所以,1.1版本提供了ExecutableValidator
这个API解决这类需求,它的实例可以通过调用Validator的该方法获得。
ConstrainViolation
约束违反详情,此对象保存了违反约束的上下文以及描述信息。
public interface ConstraintViolation<T> {
}
只有违反的约束才会生成此对象,违反一个约束对应一个实例。
// 已经插值(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 test5() {
ValidatorFactoryImpl validatorFactory = (ValidatorFactoryImpl) ValidatorUtil.obtainValidatorFactory();
ValidatorContext validatorContext = new ValidatorContextImpl(validatorFactory)
.parameterNameProvider(new DefaultParameterNameProvider())
.clockProvider(DefaultClockProvider.INSTANCE);
System.out.println(validatorContext.getValidator());
}
虽然这种方式比较直接,但是这么使用是有缺陷的,体现在下面两个方面:
- 不够抽象;
- 强耦合了Hibernate Validator 的API。
- 工厂生成
@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();
}
默认方式获取。
从上下文获取
@Test
public void test3() {
Validator validator = ValidatorUtil.obtainValidatorFactory().usingContext()
.parameterNameProvider(new DefaultParameterNameProvider())
.clockProvider(DefaultClockProvider.INSTANCE)
.getValidator();
}
可以任意指定核心组件实现,具有很强的扩展性。
Validator校验器的五大核心组件
MessageInterpolator 消息插值器
简单来说就是对message内容进行格式化,若有占位符{ }
或el表达式${ }
就执行替换和计算。
Hibernate Validation它使用的是ResourceBundleMessageInterpolator来既支持参数,也支持EL表达式。
Hibernate Validation它使用的是ResourceBundleMessageInterpolator来既支持参数,也支持EL表达式。内部使用了「javax.el.ExpressionFactory」
这个API来支持EL表达式${}
的,形如这样:must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
它是能够动态计算出${inclusive == true ? 'or equal to ' : ''}
这部分的值的。
TraversableResolver 跨越处理器
确定某个属性是否能被ValidationProvider访问,每访问一个属性时都会通过它来判断一下,提供两个判断方法:
public interface TraversableResolver {
// 是否是可达的
boolean isReachable(Object traversableObject,
Node traversableProperty,
Class<?> rootBeanType,
Path pathToTraversableObject,
ElementType elementType);
// 是否是可级联的(是否标注有@Valid注解)
boolean isCascadable(Object traversableObject,
Node traversableProperty,
Class<?> rootBeanType,
Path pathToTraversableObject,
ElementType elementType);
}
该接口主要根据配置项来进行判断,并不负责内部使用。
ConstrainValidatorFactory 约束校验工厂
每个约束注解都得指定一个或多个约束校验器。
public interface ConstraintValidatorFactory {
// 生成实例:接口并不规定你的生成方式
<T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key);
// 释放实例。标记此实例不需要再使用,一般为空实现
// 和Spring容器集成时 .destroyBean(instance)时会调用此方法
void releaseInstance(ConstraintValidator<?, ?> instance);
}
Hibernate 提供了唯一实现 ConstrainValidatorFactoryImpl,使用空构造器生成实例clazz.getConstructor().newInstance()
。
ParameterNameProvider 参数名提供器
public interface ParameterNameProvider {
List<String> getParameterNames(Constructor<?> constructor);
List<String> getParameterNames(Method method);
}
ClockProvider 时钟提供器
提供一个Clock,给@Past、@Future
等阅读判断提供参考,唯一实现为DefaultClockProvider。
public class DefaultClockProvider implements ClockProvider {
public static final DefaultClockProvider INSTANCE = new DefaultClockProvider();
private DefaultClockProvider() {
}
// 默认是系统时钟
@Override
public Clock getClock() {
return Clock.systemDefaultZone();
}
}
ValueExtractor 值提取器
把值从容器内提取出来,这里的容器包括数组、集合、Map、Optional等。
// T:待提取的容器类型
public interface ValueExtractor<T> {
// 从原始值originalValue提取到receiver里
void extractValues(T originalValue, ValueReceiver receiver);
// 提供一组方法,用于接收ValueExtractor提取出来的值
interface ValueReceiver {
// 接收从对象中提取的值
void value(String nodeName, Object object);
// 接收可以迭代的值,如List、Map、Iterable等
void iterableValue(String nodeName, Object object);
// 接收有索引的值,如List Array
// i:索引值
void indexedValue(String nodeName, int i, Object object);
// 接收键值对的值,如Map
void keyedValue(String nodeName, Object key, Object object);
}
}
Bean Valiadation 声明式验证四大级别:字段、属性、容器元素、类
「Jakarta Bean」共支持四个级别的约束:
- 字段约束(Field)
- 属性约束(Property)
- 容器「元素」约束(Container Element)
- 类约束(Class)
Bean Validation 自带的22个标准约束全部支持1、2、3级别,且全部不支持第4级别。
字段级别约束
public class Room {
@NotNull
public String name;
@AssertTrue
public boolean finished;
}
当把约束标注在Field字段上时,Bean Validation将使用字段的访问策略来校验,不会调用任何方法。
- 字段约束可以应用于任何访问修饰符的字段;
- 不支持对静态字段的约束。
属性级别约束
public class Room {
public String name;
public boolean finished;
@NotNull
public String getName() {
return name;
}
@AssertTrue
public boolean isFinished() {
return finished;
}
}
- 约束放在get上优于放在set上,这样只读属性依然可以执行约束逻辑;
- 不要在属性和字段上都标注注解,否则会重复执行逻辑。
- 不要既在get方法又在set方法上标注约束。
容器元素级别约束
有一种非常常见的验证场景,验证容器内元素,也就是验证参数化类型。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Rooms {
private List<@Valid @NotNull Room> rooms;
}
public static void main(String[] args) {
List<@NotNull Room> beans = new ArrayList<>();
beans.add(null);
beans.add(new Room());
Room room = new Room();
room.name = "YourBatman";
beans.add(room);
// 必须基于Java Bean,验证才会生效
Rooms rooms = new Rooms(beans);
ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms));
}
使用细节
- 若约束注解想标注在容器元素上,那么注解定义的
@Target
里必须包含TYPE_USE
这个类型;
类级别约束
Hibernate-Validator已内置提供一部分能力,但可能还不够。