乐趣区

关于java:Hibernate数据校验简介

咱们在业务中常常会遇到参数校验问题,比方前端参数校验、Kafka 音讯参数校验等,如果业务逻辑比较复杂,各种实体比拟多的时候,咱们通过代码对这些数据一一校验,会呈现大量的反复代码以及和次要业务无关的逻辑。Spring MVC 提供了参数校验机制,然而其底层还是通过 Hibernate 进行数据校验,所以有必要去理解一下 Hibernate 数据校验和 JSR 数据校验标准。

JSR 数据校验标准

Java 官网先后公布了 JSR303 与 JSR349 提出了数据合法性校验提供的规范框架:BeanValidator,BeanValidator 框架中,用户通过在 Bean 的属性上标注相似于 @NotNull、@Max 等规范的注解指定校验规定,并通过规范的验证接口对 Bean 进行验证。

JSR 注解列表

JSR 规范中的数据校验注解如下所示:

注解名 注解数据类型 注解作用 示例
AssertFalse boolean/Boolean 被正文的元素必须为 False @AssertFalse private boolean success;
AssertTrue boolean/Boolean 被正文的元素必须为 True @AssertTrue private boolean success;
DecimalMax BigDecimal/BigInteger/CharSequence/byte/short/int/long 及其包装类 被正文的值应该小于等于指定的最大值 @DecimalMax("10") private BigDecimal value;
DecimalMin BigDecimal/BigInteger/CharSequence/byte/short/int/long 及其包装类 被正文的值应该大于等于指定的最小值 @DecimalMin("10") private BigDecimal value;
Digits BigDecimal/BigInteger/CharSequence/byte/short/int/long 及其包装类 integer 指定整数局部最大位数,fraction 指定小数局部最大位数 @Digits(integer = 10,fraction = 4) private BigDecimal value;
Email CharSequence 字符串为非法的邮箱格局 @Email private String email;
Future java 中的各种日期类型 指定日期应该在当期日期之后 @Future private LocalDateTime future;
FutureOrPresent java 中的各种日期类型 指定日期应该为当期日期或当期日期之后 @FutureOrPresent private LocalDateTime futureOrPresent;
Max BigDecimal/BigInteger/byte/short/int/long 及包装类 被正文的值应该小于等于指定的最大值 @Max("10") private BigDecimal value;
Min BigDecimal/BigInteger/byte/short/int/long 及包装类 被正文的值应该大于等于指定的最小值 @Min("10") private BigDecimal value;
Negative BigDecimal/BigInteger/byte/short/int/long/float/double 及包装类 被正文的值应该是正数 @Negative private BigDecimal value;
NegativeOrZero BigDecimal/BigInteger/byte/short/int/long/float/double 及包装类 被正文的值应该是 0 或者正数 @NegativeOrZero private BigDecimal value;
NotBlank CharSequence 被正文的字符串至多蕴含一个非空字符 @NotBlank private String noBlankString;
NotEmpty CharSequence/Collection/Map/Array 被正文的汇合元素个数大于 0 @NotEmpty private List<string> values;
NotNull any 被正文的值不为空 @NotEmpty private Object value;
Null any 被正文的值必须空 @Null private Object value;
Past java 中的各种日期类型 指定日期应该在当期日期之前 @Past private LocalDateTime past;
PastOrPresent java 中的各种日期类型 指定日期应该在当期日期或之前 @PastOrPresent private LocalDateTime pastOrPresent;
Pattern CharSequence 被正文的字符串应该合乎给定失去正则表达式 @Pattern(\d*) private String numbers;
Positive BigDecimal/BigInteger/byte/short/int/long/float/double 及包装类 被正文的值应该是负数 @Positive private BigDecimal value;
PositiveOrZero BigDecimal/BigInteger/byte/short/int/long/float/double 及包装类 被正文的值应该是负数或 0 @PositiveOrZero private BigDecimal value;
Size CharSequence/Collection/Map/Array 被正文的汇合元素个数在指定范畴内 @Size(min=1,max=10) private List<string> values;

JSR 注解内容

咱们以罕用的比较简单的 @NotNull 注解为例,看看注解中都蕴含那些内容,如下边的源码所示,能够看到 @NotNull 注解蕴含以下几个内容:

  1. message:谬误音讯,示例中的是错误码,能够依据国际化翻译成不同的语言。
  2. groups:分组校验,不同的分组能够有不同的校验条件,比方同一个 DTO 用于 create 和 update 时校验条件可能不一样。
  3. payload:BeanValidation API 的使用者能够通过此属性来给约束条件指定重大级别. 这个属性并不被 API 本身所应用.
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = {})
public @interface NotNull {String message() default "{javax.validation.constraints.NotNull.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    /**
     * Defines several {@link NotNull} annotations on the same element.
     */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    @interface List {NotNull[] value();}
}

