Appearance
Spring MVC 数据校验
当接口接收到请求时,我们需要对请求中中携带的数据进行校验。例如,用户注册时我们需要校验用户是否填写了用户名和密码,前端和后端都需要进行校验。
1. 引入依赖以及搭建环境
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.4.0</version>
</dependency>引入全局异常处理器:
java
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(Throwable.class)
private R handleException(Throwable e){
return R.error(e.getMessage(), null);
}
}java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class R<T> {
private int code;
private String msg;
@JsonRawValue
private T data;
public static <T> R error(String msg, T data){
return new R<T>(-1, msg, data);
}
}2. 校验初体验
2.1 请求参数校验
java
@GetMapping("/user")
public User getUserById(@RequestParam @NotNull String id) {
return new User();
}@NotNull表示参数id不能为空,如果为空则抛出异常:MissingServletRequestParameterException。
结果如下:

2.2 请求头校验
java
@GetMapping("/userInHeader")
public User getUserByIdInHeaser(@RequestHeader @NotNull String id) {
return new User();
}与上例同理,只不过参数位置放在了请求头。结果如下:

2.3 请求体校验
我们将参数放在请求体中,例如,注册用户:
java
@Data
public class RegisterUserRequest {
@NotNull(message = "用户名不能为空")
private String username;
@NotNull(message = "密码不能为空")
private String password;
@NotNull(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}java
@PostMapping("/registerUser")
public boolean registerUser(@RequestBody @Valid RegisterUserRequest user){
return true;
}在RegisterUserRequest实体上,通过在字段上标注解的方式校验每个字段。
在接口方法上,通过@Valid注解开启校验。
结果如下:

3. 校验注解
以下是一些常用的校验注解:
- 空值检查:
@Null:验证注解的元素值必须为null。@NotNull:验证注解的元素值不能为null。@NotEmpty:验证注解的元素值不为null且不为空字符串(""),用于字符串、集合、Map 和数组。@NotBlank:验证注解的元素值不为null且至少包含一个非空白字符,用于字符串。
- 布尔值检查:
@AssertTrue:验证注解的元素值必须为true。@AssertFalse:验证注解的元素值必须为false。
- 数值检查:
@Min(value):验证注解的元素值必须大于等于指定的最小值。@Max(value):验证注解的元素值必须小于等于指定的最大值。@DecimalMin(value):验证注解的元素值必须大于等于指定的最小值,可以指定是否包含边界值。@DecimalMax(value):验证注解的元素值必须小于等于指定的最小值,可以指定是否包含边界值。@Digits(integer, fraction):验证注解的元素值是否是一个数字,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction。@Positive: 验证注解的元素值必须是正数(不包括 0)。@PositiveOrZero: 验证注解的元素值必须是正数或 0。@Negative: 验证注解的元素值必须是负数(不包括 0)。@NegativeOrZero: 验证注解的元素值必须是负数或 0。
- 日期检查:
@Past:验证注解的元素值必须是一个过去的日期。@Future:验证注解的元素值必须是一个将来的日期。@PastOrPresent: 验证注解的元素值必须是过去或现在的日期。@FutureOrPresent: 验证注解的元素值必须是将来或现在的日期。
- 字符串检查:
@Size(min, max):验证注解的元素值的长度必须在指定的范围内。@Pattern(regexp):验证注解的元素值必须符合指定的正则表达式。@Email:验证注解的元素值必须是合法的电子邮件地址。
4. @Valid 和 @Validated
两者都可以用来开启校验,但@Valid 是 JSR-303 的标准注解,而 @Validated 是 Spring 提供的,支持分组校验。
接下来讲解如何使用@Validated进行分组校验。
首先,在实体中定义校验组:
java
@Data
public class UserRequest {
@NotNull(groups = UserRequest.UpdateUser.class, message = "id不能为空")
private String id;
@NotNull(groups = {UserRequest.AddUser.class, UserRequest.UpdateUser.class}, message = "用户名不能为空")
private String username;
@NotNull(groups = {UserRequest.AddUser.class, UserRequest.UpdateUser.class}, message = "密码不能为空")
private String password;
@NotNull(groups = {UserRequest.AddUser.class, UserRequest.UpdateUser.class}, message = "邮箱不能为空")
@Email(groups = {UserRequest.AddUser.class, UserRequest.UpdateUser.class}, message = "邮箱格式不正确")
private String email;
// 以下定义了两个校验组
public interface AddUser{}
public interface UpdateUser{}
}然后在接口方法中,通过@Validated指定校验组:
java
@PostMapping("/addUser")
public boolean addUser(@RequestBody @Validated(UserRequest.AddUser.class) UserRequest userRequest){
return true;
}
@PostMapping("/updateUser")
public boolean updateUser(@RequestBody @Validated(UserRequest.UpdateUser.class) UserRequest userRequest){
return true;
}最后请求接口,结果如下:


在实际情况中,我们通过将新增和修改请求参数定义为两个实体分别校验,降低代码冗余。
5. 获取校验结果
在上面的案例中,如果请求体参数校验失败,抛出的异常为:MethodArgumentNotValidException。我们可以在全局异常处理器中精确处理该异常:
java
@ExceptionHandler(MethodArgumentNotValidException.class)
private R handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
Map<String, String> errorMap = new HashMap<>();
if(fieldErrors != null && fieldErrors.size() > 0){
fieldErrors.forEach(x->{
errorMap.put(x.getField(), x.getDefaultMessage());
});
}
return R.error(-1, "请求参数校验失败", errorMap);
}如果之后参数校验失败,那么就会按字段返回具体的失败原因:

