Spring3 集成 Bean 参数校验框架 Java Bean Validation API
1. 依赖
Spring 版本:3.0.5
Java 版本:jdk21
检验框架依赖(也可能不需要,在前面 spring 的启动依赖里就有):
<!-- 自定义验证注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
目前我还没有找到 spring/java 低版本的一个很方便的方式去进行参数校验,之前的 javax 无法实现,在高版本中被更名为 jakarta,我们使用的就是其中的 jakarta
2. 基本使用
2.1 常见注解
Annotation | Description |
---|---|
@NotNull | 参数不能为 null |
@NotBlank | 参数不能为 null 或 trim 后为空字符串 |
@NotEmpty | 字符串、数组、集合是否为 null 也不为空字符串、空数组、空集合 |
@Size | 若不为 null,指定字符串、数组、集合的长度范围 |
@Max | 若不为 null,参数最大值 |
@Min | 若不为 null,参数最小值 |
@DecimalMax | 若不为 null,采取精度较高的最小值限制 |
@DecimalMax | 若不为 null,采取精度较高的最大值限制 |
@Pattern | 若不为 null,字符串正则表达式匹配 |
若不为 null,字符串是否符合邮件格式 |
按需去查就行,更多注解在:
package jakarta.validation.constraints;
自觉规范地据场景去使用,注解可以标注在任何地方,但是不是每个地方都有用,轻则失效,重则报错
精力有限,这些我们也没法一一去探寻“乱搞的现象”
2.2 自定义校验注解
如果框架自带的不足以满足我们的要求,那么我们可以选择自定义注解
例如,这些注解都无法针对 Map 这种非单列的类型
或者,我们需要一个注解,其可以检测一个 Number 类型的或者其数组集合的对象,若不为 null,元素在一个特定的数值范围内
我们就要自己去写一个会被 Jakarta 框架识别的注解:
/**
* Created With Intellij IDEA
* User: 马拉圈
* Date: 2024-08-07
* Time: 17:19
* Description: 此注解用于判断数值是否在规定氛围内
* min 代表最小值,max 代表最大值,被注解的变量数值必须在闭区间 [min, max]
* 支持该变量是 Number 类型的变量,以及其数组、集合;
* 对于数组和集合,必须每个元素都满足该规则,否则就不通过
*/
@Documented
@Constraint(validatedBy = {IntRangeValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IntRange {
String message() default "数值不在有效范围内"; // 默认消息
int min();
int max();
Class<?>[] groups() default {}; // 分组校验
Class<? extends Payload>[] payload() default {}; // 负载信息
}
黄色为必须部分,红色为自定义部分,其中 IntRangeValidator.class 是自定义的处理类:
public class IntRangeValidator implements ConstraintValidator<IntRange, Object> {
private int min;
private int max;
@Override
public void initialize(IntRange intRange) {
this.min = intRange.min();
this.max = intRange.max();
}
private int compare(Number number1, Number number2) {
return Double.compare(number1.doubleValue(), number2.doubleValue());
}
private boolean isValid(Object value) {
if(Objects.isNull(value)) {
return Boolean.TRUE;
} else if (value instanceof Number number) {
return compare(number, min) >= 0 && compare(number, max) <= 0;
} else if (value instanceof Collection<?> collection) {
return collection.stream().allMatch(this::isValid);
} else if (value.getClass().isArray()) {
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
if(!isValid(Array.get(value, i))) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
} else {
return Boolean.FALSE;
}
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
return isValid(value);
}
}
代码就不解释了,主要是得实现 ConstraintValidator 接口,其中第一个泛型是自定义注解类,第二个泛型是预期注解标注在什么类型的对象上,isValid 返回 false,就拦截
2.3 自定义异常处理
如果拦截,会统一抛出异常:MethodArgumentNotValidException.class 或者 ConstraintViolationException.class
- MethodArgumentNotValidException 由一整个对象被检测出问题时抛出
- ConstraintViolationException 由单一属性或单一参数被检测出问题时抛出
- 可能有其他,但是如果是违背我们的注解那一定是上面这两个,其他可能是使用不当的问题
我觉得都处理就行,不要纠结抛哪个异常,都处理就行:
public static SystemJsonResponse getGlobalServiceExceptionResult(GlobalServiceException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String message = e.getMessage();
GlobalServiceStatusCode statusCode = e.getStatusCode();
log.error("请求地址'{}', {}: {}", requestURI, statusCode, message);
return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(statusCode, message);
}
/**
* 自定义验证异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public SystemJsonResponse constraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
log.error("数据校验出现问题,异常类型:{}", e.getMessage());
String message = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.filter(Objects::nonNull)
.collect(Collectors.joining("\n"));
return getGlobalServiceExceptionResult(
new GlobalServiceException(message, GlobalServiceStatusCode.PARAM_FAILED_VALIDATE),
request
);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public SystemJsonResponse ValidationHandler(MethodArgumentNotValidException e, HttpServletRequest request) {
log.error("数据校验出现问题,异常类型:{}", e.getMessage());
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.filter(Objects::nonNull)
.collect(Collectors.joining("\n"));
return getGlobalServiceExceptionResult(
new GlobalServiceException(message, GlobalServiceStatusCode.PARAM_FAILED_VALIDATE),
request
);
}
2.4 如何应用
2.4.1 触发条件
最重要的条件是:
- 必须是 Bean 对象的实例方法
- 检测的对象是方法的形参
2.4.1 形参是普通类型
我们使用 @NotNull 等检测参数的注解,或者自定义的校验注解,标注在形参之前,注解的校验可以进行叠加
并且,我们需要在 Bean 的类之前标注 @Validated,声明这个 Bean 的方法参数受代理
这个 bean 在调用这个方法的时候,输入参数就会被监控
2.4.2 形参是自定义类型
我们使用 @NotNull 等检测参数的注解,或者自定义的校验注解,标注在形参之前,注解的校验可以进行叠加
如果这个类的定义设置了属性校验,我们要对其内部每个属性都校验,那就标注 @Valid,表示循环递归校验
其中,@Valid 的触发不依赖于类上的 @Validated,其他注解则依赖
-
什么是循环递归校验
- 如果 @Valid 标注的是集合或数组,则依次对每个元素校验,递归就是校验元素内部的属性
-
什么是类的定义设置了属性校验
-
例如这个对象:
-
@Data public class EmailLoginDTO { @NotBlank(message = "code 不能为空") private String code; @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不合法") private String email; }
-
值得注意的是,@Valid 只会在非 null 的时候触发
若这个对象,的属性又有自定义对象,则继续标注 @Valid 循环递归校验即可,
@Data
public class LoginDTO {
@Valid
private EmailLoginDTO emailLoginDTO;
@Valid
private WxLoginDTO wxLoginDTO;
}
2.4.3 特殊需求
如果你需要对一个方法的返回值进行校验,如果直接标注注解在返回值类型前,是无效的;
- 对于普通类型,我们手动校验没啥大问题
- 对于自定义类型,且类的定义设置了属性校验,我们可不想再写一遍啊~
我们其实可以通过封装以下这个方法进行校验:
import cn.hutool.extra.spring.SpringUtil;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
import java.util.Set;
public class ValidatorUtils {
private static final Validator validator = SpringUtil.getBean(Validator.class);
public static <T> void validate(T object, Class<?>... groups) {
Set<ConstraintViolation<T>> validate = validator.validate(object, groups);
if (!validate.isEmpty()) {
String message = String.format("请求对象:'%s'", object.toString());
throw new ConstraintViolationException(message, validate);
}
}
}
我们只需要将方法的返回值传进去就行(groups 可以为空数组),便可完成对自定义类的校验
2.4.4 主要应用场景
主要应用场景就是用于 Controller 的目标方法,因为 Controller 也是 Bean 嘛,接受请求的时候,会调用这个 Bean 对应的目标方法,例如一下示例:
对于无状态的参数进行校验,与业务控制层解耦,避免了重复校验的冗余现象,也不会犯是在控制层还是在业务层进行校验的选择困难症
@RestController
@RequiredArgsConstructor
@Validated
public class XXXController {
@PostMapping("/set/{value}")
@Operation(summary = "设置值")
public SystemJsonResponse setValue(@Valid @RequestBody XXXDTO xxxDTO,
@NotBlank @RequestHeader("token") String token
@IntRange (min = 1, max = 7) @PathVariable("value") Integer value) {
// ......
}
}
更多使用场景,只要合理推理就应该没问题,举一反三一下就行,更多细节需要就去查去探索,这里就不一一罗列了