需求描述

需要对列表数据中某个字段验重。

解决方案

通过自定义注解,借助 spring 参数校验框架,抽象通用逻辑,通过反射获取字段值进行比较。

注解 - NoRepeat

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
/**
* 非重复校验
*
* @author daiwenzh5
* @date 2021/11/22
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Repeatable(NotRepeat.List.class)
@Constraint(validatedBy = NotRepeatConstraintValidator.class)
public @interface NotRepeat {

/**
* 字段名称
*/
String field();

/**
* 增强校验方法注册的 bean 名称 <br>
* 可以将数据库查重注册为 bean
*/
String beanName() default "";

String message() default "{javax.validation.constraints.NotRepeat.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List {
NotRepeat[] value();
}

}

校验逻辑

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* 非重复字段校验
*
* @author daiwenzh5
* @date 2021/11/22
*/
public class NotRepeatConstraintValidator implements ConstraintValidator<NotRepeat, List<?>> {

private String field;

private String message;

@SuppressWarnings({"java:S3740", "rawtypes"})
private IRepeatValidator bean;

@Override
public void initialize(NotRepeat constraintAnnotation) {
field = constraintAnnotation.field();
message = constraintAnnotation.message();
if (CharSequenceUtil.isNotEmpty(constraintAnnotation.beanName())) {
bean = SpringUtil.getBean(constraintAnnotation.beanName());
}
}

@Override
public boolean isValid(List<?> value, ConstraintValidatorContext context) {
Consumer<String> setMessage = param -> {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(String.format(
message,
param
))
.addConstraintViolation();
};
var first = findInList(value);
if (first.isPresent()) {
setMessage.accept(String.format(
message,
first.get()
));
return false;
}
if (bean != null) {
first = findInBean(value);
if (first.isPresent()) {
setMessage.accept(first.get());
return false;
}
}
return true;
}

/**
* 从 bean 方法中查找重复项
*/
@NotNull
private Optional<String> findInBean(List<?> value) {
Optional<String> first;
//noinspection unchecked
first = bean.pick(value)
.stream()
.map(Object::toString)
.sorted()
.findFirst();
return first;
}

/**
* 从参数中查找
*/
@NotNull
private Optional<String> findInList(List<?> value) {
return value.stream()
// 获取字段值
.map(this::getValue)
// 去除 null 值
.filter(Objects::nonNull)
// 按值分组,并统计数量
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
)).entrySet()
.stream()
// 筛选数量大于 1
.filter(it -> it.getValue() > 1)
// 取第一个数
.findFirst()
.map(Map.Entry::getKey)
.map(Object::toString);
}

private Object getValue(Object object) {
return ReflectUtil.getFieldValue(object, field);
}
}

增强校验方法

简单的建造工厂

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
* 基于 mybatis plus 的重复校验简单工厂
*
* @author daiwenzh5
* @date 2021/11/23
*/
@UtilityClass
public class RepeatValidatorMybatisPlusFactory {

/**
* 创建一个基于 mybatis plus 的{@linkplain IRepeatValidator 重复校验器}
*
* @param baseMapper 基于 mybatis plus mapper 类
* @param field 字段名
* @param getter 字段的 getter 方法
* @param <T> 待校验数据的实体类型
* @param <R> mapper 映射的实体类型
* @return {@linkplain IRepeatValidator 重复校验器}
*/
public <T, R> IRepeatValidator<T> create(
BaseMapper<R> baseMapper,
String field,
Function<T, String> getter
) {
return create(baseMapper, field, getter, (list, rQueryWrapper) -> rQueryWrapper);
}

/**
* 创建一个基于 mybatis plus 的{@linkplain IRepeatValidator 重复校验器}
*
* @param baseMapper 基于 mybatis plus mapper 类
* @param field 字段名
* @param getter 字段的 getter 方法
* @param moreWrapper 更多查询条件
* @param <T> 待校验数据的实体类型
* @param <R> mapper 映射的实体类型
* @return {@linkplain IRepeatValidator 重复校验器}
*/
public <T, R> IRepeatValidator<T> create(
BaseMapper<R> baseMapper,
String field,
Function<T, String> getter,
BiFunction<List<T>, QueryWrapper<R>, QueryWrapper<R>> moreWrapper) {
return list -> {
val collect = list.stream()
.map(getter)
.collect(Collectors.toList());
return baseMapper.selectObjs(
moreWrapper.apply(list, new QueryWrapper<R>()
.select(field)
.in(field, collect))
);
};
}
/**
* 创建一个基于 mybatis plus 的{@linkplain IRepeatValidator 重复校验器} <br>
* 防止更新时和自身发生验重
*
* @param baseMapper 基于 mybatis plus mapper 类
* @param field 字段名
* @param getter 字段的 getter 方法
* @param id 主键名
* @param idGetter 主键的 getter 方法
* @param <T> 待校验数据的实体类型
* @param <R> mapper 映射的实体类型
* @return {@linkplain IRepeatValidator 重复校验器}
*/
public <T, R> IRepeatValidator<T> create(
BaseMapper<R> baseMapper,
String field,
Function<T, String> getter,
String id,
Function<T, Object> idGetter
) {
return create(baseMapper, field, getter, id, idGetter, (list, rQueryWrapper) -> rQueryWrapper);
}


/**
* 创建一个基于 mybatis plus 的{@linkplain IRepeatValidator 重复校验器} <br>
* 防止更新时和自身发生验重
*
* @param baseMapper 基于 mybatis plus mapper 类
* @param field 字段名
* @param getter 字段的 getter 方法
* @param id 主键名
* @param idGetter 主键的 getter 方法
* @param moreWrapper 更多查询条件
* @param <T> 待校验数据的实体类型
* @param <R> mapper 映射的实体类型
* @return {@linkplain IRepeatValidator 重复校验器}
*/
public <T, R> IRepeatValidator<T> create(
BaseMapper<R> baseMapper,
String field,
Function<T, String> getter,
String id,
Function<T, Object> idGetter,
BiFunction<List<T>, QueryWrapper<R>, QueryWrapper<R>> moreWrapper
) {
return create(baseMapper, field, getter, (list, rQueryWrapper) -> {
val ids = list.stream().map(idGetter).collect(Collectors.toList());
return moreWrapper.apply(list, rQueryWrapper.notIn(
!ids.isEmpty(),
id,
ids
));
});
}
}

注册方法

1
2
3
4
5
6
7
8
@Bean
public IRepeatValidator<T> checkSomeField() {
return RepeatValidatorMybatisPlusFactory.create(
mapper,
"字段",
T::getField
));
}

注意事项

对于更新时的校验,为了防止字段值未修改,但因为在库中查询已存在,导致异常的情况,应该将 id 作为查询条件,筛选出所有不在 id 列表中的数据。

1
2
3
4
5
6
7
8
9
10
@Bean
public IRepeatValidator<T> checkSomeField() {
return RepeatValidatorMybatisPlusFactory.create(
mapper,
"字段",
T::getField,
"主键",
T::getId
));
}

评论