EasyRules規(guī)則引擎工具類(lèi)

EasyRules是一款基于Java的開(kāi)源的輕量級(jí)的規(guī)則引擎框架。它可以幫助開(kāi)發(fā)人員快速開(kāi)發(fā)并管理規(guī)則,實(shí)現(xiàn)應(yīng)用程序的自動(dòng)化決策。EasyRules框架非常易于使用,且可以與任何Java應(yīng)用程序無(wú)縫集成。在本文中,我們將對(duì)其進(jìn)行一個(gè)簡(jiǎn)單的封裝,以實(shí)現(xiàn)復(fù)雜的規(guī)則表達(dá)式匹配。

一、EasyRules的基本概念


EasyRules是一個(gè)基于規(guī)則的引擎,它基于規(guī)則引擎的常見(jiàn)原則和概念。以下是一些EasyRules框架中的重要概念:

  • 規(guī)則(Rule):規(guī)則是EasyRules框架中的核心概念,它用于描述應(yīng)用程序中需要遵循的規(guī)則。每個(gè)規(guī)則通常包含兩個(gè)部分:規(guī)則名稱(chēng)和規(guī)則條件。
  • 規(guī)則條件(Condition):規(guī)則條件定義了規(guī)則的前提條件。如果規(guī)則條件為true,則規(guī)則將被觸發(fā)執(zhí)行。否則,規(guī)則將被忽略。
  • 規(guī)則動(dòng)作(Action):規(guī)則動(dòng)作是在規(guī)則被觸發(fā)時(shí)執(zhí)行的一段代碼。它可以用于實(shí)現(xiàn)各種應(yīng)用程序邏輯,例如更新數(shù)據(jù)、發(fā)送消息等。
  • 規(guī)則執(zhí)行(Rule Engine):規(guī)則執(zhí)行是EasyRules框架的核心功能之一,它負(fù)責(zé)解析規(guī)則條件,并根據(jù)條件執(zhí)行相應(yīng)的規(guī)則動(dòng)作。

二、快速上手Demo


了解了easyRules的基本概念后,我們來(lái)寫(xiě)一個(gè)簡(jiǎn)單的demo:

假設(shè)我們有一個(gè)需求,根據(jù)用戶(hù)的年齡來(lái)決定是否可以購(gòu)買(mǎi)酒類(lèi)產(chǎn)品,如果用戶(hù)年齡小于18歲,則不能購(gòu)買(mǎi)酒類(lèi)產(chǎn)品。

首先,我們需要定義一個(gè)規(guī)則類(lèi),繼承自org.jeasy.rules.annotation.Rule,如下所示:

import org.jeasy.rules.annotation.Condition;
import org.jeasy.rules.annotation.Fact;
import org.jeasy.rules.annotation.Rule;

@Rule(name = "age rule", description = "Check if user is of legal age to buy alcohol")
public class AgeRule {

    @Condition
    public boolean checkAge(@Fact("age") int age) {
        return age >= 18;
    }
}

在這個(gè)規(guī)則類(lèi)中,我們定義了一個(gè)名為“checkAge”的條件方法,它接受一個(gè)名為“age”的事實(shí)參數(shù),并返回一個(gè)布爾值表示用戶(hù)是否滿(mǎn)足購(gòu)買(mǎi)酒類(lèi)產(chǎn)品的年齡要求。

接下來(lái),我們需要?jiǎng)?chuàng)建一個(gè)規(guī)則引擎實(shí)例,并將規(guī)則類(lèi)添加到規(guī)則引擎中。代碼如下所示:

import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rules;
import org.jeasy.rules.core.RulesImpl;
import org.jeasy.rules.core.DefaultRulesEngine;

public class RuleEngineDemo {
    public static void main(String[] args) {
        AgeRule ageRule = new AgeRule();
        Rules rules = new RulesImpl();
        rules.register(ageRule);
        DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
        Facts facts = new Facts();
        facts.put("age", 20);
        rulesEngine.fire(rules, facts);
    }
}

