原文作者:彌諾R
原文地址:http://www.minuor.com/1524369999/article
轉載聲明:轉載請註明原文地址,注意版權維護,謝謝!
場景小計
之前項目都是使用hibernate-validator來校驗參數,但是實際上會出現一些小問題,就是校驗規則都是通過註解的方式來完成,這樣如果項目上線了,這個參數校驗規則就沒辦法修改,如果出現校驗規則問題,就必須修改後重新緊急上線(之前因爲手機號碼格式校驗就出現過這個問題,因爲新的號段不支持)。爲了適應動態配置校驗規則,在新起的項目我們就不再使用hibernate-validator校驗規則,而是自己寫個小功能來實現。
實現思路
1、實現這種動態配置,就要能隨時修改規則,並應用到實際業務邏輯中,直接在代碼中寫是不行的,因此這裏採用數據庫記錄的方式是一個不錯的選擇;
2、需要對所有controller進入的參數校驗,不能每個方法中加調用邏輯,這個必須寫一個公共的方法,使用Spring AOP做切面切入所有的controller方法;
3、服務的請求方式,使用這種方式,最方便的就是使用post請求,入參後,參數都在一個類中封裝,拿到類,使用反射,拿出參數的參數名和參數值。
基本都是以上思路,切面切入controller類中所有方法,拿到請求Dto類,利用反射技術拿出所有的參數名和參數值,從數據庫中獲取當前Dto類下所有參數的校驗規則,依次對參數進行校驗。
項目構建
項目結構
aspect:切面(DynamicCheckAspect)和校驗引擎(DynamicCheckEngine),切面中反射出字段,查詢校驗規則,然後將字段交給檢驗引擎完成校驗動作;
controller:接口入口,DynamicCheckController提供校驗測試;
dao:dao下有兩個目錄,分別是mapper和model,用於存放Mapper接口類和查詢結果數據封裝類;
dto:請求參數封裝類(DynamicCheckReqDto),響應參數封裝類(DynamicCheckRespDto);
exception:自定義異常類存放位置;
service:業務邏輯代碼;
ApplicationStart:Spring Boot啓動入口;
resource:存放mapper.xml文件和application.properties配置以及日誌配置logback.xml。
數據庫準備
數據庫需要建三張表,校驗模板表(t_template_info),校驗模板規則表(t_template_rule_info),實體規則關聯表(t_bean_rule_info),只說表的基本字段,需要SQL可以到碼雲或者git上現在原代碼,項目中有datasql.sql文件中很詳細,還包含初始數據。
t_template_info:
template_id
varchar(16) NOT NULL COMMENT '模板編號',template_desc
varchar(64) DEFAULT NULL COMMENT '模板描述',template_status
tinyint(4) NOT NULL DEFAULT '1' COMMENT '模板狀態(0:不使用,1:使用)',check_level
int(11) NOT NULL COMMENT '檢查優先級'
t_template_rule_info:
rule_id
varchar(16) NOT NULL COMMENT '規則編號',template_id
varchar(16) NOT NULL COMMENT '模板編號',rule_express
varchar(128) NOT NULL COMMENT '規則表達式',toast_msg
varchar(128) NOT NULL COMMENT '提示信息',rule_status
tinyint(4) NOT NULL DEFAULT '1' COMMENT '規則狀態'
t_bean_rule_info:
bean_id
varchar(32) NOT NULL COMMENT '實體類編號',rule_id
varchar(16) NOT NULL COMMENT '規則編號',field_name
varchar(32) NOT NULL COMMENT '字段名',field_desc
varchar(128) DEFAULT NULL COMMENT '字段描述',check_status
tinyint(4) DEFAULT '1' COMMENT '是否校驗'
上手代碼
#####pom.xml配置
<!-- 統一制定spring boot版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
</parent>
<!-- 版本配置信息 -->
<properties>
<java.version>1.8</java.version>
<lombok.version>1.16.10</lombok.version>
<druid.version>1.1.0</druid.version>
<mybatis.version>1.3.0</mybatis.version>
<mysql.version>5.1.35</mysql.version>
<commons-lang3.version>3.5</commons-lang3.version>
</properties>
<!-- 所需依賴 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 日誌 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- 數據庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- spring AOP包含aspectj等依賴,不需要單獨引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- spring+mybatis整合依賴 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- mysql驅動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- lombok註解(注意在這裏使用需要在idea上安裝lombok插件) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- 工具類 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- maven編譯插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
DynamicCheckAspect核心代碼
@Component
@Slf4j
@Aspect
public class DynamicCheckAspect {
@Autowired
private DynamicCheckRuleService dynamicCheckRuleService;
@Autowired
private DynamicCheckEngine paramCheckEngine;
/**
* 定義切點
*/
@Pointcut("execution(* com.minuor.dynamic.check.controller.*.*(..))")
public void pointcut() {
}
/**
* 定義環切
*/
@Around("pointcut()")
public void check(ProceedingJoinPoint joinPoint) {
try {
// 查詢獲取請求參數封裝類(dto)的類名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
String beanName = null;
if (parameterTypes != null && parameterTypes.length > 0) {
beanName = parameterTypes[0].getSimpleName();
}
//查詢當前beanName下字段的所有校驗規則
List<DynamicCheckRuleModel> modelList = null;
if (StringUtils.isNotBlank(beanName)) {
modelList = dynamicCheckRuleService.queryRuleByBeanName(beanName);
}
if (modelList != null && !modelList.isEmpty()) {
//規則分類(根據字段名分類)
Map<String, List<DynamicCheckRuleModel>> ruleMap = new HashMap<>();
for (DynamicCheckRuleModel ruleModel : modelList) {
List<DynamicCheckRuleModel> fieldRules = ruleMap.get(ruleModel.getFieldName());
if (fieldRules == null) fieldRules = new ArrayList<>();
fieldRules.add(ruleModel);
ruleMap.put(ruleModel.getFieldName(), fieldRules);
}
//獲取請求參數
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
Object reqDto = args[0];
Field[] fields = reqDto.getClass().getDeclaredFields();
if (fields != null && fields.length > 0) {
for (Field field : fields) {
String fieldName = field.getName();
boolean isCheck = ruleMap.containsKey(fieldName);
if (!isCheck) continue;
field.setAccessible(true);
List<DynamicCheckRuleModel> paramRules = ruleMap.get(fieldName);
for (DynamicCheckRuleModel ruleModel : ruleMap.get(fieldName)) {
ruleModel.setFieldValue(field.get(reqDto));
}
//校驗
paramCheckEngine.checkParamter(paramRules);
}
}
}
}
joinPoint.proceed();
} catch (Exception e) {
throw new DynamicCheckException(e.getMessage());
} catch (Throwable throwable) {
throw new DynamicCheckException(throwable.getMessage());
}
}
}
這裏首先是獲取Dto的名稱,然後到數據庫中查詢校驗規則列表,如果沒有,就不需要校驗,中間的校驗邏輯就無需再走。
DynamicCheckEngine核心代碼
@Slf4j
@Component
public class DynamicCheckEngine {
/**
* 綜合校驗分發器
*
* @param paramRules
*/
public void checkParamter(List<DynamicCheckRuleModel> paramRules) throws Exception {
paramRules.sort(Comparator.comparing(DynamicCheckRuleModel::getCheckLevel));
for (DynamicCheckRuleModel ruleModel : paramRules) {
Method method = this.getClass().getMethod(ruleModel.getTemplateId(), DynamicCheckRuleModel.class);
Object result = method.invoke(this, ruleModel);
if (result != null) {
throw new DynamicCheckException((String) result);
}
}
}
/**
* 檢查非空
* 模板編號:notBlank
*/
public String notBlank(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
Object fieldValue = roleModel.getFieldValue();
if (fieldValue == null) {
return generateToastMsg(roleModel);
} else {
if ((fieldValue instanceof String) && StringUtils.isBlank((String) fieldValue)) {
return generateToastMsg(roleModel);
}
}
return null;
}
/**
* 檢查非空
* 模板編號:notNull
*/
public String notNull(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
if (roleModel.getFieldValue() == null) return generateToastMsg(roleModel);
return null;
}
/**
* 檢查長度最大值
* 模板編號:lengthMax
*/
public String lengthMax(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
String fieldValue = (String) roleModel.getFieldValue();
if (fieldValue.length() > Integer.valueOf(roleModel.getRuleExpress().trim())) {
return generateToastMsg(roleModel);
}
return null;
}
/**
* 檢查長度最小值
* 模板編號:lengthMin
*/
public String lengthMin(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
String fieldValue = (String) roleModel.getFieldValue();
if (fieldValue.length() < Integer.valueOf(roleModel.getRuleExpress().trim())) {
return generateToastMsg(roleModel);
}
return null;
}
/**
* 檢查值最大值
* 模板編號:valueMax
*/
public String valueMax(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
Double fieldValue = Double.valueOf(roleModel.getFieldValue().toString());
if (fieldValue > Double.valueOf(roleModel.getRuleExpress())) {
return generateToastMsg(roleModel);
}
return null;
}
/**
* 檢查值最小值
* 模板編號:valueMin
*/
public String valueMin(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
Double fieldValue = Double.valueOf(roleModel.getFieldValue().toString());
if (fieldValue < Double.valueOf(roleModel.getRuleExpress())) {
return generateToastMsg(roleModel);
}
return null;
}
/**
* 正則格式校驗
* 模板編號:regex
*/
public String regex(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
String value = (String) roleModel.getFieldValue();
if (!Pattern.matches(roleModel.getRuleExpress(), value)) {
return generateToastMsg(roleModel);
}
return null;
}
/**
* 構建結果信息
*/
private String generateToastMsg(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
String[] element = new String[]{StringUtils.isNotBlank(roleModel.getFieldDesc())
? roleModel.getFieldDesc() : roleModel.getFieldName(), roleModel.getRuleExpress()};
String toast = roleModel.getToastMsg();
int index = 0;
while (index < element.length) {
String replace = toast.replace("{" + index + "}", element[index] + "");
if (toast.equals(replace)) break;
toast = replace;
index++;
}
return toast;
}
}
在校驗方法checkParameter中,並不是去if else取判斷校驗模板名稱,而是使用反射的方式執行方法,當然這裏執行的校驗的方法名要和模板名稱相同,如校驗非空,模板名是notBlank,那麼對應的檢驗方法名就是notBlank。
總結
1、這裏沒有列出項目中的所有代碼,感覺沒有必要,太冗餘,主要思路和核心代碼足矣,其他的代碼下面會提供git和碼雲上的下載鏈接地址;
2、這裏校驗及基於post請求,如果你所在的項目中必須有get請求,那麼就需要重新籌劃一下這個校驗規則如何定義,如get採用方法名,post採用Dto名稱;
3、這裏代碼作爲demo展示,記得使用根據自己項目做優化;
4、這裏面校驗的異常都是往外拋出的,實際是不會把異常拋給用戶,可以在controller中做異常的統一過濾封裝。
項目代碼
碼雲:https://gitee.com/minuor/dynamic-check
gitHub:https://github.com/minuor/dynamic-check