大家知道,在 Spring 中,有个很实用的 Bean Validation 的功能,它可以让我们用声明式的方式轻松分离验证逻辑。它内置了一些基础的验证器,但是,有一个比较常见的场景,这些内置的验证器是没有支持的,这个场景就是 “开始时间必须在结束时间之前”。我想了一想,通过 Java 中的反射以及 Comparable/Comparator 实现了一套通用的验证器,理论上,任何一种能通过比较逻辑比较的值,都可以验了。
大家可以先想一下,要实现一个类中去验证某两个属性的大小关系(或者一般的来讲:基于比较器的关系),该有哪些步骤呢?首先要比较两个属性,那么就需要拿到这两个属性的值,凭借经验,我们很容易就能想到:反射;其次,想比较两个值的大小,这个就更简单了,若两个值都是 Comparable 的,那么直接调用 java.lang.Comparable#compareTo
就好了。倘若不是 Comparable 的,那也好办,在 Java 中,有这么一个类 java.util.Comparator
它是任何一种比较逻辑的总接口,只要我们能给出一个实现了它的比较逻辑(或者说函数),也就可以验了。
OK,思路有了,按照 Spring 以及 Bean Validation 的规则实现出来就好了(这些规则请自行 Google),先来看基于 Comparable 的验证:
@Constraint(validatedBy = ComparableFieldsMatchValidator.class) //注意这个,这个声明了用哪个验证器去验证 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ComparableFieldsMatch { // 想要参与比较的左值 String leftFieldName(); // 想要参与比较的左值 String rightFieldName(); // 比较的规则 CompareRule compareRule(); // 以下三个方法是 @Constraint 必须要有的 String message() default ""; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
// 比较的规则 public enum CompareRule { LEFT_GREATER_THEN_RIGHT { @Override public List<Integer> acceptableValue() { return Collections.singletonList(1); } @Override public String messageTemplate() { return "⚠ 左值 [%s] 应比右值 [%s] 大"; } }, LEFT_EQUAL_RIGHT { @Override public List<Integer> acceptableValue() { return Collections.singletonList(0); } @Override public String messageTemplate() { return "⚠ 左值 [%s] 应等于右值 [%s]"; } }, LEFT_LESS_THEN_RIGHT { @Override public List<Integer> acceptableValue() { return Collections.singletonList(-1); } @Override public String messageTemplate() { return "⚠ 左值 [%s] 应比右值 [%s] 小"; } }, LEFT_GREATER_EQUAL_THEN_RIGHT { @Override public List<Integer> acceptableValue() { return Arrays.asList(1, 0); } @Override public String messageTemplate() { return "⚠ 左值 [%s] 应大于等于右值 [%s]"; } }, LEFT_LESS_EQUAL_THEN_RIGHT { @Override public List<Integer> acceptableValue() { return Arrays.asList(-1, 0); } @Override public String messageTemplate() { return "⚠ 左值 [%s] 应小于等于右值 [%s]"; } }; public abstract List<Integer> acceptableValue(); public abstract String messageTemplate(); }
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ComparableFieldsMatches { ComparableFieldsMatch[] value() default {}; }
// 验证器 public class ComparableFieldsMatchValidator implements ConstraintValidator<ComparableFieldsMatch, Object> { private ComparableFieldsMatch constraintAnnotation; @Override public void initialize(ComparableFieldsMatch constraintAnnotation) { this.constraintAnnotation = constraintAnnotation; } @Override public boolean isValid(Object fieldsOwner, ConstraintValidatorContext context) { // 通过反射拿到需要比较的 左、右值 Object leftFieldValue = Utils.getFieldThenMakeAccessible(fieldsOwner, constraintAnnotation.leftFieldName()); Object rightFieldValue = Utils.getFieldThenMakeAccessible(fieldsOwner, constraintAnnotation.rightFieldName()); CompareRule compareRule = constraintAnnotation.compareRule(); int compareToResult = ((Comparable) leftFieldValue).compareTo(rightFieldValue); return compareRule.acceptableValue().contains(compareToResult); // 如果比较结果命中了比较规则编码的 acceptableValue,则是合规的 } }
再来看基于 Comparator 的验证:
@Constraint(validatedBy = ComparatorFieldsMatchValidator.class) @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ComparatorFieldsMatch { String leftFieldName(); String rightFieldName(); // 需要的 Comparator 的 Class Class<? extends Comparator> comparatorClass(); CompareRule compareRule(); String message() default ""; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ComparatorFieldsMatches { ComparatorFieldsMatch[] value() default {}; }
// https://www.baeldung.com/spring-mvc-custom-validator // http://daobin.wang/2017/06/Spring-Validation/ public class ComparatorFieldsMatchValidator implements ConstraintValidator<ComparatorFieldsMatch, Object> { private ComparatorFieldsMatch constraintAnnotation; @Override public void initialize(ComparatorFieldsMatch constraintAnnotation) { this.constraintAnnotation = constraintAnnotation; } @Override public boolean isValid(Object fieldsOwner, ConstraintValidatorContext context) { Object leftFieldValue = Utils.getFieldThenMakeAccessible(fieldsOwner, constraintAnnotation.leftFieldName()); Object rightFieldValue = Utils.getFieldThenMakeAccessible(fieldsOwner, constraintAnnotation.rightFieldName()); CompareRule compareRule = constraintAnnotation.compareRule(); // 获得 Comparator 的 Class,并用反射创建示例 try { Comparator comparator = constraintAnnotation.comparatorClass().newInstance(); int compareResult = comparator.compare(leftFieldValue, rightFieldValue); return compareRule.acceptableValue().contains(compareResult); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } }
好了,验证器写好了,我们再来看一下使用:
@ComparableFieldsMatches( @ComparableFieldsMatch( leftFieldName = "startTime", rightFieldName = "endTime", compareRule = CompareRule.LEFT_LESS_THEN_RIGHT, message = "开始时间不能在结束时间之后" ) ) public class DatePeriod { public DatePeriod() { } public DatePeriod(@PastOrPresent @NotNull Date startTime, @PastOrPresent @NotNull Date endTime) { this.startTime = startTime; this.endTime = endTime; } @ApiModelProperty(value = "开始时间", required = true, example = "2018-10-01 00:00:00") @PastOrPresent @NotNull private Date startTime; @ApiModelProperty(value = "结束时间", required = true, example = "2018-12-01 00:00:00") @PastOrPresent @NotNull private Date endTime; public Date getStartTime() { return startTime; } public void setStartTime(Date startTime) { this.startTime = startTime; } public Date getEndTime() { return endTime; } public void setEndTime(Date endTime) { this.endTime = endTime; } // {"startTime": "%s", "endTime": "%s"} @Override public String toString() { return String.format("{/"startTime/": /"%s/", /"endTime/": /"%s/"}", DateFormatUtils.format(startTime, "yyyyMMdd"), DateFormatUtils.format(endTime, "yyyyMMdd")); } }
是不是很简单呢,这样我们的通用验证器能为我们免去很多重复的验证逻辑,解放了生产力 :smile: