7、spring validation 参数校验使用详解

说明

《spring mvc 接收参数注解对比及最佳使用方案推荐》章节中作者已经整理了RESTful接口的传参方案。而在日常的项目开发中,我们需要严格控制参数规范,以避免不合规的参数导致程序处理异常。
spring validation为我们提供了基于POJO类的参数验证解决方案,本文将详细整理相关知识。

一. 依赖添加

在我们的tysite-service搭建时依赖使用的是org.springframework.boot:spring-boot-starter-web,该依赖中已经引用spring-boot-starter-validation,无需额外添加。
spring validation相关依赖

二. 常用的约束验证注解

面对服务器接口参数校验问题,J2EE 6 定义了一项子规范:Bean Validation,官方实现是Hibernate Validator,Spring 4.0 之后的版本均已经支持Bean Validation功能。

常用的约束验证注解,如下表所示:

约束注解 描述 适用场景
@null 被注释元素必须为NULL
@NotNull 被注释元素必须不为NULL,无法检查长度为0的字符串
@NotEmpty 被注释元素不能为 NULL或 EMPTY
@NotBlank 被注释元素不能为 NULL 且 字符串trim后长度必须大于0 适用于验证字符串字段非空
@AssertTrue 被注释元素必须为Boolean类型的 true 适用于属性必须为true的场景
@AssertFalse 被注释元素必须为Boolean类型的 false 适用于属性必须为false的场景
@Min 被注释元素必须大于指定数值,格式为 long 类型 适用于属性限制long类型数据最小值的场景
@Max 被注释元素必须小于指定数值,格式为 long 类型 适用于属性限制long类型数据最大值的场景
@DecimalMin 被注释元素必须大于指定数值,参数为BigDecimal定义的小数 适用于属性限制小数数据最小值的场景
@DecimalMax 被注释元素必须大于指定数值,参数为BigDecimal定义的小数 适用于属性限制小数数据最大值的场景
@Digits 被注释元素必须为指定格式的数字,integer指定整数精度,fraction指定小数精度 适用于属性限制金额格式,字段类型必须使用 Double
@Size 被注释元素必须的大小必须在指定的范围内 适用于属性限制字符串长度的场景
@Length 被注释元素必须的大小必须在指定的范围内
@Past 被注释元素必须是过去的一个日期 适用于属性限制必须为过去日期的场景
@Future 被注释元素必须是未来的一个日期 适用于属性限制必须为未来日期的场景
@Pattern 被注释元素必须符合正则表达式regexp的规范 适用于属性为密码、身份证号等需要正则限制的场景
@Email 被注释元素必须符合Email格式 适用于属性限制为Email格式的场景
@Range 被注解的元素(可以是数字或者表示数字的字符串)必须在给定的范围内
@URL 被注释元素必须符合URL格式规范 适用于属性限制为网址的场景

备注:spring validation提供默认的错误提示信息,在使用时也可以通过注解的message自定义错误提示信息。

三. spring validation 常规用法

spring validation的使用需要完成两步操作:

1、以POJO类接收参数,在该POJO类的属性上通过约束验证注解,限定参数格式。

以下为作者根据个人经验,整理的常用验证规范的使用样例

……
    private enum SexEnum {
        /** 男 */
        MALE,
        /** 女 */
        FEMALE
    }
    
    /** 主键ID */
    private Integer id;
    
    /** 用户名 */
    @NotBlank(message = "请输入用户名")
    @Size(min = 3, max = 20, message = "用户名长度必须在3~20之间")
    private String username;
    
    /** 名称 */
    @NotBlank(message = "请输入姓名")
    @Size(min = 2, max = 20, message = "姓名长度必须在2~20之间")
    private String name;
    
    /** 资源名称 */
    @NotBlank(message = "请输入资源名称")
    @Size(max = 50, message = "资源名称长度必须在1~50之间")
    private String resourceName;
    
    /** 文本域 */
    @NotBlank(message = "请输入资源简介")
    @Size(max = 1000, message = "文本域长度必须在1~1200之间")
    private String textField;
    
    /** 密码 */
    @NotBlank(message = "请输入密码")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[$@$!%*?&])[A-Za-z\\d$@$!%*?&]{8,20}",
        message = "密码长度8~20位,必须包含数字、字母(大小写各一个)、特殊字符")
    private String password;
    
    /** 性别 */
    private SexEnum genders;
    
    /** 邮箱 */
    @Email(message = "请输入正确的邮箱地址")
    private String email;
    
    /** 手机号码 */
    @Pattern(regexp = "^1[3|4|5|8][0-9]\\d{8}$", message = "请输入正确的手机号码")
    private String phone;
    
    /** 存款 */
    @Digits(integer = 14, fraction = 2, message = "请输入有效的金额")
    private Double deposit;
    
    /** 出生日期 */
    @Past(message = "请输入正确的出生日期")
    private Date birthday;
    
    /** 身高 */
    @NotNull(message = "请填写您的身高")
    @DecimalMin(value = "44.7", message = "请填写有效的身高")
    private Float stature;
    
    /** 爱好 */
    @Valid
    private List<AmateurDTO> amateur;
    
    /** 博客 */
    @URL
    private String blogs;
……

源代码地址:UserInfoDemoDTO

2、在 Controller 中,使用添加属性验证注解的POJO类接收参数,并且在POJO类前使用@Validated 注解。

……
    @PostMapping("")
    public UserInfoDemoDTO addDemo(@Validated() @RequestBody UserInfoDemoDTO dto) {
        return dto;
    }
