Drools規則引擎探究以及在IOT的應用

前言:我上家公司是做物聯網的,任職在IOT部門,業務上針對不同類型的燃氣表,水錶,報警器等有不同協議,其中又包含不同廠家的表和自己公司的表。針對不同的協議,如何解析不同的協議頭幀,根據頭幀進行不同的複雜業務處理,後來引入了Drools規則引擎,通過規則的邏輯和數據的分離以及可擴展解決問題。
目前所處爲淘系技術部負責天貓奢品的業務,業務背景如下:業務包含天貓奢品頻道,奢品折扣頻道,天貓奢品官方直營旗艦店,魅力惠旗艦店,魅力惠APP等。基於業務場景下會員分爲店鋪會員,APP會員,天貓奢品行業會員等,而業務需要進行會員精細化的運營,通過不同的會員等級享受不同的權益,而相同的等級還需要做到根據偏好做到千人多權,如何根據複雜的業務需求變化更加精準的進行匹配,考慮Drools規則的邏輯數據的分離和可擴展性,接下來也會在天貓奢品的相關的會員模塊中和組內成員探討是否適合引入。下面是一些基於業務場景的總結和分享。

引入

問題引入

天貓奢品業務方爲了吸引更多的新客,和提高會員的活躍度,做了一期活動,通過購買天貓奢品頻道內的任意商品就贈送特殊積分,積分可以直接兌換限量的奢品商品。假如業務方給的規則如下:

主刃同學一看,這需求果然簡單呀,作爲團隊的核心開發,這不就是小case,5分鐘搞定,送贈品的簡單代碼如下:

public void exchangeGift(Integer points) {
        if (points < 100) {
            System.out.println("無商品");
        } else if (points > 100 && points <= 200) {
            System.out.println("Dior限量口紅");
        } else if (points > 200 && points <= 300) {
            System.out.println("TF限量口紅");
        } else if (points > 300 && points <= 400) {
            System.out.println("SK-II套裝");
        } else if (points > 400 && points <= 500) {
            System.out.println("MCM雙肩包");
        } else if (points > 500) {
            System.out.println("RADO雷達限量表");
        }
    }

活動進行了一天,運營發現Dior限量版口紅被兌換的很多,即將超出預期,業務方根據實際情況想改變規則,規則要求在各個層級上各加200分從而促進更多的消費。

主刃一看改動不大,於是五分鐘後修改完代碼並經測試後發佈上線。活動又進行了一天,運營人員通過後臺監控發現提到200分後,用戶達成比較少,想再現有基礎上再降100分。沒辦法還的改不是,主刃同學這次把配置數據放入到了阿里集團的配置中心diamond中,當運營改策略的,只要改一下Diamond的值就可以了。

核心代碼編程了這樣:

public void exchangeGift(Integer points) {
        if (points < CommDiamondProperty.getInstance().getLevelOne()) {
            System.out.println("無商品");
        } else if (points > CommDiamondProperty.getInstance().getLevelOne() && points <= CommDiamondProperty.getInstance().getLevelTwo()) {
            System.out.println("Dior限量口紅");
        } else if (points > CommDiamondProperty.getInstance().getLevelTwo() && points <= CommDiamondProperty.getInstance().getLevelThree()) {
            System.out.println("TF限量口紅");
        } else if (points > CommDiamondProperty.getInstance().getLevelThree() && points <= CommDiamondProperty.getInstance().getLevelFour()) {
            System.out.println("SK-II套裝");
        } else if (points > CommDiamondProperty.getInstance().getLevelFour() && points <= CommDiamondProperty.getInstance().getLevelFive()) {
            System.out.println("MCM雙肩包");
        } else if (points > CommDiamondProperty.getInstance().getLevelFive()) {
            System.out.println("RADO雷達限量表");
        }
    }

新功能上線後,運營側提出這次的分層太少了,BD到了更多的好的贈品,需要更加精細化的運營,由以前的5組變成10組,主刃此刻的心情:kao …

那麼是否有什麼技術可以將活動規則和代碼解耦,不管規則如何變化,執行端不用動。那就是規則引擎,那麼規則引擎到底是什麼東西呢?我們來看看。

規則引擎

相關介紹

目前對領域模型的常見抽象方式是面向對象思想,即簡單的來說把業務邏輯抽象爲對象、對象的屬性和對象的方法。這樣規則跟整個系統耦合,修改規則需要走全Case的開發測試流程發佈流程,對於頻繁修改的規則效率比較低,爲了解決這個問題就出現了規則引擎。

