Java Bean Validation 2.0 (二): 自定义校验规则

2,857 阅读3分钟

前言

Jakarta Bean Validation API定义了丰富的内建注解支持数据的约束验证,如@NotNull@NotEmpty等,上篇文章已经列举了全部的内建注解,但这些只是最通用的验证规则,对于千奇百怪的业务需求,显然是不能全部满足的,所以需要自定义的验证规则来应对这些需求。

只需三步即可拥有自定义约束

假如对于以下代码中UserResource, 要对salary字段增加约束验证,规则是当employed为true时,salary应当大于1000.

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserResource {
    @NotNull
    private UUID id;
    @NotBlank
    private String name;
    @Max(value = 200)
    @Min(value = 1)
    private int age;
    private boolean gender;
    @Email
    private String email;
    private boolean employed;
    private BigDecimal salary;
}

第一步:创建一个约束注解

使用@interface关键字创建一个注解SalaryConstraint:

@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = SalaryValidator.class)
@Documented
public @interface SalaryConstraint {
    String message() default "chengco.validation.demo.controller.validation.SalaryConstraint.message";

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

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

    double value();
}

此注解包含几个方法:

  • message 提供error message key,最后一步会说明怎么据此找到要显示的error message
  • groups 指定约束分组,在某一个分组时才执行此约束验证
  • payload 指定的payload,会在验证结果中携带此字段,比如:可以用于验证结果的严重等级分类
  • value 可以设置注解的变量部分,在validator可以拿到此值,做相应的逻辑处理,比如本例中的salary应当大于1000,如果1000这个最小值可定制,则可通过value传入。

除此之外,SalaryConstraint上还有其他一些注解修饰:

  • @Target 指定支持的目标类型,比如:方法,类等
  • @Retention(RUNTIME) 这种类型的注解将在运行时通过反射的方式提供
  • @Constraint(validatedBy = SalaryValidator.class) 指定validator
  • @Documented 指定将包含到java Doc文档中

第二步:实现一个validator

已经定义了SalaryConstraint注解声明一些元数据,现在还需要创建一个validator来实现具体的验证逻辑:

public class SalaryValidator implements ConstraintValidator<SalaryConstraint, UserResource> {
    private BigDecimal minSalary;

    @Override
    public void initialize(SalaryConstraint constraintAnnotation) {
        minSalary = BigDecimal.valueOf(constraintAnnotation.value());
    }

    @Override
    public boolean isValid(UserResource user, ConstraintValidatorContext context) {
        boolean isValid = !user.isEmployed() || user.getSalary().compareTo(minSalary) > 0;
        if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("{chengco.validation.demo.controller.validation.SalaryConstraint.message}")
                    .addPropertyNode("salary")
                    .addConstraintViolation();

        }
        return isValid;
    }
}

validatior的实现非常简单,ConstraintValidator接口。

  • initialize方法,顾名思义是做一些初始化工作,获取约束的元数据并将它们存储在validator的实例中。
  • isValid,实际的验证逻辑,Jakarta Bean Validation 规范建议将空值视为有效。如果null不是元素的有效值,则应使用@NotNull显式进行验证。

第三步:定义一个Error message

前两步中配置了message key:chengco.validation.demo.controller.validation.SalaryConstraint.message, 当违反约束时,展示什么错误信息呢?我们需要在resource目录下创建一个ValidationMessages.properties文件,并添加映射:

chengco.validation.demo.controller.validation.SalaryConstraint.message=Salary invalid

使用自定义约束

本例中的验证规则@SalaryConstraint(1000)需要综合employed和salary两个字段验证,为取得两个字段信息,所以需要将约束加到上一级UserResource上:

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@SalaryConstraint(1000)
public class UserResource {
    @NotNull
    private UUID id;
    @NotBlank
    private String name;
    @Max(value = 200)
    @Min(value = 1)
    private int age;
    private boolean gender;
    @Email
    private String email;
    private boolean employed;
    private BigDecimal salary;
}

单元测试验证自定义规则

public class UserResourceValidationTest {
    private Validator validator;

    @Before
    public void before(){
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }
    @Test
    public void test_validation(){
        UserResource user = UserResource.builder()
                .age(500)
                .name(" ")
                .email("email")
                .employed(true)
                .salary(BigDecimal.TEN)
                .build();

        List<String> result = validator.validate(user).stream()
                .map(c -> c.getPropertyPath() + " - " + c.getMessage())
                .collect(Collectors.toList());

        assertThat(result, hasItems(
                "age - must be less than or equal to 200",
                "name - must not be blank",
                "email - must be a well-formed email address",
                "id - must not be null",
                "salary - Salary invalid"
        ));
    }
}