背景
最近有個新項目可能會用到規則引擎,所以花了些時間對相關技術做調研,在百度、google用“規則引擎”作為關鍵字進行搜索,可以找到很多關於這方面的資料,絕大部分都會提到 drools、urules、easy-rules等等這么些開源項目,有一些文章也提到他們是采用groovy腳本來實現的。通過對項目需求的評估,初步判定groovy腳本已經可以滿足實際的場景。
然而,在這些資料或者方案之中,除了urules,大部分只是關注框架的性能和使用上的簡便,很少探討如何讓業務人員可以自行進行規則定義的方案。而urules雖然自帶了可視化的規則管理界面,但是界面樣式不好自定義,無法跟現有后台管理界面不突兀的融合。
通過不斷嘗試變換關鍵字在搜索引擎搜索,最終在stackoverflow找到了一個探討這個問題的帖子,特此將帖子中提到的方案分享一下,如果你跟我一樣在研究同樣的問題,也許對你有用。不過在介紹這個方案之前,得先簡單了解一下什么是規則引擎
什么是規則引擎?
簡單的說,規則引擎所負責的事情就是:判定某個數據或者對象是否滿足某個條件,然后根據判定結果,執行不同的動作。例如:
對於剛剛在網站上完成購物的一個用戶(對象),如果她是 "女性用戶 並且 (連續登錄天數大於10天 或者 訂單金額大於200元 )" (條件) , 那么系統就自動給該用戶發放一張優惠券(動作)。
在上面的場景中,規則引擎最重要的一個優勢就是實現“條件“表達式的配置化。如果條件表達式不能配置,那么就需要程序員在代碼里面寫死各種if...else... ,如果條件組合特別復雜的話,代碼就會很難維護;同時,如果不能配置化,那么每次條件的細微變更,就需要修改代碼,然后通過運維走發布流程,無法快速響應業務的需求。
在groovy腳本的方案中,上面的場景可以這么實現:
- 1)定義一個groovy腳本:
def validateCondition(args){return args.用戶性別 == "女性" && (args.連續登錄天數>10 || args.訂單金額 > 200);}
- 2)通過Java提供的 ScriptEngineManager 對象去執行
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.7</version>
</dependency>
/*
*
* @params condition 從數據庫中讀出來的條件表達式
*/
private Boolean validateCondition(String condition){
//實際使用上,ScriptEngineManager可以定義為單例
ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName(scriptLang);
Map<String, Object> args = new HashMap<>();
data.put("用戶性別", "女性");
data.put("連續登錄天數", 11);
data.put("訂單金額", 220);
engine.eval(script);
return ((Invocable) engine).invokeFunction("validateCondition", args);
}
在上面的groovy腳本中,經常需要變動的部分就是 ”args.用戶性別 == "女性" && (args.連續登錄天數>10 || args.訂單金額 > 200)“ 這個表達式,一個最簡單的方案,就是在后台界面提供一個文本框,在文本框中錄入整個groovy腳本,然后保存到數據庫。但是這種方案有個缺點:表達式的定義有一定門檻。對於程序員來說,這自然是很簡單的事,但是對於沒接觸過編程的業務人員,就有一定的門檻了,很容易錄入錯誤的表達式。這就引出了本文的另一個話題,如何實現bool表達式的可視化編輯?
如何實現bool表達式的可視化編輯?
一種方案就是對於一個指定的表達式,前端人員進行語法解析,然后渲染成界面,業務人員編輯之后,再將界面元素結構轉換成表達式。然而,直接解析語法有兩缺點:
- 1)需要考慮的邊界條件比較多,一不小心就解析出錯。
- 2)而且也限定了后端可以選用的腳本語言。例如,在上面的方案中選用的是groovy,它使用的"與"運算符是 && , 假如某天有一種性能更好的腳本語言,它的"與"運算符定位為 and ,那么就會需要修改很多表達式解析的地方。
另一種方案,是定義一個數據結構來描述表達式的結構(說了這么多,終於來到重點了):
{ "all": [
{ "any": [
{ "gt": ["連續登錄天數", 10] },
{ "gt": ["訂單金額", 200] }
]},
{ "eq": ["用戶性別", "女性"] }
]}
然后,使用遞歸的方式解析該結構,對於前端開發,可以在遞歸解析的過程中渲染成對應的界面元素;對於后端人員,可以生成對應的bool表達式,有了bool表達式,就可以使用預定的腳本模板,生成最終的規則。
// 模板的例子
def validateCondition(args){return $s;}
/**
* 動態bool表達式解析器
*/
public class RuleParser {
private static final Map<String, String> operatorMap = new HashMap<>();
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
operatorMap.put("all", "&&");
operatorMap.put("any", "||");
operatorMap.put("ge", ">=");
operatorMap.put("gt", ">");
operatorMap.put("eq", "==");
operatorMap.put("ne", "!=");
operatorMap.put("le", "<=");
operatorMap.put("lt", "<");
}
/**
* 解析規則字符串,轉換成表達式形式
* 示例:
* 輸入:
* { "any": [
* { "all": [
* { "ge": ["A", 10] },
* { "eq": ["B", 20] }
* ]},
* { "lt": ["C", 30] },
* { "ne": ["D", 50] }
* ]}
*
* 輸出:
* ( A >= 10 && B == 20 ) || ( C < 30 ) || ( D != 50 )
* @param rule 規則的json字符串形式
* @return 返回 bool 表達式
* @throws IOException 解析json字符串異常
*/
public static String parse(String rule) throws IOException {
JsonNode jsonNode = objectMapper.readTree(rule);
return parse(jsonNode);
}
/**
* 解析規則節點,轉換成表達式形式
* @param node Jackson Node
* @return 返回bool表達式
*/
private static String parse(JsonNode node) {
// TODO: 支持變量的 ”arg.“ 前綴定義
if (node.isObject()) {
Iterator<Map.Entry<String, JsonNode>> it = node.fields();
if(it.hasNext()){
Map.Entry<String, JsonNode> entry = it.next();
List<String> arrayList = new ArrayList<>();
for (JsonNode jsonNode : entry.getValue()) {
arrayList.add(parse(jsonNode));
}
return "(" + String.join(" " + operatorMap.get(entry.getKey()) + " ", arrayList) + ")";
} else {
// 兼容空節點:例如 {"all": [{}, "eq":{"A","1"}]}
return " 1==1";
}
} else if (node.isValueNode()) {
return node.asText();
}
return "";
}
結語
以上就是本文要闡述的全部內容,對於這個話題,如果你有這方面的經驗或者更好的方案,也請多多指教,謝謝!