今天分享的Drools,最早由Jboss開發,目前由Redhat開源的規則引擎,選擇分享Drools的原因是它是Redhat的KIE Group中的組件之一,可以比較方便的跟另一個組件JBPM工作流配合用於管理複雜的規則流;同時Drools的推理策略算法在經典Rete算法以及其它算法的基礎上做了多個版本的增強,目前支持結合正向推理和反向推理優點的混合推理。

規則引擎主要完成的就是將業務規則從代碼中分離出來。 在規則引擎中,利用規則語言將規則定義爲if-then的形式,if中定義了規則的條件,then中定義了規則的結果。規則引擎會基於數據對這些規則進行計算,找出匹配的規則。這樣,當規則需要修改時,無需進行代碼級的修改,只需要修改對應的規則,可以有效減少代碼的開發量和維護量。

Java開源的規則引擎有:Drools、Easy Rules、Mandarax、IBM ILOG。使用最爲廣泛並且開源的是Drools。

規則引擎優點

Drools 是用 Java 語言編寫的開放源碼規則引擎,使用 Rete 算法對所編寫的規則求值。Drools 允許使用聲明方式表達業務邏輯。可以使用非 XML 的本地語言編寫規則,從而便於學習和理解。並且,還可以將 Java 代碼直接嵌入到規則文件中,這令 Drools 的學習更加吸引人。

  • 聲明式編程

    使用規則的核心優勢在於可以簡化對於複雜問題的邏輯表述,並對這些邏輯進行驗證(規則比編碼具有更好的可閱讀性)。規則機制可以解決很複雜的問題,提供一個如何解決問題的說明,並說明每個決策的是如何得出的。

  • 邏輯和數據分離

    將業務邏輯都放在規則裏的好處是業務邏輯發生變化時,可以更加方便的進行維護。尤其是這個業務邏輯是一個跨域關聯多個域的邏輯時。不像原先那樣將業務邏輯分散在多個對象或控制器中,業務邏輯可以被組織在一個或多個清晰定義的規則文件中。

    數據位於“域對象”中,業務邏輯位於“規則”中。根據項目的種類,這種分離是非常有利的。

  • 速度和可擴展性

    網絡算法(Rete algorithm),跳躍算法(Leaps algorithm),以及有它們派生出的 Drools 的 Reteoo算法(以及跳躍算法),提供了非常高效的方式根據業務對象的數據匹配規則。

    Drools的Rete OO算法已經是一個成熟的算法。在Drools的幫助下,您的應用程序變得非常可擴展。如果頻繁更改請求,可以添加新規則,而無需修改現有規則。

  • 知識集中化

    通過使用規則,您創建一個可執行的知識庫(知識庫)。這是商業政策的一個真理點。理想情況下,規則是可讀的,它們也可以用作文檔。

規則引擎缺點

  • 複雜性提高
  • 需要學習新的規則語法
  • 引入新組件的風險

規則引擎的實現

Drools規則引擎的結構示意圖:

Rete 算法

Rete 算法最初是由卡內基梅隆大學的 Charles L.Forgy 博士在 1974 年發表的論文中所闡述的算法 , 該算法提供了專家系統的一個高效實現。自 Rete 算法提出以後 , 它就被用到一些大型的規則系統中 , 像 ILog、Jess、JBoss Rules 等都是基於 RETE 算法的規則引擎。

Rete 在拉丁語中譯爲”net”,即網絡。Rete 匹配算法是一種進行大量模式集合和大量對象集合間比較的高效方法,通過網絡篩選的方法找出所有匹配各個模式的對象和規則。

其核心思想是將分離的匹配項根據內容動態構造匹配樹,以達到顯著降低計算量的效果。 Rete 算法可以被分爲兩個部分:規則編譯和規則執行。 當Rete算法進行事實的斷言時,包含三個階段: 匹配選擇執行 ,稱做 match-select-act cycle。

Reta網絡節點圖如下所示:

fact: 對象之間及對象屬性之間的關係,例如Product類及其類目下的points、gift屬性等Root。

Node: 根節點,Rete網絡入口。

Type Node: 對應不同的fact對象,也就是規則用到的POJO,每個fact就是一個TypeNode節點Alpha Node :對應規則裏的每個條件,例如規則條件Product(points < 100)中,points<100就是一個AlphaNode

