天天看點

如何設計一個規則引擎

之前答應同僚寫一篇關于規則引擎的文章,本文結合工作中實際場景,列舉了一些規則引擎設計方案

工單系統-規則引擎

一個通用的工單系統,一般會考慮以下幾點問題:

1、前端可以通過界面來配置審批流程節點,每個節點可以通過條件來配置不同的流向

2、每個節點需要根據不同的條件展示不同的表單元件,或者根據規則設定預設值

簡單來說上面的規則可以分為兩種:

1、判斷類型:根據不同條件處理不同的邏輯

2、指派類型:為某個元素賦予預設值

判斷類型

判斷類型使用到最多的一種規則,比如合同審批流程,會根據合同金額、類型走不同的審批流程,而且每一個審批節點,也需要動态控制合同金額是否展示、編輯。

以下是一個簡易版的合同審批流程:

如何設計一個規則引擎

在不同的節點可以根據條件來設定下一步的流向,比如:“合同金額大于5萬”

如何設計一個規則引擎

對于表單資料的權限,我們可以設定3種權限,可編輯、隻讀、不可見,然後對3種權限設定規則

如何設計一個規則引擎

指派類型

在每個審批流程節點,都會有對應的審批資訊,比如一個報帳稽核資訊,有些資訊需要人工填寫,有些資訊則需要動态賦予一個預設值,指派類型一般是在點選元件的時候才會加載,這樣可以有效的避免一些無用的字段在業務接口中一次性傳回,根據不同的場景加載需要的字段。比如下面:

如何設計一個規則引擎

設計思路:對應指派類型的,前面用=開始,後面接對應的表達式。

或者資料通過接口擷取,每個審批節點的審批人可能不同,一般先是一級負責人審批、再是二級負責人審批...,這樣依次到總負責人審批完成。

是以在每個節點,可以在規則引擎中配置一個接口,動态擷取審批人。

設計思路:後端定義好每個接口的枚舉類型,然後傳回給前端,前端将流程節點要擷取審批人和枚舉類型綁定,後端解析調用對應的接口。

解析規則

以上兩種類型,指派類型以‘=‘開頭

=$交通金額$ * $交通發票數量$ + $餐飲金額$ * $餐飲發票數量$           
$合同類型$ = ‘商務合同’ || ( $合同總金額$ > 1000000 && $合同總金額$ < 2000000 )           

對應表達式解析,如果表達式中有自定義的表達式,可以自己實作解析算法,具體可參考我的這篇文章《逆波蘭算法在規則引擎中的運用》

對于一般簡單的表達式,也可以使用spring自帶的解析器,具體使用如下:

// 1. 建構解析器
org.springframework.expression.ExpressionParser parser = new SpelExpressionParser();
// 2. 解析表達式
// 數字類型
Expression integerExpression = parser.parseExpression("100 * 2 + 400 * 1 + 66");
// boolean類型
Expression booleanExpression = parser.parseExpression("1>0 && 1<2 || (4%2=0)");

// 3. 擷取結果
int integerResult = (Integer) integerExpression.getValue();
System.out.println(integerResult); // 結果:666

boolean booleanResult = (Boolean) booleanExpression.getValue();
System.out.println(booleanResult); // 結果:true           

規則引擎架構

Drools

目前使用比較多的是開源規則引擎是Drools,Drools 是一個基于 Java 的開源的規則引擎,可以将複雜多變的規則從寫死中解放出來,以規則腳本的形式存放在檔案中,使得規則的變更不需要修正代碼重新開機機器就可以立即線上上環境生效。

舉個栗子,商城一個促銷活動,如果商品金額小于100元,則不加積分

金額大于100小于500,加100積分

金額大于500,加200積分

package rules

import com.neo.drools.entity.Order

rule "zero"
    when
        $s : Order(amout <= 100)
    then
        $s.setScore(0);
        update($s);
end

rule "add100"
    when
        $s : Order(amout > 100 && amout <= 500)
    then
        $s.setScore(100);
        update($s);
end

rule "add200"
    when
        $s : Order(amout > 500)
    then
        $s.setScore(200);
        update($s);
end           

将規則寫到Drools腳本檔案(score-rule.drl)中,然後通過java代碼來執行。

public static final void main(String[] args) throws Exception{
    KieServices ks = KieServices.Factory.get();
    KieContainer kc = ks.getKieClasspathContainer();
    execute( kc );
}

public static void execute( KieContainer kc ) throws Exception{
    KieSession ksession = kc.newKieSession("score-rule");
    List<Order> orderList = getInitData();
    for (int i = 0; i < orderList.size(); i++) {
        Order o = orderList.get(i);
        ksession.insert(o);
        ksession.fireAllRules();
        addScore(o);
    }
    ksession.dispose();
}           

