2018-12-17 · Develop

Spring Boot 数据校验框架

前面我们通过几篇文章了解了如何使用 Spring Boot 进行数据绑定,数据绑定结束后肯定是需要进行数据的合法、正确等一系列的校验工作的。除了业务逻辑方面的验证,其它如必传字段校验等工作会产生大量沉郁的代码。为了解决这个问题,Spring Boot 为我们提供了一套完备的验证框架。
Spring Boot 支持 JSR-303、Bean 验证框架,默认实现用的是 Hibernate validator。通过 @Valid@Validated 两个注解,Spring Boot 即可对参数对象进行校验,校验结果放在 BindingResult 对象中。

JSR-303 是 Java 标准的验证框架,而 Hibernate validator 就是实现的这套框架,其中定义了许多的注解来验证 Bean 的属性,这篇文章并不是介绍这些注解的,如果你有需要请阅读其官方文档

@Valid 和 @Validated

@Valid 是 javax 提供,标准的 JSR-303规范。
@Validated 是 Spring 提供, Spring's JSR-303 规范,是标准 JSR-303 的一个变种。

两者在基本的校验功能上没有太多的区别,但是在分组、注解标注地方、嵌套校验等功能上还是有点不同。在工作中一般都会进行混用,下面我们来说说这些不同的地方。

单参数,少参数

在参数很少的情况下,再封装正对象进行校验显然是不合理的,可以在将参数中使用这两个注解都没有进行正确的校验,这个时候就需要将 @Validated 注解添加到 Controller 类上面才能正常工作。

@RestController
@Api(description = "数据校验")
@Validated
public class ValidController {

    @GetMapping("/valid")
    @ApiOperation(value = "valid")
    public Integer valid(@RequestParam(required = false) @Min(10) Integer age) {
        return age;
    }
}

注意上面 @Validated 注解的使用地方。

分组校验

有这样一种场景:当我们创建用户的时候,希望 userId 为 null, 应为这个值需要后台生成,但是在修改的时候,希望 userId 不能为 null, 因为更新操作需要通过 ID 进行操作。这个时候就需要使用到分组进行校验的功能。

首先定义分组

public interface ValidGroup {
    interface Add {}
    interface Update{}
}

使用注解校验

@Getter
@Setter
@ToString
public class User {
    @Null(groups = ValidGroup.Add.class)
    @NotNull(groups = ValidGroup.Update.class)
    private String userId;

    @NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class})
    private String name;
}

使用 @Validated 注解,传入需要校验的分组,进行校验。

@GetMapping("/valid")
@ApiOperation(value = "validGroup")
public User valid(@Validated({ValidGroup.Add.class}) User user) {
    return user;
}

这里需要注意的是 userId 进行分组校验的时候,如果再对 name 在添加和更新都进行 @NotBlank 校验的情况下,需要将两个组都加上,只写 @NotBlank 不添加分组的话,是不能参与校验运算的。或者在 Controller 层进行两次校验 public User valid(@Valid @Validated({ValidGroup.Add.class}) User user){...}

嵌套验证

有些时候数据会是一个很复杂的对象,对象里面的对象需要进行嵌套校验,这个时候需要在嵌套的上层字段上加上注解 @Valid 才能生效。

@Getter
@Setter
@ToString
public class ContactInfo {
    @NotBlank
    private String phone;
    private String address;
}
@Getter
@Setter
@ToString
public class User {
    private String userId;
    private String name;
    private Integer age;
    @NotNull
    @Valid
    private ContactInfo contactInfo;
}

自定义校验

JSR-303 提供的大部分校验注解已经够用,也允许定制校验注解。自定义校验注解和其他的校验注解并没有其他的差别,但是需要通过 @Constraint 注解来说明使用什么类来作为校验注解的实现类。我们来实现个和 @NotBlank 注解差不多的 HasLength 校验注解,代码如下:

@Documented
@Constraint(validatedBy = {HasLengthValidator.class})
@Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HasLength {

    String message() default "{validation.constraints.HasLength.message}";

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

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

在 resources 目录下创建 ValidationMessages.properties 配置文件,提供统一的 message

validation.constraints.HasLength.message=ABCDEFG

@Constraint 注解指定使用 HasLengthValidator 进行校验的实现类。实现类的代码如下:

public class HasLengthValidator implements ConstraintValidator<HasLength, CharSequence> {

    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        return StringUtils.hasLength(value);
    }
}

在代码中使用

@Getter
@Setter
@ToString
public class User {
    @HasLength
    private String userId;
    @NotBlank
    private String name;
}

校验结果返回

上面说了校验的结果将封装在 org.springframework.validation.BindingResult 对象中,可以如下进行使用:

    @GetMapping("/valid")
    @ApiOperation(value = "valid")
    public User valid(@Valid User user, BindingResult bindingResult) {
        if(bindingResult.hasErrors()) {
            for (ObjectError objectError : bindingResult.getAllErrors()) {
                // TODO
            }
        }
        return user;
    }

或者在 controller 层不进行处理,这样 Spring Boot 将抛出 org.springframework.validation.BindException 异常,这样就可以通过全局异常统一处理,这种全局异常网络上有很多,下面就举个示例代码:

@RestControllerAdvice
@Slf4j
public class ExceptionHandlerControllerAdvice {

    @ExceptionHandler(BindException.class)
    public String handleBindException(BindException e) {
        log.error("handleBindException", e);
        BindingResult bindingResult = e.getBindingResult();
        if (bindingResult.hasErrors()) {
            // TODO
            return "参数校验失败";
        }
        return "请联系管理员";
    }
}

校验配置

默认会校验完所有属性,然后将错误信息一起返回,但很多时候不需要这样,一个校验失败了,其它就不必校验了

@Configuration
public class ValidatedConfig {

    private static Validator validator;

    static {
        validator = Validation.byProvider(HibernateValidator.class)
        .configure()
        .failFast(true) // 设置为 true 遇到校验失败就不往后校验了
        .buildValidatorFactory()
        .getValidator();
    }

    @Bean
    public Validator getValidator() {
        return validator;
    }
}

有些时候希望能够主动调用校验,不添加 @Valid 或者 @Validated 注解,可以将上面的 ValidatedConfig 定义成如下的工具类:

@Configuration
public class ValidatedUtils {

    private static Validator validator;

    static {
        validator = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator();
    }

    @Bean
    public Validator getValidator() {
        return validator;
    }

    public static void validated(Object object, Class<?>... groups) {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        if (!constraintViolations.isEmpty()) {
            throw new IllegalArgumentException(constraintViolations.iterator().next().getMessage());
        }
    }

    public static <T> void validated(List<T> list) {
        if (CollectionUtils.isNotEmpty(list)) {
            for (T t : list) {
                validated(t);
            }
        }
    }
}