SpringBoot-数据校验

参数验证是一个常见的问题,无论是前端还是后台,都需对用户输入进行验证,以此来保证系统数据的正确性。对于web来说,有些人可能理所当然的想在前端验证就行了,但这样是非常错误的做法,前端代码对于用户来说是透明的,稍微有点技术的人就可以绕过这个验证,直接提交数据到后台。无论是前端网页提交的接口,还是提供给外部的接口,参数验证随处可见,也是必不可少的。前端做验证只是为了用户体验,比如控制按钮的显示隐藏,单页应用的路由跳转等等。后端才是最终的保障,总之,对于后端接口来说,一切用户的输入都是不可信的。

1、依赖关系

1
compile 'org.springframework.boot:spring-boot-starter-validation'

该依赖在spring-boot-starter-web中已经引入,如果是springboot Web项目,则不用再单独添加依赖。

springboot的数据绑定和数据校验功能在org.springframework.validation包中,@Validated注解就是在此定义的。

validation包实现参数校验主要通过调用Jakarta.Validation-api.jar包,该jar包定义了一套参数验证的接口,没有具体实现,我们常用的约束注解就是定义在此处。

validation-api的一个实现就是Hibernate-validator,spring默认使用该实现进行数据校验。

2、常用约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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(regex=,flag=) //被注释的元素必须符合指定的正则表达式Hibernate Validator附加的constraint
@NotEmpty //被注释的字符串的必须非空
@Length(min=,max=) //被注释的字符串的大小必须在指定的范围内
@NotBlank(message =) //验证字符串非null,且长度必须大于0
@Email //被注释的元素必须是电子邮箱地址
@Range(min=,max=,message=) //被注释的元素必须在合适的范围内

3、controller层入参校验

3.1 平铺参数的校验-GetMapping 参数校验

controller类上需要注解@Validated,在controller方法入参前添加约束注解,校验方能生效。此外,类上注解@Validated后,方法的返回值也能进行约束。如下:

1
2
3
4
5
6
7
8
@RestController
@Validated
public class DemoController{
@GetMapping("/valid")
public @Length(min=4) String test(@RequestParam @Email String email){
return email;
}
}

入参校验失败将抛出 javax.validation.ConstraintViolationException

平铺参数校验原理与下文的service层校验一致

此外,当GetMapping请求参数过多,开发时我们可能会使用queryVo接收请求参数,此时,Get方法中QueryVo前不能注解@RequestBody 和 @RequestParam,如下:

1
2
3
4
@GetMapping("/vo_valid")
public String queryByVo(@Valid OrderItem item){ // @Valid注解不生效
return item.toString();
}

此处@Valid校验不生效,因为queryVo看上去是一个javaBean,但实际上数据绑定阶段是逐个字段进行绑定的,并没有将其当成是一个javaBean。既然如此,如果去掉@Valid注解,queryVo中的约束注解会生效吗?答案是不生效

此时有两个选择

  • 继续选择使用GetMapping方式,queryVo的校验在service层进行
  • 改选PostMapping方式,对参数注解@Valid @RequestBody进行校验

3.2 javaBean参数校验

在bean类中使用注解约束字段:

1
2
3
4
5
6
7
8
9
@Data
public class Order{
@NotEmpty
private String oid;
@NotNull
private Date createTime;
@Valid
private List<OrderItem> items;
}

在方法中需要校验的javaBean参数前注解@Valid/@Validated(可分组)。

注意:此处所说的“方法”不包括controller层的GetMapping方法。

校验失败将抛出 org.springframework.web.bind.MethodArgumentNotValidExption ,该异常中含有一个BindingResult对象。

spring使用默认异常处理器DefaultExceptionHandlerResolver处理该异常,我们可以在controller方法参数列表中增加一个BindingResult对象来接受校验错误信息,然后使用自定义处理器处理。

1
2
3
4
5
6
7
8
9
@PostMapping(value = "/demo")
public Integer addDemo(@Valid @RequestBody Demo demo, BindingResult bindingResult){
if(bindingResult.hasErrors()){
for(ObjectError error : bindingResult.getAllErrors()){
throw new DemoException(DemoExceptionEnum.PARAM_ERROR.getCode(),error.getDefaultMessage());
}
}
return demoService.insert(demo);
}

注意:如果在一个方法中有多个javaBean参数需要校验,那么每一个javaBean都需要定义一个BindingResult对象来接收校验结果

1
2
3
public void test1()(@RequestBody @Valid DemoModel demo, BindingResult result)

