<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一個規(guī)則引擎的可視化方案

          共 9125字,需瀏覽 19分鐘

           ·

          2021-04-22 13:31

          點擊上方藍色字體,選擇“標(biāo)星公眾號”

          優(yōu)質(zhì)文章,第一時間送達

            作者 |  Ronzy

          來源 |  urlify.cn/6367Fn

          背景

          最近有個新項目可能會用到規(guī)則引擎,所以花了些時間對相關(guān)技術(shù)做調(diào)研,在百度、google用“規(guī)則引擎”作為關(guān)鍵字進行搜索,可以找到很多關(guān)于這方面的資料,絕大部分都會提到 drools、urules、easy-rules等等這么些開源項目,有一些文章也提到他們是采用groovy腳本來實現(xiàn)的。通過對項目需求的評估,初步判定groovy腳本已經(jīng)可以滿足實際的場景。

          然而,在這些資料或者方案之中,除了urules,大部分只是關(guān)注框架的性能和使用上的簡便,很少探討如何讓業(yè)務(wù)人員可以自行進行規(guī)則定義的方案。而urules雖然自帶了可視化的規(guī)則管理界面,但是界面樣式不好自定義,無法跟現(xiàn)有后臺管理界面不突兀的融合。

          通過不斷嘗試變換關(guān)鍵字在搜索引擎搜索,最終在stackoverflow找到了一個探討這個問題的帖子,特此將帖子中提到的方案分享一下,如果你跟我一樣在研究同樣的問題,也許對你有用。不過在介紹這個方案之前,得先簡單了解一下什么是規(guī)則引擎

          什么是規(guī)則引擎?

          簡單的說,規(guī)則引擎所負(fù)責(zé)的事情就是:判定某個數(shù)據(jù)或者對象是否滿足某個條件,然后根據(jù)判定結(jié)果,執(zhí)行不同的動作。例如:

          對于剛剛在網(wǎng)站上完成購物的一個用戶(對象),如果她是 "女性用戶 并且 (連續(xù)登錄天數(shù)大于10天 或者 訂單金額大于200元 )" (條件) , 那么系統(tǒng)就自動給該用戶發(fā)放一張優(yōu)惠券(動作)。

          在上面的場景中,規(guī)則引擎最重要的一個優(yōu)勢就是實現(xiàn)“條件“表達式的配置化。如果條件表達式不能配置,那么就需要程序員在代碼里面寫死各種if...else... ,如果條件組合特別復(fù)雜的話,代碼就會很難維護;同時,如果不能配置化,那么每次條件的細(xì)微變更,就需要修改代碼,然后通過運維走發(fā)布流程,無法快速響應(yīng)業(yè)務(wù)的需求。

          在groovy腳本的方案中,上面的場景可以這么實現(xiàn):

          • 1)定義一個groovy腳本:

          def validateCondition(args){return args.用戶性別 == "女性" && (args.連續(xù)登錄天數(shù)>10 || args.訂單金額 > 200);}
          • 2)通過Java提供的 ScriptEngineManager 對象去執(zhí)行

              <dependency>
                <groupId>org.codehaus.groovy</groupId>
                <artifactId>groovy</artifactId>
                <version>3.0.7</version>
              </dependency>
          /*
           *
           * @params condition  從數(shù)據(jù)庫中讀出來的條件表達式
           */
          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("連續(xù)登錄天數(shù)", 11);
              data.put("訂單金額", 220);
              engine.eval(script);
              return ((Invocable) engine).invokeFunction(functionName, args);
          }

          在上面的groovy腳本中,經(jīng)常需要變動的部分就是 ”args.用戶性別 == "女性" && (args.連續(xù)登錄天數(shù)>10 || args.訂單金額 > 200)“ 這個表達式,一個最簡單的方案,就是在后臺界面提供一個文本框,在文本框中錄入整個groovy腳本,然后保存到數(shù)據(jù)庫。但是這種方案有個缺點:表達式的定義有一定門檻。對于程序員來說,這自然是很簡單的事,但是對于沒接觸過編程的業(yè)務(wù)人員,就有一定的門檻了,很容易錄入錯誤的表達式。這就引出了本文的另一個話題,如何實現(xiàn)bool表達式的可視化編輯?

          如何實現(xiàn)bool表達式的可視化編輯?

          一種方案就是對于一個指定的表達式,前端人員進行語法解析,然后渲染成界面,業(yè)務(wù)人員編輯之后,再將界面元素結(jié)構(gòu)轉(zhuǎn)換成表達式。然而,直接解析語法有兩個確定:

          • 1)需要考慮的邊界條件比較多,一不小心就解析出錯。

          • 2)而且也限定了后端可以選用的腳本語言。例如,在上面的方案中選用的是groovy,它使用的"與"運算符是 && , 假如某天有一種性能更好的腳本語言,它的"與"運算符定位為 and ,那么就會需要修改很多表達式解析的地方。

          另一種方案,是定義一個數(shù)據(jù)結(jié)構(gòu)來描述表達式的結(jié)構(gòu)(說了這么多,終于來到重點了):

              { "all": [
                  { "any": [
                      { "gl": ["連續(xù)登錄天數(shù)", 10] },
                      { "gl": ["訂單金額", 200] }
                  ]},
                  { "eq": ["用戶性別""女性"] }
              ]}

          然后,使用遞歸的方式解析該結(jié)構(gòu),對于前端開發(fā),可以在遞歸解析的過程中渲染成對應(yīng)的界面元素;對于后端人員,可以生成對應(yīng)的bool表達式,有了bool表達式,就可以使用預(yù)定的腳本模板,生成最終的規(guī)則。

          // 模板的例子
          def validateCondition(args){return $s;}
          /**
           * 動態(tài)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""<");
              }

              /**
               * 解析規(guī)則字符串,轉(zhuǎn)換成表達式形式
               * 示例:
               * 輸入:
               *    { "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 表達式
               * @throws IOException 解析json字符串異常
               */
              public static String parse(String rule) throws IOException {

                  JsonNode jsonNode = objectMapper.readTree(rule);
                  return parse(jsonNode);
              }

              /**
               * 解析規(guī)則節(jié)點,轉(zhuǎn)換成表達式形式
               * @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 {
                          // 兼容空節(jié)點:例如 {"all": [{}, "eq":{"A","1"}]}
                          return " 1==1";
                      }
                  } else if (node.isValueNode()) {
                      return node.asText();
                  }

                  return "";
              }

          結(jié)語

          以上就是本文要闡述的全部內(nèi)容,對于這個話題,如果你有這方面的經(jīng)驗或者更好的方案,也請多多指教,謝謝!





          粉絲福利:Java從入門到入土學(xué)習(xí)路線圖

          ??????

          ??長按上方微信二維碼 2 秒


          感謝點贊支持下哈 

          瀏覽 108
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  热99精品视频 | 日本A在线播放 | 日韩无码视频不卡 | 波多野结衣一区不卡 | 黄色视频图片免费看 |