……

源代码地址:ValidationController

四. 枚举类型验证

细心的朋友会看到,作者对 枚举类型的 性别字段,并未使用格式验证注解。
这是因为spring mvc 中,在spring validation验证调用之前,会优先进行反序列化,如果请求参数不符合枚举类型规范,则会直接抛出HttpMessageConversionException异常。
反序列化异常

五. 嵌套注解的应用

在参数接收的实际业务场景中,我们经常会遇到需要接收对象属性的场景,如下图所示:
amateur
在这种业务场景下,仅使用@Validated 注解,是无法验证对象列表中属性值的合法性的。
这时候我们需要使用@Valid注解的嵌套验证功能。

首先,我们在作为列表对象的POJO类 AmateurDTO 中设置属性验证规范。

……
    @NotNull(message = "兴趣爱好的id不能为空")
    private Integer id;
    
    @NotBlank(message = "兴趣爱好的名称不能为空")
    @Size(max = 50, message = "兴趣爱好的长度必须在1~50之间")
    private String name;
……

然后,我们在UserInfoDemoDTO示例类的 List<AmateurDTO>类型属性amateur上,添加@Valid注解,如下所示:

……
    /** 爱好 */
    @Valid
    private List<AmateurDTO> amateur;
……

最后,我们采用不合规的爱好名称请求演示API,验证提示信息如下:
在这里插入图片描述

六. @Validated 分组功能

@Validated 注解还支持参数验证的分组功能,可以根据分组接口类,判断验证注解是否生效。

这里作者以 “创建操作,允许ID为空;更新操作,不允许ID为空” 的场景作为演示示例

首先,我们在org.tysite.tyservice.example.validation.group包下创建两个接口类 AddOperationModifyOperation,用于判断添加和修改操作。

/** @Validated 分组类 - 新增数据 */
public interface AddOperation {
}

/** @Validated 分组类 - 修改数据 */
public interface ModifyOperation {
}

然后,我们在POJO类GroupDemoDTOid 属性设置验证分组

……
    @NotNull(message = "编辑操作ID不能为空", groups = ModifyOperation.class)
    private Integer id;
……

最后,在Controller类ValidationGroupController中,分别传入AddOperation.classModifyOperation.class分组接口

……
    @PostMapping("/add")
    public GroupDemoDTO insertDemo(@Validated({AddOperation.class}) @RequestBody GroupDemoDTO dto) {
        return dto;
    }
    
    @PutMapping("/update")
    public GroupDemoDTO modifyDemo(@Validated({ModifyOperation.class}) @RequestBody GroupDemoDTO dto) {
        return dto;
    }
……

下面我们分表以添加和修改接口的调用,演示请求结果。
调用添加接口:
添加接口
调用修改接口:
编辑接口

七. 自定义校验注解

上面的 Spring Validation 参数验证方案,基本上可以解决我们日常工作中遇到的参数验证场景,如果有其他特殊的验证需求,我们也可以自定义验证注解来实现。

这里我们可以模仿validation官方注解的语法,实现自定义注解。

首先,通过阅读@NotBlank@Email@Digits@Past 等注解的源代码,我们会发现validation的官方注解,都使用了@Constraint(validatedBy = { }) 注解。

打开该注解的源代码,通过阅读源码注释,我们发现该注解要求必须包含 message()groups()payload() 三个属性。
注解必须包含的属性
继续阅读源代码,我们会发现源代码说明中给我们提供了Validation自定义注解的样例代码,如下图所示:
validation示例代码
根据以上示例代码中 @Constraint(validatedBy = OrderNumberValidator.class) 的语法规则,我们找到@NotBlank注解的注解解析器NotBlankValidator ,我们会发现该注解实现了ConstraintValidator接口。

……
public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
……

然后,阅读ConstraintValidator接口的源代码,我们发现该接口包含 initializeisValid 方法。

  • initialize 方法为初始化方法,其参数为constraintAnnotation,我们可以通过该参数获取到注解的相关信息,如注解设置的属性值等。
  • isValid 方法为验证方法,用于判断验证对象是否合规。也就是说我们可以通过实现该方法完成验证逻辑。 其参数包含验证对象value和约束验证器的上下文context

ConstraintValidator源代码
注解基本语法可参考作者博客:https://blog.csdn.net/tysite/article/details/90691368

了解了验证注解和其注解解析器的语法规则,我们就可以根据自己的业务需要,开发自己的验证注解了,如下所示:

首先,我们编写注解类 @DemoName

@Target({ ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {DemoNameValidator.class})
@Documented
public @interface DemoName {
    
    String message() default "名称的长度必须在1~20之间";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
}

然后,我们编写 注解解析器类 DemoNameValidator

public class DemoNameValidator implements ConstraintValidator<DemoName, CharSequence> {
    
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if ( value == null ) {
            return false;
        } else {
            Integer charLength = value.toString().trim().length();
            if (charLength >= 1 && charLength <= Constant.NAME_MAX) {
                return true;
            } else {
                return false;
            }
        }
    }
}

最后,编写验证代码并调用验证入口API ,使用不合规的参数,可以得到如下验证效果。
自定义验证注解
详细示例代码,可以通过作者开源项目tysite-serviceorg.tysite.tyservice.example.validation包中找到
https://gitee.com/tysite-web/tysite-service

发布了27 篇原创文章 · 获赞 2 · 访问量 3489

猜你喜欢

转载自blog.csdn.net/tysite/article/details/104646587