6. 自定义校验注解
在有的时候,已有的注解并不能满足我们的需求,需要我们自定义注解来实现自己的校验逻辑。比如,我们自定义注解来校验传入的性别是否为“男”或“女”两个枚举值之一。
首先定义枚举类:
java
public enum GenderEnum {
MALE("男"),
FEMALE("女");
private String desc;
private GenderEnum(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
public String toString() {
return desc;
}
}然后自定义注解:
java
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {GenderEnumValidator.class}) // 指定校验器
public @interface GenderEnumConstraint {
// 默认的消息提示
String message() default "性别必须为男或女";
// 指定校验的组
Class<?>[] groups() default { };
// 指定校验的负载,一般用不到(虽然不知道有什么用,不过写在这里吧)
Class<? extends Payload>[] payload() default { };
// 指定校验的枚举类
Class<? extends Enum<?>> enumClass();
}在第三行指定校验器,所以我们要实现ConstraintValidator接口:
java
public class GenderEnumValidator implements ConstraintValidator<GenderEnumConstraint, String> {
private List<String> enumValues;
@Override
public void initialize(GenderEnumConstraint constraintAnnotation) {
Class<? extends Enum<?>> enumClass = constraintAnnotation.enumClass();
enumValues = Arrays.stream(enumClass.getEnumConstants())
.map(x->((GenderEnum)x).getDesc()) // 获取枚举值的名称
.collect(Collectors.toList());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) { // 允许为空
return true;
}
return enumValues.contains(value.toString());
}
}接口第一个泛型表示校验注解,第二个泛型表示要校验的数据类型。
之后,我们就可以像使用其他校验注解一样使用该注解进行校验了:
java
@Data
public class UserRequest {
// ... 省略其他字段
@GenderEnumConstraint(
groups = {UserRequest.AddUser.class, UserRequest.UpdateUser.class},
enumClass = GenderEnum.class)
private String gender;
// 以下定义了两个校验组
public interface AddUser{}
public interface UpdateUser{}
}结果如下:

其实,对于枚举类型的校验,我们也可以通过Jackson反序列化异常来实现:
首先在application.yml中开启配置:
yaml
spring:
jackson:
serialization:
write-enums-using-to-string: true
deserialization:
read-enums-using-to-string: true然后在实体定义中,将数据类型改为枚举:
java
@Data
public class UserRequest {
// ... 省略其他字段
private GenderEnum gender;
}结果如下:
