在大多数项目中,无论是 Bean Validation 定义的约束,还是 Hibernate Validator 附加的约束,都是无法满足我们复杂的业务场景。所以,我们需要自定义约束。这个很重要。
开发自定义约束一共只要两步:1)编写自定义约束的注解;2)编写自定义的校验器 ConstraintValidator 。
下面,就让我们一起来实现一个自定义约束,用于校验参数必须在枚举值的范围内。
IntArrayValuable
// IntArrayValuable.java
public interface IntArrayValuable {
/**
* @return int 数组
*/
int[] array();
}
因为对于一个枚举类来说,我们无法获得它具体有那些值。所以,我们会要求这个枚举类实现该接口,返回它拥有的所有枚举值。
GenderEnum
在 cn.iocoder.springboot.lab22.validation.constants
包路径下,创建 GenderEnum 枚举类,枚举性别。代码如下:
// GenderEnum.java
public enum GenderEnum implements IntArrayValuable {
MALE(1, "男"),
FEMALE(2, "女");
/**
* 值数组
*/
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(GenderEnum::getValue).toArray();
/**
* 性别值
*/
private final Integer value;
/**
* 性别名
*/
private final String name;
GenderEnum(Integer value, String name) {
this.value = value;
this.name = name;
}
public Integer getValue() {
return value;
}
public String getName() {
return name;
}
@Override
public int[] array() {
return ARRAYS;
}
}
- 实现 IntArrayValuable 接口,返回值数组
ARRAYS
。
@InEnum
在 cn.iocoder.springboot.lab22.validation.core.validator
包路径下,创建 @InEnum
自定义约束的注解。代码如下:
// InEnum.java
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = InEnumValidator.class)
public @interface InEnum {
/**
* @return 实现 IntArrayValuable 接口的
*/
Class extends IntArrayValuable> value();
/**
* @return 提示内容
*/
String message() default "必须在指定范围 {value}";
/**
* @return 分组
*/
Class>[] groups() default {};
/**
* @return Payload 数组
*/
Class extends Payload>[] payload() default {};
/**
* Defines several {@code @InEnum} constraints on the same element.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
InEnum[] value();
}
}
- 在类上,添加
@@Constraint(validatedBy = InEnumValidator.class)
注解,设置使用的自定义约束的校验器。 -
value()
属性,设置实现 IntArrayValuable 接口的类。这样,我们就能获得参数需要校验的值数组。 -
message()
属性,设置提示内容。默认为"必须在指定范围 {value}"
。 - 其它属性,复制粘贴即可,都可以忽略不用理解。
5.4 InEnumValidator
在 cn.iocoder.springboot.lab22.validation.core.validator
包路径下,创建 InEnumValidator 自定义约束的校验器。代码如下:
// InEnumValidator.java
public class InEnumValidator implements ConstraintValidator {
/**
* 值数组
*/
private Set values;
@Override
public void initialize(InEnum annotation) {
IntArrayValuable[] values = annotation.value().getEnumConstants();
if (values.length == 0) {
this.values = Collections.emptySet();
} else {
this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toSet());
}
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
// <2.1> 校验通过
if (values.contains(value)) {
return true;
}
// <2.2.1>校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
.replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
return false; // <2.2.2.>
}
}
- 实现 ConstraintValidator 接口。
- 第一个泛型为
A extends Annotation
,设置对应的自定义约束的注解。例如说,这里我们设置了@InEnum
注解。 - 第二个泛型为
T
,设置对应的参数值的类型。例如说,这里我们设置了 Integer 类型。
- 第一个泛型为
- 实现
#initialize(annotation)
方法,获得@InEnum
注解的values()
属性,获得值数组,设置到values
属性种。 - 实现
#isValid(value, context)
方法,实现校验参数值,是否在values
范围内。-
<2.1>
处,校验参数值在范围内,直接返回true
,校验通过。 -
<2.2.1>
处,校验不通过,自定义提示语句。 -
<2.2.2>
处,校验不通过,所以返回false
。
-
至此,我们已经完成了自定义约束的实现。下面,我们来进行下测试。
5.5 UserUpdateGenderDTO
在 cn.iocoder.springboot.lab22.validation.dto
包路径下,创建 UserUpdateGenderDTO 类,为用户更新性别 DTO。代码如下:
// UserUpdateGenderDTO.java
public class UserUpdateGenderDTO {
/**
* 用户编号
*/
@NotNull(message = "用户编号不能为空")
private Integer id;
/**
* 性别
*/
@NotNull(message = "性别不能为空")
@InEnum(value = GenderEnum.class, message = "性别必须是 {value}")
private Integer gender;
// ... 省略 set/get 方法
}
- 在
gender
字段上,添加@InEnum(value = GenderEnum.class, message = "性别必须是 {value}")
注解,限制传入的参数值,必须在 GenderEnum 枚举范围内。
5.6 UserController
修改 UserController 类,增加修改性别 API 接口。代码如下:
// UserController.java
@PostMapping("/update_gender")
public void updateGender(@Valid UserUpdateGenderDTO updateGenderDTO) {
logger.info("[updateGender][updateGenderDTO: {}]", updateGenderDTO);
}
模拟请求该 API 接口,响应结果如下:[图片上传失败...(image-d870d6-1699111506363)]
因为我们传入的请求参数 gender
的值为 null
,显然不在 GenderEnum 范围内,所以校验不通过,输出 "性别必须是 [1, 2]"
。