谬误音讯 message、分组 group 这些性能咱们程序中应用比拟多,在我介绍 Spring Validator 数据校验的文章中有具体阐明,然而对于 payload 咱们接触的比拟少,上面咱们举例说明以下 payload 的应用,上面的示例中,咱们用 payload 来标识数据校验失败的严重性,通过以下代码。在校验完一个 ContactDetails 的示例之后, 你就能够通过调用 ConstraintViolation.getConstraintDescriptor().getPayload()来失去之前指定到谬误级别了, 并且能够依据这个信息来决定接下来到行为.

public class Severity {public static class Info extends Payload {};
    public static class Error extends Payload {};}

public class ContactDetails {@NotNull(message="Name is mandatory", payload=Severity.Error.class)
    private String name;

    @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
    private String phoneNumber;

    // ...
}

JSR 校验接口

通过后面的 JSR 校验注解,咱们能够给某个类的对应字段增加校验条件,那么怎么去校验这些校验条件呢?JSR 进行数据校验的外围接口是 Validation,该接口的定义如下所示,咱们应用比拟多的接口应该是<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);,该办法能够用于校验某个 Object 是否合乎指定分组的校验规定,如果不指定分组,那么只有默认分组的校验规定会失效。

public interface Validator {

    /**
     * Validates all constraints on {@code object}.
     */
    <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);

    /**
     * Validates all constraints placed on the property of {@code object}
     * named {@code propertyName}.
     */
    <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName,Class<?>... groups);

    /**
     * Validates all constraints placed on the property named {@code propertyName}
     * of the class {@code beanType} would the property value be {@code value}.
     */
    <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups);

    /**
     * Returns the descriptor object describing bean constraints.
     * The returned object (and associated objects including
     * {@link ConstraintDescriptor}s) are immutable.
     */
    BeanDescriptor getConstraintsForClass(Class<?> clazz);

    /**
     * Returns an instance of the specified type allowing access to
     * provider-specific APIs.
     * <p>
     * If the Jakarta Bean Validation provider implementation does not support
     * the specified class, {@link ValidationException} is thrown.call
     */
    <T> T unwrap(Class<T> type);

    /**
     * Returns the contract for validating parameters and return values of methods
     * and constructors.
     */
    ExecutableValidator forExecutables();}

Hibernate 数据校验

基于 JSR 数据校验标准,Hibernate 增加了一些新的注解校验,而后实现了 JSR 的 Validator 接口用于数据校验。

Hibernate 新增注解

注解名 注解数据类型 注解作用 示例
CNPJ CharSequence 被正文的元素必须为非法的巴西法人国家登记号 @CNPJ private String cnpj;
CPF CharSequence 被正文的元素必须为非法的巴西纳税人注册号 @CPF private String cpf;
TituloEleitoral CharSequence 被正文的元素必须为非法的巴西选民身份证号码 @TituloEleitoral private String tituloEleitoral;
NIP CharSequence 被正文的元素必须为非法的波兰税号 @NIP private String nip;
PESEL CharSequence 被正文的元素必须为非法的波兰身份证号码 @PESEL private String pesel;
REGON CharSequence 被正文的元素必须为非法的波兰区域编号 @REGON private String regon;
DurationMax Duration 被正文的元素 Duration 的工夫长度小于指定的工夫长度 @DurationMax(day=1) private Duration duration;
DurationMin Duration 被正文的元素 Duration 的工夫长度大于指定的工夫长度 @DurationMin(day=1) private Duration duration;
CodePointLength CharSequence 被正文的元素 CodPoint 数目在指定范畴内,unicode 中每一个字符都有一个惟一的识别码,这个码就是 CodePoint。比方咱们要限度中文字符的数目,就能够应用这个 @CodePointLength(min=1) private String name;
ConstraintComposition 其它数据校验注解 组合注解的组合关系,与或等关系
CreditCardNumber CharSequence 用于判断一个信用卡是不是非法格局的信用卡 @CreditCardNumber private String credictCardNumber;
Currency CharSequence 被正文的元素是指定类型的汇率 @Currency(value = {"USD"}) private String currency;
ISBN CharSequence 被正文的元素是非法的 ISBN 号码 @ISBN private String isbn;
Length CharSequence 被正文的元素是长度在指定范畴内 @Length(min=1) private String name;
LuhnCheck CharSequence 被正文的元素能够通过 Luhn 算法查看 @LuhnCheck private String luhn;
Mod10Check CharSequence 被正文的元素能够通过模 10 算法查看 @Mod10Check private String mod10;
ParameterScriptAssert 办法 参数脚本校验 ————
ScriptAssert 类脚本校验 ————
UniqueElements 汇合 汇合中的每个元素都是惟一的 @UniqueElements private List<String> elements;

