上篇文章介绍了 @Valid 和 @Validated 的区别和参数校验的进阶使用,本文将介绍如何自定义一个参数校验约束注解和校验器。
看源码
想要自定义,先找个官方提供的注解看看它是怎么实现的,然后我们就照葫芦画瓢写一个呗。
葫芦就随便找个@NotNull
注解吧,看下它是怎么实现的,下面是@NotNull
注解的源码截图:
只是因为在@NotNull
注解多看了一眼,再也没有忘记@Constraint
注解,很明显,@Constraint
注解就是让@NotNull
校验生效的注解,那接下来我们就来看看@Constraint
。
@Constraint
用于标注自定义约束注解。它有一个属性:
validatedBy
:指定一个或多个实现了ConstraintValidator 接口的验证器类,用于定义对应的验证逻辑。这个属性的值是一个Class数组,可以指定一个或多个验证器类。
那ConstraintValidator
接口是什么呢?看一下源码。
java
public interface ConstraintValidator<A extends Annotation, T> {
/**
* Initializes the validator in preparation for
* {@link #isValid(Object, ConstraintValidatorContext)} calls.
* The constraint annotation for a given constraint declaration
* is passed.
* <p>
* This method is guaranteed to be called before any use of this instance for
* validation.
* <p>
* The default implementation is a no-op.
*
* @param constraintAnnotation annotation instance for a given constraint declaration
*/
default void initialize(A constraintAnnotation) {
}
/**
* Implements the validation logic.
* The state of {@code value} must not be altered.
* <p>
* This method can be accessed concurrently, thread-safety must be ensured
* by the implementation.
*
* @param value object to validate
* @param context context in which the constraint is evaluated
*
* @return {@code false} if {@code value} does not pass the constraint
*/
boolean isValid(T value, ConstraintValidatorContext context);
}
ConstraintValidator
接口用于定义自定义约束注解的验证逻辑。它定义了两个泛型参数:第一个参数表示要验证的注解类型,第二个参数表示要验证的字段类型。
ConstraintValidator
接口有两个方法:
initialize()
方法: 这个方法在验证器初始化时调用,可以用于获取注解中的属性值,进行一些初始化操作。isValid()
方法:这是ConstraintValidator接口中最重要的方法,用于实际执行验证逻辑。在这个方法中编写验证规则的具体逻辑,判断字段值是否符合约束条件,并返回一个布尔值表示验证结果。
介绍了这么多,下面我们就来自定义一个约束注解和校验器。
实操
实际工作中我们可能会遇到这样的情况,添加用户时可能要校验性别字段传值是否在性别数组或者枚举中,以此来校验性别传递的数据是否正确,下面我们就以这个例子自定义一个参数校验器。
前戏
实操动手之前先要准备一些东西。
首先定义一个接口,实现该接口之后将数据放到集合中,方便校验时获取。
java
public interface EnumValid {
List<Integer> validValues();
}
这里定义一个枚举GenderEnum
,实现EnumValid
接口把枚举值放入到集合中。
java
/**
* 性别枚举
* author: 公众号:索码理(suncodernote)
*/
@AllArgsConstructor
@Getter
public enum GenderEnum implements EnumValid{
MALE(1),
FEMALE(2),
UNKNOWN(0),
;
private final Integer gender;
@Override
public List<Integer> validValues() {
return Arrays.stream(GenderEnum.values()).map(GenderEnum::getGender).collect(Collectors.toList());
}
}
自定义约束注解
仿照@NotNull
注解定义一个约束注解InEnum
,它用于约束枚举值字段必须在集合中。message
属性表示校验失败时的提示语。
java
/**
* 判断值是否在枚举中
* author: 公众号:索码理(suncodernote)
*/
@Documented
@Constraint(validatedBy = {InEnumValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(InEnum.List.class)
public @interface InEnum {
/**
* 提示语
* @return
*/
String message() default "{site.suncodernote.validation.constraints.InEnum.message}";
/**
* 分组
* @return
*/
Class<?>[] groups() default { };
/**
* 枚举类
* @return
*/
Class<? extends EnumValid> value();
Class<? extends Payload>[] payload() default { };
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
InEnum[] value();
}
}
Class<? extends Payload>[] payload() default { }
一定要有,不管是否用到。payload 是一种用于将额外信息传递到验证约束的机制。实际上,payload 本身并不具有具体的功能,它只是一个用于携带额外信息的容器。 这里不过多介绍payload,感兴趣的可以自己试试。
自定义校验器
下面来自定义一个参数校验器InEnumValidator
实现 ConstraintValidator
接口,initialize
初始化时将实现了EnumValid
接口,并重写了validValues()
方法的子类中的集合赋值给list
属性,然后在isValid
方法中获取被InEnum 注解标记的字段的值,并判断该字段的值是否在list
中。
java
/**
* 是否在枚举中验证器
* author: 公众号:索码理(suncodernote)
*/
public class InEnumValidator implements ConstraintValidator<InEnum, Integer> {
private List<Integer> list;
@Override
public void initialize(InEnum constraintAnnotation) {
Class<? extends EnumValid> value = constraintAnnotation.value();
EnumValid[] enumConstants = value.getEnumConstants();
list = enumConstants[0].validValues();
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return list.contains(value);
}
}
测试
上面准备好了,下面来测试一下,依旧用UserBean ,用InEnum 注解标记 gender
属性。
java
@Data
public class UserBean {
@NotEmpty
private String username;
@Min(value = 18 )
private Integer age;
private String email;
@InEnum(value = GenderEnum.class)
private Integer gender;
}
定义一个访问接口:
java
@RestController
@RequestMapping("validation")
public class ValidationController {
@GetMapping("user")
public UserBean validUserBean(@Validated UserBean userBean) {
System.out.println(userBean);
return userBean;
}
}
测试: 从测试结果中可以看到校验是成功的,message 也是我们在InEnum 注解中定义的message。为了友好的提示,我们可以在resources 目录下新建一个ValidationMessages_zh_CN.properties 文件,文件内容如下:
java
site.suncodernote.validation.constraints.InEnum.message=不在枚举中
然后修改配置文件,加上如下配置:
java
# 国际化
# 默认名称,可以写多个,用逗号分隔
spring.messages.basename=ValidationMessages
spring.messages.encoding=UTF-8
以上步骤就是配置了参数校验的国际化信息,关于Springboot国际化操作可以参考我之前的文章。
接下来再测试一下,可以看到结果已经是我们配置的国际化信息的数据了。 到此就结束了。
总结
本文介绍了如何在Springboot中自定义参数校验,用好参数校验能帮助我们节省很多重复的校验逻辑。你发现了吗?在本文示例中,我们使用参数校验都是在Controller控制层 进行校验的,在工作中并不是所有的校验都是在Controller控制层,那如果这样该怎么办呢?敬请关注,下篇文章将为你揭晓答案。