Beta Node: 用於組合兩個fact的alpha條件或BetaNode與fact條件的組合。

LeftInputAdapter Node: 用來對2個規則隊形進行比較,將一個single Object 轉化爲一個單對象數組(因爲BetaNode左邊入口往往是一個list規則隊形),傳播到 JoinNode 節點。

Join Node : 用於聚合BetaNode節點的結果。

Drools 中的 Rete 算法被稱爲 ReteOO,表示 Drools 爲面向對象系統(Object Oriented systems)增強並優化了 Rete 算法。在 Drools 中,規則被存 放在 Production Memory(規則庫)中,推理機要匹配的 facts(事實)被存在 Working Memory(工作內存)中。當時事實被插入到工作內存中後,規則引擎會把事實和規則庫裏的模式進行匹配,對於匹配成功的規則再由 Agenda 負責具體執行推理算法中被激發規則的結論部分,同時 Agenda 通過沖突決策策略管理這些衝突規則的執行順序,Drools 中規則衝突決策策略有:(1) 優先級策略 (2) 複雜度優先策略 (3) 簡單性優先策略 (4) 廣度策略 (5) 深度策略 (6) 裝載序號策略 (7) 隨機策略 。

Drools規則引擎

Drools介紹

官網:http://www.drools.org/

官方文檔:http://www.drools.org/learn/documentation.html

Drools 是一個基於Charles Forgy’s的RETE算法的,易於訪問企業策略、易於調整以及易於管理的開源業務規則引擎,符合業內標準,速度快、效率高。業務分析師人員或審覈人員可以利用它輕鬆查看業務規則,從而檢驗是否已編碼的規則執行了所需的業務規則。

Drools 是用Java語言編寫的開放源碼規則引擎,使用Rete算法對所編寫的規則求值。Drools允許使用聲明方式表達業務邏輯。可以使用非XML的本地語言編寫規則,從而便於學習和理解。並且,還可以將Java代碼直接嵌入到規則文件中,這令Drools的學習更加吸引人。

Drools相關概念

  • 事實(Fact): 對象之間及對象屬性之間的關係
  • 規則(rule): 是由條件和結論構成的推理語句,一般表示爲if…Then。一個規則的if部分稱爲LHS,then部分稱爲RHS。
  • 模式(module): 就是指IF語句的條件。這裏IF條件可能是有幾個更小的條件組成的大條件。模式就是指的不能在繼續分割下去的最小的原子條件。

Drools通過 事實、規則和模式相互組合來完成工作,drools在開源規則引擎中使用率最廣,但是在國內企業使用偏少,保險、支付行業使用稍多。

解決問題

Drools有專門的規則語法drl,就是專門描述活動的規則是如何執行的,按照主刃的需求規則如下:

rule.drl文件內容如下:

package com.alibaba.rules

import com.alibaba.Order

rule "zero"
    no-loop true
    lock-on-active true
    salience 1
    when
        $s : Order(point <= 300)
    then
        System.out.println("無贈品");
        doSth($s);
end

rule "giftOne"
    no-loop true
    lock-on-active true
    salience 1
    when
        $s : Order(amout > 300 && amout <= 500)
    then
        System.out.println("Dior限量口紅");
        doSth($s);
end

rule "giftTwo"
    no-loop true
    lock-on-active true
    salience 1
    when
        $s : Order(amout > 500 && amout <= 700)
    then
        System.out.println("TF限量口紅");
        doSth($s);
end

rule "giftThree"
    no-loop true
    lock-on-active true
    salience 1
    when
        $s : Order(amout > 700 && amout <= 900)
    then
        System.out.println("SK-II套裝");
        doSth($s);
end

rule "giftFour"
    no-loop true
    lock-on-active true
    salience 1
    when
        $s : Order(amout > 900 && amout <= 1100)
    then
        System.out.println("MCM雙肩包");
        doSth($s);
end

rule "giftFive"
    no-loop true
    lock-on-active true
    salience 1
    when
        $s : Order(amout > 1100)
    then
        System.out.println("RADO雷達限量表");
        doSth($s);
end 