Hibiernate 数据校验

如何应用 Hibernate 进行数据校验呢?咱们晓得 JSR 规定了数据校验的接口 Validator,Hibernate 用 ValidatorImpl 类中实现了 Validator 接口,咱们能够通过 Hibernate 提供的工厂类 HibernateValidator.buildValidatorFactory 创立一个 ValidatorImpl 实例。应用 Hibernate 创立一个 Validator 实例的代码如下所示。

ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
    .configure()
    .addProperty("hibernate.validator.fail_fast", "true")
    .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

Hibernate 校验源码

通过下面的内容,咱们晓得 Hibernate 能够用工厂办法实例化一个 Validator 接口的实例,这个实例能够用于带有校验注解的校验 JavaBean,那么 Hibernate 底层是如何实现这些校验逻辑的呢?咱们以如下 JavaBean 为例,解析 Hibernate 校验的源码。

@Data
public class Person {

    @NotBlank
    @Size(max=64)
    private String name;

    @Min(0)
    @Max(200)
    private int age;
}

ConstraintValidator 介绍

ConstraintValidator 是 Hibernate 中数据校验的最细粒度,他能够校验指定注解和类型的数值是否非法。比方下面例子中的 @Max(200)private int age;,对于 age 字段的校验就会应用一个叫MaxValidatorForInteger 的 ConstraintValidator,这个 ConstraintValidator 在校验的时候会判断指定的数值是不是大于指定的最大值。

public class MaxValidatorForInteger extends AbstractMaxValidator<Integer> {

    @Override
    protected int compare(Integer number) {return NumberComparatorHelper.compare( number.longValue(), maxValue );
    }
}

public abstract class AbstractMaxValidator<T> implements ConstraintValidator<Max, T> {

    protected long maxValue;

    @Override
    public void initialize(Max maxValue) {this.maxValue = maxValue.value();
    }

    @Override
    public boolean isValid(T value, ConstraintValidatorContext constraintValidatorContext) {
        // null values are valid
        if (value == null) {return true;}

        return compare(value) <= 0;
    }

    protected abstract int compare(T number);
}

ConstraintValidator 初始化

咱们在后面的内容中说到 Hibernate 提供了 ValidatorImpl 用于数据校验,那么 ValidatorImpl 和 ConstraintValidator 是什么关系呢,简略来说就是 ValidatorImpl 在初始化的时候会初始化所有的 ConstraintValidator,在校验数据的过程中调用这些内置的 ConstraintValidator 校验数据。内置 ConstraintValidator 的对应注解的 @Constraint(validatedBy = {})是空的。

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = {}) // 这儿是空的
public @interface AssertFalse {String message() default "{javax.validation.constraints.AssertFalse.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    /**
     * Defines several {@link AssertFalse} annotations on the same element.
     *
     * @see javax.validation.constraints.AssertFalse
     */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    @interface List {AssertFalse[] value();}
}

自定义 ConstraintValidator

如果 Hibernate 和 JSR 中的注解不够我用,我须要自定义一个注解和约束条件,咱们应该怎么实现呢。实现一个自定义校验逻辑一共分两步:1. 注解的实现。2. 校验逻辑的实现。比方咱们须要一个校验字段状态的注解,咱们能够应用以下示例定义一个注解:

