Validation 和 validator 包提供了一系列校验用注解,帮助我们在 RESTful 服务请求中实现期望的数据校验,其注解的功能包括但不限于入参的存在性判断、非空判断、数值取值范围限定、特定含义数据格式校验、校验失败提示信息等。

Maven 依赖

在 SpringBoot 2.3 版本之前的项目中,主要需要添加的依赖包括以下两个。其中 spring-boot-starter-web 包含了 spring-boot-starter-validation 这个 maven 包,而里面则添加了 javax-validationhibernate-validator 两个包含检验注解的依赖包。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

而在 SpringBoot 2.3 之后的版本则需要单独引入 spring-boot-starter-validation 这个 maven 包。

常用注解说明

Maven 包:javax.validation.constraints:validation-api-2.0.1.Final

注解作用数据类型说明
@Null任何类型对象必须为空
@NotNull任何类型对象不为空
@NotBlank字符串对象不为空,字符串去掉前后空格后长度不为0
@NotEmpty字符串、集合、数组对象不为空,且字符串长度不为0,集合、数组大小不为0
@AssertTrue布尔型必须为true;null值有效,Boolean通过校验,boolean不可
@AssertFalse布尔型必须为false;null 可通过校验
@Min(number)整型数数值必须大于或等于指定的最小值
@Max(number)整型数数值必须小于或等于指定的最大值
@DecimalMin(decimal)浮点型数值必须大于或等于指定的最小值,内部使用BigDecimal定义数值对象;为 null 是校验通过;默认包含边界值
@DecimalMax(decimal)浮点型数值必须小于或等于指定的最大值,内部使用BigDecimal定义数值对象;为 null 是校验通过;默认包含边界值
@Positive整型数数值必须为正整数
@PositiveOrZero整型数数值必须为正整数或0
@Negative整型数数值必须为负整数
@NegativeOrZero整型数数值必须为负整数或0
@Digits数值型或者字符串作为数值其构成必须合法
@Digits(integer=, fraction=)数值型数值必须符合指定的整数精度和小数精度
@Size(min=, max=)字符串、集合、数组对象的大小在指定区间内;为 null 是校验通过
@PastDate或者Calendar对象必须是一个过去的日期
@PastOrPresentDate或者Calendar对象必须是一个过去或者当前的日期
@FutureDate或者Calendar对象必须是一个将来的日期
@FutureOrPresentDate或者Calendar对象必须是一个将来或者当前的日期
@Pattern字符串必须是规则正确的正则表达式
@Email字符串必须是Email类型;可以通过指定regexp和flags来自定义email格式;为null时算作通过验证

关于 @AssertTrue 与 @AssertFalse 注解对 null 的校验说明,需要区分基本类型(boolean)与装箱类型(Boolean)两种情况。参数为null,校验时则将参数看作其数据类型对应的默认值。boolean 型的默认值为 false,因此 @AssertTure 验证失败;Boolean 型则仍为 null,根据 javadoc 的说明 “null elements are considered valid”,因此都认为是校验通过。

javax.validation.constraints 包仅仅定义了上面的一系列注解,真正的实现逻辑在 org.hibernate.validator 包中,在 SpringBoot 项目启动时一般都会自动引入 hibernate 包。

Maven 包:org.hibernate.validator.constraints:hibernate-validator-6.0.18.Final

注解作用数据类型说明
@Length(min=, max=)字符串字符串的长度在指定区间内
@Range(min=, max=)数值型数值必须在指定闭区间内
@CreditCardNumber字符串必须是通过Luhn校验和测试的信用卡号码
@URL字符串必须是URL地址
@UniqueElements集合类校验集合中的值都是唯一的,null 视为有效成员

@Validated实现字段分组校验

在请求参数中添加了上述注解后,还需要在接口接收的参数前加上 @Validated 注解,这些校验的注解才会生效,不加的话则是无效的。

在 controller 接口被调用时,上述注解对应的入参将会进行校验,但这种校验一旦接口被调用则是必须执行的,而不能根据调用调用业务的不同而进行分组处理。例如,在执行添加操作时,一般是多个参数都要求非空,个别参数需要满足其含义的约束;而在执行删除操作时,通常只需要一个起标识作用的 id 或者 seq 非空,其他参数一般都可以为空。为了以一种比较友好的代码逻辑实现这种需求,这里就可以引入分组校验的功能。

