文章目录
- 一: 使用场景
- 二: 定义FieldUniqueValid注解
- 2.1 @FieldUniqueValid
- 2.2 注解说明
- 2.3 @Constraint 注解介绍
- 2.4 @FieldUniqueValid注解使用
- 三:自定义FieldUniqueValidator校验类
- 3.1 实现ConstraintValidator
- 3.2 重写initialize方法
- 3.3 重写isValid方法
- 3.4 获取list集合重复数据的下标
- 3.5 思路
- 3.6 测试
- 3.6.1 前端传递参数,需要进行唯一性校验的字段
- 3.6.2 message提示
一: 使用场景
在开发过程中,前端给后端传递集合,并且需要保证集合的实体类中的某些字段必须是惟一的,不能重复。
传递的集合:
private List<User> userInfoList;
集合对应的实体类:
@Data
public class User {
private int id;
private String name;
private String card;
}
如果我们要保证传递的name或者card必须是唯一的,不能重复,应该如何实现呢,此时可以通过自定义注解的方式实现。
二: 定义FieldUniqueValid注解
2.1 @FieldUniqueValid
/**
* 该注解用于校验List集合实体类当中的某些字段的唯一性
* <p>
* 条件表达式支持使用"$parent."获取父节点属性(实体内不能使用除List集合外的其他集合类型,例如Set等)
* <ul>
* <li>标记在字段上:用于指定单个或多个字段,fields需填写</li>
* </ul>
* @author ikun
* @date 2023.07.27
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FieldUniqueValidator.class)
public @interface FieldUniqueValid {
//需要进行唯一校验的字段
String[] fieldsCode() default{};
String[] fieldsName() default{};
String message() default "[fieldName]列[index]行数据重复";
//不能有默认值,报错:contains Constraint annotation, but the groups parameter default value is not the empty array
Class<?>[] groups() default {};//在那种组中使用
Class<? extends Payload>[] payload() default {};
}
2.2 注解说明
@Documented
@Document 是 java 在生成文档,是否显示注解的开关。
@Target(ElementType.FIELD)
ElementType.FIELD:该注解只能声明在一个类的字段前。
2.3 @Constraint 注解介绍
@Constraint注解是Java Bean Validation框架中的一个注解,用于自定义约束注解,即自定义校验规则。
通过在自定义注解上添加@Constraint注解,可以将该注解标记为一个自定义约束注解。同时,需要指定一个实现了ConstraintValidator接口的验证器类,用于验证该注解所标记的字段或参数是否符合自定义的校验规则。
@Constraint注解有以下属性:
-
validatedBy:用于指定实现了ConstraintValidator接口的验证器类。该属性的值是一个Class对象数组,可以指定多个验证器类。
-
message:用于指定当校验失败时,所返回的错误信息。可以使用占位符{},在校验器中使用具体的参数替换。
-
groups:用于指定分组,即根据不同的分组应用不同的校验规则。
-
payload:用于指定元数据,即可以通过该属性传递一些额外的验证信息。
使用@Constraint注解,可以通过自定义注解的方式,为字段或参数添加自定义的校验规则,并实现校验逻辑。这样,在进行参数校验时,可以方便地通过注解的方式来调用自定义的校验规则。
2.4 @FieldUniqueValid注解使用
@FieldUniqueValid(fieldsCode = {"name,card"}, fieldsName = {"姓名,身份证号"})
private List<User> userInfoList;
- fieldsCode :需要校验的字段
- fieldsName :校验字段对应的名称
三:自定义FieldUniqueValidator校验类
@Slf4j
public class FieldUniqueValidator implements ConstraintValidator<FieldUniqueValid, Iterable<?>> {
private String[] fieldsCode;
private String[] fieldsName;
/**
* 数据有重复的字段名称
*/
private static final String FIELD_NAME= "[fieldName]";
/**
* 相同元素下标集合
*/
private static final String INDEX = "[index]";
/**
* 初始化参数
* @param constraintAnnotation 注解的值
*/
@Override
public void initialize(FieldUniqueValid constraintAnnotation) {
fieldsCode = constraintAnnotation.fieldsCode();
fieldsName = constraintAnnotation.fieldsName();
}
@Override
public boolean isValid(Iterable<?> value, ConstraintValidatorContext context) {
//如果没有配置校验字段信息,则直接通过
if(fieldsCode.length == 0 && fieldsName.length == 0){
return true;
}
if(fieldsCode.length != fieldsName.length){
throw new RuntimeException("@FieldUniqueValid注解所对应的fieldsCode和fieldsName无法相互映射");
}
if(value == null){
throw new RuntimeException("@FieldUniqueValid注解所在的集合为空,无法判断");
}
for (int i = 0; i < fieldsCode.length; i++) {
List<Object> list = new ArrayList<>();
Iterator<?> iterator = value.iterator();
long index;
for (index = 0; iterator.hasNext(); index++) {
Object fieldValue = null;
try {
//forceAccess = true,属性是私有的,需要设置为true打开权限
fieldValue = FieldUtils.readField(iterator.next(),fieldsCode[i],true);
} catch (IllegalAccessException e) {
log.error(fieldsName[i] + "转化失败,无法进行校验", e);
}
list.add(fieldValue);
}
//去重后的总数
long count = list.stream().distinct().count();
//去重之前和去重以后进行比较
if(count < index){
//返回重复元素下标集合
String sameIndex = getListSameIndex(list);
String defaultConstraintViolation = context.getDefaultConstraintMessageTemplate();
context.disableDefaultConstraintViolation();
String convertedConstraintViolation = defaultConstraintViolation.replace(FIELD_NAME, fieldsName[i]).replace(INDEX, sameIndex);
context.buildConstraintViolationWithTemplate(convertedConstraintViolation).addConstraintViolation();
return false;
}
}
return true;
}
}
3.1 实现ConstraintValidator
ConstraintValidator<FieldUniqueValid, Iterable<?>>:
- FieldUniqueValid:需要校验的注解,就是我们自定义的
- Iterable<?>:前端传递list的类型,此时用Iterable是因为数据支持list和set集合
3.2 重写initialize方法
可以从onstraintAnnotation参数中获取fieldsCode、fieldsName里面的参数。主要作用就是将注解的参数进行初始化
3.3 重写isValid方法
Iterable<?> value, ConstraintValidatorContext context
- value:可以获取到传递的集合数据
- context:获取注解上的message信息
3.4 获取list集合重复数据的下标
/**
* 集合【List】找出list中重复元素的下标(显示下标所在位置)
* @param list
*/
public static String getListSameIndex(List<?> list){
List<Object> same = new ArrayList<>();
List<?> collect = list.stream().distinct().collect(Collectors.toList());
if(collect.size() == list.size()){
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i <collect.size(); i++) {
for (int j = 0; j < list.size(); j++) {
if (list.get(j).equals(collect.get(i))){
same.add(j+1);
}
}
if (same.size() > 1){
sb.append(same).append("、");
}
same.clear();
}
return sb.substring(0, sb.toString().lastIndexOf("、"));
}
3.5 思路
首先获取到集合的数据,然后通过反射,用循环遍历获取到name字段的list数据,然后去重。将去重前后的list进行比较。如果长度变化了则说明有重复数据。此时返回false。然后我们我们通过getListSameIndex方法获取到list重复数据的下标然后替换[index]。