public void test2()(@RequestBody @Valid DemoModel demo, BindingResult result,@RequestBody @Valid DemoModel demo2, BindingResult result2)

3.3 配置校验模式

  • 默认的校验模式为普通模式,普通模式下会校验完所有的属性然后返回所有的校验失败信息
  • 可配置为快速失败返回模式,只要有一个属性校验失败则立即返回

配置方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
  /**设置validator模式为快速失败返回*/
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}

4. service层数据校验

在service类前注解@Validated开启校验。

在service接口方法参数类型或返回值类型前注解约束:

1
2
3
4
5
6
7
8
@Validated(Default.class)
public interface OrderService {
Object hello(@NotNull @Min(10) Integer id, @NotNull String name);
//平铺参数校验
Order queryById(@NumberLength("4,6,8,10,12") @Length(max=10) String id);
//使用@Valid实现javaBean参数校验
String saveOrder(@Valid Order order);
}

注意:在service层进行校验并不需要使用BindingResult来接收校验结果,因为参数的Binding是在controller层进行的。

@Validated(Default.class)也可以注解在接口实现类上面,实现类编写方式无殊。

校验失败抛异常javax.validation.ConstraintViolationException

5. 扩展需求

5.1 分组校验

分组校验只有使用@Validated注解才能实现

使用场景:针对同一个model类,不同的接口需要对不同的属性进行校验

例如,数据插入接口与数据更新接口需要校验的参数是不同的

使用方法

  1. 在model类中定义内部接口
  2. 约束增加组别属性
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo{
public interface AddGorup{}
public interface UpdateGroup{}

@Range(min = 1,max = Integer.MAX_VALUE,groups = {UpdateGroup.class})
private Integer id;

@Email(groups = {AddGroup.class,UpdateGroup.class})
private String email;

@Past(groups = {UpdateGroup.class})
private Date birthday;
}
  1. 在接口方法或者方法参数上使用@Validated({Demo.AddGroup.class})来注解参数,表示该参数使用AddGroup来进行校验。
    约束的groups属性中可以填写多个接口名,表示该参数加入多个组进行校验

5.2 嵌套校验

嵌套校验指的是需要校验的javaBean中有的校验成员也是JavaBean类型,此时在成员上面注解@Valid即可实现嵌套校验

5.3 自定义约束校验

创建约束标注

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE,ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DemoConstraintValidator.class)
@Documented
public @interface DemoConstraint {
String message() default "default message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
E value();//约束中设置的value值
}

实现一个验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* A 自定义的约束注解类型
* T 需要检验的目标参数类型
*/
public class DemoConstraintValidator implements ConstraintValidator<A, T>{
private T value;//注入设置的具体约束
@Override
public void initialize(A a) {
this.value = a.value();
}

@Override
public boolean isValid(T t, ConstraintValidatorContext constraintValidatorContext) {
//根据value 对 参数t 进行一些判断
return true;
if(!isValid) {
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate("new default message").addConstraintViolation();
return false;
}
}
}

A表示创建的注解,T表示该约束校验的数据类型

定义默认的验证错误信息,可以通过ConstraintValidatorContext修改默认的message信息,一旦使用,在注解中给message赋值将不起作用(一般情况下不推荐使用)

5.4 检验组序列

默认情况下,约束的计算没有特定的顺序,这与它们属于哪个组无关。然而,在某些情况下,控制约束求值的顺序是有用的,例如,我们可以要求在检查汽车的道路价值之前,首先通过所有默认的汽车约束。最后,在我们开车离开之前,我们检查了实际司机的约束条件。为了实现这样的顺序,需要定义一个新的接口,并使用@GroupSequence对其进行注释,以定义必须验证组的顺序。

注意:
如果这个校验组序列中有一个约束条件没有通过验证的话, 那么此约束条件后面的都不会再继续被校验了.

1
2
3
@GroupSequence({Default.class, CarChecks.class, DriverChecks.class})
public interface OrderedChecks {
}

6. 总结

@Validated 和 @Valid

  • Java Bean中的嵌套校验只能用@Valid
  • 在controller方法中校验@RequestBody参数,参数前注解@Valid和@Validated都行,但如果要使用分组校验功能,只能使用后者
  • controller中校验平铺型@RequestParam参数,需要在controller类前注解@Validated,参数前注解约束规则
  • service层校验统一在service接口类上注解@Validated,接口方法参数列表中注解约束(类型参数前可以注解@Valid),实现类中不涉及验证相关代码

参考:
官方文档
springboot使用hibernate validator校验
@Validated和@Valid区别