在這個(gè)示例代碼中,我們創(chuàng)建了一個(gè)名為“ageRule”的規(guī)則對(duì)象,并將其注冊(cè)到名為“rules”的規(guī)則集合中。接著,我們創(chuàng)建了一個(gè)默認(rèn)的規(guī)則引擎實(shí)例,并創(chuàng)建了一個(gè)名為“facts”的事實(shí)對(duì)象,并將“age”和“20”作為鍵值對(duì)添加到事實(shí)對(duì)象中。最后,我們通過(guò)調(diào)用規(guī)則引擎的fire方法來(lái)啟動(dòng)規(guī)則引擎并觸發(fā)規(guī)則執(zhí)行。

三、easyRules工具類(lèi)


以上示例展示了如何快速的使用easyRule實(shí)現(xiàn)規(guī)則創(chuàng)建及調(diào)用的流程。但是如果我們有更復(fù)雜的場(chǎng)景,比如在數(shù)據(jù)質(zhì)量規(guī)則校驗(yàn)中,我們有如下的規(guī)則判斷:

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-XJC0OufR-1685683113114)(/Users/casey/Library/Application Support/typora-user-images/image-20230329152414895.png)]

對(duì)于這種比較復(fù)雜且校驗(yàn)規(guī)則經(jīng)常會(huì)發(fā)生變化的規(guī)則,通過(guò)上述代碼就無(wú)法實(shí)現(xiàn)了,為此,我將對(duì)easyRule做進(jìn)一步的封裝,以達(dá)到此目的。

1. 定義常量類(lèi)

首先定義一個(gè)常量類(lèi)

package com.shsc.bigdata.indicator.monitor.common.constant;

public class EasyRulesConstants {

    // 事實(shí)別名
    public static final String FACT_ALIAS = "fact";
    // 結(jié)果別名
    public static final String RESULT_ALIAS = "result";
    // and關(guān)系
    public static final String RELATION_AND = "and";
    // or關(guān)系
    public static final String RELATION_OR = "or";
    // 匹配成功信息
    public static final String MATCH_SUCCESS_MESSAGE = "匹配成功";
    public static final String FIELD_TYPE = "type";
    public static final String FIELD_OPERATOR = "operator";
    public static final String FIELD_NAME = "metricName";
    public static final String FIELD_VALUE = "value";
    public static final String FIELD_CHILDREN = "children";
    public static final String EXPRESSION_TYPE = "EXPRESSION";
    public static final String RELATION_TYPE = "RELATION";
    public static final String LEFT_BRACKETS = "(";
    public static final String RIGHT_BRACKETS = ")";
    public static final String SYMBOL_SPACE = " ";
    public static final String SYMBOL_EMPTY = "";
    public static final String LOGICAL_AND = "&&";
    public static final String LOGICAL_OR = "||";
}

2. 定義枚舉類(lèi)

定義一個(gè)枚舉類(lèi),羅列出常用的運(yùn)算符

package com.shsc.bigdata.indicator.monitor.common.enums;

public enum EasyRulesOperation {
    GREATER_THAN("GREATER_THAN", "%s > %s", "大于"),
    GREATER_THAN_EQUAL("GREATER_THAN_EQUAL", "%s >= %s", "大于等于"),
    LESS_THAN("LESS_THAN", "%s < %s", "小于"),
    LESS_THAN_EQUAL("LESS_THAN_EQUAL", "%s <= %s", "小于等于"),
    EQUAL("EQUAL", "%s == %s", "等于"),
    UNEQUAL("UNEQUAL", "%s != %s", "不等于"),
    BETWEEN("BETWEEN", "%s >= %s && %s <= %s", "介于之間"),
    OUT_OF_RANGE("OUT_OF_RANGE", "%s >= %s || %s >= %s", "超出范圍"),
    CONTAINS("CONTAINS", "%s.contains(\"%s\")", "包含"),
    STARTSWITH("STARTSWITH", "%s.startsWith(\"%s\")", "前綴"),
    ENDSWITH("ENDSWITH", "%s.endsWith(\"%s\")", "后綴"),
    ;

    public static EasyRulesOperation getOperationByOperator(String operator) {
        EasyRulesOperation[] list = EasyRulesOperation.values();
        for (EasyRulesOperation item : list) {
            String compareOperator = item.getOperator();
            if (compareOperator.equals(operator)) {
                return item;
            }
        }
        return null;
    }

    private final String operator;
    private final String expression;
    private final String remark;

    EasyRulesOperation(String operator, String expression, String remark) {
        this.operator = operator;
        this.expression = expression;
        this.remark = remark;
    }

    public String getOperator() {
        return operator;
    }

