Java自定义校验注解实现List、set集合字段唯一性校验

文章目录

  • [一: 使用场景](#一: 使用场景)
  • [二: 定义FieldUniqueValid注解](#二: 定义FieldUniqueValid注解)
    • [2.1 @FieldUniqueValid](#2.1 @FieldUniqueValid)
    • [2.2 注解说明](#2.2 注解说明)
    • [2.3 @Constraint 注解介绍](#2.3 @Constraint 注解介绍)
    • [2.4 @FieldUniqueValid注解使用](#2.4 @FieldUniqueValid注解使用)
  • 三:自定义FieldUniqueValidator校验类
    • [3.1 实现ConstraintValidator](#3.1 实现ConstraintValidator)
    • [3.2 重写initialize方法](#3.2 重写initialize方法)
    • [3.3 重写isValid方法](#3.3 重写isValid方法)
    • [3.4 获取list集合重复数据的下标](#3.4 获取list集合重复数据的下标)
    • [3.5 思路](#3.5 思路)
    • [3.6 测试](#3.6 测试)
      • [3.6.1 前端传递参数,需要进行唯一性校验的字段](#3.6.1 前端传递参数,需要进行唯一性校验的字段)
      • [3.6.2 message提示](#3.6.2 message提示)

一: 使用场景

在开发过程中,前端给后端传递集合,并且需要保证集合的实体类中的某些字段必须是惟一的,不能重复。

传递的集合:

java 复制代码
private List<User> userInfoList;

集合对应的实体类:

java 复制代码
@Data
public class User {

    private int id;

    private String name;
    
    private String card;

}

如果我们要保证传递的name或者card必须是唯一的,不能重复,应该如何实现呢,此时可以通过自定义注解的方式实现。

二: 定义FieldUniqueValid注解

2.1 @FieldUniqueValid

java 复制代码
/**
 * 该注解用于校验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注解使用

java 复制代码
@FieldUniqueValid(fieldsCode = {"name,card"}, fieldsName = {"姓名,身份证号"})
private List<User> userInfoList;
  • fieldsCode :需要校验的字段
  • fieldsName :校验字段对应的名称

三:自定义FieldUniqueValidator校验类

java 复制代码
@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集合重复数据的下标

java 复制代码
/**
     * 集合【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

3.6 测试

3.6.1 前端传递参数,需要进行唯一性校验的字段

3.6.2 message提示

相关推荐
YikNjy13 小时前
break和continue
java·开发语言·算法
SomeOtherTime13 小时前
Geojson相关(AI回答)
java·前端·python
日月云棠14 小时前
10 Integer —— 最常用的整数包装类深度解析
java·后端
秋914 小时前
java项目中cpu飙升排查及解决方法
java·开发语言
野生技术架构师14 小时前
牛客网2026最新大厂Java高频面试题精选(附标准答案)
java·开发语言
PH = 714 小时前
JAVA的SPI机制
java·开发语言
一 乐14 小时前
高校实习信息发布网站|基于Spring Boot的高校实习信息发布网站的设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·高校实习信息发布网站
weelinking14 小时前
【产品】11_实现后端接口——数据在背后如何流动
java·人工智能·python·sql·oracle·json·ai编程
摇滚侠14 小时前
东方通替换tomcat,实战经验
java
IT猿手14 小时前
多目标优化算法:多目标蛇优化算法(Multiple Objective Snake Optimizer,MOSO)(提供MATLAB代码)
开发语言·算法·matlab·动态路径规划·光伏模型参数估计