实现一个通用的基于Comparable的Validator

前言

大家知道,在 Spring 中,有个很实用的 Bean Validation 的功能,它可以让我们用声明式的方式轻松分离验证逻辑。它内置了一些基础的验证器,但是,有一个比较常见的场景,这些内置的验证器是没有支持的,这个场景就是 “开始时间必须在结束时间之前”。我想了一想,通过 Java 中的反射以及 Comparable/Comparator 实现了一套通用的验证器,理论上,任何一种能通过比较逻辑比较的值,都可以验了。

正文

大家可以先想一下,要实现一个类中去验证某两个属性的大小关系(或者一般的来讲:基于比较器的关系),该有哪些步骤呢?首先要比较两个属性,那么就需要拿到这两个属性的值,凭借经验,我们很容易就能想到:反射;其次,想比较两个值的大小,这个就更简单了,若两个值都是 Comparable 的,那么直接调用 java.lang.Comparable#compareTo 就好了。倘若不是 Comparable 的,那也好办,在 Java 中,有这么一个类 java.util.Comparator 它是任何一种比较逻辑的总接口,只要我们能给出一个实现了它的比较逻辑(或者说函数),也就可以验了。

OK,思路有了,按照 Spring 以及 Bean Validation 的规则实现出来就好了(这些规则请自行 Google),先来看基于 Comparable 的验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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 {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 比较的规则
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();
}
1
2
3
4
5
6
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ComparableFieldsMatches {

ComparableFieldsMatch[] value() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 验证器
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 的验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 {};
}
1
2
3
4
5
6
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ComparatorFieldsMatches {

ComparatorFieldsMatch[] value() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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);
}
}
}

好了,验证器写好了,我们再来看一下使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@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"));
}
}

是不是很简单呢,这样我们的通用验证器能为我们免去很多重复的验证逻辑,解放了生产力 😄

参考资料

Spring MVC Custom Validation

Spring Validation 实现前置参数校验