    public String getExpression() {
        return expression;
    }

    public String getRemark() {
        return remark;
    }
}

3. 工具類(lèi)

package com.shsc.bigdata.indicator.monitor.common.utils.easyRules;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.shsc.bigdata.indicator.monitor.common.enums.EasyRulesOperation;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rules;
import org.jeasy.rules.api.RulesEngine;
import org.jeasy.rules.core.DefaultRulesEngine;
import org.jeasy.rules.mvel.MVELRule;
import static com.shsc.bigdata.indicator.monitor.common.constant.EasyRulesConstants.*;

@Slf4j
public class EasyRulesUtil {

    /**
     * 執(zhí)行規(guī)則匹配
     * @param fact 事實(shí)json
     * @param ruleModel 規(guī)則模型
     */
    public static RuleResult match(JSONObject fact, RuleModel ruleModel){
        // 結(jié)果
        RuleResult result = new RuleResult();
        result.setRuleId(ruleModel.getRuleId());
        // 規(guī)則實(shí)例
        Facts facts = new Facts();
        facts.put(FACT_ALIAS, fact);
        facts.put(RESULT_ALIAS, result);
        // 規(guī)則內(nèi)容
        org.jeasy.rules.api.Rule mvelrule = new MVELRule()
                .name(ruleModel.getRuleName())
                .description(ruleModel.getDescription())
                .when(ruleModel.getWhenExpression())
                .then(ruleModel.getThenExpression());
        // 規(guī)則集合
        Rules rules = new Rules();
        // 將規(guī)則添加到集合
        rules.register(mvelrule);
        // 創(chuàng)建規(guī)則執(zhí)行引擎,并執(zhí)行規(guī)則
        RulesEngine rulesEngine = new DefaultRulesEngine();
        rulesEngine.fire(rules, facts);
        return result;
    }

    /**
     * 構(gòu)建mvel條件表達(dá)式
     * @param json 節(jié)點(diǎn)json,例如:
     * {
     *     "type": "EXPRESSION",
     *     "operator": "LESS_THAN",
     *     "metricName": "NORMAL_NUMBER",
     *     "value": "11",
     *     "children": []
     * }
     */
    public static String buildWhenExpression(JSONObject json) {
        StringBuilder mvelExpression = new StringBuilder();
        String type = json.getString(FIELD_TYPE);
        String operator = json.getString(FIELD_OPERATOR);

        switch (type) {
            case EXPRESSION_TYPE:
                String fieldName = json.getString(FIELD_NAME);
                String fieldValue = json.getString(FIELD_VALUE);
                mvelExpression.append(buildOperatorExpress(operator, fieldName, fieldValue));
                break;
            case RELATION_TYPE:
                JSONArray children = json.getJSONArray(FIELD_CHILDREN);
                if (children.size() == 0) {
                    return SYMBOL_EMPTY;
                }
                operator = convertRelationExpress(operator);
                StringBuilder childrenExpression = new StringBuilder();
                for (int i = 0; i < children.size(); i++) {
                    JSONObject child = children.getJSONObject(i);
                    // 遞歸構(gòu)建單個(gè)規(guī)則條件
                    String childExpression = buildWhenExpression(child);
                    if (!childExpression.isEmpty()) {
                        if (childrenExpression.length() > 0) {
                            childrenExpression.append(SYMBOL_SPACE).append(operator).append(SYMBOL_SPACE);
                        }
                        childrenExpression.append(LEFT_BRACKETS).append(childExpression).append(RIGHT_BRACKETS);
                    }
                }
                mvelExpression.append(childrenExpression);
                break;
            default:
                break;
        }
        return mvelExpression.toString();
    }

    /**
     * 構(gòu)建mvel結(jié)果表達(dá)式
     */
    public static String buildThenExpression() {
        StringBuilder expression = new StringBuilder();
        expression.append(RESULT_ALIAS).append(".setValue(\"").append(MATCH_SUCCESS_MESSAGE).append("\");");
        log.info("thenExpression: {}", expression);
        return expression.toString();
    }

    /**
     * 轉(zhuǎn)換條件連接符
     * @param relation 條件連接符
     */
    private static String convertRelationExpress(String relation) {
        if (StringUtils.isEmpty(relation)){
            return SYMBOL_EMPTY;
        } else if(relation.equalsIgnoreCase(RELATION_AND)){
            return LOGICAL_AND;
        } else if(relation.equalsIgnoreCase(RELATION_OR)){
            return LOGICAL_OR;
        }
        return relation;
    }

