Java 自定义注解 校验指定字段对应数据库内容重复

一、前言

在项目中,某些情景下我们需要验证编码是否重复,账号是否重复,身份证号是否重复等...
而像验证这类代码如下:
在这里插入图片描述
那么有没有办法可以解决这类似的重复代码量呢?

我们可以通过自定义注解校验的方式去实现,如下 在实体类上面加上自定义的注解 @FieldRepeatValidator(field = "resources", message = "菜单编码重复!") 即可
在这里插入图片描述
下面就先来上代码吧 ~

二、实现

基本环境:
  1. javax.validation.validation-api
  2. org.hibernate.hibernate-validator

在 SpringBoot 环境中已经自动包含在spring-boot-starter-web中了,如果因为版本导致没有,可去 maven 仓库搜索手动引入到项目中使用

小编的 springboot 版本为: 2.1.7

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
[ 注:小编是基于MyBatis-Plus的架构下实现的,其他架构略不同,本文实现方式可做参考 ]

1、自定义注解 @FieldRepeatValidator

// 元注解: 给其他普通的标签进行解释说明 【@Retention、@Documented、@Target、@Inherited、@Repeatable】
@Documented
/**
 * 指明生命周期:
 *      RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
 *      RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
 *      RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
 */
@Retention(RetentionPolicy.RUNTIME)
/**
 * 指定注解运用的地方:
 *      ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
 *      ElementType.CONSTRUCTOR 可以给构造方法进行注解
 *      ElementType.FIELD 可以给属性进行注解
 *      ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
 *      ElementType.METHOD 可以给方法进行注解
 *      ElementType.PACKAGE 可以给一个包进行注解
 *      ElementType.PARAMETER 可以给一个方法内的参数进行注解
 *      ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
 */
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE})
@Constraint(validatedBy = FieldRepeatValidatorClass.class)
//@Repeatable(LinkVals.class)( 可重复注解同一字段,或者类,java1.8 后支持)
public @interface FieldRepeatValidator {
<span class="hljs-comment">/**
 * 实体类id字段 - 默认为id (该值可无)
 * <span class="hljs-doctag">@return</span>
 */</span>
String <span class="hljs-title function_">id</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-string">"id"</span>;;

<span class="hljs-comment">/**
 * 注解属性 - 对应校验字段
 * <span class="hljs-doctag">@return</span>
 */</span>
String <span class="hljs-title function_">field</span><span class="hljs-params">()</span>;

<span class="hljs-comment">/**
 * 默认错误提示信息
 * <span class="hljs-doctag">@return</span>
 */</span>
String <span class="hljs-title function_">message</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-string">"字段内容重复!"</span>;

Class&lt;?&gt;[] groups() <span class="hljs-keyword">default</span> {};
Class&lt;? <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Payload</span>&gt;[]  payload() <span class="hljs-keyword">default</span> {};

}

2、@FieldRepeatValidator注解接口实现类

/**
 *  <p> FieldRepeatValidator 注解接口实现类 </p>
 *
 * @description :
 *        技巧 01:必须实现 ConstraintValidator 接口
 *     技巧 02:实现了 ConstraintValidator 接口后即使不进行 Bean 配置,spring 也会将这个类进行 Bean 管理
 *     技巧 03:可以在实现了 ConstraintValidator 接口的类中依赖注入其它 Bean
 *     技巧 04:实现了 ConstraintValidator 接口后必须重写 initialize 和 isValid 这两个方法;
 *              initialize 方法主要来进行初始化,通常用来获取自定义注解的属性值;
 *              isValid 方法主要进行校验逻辑,返回 true 表示校验通过,返回 false 表示校验失败,通常根据注解属性值和实体类属性值进行校验判断 [Object: 校验字段的属性值]
 * @author : zhengqing
 * @date : 2019/9/10 9:22
 */
public class FieldRepeatValidatorClass implements ConstraintValidator<FieldRepeatValidator, Object> {
<span class="hljs-keyword">private</span> String id;
<span class="hljs-keyword">private</span> String field;
<span class="hljs-keyword">private</span> String message;

<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">initialize</span><span class="hljs-params">(FieldRepeatValidator fieldRepeatValidator)</span> {
    <span class="hljs-built_in">this</span>.id = fieldRepeatValidator.id();
    <span class="hljs-built_in">this</span>.field = fieldRepeatValidator.field();
    <span class="hljs-built_in">this</span>.message = fieldRepeatValidator.message();
}

