基于注解Uni检验字段是否重复

写在开头

你是否经常做CUrd的时候需要检查字段是否已经存在数据库。创建/更新会员的时候,要分别检查会员名称、电话号码、邮箱...是否已经存在数据库。这是很简单的需求,花了三分钟写了出来。

image.png 完成需求的你得意洋洋,看二次元摸鱼的时候,组长又丢一个新需求给你,要求分别检查会员的昵称、第二昵称、第三昵称...第一百昵称唯一性。你大道委屈,“一百个字段啊,得写到什么时候“!旁边的眼镜轻轻推了一下眼镜,向你丢出救命稻草--@Uni

思路

不废话,先说思路。 使用自定义注解@Uni、hibernate-validator和基于mybatis-plus动态拼接sql实现。只需要在类上标注@Uni注解,表明检测的字段即可。下面是实现代码。

自定义注解

@Documented
@Constraint(
        validatedBy = UniqueValidator.class
)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Unique.List.class)
public @interface Unique {

    // 检验类的名字
    String name();

    // 需要校验的字段
    String[] value();

    // 字段描述(默认从swagger的ApiModelProperty中获取)
    String[] desc() default {};

    
    // mapper数据库唯一性校验时虚使用到的mapper,默认会自动去查找mapper
    Class<? extends BaseMapper> mapper() default BaseMapper.class;

    // 实体类,用于读取table属性
    Class<? extends AbstractBasePo> clazz() default AbstractBasePo.class;

    String message() default "{desc}--->{values}已存在";

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

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

    @Documented
    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface List {
        Unique[] value();
    }
}
复制代码

基于hibernate validator ConstraintValidator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>${spring-boot-starter-validation}</version>
</dependency>
复制代码

ConstraintValidator是hibernate提供的用于字段validator,继承实现initialize()和isValid()即可自己控制检验规则,其实用较简单,具体用法这里不细说。

public abstract class AbstractConstraintValidator<A extends Annotation, T> implements ConstraintValidator<A, T>, ValidatorHandler<A, T> {

    /**
     * 校验注解
     */
    protected A annotation;


    @Override
    public void initialize(A annotation) {
        this.annotation = annotation;
        this.validateParameters();
    }

    @Override
    public boolean isValid(T t, ConstraintValidatorContext context) {
        if(Objects.isNull(t)) {
            return doValidNull(context);
        }
        doInitialize(annotation, t);
        return doValid(t, context);
    }

    /**
     * 空值验证
     * @author liuhr
     * @date 2021/6/5 13:44
     * @param context
     * @return
     */
    protected boolean doValidNull(ConstraintValidatorContext context) {
        return true;
    }

    /**
     * 注解参数验证
     * @author liuhr
     * @date 2021/6/5 13:44
     * @return
     */
    protected abstract void validateParameters();

}
复制代码

创建ValidatorHandler接口,由handler检测字段

public interface ValidatorHandler<A extends Annotation, T> {

    boolean doInitialize(A annotation, T obj);

    boolean doValid(T obj, ConstraintValidatorContext context);
}
复制代码

创建UniqueValidator实现类AbstractConstraintValidator

@Slf4j
public class UniqueValidator extends AbstractConstraintValidator<Unique, Object> {
    private ValidatorHandler validatorHandler;

    @Override
    protected void validateParameters() {
        Assert.notEmpty(annotation.value(), "验证字段不能为空");
        Assert.isTrue(ArrayUtils.isEmpty(annotation.desc()) || annotation.desc().length == annotation.value().length, "desc与value长度不一致");
    }

    // ApplicationUtil是获取bean的工具类,实现简单,具体可百度
    @Override
    public boolean doInitialize(Unique annotation, Object obj) {
        // 需要注意,此处基于DCL加锁,具体原因结合后文说明
        if (null == (validatorHandler = ApplicationUtil.getBean(CacheService.class).get(CacheName.VALIDATOR, annotation.name(), ValidatorHandler.class))) {
            synchronized (annotation.clazz()) {
                if (null == (validatorHandler = ApplicationUtil.getBean(CacheService.class).get(CacheName.VALIDATOR, annotation.name(), ValidatorHandler.class))) {
                    log.info("初始化校验器:[annotation: {}]", annotation.clazz());
                    // UniqueValidatorHandlerFactory获取具体点handler,具体实现后文提供
                    validatorHandler = UniqueValidatorHandlerFactory.getHandler(obj.getClass());
                    validatorHandler.doInitialize(annotation, obj);
                    ApplicationUtil.getBean(CacheService.class).put(CacheName.VALIDATOR, annotation.name(), validatorHandler);
                }
            }
        }
        return true;
    }

    @Override
    public boolean doValid(Object obj, ConstraintValidatorContext context) {
        return validatorHandler.doValid(obj, context);
    }
}
复制代码

实际执行检测逻辑的EntityUniqueValidatorHandler

@Slf4j
public class EntityUniqueValidatorHandler implements ValidatorHandler<Unique, Object> {

