在API开发过程中,我们经常会遇到需要对接口参数中的集合进行重复校验的场景,这些集合可能是基本数据类型,也可能是Java Bean对象。如果是基本类型,我们可以单纯通过将接收集合设置为Set来实现去重,下面我来展示一下如何通过Set的形式实现最简单的请求参数去重。
首先在接收参数的Bean中声明待去重集合testSet
:
public class uniqueCollectionRequest {
private Set<String> testSet;
}
然后在Controller中声明一个接口去接受这个Request为Body的请求:
@PostMapping
public Integer uniqueCollectionTest(@RequestBody uniqueCollectionRequest request) throws HttpException{
log.info("log for set size:{}", request.getTestSet().size());
return request.getTestSet().size();
}
接下来构造请求:
curl --location --request POST 'http://localhost:8080/api/uniqueCollectionTest' \
--header 'Content-Type: application/json;charset=utf-8' \
--header 'Content-Type: text/plain' \
--data-raw '{
"testSet": [
"a","b","c","c"
]
}'
可以看到最后返回的结果是3,也就是说我们所使用的Set实现了我们需要的去重功能。
我们知道java中的HashSet,HashMap等结构都是通过equals方法来实现,这里有一篇很好的文章阐述了这个问题:hashcode()和equals()及HashSet判断对象相等。
为了实现这个目的,我们定义对象Student并重写它的hashCode及equals方法:
public class Student {
private int studentNumber;
private String studentName;
@Override
public int hashCode() {
return studentNumber * studentName.hashCode();
}
@Override
public boolean equals(Object obj) {
Student s = (Student) obj;
return this.studentName.equals(s.studentName) && this.studentNumber == s.studentNumber;
}
@Override
public String toString() {
return studentNumber + ":" + studentName;
}
}
然后构造包含学生Set的Request来实现,我们重新定义UniqueCollectionRequst:
public class UniqueCollectionRequest {
private Set<Student> studentSet;
}
同理,在controller中接收这个request并打印出studentSet的大小:
@PostMapping
public Integer uniqueCollectionTest(@RequestBody uniqueCollectionRequest request) throws HttpException{
log.info("log for set size:{}", request.getStudentSet().size());
return request.getStudentSet().size();
}
接下来构造请求:
curl --location --request POST 'http://localhost:8080/api/uniqueCollectionTest' \
--header 'Content-Type: application/json;charset=utf-8' \
--header 'Content-Type: text/plain' \
--data-raw '{
"studentSet": [
{ "studentNumber":0,"studentName":"jerry" },
{ "studentNumber":1,"studentName":"tom" },
{ "studentNumber":1,"studentName":"tom" },
]
}'
可以看到输出的结果为2。
通过上面的两个例子我们可以看到,使用Set的确可以实现针对基础类型或者Bean的去重操作,但是当我们想要对输入集合进行重复性校验,违反校验规则时抛出异常或错误码的时候,就显得无能为力了,这时候我会推荐大家使用ConstrainValidator来优雅的处理参数校验问题。这里有一篇很好的文章向你介绍ConstrainValidator的机制和例子,来自baeldung。推荐感兴趣的同学阅读。Spring MVC Custom Validation
接下来的两个例子与bealdung的文章有些许类似,如果你英文足够好已经看过了上面链接中的文章,那么大可以愉快的忽略掉接下来我要讲的内容。
首先自定义去重操作的注解将会发生在针对于class对象的指定域(feild)进行去重操作,同时支持注解的方式指定联合域来进行去重操作。
第一步,定义你需要的注解类:
@Target({
FIELD
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
UniqueCollectionValidator.class})
public @interface UniqueCollection {
String[] uniqueKeys();
String message() default "object in input collection should be unique.";
Class<?>[] groups() default {
};
Class<? extends Payload>[] payload() default {
};
}
其中元注解@Targe注解指明该注解@UniqueCollection是作用于域之上的;
@Retention(RUNTIME)表明这种类型的Annotations将会被JVM保留,所以他们能在运行时被JVM或者其他反射机制的代码所读取和使用;
@Documented表明该注解将会被包含在javadoc中;
@Constraint表明该注解的作用是表明该注解是用于校验,同时该注解使用的是UniqueCollectionValidator类实现的具体校验逻辑。
第二步,实现UniqueCollectionValidator进行参数校验逻辑:
public class UniqueCollectionValidator implements ConstraintValidator<UniqueCollection, Collection<?>> {
private UniqueCollection uniqueCollection;
@Override
public void initialize(UniqueCollection constraintAnnotation) {
this.uniqueCollection = constraintAnnotation;
}
@Override
public boolean isValid(Collection<?> target, ConstraintValidatorContext constraintValidatorContext) {
if (CollectionUtils.isEmpty(target)) {
return true;
}
List<String> uniqueKeys = Arrays.asList(uniqueCollection.uniqueKeys());
if (uniqueKeys.isEmpty()) {
return true;
}
int targetSize = target.size();
int targetAfterDistinctSize = (int) target.stream()
.filter(distinctByKey(
i -> uniqueKeys.stream()
.map(key -> buildDistinctKey(i, key))
.collect(Collectors.toList())))
.count();
return targetSize == targetAfterDistinctSize;
}
private String buildDistinctKey(Object o, String fieldName) {
try {
Field field = o.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (String) field.get(o);
} catch (Exception e) {
throw new IllegalArgumentException("can not find field named:" + fieldName);
}
}
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
}
首先我们看到,参数校验逻辑实现类实现了ConstraintValidator接口,同时该接口两个泛型分别是这个Validator工作的注解类或其子类,这个Validator所去进行校验的对象。
该Validator持有注解类对象并在initialize的同时将该注解类对象初始化。
覆盖该类的isValida方法,当满足我们需要的校验条件的时候该方法返回true,否则返回false,返回false的同时,在注解接口中我们定义了错误信息:message()。
使用该注解的方式也非常简单:
@UniqueCollection(uniqueKeys = {
"key1", "key2"}, message = "collection items not unique.")
List<SomeClass> toBeValidateCollection;
我们针对SomeClass里面的field key1和key2进行校验,并且在校验失败时会抛出collection items not unique
的异常信息。
以上就是自定义参数校验注解的全部流程,在这里我们用到了Spring Boot注解,反射,元注解等知识,对相关知识仍旧存在欠缺的同学我推荐你们阅读其他文章进行了解,真正能够做到知其然,知其所以然。