<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isValid</span><span class="hljs-params">(Object o, ConstraintValidatorContext constraintValidatorContext)</span> {
    <span class="hljs-keyword">return</span> FieldRepeatValidatorUtils.fieldRepeat(id, field, o, message);
}

}

3、数据库字段内容重复判断处理工具类

public class FieldRepeatValidatorUtils {
<span class="hljs-comment">/**
 * 实体类id字段
 */</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> String id;
<span class="hljs-comment">/**
 * 实体类id字段值
 */</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> Integer idValue;
<span class="hljs-comment">/**
 * 校验字段
 */</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> String field;
<span class="hljs-comment">/**
 * 校验字段值 - 字符串、数字、对象...
 */</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> Object fieldValue;
<span class="hljs-comment">/**
 * 校验字段 - 对应数据库字段
 */</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> String db_field;
<span class="hljs-comment">/**
 * 实体类对象值
 */</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> Object object;

<span class="hljs-comment">/**
 * 校验数据 TODO 后期如果需要校验同个字段是否重复的话,将 `field` 做 , 或 - 分割... ;  如果id不唯一考虑传值过来判断 或 取fields第二个字段值拿id
 *
 * <span class="hljs-doctag">@param</span> field:校验字段
 * <span class="hljs-doctag">@param</span> object:对象数据
 * <span class="hljs-doctag">@param</span> message:回调到前端提示消息
 * <span class="hljs-doctag">@return</span>: boolean
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">fieldRepeat</span><span class="hljs-params">(String id, String field, Object object, String message)</span> {
    <span class="hljs-comment">// 使用Class类的中静态forName()方法获得与字符串对应的Class对象 ; className: 必须是接口或者类的名字</span>
    <span class="hljs-comment">// 静态方法forName()调用 启动类加载器 -&gt; 加载某个类xx -&gt; 实例化 ----&gt; 从而达到降耦 更灵活</span>

// Object object = Class.forName(className).newInstance();

    FieldRepeatValidatorUtils.id = id;
    FieldRepeatValidatorUtils.field = field;
    FieldRepeatValidatorUtils.object = object;
    getFieldValue();

    <span class="hljs-comment">// ⑦ 校验字段内容是否重复</span>
    <span class="hljs-comment">// 工厂模式 + ar动态语法</span>
    <span class="hljs-type">BaseEntity</span> <span class="hljs-variable">entity</span> <span class="hljs-operator">=</span> (BaseEntity) object;

// List list = entity.selectPage(new Page<>( 1,1), new EntityWrapper().eq( field, fieldValue) ).getRecords();
List list = entity.selectList( new EntityWrapper().eq( db_field, fieldValue) );
// 如果数据重复返回 false -> 再返回自定义错误消息到前端
if ( idValue == null ){
if (!CollectionUtils.isEmpty( list) ){
throw new MyException(message);
}
} else {
if (!CollectionUtils.isEmpty( list) ){
// fieldValueNew:前端输入字段值
Object fieldValueNew = fieldValue;
FieldRepeatValidatorUtils.object = entity.selectById(idValue);
// 获取该 id 所在对象的校验字段值 - 旧数据
getFieldValue();
if (!fieldValueNew.equals( fieldValue) || list.size() > 1 ){
throw new MyException(message);
}
}
}
return true;
}

<span class="hljs-comment">/**
 * 获取id、校验字段值
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">getFieldValue</span><span class="hljs-params">()</span>{
    <span class="hljs-comment">// ① 获取所有的字段</span>
    Field[] fields = object.getClass().getDeclaredFields();
    <span class="hljs-keyword">for</span> (Field f : fields) {
        <span class="hljs-comment">// ② 设置对象中成员 属性private为可读</span>
        f.setAccessible(<span class="hljs-literal">true</span>);
        <span class="hljs-comment">// ③ 判断字段注解是否存在</span>
        <span class="hljs-keyword">if</span> ( f.isAnnotationPresent(ApiModelProperty.class) ) {
            <span class="hljs-comment">// ④ 如果存在则获取该注解对应的字段,并判断是否与我们要校验的字段一致</span>
            <span class="hljs-keyword">if</span> ( f.getName().equals( field ) ){
                <span class="hljs-keyword">try</span> {
                    <span class="hljs-comment">// ⑤ 如果一致则获取其属性值</span>
                    fieldValue = f.get(object);
                    <span class="hljs-comment">// ⑥ 获取该校验字段对应的数据库字段属性  目的: 给 mybatis-plus 做ar查询使用</span>
                    <span class="hljs-type">TableField</span> <span class="hljs-variable">annotation</span> <span class="hljs-operator">=</span> f.getAnnotation(TableField.class);
                    db_field = annotation.value();
                } <span class="hljs-keyword">catch</span> (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            <span class="hljs-comment">// ⑦ 获取id值 -&gt; 作用:判断是插入还是更新操作</span>
            <span class="hljs-keyword">if</span> ( id.equals( f.getName() ) ){
                <span class="hljs-keyword">try</span> {
                    idValue = (Integer) f.get(object);
                } <span class="hljs-keyword">catch</span> (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

}

4、全局异常处理

作用:让校验生效,即参数校验时如果不合法就会抛出异常,我们就可以在全局异常中捕获拦截到,然后进行逻辑处理之后再返回给前端
@Slf4j
@RestControllerAdvice
public class MyGlobalExceptionHandler {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Logger</span> <span class="hljs-variable">LOG</span> <span class="hljs-operator">=</span> LoggerFactory.getLogger(MyGlobalExceptionHandler.class);

<span class="hljs-comment">/**
 * 自定义异常处理
 */</span>
<span class="hljs-meta">@ExceptionHandler(value = MyException.class)</span>
<span class="hljs-keyword">public</span> ApiResult <span class="hljs-title function_">myException</span><span class="hljs-params">(MyException be)</span> {
    log.error(<span class="hljs-string">"自定义异常:"</span>, be);
    <span class="hljs-keyword">if</span>(be.getCode() != <span class="hljs-literal">null</span>){
        <span class="hljs-keyword">return</span> ApiResult.fail(be.getCode(), be.getMessage());
    }
    <span class="hljs-keyword">return</span> ApiResult.fail( be.getMessage() );
}

<span class="hljs-comment">// 参数校验异常处理 ===========================================================================</span>
<span class="hljs-comment">// MethodArgumentNotValidException是springBoot中进行绑定参数校验时的异常,需要在springBoot中处理,其他需要处理ConstraintViolationException异常进行处理.</span>

<span class="hljs-comment">/**
 * 方法参数校验
 */</span>
<span class="hljs-meta">@ExceptionHandler(MethodArgumentNotValidException.class)</span>
<span class="hljs-keyword">public</span> ApiResult <span class="hljs-title function_">handleMethodArgumentNotValidException</span><span class="hljs-params">( MethodArgumentNotValidException e )</span> {
    log.error( <span class="hljs-string">"方法参数校验:"</span> + e.getMessage(), e );
    <span class="hljs-keyword">return</span> ApiResult.fail( e.getBindingResult().getFieldError().getDefaultMessage() );
}

<span class="hljs-comment">/**
 * ValidationException
 */</span>
<span class="hljs-meta">@ExceptionHandler(ValidationException.class)</span>
<span class="hljs-keyword">public</span> ApiResult <span class="hljs-title function_">handleValidationException</span><span class="hljs-params">(ValidationException e)</span> {
    log.error( <span class="hljs-string">"ValidationException:"</span>, e );
    <span class="hljs-keyword">return</span> ApiResult.fail( e.getCause().getMessage() );
}

<span class="hljs-comment">/**
 * ConstraintViolationException
 */</span>
<span class="hljs-meta">@ExceptionHandler(ConstraintViolationException.class)</span>
<span class="hljs-keyword">public</span> ApiResult <span class="hljs-title function_">handleConstraintViolationException</span><span class="hljs-params">(ConstraintViolationException e)</span> {
    log.error( <span class="hljs-string">"ValidationException:"</span> + e.getMessage(), e );
    <span class="hljs-keyword">return</span> ApiResult.fail( e.getMessage() );
}

}

