validator automated verification

Keywords: Java Hibernate Bean Validation Attribute

Reminder

Please collect and see again. This article is too long for you to read in a short time. It's a pity to miss it because it has too much dry goods.

The sample code can focus on Yifeilu (Public Number) replying to jy acquisition.

Harvest

  1. Explain in detail: You can master various postures using hibernate-validator and similar calibration tools.
  2. Comprehensive content: can be queried as a knowledge dictionary

what

Note: hibernate-validator has nothing to do with the persistence layer framework hibernate. hibernate-validator is one of the hibernate organizations. Open source project.

hibernate-validator is the implementation of JSR 380 (Bean Validation 2.0) and JSR 303 (Bean Validation 1.0).

JSR 380 - Bean Validation 2.0 defines a metadata model and API for entity and method validation.

Java EE (renamed: Jakarta EE) has developed the validation specification, namely: javax.validation-api (now jakarta.validation-api, jar package name changes, package name, class name unchanged, so the use of the same) package, spring-boot-starter-web, spring-boot-starter-webflux package have been introduced accordingly. Lai, you can use it directly.

Similar to the relationship between slf4j and logback (log4j2), when used, the interface specification function provided by javax.validate is used in the code. When loaded, the corresponding specification implementation class is loaded according to the SPI specification.

It has nothing to do with hibernate, so use it boldly.

why

Officially, hibernate-validator has the following description:

Previous checks are as follows:

After using hibernate-validator, the verification logic is as follows:

The same validation logic in controller, service and dao layers can use the same data validation model.

how

Logo annotation

@ Valid (specification, common)

Tags are used to validate cascading attributes, method parameters, or method return types.
When validating attributes, method parameters, or method return types, constraints defined on objects and their attributes are validated.
This behavior is applied recursively.

@Validated(spring)

spring provides extended annotations that can be easily used for group validation

22 constraints annotations

In addition to the parameters listed below, each constraint has parameters message, groups, and payload. This is the requirement of the Bean Validation specification.

Among them, message is a prompt message, and groups can be grouped according to the situation.

Each of the following annotations can define more than one on the same element.

@AssertFalse

Check whether the element is false and support data types: boolean, Boolean

@AssertTrue

Check whether the element is true and support data types: boolean, Boolean

@DecimalMax(value=, inclusive=)

Incusive: boolean, default true, indicates whether or not to include, whether or not to equal
Value: When inclusive=false, check whether the annotated value is less than the specified maximum. When inclusive=true, check whether the value is less than or equal to the specified maximum. The parameter value is the maximum value represented by the bigdecimal string.
Supporting data types: BigDecimal, BigInteger, CharSequence, (byte, short, int, long and its encapsulation classes)

@DecimalMin(value=, inclusive=)

Supporting data types: BigDecimal, BigInteger, CharSequence, (byte, short, int, long and its encapsulation classes)
Incusive: boolean, default true, indicates whether or not to include, whether or not to equal
value:
When inclusive=false, check whether the annotated value is greater than the specified maximum. When inclusive=true, check whether the value is greater than or equal to the specified maximum value. The parameter value is the minimum value represented by the bigdecimal string.

@Digits(integer=, fraction=)

Check whether the value is a number that contains integer and fraction decimal digits at most
Supported data types:
BigDecimal, BigInteger, CharSequence, byte, short, int, long, native type encapsulation class, any Number subclass.

@Email

Check that the specified character sequence is a valid e-mail address. The optional parameters regexp and flags allow you to specify additional regular expressions (including regular expression flags) that e-mail must match.
Supported data type: CharSequence

@Max(value=)

Check if the value is less than or equal to the specified maximum value
Supported data types:
BigDecimal, BigInteger, byte, short, int, long, encapsulation class of native type, any subclass of CharSequence (number represented by character sequence), any subclass of Number, any subclass of javax.money.MonetaryAmount

@Min(value=)

Check whether the value is greater than or equal to the specified maximum
Supported data types:
BigDecimal, BigInteger, byte, short, int, long, encapsulation class of native type, any subclass of CharSequence (number represented by character sequence), any subclass of Number, any subclass of javax.money.MonetaryAmount

