drools規則動態化實踐

語言: CN / TW / HK

作者:京東物流 李振 康睿 劉斌 王北永

、 規則引擎業務應用背景

業務邏輯中經常會有一些冗長的判斷,需要寫特別多的if else,或者一些判斷邏輯需要經常修改。這部分邏輯如果以java程式碼來實現,會面臨程式碼規模控制不住,經常需要修改邏輯上線等多個弊端。這時候我們就需要整合規則引擎對這些判斷進行線上化的管理

二、規則引擎選型

目前開源的規則引擎也比較多,根據原有專案依賴以及短暫接觸過的規則引擎,我們著重瞭解了一下幾個

drools:

-社群活躍,持續更新

-使用廣泛

-複雜

-學習成本較高

https://github.com/kiegroup/drools

easy-rule:

-簡單易學

-滿足使用訴求

-長時間未釋出新版

https://github.com/j-easy/easy-rules

easycode:

-京東物流同事維護的平臺

-基於flowable dmn實現

-配置簡單直觀

-已有大量系統使用

總結:

  1. 簡單配置型規則可以接入easycode,平臺提供配置頁面,通過jsf互動。

<!---->

  1. 複雜規則,需要動態生成規則,easycode目前還不支援。drools從流行度及活躍度考慮,都比easy-rule強,所以選擇drools。

三、 drools簡單示例

3.1 引入依賴

<dependency>
    <groupId>org.kie</groupId>
    <artifactId>kie-spring</artifactId>
    <version>${drools.version}</version>
</dependency>

3.2 寫drl檔案

我們寫一個簡單的demo

規則為:

匹配一個sku物件

0<價格<=100 10積分

100<價格<=1000 100積分

1000<價格<=10000 1000積分

在resources資料夾下新增 rules/skupoints.drl 檔案 內容如下

package com.example.droolsDemo
import com.example.droolsDemo.bean.Sku;

// 10積分
rule "10_points"
    when
        $p : Sku( price > 0 && price <= 100 )
    then
        $p.setPoints(10);
        System.out.println("Rule name is [" + drools.getRule().getName() + "]");
end

// 100積分
rule "100_points"
    when
        $p : Sku( price > 100 && price <= 1000 )
    then
        $p.setPoints(100);
        System.out.println("Rule name is [" + drools.getRule().getName() + "]");
end

// 1000積分
rule "1000_points"
    when
        $p : Sku( price > 1000 && price <= 10000 )
    then
        $p.setPoints(1000);
        System.out.println("Rule name is [" + drools.getRule().getName() + "]");
end

3.3 使用起來

    @Test
    public void testOneSku() {
        Resource resource = ResourceFactory.newClassPathResource("rules/skupoints.drl");
        KieHelper kieHelper = new KieHelper();
        kieHelper.addResource(resource);
        KieBase kieBase = kieHelper.build();
        KieSession kieSession = kieBase.newKieSession();
        Sku sku1 = new Sku();
        sku1.setPrice(10);
        kieSession.insert(sku1);
        int allRules = kieSession.fireAllRules();
        kieSession.dispose();
        System.out.println("sku1:" + JSONObject.toJSONString(sku1));
        System.out.println("allRules:" + allRules);
    }

    @Test
    public void testOneSku2() {
        Resource resource = ResourceFactory.newClassPathResource("rules/skupoints.drl");
        KieHelper kieHelper = new KieHelper();
        kieHelper.addResource(resource);
        KieBase kieBase = kieHelper.build();
        StatelessKieSession statelessKieSession = kieBase.newStatelessKieSession();
        Sku sku1 = new Sku();
        sku1.setPrice(10);
        statelessKieSession.execute(sku1);
        System.out.println("sku1:" + JSONObject.toJSONString(sku1));
    }

3.4 輸出

21.png

22.png

3.5 總結