Drools足以實作大多數場景的需求。但是為了使用它,則需要去專門學習它的腳本規則,而且對于營運來說學習成本很大。

Java與Lua/JS

通過Java與Lua/JS等腳本語言的結合,

首先是在項目中添加需要的jar包,隻需在maven中引入下面的包即可:

<dependency>
    <groupId>org.luaj</groupId>
    <artifactId>luaj-jse</artifactId>
    <version>3.0.1</version>
</dependency>           

在這裡我們使用的是Luaj,Luaj是Lua的一個Java實作,相比其他的native調用要更為穩定,并且本身也支援JSR223标準。

如果需要在Java中調用Lua腳本,隻需建立一個Globals對象,載入腳本,然後就能在Java中去調用Lua腳本了,例如:

Globals globals =JsePlatform.standardGlobals();
LuaValue chunk =globals.load("print(\"hello luaj !!!\")");
chunk.call();           

其中load方法内執行的就是一段Lua代碼。

當然我們也可以去執行腳本中的某個具體方法,并且傳入參數,例如這段Lua腳本:

function oneArgTest(str)
    print(#str)
end           

這段腳本的作用是傳回傳入參數的長度,隻需要将之前的Java代碼加上下面一段:

LuaValue oneArgTest= globals.get(LuaValue.valueOf("oneArgTest"));
LuaValue results =oneArgTest.call("test func");           

就能在Java中傳入參數并執行該方法了。

我們甚至可以将Java對象也作為參數傳入Lua腳本裡,進而實作從Lua中對Java對象進行操作,以及調用Java對象中的方法,例如:

LuaValue luaValue =CoerceJavaToLua.coerce(test);           

其中test是一個Java對象,通過CoerceJavaToLua.coerce方法将其轉換成一個LuaValue對象,然後就能像之前的代碼那樣傳入Lua腳本中,如果需要在腳本中調用test對象的方法,隻需像這樣:

test:getTestName()
test:setTestName("objecttest")           

通過對象+”:”+方法名的形式調用。

當然這種Lua+Java混合調用的模式非常靈活,不過也帶來了一個麻煩, 那就是有Java方法的Lua腳本隻能在JVM中去執行,這為代碼的調試帶來了一定麻煩。

自定義規則

對于某些具體的業務,本身其業務邏輯就已經比較固定,所需要制定的規則也很有限。

比如一個系統需要頻繁對不同資料源的進行封包過濾、替換等操作,在這種情況下我們可以去嘗試自己開發一個規則引擎。

首先我們需要制定一個規則腳本,可以使用通用的json格式的規則腳本,用來對某個航班的資料封包進行加工處理,如下。

比對航班為CZ8123,将CZ81替換為OQ23

{
      "conditions": [
        {"args":["flightNo","CZ8123"],"method": "equals"}
      ],
      "operations": [
        {"args":["flightNo","CZ81","OQ23"]," method ":"replace"}
      ]
}           

在一個腳本中包含多個規則,每個規則獨立執行。對于每一個規則,都包括兩個部分,條件判斷(conditions)與執行(operations)操作。條件與操作均支援多個,也就是說當滿足所有的條件時,會執行後面的所有操作。每一個條件與操作都包含兩部分,參數(args)與對應的方法(method),參數可以是固定值,也可以是封包的某一屬性(腳本中以dyn,開頭),又或是約定好的特定值(例如做時間判斷時可以用now表示目前時間);方法則和Java代碼中的方法名相對應。

規則解析部分就比較簡單了,因為本身就是json格式,隻需要分别讀取條件與操作部分,然後去比對傳入參數與方法名,然後通過反射的方式去執行Java代碼中的方法即可。

這個自定義的規則引擎看起來似乎沒有前面兩種解決方案強大,并且也有着很大的局限性,但是可以通過這個簡單的規則腳本,為其編寫一個前端頁面,然後實作直接通過一個可視化的界面來配置規則。

成品大概是這樣的:

如何設計一個規則引擎

大緻思路就是在頁面中實作DOM元素與JSON對象的互相轉化,直接用JQuery就可以實作,再加上一個腳本的查找和更新功能就可以了。

這麼一來,不僅代碼不用寫了,腳本也不用管了。直接在頁面上用滑鼠點幾下就能完成一個規則的配置,而且不僅僅是研發人員,沒有任何程式設計基礎的營運與客服也能輕松上手。

以上幾種方案,既有功能強大但實作複雜的,也有功能簡單但是實作友善的,不管是用哪種方案,在面對需要頻繁變動業務邏輯的場景時,大部分時候都能輕松的應對了。

本文來源于公衆号《百川分享會》:baichuanshare
如何設計一個規則引擎