@NotBlank

Check whether the character sequence is empty and if the length of the space removed is greater than 0. Unlike @NotEmpty, this constraint applies only to character sequences and ignores trailing spaces.
Supporting data types: CharSequence

@NotNull

Check if the value is not null
Supporting data types: any type

@NotEmpty

Check whether the element is null or empty
Support data types: CharSequence, Collection, Map, arrays

@Size(min=, max=)

Check whether the number of elements is between min (including) and max (containing)
Support data types: CharSequence, Collection, Map, arrays

@Negative

Check whether the element is strictly negative. Zero values are considered invalid.
Supports data types:
BigDecimal, BigInteger, byte, short, int, long, encapsulation class of native type, any subclass of CharSequence (number represented by character sequence), any subclass of Number, any subclass of javax.money.MonetaryAmount

@NegativeOrZero

Check whether the element is negative or zero.
Supports data types:
BigDecimal, BigInteger, byte, short, int, long, encapsulation class of native type, any subclass of CharSequence (number represented by character sequence), any subclass of Number, any subclass of javax.money.MonetaryAmount

@Positive

Check that the element is strictly positive. Zero values are considered invalid.
Supports data types:
BigDecimal, BigInteger, byte, short, int, long, encapsulation class of native type, any subclass of CharSequence (number represented by character sequence), any subclass of Number, any subclass of javax.money.MonetaryAmount

@PositiveOrZero

Check whether the element is positive or zero.
Supports data types:
BigDecimal, BigInteger, byte, short, int, long, encapsulation class of native type, any subclass of CharSequence (number represented by character sequence), any subclass of Number, any subclass of javax.money.MonetaryAmount

@Null

Check whether the value is null
Supporting data types: any type

@Future

Check whether the date is in the future
Supported data types:
java.util.Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate
If Joda Time API in the classpath, any implementation classes of ReadablePartial and ReadableInstant

@FutureOrPresent

Check date is now or in the future
Supporting Data Types: Same as @Future

@Past

Check whether the date is in the past
Supporting Data Types: Same as @Future

@PastOrPresent

Check whether the date is past or present
Supporting Data Types: Same as @Future

@Pattern(regex=, flags=)

Check whether the string matches the regex of the regular expression based on the given flag match
Supporting data types: CharSequence

Implementation example

@Size

As you can see from the above, the data types supported by @Size in the specification are: CharSequence, Collection, Map, arrays
The implementation in hibernate-validator is as follows:

For CharSequence, Collection, Map, there is an implementation, because arrays has many possibilities, providing multiple implementations.
Among them, SizeValidator ForCollection. Java is as follows:

import java.lang.invoke.MethodHandles;
import java.util.Collection;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraints.Size;

@SuppressWarnings("rawtypes")
// as per the JLS, Collection<?> is a subtype of Collection, so we need to explicitly reference
// Collection here to support having properties defined as Collection (see HV-1551)
public class SizeValidatorForCollection implements ConstraintValidator<Size, Collection> {

    private  static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );

    private int min;
    private int max;

    @Override
    public void initialize(Size parameters) {
        min = parameters.min();
        max = parameters.max();
        validateParameters();
    }
    
    @Override
    public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) {
        if ( collection == null ) {
            return true;
        }
        int length = collection.size();
        return length >= min && length <= max;
    }

    private void validateParameters() {
        if ( min < 0 ) {
            throw LOG.getMinCannotBeNegativeException();
        }
        if ( max < 0 ) {
            throw LOG.getMaxCannotBeNegativeException();
        }
        if ( max < min ) {
            throw LOG.getLengthCannotBeNegativeException();
        }
    }
}

The implementation logic is implemented according to the specification.

actual combat

Declare Java Bean Constraints

Constraints can be declared as follows:

  1. Field level constraints
@NotNull
private String manufacturer;
  1. Attribute level constraints
@NotNull
public String getManufacturer(){
  return manufacturer;
}
  1. Container-level constraints
