We often encounter parameter verification problems in business, such as front-end parameter verification and 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, and there will be a lot of repeated codes and logic irrelevant to the main business. Spring MVC provides a validation mechanism for parameters, but the underlying validation is done through Hibernate, so it’s worth taking a look at Hibernate validation and the JSR validation specification.
JSR data verification specification
Java has officially released JSR303 and JSR349 to propose a standard framework for data validity verification: BeanValidator. In the BeanValidator framework, users specify validation rules by annotating the attributes of beans with standard annotations like @notnull and @max, and verify beans through standard validation interfaces.
List of JSR annotations
Data validation annotations in the JSR standard are as follows:
Annotation name | Annotation data type | Annotations role | The sample |
---|---|---|---|
AssertFalse | boolean/Boolean | The annotated element must be False | @AssertFalse private boolean success; |
AssertTrue | boolean/Boolean | The annotated element must be True | @AssertTrue private boolean success; |
DecimalMax | BigDecimal/BigInteger/CharSequence/byte/short/int/long and wrapper classes | The annotated value should be less than or equal to the specified maximum value | @DecimalMax("10") private BigDecimal value; |
DecimalMin | BigDecimal/BigInteger/CharSequence/byte/short/int/long and wrapper classes | The annotated value should be greater than or equal to the specified minimum | @DecimalMin("10") private BigDecimal value; |
Digits | BigDecimal/BigInteger/CharSequence/byte/short/int/long and wrapper classes | Integer specifies the maximum number of digits of the integer part, and fraction specifies the maximum number of digits of the decimal part | @Digits(integer = 10,fraction = 4) private BigDecimal value; |
CharSequence | The string is a valid email address format | @Email private String email; |
|
Future | Various date types in Java | The specified date should be after the current date | @Future private LocalDateTime future; |
FutureOrPresent | Various date types in Java | The specified date should be the current date or after the current date | @FutureOrPresent private LocalDateTime futureOrPresent; |
Max | BigDecimal/BigInteger/byte/short/int/long and wrapper classes | The annotated value should be less than or equal to the specified maximum value | @Max("10") private BigDecimal value; |
Min | BigDecimal/BigInteger/byte/short/int/long and wrapper classes | The annotated value should be greater than or equal to the specified minimum | @Min("10") private BigDecimal value; |
Negative | BigDecimal BigInteger/byte/short/int/long/float/double and wrapper classes | The commented value should be negative | @Negative private BigDecimal value; |
NegativeOrZero | BigDecimal BigInteger/byte/short/int/long/float/double and wrapper classes | The commented value should be 0 or negative | @NegativeOrZero private BigDecimal value; |
NotBlank | CharSequence | The annotated string contains at least one non-null character | @NotBlank private String noBlankString; |
NotEmpty | CharSequence/Collection/Map/Array | The number of annotated collection elements is greater than 0 | @NotEmpty private List<string> values; |
NotNull | any | Annotated values are not empty | @NotEmpty private Object value; |
Null | any | The annotated value must be null | @Null private Object value; |
Past | Various date types in Java | The specified date should be before the current date | @Past private LocalDateTime past; |
PastOrPresent | Various date types in Java | The specified date should be on or before the current date | @PastOrPresent private LocalDateTime pastOrPresent; |
Pattern | CharSequence | The annotated string should conform to the given regular expression | @Pattern(\d*) private String numbers; |
Positive | BigDecimal BigInteger/byte/short/int/long/float/double and wrapper classes | The commented value should be positive | @Positive private BigDecimal value; |
PositiveOrZero | BigDecimal BigInteger/byte/short/int/long/float/double and wrapper classes | The commented value should be positive or 0 | @PositiveOrZero private BigDecimal value; |
Size | CharSequence/Collection/Map/Array | The number of annotated collection elements is within the specified range | @Size(min=1,max=10) private List<string> values; |
JSR annotation content
Take the simple @notnull annotation as an example to see what it contains. As shown in the source code below, the @notnull annotation contains the following:
- Message: Error message, in this case error code, which can be translated into different languages depending on internationalization.
- Groups: Groups check. Different groups can have different check conditions. For example, the same DTO used for create and Update may have different check conditions.
- Payload: The user of the BeanValidation API can use this attribute to specify the severity level of a constraint. 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(a) default "{javax.validation.constraints.NotNull.message}"; Class
[] groups() default { }; Class
[] 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(); }}Copy the code
We use error messages such as “Message” and “group” in many applications. In my article about the Spring Validator data verification, I explain the details about the payload. In the following example, we illustrate the usage of the following payload. Payload is used to indicate the severity of a data verification failure. After verifying an example of ContactDetails, . You can by calling ConstraintViolation getConstraintDescriptor () getPayload before () to get assigned to the level of error, and can according to this information to decide the next to the behavior.
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;
// ...
}
Copy the code
JSR verification interface
Using the JSR validation annotations above, we can add validation conditions to the corresponding fields of a class. How do we validate these validation conditions?
Set
> validate(T object, Class
… groups); , this method can be used to verify whether an Object complies with the verification rules of a specified group. If no group is specified, only the verification rules of the default group 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(a);
}
Copy the code
Hibernate data verification
Based on the JSR data validation specification, Hibernate adds some new annotation validation and then implements JSR’s Validator interface for data validation.
Hibernate new Annotations
Annotation name | Annotation data type | Annotations role | The sample |
---|---|---|---|
CNPJ | CharSequence | The annotated element must be the national registration number of a legal Brazilian corporation | @CNPJ private String cnpj; |
CPF | CharSequence | The annotated element must be a valid Brazilian taxpayer registration number | @CPF private String cpf; |
TituloEleitoral | CharSequence | The annotated element must be a valid Brazilian voter ID number | @TituloEleitoral private String tituloEleitoral; |
NIP | CharSequence | The annotated element must be a valid Polish tax code | @NIP private String nip; |
PESEL | CharSequence | The annotated element must be a valid Polish id number | @PESEL private String pesel; |
REGON | CharSequence | Annotated elements must be numbered with a valid Polish region | @REGON private String regon; |
DurationMax | Duration | The annotated element Duration has a length less than the specified length | @DurationMax(day=1) private Duration duration; |
DurationMin | Duration | The annotated element Duration is longer than the specified Duration | @DurationMin(day=1) private Duration duration; |
CodePointLength | CharSequence | CodePoint Number of annotated elements Within a specified range, each character in Unicode has a unique identifier, called a CodePoint. For example, if we want to limit the number of Chinese characters, we can use this | @CodePointLength(min=1) private String name; |
ConstraintComposition | Other data validation annotations | The combinatorial relationship of a combinatorial annotation, and or | — |
CreditCardNumber | CharSequence | Used to determine if a credit card is in a valid format | @CreditCardNumber private String credictCardNumber; |
Currency | CharSequence | The annotated element is the exchange rate of the specified type | @Currency(value = {"USD"}) private String currency; |
ISBN | CharSequence | The annotated element is a valid ISBN number | @ISBN private String isbn; |
Length | CharSequence | Annotated elements are within a specified length | @Length(min=1) private String name; |
LuhnCheck | CharSequence | Annotated elements can be checked by the Luhn algorithm | @LuhnCheck private String luhn; |
Mod10Check | CharSequence | Annotated elements can be checked by the modulo 10 algorithm | @Mod10Check private String mod10; |
ParameterScriptAssert | methods | Parameter script verification | — — — |
ScriptAssert | class | Class script verification | — — — |
UniqueElements | A collection of | Each element in the set is unique | @UniqueElements private List<String> elements; |
Hibiernate data verification
How to use Hibernate for data validation? Hibernate implements the Validator interface in the ValidatorImpl class. We can provide the factory class by Hibernate HibernateValidator. Create a ValidatorImpl buildValidatorFactory examples. The code to create a Validator instance using Hibernate is shown below.
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast"."true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
Copy the code
Hibernate verification source code
Hibernate uses factory methods to instantiate an instance of the Validator interface that can be used in a validation JavaBean with validation annotations. How does Hibernate implement this validation logic underneath? We take the following JavaBean as an example to parse the source code of Hibernate verification.
@Data
public class Person {
@NotBlank
@Size(max=64)
private String name;
@Min(0)
@Max(200)
private int age;
}
Copy the code
ConstraintValidator introduction
ConstraintValidator is the most fine-grained validation of data in Hibernate. It verifies that values of specified annotations and types are valid. @max (200)private int age; The age field is validated with a ConstraintValidator called MaxValidatorForInteger, which checks whether the specified value is greater than the specified maximum value.
public class MaxValidatorForInteger extends AbstractMaxValidator<Integer> {
@Override
protected int compare(Integer number) {
returnNumberComparatorHelper.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);
}
Copy the code
ConstraintValidator initialization
Hibernate provides a ValidatorImpl for verifying data. What is the relationship between ValidatorImpl and ConstraintValidator? Simply put, the ValidatorImpl initializes all ConstraintValidators and calls the built-in ConstraintValidator validation data during validation. The @constraint (validatedBy = {}) annotation for the built-in ConstraintValidator is empty.
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { }) // This is empty
public @interface AssertFalse {
String message(a) default "{javax.validation.constraints.AssertFalse.message}"; Class
[] groups() default { }; Class
[] 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(); }}Copy the code
Custom ConstraintValidator
If the annotations in Hibernate and JSR are not enough for me and I need to define a custom annotation and constraint, how should we implement it? Implementing a custom validation logic consists of two steps: 1. Annotation implementation. 2. Verify the implementation of logic. For example, if we need an annotation to verify the state of a field, we can define an annotation using the following example:
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = StatusValidator.class)
@Documented
public @interface ValidStatus {
String message(a) default"Status error"; Class<? >[] groups()default {};
Class<? extends Payload>[] payload() default {};
/** * a valid set of status values, default {1,2} */
int[] value() default {1.2};
}
Copy the code
Once we have implemented the annotation, we need to implement @constraint (validatedBy = StatusValidator.class) in the annotation with the following example code:
/** * Validates whether the state belongs to the specified state set (ConstraintValidator
) */
,>
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; }}Copy the code
The characteristics of the Validator
Four levels of constraint
Constraints at the member variable level
Constraints can be expressed by annotating a class’s member variables. The following code looks like this:
@Data
public class Person {
@NotBlank
@Size(max=64)
private String name;
@Min(0)
@Max(200)
private int age;
}
Copy the code
Attribute constraints
If your model class follows javabean standards, it may also annotate the bean’s properties instead of its member variables. An introduction to Javabeans can be found in my other blog post.
@Data
public class Person {
private String name;
@Min(0)
@Max(200)
private int age;
@NotBlank
@Size(max=64)
public String getName(a){
returnname; }}Copy the code
A collection of constraints
Constraints on elements in the container can be implemented by specifying elementType.type_use in the constraint definition in the @target annotation of the constraint annotation
Class level constraints
A constraint is placed at the class level; in this case, the object being validated is not simply a property, but a complete object. Using class-level constraints, you can verify correlations 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(a){
returnname; }}Copy the code
Verify the inheritability of annotations
The parent class adds a field to the constraint, and the child class verifies the field in the parent class.
Recursive check
Suppose we have a field of type Address in our example above, and Address has its own validation. How do we verify the field in Address? You can implement recursive validation by adding an @valid annotation to 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;
}
Copy the code
Method parameter verification
We can implement method-level parameter validation by adding validation annotations to method parameters, although these annotations need to be implemented using some AOP implementation (such as Spring’s method parameter validation).
public void createPerson(@NotNull String name,@NotNull Integer age){}Copy the code
Method parameter cross verification
Methods also support validation between parameters. For example, the following annotation does not allow the user name and age to be null when creating a user. The parameter of cross-check is Object[], and different parameter positions correspond to different Obj.
@NotAllPersonFieldNull
public void createPerson( String name,Integer age){}Copy the code
Method return value validation
public @NotNull Person getPerson( String name,Integer age){
return null;
}
Copy the code
Group function
As I mentioned in another article on Spring validation annotations, the @VALID annotation does not support group validation in Spring’s validation system. The @validated annotation supports group validation. It’s not that the @VALID annotation in JSR doesn’t support grouping validation. It’s that the Spring layer has disabled the grouping validation for the @VALID annotation.
So both native JSR annotations and Hibernate validations support grouping validation. See my article on Spring validation for the validation logic.
Grouping inheritance
The JSR group verification function uses the group field in the annotation. The group field stores the category of the group. If there is an inheritance relationship between the groups, will the group verification be inherited? The answer is yes.
Group order
If we need to specify the check order during the check, we can group the check conditions, and then the properties of the object are checked in order.
GroupSequence({ Default.class, BaseCheck.class, AdvanceCheck.class }) public interface OrderedChecks { }
Payload
So if you want to have different validation methods in different situations, like in English or something like that, you can’t use groups, so you can use PayLoad. The user can specify the payload of the current environment when initializing the Validator. In the verification process, the user can obtain the payload of the environment and perform 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 {
// ...
}
returnisValid; }}Copy the code
I am the god of the royal Fox. Welcome to follow my wechat official account: Wzm2ZSD
This article was first published to wechat public account, all rights reserved, reprint prohibited!