[SpringBoot] Use the validator framework to elegantly verify parameters in SpringBoot

1. Why do we need to verify parameters?

In daily development, in order to prevent illegal parameters from affecting the business, the parameters of the interface need to be verified so that they can be stored correctly.
For example: when logging in, you need to determine whether the user name, password and other information are empty. Although the front-end also has verification, for the security of the interface, it is still necessary to perform parameter verification on the back-end interface.

At the same time, in order to verify parameters more elegantly, the method is introduced here Spring Validation.

The Java API specification (JSR303: a sub-specification in JAVA EE 6, called Bean Validation) defines the standard validation-api for Bean verification, but does not provide an implementation. hibernate validation is an implementation of this specification and adds verification annotations. Such as: @Email, @Length.

JSR official website

Hibernate Validator official website

Spring Validation is a secondary encapsulation of hibernate validation, used to support automatic verification of spring mvc parameters.

2. Introduce dependencies

If the spring-boot version is less than 2.3.x, spring-boot-starter-web will automatically pass in the hibernate-validator dependency. If the spring-boot version is greater than or equal to 2.3.x, you need to manually introduce dependencies.

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.0.Final</version>
</dependency>

For web services, in order to prevent illegal parameters from affecting the business, parameter verification must be done at the Controller layer! In most cases, request parameters are divided into the following two forms:

  • POST, PUT requests, use @requestBodyto receive parameters
  • GET request, use @requestParam、@PathVariableto receive parameters

Next, let’s briefly introduce it~~~


3. @requestBody parameter verification

For POST and PUT requests, the backend generally uses @requestBody + 对象receive parameters. At this point, you only need to add @Validated 或 @Validannotations to the object to easily implement automatic parameter verification. If the verification fails, MethodArgumentNotValidExceptionan exception will be thrown.

UserVo: Add verification annotation

@Data
public class UserVo {
    
    

	private Long id;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}

UserController :

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid UserVo userVo) {
    
    
        return "addUser";
    }
}

Or use @Validatedannotations:

@PostMapping("/addUser")
public String addUser(@RequestBody @Validated UserVo userVo) {
    
    
    return "addUser";
}

4. @requestParam, @PathVariable parameter verification

GET requests generally use @requestParam、@PathVariableannotations to receive parameters. If there are many parameters (for example, more than 5), it is recommended to use object reception. Otherwise, it is recommended to flatten the parameters into the method input parameters one by one.

In this case, you must annotate the Controller class @Validatedand declare constraint annotations (such as: @Min) on the input parameters. If the verification fails, ConstraintViolationExceptionan exception will be thrown

@RestController
@RequestMapping("/user")
@Validated
public class UserController {
    
    

    @GetMapping("/getUser")
    public String getUser(@Min(1L) Long id) {
    
    
        return "getUser";
    }
}

5. Unified exception handling

If the verification fails, an exception will be MethodArgumentNotValidExceptionthrown ConstraintViolationException. In actual project development, unified exception handling is usually used to return a more friendly prompt.

@RestControllerAdvice
public class ExceptionControllerAdvice {
    
    

    @ExceptionHandler({
    
    MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
    
    
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
    
    
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        return "参数校验失败" + msg;
    }

    @ExceptionHandler({
    
    ConstraintViolationException.class})
    public String handleConstraintViolationException(ConstraintViolationException ex) {
    
    
        return "参数校验失败" + ex;
    }

}

6. Group verification

In actual projects, multiple methods may need to use the same class object to receive parameters, and the verification rules of different methods are likely to be different. At this time, simply adding constraint annotations to the fields of the class cannot solve the problem. Therefore, spring-validationthe group verification function is supported, specifically used to solve this type of problem.

For example: when saving User, userId is nullable, but when updating User, the value of userId must be >= 1L; the verification rules for other fields are the same in both cases. The code example for using group verification at this time is as follows:

Declare applicable grouping information on the constraint annotationgroups

1: Define the grouping interface

public interface ValidGroup extends Default {
    
    
	// 添加操作
    interface Save extends ValidGroup {
    
    }
    // 更新操作
    interface Update extends ValidGroup {
    
    }
	// ...
}

Why inherit Default? There are below.

2: Assign groups to the fields that need to be verified

@Data
public class UserVo {
    
    
	
