一、前言
在項目中,某些情景下我們需要驗證編碼是否重復,賬號是否重復,身份證號是否重復等...
而像驗證這類代碼如下:

那么有沒有辦法可以解決這類似的重復代碼量呢?
我們可以通過自定義注解校驗的方式去實現,如下 在實體類上面加上自定義的注解 @FieldRepeatValidator(field = "resources", message = "菜單編碼重復!") 即可

下面就先來上代碼吧~
二、實現
基本環境:
- javax.validation.validation-api
- 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 {
/**
* 實體類id字段 - 默認為id (該值可無)
* @return
*/
String id() default "id";;
/**
* 注解屬性 - 對應校驗字段
* @return
*/
String field();
/**
* 默認錯誤提示信息
* @return
*/
String message() default "字段內容重復!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
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> {
private String id;
private String field;
private String message;
@Override
public void initialize(FieldRepeatValidator fieldRepeatValidator) {
this.id = fieldRepeatValidator.id();
this.field = fieldRepeatValidator.field();
this.message = fieldRepeatValidator.message();
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
return FieldRepeatValidatorUtils.fieldRepeat(id, field, o, message);
}
}
3、數據庫字段內容重復判斷處理工具類
public class FieldRepeatValidatorUtils {
/**
* 實體類id字段
*/
private static String id;
/**
* 實體類id字段值
*/
private static Integer idValue;
/**
* 校驗字段
*/
private static String field;
/**
* 校驗字段值 - 字符串、數字、對象...
*/
private static Object fieldValue;
/**
* 校驗字段 - 對應數據庫字段
*/
private static String db_field;
/**
* 實體類對象值
*/
private static Object object;
/**
* 校驗數據 TODO 后期如果需要校驗同個字段是否重復的話,將 `field` 做 , 或 - 分割... ; 如果id不唯一考慮傳值過來判斷 或 取fields第二個字段值拿id
*
* @param field:校驗字段
* @param object:對象數據
* @param message:回調到前端提示消息
* @return: boolean
*/
public static boolean fieldRepeat(String id, String field, Object object, String message) {
// 使用Class類的中靜態forName()方法獲得與字符串對應的Class對象 ; className: 必須是接口或者類的名字
// 靜態方法forName()調用 啟動類加載器 -> 加載某個類xx -> 實例化 ----> 從而達到降耦 更靈活
// Object object = Class.forName(className).newInstance();
FieldRepeatValidatorUtils.id = id;
FieldRepeatValidatorUtils.field = field;
FieldRepeatValidatorUtils.object = object;
getFieldValue();
// ⑦ 校驗字段內容是否重復
// 工廠模式 + ar動態語法
BaseEntity entity = (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;
}
/**
* 獲取id、校驗字段值
*/
public static void getFieldValue(){
// ① 獲取所有的字段
Field[] fields = object.getClass().getDeclaredFields();
for (Field f : fields) {
// ② 設置對象中成員 屬性private為可讀
f.setAccessible(true);
// ③ 判斷字段注解是否存在
if ( f.isAnnotationPresent(ApiModelProperty.class) ) {
// ④ 如果存在則獲取該注解對應的字段,並判斷是否與我們要校驗的字段一致
if ( f.getName().equals( field ) ){
try {
// ⑤ 如果一致則獲取其屬性值
fieldValue = f.get(object);
// ⑥ 獲取該校驗字段對應的數據庫字段屬性 目的: 給 mybatis-plus 做ar查詢使用
TableField annotation = f.getAnnotation(TableField.class);
db_field = annotation.value();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
// ⑦ 獲取id值 -> 作用:判斷是插入還是更新操作
if ( id.equals( f.getName() ) ){
try {
idValue = (Integer) f.get(object);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
}
4、全局異常處理
作用:讓校驗生效,即參數校驗時如果不合法就會拋出異常,我們就可以在全局異常中捕獲攔截到,然后進行邏輯處理之后再返回給前端
@Slf4j
@RestControllerAdvice
public class MyGlobalExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(MyGlobalExceptionHandler.class);
/**
* 自定義異常處理
*/
@ExceptionHandler(value = MyException.class)
public ApiResult myException(MyException be) {
log.error("自定義異常:", be);
if(be.getCode() != null){
return ApiResult.fail(be.getCode(), be.getMessage());
}
return ApiResult.fail( be.getMessage() );
}
// 參數校驗異常處理 ===========================================================================
// MethodArgumentNotValidException是springBoot中進行綁定參數校驗時的異常,需要在springBoot中處理,其他需要處理ConstraintViolationException異常進行處理.
/**
* 方法參數校驗
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult handleMethodArgumentNotValidException( MethodArgumentNotValidException e ) {
log.error( "方法參數校驗:" + e.getMessage(), e );
return ApiResult.fail( e.getBindingResult().getFieldError().getDefaultMessage() );
}
/**
* ValidationException
*/
@ExceptionHandler(ValidationException.class)
public ApiResult handleValidationException(ValidationException e) {
log.error( "ValidationException:", e );
return ApiResult.fail( e.getCause().getMessage() );
}
/**
* ConstraintViolationException
*/
@ExceptionHandler(ConstraintViolationException.class)
public ApiResult handleConstraintViolationException(ConstraintViolationException e) {
log.error( "ValidationException:" + e.getMessage(), e );
return ApiResult.fail( e.getMessage() );
}
}
其中自定義異常處理代碼如下:
public class MyException extends RuntimeException {
/**
* 異常狀態碼
*/
private Integer code;
public MyException(Throwable cause) {
super(cause);
}
public MyException(String message) {
super(message);
}
public MyException(Integer code, String message) {
super(message);
this.code = code;
}
public MyException(String message, Throwable cause) {
super(message, cause);
}
public Integer getCode() {
return 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 被注釋的字符串的必須非空
... ... ...
五、總結
這里簡單說下小編的實現思路吧
首先我們自定義一個注解,放在字段或者類上,目的:通過反射獲取其值,然后拿到值我們就可以進行一系列自己的業務操作了,比如更具字段屬性和屬性值查詢到相應的數據庫數據,然后進行校驗,如果不符合自己的邏輯,我們就拋出一個異常交給全局統一異常類處理錯誤信息,最后返回給前端做處理,大體思路就是這樣,實現起來很簡單,代碼中該有的注釋都有,相信不會太難理解
最后再給出小編的源碼讓大家作參考吧
