聊聊jsr-303接口参数校验

2019-11-28260

前言

目前在软件开发过程中 , 前后端基本是通过数据接口方式进行交互 . 作为一名后端程序员 , 对前端的同事提交数据的合法性进行校验俨然是一个必不可少的环节 .

而Spring Boot 或者 Spring MVC 中已经集成了 用于 校验JSR 303 规范 (Bean Validation 1.0) 的jar包 , 我们内置许多事先定义好的validator , 让我们仅通过注解的方式便可以对接口参数进行校验 .

image-20191127150019486

快速上手

这里以一个登录接口为例 :

UserLoginDTO :

@Data
@EqualsAndHashCode(callSuper = false)
public class UserLoginDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    @NotBlank(message = "账号不能为空")
    @Size(min = 6, max = 10, message = "账号长度在6-10位之间")
    private String account;

    @NotBlank(message = "密码不能为空")
    private String password;
}

接口 :

/**
 * 登录接口
 */
@PostMapping
public LoginSuccessVO login(@RequestBody @Valid UserLoginDTO loginDTO) {
    return loginService.login(loginDTO);
}

这里只需要在DTO的字段上加上validation包的注解 , 一个字段可以被多个注解所标记 , 例如示例中的@NotBlank , @Size . 然后在作为controller方法的入参处使用@Valid 注解进行标记即可 . 如上所示 , 这样当请求进入到controller方法内部前 , 就会对前端传过来的参数进行校验判断 . 若校验不通过 , 则便抛出异常 , 且message为注解中message属性指定的消息 .

注解作用列表

validation 包中可用的注解如下图所示 :

image-20191128010347431.png

其中各个注解的作用可以参考下表 :

  • 空值判断
注解 作用
@Null 被标记的字段必须为null
@NotNull 被标记的字段必须不为null (无法检查长度为0的字符串)
@NotBlank 被标记的字符串被trim后的长度不能为0
@NotEmpty 被标记的字段不能为null 或者 empty (适用于String , Collection , Map , Array)
  • 长度判断
注解 作用
@Size(min=?, max=?) 被标记的字段的长度大小必须要在minmax指定的范围内 (适用于String , Collection , Map , Array)
@Length(min=?, max=?) 被标记的字符串长度必须要为在minmax指定的范围内
  • 日期检查

下表注解支持的日期类型包括 java 8 以前的 Date 对象 , 以及 java 8的LocalDateTime系列 . 具体可以参考注解的java doc 注释 .

注解 作用
@Past 被标记的日期字段必须在当前时间之前
@PastOrPresent 被标记的日期字段必须在当前时间之前或等于当前时间
@Future 被标记的日期字段必须在当前时间之后
@FutureOrPresent 被标记的日期字段必须在当前时间或等于当前时间
  • boolean 检查
注解 作用
@AssertFalse 被标记的字段必须要为false
@AssertTrue 被标记的字段必须要为true
  • 数值检查

下表注解当被标记的字段为null时 , 会通过校验 , 所以一般搭配@NotNull进行使用 .

注解 作用
@Min(value=?) 被标记的字段必须要大于value值(适用于BigDecimal , BigInteger , byte , short , int , long 以及他们的包装类型)
@Max(value=?) 被标记的字段必须要小于value值(适用于BigDecimal , BigInteger , byte , short , int , long 以及他们的包装类型)
@DecimalMax(value=? ,inclusive=true ) 被标记的字段必须要小于(等于)value值 , inclusivetrue表示等于value值也符合条件 , 反之则不符合 . (适用于BigDecimal , BigInteger ,String , byte , short , int , long 以及他们的包装类型)
@DecimalMin(value=? ,inclusive=true ) 被标记的字段必须要大于(等于)value值 , inclusivetrue表示等于value值也符合条件 , 反之则不符合 .(适用于BigDecimal , BigInteger ,String , byte , short , int , long 以及他们的包装类型)
@Digits(integer=?,fraction=?) 被标记的字段必须要为符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
@Range(min=?, max=?) 被标记的字段必须在minmax范围内 , (适用于BigDecimal , BigInteger , byte , short , int , long 以及他们的包装类型)
@Positive 被标记的字段必须为正数 (适用于BigDecimal , BigInteger ,String , byte , short , int , long 以及他们的包装类型)
@PositiveOrZero 被标记的字段必须为正数或0 (适用于BigDecimal , BigInteger ,String , byte , short , int , long 以及他们的包装类型)
@Negative 被标记的字段必须为负数 (适用于BigDecimal , BigInteger ,String , byte , short , int , long 以及他们的包装类型)
@NegativeOrZero 被标记的字段必须为负数或0 (适用于BigDecimal , BigInteger ,String , byte , short , int , long 以及他们的包装类型)
  • 格式检查
注解 作用
@Email 被标记的字段必须要为邮件地址格式 , 如果是null , 会算作通过校验 , 一般与@NotNull搭配使用 (适用于字符串)
@Pattern(regexp=?) 被标记的字段必须要符合regexp指定的正则表达式 (适用于字符串)

自定义校验注解与validator

单凭一个注解肯定无法进行对字段进行校验 , 阅读文档可知 . 注解的校验逻辑均编写在对应的Validator类中 , 这里以@NotNull为例 . 其校验注解对应的validator如下所示 :

/**
 * Validate that the object is not {@code null}.
 *
 * @author Emmanuel Bernard
 */
public class NotNullValidator implements ConstraintValidator<NotNull, Object> {

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
        return object != null;
    }
}

可以看到 , 上面@NotNull的validator 实现了一个叫ConstraintValidator的接口 . 如下所示 :

/**
 * <A> 为注解类型
 * <T> 为被校验的字段类型
 */
public interface ConstraintValidator<A extends Annotation, T> {

    /**
     * 该方法用于读取注解中传递的值 , 初始化到Validator中 .
     */
    default void initialize(A constraintAnnotation) {
    }

    /**
     * 该方法用于编写校验逻辑,返回true表示校验通过 , false表示不通过 . 
     */
    boolean isValid(T value, ConstraintValidatorContext context);
}

同理 , 如果我们想实现自己的jsr-303规范的校验注解 , 自定义一个注解并实现一个validator即可 . 这里我编写一个用于校验身份证格式的注解 . 如下所示 :

  1. 自定义一个注解
/**
 * @author wukun
 * @since 2019/11/26
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IdCardValidator.class}) // 必须使用 @Constraint注解标记自定义注解, 并在validatedBy中指定用于校验的validator类型 , 可指定多个.
public @interface IdCard {

    // 是否必传,默认必传. (该字段为自定义字段)
    boolean required() default true;

    // 以下三个字段为必备字段,直接复制过来即可 . 
    String message() default "IdCard format is invalid!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  1. 自定义validator类

/**
 * @author wukun
 * @since 2019/11/26
 */
public class IdCardValidator implements ConstraintValidator<IdCard, String> {

    //用于接收注解上自定义的 required
    private boolean required;

    @Override
    public void initialize(TodayOrAfter constraintAnnotation) {
        required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(LocalDateTime value, ConstraintValidatorContext context) {
        if (Objects.isNull(value) && required) {
            return false;
        } else {
            //此处编写校验逻辑 ....
        }

    }
}

到此 , 自定义jsr 303 注解便编写完成了 .

参考连接

分享
点赞2
打赏
上一篇:Docker常用命令笔记(一)
下一篇:翻译 | DevSecOps 冰山