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