drools的簡單入門案例

huan1993發表於2022-05-12

一、背景

最近在學習規則引擎drools,此處簡單記錄一下drools的入門案例。

二、為什麼要學習drools

假設我們存在如下場景:
在我們到商店購買衣服的時候,經常會發生這樣的事情,購買1件不打折,購買2件打0.98折,購買3件級以上打0.85折。
那麼我們在程式碼中如果要實現上述功能,是不是就需要編寫if ... else語句,假設後期規則變了,是不是就需要修改這些if ... else語句,然後程式重新部署。這樣是可以實現,但是不夠優雅。那麼我們是否可以將這些業務規則寫入到規則檔案中,以後規則變更直接修改規則檔案即可?而drools就可以實現這個功能。

三、實現上方這個簡單的打折案例

1、引入jar包

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-bom</artifactId>
            <type>pom</type>
            <version>7.69.0.Final</version>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
       <groupId>org.drools</groupId>
        <artifactId>drools-compiler</artifactId>
    </dependency>
    <dependency>
        <groupId>org.drools</groupId>
        <artifactId>drools-mvel</artifactId>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.11</version>
    </dependency>
</dependencies>

2、編寫kmodule.xml配置檔案

此配置檔案需要放置在resources/META-INF目錄下。

<kmodule xmlns="http://www.drools.org/xsd/kmodule">
    <!--
        kbase 可以存在多個
        name: 指定kbase的名字,需要是唯一的
        packages: 包名,可以理解為到src/main/resources目錄下查詢這個包名下的規則檔案,多個包使用逗號分割
        default: 當前kbase是否是預設的kbase
    -->
    <kbase name="shop-kabse" packages="com.huan.shop" default="false">
        <!--
            ksession 可以存在多個
            name: 指定ksession 的名字,需要唯一
            defalut: 當前ksession在這個kbase下是否是預設的
            type: 指定當前ksession是否是有狀態的 stateless表示是無狀態的
        -->
        <ksession name="shop-ksession" default="false" type="stateless"/>
        <ksession name="shop-ksession-stateful" default="false" type="stateful"/>
    </kbase>
</kmodule>

此處我們需要關注一下 kbasepackage的值,這個值需要和規則檔案中的package值一致,否則會找不到規則,具體看下方。

3、編寫規則檔案

1、規則檔案的語法

包名,必須放置在第一行
package
// 引入Java中的類,需要些全限定名
import

// 定義function ,可選
function  // Optional

// 定義 query ,可選
query  // Optional

declare   // Optional

global   // Optional

// rule 關鍵字 "rule name" 規則的名字
rule "rule name"
    // Attributes 屬性可選
    when  // 關鍵字
        // Conditions  條件,可為空
    then
        // Actions // 匹配後執行的結果
end // 關鍵字

2、編寫規則檔案

規則檔案的名字無所謂,比如: book-discount.drl

// 包名,必須防止到第一行,這個名字需要和 kbase中package屬性的值一致
package com.huan.shop

/**
 * 倒入類
 */
import com.huan.drools.CustomerOrder

// 定義規則
rule "shop-rule-01"
    when
        // 模式匹配:到工作記憶體中查詢CustomerOrder,並且這個物件的purchaseQuantity值需要是1,
        // 如果條件成立,$order是繫結變數名,一般以$開頭,和fact物件區分開
        $order:CustomerOrder(purchaseQuantity == 1)
    then
        System.out.println("匹配規則 shop-rule-01");
        // 賦值,此處賦值後,在Java程式碼中獲取獲取到賦值後的值
        $order.setDiscount(1D);
end

rule "shop-rule-02"
    when
        $order:CustomerOrder(purchaseQuantity == 2)
    then
        System.out.println("匹配規則 shop-rule-02");
        $order.setDiscount(0.98);
end

rule "shop-rule-03"
    when
        $order:CustomerOrder(purchaseQuantity >= 3)
    then
        System.out.println("匹配規則 shop-rule-03");
        $order.setDiscount(0.85);
end

3、解釋一下包名

包名

如果 shop-discount.drl的包名修改為com.huan.shop1則會提示如下警告:

12:43:01.589 [main] WARN org.drools.compiler.kie.builder.impl.KieBuilderImpl - File 'com/huan/shop/shop-discount.drl' is in folder 'com/huan/shop' but declares package 'com.huan.shop1'. It is advised to have a correspondance between package and folder names.

四、編寫Java程式碼

1、編寫一個訂單物件

