【Java Bean Validation API】Spring3 集成 Bean 参数校验框架

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 常见注解

AnnotationDescription
@NotNull参数不能为 null
@NotBlank参数不能为 null 或 trim 后为空字符串
@NotEmpty字符串、数组、集合是否为 null 也不为空字符串、空数组、空集合
@Size若不为 null,指定字符串、数组、集合的长度范围
@Max若不为 null,参数最大值
@Min若不为 null,参数最小值
@DecimalMax若不为 null,采取精度较高的最小值限制
@DecimalMax若不为 null,采取精度较高的最大值限制
@Pattern若不为 null,字符串正则表达式匹配
@Email若不为 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 触发条件

最重要的条件是:

  1. 必须是 Bean 对象的实例方法
  2. 检测的对象是方法的形参
2.4.1 形参是普通类型

我们使用 @NotNull 等检测参数的注解,或者自定义的校验注解,标注在形参之前,注解的校验可以进行叠加

并且,我们需要在 Bean 的类之前标注 @Validated,声明这个 Bean 的方法参数受代理

这个 bean 在调用这个方法的时候,输入参数就会被监控

2.4.2 形参是自定义类型

我们使用 @NotNull 等检测参数的注解,或者自定义的校验注解,标注在形参之前,注解的校验可以进行叠加

如果这个类的定义设置了属性校验,我们要对其内部每个属性都校验,那就标注 @Valid,表示循环递归校验

其中,@Valid 的触发不依赖于类上的 @Validated,其他注解则依赖

  1. 什么是循环递归校验

    • 如果 @Valid 标注的是集合或数组,则依次对每个元素校验,递归就是校验元素内部的属性
  2. 什么是类的定义设置了属性校验

    • 例如这个对象:

    • @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 特殊需求

如果你需要对一个方法的返回值进行校验,如果直接标注注解在返回值类型前,是无效的;

  1. 对于普通类型,我们手动校验没啥大问题
  2. 对于自定义类型,且类的定义设置了属性校验,我们可不想再写一遍啊~

我们其实可以通过封装以下这个方法进行校验:

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) {
        // ......

    }


}

更多使用场景,只要合理推理就应该没问题,举一反三一下就行,更多细节需要就去查去探索,这里就不一一罗列了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s:103

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值