private Map<@NotNull FuelConsumption, @MaxAllowedFuelConsumption Integer> fuelConsumption = new HashMap<>();
  1. Class level constraints
    In this case, the object of validation is not a single attribute, but a complete object. Class-level constraints are useful if validation depends on the correlation between multiple attributes of an object.
    For example, in a car, the number of passengers should not be greater than the number of seats, otherwise overloaded.
@ValidPassengerCount
public class Car {

    private int seatCount;

    private List<Person> passengers;

    //...
}
  1. Constraint inheritance
    When one class inherits / implements another class, all constraints declared by the parent class are applied to the corresponding attributes of subclass inheritance.
    If the method is overridden, the constraint annotations will be aggregated, that is, the constraints declared by the method's parent and child classes will work.

  2. Cascade verification
    The Bean Validation API not only allows validation of individual class instances, but also supports cascading validation.
    Just use @Valid to modify the reference to the object attribute, and all the constraints declared in the object attribute will also work.
    As the following example, the name field in the Person object is also validated when the Car instance is validated.

public class Car {
    @NotNull
    @Valid
    private Person driver;
    //...
}
public class Person {
    @NotNull
    private String name;
    //...
}

Declare method constraints

Parameter constraint

By adding constraint annotations to the parameters of a method or constructor to specify the preconditions of the method or constructor, the official examples are as follows:

public RentalStation(@NotNull String name){}

public void rentCar(@NotNull Customer customer,
                          @NotNull @Future Date startDate,
                          @Min(1) int durationInDays){}

Return value constraint

By adding constraint annotations to the body of a method to specify a postcondition for a method or constructor, the official example is as follows:

public class RentalStation {
    @ValidRentalStation
    public RentalStation() {
        //...
    }
    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getCustomers() {
        //...
        return null;
    }
}

This example specifies three constraints:

  • Any newly created Rational Station object must satisfy the @valid RentalStation constraint
  • The list of customers returned by getCustomers() cannot be empty and must contain at least one element
  • The list of customers returned by getCustomers() cannot contain empty objects

Cascade constraints

Similar to cascading validation of JavaBeans properties, the @Valid annotation can be used to tag cascading validation of method parameters and return values.

Similar to cascading validation of javabeans attributes (see Section 2.1.6, "Object Graph"), the @valid annotation can be used to mark the executable parameters and the return value of cascading validation. When validating parameters or return values annotated with @valid, constraints declared on parameters or return value objects are also validated.
Also, it can be used in container elements.

public class Garage {
    public boolean checkCars(@NotNull List<@Valid Car> cars) {
        //...
        return false;
    }
}

Inheritance verification

When declaring method constraints in an inheritance system, two rules must be understood:

  • Method callers cannot be enhanced in subtypes to satisfy preconditions
  • Method callers need to ensure that postconditions are no longer weakened in subtypes

These rules are determined by the concept of subclass behavior: wherever type T is used, subclasses of T can also be used without changing program behavior.

When two classes have the same name and the same list of parameters, and another class rewrites/implements the same name method of the two classes with one method, there can be no parameter constraint on the same name method of the two parent classes, because it will conflict with the above rules in any case.
Example:

public interface Vehicle {
  void drive(@Max(75) int speedInMph);
}
public interface Car {
  void drive(int speedInMph);
}
public class RacingCar implements Car, Vehicle {
  @Override
  public void drive(int speedInMph) {
      //...
  }
}

Group constraint

Request group

Note: The above 22 constraint annotations all have group attributes. Default groups are defaulted when groups are not specified.

The JSR specification supports manual validation and does not directly support the use of annotation validation, but spring provides extended support for grouping validation annotations, i.e. @Validated, with parameters set as group classes.

Group inheritance

In some scenarios, you need to define a group that contains constraints from other groups and can be inherited by groups.
Such as:

public class SuperCar extends Car {
    @AssertTrue(
            message = "Race car must have a safety belt",
            groups = RaceCarChecks.class
    )
    private boolean safetyBelt;
    // getters and setters ...
}
public interface RaceCarChecks extends Default {}

Define grouping sequence

By default, no matter which grouping the constraints belong to, their computations are not in a specific order, and in some scenarios, it is useful to control the computational order of the constraints.
For example, first check the default constraints of the car, then check the performance constraints of the car, and finally check the actual constraints of the driver before driving.
You can define an interface and use @GroupSequence to define the sequence of groups that need to be validated.
Example:

