为什么要有数据校验

传统的if-else判断参数是否合法的方法存在以下几个问题:

  1. 需要写大量的代码来进行参数基本验证;
  2. 需要通过文字注释来知道每个入参的约束是什么;
  3. 每个程序员的参数验证方式可能不一样,参数验证抛出的异常也不一样,导致后期几乎无法维护;

如上会导致代码冗余和一些管理的问题,最好是将验证逻辑与相应域模型进行绑定。

Bean Validation是标准,它的参考实现除了Hibernate Validator外还有Apcache BVal

核心API分析

Validation

它作为校验的入口,有三种方式来启动:

  1. 使用默认的ValidatorFactory factory = Validation.buildDefaultValidatorFactory();,芮然是默认的但也会有两种情况:
  1. 若使用xml配置一个provider,那就会使用这个provider来提供Factory;
  2. 若没有xml或者xml里没有配置provider,那就用默认的ValidationProviderResolver实现类来处理。
  1. 选择自定义的ValidationProviderResolver来跟xml配置逻辑选出一个ValidationProvider来。
  2. 直接提供一个类型安全的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);
    }

java validator 使用 java中validation_java


针对@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);
    }

java validator 使用 java中validation_java_02

获取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());
    }

java validator 使用 java中validation_ide_03

获得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());
    }

虽然这种方式比较直接,但是这么使用是有缺陷的,体现在下面两个方面:

  1. 不够抽象;
  2. 强耦合了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将使用字段的访问策略来校验,不会调用任何方法。

  1. 字段约束可以应用于任何访问修饰符的字段;
  2. 不支持对静态字段的约束。

属性级别约束

public class Room {

    public String name;
    public boolean finished;

    @NotNull
    public String getName() {
        return name;
    }

    @AssertTrue
    public boolean isFinished() {
        return finished;
    }
}
  1. 约束放在get上优于放在set上,这样只读属性依然可以执行约束逻辑;
  2. 不要在属性和字段上都标注注解,否则会重复执行逻辑。
  3. 不要既在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));
}

使用细节

  1. 若约束注解想标注在容器元素上,那么注解定义的@Target里必须包含TYPE_USE这个类型;

类级别约束

Hibernate-Validator已内置提供一部分能力,但可能还不够。