影子的知识库

影子的知识库

  • 知识库
  • GitHub

›Java系列

JVM系列

  • JVM内存区域
  • 对象创建-布局-访问
  • 内存溢出实战
  • 内存区域回收
  • 四大引用
  • 垃圾回收算法
  • HotSpot回收算法细节

Java系列

  • java注解
  • springboot请求参数绑定
  • springboot请求参数校验框架
  • YAML语法
  • 动态代理
  • classpath和java命令
  • springboot-aop编程
  • springboot统一异常处理
  • springboot数据库和事务
  • springboot拦截器
  • springboot中的web配置
  • docker的简单开发
  • springboot自动配置
  • 数据库的隔离级别
  • springboot监控
  • java类加载
  • java-agent的相关内容
  • 类加载器详解
  • java的SecurityManager
  • maven学习

Node

    JS 基础

    • 语法基础和数据类型
    • 数据类型转换
    • 语句 表达式 运算符
    • 变量与对象
    • 函数
    • 数据处理
    • 常用 API
    • 重点知识

    ES6

    • 块级作用域
    • 字符串和正则表达式
    • 函数
    • 对象
    • Symbol
    • Set和Map
    • 迭代器和生成器
    • 类
    • 数组
    • Promise

    Node 基础

    • 模块系统
    • package.json
    • 内置对象
    • npm脚本的使用
    • Buffer
    • Stream
    • 事件循环机制
    • 示例代码

    stream系列

    • 流的缓冲
    • 可读流
    • 可写流
    • 双工流和转换流
    • 自定义流

后期计划

  • 学习计划
  • 专题研究计划
Edit

本文内容

参考

https://juejin.im/post/5d3fbeb46fb9a06b317b3c48#heading-6

JSR303

JSR303 是一套校验注解,位于 javax.validation 包下。

它只是规定了校验的一些规则,但是没有提供校验的具体实现,hibernate validation 提供了相应的实现,还提供了一些增加的注解给我们使用,Spring MVC 也支持校验,并且对 hibernate validation 进行了二次封装。使得我们只需要提供注解信息即可对参数进行校验

校验注解列表

验证注解被注解的类型说明
@AssertFalseBoolean,boolean验证注解的元素值必须是false
@AssertTrueBoolean,boolean验证注解的元素值必须是true
@NotNull任意类型验证注解的元素值不是null
@Null任意类型验证注解的元素值是null
@Min(value=值)             BigDecimal,BigInteger, byte,short, int, long,等任何Number 或 CharSequence(存储的是数字)子类型验证注解的元素值大于等于@Min指定的value值
@Max(value=值)和 @Min 一样验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值)                    和 @Min 一样验证注解的元素值大于等于@ DecimalMin指定的value值
@Digits(integer=整数位数, fraction=小数位数)和 @Min 一样被注释的元素必须是一个数字,验证元素值的整数位数和小数位数上限
@Size(min=下限, max=上限)字符串、Collection、Map、数组等验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小。新版和旧版好像行为不一致,旧版的行为似乎会同时对数组进行非空校验
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型验证注解的元素值(日期类型)比当前时间早
@Future与@Past要求一样验证注解的元素值(日期类型)比当前时间晚
@NotBlankCharSequence子类型验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@NotEmptyCharSequence子类型、Collection、Map、数组验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值)BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式)CharSequence子类型(如String)验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式)String,任何CharSequence的子类型验证注解的元素值与指定的正则表达式匹配
@Valid任何非原子类型指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证

springboot 校验使用方式

简单使用方式

@GetMapping("/hello")
public Object hello(@Validated HelloDTO helloDTO) {
  return helloDTO.getStrArr();
}

@Data
public class HelloDTO {
    @Size(max = 3)
    private String[] strArr;
}
GET localhost:8080/hello?strArr=x&strArr=x&strArr=y
My-Header: xxx
cookie: abc=jkl
结果:["x","x","y"]

GET localhost:8080/hello?strArr=x&strArr=x&strArr=y&strArr=c
My-Header: xxx
cookie: abc=jkl
结果:校验异常

最简单的使用方式步骤:

  • 在入参的 DTO 类的字段上添加校验注解
  • 在 controller 中方法参数上添加 @Validated 注解

可以看到,这种方式十分简单

校验结果参数

