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.
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
@requestBody
to receive parameters - GET request, use
@requestParam、@PathVariable
to 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 或 @Valid
annotations to the object to easily implement automatic parameter verification. If the verification fails, MethodArgumentNotValidException
an 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 @Validated
annotations:
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated UserVo userVo) {
return "addUser";
}
4. @requestParam, @PathVariable parameter verification
GET requests generally use @requestParam、@PathVariable
annotations 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 @Validated
and declare constraint annotations (such as: @Min
) on the input parameters. If the verification fails, ConstraintViolationException
an 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 MethodArgumentNotValidException
thrown 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-validation
the 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 ValidGroup
the interface does not inherit Default
the interface, then email
the field cannot be verified (no grouping is assigned); after inheritance, ValidGroup
it belongs to Default
the type, that is: the default group/so, it can be email
verified
7. Nested verification
@Valid
Annotations 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 ConstraintValidator
the 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 ConstraintValidator
the implementation class of the interface does not need to be added. @Component
It 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:
11. Commonly used annotations
Bean Validation
There 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 Validator
Several annotations are also embedded on the original basis, as follows:
annotation | details |
---|---|
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 |