Spring Validation

使用 Spring Validation 优雅地校验参数

Constraint

详细信息

@Null

被注释的元素必须为 null

@NotNull

被注释的元素必须不为 null

@AssertTrue

被注释的元素必须为 true

@AssertFalse

被注释的元素必须为 false

@Min(value)

被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Max(value)

被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@DecimalMin(value)

被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@DecimalMax(value)

被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@Size(max, min)

被注释的元素的大小必须在指定的范围内

@Digits (integer, fraction)

被注释的元素必须是一个数字,其值必须在可接受的范围内

@Past

被注释的元素必须是一个过去的日期

@Future

被注释的元素必须是一个将来的日期

@Pattern(value)

被注释的元素必须符合指定的正则表达式

Hibernate Validator 附加的 constraint:

Constraint

详细信息

@Email

被注释的元素必须是电子邮箱地址

@Length

被注释的字符串的大小必须在指定的范围内

@NotEmpty

被注释的字符串的必须非空

@Range

被注释的元素必须在合适的范围内

基本校验

以一个User实体为例

public class User {

    @NotNull(message = "用户名不能为空")
    private String userName;

    @NotNull(message = "密码不能为空")
    @Pattern(regexp = "^[a-zA-Z0-9|_]+$", message = "密码必须由字母、数字、下划线组成")
    @Size(min = 6, max = 12, message = "密码长度必须在6-12字符之间")
    private String passWord;

    @Range(min = 1, max = 150, message = "年龄必须在1-150区间")
    private Integer age;
    
    @NotEmpty(message = "用户的兴趣不能为空")
    private List<String> interest;
    
    // 省略get/set

}

假设要求传入的Json字段(@RequestBody)中,用户名、密码、年龄都有特定的规则

对应的Controller应该为

@RestController
@RequestMapping("/test")
public class TestController {

    @PostMapping("/validate")
    public String test(@Valid @RequestBody User user) {
        System.out.println(1);
        return "success";
    }

    @PostMapping("/validate2")
    public String test2(@Validated @RequestBody User user) {
        System.out.println(1);
        return "success";
    }
}

使用@Valid@Validated均可

嵌套校验

嵌套校验支持用户将@Valid@Validated混合使用,可用于更复杂的校验

还是以User为例,新增一个friends字段,代表用户的朋友们,同时加上@Valid注解代表如果friends入参有传,则需要对Friend类的内部字段进行校验,如果没有传递则无需校验。

public class User {

    @NotNull(message = "用户名不能为空")
    private String userName;

    @NotNull(message = "密码不能为空")
    @Pattern(regexp = "^[a-zA-Z0-9|_]+$", message = "密码必须由字母、数字、下划线组成")
    @Size(min = 6, max = 12, message = "密码长度必须在6-12字符之间")
    private String passWord;

    @Range(min = 1, max = 150, message = "年龄必须在1-150区间")
    private Integer age;

    @Valid
    private List<Friend> friends;
    
    // 省略get/set
}

Friend

public class Friend {

    @NotNull(message = "朋友名称不能为空")
    private String userName;

    @Range(min = 1, max = 150, message = "年龄必须在1-150区间")
    private Integer age;
    
    // 省略get/set
}

假设此时参数传递为

{
    "userName" : "11",
    "passWord" : "test123_2",
    "age" : 11,
    "friends" : [
        {
            "age" : "22"
        },
        {
            "userName" : "33"
        }
    ]
}

表示该用户有2个friend,其中一个只写了名字,其中一个只写了年龄,由代码可知年龄是非必填字段,对应的控制台日志为

符合校验预期,当此时friend字段没有传递时,则不进行校验

分组校验

分组校验是Spring Validation的特性,校验时在Controller层对实体的书写必须使用@Validated,分组校验提高了实体校验注解的可复用能力,只需要指定校验分组即可让同一实体适配多种场景。

@RestController
@RequestMapping("/test")
public class TestController {

    @PostMapping("/validate2")
    public String test2(@Validated @RequestBody User user) {
        System.out.println(1);
        return "success";
    }
}

首先需要定义常见的CRUD分组场景,取任意名字均可,接口无需实现