@GetMapping("/hello")
public Object hello(@Validated HelloDTO helloDTO, BindingResult bindingResult) {
  if(bindingResult.hasErrors()){
    return bindingResult.getFieldError().getField() + bindingResult.getFieldError().getDefaultMessage();
  }
  return "no problem";
}

@Data
public class HelloDTO {
    @Size(max = 3)
    private String[] strArr;
}
GET localhost:8080/hello?strArr=x&strArr=x&strArr=y&strArr=c
My-Header: xxx
cookie: abc=jkl

结果:strArr个数必须在0和3之间

GET localhost:8080/hello?strArr=x&strArr=x&strArr=y
My-Header: xxx
cookie: abc=jkl
结果:no problem

我们可以在校验的模型后面加上 BindingResult 获取本次校验的结果,如果校验多个模型参数,controller 方法的参数应该如下

public Object xxxFunc(@Validated Model1 model1, BindingResult result1, @Validated Model2 model2, BindingResult result2)

校验结果参数都紧跟在校验模型参数的后面

分组校验

有时候,controller 的几个方法入参模型都是一样的,但是校验规则不一样,这个是很常见的。

比如说添加一个用户和更新一个用户信息,它们都需要传入一个 User(id, name, address) 模型进来,但是校验规则不同:

  • 对于添加用户功能来说,不需要传入 id,但是需要传入 name、address 等必填信息
  • 对于更新用户信息来说,需要传入 id,但是不需要传入 name、address 等信息,填 name 表示要覆盖原有的 name,填 address 表示要覆盖原有的 address,都不填就不做更改

因此,为了满足这种常见的需求,可以使用分组的功能来校验

@GetMapping("/helloA")
public Object helloA(@Validated(HelloDTO.A.class) HelloDTO helloDTO, BindingResult bindingResult) {
  if(bindingResult.hasErrors()){
    return bindingResult.getFieldError().getField() + bindingResult.getFieldError().getDefaultMessage();
  }
  return "no problem";
}
@GetMapping("/helloB")
public Object helloB(@Validated(HelloDTO.B.class) HelloDTO helloDTO, BindingResult bindingResult) {
  if(bindingResult.hasErrors()){
    return bindingResult.getFieldError().getField() + bindingResult.getFieldError().getDefaultMessage();
  }
  return "no problem";
}

@Data
public class HelloDTO {
    public interface A{}
    public interface B{}
    @Size(max = 3,groups = A.class)
    @Size(max = 5,groups = B.class)
    private String[] strArr;
}
GET localhost:8080/helloA?strArr=x&strArr=x&strArr=y
My-Header: xxx
cookie: abc=jkl
结果:no problem

GET localhost:8080/helloB?strArr=x&strArr=x&strArr=y
My-Header: xxx
cookie: abc=jkl
结果:no problem

GET localhost:8080/helloA?strArr=x&strArr=x&strArr=y&strArr=c
My-Header: xxx
cookie: abc=jkl
结果:strArr个数必须在0和3之间

GET localhost:8080/helloB?strArr=x&strArr=x&strArr=y&strArr=c
My-Header: xxx
cookie: abc=jkl
结果:no problem

GET localhost:8080/helloB?strArr=x&strArr=x&strArr=y&strArr=c&strArr=c&strArr=c
My-Header: xxx
cookie: abc=jkl
结果:strArr个数必须在0和5之间

分组校验的使用方法如上,步骤是:

  • DTO 内部添加多个内部公共空接口,上述例子是 A 和 B
  • DTO 内部字段的校验注解添加 groups 信息
    • @Size(max = 3,groups = A.class) 表示这个校验只在 A 组生效,数组长度最大是 3
    • @Size(max = 5,groups = B.class) 表示这个校验只在 B 组生效,数组长度最大是 5
  • @Validated 注解添加 groups 信息,表示要按照什么规则进行校验
    • @Validated(HelloDTO.A.class) HelloDTO helloDTO 表示按照 A 组规则校验
    • @Validated(HelloDTO.B.class) HelloDTO helloDTO 表示按照 B 组规则校验
  • 以上的分组,均可以是数组,也就是说,可以写成是
    • @Size(max = 3,groups = {A.class,B.class}) 表示这个校验规则在 AB 组规则下都生效
    • @Validated({HelloDTO.A.class,HelloDTO.B.class}) 表示按照 A 组和 B 组的规则都校验,也就是说这两组的规则全都要满足才校验通过

