highlight: xcode
theme: vuepress
概述
如本篇文章标题所述,本篇文章要给大家介绍如何扩展 javax 的 validation-api。为什么要扩展它原有的注解呢,那肯定是因为原有的注解无法满足业务需求嘛。
准备工作
首先我们需要导入几个依赖包:
```xml javax.validation validation-api 2.0.1.Final
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>
```
@RangeIn注解
在某次需求开发中,我突然发现 Hibernate 的 validator 库的 @Range
注解太局限了,只能限定最小值和最大值,而不能限定枚举值。如下所示:
java @Range(min = 0, max = 10) private Integer amount;
这样的写法表示 amount
字段只能最小为 0,最大为 10。如果我想让这个字段的值限定在某个集合中那就不行了。于是我想到了开发自定义注解,比如命名为 @RangeIn
。
@RangeIn
注解:
```java package org.codeart;
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Target({ ElementType.FIELD, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = { RangeInValidator.class }) // 指定校验器 public @interface RangeIn {
long[] value() default {};
// 默认的错误消息
String message() default "value must range in the given array";
// 分组
Class<?>[] groups() default {};
// 负载信息
Class<? extends Payload>[] payload() default {};
} ```
long[]
类型的 value
属性表示字段的值限定在某个数组之中。这个注解的顶部使用了 @Constraint
注解修饰,内部 validatedBy
属性指定了由那个类来处理这个注解。注意它是一个数组,说明可以指定多个校验器。
元注解 @Target
的参数为 ElementType.FIELD, ElementType.TYPE_USE
表示它可以修饰成员变量以及泛型的类型参数。
实现校验器类
下一步需要声明校验器 RangeInValidator
类,这是官方固定写法不要问我为什么。
RangeInValidator
校验器(注解解析器)定义如下:
```java package org.codeart;
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.Arrays;
public class RangeInValidator implements ConstraintValidator {
private long[] values;
@Override
public void initialize(RangeIn annotation) {
this.values = annotation.value();
}
@Override
public boolean isValid(Number number, ConstraintValidatorContext context) {
long fieldVal = number.longValue();
for (long value : values) {
if (value == fieldVal) {
return true;
}
}
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(getMessage()).addConstraintViolation();
return false;
}
private String getMessage() {
return "field value must range in the given array: " + Arrays.toString(values);
}
} ```
这个校验器类的成员属性中有 long[]
类型的 value
字段,用来接收注解的 value
字段的值。这个校验器类实现了 ConstraintValidator
接口,需要实现内部的抽象方法 isValid
。这个方法的返回值表示是否校验通过。
通过重写 initialize
方法,我们拿到注解对象。因为在声明类的时候指定了 ConstraintValidator
的泛型是 <RangeIn, Number>
,所以我们可以直接设置方法的参数是 RangIn
类型,从而获取注解的 value
的字段值赋值给成员变量。
在 isValid
中,因为设置了接口泛型第二个类型使用(type use)参数是 Number
类型,所以 isValid
第一个参数可以直接设置为 Number
类型,从而获取需要校验的字段的值,然后再进行校验。
成员变量 value
已经赋值,然后现在开始遍历这个数组,在数组中查找是否存在需要校验字段的值。假如存在那么就返回 true
,否则返回 false
。
最后调用 ConstraintValidatorContext#disableDefaultConstraintViolation
方法表示禁用默认消息,使用自定义的返回错误消息。
context.buildConstraintViolationWithTemplate(getMessage()).addConstraintViolation();
这一段表示生成自定义的错误消息模板,最后添加到 ConstraintValidatorContext
内部的集合中去,这个由实现类实现。
测试一下
在运行之前先声明一个实体类 Person
:
```java package org.codeart.pojo;
import lombok.Data; import org.codeart.validator.RangeIn;
import java.util.List;
@Data public class Person {
private String name;
@RangeIn({10, 20, 50})
private Integer age;
private List<@RangeIn({8, 88, 888}) Integer> luckNumbers;
} ```
Person
的字段添加了我们自定义的字段校验注解。
运行测试一下:
```java package org.codeart;
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import java.util.ArrayList; import java.util.List; import java.util.Set;
public class Main {
public static void main(String[] args) {
// 创建校验器工厂类
ValidatorFactory factory = Validation.byDefaultProvider()
.configure()
.messageInterpolator(new ParameterMessageInterpolator())
.buildValidatorFactory();
Validator validator = factory.getValidator();
Person person = new Person();
person.setAge(15);
person.setName("Trump");
List<Integer> luckNumbers = new ArrayList<>();
luckNumbers.add(999);
luckNumbers.add(666);
person.setLuckNumbers(luckNumbers);
// 输入校验信息
Set<ConstraintViolation<Person>> violations = validator.validate(person);
for (ConstraintViolation<Person> violation : violations) {
System.out.println(violation.getMessage());
}
}
} ```
结果如下:
因为 Person
的 age
字段设置为了 15 所以不在限定值集合中。luckNumbers
添加了 999 和 666,也不在限定集合中,所以也触发了校验失败。
注意小坑
在导入依赖的时候,假如你没有导入 hibernate-validator
,会提示一个错误,没有服务提供者。因为 javax.constraint 是一个规范只声明了接口和注解,但是并没有实现,所以需要实现的库。
结合SpringBoot使用
当然最后肯定要将注解应用到 SpringBoot 的 Restful 接口中去,现在来定义一个接口来接收一个请求体参数并校验:
```java @RestController public class TestController {
@PostMapping("/person")
public String addPerson(@Valid @RequestBody Person person) {
System.out.println(person);
return "ok";
}
} ```
使用 ApiPost 简单测试一下:
果然报错了:
被校验住了。