Detailed explanation of spring boot validation parameter validation custom annotation rules and grouping validation

Time:2021-12-9

preface

Hibernate validator is the reference implementation of bean validation. Hibernate validator provides the implementation of all built-in constraints in JSR 303 specification. In addition, there are some additional constraints
In daily development, hibernate validator is often used to verify the fields of beans. Based on annotations, it is convenient, fast and efficient.

It can be used in springboot@ValidatedFor annotation, hibernate validator enhanced version can also be used@ValidOriginal bean validation java version

Built in verification annotation

Built in constraint in bean validation

annotation effect
@Valid The annotated element is an object, and all field values of this object need to be checked
@Null The annotated element must be null
@NotNull The annotated element must not be null
@AssertTrue The annotated element must be true
@AssertFalse The annotated element must be false
@Min(value) The annotated element must be a number and its value must be greater than or equal to the specified minimum value
@Max(value) The annotated element must be a number and its value must be less than or equal to the specified maximum value
@DecimalMin(value) The annotated element must be a number and its value must be greater than or equal to the specified minimum value
@DecimalMax(value) The annotated element must be a number and its value must be less than or equal to the specified maximum value
@Size(max, min) The size of the annotated element must be within the specified range
@Digits (integer, fraction) The annotated element must be a number and its value must be within an acceptable range
@Past The annotated element must be a past date
@Future The annotated element must be a future date
@Pattern(value) The annotated element must conform to the specified regular expression

Constraint attached to hibernate validator

annotation effect
@Email The annotated element must be an email address
@Length(min=, max=) The size of the annotated string must be within the specified range
@NotEmpty The of the annotated string must be non empty
@Range(min=, max=) The annotated element must be within the appropriate range
@NotBlank The of the annotated string must be non empty
@URL(protocol=,host=, port=, regexp=, flags=) The annotated string must be a valid URL
@CreditCardNumber The annotated string must pass the Luhn verification algorithm. Generally, Luhn is used to calculate the legitimacy of bank card, credit card and other numbers
@ScriptAssert(lang=, script=, alias=) There should be the implementation of Java scripting API, namely JSR 223 (“scripting for the JavaTM platform”)
@SafeHtml(whitelistType=,additionalTags=) There should be a jsoup package in the classpath

Message supports expressions and El expressions, such as message = “name length is limited to {min} to {Max} ${1 + 2}”)

If you want to write the error description to properties uniformly, create a new validationmessages under the classpath_ zh_ CN. Properties file (note that value needs to be converted to unicode encoding), and then use placeholders in {} format

In the hibernate supplementary annotations, the last three are not commonly used and can be ignored.
The following three annotations @ notnull @ notempty @ notblank are distinguished:

  1. @Notnull the value of any object cannot be null
  2. @The element of the notempty collection object is not 0, that is, the collection is not empty, or it can be used for strings that are not null
  3. @Notblank can only be used when the string is not null, and the length after string trim() must be greater than 0

Group check

If the same parameter requires different verification rules to be applied in different scenarios, group verification is required. For example, if a newly registered user has not named a name, we allow the name field to be empty, but we do not allow the name to be updated to empty characters during update.

Group verification has three steps:

  1. Define a grouping class (or interface)
public interface Update extends Default{
}
  1. Add the groups attribute on the verification annotation to specify the groups
public class UserVO {
    @Notblank (message = "name cannot be empty", groups = update. Class)
    private String name;
    //Omit other code
}
  1. Add a grouping class with the @ validated annotation of the controller method
@PostMapping("update")
public ResultInfo update(@Validated({Update.class}) UserVO userVO) {
    return new ResultInfo().success(userVO);
}

The custom update grouping interface inherits the default interface. The verification annotation (e.g. @ notblank) and @ validated belong to default Class grouping, which is in javax validation. groups. It is described in the default note

/**
 * Default Jakarta Bean Validation group.
 * <p>
 * Unless a list of groups is explicitly defined:
 * <ul>
 *     <li>constraints belong to the {@code Default} group</li>
 *     <li>validation applies to the {@code Default} group</li>
 * </ul>
 * Most structural constraints should belong to the default group.
 *
 * @author Emmanuel Bernard
 */
public interface Default {
}

When writing the update grouping interface, if default is inherited, the following two methods are equivalent:
@Validated({Update.class}),@Validated({Update.class,Default.class})
If update does not inherit default, @ validated ({update. Class}) will only verify that it belongs to update Class grouped parameter fields

Recursive check

If an attribute of the ordervo class is added to the uservo class and the attributes in the ordervo also need to be verified, recursive verification is used, as long as the corresponding attribute is added@ValidAnnotations can be implemented (the same applies to collections)

public class OrderVO {
    @NotNull
    private Long id;
    @Notblank (message = "itemname cannot be empty")
    private String itemName;
    //Omit other code
}
public class UserVO {
    @Notblank (message = "name cannot be empty", groups = update. Class)
    private String name;
    //Ordervo requiring recursive verification
    @Valid
    private OrderVO orderVO;
    //Omit other code
}

Custom verification