对非 DTO 的单个参数进行校验

上面的校验都是对 DTO 内部的字段进行校验,有时候我们使用的是单个参数,没有使用 DTO 对象,这种情况下校验的使用方式有些不同:

@RestController
@Validated
public class TestController {
    @GetMapping("/hello")
    public Object hello(@Size(max = 5) String str) {
        return str;
    }
}
GET localhost:8080/hello?str=aa
My-Header: xxx
cookie: abc=jkl
结果:aa

GET localhost:8080/hello?str=aaccccc
My-Header: xxx
cookie: abc=jkl
结果:错误

校验非模型的单个参数,步骤如下:

  • @Validated 注解必须标注在 Controller 的类声明上,不是方法参数了。
  • @Size(max = 5) 这种校验注解标注在要验证的单个参数前,如上就是
    • public Object hello(@Size(max = 5) String str)
  • 这种验证方式没有 BindingResult 来接收结果了,BindingResult 仅用于模型的绑定结果
  • 类上面添加 @Validated 注解仅仅影响这种单体参数的校验,不影响模型参数的校验
  • 尽量少用这种方式

@Valid 注解内部对象嵌套校验

比如我们现在有个 OneDTO,本身有自己的校验规则,OneDTO 内部持有 AnotherDTO,AnotherDTO 也有自己的校验规则,controller 接收 OneDTO 的时候,同时要校验 OneDTO 本身,以及 OneDTO 内部的 AnotherDTO,就可以使用 @Valid 注解。

先看下没使用 @Valid 的情况

@GetMapping("/hello")
public Object hello(@Validated OneDTO oneDTO) {
  return oneDTO.getOne() + "," + oneDTO.getAnotherDTO().getAnother();
}

@Data
public class OneDTO {
    @Max(100)
    private Integer one;
    private AnotherDTO anotherDTO;
}

@Data
public class AnotherDTO {
    @Min(200)
    private Integer another;
}
GET localhost:8080/hello?one=1&anotherDTO.another=3
My-Header: xxx
cookie: abc=jkl
结果:1,3

GET localhost:8080/hello?one=111&anotherDTO.another=3
My-Header: xxx
cookie: abc=jkl
结果:错误

可以看到,不在 OneDTO 中的 anotherDTO 属性上添加 @Valid 时,仅仅是校验了 OneDTO.one 属性最大值上线是 100,没有校验 anotherDTO.another 必须大于等于 200。

下面添加 @Valid 注解

@Data
public class OneDTO {
    @Max(100)
    private Integer one;
    @Valid
    private AnotherDTO anotherDTO;
}
GET localhost:8080/hello?one=1&anotherDTO.another=3
My-Header: xxx
cookie: abc=jkl
结果:错误

GET localhost:8080/hello?one=1&anotherDTO.another=333
My-Header: xxx
cookie: abc=jkl
结果:1,333

可以看到,anotherDTO 的校验规则也生效了

小技巧 使用 @Valid 校验内部的 List 参数

@PostMapping("/hello")
public Object hello(@RequestBody @Validated OneDTO oneDTO) {
  return oneDTO;
}

@Data
public class OneDTO {
    @Max(100)
    private Integer one;
    @Valid
    private List<AnotherDTO> anotherDTO;
}

@Data
public class AnotherDTO {
    @Min(200)
    private Integer another;
}

使用如上的方式,就可以校验内部 List 的参数里的 AnotherDTO 模型了。

有时候可能一个 DTO 内部的 list 是 List<String> 或者 List<Integer> 这种装载的非 Model 类型,这个时候我们可以改造一下,改成只有单个属性的 Model,例如

//改造前
@Data
public class OneDTO {
    @Max(100)
    private Integer one;
    private List<Integer> anotherDTO;
}
// 改造后
@Data
public class OneDTO {
    @Max(100)
    private Integer one;
    @Valid
    private List<AnotherDTO> anotherDTO;
}

@Data
public class AnotherDTO {
    @Min(200)
    private Integer another;
}

自定义校验注解

springboot 提供了自定义校验的接口,可以让我们完成自定义校验的功能。