    @Null(groups = ValidGroup.Save.class, message = "id要为空")
    @NotNull(groups = ValidGroup.Update.class, message = "id不能为空")
    private Long id;

    @NotBlank(groups = ValidGroup.Save.class, message = "用户名不能为空")
    @Length(min = 2, max = 10)
    private String userName;

    @Email
    @NotNull
    private String email;
}

According to the check field:

  • id: Assignment group: Save, Update. When adding, it must be null; when updating, it must not be null.
  • userName: Assign group: Save. When adding, it must not be empty
  • email: Assignment group: None. That is: use the default grouping

3: Specify groups for parameters that need to be verified

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Validated(ValidGroup.Save.class) UserVo userVo) {
    
    
        return "addUser";
    }

    @PostMapping("/updateUser")
    public String updateUser(@RequestBody @Validated(ValidGroup.Update.class) UserVo userVo) {
    
    
        return "updateUser";
    }
}

Test verification.

4: Default grouping

If ValidGroupthe interface does not inherit Defaultthe interface, then emailthe field cannot be verified (no grouping is assigned); after inheritance, ValidGroupit belongs to Defaultthe type, that is: the default group/so, it can be emailverified

7. Nested verification

@ValidAnnotations are required

@Data
public class UserVo {
    
    

	@NotNull(groups = {
    
    ValidGroup.Save.class, ValidGroup.Update.class})
    @Valid
    private Address address;
}

8. Customized verification

Case 1. Customized verification of encrypted id

Suppose we customize the encrypted ID (composed of numbers or letters af, 32-256 length) verification, which is mainly divided into two steps:

1. Custom constraint annotations

@Target({
    
    METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
    
    EncryptIdValidator.class}) // 自定义验证器
public @interface EncryptId {
    
    

    // 默认错误消息
    String message() default "加密id格式错误";
    // 分组
    Class<?>[] groups() default {
    
    };
    // 负载
    Class<? extends Payload>[] payload() default {
    
    };
}