其中自定义异常处理代码如下:

public class MyException extends RuntimeException {
<span class="hljs-comment">/**
 * 异常状态码
 */</span>
<span class="hljs-keyword">private</span> Integer code;

<span class="hljs-keyword">public</span> <span class="hljs-title function_">MyException</span><span class="hljs-params">(Throwable cause)</span> {
    <span class="hljs-built_in">super</span>(cause);
}

<span class="hljs-keyword">public</span> <span class="hljs-title function_">MyException</span><span class="hljs-params">(String message)</span> {
    <span class="hljs-built_in">super</span>(message);
}

<span class="hljs-keyword">public</span> <span class="hljs-title function_">MyException</span><span class="hljs-params">(Integer code, String message)</span> {
    <span class="hljs-built_in">super</span>(message);
    <span class="hljs-built_in">this</span>.code = code;
}

<span class="hljs-keyword">public</span> <span class="hljs-title function_">MyException</span><span class="hljs-params">(String message, Throwable cause)</span> {
    <span class="hljs-built_in">super</span>(message, cause);
}

<span class="hljs-keyword">public</span> Integer <span class="hljs-title function_">getCode</span><span class="hljs-params">()</span> {
    <span class="hljs-keyword">return</span> code;
}

}

三、@FieldRepeatValidator注解使用举例

