写在开头
你是否经常做CUrd的时候需要检查字段是否已经存在数据库。创建/更新会员的时候,要分别检查会员名称、电话号码、邮箱...是否已经存在数据库。这是很简单的需求,花了三分钟写了出来。
完成需求的你得意洋洋,看二次元摸鱼的时候,组长又丢一个新需求给你,要求分别检查会员的昵称、第二昵称、第三昵称...第一百昵称唯一性。你大道委屈,“一百个字段啊,得写到什么时候“!旁边的眼镜轻轻推了一下眼镜,向你丢出救命稻草--@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
}
复制代码
写在后面
理科生不善言辞,唯有祝大家元旦快乐。