Introduction to Hibernate data verification

We often encounter parameter verification problems in business, such as front-end parameter verification, Kafka message parameter verification, etc. if the business logic is complex and there are many entities, we verify these data one by one through the code, there will be a large number of duplicate codes and logic unrelated to the main business. Spring MVC provides a parameter verification mechanism, but its bottom layer still performs data verification through Hibernate, so it is necessary to understand Hibernate data verification and JSR data verification specifications.

JSR data verification specification

Java has officially released JSR303 and JSR349, and proposed a standard framework for data validity verification: BeanValidator. In the BeanValidator framework, users specify verification rules by labeling standard annotations such as @ NotNull and @ Max on Bean properties, and verify beans through standard verification interfaces.

JSR annotation list

The data verification notes in JSR standard are as follows:

Annotation nameAnnotation data typeAnnotation functionExample
AssertFalseboolean/BooleanThe annotated element must be False@AssertFalse private boolean success;
AssertTrueboolean/BooleanThe annotated element must be True@AssertTrue private boolean success;
DecimalMaxBigDecimal/BigInteger/CharSequence/byte/short/int/long and its wrapper classesThe annotated value should be less than or equal to the specified maximum value@DecimalMax("10") private BigDecimal value;
DecimalMinBigDecimal/BigInteger/CharSequence/byte/short/int/long and its wrapper classesThe annotated value should be greater than or equal to the specified minimum value@DecimalMin("10") private BigDecimal value;
DigitsBigDecimal/BigInteger/CharSequence/byte/short/int/long and its wrapper classesInteger specifies the maximum number of digits in the integer part and fraction specifies the maximum number of digits in the decimal part@Digits(integer = 10,fraction = 4) private BigDecimal value;
EmailCharSequenceString is in legal mailbox format@Email private String email;
FutureVarious date types in javaThe specified date should be after the current date@Future private LocalDateTime future;
FutureOrPresentVarious date types in javaThe specified date should be the current date or later@FutureOrPresent private LocalDateTime futureOrPresent;
MaxBigDecimal/BigInteger/byte/short/int/long and wrapper classesThe annotated value should be less than or equal to the specified maximum value@Max("10") private BigDecimal value;
MinBigDecimal/BigInteger/byte/short/int/long and wrapper classesThe annotated value should be greater than or equal to the specified minimum value@Min("10") private BigDecimal value;
NegativeBigDecimal/BigInteger/byte/short/int/long/float/double and wrapper classesThe annotated value should be negative@Negative private BigDecimal value;
NegativeOrZeroBigDecimal/BigInteger/byte/short/int/long/float/double and wrapper classesThe annotated value should be 0 or negative@NegativeOrZero private BigDecimal value;
NotBlankCharSequenceThe annotated string contains at least one non empty character@NotBlank private String noBlankString;
NotEmptyCharSequence/Collection/Map/ArrayThe number of annotated collection elements is greater than 0@NotEmpty private List<string> values;
NotNullanyThe annotated value cannot be empty@NotEmpty private Object value;
NullanyThe annotated value must be empty@Null private Object value;
PastVarious date types in javaThe specified date should be before the current date@Past private LocalDateTime past;
PastOrPresentVarious date types in javaThe specified date should be on or before the current date@PastOrPresent private LocalDateTime pastOrPresent;
PatternCharSequenceThe annotated string should match the given regular expression@Pattern(\d*) private String numbers;
PositiveBigDecimal/BigInteger/byte/short/int/long/float/double and wrapper classesThe annotated value should be a positive number@Positive private BigDecimal value;
PositiveOrZeroBigDecimal/BigInteger/byte/short/int/long/float/double and wrapper classesThe annotated value should be a positive number or 0@PositiveOrZero private BigDecimal value;
SizeCharSequence/Collection/Map/ArrayThe number of annotated collection elements is within the specified range@Size(min=1,max=10) private List<string> values;

JSR annotation content

Take the commonly used simple @ NotNull annotation as an example to see what content is included in the annotation. As shown in the source code below, you can see that the @ NotNull annotation contains the following contents:

  1. Message: error message. The error code in the example can be translated into different languages according to internationalization.
  2. Groups: Group verification. Different groups can have different verification conditions. For example, the verification conditions may be different when the same DTO is used for create and update.
  3. payload: the user of BeanValidation API can use this property to specify the severity level of constraints. This property is not used by the API itself