    /**
     * 構(gòu)建mvel表達(dá)式
     * @param operator 操作符
     * @param fieldName 字段名稱(chēng)
     * @param value 字段值
     */
    private static String buildOperatorExpress(String operator, String fieldName, Object value) {
        EasyRulesOperation operation = EasyRulesOperation.getOperationByOperator(operator);
        if (ObjectUtils.isNotEmpty(operation)) {
            String expression = operation.getExpression();
            return String.format(expression, buildValueExpress(fieldName), value);
        }
        return SYMBOL_EMPTY;
    }

    /**
     * 構(gòu)建mvel取值表達(dá)式
     * @param fieldName 字段名稱(chēng)
     */
    private static String buildValueExpress(String fieldName) {
        return String.format("%s.get(\"%s\")", FACT_ALIAS, fieldName);
    }

     @Data
     public static class RuleModel {
        private String ruleId;
        String ruleName;
        String description;
        String whenExpression;
        String thenExpression;
    }

    @Data
    public static class RuleResult {
        // 規(guī)則主鍵
        private String ruleId;

        // 是否匹配, 默認(rèn)false
        boolean isMatch = false;

        // 匹配信息,默認(rèn)為匹配失敗
        String message = "匹配失敗";

        /**
         * 匹配成功后設(shè)置成功信息
         */
        public void setValue(String message){
            this.message = message;
            this.isMatch = true;
        }
    }
}

4. 測(cè)試


package com.shsc.bigdata.indicator.monitor.common;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.shsc.bigdata.indicator.monitor.common.utils.easyRules.EasyRulesUtil;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestEasyRules {
    public static void main(String[] args) {
        // 1. 新增規(guī)則
        EasyRulesUtil.RuleModel ruleModel = new EasyRulesUtil.RuleModel();
        ruleModel.setRuleId("1");
        ruleModel.setRuleName("rule1");
        ruleModel.setDescription("測(cè)試規(guī)則");
        // 2. 設(shè)置規(guī)則條件
        String ruleJson = "{\n" +
                "    \"validateCondition\": {\n" +
                "        \"type\": \"RELATION\",\n" +
                "        \"operator\": \"OR\",\n" +
                "        \"children\": [\n" +
                "            {\n" +
                "                \"type\": \"EXPRESSION\",\n" +
                "                \"operator\": \"LESS_THAN\",\n" +
                "                \"metricName\": \"NORMAL_NUMBER\",\n" +
                "                \"value\": \"11\",\n" +
                "                \"children\": []\n" +
                "            },\n" +
                "            {\n" +
                "                \"type\": \"EXPRESSION\",\n" +
                "                \"operator\": \"LESS_THAN_EQUAL\",\n" +
                "                \"metricName\": \"ERROR_NUMBER\",\n" +
                "                \"value\": \"11\",\n" +
                "                \"children\": []\n" +
                "            },\n" +
                "            {\n" +
                "                \"type\": \"RELATION\",\n" +
                "                \"children\": [\n" +
                "                    {\n" +
                "                        \"type\": \"EXPRESSION\",\n" +
                "                        \"metricName\": \"NORMAL_NUMBER\",\n" +
                "                        \"operator\": \"GREATER_THAN\",\n" +
                "                        \"value\": 10,\n" +
                "                        \"children\": []\n" +
                "                    },\n" +
                "                    {\n" +
                "                        \"type\": \"EXPRESSION\",\n" +
                "                        \"metricName\": \"ERROR_NUMBER\",\n" +
                "                        \"operator\": \"GREATER_THAN\",\n" +
                "                        \"value\": 100,\n" +
                "                        \"children\": []\n" +
                "                    },\n" +
                "                    {\n" +
                "                        \"type\": \"RELATION\",\n" +
                "                        \"children\": [\n" +
                "                            {\n" +
                "                                \"type\": \"EXPRESSION\",\n" +
                "                                \"metricName\": \"NORMAL_NUMBER\",\n" +
                "                                \"operator\": \"EQUAL\",\n" +
                "                                \"value\": 1,\n" +
                "                                \"children\": []\n" +
                "                            },\n" +
                "                            {\n" +
                "                                \"type\": \"EXPRESSION\",\n" +
                "                                \"metricName\": \"ERROR_NUMBER\",\n" +
                "                                \"operator\": \"EQUAL\",\n" +
                "                                \"value\": 1,\n" +
                "                                \"children \": []\n" +
                "                            }\n" +
                "                        ],\n" +
                "                        \"operator\": \"OR\"\n" +
                "                    }\n" +
                "                ],\n" +
                "                \"operator\": \"OR\"\n" +
                "            }\n" +
                "        ]\n" +
                "    }\n" +
                "}";
        JSONObject conditionJson = JSON.parseObject(ruleJson);
        // 3. 設(shè)置fact
        String whenExpression = EasyRulesUtil.buildWhenExpression(conditionJson.getJSONObject("validateCondition"));
        log.info("whenExpression:{}", whenExpression);
        ruleModel.setWhenExpression(whenExpression);
        // 4. 設(shè)置結(jié)果表達(dá)式
        ruleModel.setThenExpression(EasyRulesUtil.buildThenExpression());

        // 5. 設(shè)置匹配條件
        JSONObject json = new JSONObject();
        json.put("NORMAL_NUMBER", "10");
        json.put("ERROR_NUMBER", 10);
        json.put("省=陜西;市=西安;", 100);
        // 6. 調(diào)用規(guī)則匹配
        EasyRulesUtil.RuleResult result = EasyRulesUtil.match(json, ruleModel);
        System.out.println(result);
    }
}

