JSR303参数校验(2)

198 阅读5分钟

前言:之前咱们讲解了什么是JSR303,并且使用他的实现Hibernate Validator做了简单的参数校验,并且通过BindingResult获取到了出现异常的信息并返回给了前端,这样大大简化了参数校验的操作。

全局异常处理+参数校验

虽然说前文讲解的参数校验已经简化了很多我们的操作,但是我们是否可以把Hibernate Validator和Spring提供的全局异常处理结合一下呢?把我们的参数校验出现的错误都集中到一个地方处理,这样我们就不需要在每个类都返回异常信息。Controller层只要抛出参数异常即可,剩下都交给我们的全局异常类来处理。下面上代码:

首先@RestControllerAdvice和@ExceptionHandler就是全局异常处理的两个重要注解,一个是标注这个类是 异常处理类,一个是标注这个方法可以处理那些异常。然后从异常中拿出异常信息存放在message中即可,当然也可以放在data中,但是data一般都是返回的数据,避免二义性所以我放在了message中。

然后在Controller层加上@Validated即可。


下面我们测试一下吧:

首先先传一个年龄超了120的非法数据,结果如下

然后再来一个用户名没传的

分组校验

接下来我们看一下分组的参数校验,顾名思义就是将参数校验分组。假如说我们在新增用户的时候我们需要对用户名进行非空校验,对密码进行非空白校验,在修改的时候也需要对密码进行非空白校验,那这个时候我们就可以使用分组校验。当然这里随便举了个例子,读者具体问题具体分析。那么我们暂时有两种解决方案(1)写两个类来分别处理(2)分组校验,下面上代码:

public interface EditOperate {
}
public interface InsertOperate {
}

定义两个接口或者类,主要就是用来给校验分组,没有啥特殊意义

    // 仅在新增时验证
    @NotNull(message = "不能为空", groups = {InsertOperate.class})
    private String username;

    // 在新增和修改时验证
    @NotBlank(message = "不能为空", groups = {InsertOperate.class, EditOperate.class})
   private String password;

在一个可能有多种校验规则的属性的注解上分别加上group属性,来指定他们所属的校验组,就是上面的接口的class

    @PostMapping
    public Result saveSysUser(@Validated(value = InsertOperate.class) @RequestBody SysUser sysUser) {
        return Result.success(HttpStatus.SUCCESS, "保存用户成功", sysUser);
    }

    @PutMapping
    public Result editSysUser(@Validated(value = EditOperate.class) @RequestBody SysUser sysUser) {
        return Result.success(HttpStatus.SUCCESS, "修改用户成功", sysUser);
    }

上面两步就是将组和校验规则做了绑定,接下来我们需要将组和接口再绑定一下(指定@Validated中的value即可)


接下来分别测试一下新增和修改操作吧

当我们执行新增操作的时候username和password就都会做校验

当我们执行修改操作的时候就只有密码会做校验

当然这里的分组校验场景可能用的不太好,遇到具体问题需要具体分析

自定义校验规则

Hibernate Validator提供了非常多的校验注解,可以解决大部分的场景,当然我们也可以使用正则表达式来做更加细致的校验。但是如果有一些校验规则很特殊,我们该怎么办呢?我们可以自定义注解+自定义校验规则,这里非常有个非常重要的接口就是ConstraintValidator,让我们接下来慢慢分析该如何自定义吧。

首先我们点开一个比较简单的校验注解@Max

javax.validation.constraints.AssertFalse.message     = must be false
javax.validation.constraints.AssertTrue.message      = must be true
javax.validation.constraints.DecimalMax.message      = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.DecimalMin.message      = must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.Digits.message          = numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
javax.validation.constraints.Email.message           = must be a well-formed email address
javax.validation.constraints.Max.message             = must be less than or equal to {value}
javax.validation.constraints.Min.message             = must be greater than or equal to {value}
//....

resource中的ValidationMessages.properties中就存放着校验失败信息的键值对,我们也可能自定义校验失败的信息

这里我们大概就能猜出Max的校验器其实是实现了ConstraintValidator,所以我们自定义的时候也需要实现这个类,然后传入@Constraint即可。当然ConstraintValidator的实现类有很多,我们随便挑一个分析一下

接下来我们自己完成一个只能传入指定值得校验规则吧

/**
* 自定义校验注解:只能传数组中的值
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ValueListValidator.class})
public @interface ValueList {
    
    String message() default "{hc.demo.springjsr303.ValueList.message}";
    
    //指定分组
    Class<?>[] groups() default {};
    
    //负载
    Class<? extends Payload>[] payload() default {};
    
    //允许用户传的值
    int[] values() default {};
}

首先自定义一个校验注解,分别配置校验器,异常信息,分组,负载,以及最重要的校验值,这里就是包含合法值的int数组

public class ValueListValidator implements ConstraintValidator<ValueList, Integer> {
    private List<Integer> list = new ArrayList<>();

    @Override

    public void initialize(ValueList constraintAnnotation) {
        int[] values = constraintAnnotation.values();
        for (int i = 0; i < values.length; i++) {
            list.add(i);
        }
    }

    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        return list.contains(integer);
    }
}

然后自定义校验器,泛型传入自定义注解,用户传入的值Integer类型,初始化的时候讲注解中int数组的值存入list集合,然后校验的时候判断是否包含即可

hc.demo.springjsr303.ValueList.message=value must in list

最后在resource目录下配置ValidatorMessages.properties


下面测试一下吧

    @ValueList(values = {1, 2, 3})
    private Integer age;

我们指定用户传入的age只能是1或2或3

传入2完全没有问题

但是如果不是合法值就会抛异常,异常信息就是我们在properties中设置好的

总结

参数校验在我们日常开发的过程中是非常常见的,为了简化开发我们可以使用Hibernate Validator中提供的注解,然后使用全局异常处理来统一对抛出的异常做处理,假如说校验规则很特殊也可以实现ConstraintValidator来完成自定义校验。