基于注解实现新增/编辑数据时的重复数据校验

基于注解实现新增/编辑数据时的重复数据校验

需求背景

之前项目组所做产品有很多单表业务的增删改查,每次新增修改时都需要查询是否存在重复数据,否则不能进行相关操作,一个一个查询太麻烦,而且容易造成代码冗余,所以这里基于注解+反射实现。

操作步骤

自定义重复数据校验注解

/**
 * @author changyuan
 * @date 2022/12/26
 * @description 新增操作字段重复性校验注解(条件 xxName+系统标识+学校 id +校区 id)
 */
@Target({ElementType.TYPE,  ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FieldRepeatValidatorHandler.class)
public @interface FieldRepeatValidator {

    /**
     * 实体类 id 字段
     * @return
     */
    String id() default "id";

    /**
     * 需要校验的字段
     * @return
     */
    String field();

    String message() default "你所输入的内容已存在";


    Class[] groups() default {};

    Class[]  payload() default {};
}


注解上添加约束器(具体验证处理逻辑)

@Constraint(validatedBy = FieldRepeatValidatorHandler.class)

验证器逻辑

/**
 * @author changyuan
 * @date 2022/12/27
 * @description 自定义字段重复校验注解处理逻辑
 */
@Slf4j
public class FieldRepeatValidatorHandler implements ConstraintValidator {

    private String id;
    private String field;
    private String message;


    @Override
    public void initialize(FieldRepeatValidator constraintAnnotation) {
        this.id = constraintAnnotation.id();
        this.field = constraintAnnotation.field();
        this.message = constraintAnnotation.message();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (Objects.isNull(value)) {
            log.info("获取入参对象失败NPE: 线程名称[{}] 线程id[{}]",Thread.currentThread().getName(),Thread.currentThread().getId());
            throw new BusinessException(ExceptionLevel.HIGH, HttpStatus.BAD_REQUEST.value(),HttpStatus.BAD_REQUEST.getReasonPhrase());
        }
        return FieldRepeatValidatorUtil.fieldRepeat(id, field, value, message,context);
    }
}

定义重复数据校验工具类

/**
 * @author changyuan
 * @date 2022/12/27
 * @description
 */
@Slf4j
public class FieldRepeatValidatorUtil {
    /**
     * 实体类中id字段
     */
    private static String id;
    /**
     * 需要校验的字段名称
     */
    private static String field;

    /**
     * 需要校验的字段对应的值
     */
    private static String fieldValue = "fieldValue";

    /**
     * 根据 field 查询到的数据库中的值
     */
    private static Object dbFieldValue;

    /**
     * 需要校验的字段对应的数据库表中字段名称
     */
    private static String dbFieldName;

    /**
     * 应用标识
     */
    private static final String IDENTIFY = "identify";

    /**
     * 应用标识值
     */
    private static Object identifyValue = "identifyValue";

    /**
     * 数据库中应用标识字段名称
     */
    private static String dbIdentifyName = "dbIdentifyName";

    /**
     * 学校 id
     */
    private static final String CUSTOMER_ID = "customerId";

    /**
     * 学校 id 值
     */
    private static String customerIdValue = "customerIdValue";

    /**
     * 数据库学校 id 对应字段名称
     */
    private static String dbCustomerIdName = "dbCustomerIdName";

    /**
     * 校区id
     */
    private static final String CAMPUS_ID = "campusId";


    /**
     * 数据库校区 id 对应字段名称
     */
    private static String dbCampusIdName = "dbCampusIdName";

    /**
     * 业务类型(该逻辑为多个业务共用同一张表时,通过类型区分)
     */
    private static final String TYPE = "type";

    /**
     * 数据类型对应字段名称
     */
    private static String dbTypeName = "dbTypeName";

    /**
     * 实体类对象值
     */
    private static String object = "object";


    /**
     *
     * @param id
     * @param field
     * @param obj
     * @param message
     * @return
     */
    public static boolean fieldRepeat(String id, String field, Object obj, String message, ConstraintValidatorContext context) {
        FieldRepeatValidatorUtil.id = id;
        FieldRepeatValidatorUtil.field = field;
        FieldRepeatValidatorUtil.dbFieldName = field;
        ThreadLocalUtil.set(FieldRepeatValidatorUtil.object,obj);
        log.info("行号: [{}]入参对象: [{}] 线程名称[{}] 线程id[{}]",Thread.currentThread().getStackTrace()[1].getLineNumber(),
                ThreadLocalUtil.get(FieldRepeatValidatorUtil.object),Thread.currentThread().getName(),Thread.currentThread().getId());
        getFieldValue();
        CommonDomain baseEntity = (CommonDomain)obj;
        QueryWrapper wrapper = new QueryWrapper<>();
        if (Objects.nonNull(ThreadLocalUtil.get(CAMPUS_ID))) {
            wrapper.eq(ThreadLocalUtil.get(dbCampusIdName).toString(),ThreadLocalUtil.get(CAMPUS_ID));
        }
        if (Objects.nonNull(ThreadLocalUtil.get(TYPE)) && ReflectUtil.hasField(ThreadLocalUtil.get(FieldRepeatValidatorUtil.object).getClass(),TYPE)) {
            wrapper.eq(ThreadLocalUtil.get(dbTypeName).toString(),ThreadLocalUtil.get(TYPE));
        }
        List list = baseEntity.selectList(wrapper.eq(ThreadLocalUtil.get(dbFieldName).toString(), ThreadLocalUtil.get(fieldValue))
                .eq(ThreadLocalUtil.get(dbIdentifyName).toString(), ThreadLocalUtil.get(IDENTIFY)).eq(ThreadLocalUtil.get(dbCustomerIdName).toString(), ThreadLocalUtil.get(CUSTOMER_ID)));
        String idColumnValue = Objects.isNull(ThreadLocalUtil.get(FieldRepeatValidatorUtil.id))? null:ThreadLocalUtil.get(FieldRepeatValidatorUtil.id).toString();
        // 新增校验
        log.info("重复数据校验反射获取的id值为: idColumnValue= {}",idColumnValue);
        if (StringUtils.isBlank(idColumnValue)) {
            log.info("新增时入参对象: [{}]",ThreadLocalUtil.get(FieldRepeatValidatorUtil.object));
            if (CollectionUtils.isNotEmpty(list)) {
                log.error("新增时字段[{}]出现重复数据: {}",field,message);
                // getDefaultConstraintMessageTemplate
                log.info("ConstraintValidatorContext DefaultConstraintMessageTemplate : [{}]",context.getDefaultConstraintMessageTemplate());
                //禁用默认约束信息
                context.disableDefaultConstraintViolation();
                message = joinMessage(message);
                context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
                return false;
            }
        }else {
            log.info("编辑时入参对象: [{}]",object);
            // 编辑校验
            if (CollectionUtils.isNotEmpty(list)) {
                // 前端输入的值
                Object fieldValueNew = ThreadLocalUtil.get(fieldValue);
                ThreadLocalUtil.set(FieldRepeatValidatorUtil.object,baseEntity.selectById(idColumnValue));
                getFieldValue();
                // 数据库中可能已经存在的重复的值
                dbFieldValue = ReflectUtil.getFieldValue(ThreadLocalUtil.get(FieldRepeatValidatorUtil.object),field);
                if (list.size() > 1 ||
                        (!fieldValueNew.equals(dbFieldValue) && fieldValueNew.equals(ReflectUtil.getFieldValue(list.get(0),field)))) {
                    log.error("编辑时字段[{}]出现重复数据: {}",field,message);
                    context.disableDefaultConstraintViolation();
                    message = joinMessage(message);
                    context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
                    return false;
                }
            }
        }
        ThreadLocalUtil.remove();
        return true;
    }

    private static void getFieldValue() {
        // TODO 待优化
        // 获取所有的字段
        log.info("行号: [{}]入参对象: [{}] 线程名称[{}] 线程id[{}]",Thread.currentThread().getStackTrace()[1].getLineNumber(),
                ThreadLocalUtil.get(FieldRepeatValidatorUtil.object),Thread.currentThread().getName(),Thread.currentThread().getId());
        Field[] fields = ReflectUtil.getFields(ThreadLocalUtil.get(FieldRepeatValidatorUtil.object).getClass());
        for (Field f : fields) {
            // 设置对象中成员属性 private 为可读
            ReflectionUtils.makeAccessible(f);
            setIdentifyContext(f);
            setCustomerIdContext(f);
            setCampusIdContext(f);
            setTypeContext(f);
            // 如果存在则获取该注解对应的字段,并判断是否与我们要校验的字段一致
            setFieldContext(f);
            // 获取id值 -> 作用:判断是插入还是更新操作
            setIdContext(f);

        }
    }


    /**
     * 设置具体校验重复的字段内容
     * @param f
     */
    private static void setFieldContext(Field f) {
        if (f.getName().equals(FieldRepeatValidatorUtil.field)) {
            //如果一致则获取其属性值
            ThreadLocalUtil.set(fieldValue,ReflectionUtils.getField(f,ThreadLocalUtil.get(FieldRepeatValidatorUtil.object)));
            //获取该校验字段对应的数据库字段属性  目的: 给 mybatis-plus 做 ar 查询使用
            TableField annotation = f.getAnnotation(TableField.class);
            ThreadLocalUtil.set(dbFieldName,annotation.value());
        }
    }


    /**
     * 设置主键 id 内容,作用:判断是插入还是更新操作
     * @param f
     */
    private static void setIdContext(Field f) {
        if (id.equals(f.getName())) {
            Object tempIdColumnValue = ReflectionUtils.getField(f,ThreadLocalUtil.get(FieldRepeatValidatorUtil.object));
            log.info("重复数据校验反射获取的id值为: tempIdColumnValue=[{}]",tempIdColumnValue);
            if (Objects.nonNull(tempIdColumnValue)) {
                String idColumnValue = String.valueOf(tempIdColumnValue);
                ThreadLocalUtil.set(FieldRepeatValidatorUtil.id,idColumnValue);
                log.info("入参实体存在id值: [{}]",idColumnValue);
            }
        }
    }


    /**
     * 设置部分对象中包含的 type 字段内容
     * @param f
     */
    private static void setTypeContext(Field f) {
        if (TYPE.equals(f.getName())) {
            ThreadLocalUtil.set(TYPE,ReflectionUtils.getField(f,ThreadLocalUtil.get(FieldRepeatValidatorUtil.object)));
            TableField annotation = f.getAnnotation(TableField.class);
            ThreadLocalUtil.set(dbTypeName,annotation.value());
        }
    }

    /**
     * 设置校区 id 数据(公共数据)
     * @param f
     */
    private static void setCampusIdContext(Field f) {
        if (CAMPUS_ID.equals(f.getName())) {
            ThreadLocalUtil.set(CAMPUS_ID,ReflectionUtils.getField(f,ThreadLocalUtil.get(FieldRepeatValidatorUtil.object)));
            TableField annotation = f.getAnnotation(TableField.class);
            ThreadLocalUtil.set(dbCampusIdName,annotation.value());
        }
    }

    /**
     * 设置学校 id 数据(公共数据)
     * @param f
     */
    private static void setCustomerIdContext(Field f) {
        if (CUSTOMER_ID.equals(f.getName())) {
            ThreadLocalUtil.set(CUSTOMER_ID,ReflectionUtils.getField(f,ThreadLocalUtil.get(FieldRepeatValidatorUtil.object)));
            TableField annotation = f.getAnnotation(TableField.class);
            ThreadLocalUtil.set(dbCustomerIdName,annotation.value());

        }
    }

    /**
     * 设置应用标识数据(公共数据)
     * @param f
     */
    private static void setIdentifyContext(Field f){
        if (IDENTIFY.equals(f.getName())) {
            ThreadLocalUtil.set(IDENTIFY,ReflectionUtils.getField(f,ThreadLocalUtil.get(FieldRepeatValidatorUtil.object)));
            TableField annotation = f.getAnnotation(TableField.class);
            ThreadLocalUtil.set(dbIdentifyName,annotation.value());
        }
    }
  
     /**
     * 拼接提示信息
     * @param message
     */
    private static String joinMessage(String message) {
        if (AnnotationUtil.hasAnnotation(ThreadLocalUtil.get(FieldRepeatValidatorUtil.object).getClass(), FieldName.class)) {
            FieldName annotation = AnnotationUtil.getAnnotation(ThreadLocalUtil.get(FieldRepeatValidatorUtil.object).getClass(),
                    FieldName.class);

            message = annotation.name()+message;
        }
        return message;
    }
}

定义本地线程工具类

该类的作用为区分每个线程的操作,防止多人同时操作同一业务时,数据未隔离而导致NPE的问题

将该注解添加在实体上

/**
 * @author changyuan
 * @date 2022/12/20
 * @description xxx 实体类
 */
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("test_user")
@FieldRepeatValidator(field = "userName",message = "名称已存在")
public class TestUser extends CommonDomain implements Serializable {
    private static final long serialVersionUID = 6184348182699749119L;
    
    private String userName;
  }

持久层添加校验注解

@Validated
public class TestUserRepository {
 @Resource
 private TestUserMapper testUserMapper;

 public Long saveData(@Valid TestUser testUser) {
        testUserMapper.insert(testUser);
        return testUser.getId();
    }
}

总结

当时网上找了很多方式,大部分都是 Controller 层入参就是PO对象,但我们系统对领域模型中的实体类进行了划分,不太适合我们,所以就自己实现了。

- END -

你可能感兴趣的:(java,spring,jvm)