上面的例子中,我使用了MVEL表達(dá)式語(yǔ)言實(shí)現(xiàn)了動(dòng)態(tài)規(guī)則,關(guān)于MVEL的知識(shí)請(qǐng)翻閱歷史文章查看。然后傳入了一個(gè)比較復(fù)雜的多層級(jí)的規(guī)則表達(dá)式,測(cè)試結(jié)果如下:

15:30:29.915 [main] INFO com.shsc.bigdata.indicator.monitor.common.EasyRules - whenExpression:(fact.get("NORMAL_NUMBER") < 11) || (fact.get("ERROR_NUMBER") <= 11) || ((fact.get("NORMAL_NUMBER") > 10) || (fact.get("ERROR_NUMBER") > 100) || ((fact.get("NORMAL_NUMBER") == 1) || (fact.get("ERROR_NUMBER") == 1)))
15:30:29.919 [main] INFO com.shsc.bigdata.indicator.monitor.common.utils.easyRules.EasyRulesUtil - thenExpression: result.setValue("匹配成功");
15:30:30.085 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Engine parameters { skipOnFirstAppliedRule = false, skipOnFirstNonTriggeredRule = false, skipOnFirstFailedRule = false, priorityThreshold = 2147483647 }
15:30:30.085 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Registered rules:
15:30:30.086 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule { name = 'rule1', description = '測(cè)試規(guī)則', priority = '2147483646'}
15:30:30.086 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Known facts:
15:30:30.086 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Fact{name='result', value=EasyRulesUtil.RuleResult(ruleId=1, isMatch=false, message=匹配失敗)}
15:30:30.086 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Fact{name='fact', value={"ERROR_NUMBER":10,"NORMAL_NUMBER":"10","省=陜西;市=西安;":100}}
15:30:30.096 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rules evaluation started
15:30:30.178 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule 'rule1' triggered
15:30:30.187 [main] DEBUG org.jeasy.rules.core.DefaultRulesEngine - Rule 'rule1' performed successfully
EasyRulesUtil.RuleResult(ruleId=1, isMatch=true, message=匹配成功)

從上述日志可以看到,isMatch=true表示匹配成功:

EasyRulesUtil.RuleResult(ruleId=1, isMatch=true, message=匹配成功)

四、寫(xiě)在最后


通過(guò)以上對(duì)easyRules的封裝,我們可以實(shí)現(xiàn)復(fù)雜的規(guī)則表達(dá)式校驗(yàn),只需要定義好表達(dá)式的json結(jié)構(gòu)以及匹配的條件即可。

PS:以上文章出自微信公眾號(hào)“毛毛小妖的筆記”。
查看更多原創(chuàng)內(nèi)容請(qǐng)?jiān)谖⑿殴娞?hào)搜索“毛毛小妖的筆記”即可關(guān)注。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容