本文内容
参考
https://juejin.im/post/5d3fbeb46fb9a06b317b3c48#heading-6
JSR303
JSR303 是一套校验注解,位于 javax.validation 包下。
它只是规定了校验的一些规则,但是没有提供校验的具体实现,hibernate validation 提供了相应的实现,还提供了一些增加的注解给我们使用,Spring MVC 也支持校验,并且对 hibernate validation 进行了二次封装。使得我们只需要提供注解信息即可对参数进行校验
校验注解列表
| 验证注解 | 被注解的类型 | 说明 |
|---|---|---|
| @AssertFalse | Boolean,boolean | 验证注解的元素值必须是false |
| @AssertTrue | Boolean,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(包含)指定区间之内,如字符长度、集合大小。新版和旧版好像行为不一致,旧版的行为似乎会同时对数组进行非空校验 |
| @Past | java.util.Date,java.util.Calendar;Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
| @Future | 与@Past要求一样 | 验证注解的元素值(日期类型)比当前时间晚 |
| @NotBlank | CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
| @Length(min=下限, max=上限) | CharSequence子类型 | 验证注解的元素值长度在min和max区间内 |
| @NotEmpty | CharSequence子类型、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 个字节