说来真巧,最近写一个项目,也是要马上写到用户模块了,也要涉及密码一致校验问题,起初也想着前端做了验证,后端对于这点不做也行,就算做了,单独写在业务校验里确实也可以不过后面越想越觉得不得劲,因为说起校验,除了咱们@NotNull, @Max, @Digits等在javax.validation包中也就是Jakarta Bean Validation规范(后续简称JBV规范)中的校验,还有就是咱们所说的业务校验了,比如查看用户是否存在这种可能跟其它业务或者数据库打交道的,但是自己写也是可以不用@NotNull, @Max等注解,直接在业务校验中校验非空和最大值等,这看个人代码风格习惯了那我个人习惯肯定是尽量分开了,相当于javax.validation包中的属于简单校验后面的业务校验属于复杂校验那这么一说来,这个密码一致校验是属于什么校验呢?我当然认为他是简单校验了。既然是简单校验那就没有必要写在后续的复杂校验逻辑中,虽然其实实现起来肯定也是简单两三行代码就解决了,但是我还是渴望着类似做到利用JBV规范的机制提供简单自定义注解达到最终校验效果,这样的复用性肯定是最好的一般咱们肯定要引spring-boot-starter-validation,而其JBV规范的实现其实是Hibernate Validator,所以我跑去看了一下其文档,关于自定义的地方(我用的Hibernate Validator版本是6.2.0.Final哈)在Custom Constraints也就是自定义约束的时候,这里是支持Class-level constraints也就是类级别的约束,看翻译吧,主要就是处理对象中多个属性的关联校验而类级别的自定义约束,实现也很简单,写一个检查一致性的注解@CheckConsistency放在需要被校验的类UserRequest上@Data @CheckConsistency public class UserRequest { private String password; private String passwordAgain; } 这个注解@CheckConsistency定义的时候要被JBV规范中的@Constraint注解注释,并且提供一个怎么校验的校验器填充在其validatedBy中,比如@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = { CheckConsistencyValidator.class }) public @interface CheckConsistency { // 下面是三个属性是JVB规范的必带属性,没有会报错 String message() default "不一致"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }而这个自定义校验器CheckConsistencyValidator需要实现JBV规范中的接口ConstraintValidator<CheckConsistency, UserRequest>,其中的两个泛型分别就是自定义的注解@CheckConsistency以及被校验的对象UserRequest了public class CheckConsistencyValidator implements ConstraintValidator<CheckConsistency, UserRequest> { @Override public boolean isValid(UserRequest request, ConstraintValidatorContext context) { return Objects.equals(request.getPassword(), request.getPasswordAgain()); } }基本实现都完成了,简单测试一下是可以的V2版本但是稍加注意,就会发现,现在的实现其实只是针对了UserRequest这一个被校验对象,假设还有个OrderRequest对象,里面也有两个属性name和nameAgain要一致性校验,岂不是这上面的一套注解+Validator就失效了因此@CheckConsistency和ConstraintValidator需要第二版,也就是升级为@CheckConsistencyV2和ConstraintValidatorV2结合刚才的问题,抽象出共同的,再把不同的作为配置配起来即可,那我们@CheckConsistencyV2应该变为@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = { CheckConsistencyValidatorV2.class }) public @interface CheckConsistencyV2 { String fieldName1(); String fieldName2(); // 下面是三个属性是JVB规范的必带属性,没有会报错 String message() default "不一致v2"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }可以看到注解中提供两个属性名字fieldName1()和fieldName2()来配置这次需要一次性校验的字段名而ConstraintValidatorV2实现的接口ConstraintValidator的泛型就不能再是UserRequest了,而应该是Object了,代码如下public class CheckConsistencyValidatorV2 implements ConstraintValidator<CheckConsistencyV2, Object> { private String fieldName1; private String fieldName2; /** * 这个方法也是{@link ConstraintValidator}中的方法 * 因为咱们注解中有额外的属性,所以需要这个方法先来初始化我们需要的值 */ @Override public void initialize(CheckConsistencyV2 constraintAnnotationV2) { this.fieldName1 = constraintAnnotationV2.fieldName1(); this.fieldName2 = constraintAnnotationV2.fieldName2(); } @Override public boolean isValid(Object object, ConstraintValidatorContext context) { // 这里BeanWrapper只是spring的一种内部接入Java Bean的方式 // 你也可以选自己喜欢的方式,比如Apache的BeanUtils,我这里就懒得引包了 BeanWrapper beanWrapper = new BeanWrapperImpl(object); Object value1 = beanWrapper.getPropertyValue(this.fieldName1); Object value2 = beanWrapper.getPropertyValue(this.fieldName2); return Objects.equals(value1, value2); } }最后咱们使用也还是和之前一样,这次我们UserRequest也升级为UserRequestV2。以示区别@Data @CheckConsistencyV2(fieldName1 = "password", fieldName2 = "passwordAgain") public class UserRequestV2 { private String password; private String passwordAgain; }再来测试一把,一切还是ok的哈到这里了,就如刚才所说,假如有个OrderRequest对象,里面也有两个属性name和nameAgain要一致性校验,那只是改改属性值,也很简单就复用了@CheckConsistencyV2(fieldName1 = "name", fieldName2 = "nameAgain")V3版本看似一切都差不多了,但是我想到这时,我很不满意,为啥,因为我最讨厌的东西就在上面的实现中,也就是fieldName1 = "password", fieldName2 = "passwordAgain"中的"password"和"passwordAgain"这两个字符串很简单,假设某天UserRequestV2的password属性名改为pw,但是@CheckConsistencyV2中的fieldName1 = "password"却忘了改,而且打包也都不会报编译错误,可能最终造成现网问题(测试漏测的前提下哈),别问,问就是每次发现这种问题,都要查看代码提交记录,满嘴飘香了,真的是,吃"别人这种屎"也吃太多回了(这也是为啥我后面专门要整一个小工具AutoConstants,扯远了哈)也就是这种编译都不不会发现的错误,因此我决定再想起他办法,把@CheckConsistencyV2和CheckConsistencyValidatorV2升级到@CheckConsistencyV3,CheckConsistencyValidatorV3来到V3,这次主要解决的问题就是声明哪些字段需要一致性校验时最好不要出现字符串这种以后可能出"雷"的做法,那怎样方法最好呢?当然是用注解啦,注解直接标在对应的字段上即可,这样以后改啥名字都无所谓了。所以我们需要一个标记的注解@ConsistencyGroup来标注哪些字段需要做一致性校验@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ConsistencyGroup { }当然我们的@CheckConsistencyV3就不需要什么fieldName1()和fieldName2()这些方法了,空的就行@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = { CheckConsistencyValidatorV3.class }) public @interface CheckConsistencyV3 { // 下面是三个属性是JVB规范的必带属性,没有会报错 String message() default "不一致v3"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }此时UserRequestV3就需要刚才新加的@ConsistencyGroup注解做标注了@Data @CheckConsistencyV3 public class UserRequestV3 { @ConsistencyGroup private String password; @ConsistencyGroup private String passwordAgain; }最后,重要的实现方式是在CheckConsistencyValidatorV3中,处理也很简单,就是扫出有@ConsistencyGroup注解的Field,然后再做取值对比public class CheckConsistencyValidatorV3 implements ConstraintValidator<CheckConsistencyV3, Object> { @Override public boolean isValid(Object object, ConstraintValidatorContext context) { List<Field> consistencyFields = new ArrayList<>(); ReflectionUtils.doWithFields(object.getClass(), consistencyFields::add, field -> field.isAnnotationPresent(ConsistencyGroup.class)); // 若这里为空或者有@ConsistencyGroup注解的字段不是2个,那就不处理直接返回true if (consistencyFields.isEmpty() || consistencyFields.size() != 2) return true; List<Object> valueList = consistencyFields.stream() .peek(ReflectionUtils::makeAccessible) .map(field -> ReflectionUtils.getField(field, object)) .collect(Collectors.toList()); return Objects.equals(valueList.get(0), valueList.get(1)); } }还是简单测试一下,很okV4版本写到这里,我本以为我会很满意,结果可达鸭突然眉头一皱,觉得还是不行,为啥呢?因为假设被校验对象中有两组需要分别校验一致性的字段时,比如下面的UserRequestTemp@Data public class UserRequestTemp { private String password; private String passwordAgain; private String name; private String nameAgain; }其中password和passwordAgain,name和nameAgain分别做一致性校验,此时我们V3版本就不管用了,这个问题我其实没有在V2中提到,但是V2是可以很简单就解决的,利用@Repeatable注解即可,再新增一个@CheckConsistencyV2List注解作为@CheckConsistencyV2注解的复数容器@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface CheckConsistencyV2List { CheckConsistencyV2[] value(); }从而UserRequestTemp就变为@Data @CheckConsistencyV2List({ @CheckConsistencyV2(fieldName1 = "password", fieldName2 = "passwordAgain"), @CheckConsistencyV2(fieldName1 = "name", fieldName2 = "nameAgain") }) public class UserRequestTemp { private String password; private String passwordAgain; private String name; private String nameAgain; }最后简单改改CheckConsistencyValidatorV2即可,我这里就不再赘述,回到我们V3的版本,我们是不能简单使用@Repeatable做扩展的,因为我们的@ConsistencyGroup注解只是一个标记注解,里面没有任何属性的此时其实我们可以直接想到的有两种办法再增加一个@ConsistencyGroup2的类似@ConsistencyGroup的分组注解-> 这样可以临时解决问题,但肯定不是可扩展的长期之道,毕竟增加一个分组注解,就需要修 改对应的Validator,这是不太符合开闭原则的@ConsistencyGroup中新增一个方法,String key(),这样不同分组通过key的方式做区分-> 但是这种使用String来控制代码逻辑,又是一个不可控的雷点,前面我也提到了,当然也 可以改成枚举XXXEnum key(),这样以后需要一组新的分组,去添加一个新的枚举即可, 只是我还是很不喜欢,毕竟还是不太满足开闭原则因此我理想的模式其实是可以新增东西,但是不要修改以前已经写好的任何逻辑,真正做到开闭原则,做到可扩展为了达到可扩展的效果,我们以UserRequestV4举例@Data public class UserRequestV4 { private String password; private String passwordAgain; private String name; private String nameAgain; }理想状态下我想验证password和passwordAgain一致性了,我新增一个@PasswordConsistency注解即可达到一致性验证的效果我想验证name和nameAgain一致性了,我新增一个@NameConsistency注解即可达到一致性验证的效果且上面实现的效果,是不需要我修改以前任何逻辑,只需要新增分组注解即可,即@Data @CheckConsistencyV4 public class UserRequestV4 { @PasswordConsistency private String password; @PasswordConsistency private String passwordAgain; @NameConsistency private String name; @NameConsistency private String nameAgain; }好,为了实现上面效果,我们就必须引入元注解,也就是注解的注解才可以达到效果那显然@PasswordConsistency和@NameConsistency都是和之前的@ConsistencyGroup的空注解,唯一不同的是它们都被新增的一个元注解@ConsistencyTag标记(注意其@Target此时是ElementType.ANNOTATION_TYPE)@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @ConsistencyTag public @interface NameConsistency { } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @ConsistencyTag public @interface PasswordConsistency { } @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ConsistencyTag { }这样@ConsistencyTag相当于是注解的抽象,类似实现类和接口的关系,@ConsistencyTag这里就是一个"接口",我们的CheckConsistencyValidatorV4只需要去关心处理@ConsistencyTag这个"接口",不用关心它的实现了(也就是@PasswordConsistency和@NameConsistency),从而实现了可扩展,实现了开闭原则那我们来看关键的类CheckConsistencyValidatorV4,如下:public class CheckConsistencyValidatorV4 implements ConstraintValidator<CheckConsistencyV4, Object> { // 这是一个小缓存,因为Class中哪些Field有ConsistencyTag注解是固定的 private static final Map<Class, List<Field>> cache = new HashMap<>(); @Override public boolean isValid(Object object, ConstraintValidatorContext context) { List<Field> consistencyFields = cache.computeIfAbsent(object.getClass(), this::getAllConsistencyFields); // 若这里为空或者有@ConsistencyTag注解的注解的字段不是偶数个,那就不处理直接返回true if (consistencyFields.isEmpty() || consistencyFields.size() % 2 != 0) return true; Map<Annotation, List<ConsistencyContext>> annotationListMap = consistencyFields.stream() .map(field -> this.toContext(field, object)) .collect(Collectors.groupingBy(ConsistencyContext::getAnnotation)); // 这里是展示所有的报错的写法 List<Annotation> annotations = annotationListMap.entrySet().stream() .filter(entry -> this.doNotEqual(entry.getValue())) .map(Map.Entry::getKey) .collect(Collectors.toList()); boolean isValid = annotations.isEmpty(); if (!isValid) { context.disableDefaultConstraintViolation(); annotations.stream() .map(AnnotationUtils::getAnnotationAttributes) .map(map -> map.get(ConsistencyTag.METHOD_KEY).toString()) .forEach(message -> context.buildConstraintViolationWithTemplate(message).addConstraintViolation()); } // 这里是展示只返回其中一个报错的写法 // Optional<Annotation> annotationOptional = annotationListMap.entrySet().stream() // .filter(entry -> this.doNotEqual(entry.getValue())) // .map(Map.Entry::getKey) // .findFirst(); // // boolean isValid = !annotationOptional.isPresent(); // if (!isValid) { // context.disableDefaultConstraintViolation(); // // Annotation annotation = annotationOptional.get(); // Map<String, Object> annotationAttributes = AnnotationUtils.getAnnotationAttributes(annotation); // String message = annotationAttributes.get(ConsistencyTag.METHOD_KEY).toString(); // context.buildConstraintViolationWithTemplate(message).addConstraintViolation(); // } return isValid; } private List<Field> getAllConsistencyFields(Class tClass) { List<Field> consistencyFields = new ArrayList<>(); ReflectionUtils.doWithFields(tClass, consistencyFields::add, field -> AnnotatedElementUtils.isAnnotated(field, ConsistencyTag.class)); return consistencyFields; } private boolean doNotEqual(List<ConsistencyContext> contexts) { return !(contexts.size() == 2 && Objects.equals(contexts.get(0).getValue(), contexts.get(1).getValue())); } private ConsistencyContext toContext(Field field, Object object) { Annotation[] annotations = field.getAnnotations(); Annotation consistencyTagAnnotation = Stream.of(annotations) .filter(annotation -> AnnotatedElementUtils.isAnnotated( AnnotatedElementUtils.forAnnotations(annotation), ConsistencyTag.class)) .findFirst() .get(); ReflectionUtils.makeAccessible(field); Object value = ReflectionUtils.getField(field, object); return ConsistencyContext.builder() .field(field) .value(value) .annotation(consistencyTagAnnotation) .build(); } @Data @Builder static class ConsistencyContext { private Field field; private Object value; private Annotation annotation; } }可以看到,实现稍微复杂了一点点,但是思路都是刚才我讲的,那是不变的,还是来测试一下,检查一下效果最后看到报错的描述是不同,就是相对之前,需要小小修改一下分组注解,以便可以区分报错,也就是每个分组注解都要加一个message方法,来指明报错@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @ConsistencyTag public @interface NameConsistency { String message() default "name不一致"; } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @ConsistencyTag public @interface PasswordConsistency { String message() default "password不一致"; }以及这个方法的关键字名字定义在@ConsistencyTag中@Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ConsistencyTag { String METHOD_KEY = "message"; }小结好了,写到这,其实呢,基本已经满足了题主问题的答案了,并且相关思路和代码都罗列在上面(若是代码不是很清楚的,可以到github上查看)但是嗷...说实在话,我还不想去睡的,因为都写到元注解这里了,我们可以说都已经在Hibernate Validator的基础上再搭建了一层关于一致性校验的元注解,也可说是一种规范,涉及的业务是对象多属性校验,那都到这地步了,为何不更近一步,去实现一个更加通用的对象多属性校验的元注解,不仅仅是针对一致性校验,还有很多其他校验,类似如public class UserRequestOther { @ConditionNotNullSource(max = 24) private Integer age; @ConditionNotNullTarget private String companyName; }也就是age超过24时,companyName属性不能空,类似这种多属性的校验,提供一种能力,方便这种模式的扩展,当然为了这么做是希望这一套注解@ConditionNotNullSource和@ConditionNotNullTarget能复用,正因为是多属性,由于业务不同,不同属性还可以组合,所以这种复用情况确实很少见的,但是这种造轮子对于编程技能还是有些帮助的,V1到V4看似版本迭代,但是实际V1,V2,V3都是造轮子给别人使用,但是V4其实是两个任务一个给造轮子的人造轮子使用(对应@ConsistencyTag,@CheckConsistencyV4和CheckConsistencyValidatorV4)还有个就是造轮子给别人使用(对应@PasswordConsistency和@NameConsistency)当然V4只是比较局限的给造轮子的人造轮子,想要写成更通用的达到V5,还是需要更多精力但今晚因为时间关系,狗命要紧( ╯▽╰),还是早点休息吧,以后有机会再说了,当然题主你也可以自己试试咯~那就拜拜了~
默认题主说的是类似 @NotNull 这种校验可以通过自定义注解+自定义校验规则实现,但是为了这么简单的场景写那么多不太值得多个字段之间的比较一般有业务属性,已经不是简单的“校验”了,做在校验里反而不灵活另外确认密码这样的操作,前端校验即可,后端不需要校验
说来真巧,最近写一个项目,也是要马上写到用户模块了,也要涉及密码一致校验问题,起初也想着前端做了验证,后端对于这点不做也行,就算做了,单独写在业务校验里确实也可以
不过后面越想越觉得不得劲,因为说起校验,除了咱们
@NotNull
,@Max
,@Digits
等在javax.validation
包中也就是Jakarta Bean Validation
规范(后续简称JBV
规范)中的校验,还有就是咱们所说的业务校验了,比如查看用户是否存在这种可能跟其它业务或者数据库打交道的,但是自己写也是可以不用@NotNull
,@Max
等注解,直接在业务校验中校验非空和最大值等,这看个人代码风格习惯了那我个人习惯肯定是尽量分开了,相当于
javax.validation
包中的属于简单校验那这么一说来,这个密码一致校验是属于什么校验呢?我当然认为他是简单校验了。
既然是简单校验那就没有必要写在后续的复杂校验逻辑中,虽然其实实现起来肯定也是简单两三行代码就解决了,但是我还是渴望着类似做到利用
JBV
规范的机制提供简单自定义注解达到最终校验效果,这样的复用性肯定是最好的一般咱们肯定要引
spring-boot-starter-validation
,而其JBV
规范的实现其实是Hibernate Validator
,所以我跑去看了一下其文档,关于自定义的地方(我用的Hibernate Validator
版本是6.2.0.Final哈)在Custom Constraints也就是自定义约束的时候,这里是支持Class-level constraints也就是类级别的约束,看翻译吧,主要就是处理对象中多个属性的关联校验

而类级别的自定义约束,实现也很简单,写一个检查一致性的注解
@CheckConsistency
放在需要被校验的类UserRequest
上这个注解
@CheckConsistency
定义的时候要被JBV
规范中的@Constraint
注解注释,并且提供一个怎么校验的校验器填充在其validatedBy
中,比如而这个自定义校验器
CheckConsistencyValidator
需要实现JBV
规范中的接口ConstraintValidator<CheckConsistency, UserRequest>
,其中的两个泛型分别就是自定义的注解@CheckConsistency
以及被校验的对象UserRequest
了基本实现都完成了,简单测试一下是可以的

V2版本
但是稍加注意,就会发现,现在的实现其实只是针对了
UserRequest
这一个被校验对象,假设还有个OrderRequest
对象,里面也有两个属性name
和nameAgain
要一致性校验,岂不是这上面的一套注解+
Validator
就失效了因此
@CheckConsistency
和ConstraintValidator
需要第二版,也就是升级为@CheckConsistencyV2
和ConstraintValidatorV2
结合刚才的问题,抽象出共同的,再把不同的作为配置配起来即可,那我们
@CheckConsistencyV2
应该变为可以看到注解中提供两个属性名字
fieldName1()
和fieldName2()
来配置这次需要一次性校验的字段名而
ConstraintValidatorV2
实现的接口ConstraintValidator
的泛型就不能再是UserRequest
了,而应该是Object
了,代码如下最后咱们使用也还是和之前一样,这次我们
UserRequest
也升级为UserRequestV2
。以示区别再来测试一把,一切还是ok的哈

到这里了,就如刚才所说,假如有个
OrderRequest
对象,里面也有两个属性name
和nameAgain
要一致性校验,那只是改改属性值,也很简单就复用了V3版本
看似一切都差不多了,但是我想到这时,我很不满意,为啥,因为我最讨厌的东西就在上面的实现中,也就是
fieldName1 = "password", fieldName2 = "passwordAgain"
中的"password"
和"passwordAgain"
这两个字符串很简单,假设某天
UserRequestV2
的password
属性名改为pw
,但是@CheckConsistencyV2
中的fieldName1 = "password"
却忘了改,而且打包也都不会报编译错误,可能最终造成现网问题(测试漏测的前提下哈),别问,问就是每次发现这种问题,都要查看代码提交记录,满嘴飘香了,真的是,吃"别人这种屎"也吃太多回了(这也是为啥我后面专门要整一个小工具AutoConstants,扯远了哈)也就是这种编译都不不会发现的错误,因此我决定再想起他办法,把
@CheckConsistencyV2
和CheckConsistencyValidatorV2
升级到@CheckConsistencyV3
,CheckConsistencyValidatorV3
来到V3,这次主要解决的问题就是声明哪些字段需要一致性校验时最好不要出现字符串这种以后可能出"雷"的做法,那怎样方法最好呢?当然是用注解啦,注解直接标在对应的字段上即可,这样以后改啥名字都无所谓了。
所以我们需要一个标记的注解
@ConsistencyGroup
来标注哪些字段需要做一致性校验当然我们的
@CheckConsistencyV3
就不需要什么fieldName1()
和fieldName2()
这些方法了,空的就行此时
UserRequestV3
就需要刚才新加的@ConsistencyGroup
注解做标注了最后,重要的实现方式是在
CheckConsistencyValidatorV3
中,处理也很简单,就是扫出有@ConsistencyGroup
注解的Field
,然后再做取值对比还是简单测试一下,很ok

V4版本
写到这里,我本以为我会很满意,结果可达鸭突然眉头一皱,觉得还是不行,为啥呢?因为假设被校验对象中有两组需要分别校验一致性的字段时,比如下面的
UserRequestTemp
其中
password
和passwordAgain
,name
和nameAgain
分别做一致性校验,此时我们V3版本就不管用了,这个问题我其实没有在V2中提到,但是V2是可以很简单就解决的,利用@Repeatable
注解即可,再新增一个@CheckConsistencyV2List
注解作为@CheckConsistencyV2
注解的复数容器从而
UserRequestTemp
就变为最后简单改改
CheckConsistencyValidatorV2
即可,我这里就不再赘述,回到我们V3的版本,我们是不能简单使用@Repeatable
做扩展的,因为我们的@ConsistencyGroup
注解只是一个标记注解,里面没有任何属性的此时其实我们可以直接想到的有两种办法
再增加一个
@ConsistencyGroup2
的类似@ConsistencyGroup
的分组注解-> 这样可以临时解决问题,但肯定不是可扩展的长期之道,毕竟增加一个分组注解,就需要修
改对应的
Validator
,这是不太符合开闭原则的@ConsistencyGroup
中新增一个方法,String key()
,这样不同分组通过key
的方式做区分-> 但是这种使用
String
来控制代码逻辑,又是一个不可控的雷点,前面我也提到了,当然也可以改成枚举
XXXEnum key()
,这样以后需要一组新的分组,去添加一个新的枚举即可,只是我还是很不喜欢,毕竟还是不太满足开闭原则
因此我理想的模式其实是可以新增东西,但是不要修改以前已经写好的任何逻辑,真正做到开闭原则,做到可扩展
为了达到可扩展的效果,我们以
UserRequestV4
举例理想状态下
password
和passwordAgain
一致性了,我新增一个@PasswordConsistency
注解即可达到一致性验证的效果name
和nameAgain
一致性了,我新增一个@NameConsistency
注解即可达到一致性验证的效果且上面实现的效果,是不需要我修改以前任何逻辑,只需要新增分组注解即可,即
好,为了实现上面效果,我们就必须引入元注解,也就是注解的注解才可以达到效果
那显然
@PasswordConsistency
和@NameConsistency
都是和之前的@ConsistencyGroup
的空注解,唯一不同的是它们都被新增的一个元注解@ConsistencyTag
标记(注意其@Target
此时是ElementType.ANNOTATION_TYPE
)这样
@ConsistencyTag
相当于是注解的抽象,类似实现类和接口的关系,@ConsistencyTag
这里就是一个"接口",我们的CheckConsistencyValidatorV4
只需要去关心处理@ConsistencyTag
这个"接口",不用关心它的实现了(也就是@PasswordConsistency
和@NameConsistency
),从而实现了可扩展,实现了开闭原则那我们来看关键的类
CheckConsistencyValidatorV4
,如下:可以看到,实现稍微复杂了一点点,但是思路都是刚才我讲的,那是不变的,还是来测试一下,检查一下效果
最后看到报错的描述是不同,就是相对之前,需要小小修改一下分组注解,以便可以区分报错,也就是每个分组注解都要加一个
message
方法,来指明报错以及这个方法的关键字名字定义在
@ConsistencyTag
中小结
好了,写到这,其实呢,基本已经满足了题主问题的答案了,并且相关思路和代码都罗列在上面(若是代码不是很清楚的,可以到github上查看)
但是嗷...说实在话,我还不想去睡的,因为都写到元注解这里了,我们可以说都已经在
Hibernate Validator
的基础上再搭建了一层关于一致性校验的元注解,也可说是一种规范,涉及的业务是对象多属性校验,那都到这地步了,为何不更近一步,去实现一个更加通用的对象多属性校验的元注解,不仅仅是针对一致性校验,还有很多其他校验,类似如也就是
age
超过24时,companyName
属性不能空,类似这种多属性的校验,提供一种能力,方便这种模式的扩展,当然为了这么做是希望这一套注解@ConditionNotNullSource
和@ConditionNotNullTarget
能复用,正因为是多属性,由于业务不同,不同属性还可以组合,所以这种复用情况确实很少见的,但是这种造轮子对于编程技能还是有些帮助的,V1到V4看似版本迭代,但是实际V1,V2,V3都是造轮子给别人使用,但是V4其实是两个任务
@ConsistencyTag
,@CheckConsistencyV4
和CheckConsistencyValidatorV4
)@PasswordConsistency
和@NameConsistency
)当然V4只是比较局限的给造轮子的人造轮子,想要写成更通用的达到V5,还是需要更多精力
但今晚因为时间关系,狗命要紧( ╯▽╰),还是早点休息吧,以后有机会再说了,当然题主你也可以自己试试咯~那就拜拜了~