public class ValidatedAction {
    
    public interface Insert {
    }

    public interface Update {
    }

    public interface Search {
    }

    public interface Delete {
    }
}

为刚才的User内的字段增加分组,如在新增时需要填写用户名、密码,在删除时需要填写id和密码

public class User {

    @NotNull(message = "id不能为空", groups = {ValidatedAction.Delete.class})
    private String id;

    @NotNull(message = "用户名不能为空", groups = {ValidatedAction.Insert.class})
    private String userName;

    @NotNull(message = "密码不能为空", groups = {ValidatedAction.Insert.class, ValidatedAction.Delete.class})
    @Pattern(regexp = "^[a-zA-Z0-9|_]+$", message = "密码必须由字母、数字、下划线组成")
    @Size(min = 6, max = 12, message = "密码长度必须在6-12字符之间")
    private String passWord;

    @Range(min = 1, max = 150, message = "年龄必须在1-150区间")
    private Integer age;

    @Valid
    private List<Friend> friends;
    
    // 省略get/set
}

修改Controller接口,指定校验分组,一个为新增分组校验,一个为删除分组校验,同时需要加上javax中自带的Default分组,避免实体中没有写group的校验注解失效

@RestController
@RequestMapping("/test")
public class TestController {

    @PostMapping("/validate")
    public String test(@Validated({ValidatedAction.Insert.class, Default.class}) @RequestBody User user) {
        System.out.println(1);
        return "success";
    }

    @PostMapping("/validateDelete")
    public String test2(@Validated({ValidatedAction.Delete.class, Default.class}) @RequestBody User user) {
        System.out.println(1);
        return "success";
    }
}

新增时Postman传参为

由于新增时,非空参数仅有用户名和密码,所以正常返回,此时分组为Deleteid并没有参与非空校验

删除时Postman传参为

由于删除时,非空参数包含id,此时传参中没有id则会在控制台输出对应提示

自定义校验

hibernate提供的校验注解在简单字段的场景已经基本够用了,如果提供的校验注解无法满足要求,这个时候可以考虑自定义注解,将校验与Controller完全隔离。

本文主要考虑4种较为通用的场景下自定义注解的实现方法

  • 场景1:字段为基础类型,约束传递的字段只能在枚举code的约束范围内,虽然定义字段为枚举字段可以简单实现传输枚举对象名完成枚举约束,但通常我们不将字段本身定义为枚举直接暴露给前端。期望能够通过直接引用枚举类,达成约束。

  • 场景2:字段为String,约束传递的字段只能是一组特定的String字符串

  • 场景3:字段为Integer,约束传递的字段只能是一组特定的Integer值

  • 场景4:字段为List<String>,约束传递的字段只能是一组特定的String字符串

自定义的过程比较简单

第一步:新增一个你的自定义注解,这里为SpecifiesValueValidator

自定义注解的写法可完全照搬@NotNull等注解,稍微改动下@ConstraintvalidatedBy属性为当前自定义注解类,同时加上可重复性校验注解(非必须)@Repeatable(SpecifiesValueValidator.List.class),用于支持多个自定义注解使用在同一字段。

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {SpecifiesValueValidatorImpl.class})
@Repeatable(SpecifiesValueValidator.List.class)
public @interface SpecifiesValueValidator {
    /**
     * 默认校验消息
     *
     * @return String
     */
    String message() default "入参必须为指定值";

    /**
     * 分组校验
     *
     * @return Class<?>[]
     */
    Class<?>[] groups() default {};

    /**
     * 负载
     *
     * @return Class<? extends Payload>[]
     */
    Class<? extends Payload>[] payload() default {};

    /**
     * 指定特定String值
     *
     * @return String[]
     */
    String[] strGroup() default {};

    /**
     * 指定特定int值
     *
     * @return int[]
     */
    int[] intGroup() default {};

    /**
     * 指定枚举类型
     *
     * @return Class<?>
     */
    Class<?> enumClass() default Class.class;

    /**
     * 可重复校验
     */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        SpecifiesValueValidator[] value();
    }
}

Spring Validation
https://shikai.info/archives/spring-validation
作者
石 凯
发布于
2023年07月30日
许可协议