此物件儲存的是使用者購買了幾件衣服和對應的折扣。

/**
 * 客戶購買衣服的訂單,省略 getter 和 setter 方法
 *
 * @author huan.fu
 * @date 2022/5/12 - 11:27
 */
public class CustomerOrder {
    /**
     * 購買了幾件衣服
     */
    private Integer purchaseQuantity;
    /**
     * 最終打多少折
     */
    private Double discount;

    public CustomerOrder(Integer purchaseQuantity) {
        this.purchaseQuantity = purchaseQuantity;
    }
}

2、編寫測試程式碼

1、無狀態測試方法statelessSessionTest規則規則2,即最終打0.98折。
2、有狀態測試方法statefulSessionTest規則規則3,即最終打0.85折。

package com.huan.drools;

import org.kie.api.KieServices;
import org.kie.api.event.rule.DebugRuleRuntimeEventListener;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.StatelessKieSession;

/**
 * drools 測試類
 */
public class DroolsApplication {
    public static void main(String[] args) throws InterruptedException {
        // 無狀態session測試
        statelessSessionTest();
        // 有狀態session測試
        statefulSessionTest();
    }

    private static void statelessSessionTest() {
        // 獲取kie services
        KieServices kieServices = KieServices.get();
        // 獲取kie容器物件
        KieContainer kieContainer = kieServices.getKieClasspathContainer();
        // 獲取kie session , 此處獲取的是無狀態的session,因為 <ksession name="shop-ksession" default="false" type="stateless"/>
        // 中type="stateless"就是無狀態的session
        StatelessKieSession kieSession = kieContainer.newStatelessKieSession("shop-ksession");
        // 建立一個物件,可以理解為 Fact物件,即事實物件
        CustomerOrder customerOrder = new CustomerOrder(2);
        // 新增監聽器,便於觀察日誌
        kieSession.addEventListener(new DebugRuleRuntimeEventListener());
        // 無狀態的session只需要執行 execute 方法即可。
        kieSession.execute(customerOrder);

        System.err.println("通過規則後,獲取到的折扣為:" + customerOrder.getDiscount());
    }

    private static void statefulSessionTest() {
        // 獲取kie services
        KieServices kieServices = KieServices.get();
        // 獲取kie容器物件
        KieContainer kieContainer = kieServices.getKieClasspathContainer();
        // 獲取kie session , 此處獲取的是有狀態的session
        KieSession kieSession = kieContainer.newKieSession("shop-ksession-stateful");
        // 建立一個物件,可以理解為 Fact物件,即事實物件
        CustomerOrder customerOrder = new CustomerOrder(3);
        // 新增監聽器,便於觀察日誌
        kieSession.addEventListener(new DebugRuleRuntimeEventListener());
        // 將customerOrder物件加入到工作記憶體中
        kieSession.insert(customerOrder);
        // 觸發所有的規則,如果只想觸發指定的規則,則使用fireAllRules(AgendaFilter agendaFilter)方法
        kieSession.fireAllRules();

        // 有狀態的session一定需要呼叫dispose方法
        kieSession.dispose();

        System.err.println("通過規則後,獲取到的折扣為:" + customerOrder.getDiscount());
    }
}

此處需要注意有狀態session無狀態session寫法的區別。

五、測試結果

測試結果

到此,我們使用drools實現的一個簡單的案例就實現了。

六、drools引擎的基本元件

規則引擎的基本元件

1、Rules:我們自己定義的業務規則,比如我們自己寫的規則檔案。所有規則必須至少包含觸發規則的條件和規則規定的操作。
2、Production memory:規則儲存在 Drools 引擎中的位置。
3、Facts:輸入或更改到 Drools 引擎中的資料,Drools 引擎匹配規則條件以執行適用規則。在規則中修改了Fact物件的值,真實的JavaBean的資料也會發生改變。
比如:當我們呼叫ksession.insert(物件),那麼插入的這個物件就可以理解成Facts物件。
4、Working memory:facts 在 Drools 引擎中儲存的位置。
5、Pattern matcher:匹配器,將Rule Base中所有的規則與Working memory中的Fact物件進行模式匹配,匹配成功的規則將被啟用並放入到Agenda中。
6、Agenda:議程,執行Agenda中被啟用的排好序的規則。

七、完整程式碼

https://gitee.com/huan1993/spring-cloud-parent/tree/master/drools/drools-quickstart

八、參考文件

1、https://docs.drools.org/7.69.0.Final/drools-docs/html_single/index.html#decision-engine-con_decision-engine