前言
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"
));
}
}