@GroupSequence({ Default.class, CarChecks.class, DriverChecks.class })
public interface OrderedChecks {}

This grouping usage is the same as other groupings except that the grouping has the function of checking in grouping order.

Groups that define sequences and groups that make up sequences cannot directly or indirectly participate in cyclic dependencies through cascade sequence definitions or group inheritance. GroupDefinitionException is triggered if a group containing such a loop is computed.

Redefining the default grouping sequence

@GroupSequence

@ GroupSequence allows you to redefine default groupings for specified classes in addition to defining grouping sequences. To do this, simply add @GroupSequence to the class and replace Default default groups with groups of specified sequences in the annotations.

@GroupSequence({ RentalChecks.class, CarChecks.class, RentalCar.class })
public class RentalCar extends Car {}

When validating constraints, validate them directly as the default grouping method

@GroupSequenceProvider

Note: This is provided for hibernate-validator, which is not supported by the JSR specification

It can be used to dynamically redefine the default grouping sequence according to the state of the object.
There are two steps to be taken:

  1. Implementation interface: Default Group Sequence Provider
  2. Use @GroupSequenceProvider on the specified class and specify value as the class in the previous step

Example:

public class RentalCarGroupSequenceProvider
        implements DefaultGroupSequenceProvider<RentalCar> {
    @Override
    public List<Class<?>> getValidationGroups(RentalCar car) {
        List<Class<?>> defaultGroupSequence = new ArrayList<Class<?>>();
        defaultGroupSequence.add( RentalCar.class );
        if ( car != null && !car.isRented() ) {
            defaultGroupSequence.add( CarChecks.class );
        }
        return defaultGroupSequence;
    }
}
@GroupSequenceProvider(RentalCarGroupSequenceProvider.class)
public class RentalCar extends Car {
    @AssertFalse(message = "The car is currently rented out", groups = RentalChecks.class)
    private boolean rented;
    public RentalCar(String manufacturer, String licencePlate, int seatCount) {
        super( manufacturer, licencePlate, seatCount );
    }
    public boolean isRented() {
        return rented;
    }
    public void setRented(boolean rented) {
        this.rented = rented;
    }
}

Packet switching

What if you want to validate car-related checks with driver checks? Of course, you can explicitly specify validation groups, but what if you want to validate them as part of the default group validation? Here @ConvertGroup comes into use, which allows you to use groups different from those originally requested during cascade validation.

Grouping transformations can be defined anywhere @Valid can be used, or multiple grouping transformations can be defined on the same element.
The following restrictions must be met:

  • @ ConvertGroup can only be used in conjunction with @Valid. If not, a ConstraintDeclarationException is thrown.
  • It is illegal to have multiple conversion rules with the same from value on the same element. In this case, a ConstraintDeclarationException is thrown.
  • The from attribute cannot refer to a grouping sequence. In this case, a Constraint Declaration Exception is thrown

Warning:

Rules are not implemented recursively. The first matching transformation rule will be used and subsequent rules will be ignored. For example, if a group @ConvertGroup declaration links group A to b and group b to c, group A will be converted to b instead of C.

Example:

// When the driver is null, no concatenated validation is performed, the default packet is used, and when the driver is concatenated validation, the Driver Checks packet is used.
@Valid
@ConvertGroup(from = Default.class, to = DriverChecks.class)
private Driver driver;

Create custom constraints

Simple constraint

Three steps:

  • Create a constraint annotation
  • Implementing a Verifier
  • Define a default error message

Create constraint annotations

Here's an example of writing an annotation to ensure that a given string is all capitalized or all lowercase.
First, define an enumeration that lists all cases: uppercase and lowercase

public enum CaseMode{
  UPPER,
  LOWER;
}

Then, define a constraint annotation

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented@Repeatable(List.class)
public @interface CheckCase {
    String message() default "{org.hibernate.validator.referenceguide.chapter06.CheckCase.message}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
    CaseMode value();

    @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        CheckCase[] value();
    }
}