2. Write a constraint checker

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
    
    

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
    
    
        if (value != null) {
    
    
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

3.Use

@Data
public class UserVo {
    
    

    @EncryptId
    private String id;
}

Case 2: Custom gender verification only allows two values

The sex attribute in the UserVo class only allows the front-end to pass the two enumeration values ​​M and F. How to implement it?

1. Custom constraint annotations

@Target({
    
    METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
    
    SexValidator.class})
public @interface SexValid {
    
    


    // 默认错误消息
    String message() default "value not in enum values";

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

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

    String[] value();
}

2. Write a constraint checker

public class SexValidator implements ConstraintValidator<SexValid, String> {
    
    

    private List<String> sexs;

    @Override
    public void initialize(SexValid constraintAnnotation) {
    
    
        sexs = Arrays.asList(constraintAnnotation.value());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
    
    
        if (StringUtils.isEmpty(value)) {
    
    
            return true;
        }
        return sexs.contains(value);
    }

}

3.Use

@Data
public class UserVo {
    
    

    @SexValid(value = {
    
    "F", "M"}, message = "性别只允许为F或M")
    private String sex;

}

4. Test

@GetMapping("/get")
private String get(@RequestBody @Validated UserVo userVo) {
    
    
    return "get";
}

9. Implement verification business rules

Business rule verification means that the interface needs to meet certain specific business rules. For example: users of the business system need to ensure their uniqueness. User attributes cannot conflict with other users, and are not allowed to overlap with the user name, mobile phone number, and email address of any existing user in the database. This requires that when creating a user, it is necessary to verify whether the user name, mobile phone number, and email address are registered; when editing a user, the information cannot be modified to the attributes of an existing user.

The most elegant implementation method should be to refer to the standard method of Bean Validation and use custom verification annotations to complete business rule verification.

1. Custom constraint annotations

First we need to create two custom annotations for business rule verification:

  • UniqueUser: Indicates that a user is unique. The uniqueness includes: user name, mobile phone number, and email address.
  • NotConflictUser: Indicates that a user's information is conflict-free. Conflict-free means that the user's sensitive information does not overlap with other users.
@Documented
@Retention(RUNTIME)
@Target({
    
    FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.UniqueUserValidator.class)
public @interface UniqueUser {
    
    

    String message() default "用户名、手机号码、邮箱不允许与现存用户重复";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };

}
@Documented
@Retention(RUNTIME)
@Target({
    
    FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.NotConflictUserValidator.class)
public @interface NotConflictUser {
    
    

    String message() default "用户名称、邮箱、手机号码与现存用户产生重复";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };

}

2. Write a constraint checker

For custom validation annotations to take effect, you need to implement ConstraintValidatorthe interface. The first parameter of the interface is the custom annotation type, and the second parameter is the class of the annotated field. Because multiple parameters need to be verified, we directly pass in the user object. One thing that needs to be mentioned is that ConstraintValidatorthe implementation class of the interface does not need to be added. @ComponentIt has already been loaded into the container when it is started.

public class UserValidator<T extends Annotation> implements ConstraintValidator<T, UserVo> {
    
    

    protected Predicate<UserVo> predicate = c -> true;

    @Override
    public boolean isValid(UserVo userVo, ConstraintValidatorContext constraintValidatorContext) {
    
    
        return predicate.test(userVo);
    }

    public static class UniqueUserValidator extends UserValidator<UniqueUser>{
    
    
        @Override
        public void initialize(UniqueUser uniqueUser) {
    
    
            UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
            predicate = c -> !userDao.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
        }
    }

    public static class NotConflictUserValidator extends UserValidator<NotConflictUser>{
    
    
        @Override
        public void initialize(NotConflictUser notConflictUser) {
    
    
            predicate = c -> {
    
    
                UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
                Collection<UserVo> collection = userDao.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone());
                // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
                return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
            };
        }
    }

}
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    
    

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        context = applicationContext;
    }

    public static ApplicationContext getContext() {
    
    
        return context;
    }

    public static Object getBean(String name) {
    
    
        return context != null ? context.getBean(name) : null;
    }

    public static <T> T getBean(Class<T> clz) {
    
    
        return context != null ? context.getBean(clz) : null;
    }

    public static <T> T getBean(String name, Class<T> clz) {
    
    
        return context != null ? context.getBean(name, clz) : null;
    }

    public static void addApplicationListenerBean(String listenerBeanName) {
    
    
        if (context != null) {
    
    
            ApplicationEventMulticaster applicationEventMulticaster = (ApplicationEventMulticaster)context.getBean(ApplicationEventMulticaster.class);
            applicationEventMulticaster.addApplicationListenerBean(listenerBeanName);
        }
    }

}

3. Test

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @PostMapping("/addUser")
    public String addUser(@RequestBody @UniqueUser UserVo userVo) {
    
    
        return "addUser";
    }

    @PostMapping("/updateUser")
    public String updateUser(@RequestBody @NotConflictUser UserVo userVo) {
    
    
        return "updateUser";
    }
}

10. The difference between @Valid and @Validated

The differences are as follows:

Insert image description here

11. Commonly used annotations

Bean ValidationThere are many built-in annotations, which are enough for basic actual development. The annotations are as follows:

annotation details
@Null Any type. The annotated element must be null
@NotNull Any type. The annotated element is not null
@Min(value) Numeric types (double and float will lose precision). Its value must be greater than or equal to the specified minimum value
@Max(value) Numeric types (double and float will lose precision). Its value must be less than or equal to the specified maximum value
@DecimalMin(value) Numeric types (double and float will lose precision). Its value must be greater than or equal to the specified minimum value
@DecimalMax(value) Numeric types (double and float will lose precision). Its value must be less than or equal to the specified maximum value
@Size(max, min) String, collection, Map, array types. The size (length) of the annotated element must be within the specified range
@Digits (integer, fraction) Numeric type, numeric string type. Its value must be within an acceptable range. integer: integer precision; fraction: decimal precision
@Past Date type. The annotated element must be a date in the past
@Future Date type. The annotated element must be a future date
@Pattern(value) String type. The annotated element must match the specified regular expression

Hibernate ValidatorSeveral annotations are also embedded on the original basis, as follows:

annotation details
@Email String type. The annotated element must be an email address
@Length String type. The length of the annotated string must be within the specified range
@NotEmpty String, collection, Map, array types. The length of the annotated element must be non-null
@Range Numeric type, string type. The annotated element must be within the appropriate scope

Guess you like

Origin blog.csdn.net/sco5282/article/details/130325918