首先,可以根据业务的需要定义一些分组接口:

public abstract interface InsertGroup {}
public abstract interface UpdateGroup {}
public abstract interface DeleteGroup {}

然后,在参数使用注解校验的地方加入响应的分组,说明只有采用指定分组时,才会对参数进行校验:

@NotBlank(message="seq不能为空", groups = {DeleteGroup.class})
private String seq;

@NotBlank(message="seq不能为空", groups = {DeleteGroup.class})
@Length(message="标题长度要求在{min}和{max}之间", min = 5, max = 20, groups = {InsertGroup.class, UpdateGroup.class})
private String title;

最后,在 controller 接口的请求参数中通过 @Validated 指定校验使用的分组:

@PostMapping("/add")
public Response add(@RequestBody @Validated({InsertGroup.class}) Request req) {
    return new Response.success();
}

@PostMapping("/update")
public Response update(@RequestBody @Validated({UpdateGroup.class}) Request req) {
    return new Response.success();
}

@PostMapping("/delete")
public Response delete(@RequestBody @Validated({DeleteGroup.class}) Request req) {
    return new Response.success();
}

@Valid

@Valid 注解自 Spring 中就开始使用,我们常常用它进行方法级别或者成员属性级别的校验。与 @Validated 相比,它常用于触发嵌套属性的验证,而不支持分组验证。这里的嵌套属性,指的是在请求体内部,将 @Valid 注解使用在一个类对象上,以触发对这个对象内部每一个嵌套属性的注解校验。举个例子:

public class ApplyForm {
    // ...
    
    @Valid
    @NotNull
    private PersonalInfo personalInfo;
    
    // ...
}

自定义校验注解

参照 @NullBlank 注解,定义了一个如下的自定义注解:

@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(DemoLength.List.class)
@Constraint(validateBy = {DemoLengthValidator.class})
public @interface DemoLength {
    
    long min() default 1;
    long max() default 10;
    
    String message() default "DemoLength validator annotation.";
    
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    
    @Documented
	@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface List {
        DemoLength[] value();
    }
}
  • 这是一个用于校验参数长度的检验注解,其可指定长度最小值、最大值、以及提示信息
  • @Constraint(validateBy = {DemoLengthValidator.class}) 指定自定义的注解校验类
public class DemoLengthValidator implements ConstraintValidator<DemoLength, Object> {
    private long min;
    private long max;
    
    @Override
    public void initialize(DemoLength constraintAnnotation) {
        this.min = constraintAnnotation.min();
        this.max = constraintAnnotation.max();
    }
    
    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        if (o == null) {
            return true;
        }
        if (o instanceof String) {
            int length ((String) o).length();
            return min <= length && length <= max;
        } else if (o instanceof Integer || o instanceof Long) {
            long val = ((Number) o).longValue();
            return min <= length && length <= max;
        }
        return false;
    }
}

注解校验类实现了 ConstraintValidator 接口,接口需要指定注解类以及注解应用的参数类型,这里的 DemoLength 注解即可用于判断字符串长度,也可用于判断数值大小,因此参数类型可以指定为 Object。实现接口主要是为了实现两个方法,顾名思义,initialize 用于初始化注解的配置信息,isValid 则是具体的校验判断逻辑,其中这里将 null 视为有效参数。

注解的校验逻辑

之所以在 Controller 层的请求体参数使用校验注解即能完成校验,是因为 Spring 启动时会为 Controller 类添加一个拦截器 MethodValidationInterceptor,每当有请求进来的时候,拦截器会对亲求进行拦截,然后判断请求体或者方法是否带有 @Valid 或者 @Validated 注解。如果存在,那么请求会被 AOP 拦截且执行参数校验:校验通过,则可以继续执行后续的业务逻辑;校验不通过,则抛出 ConstraintViolationException 异常,携带配置的特定提示信息返回给请求方。具体源码可见 MethodValidationInterceptor 类。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