如上,我們簡單使用drools,僅需要注意drl檔案語法。根據drl檔案生成規則的工作記憶體,通過KieSession或者StatelessKieSession與工作記憶體互動。整個流程並不複雜。注意 KieHelper僅是在演示中簡單使用,demo中包含使用bean來管理容器的方式,即便在簡單使用場景也不應通過 KieHelper來重複載入規則。

但是這樣並不能滿足我們線上化判斷,或者頻繁更改規則的訴求。於是我們在實踐中需要對drools更高階的使用方式。

四、 drools動態化實踐

從以上簡單demo中我們可以看出,規則依賴drl檔案存在。而業務實際使用中,需要動態對規則進行修改,無法直接使用drl檔案。

以下是我瞭解過的四種動態的方案:

  • drt檔案,建立模板,動態生成drl檔案,也是我們目前所用的方式。

<!---->

  • excel檔案匯入,實際上和模板檔案類似,依然無法直接交給業務人員來使用。

<!---->

  • 自己拼裝String,動態生成drl檔案,網上大多數博文使用方式,過於原始。

<!---->

  • api方式,drools的api方式複雜,使用需要對drl檔案有足夠的瞭解。

最後介紹以下drools在專案中的實際使用方式

4.1 配置規則

我們的業務場景可以理解為多個緩衝池構成的一個網狀結構。

示例如下:

23.png

上圖中每個方塊為一個緩衝池,每條連線為一條從A緩衝池流向B緩衝池的規則。實際場景中緩衝池有數百個,絕大多數都有自己的規則,這些規則構成一張複雜的網路。基於業務訴求,緩衝池的流向規則需要經常變動,我們需要在業務中能動態改變這些連線的條件,或者改變連線。在這種情況下,如果使用靜態的drl檔案來實現這些規則,需要數百規則檔案,維護量大,且每次修改後使規則生效的代價較大。在此背景下,我們嘗試drools高階應用,既規則動態化實踐。

我們在建立緩衝池的頁面中加入了流向規則的建立環節。每個緩衝池維護自己的流向規則,即為自己的一根連線。如下圖:

24.png

4.2 動態生成drl

drt檔案內容:

(實際業務模板中比這個複雜,有一定校驗及業務邏輯,此處做了簡化)

template header
// 模板需要使用的引數
id
cluePoolId
sourceList
cooperateTypeList
regionId
secondDepartmentId
battleId
outCluePoolId
amountCompareFlag
amount
salience

package rulePoolOut
// 全域性物件
global java.util.List list;
global java.util.List stopIdList;
global java.util.List ruleIdList;
// 引入的java類
import com.example.drools.bean.ClueModel
import org.springframework.util.CollectionUtils
import org.apache.commons.lang3.StringUtils;
import java.lang.Long
template "CluePoolOut"
// 規則名稱
rule "[email protected]{cluePoolId}[email protected]{id}"
//  引數 標識當前的規則是否不允許多次迴圈執行
no-loop true
//  引數 優先順序
salience @{salience}
//  引數 規則組 本組規則只能有一個生效 
activation-group "[email protected]{cluePoolId}"
// 匹配的LHS
when
    $clue:ClueModel(cluePoolId == @{cluePoolId})
    ClueModel(CollectionUtils.isEmpty(@{sourceList}) || source memberOf @{sourceList})
    ClueModel(CollectionUtils.isEmpty(@{cooperateTypeList}) || cooperateType memberOf @{cooperateTypeList})
    ClueModel(secondDepart == @{secondDepartmentId})
    ClueModel(regionNo == @{regionId})
    ClueModel(battleId == @{battleId})
    ClueModel(null != estimateOrderCount && (Long.valueOf(estimateOrderCount) @{amountCompareFlag} Long.valueOf(@{amount})))
// 如果配置要執行的RHS 支援java語法
then
    ruleIdList.add(@{id});
    $clue.setCluePoolId(Long.valueOf(@{outCluePoolId}));
    list.add(@{outCluePoolId});
    update($clue);
    }
end
end template

生成drl內容: 根據一個佇列及模板的路徑進行drl內容的生成