@Target({ METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = StatusValidator.class)
@Documented
public @interface ValidStatus {String message() default "状态谬误";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    /**
     * 无效的状态值汇合,默认{1,2}
     */
    int[] value() default {1,2};
}

实现了注解之后,咱们须要实现注解中的@Constraint(validatedBy = StatusValidator.class),示例代码如下:

/**
 * 校验状态是否属于指定状态集
 (ConstraintValidator 后指定的泛型对象类型为
 注解类和注解正文的字段类型 <ValidStatus, Integer>)
 */
public class StatusValidator implements ConstraintValidator<ValidStatus, Integer> {private Integer[] validStatus;

    @Override
    public void initialize(ValidStatus validStatus) {int[] ints = validStatus.value();
        int n = ints.length;
        Integer[] integers = new Integer[n];
        for (int i = 0; i < n; i++) {integers[i] = ints[i];
        }
        this.validStatus = integers;
    }

    @Override
    public boolean isValid(Integer n, ConstraintValidatorContext constraintValidatorContext) {List<Integer> status = Arrays.asList(validStatus);
        if (status.contains(n)) {return true;}
        return false;
    }
}

Validator 的个性

四种束缚级别

成员变量级别的束缚

束缚能够通过注解一个类的成员变量来表白。如下代码所示:

@Data
public class Person {

    @NotBlank
    @Size(max=64)
    private String name;

    @Min(0)
    @Max(200)
    private int age;
}

属性束缚

如果你的模型类遵循 javabean 的规范,它也可能注解这个 bean 的属性而不是它的成员变量。对于 JavaBean 的介绍能够看我的另外一篇博客。

@Data
public class Person {

    private String name;

    @Min(0)
    @Max(200)
    private int age;

    @NotBlank
    @Size(max=64)
    public String getName(){return name;}
}

汇合束缚

通过在束缚注解的 @Target 注解在束缚定义中指定 ElementType.TYPE_USE,就能够实现对容器内元素进行束缚

类级别束缚

一个束缚被放到类级别上,在这种状况下,被验证的对象不是简略的一个属性,而是一个残缺的对象。应用类级别束缚,能够验证对象几个属性之间的相关性,比方不容许所有字段同时为 null 等。

@Data
@NotAllFieldNull
public class Person {

    private String name;

    @Min(0)
    @Max(200)
    private int age;

    @NotBlank
    @Size(max=64)
    public String getName(){return name;}
}

校验注解的可继承性

父类中增加了束缚的字段,子类在进行校验时也会校验父类中的字段。

递归校验

假如咱们下面例子中的 Person 多了一个 Address 类型的字段,并且 Address 也有本人的校验,咱们怎么校验 Address 中的字段呢?能够通过在 Address 上增加 @Valid 注解实现递归校验。

@Data
public class Person {

    private String name;

    @Min(0)
    @Max(200)
    private int age;

    @Valid
    public Address address;
}

@Data
public class Address{

    @NotNull
    private string city;
}

办法参数校验

咱们能够通过在办法参数中增加校验注解,实现办法级别的参数校验,当然这些注解的失效须要通过一些 AOP 实现(比方 Spring 的办法参数校验)。


public void createPerson(@NotNull String name,@NotNull Integer age){

}

办法参数穿插校验

办法也反对参数之间的校验,比方如下注解不容许创立用户时候用户名和年龄同时为空,注解校验逻辑须要本人实现。穿插校验的参数是 Object[]类型,不同参数地位对应不同的 Obj。

@NotAllPersonFieldNull
public void createPerson(String name,Integer age){

}

办法返回值校验

public @NotNull Person getPerson(String name,Integer age){return null;}

分组性能

我在另一篇介绍 Spring 校验注解的文章中说过,在 Spring 的校验体系中,@Valid 注解不反对分组校验,@Validated 注解反对分组校验。事实上这并不是 JSR 注解中的 @Valid 不反对分组校验,而是 Spring 层面把 @Valid 注解的分组校验性能屏蔽了。

所以原生的 JSR 注解和 Hibernate 校验都反对分组校验性能,具体校验逻辑能够参考我无关 Spring 数据校验的文章。

分组继承

咱们晓得 JSR 分组校验性能是应用注解中的 group 字段,group 字段存储了分组的类别,那么如果分组的类之间有继承关系,分组校验会被继承吗?答案是会的。

分组程序

如果咱们在校验的过程中须要指定校验程序,那么咱们能够给校验条件分组,分组之后就会依照程序校验对象中的各个属性。

GroupSequence({Default.class, BaseCheck.class, AdvanceCheck.class})
public interface OrderedChecks {
}

Payload

如果咱们须要在不同的状况下有不同的校验形式,比方中英文环境之类的,这种时候用分组就不是很适合了,能够思考应用 PayLoad。用户能够在初始化 Validator 时候指定以后环境的 payload,而后在校验环节拿到环境中的 payload 走不同的校验流程:

ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
        .configure()
        .constraintValidatorPayload("US")
        .buildValidatorFactory();

Validator validator = validatorFactory.getValidator();

public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> {

    public String countryCode;

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {if ( object == null) {return true;}

        boolean isValid = false;

        String countryCode = constraintContext
                .unwrap(HibernateConstraintValidatorContext.class)
                .getConstraintValidatorPayload(String.class);

        if ("US".equals( countryCode) ) {// checks specific to the United States}
        else if ("FR".equals( countryCode) ) {// checks specific to France}
        else {// ...}

        return isValid;
    }
}

我是御狐神,欢送大家关注我的微信公众号:wzm2zsd

本文最先公布至微信公众号,版权所有,禁止转载!

退出移动版