下面以开发一个自定义校验注解 @MaxBytes 为例,它声明在一个 String 类型的字段上,注解有一个参数值,表示这个 String 的字节数大小不能超过这个参数值。(@Size 注解只能定义字符数量,有时候是不满足需求的)

定义注解类和校验逻辑的实现类

@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MaxBytes.MaxBytesValidator.class) // 校验逻辑的实现类
public @interface MaxBytes {
    String message() default "字符串大小不能超过{value}字节"; // 校验失败时的信息

    Class<?>[] groups() default {}; // 分组校验的内容

    Class<? extends Payload>[] payload() default {}; // bean 相关的内容,目前没有用过

    int value() default 0; // 本注解的参数值,表示定义的最大字节大小

    /**
     * 真正实现注解的校验逻辑的类
     * 从 MaxBytes 参数中可以获取注解的初始化的值,例如最大字节大小
     * String 表示被这个注解声明的字段或者参数类型
     */
    class MaxBytesValidator implements ConstraintValidator<MaxBytes, String> {
        private int max;

        @Override
        public void initialize(MaxBytes constraintAnnotation) {
            max = constraintAnnotation.value();
        }

        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            if (value == null) {
                return true;
            }
            return value.getBytes().length <= max;
        }
    }
}

以上代码有这些细节:

  • @Constraint(validatedBy = MaxBytes.MaxBytesValidator.class) 表示注解的校验逻辑是由 MaxBytes.MaxBytesValidator.class 来实现的

    • Java 的注解实际上只是个标记,并不具备任何的影响,必须要有其它的代码逻辑来处理注解内容,所以我们除了定义校验注解外,还需要有验证的逻辑的实现类
  • @Target({ElementType.PARAMETER, ElementType.FIELD}) 表示这个注解可以声明在方法参数和字段上

  • 注解的代码

    String message() default "字符串大小不能超过{value}字节"; // 校验失败时的信息
    
    Class<?>[] groups() default {}; // 分组校验的内容
    
    Class<? extends Payload>[] payload() default {}; // bean 相关的内容,目前没有用过
    
    int value() default 0; // 本注解的参数值,表示定义的最大字节大小
    
    • 前三个属性都是校验注解应该要有的
    • 字符串大小不能超过{value}字节 这里引用了注解内部的 value() 方法的值
  • class MaxBytesValidator implements ConstraintValidator<MaxBytes, String> 校验逻辑实现类必须实现这个接口

  • ConstraintValidator<MaxBytes, String>

    • 第一个泛型表示初始化参数可以使用什么注解的值,这里就是 MaxBytes 注解,这样就可以从 MaxBytes 注解的 value 方法中获取到最大字节数
    • 第二个泛型表示被校验的是什么类型,我们只想校验 String 类型,所以就使用 String 这个泛型。这样 isValid 方法接收的参数类型也是 String,这个 String 就表示我们当前正在校验的内容

将注解声明在字段上

@Data
public class StringDTO {
    @MaxBytes(5)
    private String str;
}

进行校验

@GetMapping("/hello")
public Object hello(@Validated StringDTO stringDTO) {
  return stringDTO;
}
GET localhost:8080/hello?str=12345
My-Header: xxx
cookie: abc=jkl
结果:{"str":"12345"}

GET localhost:8080/hello?str=123456
My-Header: xxx
cookie: abc=jkl
结果:错误

GET localhost:8080/hello?str=中12
My-Header: xxx
cookie: abc=jkl
结果:{"str":"中12"}

GET localhost:8080/hello?str=中123
My-Header: xxx
cookie: abc=jkl
结果:错误

可以看到,这里准确的按照字节数进行了校验

这里的中文字符占用了 3 个字节

Last updated on 11/8/2020
← springboot请求参数绑定YAML语法 →
  • 参考
  • JSR303
  • 校验注解列表
  • springboot 校验使用方式
    • 简单使用方式
    • 校验结果参数
    • 分组校验
    • 对非 DTO 的单个参数进行校验
    • @Valid 注解内部对象嵌套校验
    • 小技巧 使用 @Valid 校验内部的 List 参数
  • 自定义校验注解
    • 定义注解类和校验逻辑的实现类
    • 将注解声明在字段上
    • 进行校验
影子的知识库
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHub
Copyright © 2020 Cen ZhiPeng