說明

  • package 與Java語言類似,drl的頭部需要有package和import的聲明,package不必和物理路徑一致。
  • import 導出java Bean的完整路徑,也可以將Java靜態方法導入調用。
  • rule 規則名稱,需要保持唯一 件,可以無限次執行。
  • no-loop 定義當前的規則是否不允許多次循環執行,默認是 false,也就是當前的規則只要滿足條件,可以無限次執行。
  • lock-on-active 將lock-on-active屬性的值設置爲true,可避免因某些Fact對象被修改而使已經執行過的規則再次被激活執行。
  • salience 用來設置規則執行的優先級,salience 屬性的值是一個數字,數字越大執行優先級越高, 同時它的值可以是一個負數。默認情況下,規則的 salience 默認值爲 0。如果不設置規則的 salience 屬性,那麼執行順序是隨機的。
  • when 條件語句,就是當到達什麼條件的時候
  • then 根據條件的結果,來執行什麼動作
  • end 規則結束

這個規則文件就是描述了,當符合什麼條件的時候,應該去做什麼事情,每當規則有變動的時候,我們只需要修改規則文件,然後重新加載即可生效。

這裏需要有一個配置文件告訴代碼規則文件drl在哪裏,在drools中這個文件就是kmodule.xml,放置到resources/META-INF目錄下。

kmodule.xml內容如下:

<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.drools.org/xsd/kmodule">
  <configuration>
    <property key="drools.evaluator.supersetOf" value="org.mycompany.SupersetOfEvaluatorDefinition"/>
  </configuration>
  <kbase name="KBase1" default="true" eventProcessingMode="cloud" equalsBehavior="equality" declarativeAgenda="enabled" packages="org.domain.pkg1">
    <ksession name="KSession2_1" type="stateful" default="true"/>
    <ksession name="KSession2_2" type="stateless" default="false" beliefSystem="jtms"/>
  </kbase>
  <kbase name="KBase2" default="false" eventProcessingMode="stream" equalsBehavior="equality" declarativeAgenda="enabled" packages="org.domain.pkg2, org.domain.pkg3" includes="KBase1">
    <ksession name="KSession3_1" type="stateful" default="false" clockType="realtime">
      <fileLogger file="drools.log" threaded="true" interval="10"/>
      <workItemHandlers>
        <workItemHandler name="name" type="org.domain.WorkItemHandler"/>
      </workItemHandlers>
      <calendars>
        <calendar name="monday" type="org.domain.Monday"/>
      </calendars>
      <listeners>
        <ruleRuntimeEventListener type="org.domain.RuleRuntimeListener"/>
        <agendaEventListener type="org.domain.FirstAgendaListener"/>
        <agendaEventListener type="org.domain.SecondAgendaListener"/>
        <processEventListener type="org.domain.ProcessListener"/>
      </listeners>
    </ksession>
  </kbase>
</kmodule>

說明

  • Kmodule 中可以包含一個到多個 kbase,分別對應 drl 的規則文件。
  • Kbase 需要一個唯一的 name,可以取任意字符串。
  • packages 爲drl文件所在resource目錄下的路徑。注意區分drl文件中的package與此處的package不一定相同。多個包用逗號分隔。默認情況下會掃描 resources目錄下所有(包含子目錄)規則文件。
  • kbase的default屬性,標示當前KieBase是不是默認的,如果是默認的則不用名稱就可以查找到該 KieBase,但每個 module 最多隻能有一個默認 KieBase。
  • kbase 下面可以有一個或多個 ksession,ksession 的 name 屬性必須設置,且必須唯一。

rules.drl中下方相關規則內容既可,如果活動規則動態的添加、減少也可以相應的去增加、減少規則文件既可,再也不用去動代碼了。

rule "xxx"
    no-loop true
    lock-on-active true
    salience 1
    when
        $s : Order(amout > yy)
    then
        System.out.println("xxxx");
        doSth();
end  

規則中的條件操作符

Drools提供了十二中類型比較操作符:< 、<=、>、>=、==、!=、contains、not contains、memberOf、not memberOf、matches、not matches,並且這些條件都可以組合使用。

  • 條件組合: 各種操作符可以組合使用
  • Fact對象私有屬性的操作: RHS中對Fact對象private屬性的操作必須使用getter和setter方法,而RHS中則必須要直接用.的方法去使用.
  • contains: 對比是否包含操作,操作的被包含目標可以是一個複雜對象也可以是一個簡單的值。
  • matches: 正則表達式匹配,與java不同的是,不用考慮’/'的轉義問題
  • memberOf: 判斷某個Fact屬性值是否在某個集合中,與contains不同的是他被比較的對象是一個集合,而contains被比較的對象是單個值或者對象。