The Bean Validation API specification requires any constraint annotation to define the following requirements:

  • A message attribute: Returns a default key in case of violation of a constraint to create an error message
  • A group attribute: Allows you to specify the authentication grouping to which this constraint belongs. Must default to an empty Class array
  • A payload attribute: Can be used by Bean Validation API clients to customize a annotated payload object. The API itself does not use this property. Custom payload can be used to define severity. As follows:
public class Severity{
  public interface Info extends Payload{}
  public interface Error extends Payload{}
}
public class ContactDetails{
  @NotNull(message="Name must be filled.", payload=Severity.Error.class)
  private String name;
  
  @NotNull(message="Mobile phone number is not specified, but it is not required to fill in", payload=Severity.Info.class)
  private String phoneNumber;
}

After the ContactDetails instance is validated, the client can obtain severity through ConstraintViolation.getConstraintDescriptor().getPayload(), and then adjust its behavior according to severity.
In addition, some meta-annotations are modified on constraint annotations:

  • @ Target: Specify the types of elements supported by this annotation, such as FIELD (attributes), METHOD (methods), etc.
  • @ Rentention(RUNTIME): Specifies that annotations of this type will be available at runtime by reflection
  • @ Constraint(): Marks the type of annotation as a constraint, specifying the validator used by the annotation (the class that writes validation logic), and if the constraint can be used in multiple data types, each data type corresponds to a validator.
  • @ Documented: This annotation will be included in the JavaDoc used
  • @ Repeatable(List.class): Indicates that annotations can be repeated multiple times in the same location, usually with different configurations. List contains annotation types.

Validator

To create an annotation, you also need to create a constraint validator to validate elements that use annotations.

The Bean Validation interface needs to be implemented: ConstraintValidator
Example:

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
    private CaseMode caseMode;
    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }
    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if ( object == null ) {
            return true;
        }
        if ( caseMode == CaseMode.UPPER ) {
            return object.equals( object.toUpperCase() );
        }else {
            return object.equals( object.toLowerCase() );
        }
    }
}

ConstraintValidator specifies two generic types:

  1. The first is to specify annotation classes that need to be validated
  2. The second is to specify the type of data to be validated. When annotations support multiple types, you write multiple implementation classes and specify the corresponding types separately.

Two approaches need to be implemented:

  • initialize() allows you to get the parameters specified when using annotations (you can save them for future use)
  • isValid() contains the actual validation logic. Note: The Bean Validation specification recommends null values as valid values. If an element null is not a valid value, the @NotNull annotation should be displayed.

Constraint ValidatorContext object parameter in isValid() method:

When the specified constraint validator is applied, context data and operations are provided.

This object has at least one ConstraintViolation, either default or custom.

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

    boolean isValid;
    if ( caseMode == CaseMode.UPPER ) {
        isValid = object.equals( object.toUpperCase() );
    }
    else {
        isValid = object.equals( object.toLowerCase() );
    }

    if ( !isValid ) {
    // Disable the default Constraint Violation and customize one
        constraintContext.disableDefaultConstraintViolation();
        constraintContext.buildConstraintViolationWithTemplate(
                "{org.hibernate.validator.referenceguide.chapter06." +
                "constraintvalidatorcontext.CheckCase.message}"
        )
        .addConstraintViolation();
    }

    return isValid;
}

The official example above shows disabling default messages and customizing an error message prompt.
hibernate-validator provides a ConstraintValidator extension interface, which is not detailed here.

public interface HibernateConstraintValidator<A extends Annotation, T> extends ConstraintValidator<A, T> {
  default void initialize(ConstraintDescriptor<A> constraintDescriptor, HibernateConstraintValidatorInitializationContext initializationContext) {}
}

Pass payload parameters to verifier

At present, it needs to be implemented through Hibernate Constraint Validator. Refer to the following official examples, which are not described in detail here.

HibernateValidatorFactory hibernateValidatorFactory = Validation.byDefaultProvider()
        .configure()
        .buildValidatorFactory()
        .unwrap( HibernateValidatorFactory.class );

Validator validator = hibernateValidatorFactory.usingContext()
        .constraintValidatorPayload( "US" )
        .getValidator();