validationIt provides us with so many features that it can almost meet most parameter verification scenarios in daily development. However, a good framework must be easy to extend. With the expansion capability, you can cope with more complex business scenarios. After all, the only constant in the development process is the change itself. Validation allows users to customize validation

The implementation is simple in two steps:

  1. Custom verification annotation
package cn.soboys.core.validator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;


/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:49 
 *Date validation constraint annotation class
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint (validatedby = {isdatetimevalidator. Class}) // indicates which class performs verification logic
public @interface IsDateTime {

    //The default message returned in case of verification error
    String message() default "date format error";
    //Group check
    Class<?>[] groups() default {};

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

    //Here are the attributes I define myself
    boolean required() default true;

    String dateFormat() default "yyyy-MM-dd";


}

be careful:messageThis field is required to display error messages,Groups and payload are also required
@Constraint (validatedby = {handsomeboyvalidator. Class}) is used to specify the class that handles this annotation logic

  1. Write verifier class
package cn.soboys.core.validator;

import cn.hutool.core.util.StrUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:51
 *Date verifier
 */
public class IsDateTimeValidator implements ConstraintValidator<IsDateTime, String> {

    private boolean required = false;
    private String dateFormat = "yyyy-MM-dd";

    /**
     *Used to initialize the value on the annotation to this validator
     * @param constraintAnnotation
     */
    @Override
    public void initialize(IsDateTime constraintAnnotation) {
        required = constraintAnnotation.required();
        dateFormat = constraintAnnotation.dateFormat();
    }

    /**
     *Specific verification logic
     * @param value
     * @param context
     * @return
     */
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (required) {
            return ValidatorUtil.isDateTime(value, dateFormat);
        } else {
            if (StrUtil.isBlank(value)) {
                return true;
            } else {
                return ValidatorUtil.isDateTime(value, dateFormat);
            }
        }
    }

}

Note that I extracted the validation logic here and wrote a tool class separately,ValidatorUtil

package cn.soboys.core.validator;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author kenx
 * @version 1.0
 * @date 2021/1/21 20:51
 *Validation expression
 */
public class ValidatorUtil {
    private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
    private static final Pattern money_pattern = Pattern.compile("^[0-9]+\\.?[0-9]{0,2}$");

    /**
     *Verify phone number
     *
     * @param src
     * @return
     */
    public static boolean isMobile(String src) {
        if (StrUtil.isBlank(src)) {
            return false;
        }
        Matcher m = mobile_pattern.matcher(src);
        return m.matches();
    }


    /**
     *Verify whether the enumeration value is legal. All enumerations need to inherit this method override
     *
     *@ param beanclass enumeration class
     *@ param status corresponding code
     * @return
     * @throws Exception
     */
    public static boolean isEnum(Class<?> beanClass, String status) throws Exception {
        if (StrUtil.isBlank(status)) {
            return false;
        }

        //Conversion enumeration class
        Class<Enum> clazz = (Class<Enum>) beanClass;
        /**
         *Enumeration is actually a syntax sugar
         *Is a real column of multiple encapsulated enum classes
         *Get all enumerated instances
         */
        Enum[] enumConstants = clazz.getEnumConstants();

        //Get method by method name
        Method getCode = clazz.getMethod("getCode");
        Method getDesc = clazz.getMethod("getDesc");
        for (Enum enums : enumConstants) {
            //Get enumeration instance name
            String instance = enums.name();
            //Execute the enumeration method to obtain the value corresponding to the enumeration instance
            String code = getCode.invoke(enums).toString();
            if (code.equals(status)) {
                return true;
            }
            String desc = getDesc.invoke(enums).toString();
            System. out. Println (strformatter. Format ("real column {}--- Code: {} desc {}", instance, code, DESC));
        }
        return false;
    }

    /**
     *Verification amount 0.00
     *
     * @param money
     * @return
     */
    public static boolean isMoney(BigDecimal money) {
        if (StrUtil.isEmptyIfStr(money)) {
            return false;
        }
        if (!NumberUtil.isNumber(String.valueOf(money.doubleValue()))) {
            return false;
        }
        if (money.doubleValue() == 0) {
            return false;
        }
        Matcher m = money_pattern.matcher(String.valueOf(money.doubleValue()));
        return m.matches();
    }


    /**
     *Verification date
     *
     * @param date
     * @param dateFormat
     * @return
     */
    public static boolean isDateTime(String date, String dateFormat) {
        if (StrUtil.isBlank(date)) {
            return false;
        }
        try {
            DateUtil.parse(date, dateFormat);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

}

I customized and supplemented many validators, including date verification, enumeration verification, mobile number verification and amount verification

Detailed explanation of spring boot validation parameter validation custom annotation rules and grouping validation

image.png

The user-defined verification annotation is no different from the built-in annotation. You can add corresponding annotations to the required fields

Verification process analysis

The whole process of parameter validation using the validation API is shown in the figure below. The user accesses the interface and then performs parameter validation. If the validation passes, it enters the business logic. Otherwise, an exception is thrown and handed over to the global exception processor for processing

Detailed explanation of spring boot validation parameter validation custom annotation rules and grouping validation

image.png

If the global exception comes out, please refer to my articleSpring boot elegant global exception handling

In my official account of apexParameter verificationGet full customizationSource code