實(shí)現(xiàn)一個(gè)規(guī)則引擎的可視化具體方案
在介紹這個(gè)方案之前,得先簡(jiǎn)單了解一下什么是規(guī)則引擎
什么是規(guī)則引擎?
簡(jiǎn)單的說(shuō),規(guī)則引擎所負(fù)責(zé)的事情就是:判定某個(gè)數(shù)據(jù)或者對(duì)象是否滿足某個(gè)條件,然后根據(jù)判定結(jié)果,執(zhí)行不同的動(dòng)作。例如:
對(duì)于剛剛在網(wǎng)站上完成購(gòu)物的一個(gè)用戶(對(duì)象),如果她是 "女性用戶 并且 (連續(xù)登錄天數(shù)大于10天 或者 訂單金額大于200元 )" (條件) , 那么系統(tǒng)就自動(dòng)給該用戶發(fā)放一張優(yōu)惠券(動(dòng)作)。
在上面的場(chǎng)景中,規(guī)則引擎最重要的一個(gè)優(yōu)勢(shì)就是實(shí)現(xiàn)“條件“表達(dá)式的配置化。如果條件表達(dá)式不能配置,那么就需要程序員在代碼里面寫(xiě)死各種if...else... ,如果條件組合特別復(fù)雜的話,代碼就會(huì)很難維護(hù);同時(shí),如果不能配置化,那么每次條件的細(xì)微變更,就需要修改代碼,然后通過(guò)運(yùn)維走發(fā)布流程,無(wú)法快速響應(yīng)業(yè)務(wù)的需求。
在groovy腳本的方案中,上面的場(chǎng)景可以這么實(shí)現(xiàn):
1)定義一個(gè)groovy腳本:[code]
def validateCondition(args){return args.用戶性別 == "女性" && (args.連續(xù)登錄天數(shù)>10 || args.訂單金額 > 200);}
2)通過(guò)Java提供的 ScriptEngineManager 對(duì)象去執(zhí)行
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy</artifactId> <version>3.0.7</version> </dependency>
/* * * @params condition 從數(shù)據(jù)庫(kù)中讀出來(lái)的條件表達(dá)式 */ private Boolean validateCondition(String condition){ //實(shí)際使用上,ScriptEngineManager可以定義為單例 ScriptEngineManager engineManager = new ScriptEngineManager(); ScriptEngine engine = engineManager.getEngineByName(scriptLang); Map<String, Object> args = new HashMap<>(); data.put("用戶性別", "女性"); data.put("連續(xù)登錄天數(shù)", 11); data.put("訂單金額", 220); engine.eval(script); return ((Invocable) engine).invokeFunction("validateCondition", args); }
在上面的groovy腳本中,經(jīng)常需要變動(dòng)的部分就是 ”args.用戶性別 == "女性" && (args.連續(xù)登錄天數(shù)>10 || args.訂單金額 > 200)“ 這個(gè)表達(dá)式,一個(gè)最簡(jiǎn)單的方案,就是在后臺(tái)界面提供一個(gè)文本框,在文本框中錄入整個(gè)groovy腳本,然后保存到數(shù)據(jù)庫(kù)。但是這種方案有個(gè)缺點(diǎn):表達(dá)式的定義有一定門檻。對(duì)于程序員來(lái)說(shuō),這自然是很簡(jiǎn)單的事,但是對(duì)于沒(méi)接觸過(guò)編程的業(yè)務(wù)人員,就有一定的門檻了,很容易錄入錯(cuò)誤的表達(dá)式。這就引出了本文的另一個(gè)話題,如何實(shí)現(xiàn)bool表達(dá)式的可視化編輯?
如何實(shí)現(xiàn)bool表達(dá)式的可視化編輯?
一種方案就是對(duì)于一個(gè)指定的表達(dá)式,前端人員進(jìn)行語(yǔ)法解析,然后渲染成界面,業(yè)務(wù)人員編輯之后,再將界面元素結(jié)構(gòu)轉(zhuǎn)換成表達(dá)式。然而,直接解析語(yǔ)法有兩個(gè)確定:
- 1)需要考慮的邊界條件比較多,一不小心就解析出錯(cuò)。
- 2)而且也限定了后端可以選用的腳本語(yǔ)言。例如,在上面的方案中選用的是groovy,它使用的"與"運(yùn)算符是 && , 假如某天有一種性能更好的腳本語(yǔ)言,它的"與"運(yùn)算符定位為 and ,那么就會(huì)需要修改很多表達(dá)式解析的地方。
另一種方案,是定義一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)描述表達(dá)式的結(jié)構(gòu)(說(shuō)了這么多,終于來(lái)到重點(diǎn)了):
{ "all": [ { "any": [ { "gl": ["連續(xù)登錄天數(shù)", 10] }, { "gl": ["訂單金額", 200] } ]}, { "eq": ["用戶性別", "女性"] } ]}
然后,使用遞歸的方式解析該結(jié)構(gòu),對(duì)于前端開(kāi)發(fā),可以在遞歸解析的過(guò)程中渲染成對(duì)應(yīng)的界面元素;對(duì)于后端人員,可以生成對(duì)應(yīng)的bool表達(dá)式,有了bool表達(dá)式,就可以使用預(yù)定的腳本模板,生成最終的規(guī)則。
// 模板的例子 def validateCondition(args){return $s;}
/** * 動(dòng)態(tài)bool表達(dá)式解析器 */ 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", "<"); } /** * 解析規(guī)則字符串,轉(zhuǎn)換成表達(dá)式形式 * 示例: * 輸入: * { "any": [ * { "all": [ * { "ge": ["A", 10] }, * { "eq": ["B", 20] } * ]}, * { "lt": ["C", 30] }, * { "ne": ["D", 50] } * ]} * * 輸出: * ( A >= 10 && B == 20 ) || ( C < 30 ) || ( D != 50 ) * @param rule 規(guī)則的json字符串形式 * @return 返回 bool 表達(dá)式 * @throws IOException 解析json字符串異常 */ public static String parse(String rule) throws IOException { JsonNode jsonNode = objectMapper.readTree(rule); return parse(jsonNode); } /** * 解析規(guī)則節(jié)點(diǎn),轉(zhuǎn)換成表達(dá)式形式 * @param node Jackson Node * @return 返回bool表達(dá)式 */ 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 { // 兼容空節(jié)點(diǎn):例如 {"all": [{}, "eq":{"A","1"}]} return " 1==1"; } } else if (node.isValueNode()) { return node.asText(); } return ""; }
以上就是本文要闡述的全部?jī)?nèi)容,對(duì)于這個(gè)話題,如果你有這方面的經(jīng)驗(yàn)或者更好的方案,也請(qǐng)多多指教,謝謝!
到此這篇關(guān)于如何實(shí)現(xiàn)一個(gè)規(guī)則引擎的可視化的文章就介紹到這了,希望對(duì)你有幫助,更多相關(guān)規(guī)則引擎可視化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot 如何設(shè)置啟動(dòng)內(nèi)存
這篇文章主要介紹了Springboot 如何設(shè)置啟動(dòng)內(nèi)存,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02解決SpringBoot webSocket 資源無(wú)法加載、tomcat啟動(dòng)報(bào)錯(cuò)的問(wèn)題
這篇文章主要介紹了解決SpringBoot webSocket 資源無(wú)法加載、tomcat啟動(dòng)報(bào)錯(cuò)的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11使用Springboot自定義注解,支持SPEL表達(dá)式
這篇文章主要介紹了使用Springboot自定義注解,支持SPEL表達(dá)式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02Java多線程生產(chǎn)者消費(fèi)者模式實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了Java多線程生產(chǎn)者消費(fèi)者模式實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03SpringBoot在一定時(shí)間內(nèi)限制接口請(qǐng)求次數(shù)的實(shí)現(xiàn)示例
在項(xiàng)目中,接口的暴露在外面,很多人就會(huì)惡意多次快速請(qǐng)求,本文主要介紹了SpringBoot在一定時(shí)間內(nèi)限制接口請(qǐng)求次數(shù)的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2022-03-03mybatis plus 自動(dòng)轉(zhuǎn)駝峰配置小結(jié)
SpringBoot提供兩種配置Mybatis的方式,第一種是通過(guò)yml或application.properties文件開(kāi)啟配置,第二種是使用自定義配置類,通過(guò)給容器添加一個(gè)ConfigurationCustomizer來(lái)實(shí)現(xiàn)更靈活的配置,這兩種方法可以根據(jù)項(xiàng)目需求和個(gè)人喜好選擇使用2024-10-10