1、在实体类上加上如下代码

@FieldRepeatValidator(field = "resources", message = "菜单编码重复!")
public class Menu extends BaseEntity { ... }

2、在 controller 层的方法中加上@Validated 注解即可!

@PostMapping(value = "/save", produces = "application/json;charset=utf-8")
    @ApiOperation(value = "保存菜单", httpMethod = "POST", response = ApiResult.class)
    public ApiResult save(@RequestBody @Validated Menu input) {
        Integer id = menuService.save(input);
        // 更新权限
        shiroService.updatePermission(shiroFilterFactoryBean, null, false);
        return ApiResult.ok("保存菜单成功", id);
}

四、一些可直接使用的原生注解

下面的这些原生注解 百度一下,就会发现发现有很多,很简单就不多说了

@Null	必须为null
@NotNull	必须不为 null
@AssertTrue	必须为 true ,支持boolean、Boolean
@AssertFalse	必须为 false ,支持boolean、Boolean
@Min(value)	值必须小于value,支持BigDecimal、BigInteger,byte、shot、int、long及其包装类
@Max(value)	值必须大于value,支持BigDecimal、BigInteger,byte、shot、int、long及其包装类
@DecimalMin(value)	值必须小于value,支持BigDecimal、BigInteger、CharSequence,byte、shot、int、long及其包装类
@DecimalMax(value)	值必须大于value,支持BigDecimal、BigInteger、CharSequence,byte、shot、int、long及其包装类
@Size(max=, min=)	支持CharSequence、Collection、Map、Array
@Digits (integer, fraction)	必须是一个数字
@Negative	必须是一个负数
@NegativeOrZero	必须是一个负数或0
@Positive	必须是一个正数
@PositiveOrZero	必须是个正数或0
@Past	必须是一个过去的日期
@PastOrPresent	必须是一个过去的或当前的日期
@Future	必须是一个将来的日期
@FutureOrPresent	必须是一个未来的或当前的日期
@Pattern(regex=,flag=)	必须符合指定的正则表达式
@NotBlank(message =)	必须是一个非空字符串
@Email	必须是电子邮箱地址
@NotEmpty	被注释的字符串的必须非空
... ... ... 

五、总结

这里简单说下小编的实现思路吧
首先我们自定义一个注解,放在字段或者上,目的:通过反射获取其值,然后拿到值我们就可以进行一系列自己的业务操作了,比如更具字段属性和属性值查询到相应的数据库数据,然后进行校验,如果不符合自己的逻辑,我们就抛出一个异常交给全局统一异常类处理错误信息,最后返回给前端做处理,大体思路就是这样,实现起来很简单,代码中该有的注释都有,相信不会太难理解


最后再给出小编的源码让大家作参考吧

案例 Demo