// [...] US specific validation checks
validator = hibernateValidatorFactory.usingContext()
        .constraintValidatorPayload( "FR" )
        .getValidator();

// [...] France specific validation checks
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;
    }
}

message

When a constraint is violated, the message should be used
You need to define a ValidationMessages.properties file and record the following:

# Org. hibernate. validator. reference guide. chapter06. CheckCase is the full name of the annotated CheckCase
org.hibernate.validator.referenceguide.chapter06.CheckCase.message=Case mode must be {value}.

If a validation error occurs, the validation runtime will use the default value specified for the annotated message attribute to find the error message in this resource bundle.

Class level constraints

Class level constraints are used to verify the state of the entire object. It is defined in the same way as the simple constraint definition mentioned above. Only the value in @Target needs to contain TYPE.

Use as a custom property annotation

Because class-level constraint validators can obtain all the attributes of such instances, they can be used to constrain some of them.

public class ValidPassengerCountValidator
        implements ConstraintValidator<ValidPassengerCount, Car> {

    @Override
    public void initialize(ValidPassengerCount constraintAnnotation) {}

    @Override
    public boolean isValid(Car car, ConstraintValidatorContext constraintValidatorContext) {
        if ( car == null ) {
            return true;
        }
        // To verify that a relationship must be satisfied between two attributes
        // Verify that the number of passengers should not be greater than the number of seats
        boolean isValid = car.getPassengers().size() <= car.getSeatCount();

        if ( !isValid ) {
            constraintValidatorContext.disableDefaultConstraintViolation();
            constraintValidatorContext
                    .buildConstraintViolationWithTemplate( "{my.custom.template}" )
                    .addPropertyNode( "passengers" ).addConstraintViolation();
        }

        return isValid;
    }
}

Combination constraint

@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { })
@Documented
public @interface ValidLicensePlate {
    String message() default "{org.hibernate.validator.referenceguide.chapter06." +
            "constraintcomposition.ValidLicensePlate.message}";

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

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

An annotation has the function of multiple annotations, and this combined annotation usually does not need to specify a validator. This annotation is validated with a set of violations of all constraints. If you want to violate one of these constraints, you can use @ReportAsSingleViolation.

//...
@ReportAsSingleViolation
public @interface ValidLicensePlate {

    String message() default "{org.hibernate.validator.referenceguide.chapter06." +
            "constraintcomposition.reportassingle.ValidLicensePlate.message}";

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

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

Practical example

// Entity class
/** Validation parameters are set to qualified default values */
@Data
public class ValidatorVO {

  @NotBlank private String name = "1";

  @Min(0)
  @Max(200)
  private Integer age = 20;

  @PastOrPresent
  @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  private LocalDateTime birthday = LocalDateTime.now().minusDays(1);

  @Digits(integer = 4, fraction = 2)
  @DecimalMax(value = "1000")
  @DecimalMin(value = "0")
  private BigDecimal money = new BigDecimal(10);

  @Email private String email = "123456@qq.com";

  @NotNull private String username = "username";

  @Size(max = 2)
  private List<String> nickname;

  @Positive /*(message = "Height should not be negative.*/ private Double height = 100D;

  @FutureOrPresent
  @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  private LocalDateTime nextBirthday = LocalDateTime.now().plusDays(1);
}

When using this object, you need to validate it, and then decorate it with the @Valid annotation.

Cascade verification

Note: Attributes that require cascading validation need to be modified with @Valid annotations, such as:

// Validation parameters are set to qualified default values
@NotNull @Valid private HairVO hair = new HairVO();

/** Validation parameters are set to qualified default values */
@Data
public class HairVO {
  @Positive private Double length = 10D;
  @Positive private Double Diameter = 1D;
  @NotBlank private String color = "black";
}

Grouping

Request grouping

Ordinary grouping here refers to a single interface without inheritance.

// Grouping: Identify with an empty interface
public interface HasIdGroup {}
@Data
public class ValidatorManual {
  @NotNull(groups = HasIdGroup.class)
  private Integer id;
}
  /**
   * Group check
   * Check annotations don't work when groupings don't match. Note: Default groupings don't work either.
   * <p>
   * Unlike the implementation of the JSR-303(javax.validate) specification, it provides an extended implementation of the JSR-303 group
   */
  @PostMapping
  public boolean addUser(@Validated(NoIdGroup.class) ValidatorVO user, BindingResult result) {
    if (result.hasErrors()) {
      for (ObjectError error : result.getAllErrors()) {
        log.error(error.getDefaultMessage());
      }
      return false;
    }
    return true;
  }

