javax.validation 提供了自定义注解的方法,用户完全可以自定义一个和 @NotNull 类似的注解,然后完全兼容原生的处理方式,非常方便。
我们先来看看 @NotNull 注解的源码:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(List.class) @Documented @Constraint( validatedBy = {} ) public @interface NotNull { String message() default "{javax.validation.constraints.NotNull.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface List { NotNull[] value(); } }
仔细观察上面源码,注意到 @Constraint(validatedBy = {}) 注解了吗?Constraint 这个单词的意思是“限制,束缚;克制,拘束”,validatedBy 的意思是“通过…验证”,很明显它和参数校验沾点关系。
@Constraint 注解的作用是用于标识自定义约束注解,并指定该注解所使用的 ConstraintValidator 实现类。通过 @Constraint 注解,开发人员可以定义自己的校验规则,并将这些规则应用到实体类的字段上。@Constraint 注解通常与 @Target 和 @Retention 注解一起使用,用于指定自定义约束注解的作用目标和生命周期。
在自定义约束注解中,@Constraint 注解通常用于指定 ConstraintValidator 实现类,以便在校验时调用该实现类中的校验逻辑。通过 @Constraint 注解,开发人员可以将自定义的校验规则与校验逻辑进行关联,实现对实体类字段的自定义校验。这样可以使得校验规则的定义更加灵活和可扩展,同时也提高了代码的可维护性和可复用性。
我们再来看下 @Constraint 的描述信息,如下:
Marks an annotation as being a Bean Validation constraint.
将注释标记为 Bean 验证约束。
A given constraint annotation must be annotated by a @Constraint annotation which refers to its list of constraint validation implementations.
给定的约束注解必须由 @Constraint 注解进行注解,该注解指向其约束验证实现列表。
Each constraint annotation must host the following attributes:
每个约束注释必须包含以下属性:
(1)String message() default [...]; which should default to an error message key made of the fully-qualified class name of the constraint followed by .message. For example "{com.acme.constraints.NotSafe.message}"
String message() default[…]; 它应该默认为由约束的完全限定类名组成的错误消息键,后面跟着 .message。例如 "{com.acme.constraints.NotSafe.message}"
(2)Class<?>[] groups() default {}; for user to customize the targeted groups
Class<?>[] groups() default {}; 供用户自定义目标组
(3)Class<? extends Payload>[] payload() default {}; for extensibility purposes
Class<? extends Payload>[] payload() default {}; 为扩展之用
When building a constraint that is both generic and cross-parameter, the constraint annotation must host the validationAppliesTo() property. A constraint is generic if it targets the annotated element and is cross-parameter if it targets the array of parameters of a method or constructor.
在创建通用和跨参数约束时,约束注解必须包含 validationAppliesTo() 属性。如果约束以注解元素为目标,则该约束为通用约束;如果约束以方法或构造函数的参数数组为目标,则该约束为跨参数约束。
ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
ConstraintTarget validationAppliesTo() 默认为 ConstraintTarget.IMPLICIT;
This property allows the constraint user to choose whether the constraint targets the return type of the executable or its array of parameters. A constraint is both generic and cross-parameter if two kinds of ConstraintValidators are attached to the constraint, one targeting ValidationTarget.ANNOTATED_ELEMENT and one targeting ValidationTarget.PARAMETERS, or if a ConstraintValidator targets both ANNOTATED_ELEMENT and PARAMETERS.
该属性允许约束用户选择约束是针对可执行文件的返回类型还是其参数数组。一个约束既是通用的又是跨参数的,如果在约束中附加了两种约束校验器,一种针对 ValidationTarget.ANNOTATED_ELEMENT,另一种针对 ValidationTarget.PARAMETERS、或者如果一个 ConstraintValidator 同时针对 ANNOTATED_ELEMENT 和 PARAMETERS。
Such dual constraints are rare. See SupportedValidationTarget for more info.
这种双重约束是罕见的。更多信息请参见 SupportedValidationTarget。
Here is an example of constraint definition:
下面是约束定义的一个例子:
@Documented @Constraint(validatedBy = OrderNumberValidator.class) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) public @interface OrderNumber { String message() default "{com.acme.constraint.OrderNumber.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }Author:
Emmanuel Bernard, Gavin King, Hardy Ferentschik
@Constraint 上的描述大概是说,通过这个注解可以把一个你自己定义的注解标记为 Jakarta Bean 的验证约束,但是它要求你自己定义的那个注解必须包含三个属性,分别是:
String message() default […]; // 验证失败的错误信息
Class<?>[] groups() default {}; // 用于分组校验
Class<? extends Payload>[] payload() default {};
Class<? extends ConstraintValidator<?,?>>[] validatedBy
validatedBy 这个字段的意思是说,指定用于验证约束注解验证器,也就是具体的验证逻辑需要写到一个验证器类里,然后用这个参数去指定验证器,验证器必须是 ConstraintValidator 的实现类。
注意,实现约束的 ConstraintValidator 类。对于给定的 ValidationTarget,给定的类必须引用不同的目标类型。如果两个 ConstraintValidator 引用了相同的类型,则会出现异常。
最多只能接受一个针对方法或构造函数参数数组(又称交叉参数)的 ConstraintValidator。如果出现两个或更多,则会出现异常。
注意,上面提到了 ConstraintValidator 类,它是什么?ConstraintValidator 类的作用是用于实现自定义约束注解的校验逻辑。通过实现 ConstraintValidator 接口,开发人员可以自定义校验规则,并在实体类的字段上使用自定义的约束注解来对字段进行校验。ConstraintValidator 类中的两个泛型参数分别表示注解类型和被校验的字段类型,开发人员需要实现 initialize() 方法来初始化 ConstraintValidator 实例,并实现 isValid() 方法来定义校验逻辑。ConstraintValidator 类的主要作用是将校验逻辑与注解进行解耦,使得校验规则的定义更加灵活和可复用。
Defines the logic to validate a given constraint A for a given object type T.
定义用于验证给定对象类型 T 的给定约束 A 的逻辑。
Implementations must comply to the following restriction:
实现必须遵守以下限制:
T must resolve to a non parameterized type(T 必须解析为非参数化类型)
or generic parameters of T must be unbounded wildcard types(或 T 的泛型参数必须是无限制通配符类型)
The annotation SupportedValidationTarget can be put on a ConstraintValidator implementation to mark it as supporting cross-parameter constraints. Check out SupportedValidationTarget and Constraint for more information.
可以在 ConstraintValidator 实现中添加注解 SupportedValidationTarget,将其标记为支持跨参数约束。请查看 SupportedValidationTarget 和 Constraint 了解更多信息。
Author:
Emmanuel Bernard, Hardy Ferentschik
通过上面介绍,自定义注解需要通过 @Constraint 注解指定自定义注解的校验器,例如:
package com.hxstrive.validation.custor; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * 自定义注解 * @author HuangXin * @since 1.0.0 2024/2/5 10:13 **/ @Documented // 指定自定义注解的校验逻辑,必须实现 ConstraintValidator 接口 @Constraint(validatedBy = { MyConstraintValidator.class }) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) public @interface My { // 提示消息 String message() default "{org.hibernate.validator.constraints.Email.message}"; // 分类 Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; // 业务属性,用来指定字段的可选值,多个值使用竖线分隔 // 如:yes|no 表示字段只允许出现 yes 或 no 字符串 String value() default ""; }
自定义注解属性说明:
message:定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制。
groups:这里主要进行将 validator 进行分类,不同的类 group 中会执行不同的 validator 操作。
payload:主要是针对 bean 的,使用不多。
value:自定义的业务字段,可选。
要实现自己的校验逻辑,需要实现 ConstraintValidator 接口,例如:
package com.hxstrive.validation.custor; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 编写自定义的注解逻辑 * @author HuangXin * @since 1.0.0 2024/2/5 10:15 **/ public class MyConstraintValidator implements ConstraintValidator<My, String> { private final List<String> optionalValue = new ArrayList<>(); // 校验逻辑 @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 判断注解字段的值是否是固定的值 return optionalValue.contains(value); } // 初始化,获取 @My 注解中 value 属性设置的值,如:yes|no @Override public void initialize(My constraintAnnotation) { String value = constraintAnnotation.value(); if(null != value) { optionalValue.addAll(Arrays.asList(value.split("\\|"))); } ConstraintValidator.super.initialize(constraintAnnotation); } }
到这里,自定义注解就完成了。接下来,我们将通过一个示例来演示如何使用自定义的 @My 注解,例如:
package com.hxstrive.validation.custor; import lombok.Builder; import lombok.Data; import org.hibernate.validator.HibernateValidator; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import java.util.Set; /** * 验证自定义的注解 @My * @author HuangXin * @since 1.0.0 2024/1/10 9:13 */ @Data @Builder public class MyDemo { // 使用自定义的注解 // 限制 name 字段的值只能是 yes 或者 no 字符串 @My(value = "yes|no", message = "非法值") private String name; public static void main(String[] args) { MyDemo.builder().name("yes").build().validator("case1"); // Fail MyDemo.builder().name("y").build().validator("case2"); // Fail MyDemo.builder().name(null).build().validator("case3"); // OK } private void validator(String caseName) { validator(caseName, this); } private <T> void validator(String caseName, T obj) { // 手动调用 API 对定义了注解的字段进行校验 Validator validator = Validation.byProvider(HibernateValidator.class).configure() .failFast(true).buildValidatorFactory().getValidator(); Set<ConstraintViolation<T>> set = validator.validate(obj); if (set.size() > 0) { // 校验失败 System.out.println(caseName + " :: Fail :: " + set.iterator().next().getMessage()); } else { // 校验通过 System.out.println(caseName + " :: Succeed"); } } }
运行示例,输出如下:
case1 :: Succeed case2 :: Fail :: 非法值 case3 :: Fail :: 非法值