1. 程式人生 > >Spring AOP+反射實現自定義動態配置校驗規則,讓校驗規則飛起來

Spring AOP+反射實現自定義動態配置校驗規則,讓校驗規則飛起來

場景小計

之前專案都是使用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中做異常的統一過濾封裝。

專案程式碼