List<CrmCluePoolDistributeRuleBusinessBattleVO> ruleCenterVOS = new ArrayList<>();
CrmCluePoolDistributeRuleBusinessBattleVO vo = new CrmCluePoolDistributeRuleBusinessBattleVO();
vo.setCooperateTypeList(Lists.newArrayList(1, 2, 4));
vo.setAmountCompareFlag(">");
vo.setAmount(100L);
ruleCenterVOS.add(vo);
String drl = droolsManager.createDrlByTemplate(ruleCenterVOS, "rules/CluePoolOutRuleTemplate.drt");


    public String createDrlByTemplate(Collection<?> objects, String path) {
        ObjectDataCompiler compiler = new ObjectDataCompiler();
        try (InputStream dis = ResourceFactory.newClassPathResource(path, this.getClass()).getInputStream()) {
            return compiler.compile(objects, dis);
        } catch (IOException e) {
            log.error("建立drl檔案失敗!", e);
        }
        return null;
    }

4.3 載入drl

上邊的簡單示例中,我們使用了KieHelper 來載入規則檔案至工作記憶體中。實際上我們不可能在每次匹配中重新載入所有規則檔案,所以我們可以單例的使用規則容器,通過以下方式或者也可以使用@Bean等方式來管理容器。

private final KieServices kieServices = KieServices.get();
// kie檔案系統,需要快取,如果每次新增規則都是重新new一個的話,則可能出現問題。即之前加到檔案系統中的規則沒有了
private final KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
// 需要全域性唯一一個
private KieContainer kieContainer;

通過將內容寫入 kieFileSystem然後重新載入整個 kieBase即可重新載入規則,但是這種行為比較重,代價較大

也可以通過 kieBase新增一個檔案來進行載入,代價小,但是同步各個例項的代價較大。

KnowledgeBaseImpl kieBase = (KnowledgeBaseImpl)kieContainer.getKieBase(kieBaseName);
KnowledgeBuilder builder = KnowledgeBuilderFactory.newKnowledgeBuilder();
Resource resource = ResourceFactory.newReaderResource(new StringReader(ruleContent));
builder.add(resource,ResourceType.DRL);
if (builder.hasErrors()) {
    throw new RuntimeException("增加規則失敗!" + builder.getErrors().toString());
}
kieBase.addPackages(builder.getKnowledgePackages());

4.4 匹配

通過 StatelessKieSession與規則引擎互動

// 獲取一個連結
StatelessKieSession kieSession = droolsManager.getStatelessKieSession(RuleTemplateEnum.CLUE_POOL_OUT_RULE.getKieBaseName());
// 建立全域性變數物件
List<Long> list = new ArrayList<>();
List<Long> stopIdList = Lists.newArrayList();
List<String> result = new ArrayList<>();
List<Long> ruleIdList = new ArrayList<>();
// 塞入全域性變數
kieSession.setGlobal("ruleIdList", ruleIdList);
kieSession.setGlobal("list", list);
kieSession.setGlobal("stopIdList", stopIdList);
kieSession.setGlobal("result", result);
// 執行規則
kieSession.execute(clueModel);

如果使用 KieSession則需要在使用完成後進行關閉

kieSession.insert(clueModel);
kieSession.fireAllRules();
kieSession.dispose();

在執行規則的過程中可以加入各種監聽器對過程中各種變化進行監聽。篇幅原因交給各位去探索。

五、 總結

從上邊的流程中我們體驗了動態規則的建立以及使用。動態規則滿足了我們規則動態變化,規則統一管理的訴求。

我也總結了在這種使用方式下drools的幾個優缺點。

優點:

  • 規則動態化方便

  • 在工作記憶體中匹配規則效能好

  • 幾乎可以滿足所有的規則需求

  • 內建方法豐富完善

缺點:

  • 分散式一致性需要自行處理

  • 需要研發瞭解drl語法

  • 學習曲線陡峭

  • 匹配過程監控手段需要自行實現