使用 Spring Validation 进行校验数据

概述

由于最近两个月一直忙于编写项目和其他的琐碎时,一直没有记录博客,遂记录下最近用到的「spring validation」。
这个东西就是校验数据的工具库。一般的,我们很多时候都需要在接口controller中去判断某些数据是否合格,或者是否为空,因此便造成了大量的代码冗余,并且也很不美观。因此我们可以通过这个工具库和以前记录过的「使用 ControllerAdvice 注解处理 controller 产生的异常」相配合,可以优雅的解决数据校验问题

代码

pom

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

使用

首先,它的使用流程分为以下步骤。

  • 1、在实体类之上声明对应的格式,如「是否为空、最大值、最小值...等」
  • 2、在controller接口的参数前加上「@Validated」注解
  • 3、配置基于「@RestControllerAdvice」注解的全局异常处理器

校验注解简介与使用

在「spring validation」之中为我们提供了很多的格式实现,如下图。
校验.png
如释义所示,我们只需要在实体类对应的字段之上加上对应的注解就好。如下,我们设置了这个整数参数的范围为1~4.

@Range(min = 1,max = 4,message = "正确的等级应该是1-4")
private Integer level;

而其他的,则是同理啦。需要注意的是,有些注解是仅限于「字符串/整数」某一种才可以使用的。

而在controller之中,我们只需要如下在参数前加上「@Validated」注解就好啦。

private AjaxResult save(@Validated @RequestBody SysCourseId sysCourseId){
    courseIdService.save(sysCourseId);
    return AjaxResult.success();
}

而当我们加上之后呢,当校验失败的时候,会抛出类型为「BindException.class」的异常,而我们只需要通过我们之前学习过的做一个统一异常处理类就可以去一起解决了。

统一异常处理

如果你有看前面所引入的那篇文章的话,可以发现在那篇文章之中我们使用的注解是「@ControllerAdvice」,而本次使用的是「@@RestControllerAdvice」。它们的具体区别是什么呢?其实等同于「@RestController」和「@Controller」的区别一样。

不过上述对于题目毫无意义。因此我们使用了「@RestControllerAdvice」注解标注了名为「BindExceptionHandler」的类。

@RestControllerAdvice
public class BindExceptionHandler {

并且在其中加入方法,如下。

@ExceptionHandler(BindException.class)
public AjaxResult handleBindException(HttpServletRequest request, BindException exception) {
    BindingResult result = exception.getBindingResult();
    final List<FieldError> allErrors = result.getFieldErrors();
    for (FieldError errorMessage : allErrors) {
        return AjaxResult.error(errorMessage.getDefaultMessage());
    }
    return AjaxResult.error("未知错误");
}

逻辑很简单,当触发类型为「BindException.class」的异常时,会执行此方法。而在这个方法中,我们通过「exception」参数获取了校验结果,再通过「getFieldErrors()」方法获得了具体的错误列表,之后将错误遍历了出来,并以此为内容返回到客户端,我使用了「AjaxResult」工具类,这个类其实就是为了统一返回值而存在的。而其中有一点是,「errorMessage.getDefaultMessage()」获取的是我们在字段之上的校验注解中的 message 参数中的内容。

你以为这就完了吗? 虽然上述是通过查阅资料在网上文章所学习到的,但是!当校验失败的时候并不会触发这个方法!具体原因可能是版本不同所导致。经过查阅资料得知,我们这个方法应该绑定类型为「MethodArgumentNotValidException.class」的异常。

因此,我们的代码改为下述。

@ExceptionHandler(MethodArgumentNotValidException.class)
public AjaxResult exception(MethodArgumentNotValidException exception) {
    BindingResult result = exception.getBindingResult();
    final List<FieldError> allErrors = result.getFieldErrors();
    for (FieldError errorMessage : allErrors) {
        return AjaxResult.error(errorMessage.getDefaultMessage());
    }
    return AjaxResult.error("未知错误");
}

主动触发校验

上述内容中,讲述了被动(自动)的触发校验机制,那么当我们什么时候想主动触发该怎么办呢?偷个小懒,因此我在网上copy了一份工具类~

public class ValidatorUtils {

    private static Validator validatorFast = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator();
    private static Validator validatorAll = Validation.byProvider(HibernateValidator.class).configure().failFast(false).buildValidatorFactory().getValidator();

    /**
     * 校验遇到第一个不合法的字段直接返回不合法字段,后续字段不再校验
     * @Time 2020年6月22日 上午11:36:13
     * @param <T>
     * @param domain
     * @return
     * @throws Exception
     */
    public static <T> Set<ConstraintViolation<T>> validateFast(T domain) throws Exception {
        Set<ConstraintViolation<T>> validateResult = validatorFast.validate(domain);
        if(validateResult.size()>0) {
            System.out.println(validateResult.iterator().next().getPropertyPath() +":"+ validateResult.iterator().next().getMessage());
        }
        return validateResult;
    }
    
    /**
     * 校验所有字段并返回不合法字段
     * @Time 2020年6月22日 上午11:36:55
     * @param <T>
     * @param domain
     * @return
     * @throws Exception
     */
    public static <T> ArrayList<String> validateAll(T domain) throws Exception {
        Set<ConstraintViolation<T>> validateResult = validatorAll.validate(domain);
        ArrayList<String> errors = new ArrayList<>();
        if(validateResult.size()>0) {
            Iterator<ConstraintViolation<T>> it = validateResult.iterator();
            while(it.hasNext()) {
                ConstraintViolation<T> cv = it.next();
                errors.add(cv.getMessage());
            }
        }
        return errors;
    }
    
}

自定义校验注解-身份证校验

而在项目之中我使用到了「校验身份证」,因此也在此记录一下。这个注解的具体原理是通过正则实现的。

@ConstraintComposition(CompositionType.OR)
@Pattern(regexp = "(^[1-9]\\d{5}(19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$)|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{2}[0-9Xx]$)")
@Null
@Length(min = 0, max = 0)
@Documented
@Constraint(validatedBy = {})
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@ReportAsSingleViolation
public @interface Cid {
    String message() default "身份证号校验错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}