@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();
    }
}

The functions of error message and grouping group are often used in our program. They are described in detail in my article on Spring Validator data verification, but we have little contact with the payload. Let's illustrate the use of the following payload. In the following example, we use the payload to identify the severity of data verification failure through the following code. After verifying an example of ContactDetails, you can call ConstraintViolation.getConstraintDescriptor().getPayload() to get the previously specified error level, and you can determine the next behavior based on this information

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 verification interface

Through the previous JSR verification annotation, we can add verification conditions to the corresponding fields of a class, so how to verify these verification conditions? The core interface of JSR for data verification is Validation. The definition of this interface is as follows. The interfaces we use more should be < T > set < constraintviolation < T > > validate (t Object, class <? >... Groups);, This method can be used to verify whether an Object conforms to the verification rules of the specified group. If no group is specified, only the verification rules of the default group will take effect.

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 data verification

Based on the JSR data verification specification, Hibernate adds some new annotation verification, and then implements the Validator interface of JSR for data verification.

Hibernate new annotation

Annotation nameAnnotation data typeAnnotation functionExample
CNPJCharSequenceThe annotated element must be a legal Brazilian legal person national registration number@CNPJ private String cnpj;
CPFCharSequenceThe annotated element must be a legal Brazilian taxpayer registration number@CPF private String cpf;
TituloEleitoralCharSequenceThe annotated element must be the legal ID number of the Brazil voters.@TituloEleitoral private String tituloEleitoral;
NIPCharSequenceThe annotated element must be a legal Polish tax number@NIP private String nip;
PESELCharSequenceThe annotated element must be the legal Poland ID number.@PESEL private String pesel;
REGONCharSequenceThe annotated element must be a legal Polish region number@REGON private String regon;
DurationMaxDurationThe length of time of the annotated element Duration is less than the specified length of time@DurationMax(day=1) private Duration duration;
DurationMinDurationThe length of time of the annotated element Duration is greater than the specified length of time@DurationMin(day=1) private Duration duration;
CodePointLengthCharSequenceThe number of annotated elements is within the specified range. Each character in unicode has a unique identification code, which is CodePoint. For example, if we want to limit the number of Chinese characters, we can use this@CodePointLength(min=1) private String name;
ConstraintCompositionOther data verification notesCombination relation of combination annotation, and or relation, etc---
CreditCardNumberCharSequenceIt is used to judge whether a credit card is in legal format@CreditCardNumber private String credictCardNumber;
CurrencyCharSequenceThe annotated element is the exchange rate of the specified type@Currency(value = {"USD"}) private String currency;
ISBNCharSequenceThe annotated element is a legal ISBN number@ISBN private String isbn;
LengthCharSequenceThe length of the annotated element is within the specified range@Length(min=1) private String name;
LuhnCheckCharSequenceAnnotated elements can be checked by Luhn algorithm@LuhnCheck private String luhn;
Mod10CheckCharSequenceThe annotated elements can be checked by the module 10 algorithm@Mod10Check private String mod10;
ParameterScriptAssertmethodParameter script verification--
ScriptAssertclassClass script verification--
UniqueElementsaggregateEach element in the collection is unique@UniqueElements private List<String> elements;

Hibiernate data verification

How to use hibernate for data verification? We know that JSR specifies the Validator interface for data verification. Hibernate implements the Validator interface in the ValidatorImpl class. We can create a ValidatorImpl instance through the factory class HibernateValidator.buildValidatorFactory provided by hibernate. The code to create a Validator instance using hibernate is as follows.

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

Hibernate verification source code

Through the above content, we know that hibernate can instantiate an instance of Validator interface with factory method, which can be used to verify JavaBeans with verification annotations. How does hibernate implement these verification logic? We take the following JavaBean as an example to analyze the source code of Hibernate verification.