注意:如果條件全部是【&&】可使用【,】來替代,但是兩者不能混用。

規則中的結果部分

  • insert: 往當前workingMemory中插入一個新的Fact對象,會觸發規則的再次執行,除非使用no-loop限定;
  • update: 更新
  • modify: 修改,與update語法不同,結果都是更新操作
  • retract: 刪除
  • function: 定義一個方法,如:

function void console { System.out.println(); StringUtils.getId();// 調用外部靜態方法,StringUtils必須使用import導入,getId()必須是靜態方法}

  • declare:可以在規則文件中定義一個class,使用起來跟普通java對象相似,你可以在RHS部分中new一個並且使用getter和setter方法去操作其屬性。

declare Address @author(dyzmj) // 元數據,僅用於描述信息 @createTime(2019-07-08) city : String @maxLengh(100) postno : intend

'@'是元數據定義,用於描述數據的數據~,沒什麼執行含義。

你可以在RHS部分中使用Address address = new Address()的方法來定義一個對象。

更多語法規則可參考:Drools規則引擎語法

Maven中使用Drools的

工程結構目錄

依賴文件pom.xml

<!--規則引擎drools-->
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-core</artifactId>
    <version>7.0.0.Final</version>
</dependency>
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-compiler</artifactId>
    <version>7.0.0.Final</version>
</dependency>
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-decisiontables</artifactId>
    <version>7.0.0.Final</version>
</dependency>
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-templates</artifactId>
    <version>7.0.0.Final</version>
</dependency>

<dependency>
    <groupId>org.kie</groupId>
    <artifactId>kie-api</artifactId>
    <version>7.0.0.Final</version>
</dependency>

配置文件kmodule.drl

package com.rules
import model.ProtocolType
dialect "java"

rule "jk16"
    when
        $protocol : ProtocolType(data matches "^1A12.*16$" )
    then
        $protocol.setType("xx燃氣表協議");
        System.out.println("觸發規則1:"+$protocol.getData());
     end

rule "jkstd"
    when
        $protocol : ProtocolType(data matches "18.*26",length == 10)
    then
        $protocol.setType("xx水錶標準協議");
        System.out.println("觸發規則2:"+$protocol.getData());
     end

測試程序Main.java

@Slf4j
public class Main {

    @Test
    public void test() {

        KieServices ks = KieServices.Factory.get();
        KieContainer kContainer = ks.getKieClasspathContainer();
        KieSession kSession = kContainer.newKieSession("ksession-rules");

        ProtocolType protocolType = new ProtocolType();
        protocolType.setData("1A1212345616");
        protocolType.setLength(12);
        kSession.insert(protocolType);
        kSession.fireAllRules();
        kSession.dispose();
        System.out.println(protocolType);

        log.info(protocolType.getType());
    }

}

運行結果

觸發規則1:1A1212345616
ProtocolType(data=1A1212345616,length=12,type=xx燃氣表協議)
2020-08-19 18:21:35 [main] [io.example.demo.Main.test(Main.java:35)] - [INFO] xx燃氣表協議

Process finished with exit code 0

Drools中的坑

Drools與SpringBoot集成時,與熱部署工具spring-boot-devtools存在類加載器衝突的問題,會導致所有的規則失效。在drools的官方網站中有人提出了這個問題,並認爲是個bug,但是drools的開發者認爲這不是一個bug。

地址:https://issues.jboss.org/browse/DROOLS-1540

drools用的是lancher classloader,而devtools會把我們自己編寫的model認爲是會隨時更改的類,所以採用的是Restart ClassLoader類加載器進行加載(爲了快速進行熱部署),devtools會有兩個類加載器,一個Classloader加載那些不會改變的類(第三方Jar包),另一個ClassLoader加載會更改的類,稱爲 Restart ClassLoader。當採用Launcher ClassLoader加載的A 類與Restart Class Loader的A類進行對比時,發現不一致,所以drools引擎自然無法進行識別。

所以解決辦法就是將devtools的maven依賴去掉即可,或者採用drools官網中說明的其它方法。

本文轉載自公衆號淘系技術(ID:AlibabaMTT)。

原文鏈接

Drools規則引擎探究以及在IOT的應用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章