  /**
   * Group check
   * Check annotations work when grouping matches, but here only HasIdGroup groupings are checked, and default groupings are not checked.
   * <p>
   * Unlike the implementation of the JSR-303(javax.validate) specification, it provides an extended implementation of the JSR-303 group
   */
  @PutMapping
  public boolean updateUser(@Validated(HasIdGroup.class) ValidatorVO user, BindingResult result) {
    if (result.hasErrors()) {
      for (ObjectError error : result.getAllErrors()) {
        log.error(error.getDefaultMessage());
      }
      return false;
    }
    return true;
  }

Group inheritance

If you want the default grouping to work and other groupings to check, how do you do it?
When in use, multiple groups can be specified to verify, as follows:

public boolean addUser1(@Validated({Default.class,NoIdGroup.class}) 
ValidatorVO user, BindingResult result){}

But here, because the Default grouping is always checked, each time with some redundancy, it is recommended that the group inherit the default grouping at the time of definition, as follows:

public interface DefaultInherGroup extends Default {}
/** Validation parameters are set to qualified default values */
@Data
public class ValidatorVO {
 
  @NotNull (groups = HasIdGroup.class)
  // Plus inheritance grouping
  @NotNull (groups = DefaultInherGroup.class)
  private Integer id = 1;
}

test

Simple test

/**
 * Interface. Objects to be tested are decorated with @Valid
 */
@Slf4j
@RequestMapping("/user")
@RestController
public class ValidatorController {