@Data
public class Person {

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

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

Introduction to ConstraintValidator

ConstraintValidator is the most fine-grained data verification in Hibernate. It can verify whether the value of the specified annotation and type is legal. For example, @ Max(200)private int age; in the above example;, For the verification of the age field, a ConstraintValidator called maxvalidatorforinter will be used. This ConstraintValidator will judge whether the specified value is greater than the specified maximum value during verification.

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 initialization

We mentioned in the previous part that Hibernate provided ValidatorImpl for data validation, so what is the relationship between ValidatorImpl and ConstraintValidator? In short, ValidatorImpl initializes all ConstraintValidator when initializing, and calls these built-in ConstraintValidator check data in the process of checking data. The @ Constraint(validatedBy = {}) of the corresponding annotation of the built-in constraint validator is empty.

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { }) // It's empty here
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();
    }
}

Custom ConstraintValidator

If the annotations in Hibernate and JSR are not enough for me, I need to customize an annotation and constraint. How should we implement it. The implementation of a user-defined verification logic is divided into two steps: 1. The implementation of annotations. 2. Implementation of verification logic. For example, we need an annotation to verify the field status. We can use the following example to define an annotation:

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = StatusValidator.class)
@Documented
public @interface ValidStatus {
    String message() default "Status error ";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    /**
     * Valid set of status values, default {1,2}
     */
    int[] value() default {1,2};
}

After implementing the annotation, we need to implement @ Constraint(validatedBy = StatusValidator.class) in the annotation. The example code is as follows:

/**
 * Verify whether the state belongs to the specified state set
 (ConstraintValidator The generic object type specified after is
 Annotation class and field type of annotation (< 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;
    }
}

Properties of Validator

Four constraint levels

Member variable level constraints

Constraints can be expressed by annotating member variables of a class. The following code is shown:

@Data
public class Person {

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

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

Attribute constraint

If your model class follows the Java Bean standard, it may also annotate the bean's properties rather than its member variables. For an introduction to JavaBeans, see my other blog.

@Data
public class Person {

    private String name;

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

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

Set constraints

Specify elementtype.type in the constraint definition through the @ Target annotation of the constraint annotation_ Use, you can implement constraints on the elements in the container

Class level constraints

A constraint is placed at the class level. In this case, the verified object is not a simple attribute, but a complete object. Using class level constraints, you can verify the correlation between several attributes of an object, such as not allowing all fields to be null at the same time.

@Data
@NotAllFieldNull
public class Person {

    private String name;

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

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

Verify the inheritability of annotations

Constraint fields are added to the parent class, and the fields in the parent class will also be verified when the child class is verified.

Recursive check

Suppose that the Person in the above example has an additional field of Address type, and Address also has its own verification, how can we verify the field in Address? Recursive verification can be realized by adding @ Valid annotation on Address.

@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;
}

Method parameter verification

We can implement method level parameter verification by adding verification annotations to method parameters. Of course, the effectiveness of these annotations needs to be implemented through some AOP (such as Spring's method parameter verification).

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

}

Method parameter cross check

The method also supports the verification between parameters. For example, the following annotation does not allow the user name and age to be empty when creating a user. The annotation verification logic needs to be implemented by itself. The cross check parameter is of Object [] type, and different parameter positions correspond to different Obj.

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

}

Method return value verification

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

Grouping function

In another article on Spring verification annotations, I said that in the Spring verification system, @ Valid annotation does not support group verification, and @ Validated annotation supports group verification. In fact, this is not because @ Valid in the JSR annotation does not support group verification, but the Spring level shields the group verification function of the @ Valid annotation.

Therefore, both the native JSR annotation and Hibernate verification support the group verification function. For the specific verification logic, please refer to my article on Spring data verification.

Group inheritance

We know that the JSR group verification function uses the group field in the annotation. The group field stores the group categories. If there is an inheritance relationship between the group classes, will the group verification be inherited? The answer is yes.

Grouping order

If we need to specify the verification order in the verification process, we can group the verification conditions. After grouping, we will verify each attribute in the object in order.

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

Payload

If we need to have different verification methods in different situations, such as Chinese and English environments, grouping is not very appropriate. We can consider using payload. You can specify the payload of the current environment when initializing the Validator, and then get the payload in the environment in the verification phase and go through different verification processes:

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;
    }
}

I am the fox fox. Welcome to my WeChat official account: wzm2zsd

This article is first released to WeChat official account, all rights reserved, no reprint!

Tags: Java

Posted on Fri, 26 Nov 2021 17:29:57 -0500 by guxin1999