    // 表主键ID field
    private Field keyField;

    // 表主键ID columnName
    private String keyColumn;

    // 验证字段映射 columnName ---> field
    private Map<String, Field> validColumnFieldMap;

    // mapper
    private BaseMapper mapper;

    @Override
    public boolean doInitialize(Unique unique, Object obj) {
        log.info("entity unique validator init......");
        Class<?> clazz = unique.clazz();
        TableInfo tableInfo = SqlHelper.table(clazz);
        keyField = ReflectUtil.getField(clazz, tableInfo.getKeyProperty());
        keyColumn = tableInfo.getKeyColumn();
        mapper = MyBatisPlusUtil.getMapper(tableInfo.getConfiguration().getMapperRegistry(), unique.mapper(), clazz);

        Map<String, Field> fieldMap = ReflectUtil.getFieldMap(clazz);
        List<Field> fieldList = Arrays.stream(unique.value()).map(fieldMap::get).collect(Collectors.toList());
        validColumnFieldMap = MyBatisPlusUtil.getColumnFieldMap(fieldList, tableInfo.getFieldList());
        return true;
    }

    @Override
    public boolean doValid(Object obj, ConstraintValidatorContext context) {
        Object id = ReflectUtil.getFieldValue(obj, keyField.getName());
        boolean isNotEmptyKey = !StringUtils.isEmpty(id);

        for (Map.Entry<String, Field> entry : validColumnFieldMap.entrySet()) {
            Object value = ReflectUtil.getFieldValue(obj, entry.getValue().getName());
            boolean isEmpty = StringUtils.isEmpty(value);
            // 拼接字段sql,只要一个字段唯一性冲突返回false
            QueryWrapper<Object> wrapper = Wrappers.query()
                    .ne(isNotEmptyKey, keyColumn, id)
                    .isNotNull(isEmpty, entry.getKey())
                    .eq(!isEmpty, entry.getKey(), String.valueOf(value));
            if (mapper.selectCount(wrapper) > 0) {
                context.unwrap(HibernateConstraintValidatorContext.class)
                        .addMessageParameter(UniqueConstant.DESC, entry.getValue().getAnnotation(ApiModelProperty.class).value())
                        .addMessageParameter(UniqueConstant.VALUES, ObjectWrapper.getInstance(obj, entry.getValue()));
                return false;
            }
        }
        return true;
    }
}
复制代码

doInitialize思想是把需要检查的字段封装,以便后续检查性能加快。对于无状态的bean来说,初始化方法只执行一次就好了,既同一个类在第一次检测的时候只初始化一次。这句话可能比较难理解,白话文就是每次请求到来都会创建一个新的ConstraintValidator对象,导致每次请求都需走一次doInitialize,于是就出现了上文的DCL加锁,把具体的handler放到缓存里,每次请求进来就拿对应的handler,就不需要进入doInitialize方法。

使用

在实体类加上,name是用于缓存的key,value是需要检查的字段,clazz是实体,因为我用的是ddd,需要这个属性。如果你把注解加到实体类,就不需要这个属性。这样的做法,以后就算检查一百个字段唯一性,只需要加上注解就好了。

@Unique(name = "account", value = {"username", "phoneNum"}, clazz = AccountPo.class)
复制代码

触发检查

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    javax.validation.Validator validator = factory.getValidator();
    // entity需要检查的对象
    Set<ConstraintViolation<Object>> constraintViolations = validator.validate(entity);
    if (!constraintViolations.isEmpty()) {
        ValidatorUtil.isTrue(true, constraintViolations.stream().findFirst().get().getMessage());
复制代码

工具类补充

@Component
public class ApplicationUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    public static ListableBeanFactory getApplicationContext() {
        return applicationContext;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public static <T> T getBean(String name) {
        return (T) applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public static boolean containsBean(String name) {
        return applicationContext.containsBean(name);
    }
}
复制代码
public class UniqueValidatorHandlerFactory {

    public static void assertValidClass(Class<?> clazz) {
        Assert.isFalse(clazz.isEnum(), "不支持枚举类型");
        Assert.isFalse(CharSequence.class.isAssignableFrom(clazz), "不支持字符类型");
        Assert.isFalse(ClassUtils.isPrimitiveOrWrapper(clazz), "不支持基本类型及其包装类型");
        Assert.isFalse(Map.class.isAssignableFrom(clazz), "暂不支持Map类型");
    }

    public static ValidatorHandler<Unique, ?> getHandler(Class<?> clazz) {
        assertValidClass(clazz);
        return new EntityUniqueValidatorHandler();
    }

}
复制代码

使用效果

{
    "code": 0,
    "msg": "用户名--->[1001]已存在",
    "data": null
}
复制代码

写在后面

理科生不善言辞,唯有祝大家元旦快乐。

Supongo que te gusta

Origin juejin.im/post/7048658743197696008
Recomendado
Clasificación