Spring Boot custom parameter validator

Posted by JamieThompson on Sun, 26 Dec 2021 23:33:57 +0100

Customize parameter validators in Spring Boot

After we introduce the starter dependency of spring boot starter validation in the project, we can directly use annotations such as @ NotNull and @ Length on the request method of the Controller for parameter verification. The bottom layer of the starter uses the verifier provided by hibernate validator for verification. Although it provides rich verifiers, sometimes we need to define the parameter verifier ourselves in order to verify the parameters more conveniently.

Prepare environment

The project must have a starter dependency of spring boot starter validation:

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>

Next, a practical example is given to illustrate the process of customizing the verifier.

If we need to verify the password provided by the user when the user registers or changes the password, the password must comply with the following rules:
The password must contain upper and lower case letters, numbers and special symbols, and the length is 8 ~ 16.
First, we use the built-in @ Pattern annotation to verify the password, as follows:

@Validated
@Controller
public class UserCenterController {
    @PostMapping("/user/modifyPassword")
    @RequiredLogin
    @ResponseBody
    public ResponseResult modifyPassword(@NotBlank(message = "Please enter the original password") String oldPassword,
                                         @NotBlank(message = "Please enter a new password")
                                         @Pattern(regexp = "^(?=.*\\\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%&*]).{8,16}$",
                                                  message = "The password must contain upper and lower case letters, numbers, special symbols and be 8 in length~16!")
                                                 String newPassword){
        return loginUserService.modifyPassword(oldPassword,newPassword);
    }
}

That's OK, but it's still a little cumbersome.
The most ideal effect is that the password verification can be completed with only one annotation and no other.

Custom verification annotation

Hibernate validator provides many validator annotations, such as @ Email. Refer to it to customize an annotation called @ Password:

package com.gyb.questionnaire.controller.validator;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

import com.gyb.questionnaire.util.PatternMatchers;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
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;

/**
 * Custom password verifier annotation
 *
 * @author cloudgyb
 * @since 2021/8/8 13:22
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(Password.List.class)
@Documented
@Constraint(validatedBy = {PasswordValidator.class})
public @interface Password {
    String message() default "The password must contain upper and lower case letters, numbers, special symbols and be 8 in length~16!";
    /**
     * Custom regular expression, default password regular expression patternmatchers passwordRegx
     */
    String regexp() default "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%&*]).{8,16}$";
    /**
     * @return the groups the constraint belongs to
     */
    Class<?>[] groups() default {};

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

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

The message attribute specifies the error message thrown when the verification fails.
The regexp attribute allows users to customize their own password verification rules (regular expressions).
The groups property specifies the verification group.

@Constraint(validatedBy = {PasswordValidator.class}) this annotation specifies the implementation class PasswordValidator of @ Password verification. See the specific implementation below.

Custom verifier implementation class

Hibernate validator has strong extensibility. To implement a custom validator, you only need to implement the ConstraintValidator interface!
The constraintvalidator < a extensions annotation, t > interface has two generic parameters:

  • A. It must be an annotation, which is our custom @ Password verification annotation.
  • T. It refers to the type of parameter to be verified. Here, the password is verified, and the type is CharSequence.
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

/**
 * Custom password verifier
 *
 * @author cloudgyb
 * @since 2021/8/8 13:32
 */
public class PasswordValidator implements ConstraintValidator<Password, CharSequence> {
    private Pattern pattern;

    @Override
    public void initialize(Password constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
        final String regexp = constraintAnnotation.regexp();
        this.pattern = Pattern.compile(regexp);
    }

    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (value == null || value.length() == 0)
            return false;
        final Matcher matcher = this.pattern.matcher(value);
        return matcher.matches();
    }
}

Just implement the initialize and isValid methods!
isValid method verifies the password according to the regular expression specified by the user.

Using custom validators

 @PostMapping("/user/modifyPassword")
 @RequiredLogin
 @ResponseBody
 public ResponseResult modifyPassword(@NotBlank(message = "Please enter the original password") String oldPassword
                                      @Password String newPassword){
     return loginUserService.modifyPassword(oldPassword,newPassword);
 }

Now you only need a @ password annotation to complete the password verification (there is no need for null verification, because the PasswordValidator implements null verification).

Topics: Spring