  @GetMapping
  public boolean getUser(@Valid ValidatorVO user, BindingResult result) {
    if (result.hasErrors()) {
      for (ObjectError error : result.getAllErrors()) {
        log.error(error.getDefaultMessage());
      }
      return false;
    }
    return true;
  }
}
// Test class

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootExampleApplicationTests {

  @Autowired WebApplicationContext context;

  private MockMvc mvc;
  private DateTimeFormatter formatter;

  @Before
  public void setMvc() throws Exception {
    mvc = MockMvcBuilders.webAppContextSetup(context).build();
    formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  }

  @Test
  public void verificationFailedWhenNameIsBlank() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("name", ""))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenAgeGreaterThan200() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("age", "201"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenBirthdayIsFuture() throws Exception {
    mvc.perform(
            MockMvcRequestBuilders.get("/user")
                .param("birthday", formatter.format(LocalDateTime.now().plusDays(1))))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenMoneyGreaterThan1000() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("money", "1001"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenFractionOverflow() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("money", "999.222"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenFractionOverflowAndGreaterThan1000() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("money", "1001.222"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenEmailNotMatchFormat() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("email", "111222@"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenUsernameIsNull() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("username", null))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenNicknameGreaterThan2() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("nickname", "Xiao Ming", "Blue", "Xiaolan"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenHeightIsNotPositive() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("height", "0"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void verificationFailedWhenNextBirthdayIsPast() throws Exception {
    mvc.perform(
            MockMvcRequestBuilders.get("/user")
                .param("nextBirthday", formatter.format(LocalDateTime.now().minusDays(1))))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }
}

Cascade test

  /** Cascade validation: Validation fails when one of the attributes contained in the validation attribute object does not meet the requirements */
  @Test
  public void verificationFailedWhenPropertiesNotPassVerification() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/user").param("hair.length", "-1"))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

Group testing

Request grouping

// ValidatorController.java
  /**
   * Group check
   * Check annotations do not work when grouping mismatches occur
   * <p>
   * Unlike the implementation of the JSR-303(javax.validate) specification, it provides an extended implementation of the JSR-303 group
   */
  @PostMapping
  public boolean addUser(@Validated(NoIdGroup.class) ValidatorVO user, BindingResult result) {
    if (result.hasErrors()) {
      for (ObjectError error : result.getAllErrors()) {
        log.error(error.getDefaultMessage());
      }
      return false;
    }
    return true;
  }
  /**
   * Group check
   * Check annotations work when grouping matches
   * <p>
   * Unlike the implementation of the JSR-303(javax.validate) specification, it provides an extended implementation of the JSR-303 group
   */
  @PutMapping
  public boolean updateUser(@Validated(HasIdGroup.class) ValidatorVO user, BindingResult result) {
    if (result.hasErrors()) {
      for (ObjectError error : result.getAllErrors()) {
        log.error(error.getDefaultMessage());
      }
      return false;
    }
    return true;
  }
  
  /**
   * Group check
   * Specify multiple groupings for matching
   * <p>
   * Unlike the implementation of the JSR-303(javax.validate) specification, it provides an extended implementation of the JSR-303 group
   */
  @PostMapping("/1")
  public boolean addUser1(@Validated({Default.class,NoIdGroup.class}) ValidatorVO user, BindingResult result) {
    if (result.hasErrors()) {
      for (ObjectError error : result.getAllErrors()) {
        log.error(error.getDefaultMessage());
      }
      return false;
    }
    return true;
  }
/** Annotation validation, which is provided by spring annotations */
  @Test
  public void validateFailedWhenGroupMatched() throws Exception {
    mvc.perform(MockMvcRequestBuilders.put("/user").param("id", ""))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }

  @Test
  public void validateSucWhenGroupNotMatched() throws Exception {
    mvc.perform(MockMvcRequestBuilders.post("/user").param("id", "").param("name", ""))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }
  
    /** Matched groupings work, mismatched groupings don't. */
  @Test
  public void validateFailedByGroup() throws Exception {
    mvc.perform(MockMvcRequestBuilders.post("/user/1").param("id", "").param("name", ""))
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.content().string("true"));
  }
/** Manual tool validation, provided by the JSR specification */
  @Test
  public void validateSucWhenGroupNotMatched() {
    ValidatorManual vm = new ValidatorManual();
    Set<ConstraintViolation<ValidatorManual>> validateResult = validator.validate(vm);
    assertEquals(0, validateResult.size());
  }

  @Test(expected = AssertionError.class)
  public void validateFailedWhenGroupMatched() {
    ValidatorManual vm = new ValidatorManual();
    Set<ConstraintViolation<ValidatorManual>> validateResult =
        validator.validate(vm, HasIdGroup.class);
    for (ConstraintViolation msg : validateResult) {
      log.error(msg.getMessage());
    }
    assertEquals(0, validateResult.size());
  }

Group inheritance

  // ValidatorController.java
@GetMapping("/1")
public boolean getUser1(@Validated(DefaultInherGroup.class) ValidatorVO user, BindingResult result) {
if (result.hasErrors()) {
  for (ObjectError error : result.getAllErrors()) {
    log.error(error.getDefaultMessage());
  }
  return false;
}
return true;
}
// Test class
@Test
public void validateFailedWhenGroupMatched1() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user/1").param("id", "").param("name", ""))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andExpect(MockMvcResultMatchers.content().string("true"));
}

Further understanding

hibernate-validator is an interface provided by Java SPI mechanism, so as long as the implementation class exists in the class path, it is possible to use javax. validate. xxxxx in the code. If you need to switch the implementation class, you can replace the implementation class. The code used need not be changed.

Usage scenarios

There are many places to validate data. Using such a validation framework will be too convenient, with fewer code and fewer bug s. If you think that the way of prompting is not friendly enough, you can reasonably expand message alerts, message internationalization and so on, and you can also use AOP to process validation information in a unified way.

Reference material

Bean Validation 2.0 (JSR 380)

hibernate-validator latest official information

hibernate-validator | github
Public Number: Yifeilu (focusing on in-depth learning of knowledge in the Java field, from source code to principle, systematic and orderly learning)

Posted by upperbid on Mon, 23 Sep 2019 06:54:00 -0700