• 【规则引擎】node规则引擎 json-rule-engine 学习记录


    一、规则引擎介绍

    1、定义

    复杂业务开发时,常常有复杂的判断逻辑,长期版本开发迭代后,程序本身逻辑代码和业务代码互相嵌套、错综复杂,同时维护成本高,可拓展性差
    规则引擎即是:可降低复杂业务逻辑组件复杂性、降低应用程序的维护和可扩展性成本的组件!
    在这里插入图片描述
    规则引擎实际上就是一个推理引擎,用于匹配facts(事实,我们可以理解为输入数据)和rules(规则),以推出结论。

    2、为什么要使用?

    业务规则经常变化,系统需依据业务的变化,实现快速、低成本的迭代更新。
    为了快速、低成本的更新,我们需将逻辑代码和业务代码进行解耦,同时可以结合也谢配置界面进行快速的低代码能力配置:

    • 维护成本低: 研发人员(不需懂业务)开发维护程序部分,同时测试通过后,后续不会经常变化改动:
    • 拓展性高:业务人员可直接管理这些业务规则,同时不需要研发人员的参与。

    3、规则引擎调研

    参考外部文章:

    https://juejin.cn/post/6972707259856093221

    二、Node规则引擎 json-rule-engine

    1、Engine

    存储并执行规则、发出事件和维护状态

    1.1)方法:

    构造、添加/删除 Fact、添加/删除/更新规则、添加/移除操作符、执行运行、停止执行;

    2、Facts

    通常是被注册在引擎中的方法或常量,在引擎运行时会被规则条件引用到。每个Fact方法应该是一个纯函数,它可以返回计算值,也可以返回解析为计算值的Promise。
    当规则在运行时计算时,会将fact值动态取回并且使用规则操作符跟规则值进行比较计算。

    2.1)方法

    构造;

    3、Rules

    规则包含一系列规则条件和一个规则事件。规则执行时,所有的规则条件都会计算。如果结果是符合的,规则事件将被触发。

    3.1)方法:

    构造、设置/获取规则、设置/获取规则事件、设置/获取优先级定义、转换为json格式

    3.2)规则条件:

    规则条件是因数(fact)、操作符、规则比较值的集合,这些信息决定了运行后是规则结果是成功还是失败。

    3.2.1 规则辅助:params

    有些时候因数需要额外的一些输入来进行计算。出于这个目的,params属性会作为一个参数传递到因数handler。Params 本质上起到因子参数的作用,使因子handler更加通用和可重用。

    3.2.2 规则辅助:path

    主要为了解决复杂类类型的属性规则值读取问题,可以通过 json-path 语法来给定一个获取fact类属性值的方式。

    3.2.3 规则辅助:自定义 path 解析器

    为了使用自定义的路径解析器而不是json-path提供的解析器, pathResolver回调函数选项可以传递给engine。在执行期间,当遇到 path 属性路径的时候,这个pathResolver回调函数会被调用。
    相比于默认的path解析器优势:如果简单的对象遍历 DSL 提供的性能比 json-path 提供的高级表达式更好,那么这个特性可能会很有用。它还可以用于利用比 json-path 提供更高级功能的更复杂的 DSL (例如 jsonata)。

    3.2.4 比较因子

    有时需要比较一个因子和另一个因子。这个时候可以通过在value属性里面嵌入第二个value属性来实现。第二个因子可以访问相同的param和path属性信息。

    3.3)事件:

    监听规则执行之后的 success 和 failure 事件。

    3.4)操作符:

    常见系统自带的比较操作符

    3.4.1 String和Numeric操作符:

    equal、notEqual (使用严格相等比较: 相等 ===、不相等!==

    3.4.2 Numeric操作符:

    lessThan(小于)、lessThanInclusive(小于等于)、greaterThan(大于)、greaterThanInclusive(大于等于)

    3.4.3 Array 操作符:

    in(fact包含在value数组中)、notIn(fact不包含在value数组中)、contains(fact 数组包含value)、doesNotContain(fact数组不包含value)

    3.5)规则结果:

    规则执行以后,规则结果对象会被提供给successfailure事件。此参数类似于常规规则,并包含关于如何计算规则的其他元数据。
    规则结果可用于提取单个条件的结果、计算的fact值和布尔逻辑结果。 name属性可以用来方便地区分给定的规则。

    3.6)持久化:

    规则可以很容易地转换为 JSON 并保存到数据库、文件系统或其他地方。要将规则转换为 JSON,只需调用rule.toJSON ()方法。
    之后,可以通过将 json 提供给 Rule 构造函数来还原规则。
    备注: fact方法无法持久化存储原因:是一个设计的feature,具体原因:
    1、根据定义,事实是为您的应用程序定制的业务逻辑,因此不在此库的范围之内;
    2、很多时候,这个请求表明了一种设计气味; 试着想想其他的方法来组成规则和事实来完成同样的目标;
    3、持久化事实方法将涉及到序列化 javascript 代码,并在之后通过 eval ()恢复它。

    4、Almanac

    在引擎运行周期内,一个Almanac搜集了fact的信息。当引擎计算fact值时,结果存储在almanac中并缓存。如果引擎检测到一个fact的计算已经被前置计算过,它就会直接使用almanac中缓存过的值信息。每次engine.run()被调用,一个新的almanac就被初始化。

    当前引擎的almanac可以在fact计算方法和引擎的success方法中获取到。almanac可以被用于在运行期间定义额外的fact。

    4.1)方法

    almanac.factValue(Fact fact, Object params, String path) -> Promise
    计算提供的fact + params信息。如果path被提供了,它会在json-path中被使用;
    almanac.addRuntimeFact(String factId, Mixed value)
    设置一系列运行中途的常量fact。经常在引擎事件触发时使用;
    almanac.getEvents(String outcome) -> Events[]
    获取当前引擎的输出事件;
    almanac.getResults() -> RuleResults[]
    获取当前引擎执行的规则结果数据信息;

    三、常用的用例demo

    json-rules-engine框架本身提供了一下demo项,参见: https://github.com/CacheControl/json-rules-engine/tree/master/examples

    补充一些官方demo没有的内容:

    场景用例:

    用例一: 判断数组对象中是否 包含/不包含 字段为某个值

    const engine = new Engine();
    
        const sceneRule = new Rule(
            {
                name: 'scene_rule_does_not_contain',
                conditions: {
                    all: [
                        {
                            fact: 'store',
                            path: '$.book[*].category',
                            operator: 'doesNotContain',
                            value: 'reference',
                        }
                    ]
                },
                event: {
                    type: 'scene_rule_does_not_contain',
                    params: {
                        data: '这里是一些payload数据'
                    }
                }
            }
        );
        const sceneRuleContain = new Rule(
            {
                name: 'scene_rule_does_contains',
                conditions: {
                    all: [
                        {
                            fact: 'store',
                            path: '$.book[*].category',
                            operator: 'contains',
                            value: 'reference',
                        }
                    ]
                },
                event: {
                    type: 'scene_rule_does_contains',
                    params: {
                        data: '这里是一些payload数据'
                    }
                }
            }
        );
    
        engine.addRule(sceneRule);
        engine.addRule(sceneRuleContain);
    
        const sceneFact = {
            "store": {
                "book": [
                    {
                        "category": "science",
                        "author": "Nigel Rees",
                        "title": "Sayings of the Century",
                        "price": 8.95
                    },
                    {
                        "category": "fiction",
                        "author": "Evelyn Waugh",
                        "title": "Sword of Honour",
                        "price": 12.99
                    },
                    {
                        "category": "fiction",
                        "author": "Herman Melville",
                        "title": "Moby Dick",
                        "isbn": "0-553-21311-3",
                        "price": 8.99
                    },
                    {
                        "category": "fiction",
                        "author": "J. R. R. Tolkien",
                        "title": "The Lord of the Rings",
                        "isbn": "0-395-19395-8",
                        "price": 22.99
                    }
                ],
                "bicycle": {
                    "color": "red",
                    "price": 19.95
                }
            }
        };
        const {events: eventsContain, almanac: almanacContain} = await engine.run(sceneFact);
        console.log(eventsContain);
    
        const sceneFactContain = {
            "store": {
                "book": [
                    {
                        "category": "reference",
                        "author": "Nigel Rees",
                        "title": "Sayings of the Century",
                        "price": 8.95
                    },
                    {
                        "category": "fiction",
                        "author": "Evelyn Waugh",
                        "title": "Sword of Honour",
                        "price": 12.99
                    },
                    {
                        "category": "fiction",
                        "author": "Herman Melville",
                        "title": "Moby Dick",
                        "isbn": "0-553-21311-3",
                        "price": 8.99
                    },
                    {
                        "category": "fiction",
                        "author": "J. R. R. Tolkien",
                        "title": "The Lord of the Rings",
                        "isbn": "0-395-19395-8",
                        "price": 22.99
                    }
                ],
                "bicycle": {
                    "color": "red",
                    "price": 19.95
                }
            }
        };
        const {events: eventNotContain, almanac: almanacNotContain} = await engine.run(sceneFactContain);
        console.log(eventNotContain);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125

    用例二: 判断数组对象中是否字段 全部是/全部不是 某个值

    const engine = new Engine();
        const sceneRule = new Rule(
            {
                name: 'scene_rule_all_is',
                conditions: {
                    all: [
                        {
                            fact: 'store',
                            path: '$.book[?(@.category !== "reference")]',
                            operator: 'equal',
                            value: undefined
                        }
                    ]
                },
                event: {
                    type: 'scene_rule_all_is',
                    params: {
                        data: 'scene_rule_all_is'
                    }
                }
            }
        );
        const sceneRuleContain = new Rule(
            {
                name: 'scene_rule_all_not',
                conditions: {
                    all: [
                        {
                            fact: 'store',
                            path: '$.book[?(@.category !== "reference")]',
                            operator: 'notEqual',
                            value: undefined,
                        }
                    ]
                },
                event: {
                    type: 'scene_rule_all_not',
                    params: {
                        data: 'scene_rule_all_not'
                    }
                }
            }
        );
    
        engine.addRule(sceneRule);
        engine.addRule(sceneRuleContain);
    
        const sceneFact = {
            "store": {
                "book": [
                    {
                        "category": "reference",
                        "author": "Nigel Rees",
                        "title": "Sayings of the Century",
                        "price": 8.95
                    },
                    {
                        "category": "reference",
                        "author": "Evelyn Waugh",
                        "title": "Sword of Honour",
                        "price": 12.99
                    },
                    {
                        "category": "reference",
                        "author": "Herman Melville",
                        "title": "Moby Dick",
                        "isbn": "0-553-21311-3",
                        "price": 8.99
                    },
                    {
                        "category": "reference",
                        "author": "J. R. R. Tolkien",
                        "title": "The Lord of the Rings",
                        "isbn": "0-395-19395-8",
                        "price": 22.99
                    }
                ],
                "bicycle": {
                    "color": "red",
                    "price": 19.95
                }
            }
        };
        const {events: eventsContain, almanac: almanacContain} = await engine.run(sceneFact);
        console.log(eventsContain);
    
        const sceneFactContain = {
            "store": {
                "book": [
                    {
                        "category": "reference",
                        "author": "Nigel Rees",
                        "title": "Sayings of the Century",
                        "price": 8.95
                    },
                    {
                        "category": "fiction",
                        "author": "Evelyn Waugh",
                        "title": "Sword of Honour",
                        "price": 12.99
                    },
                    {
                        "category": "fiction",
                        "author": "Herman Melville",
                        "title": "Moby Dick",
                        "isbn": "0-553-21311-3",
                        "price": 8.99
                    },
                    {
                        "category": "fiction",
                        "author": "J. R. R. Tolkien",
                        "title": "The Lord of the Rings",
                        "isbn": "0-395-19395-8",
                        "price": 22.99
                    }
                ],
                "bicycle": {
                    "color": "red",
                    "price": 19.95
                }
            }
        };
        const {events: eventNotContain, almanac: almanacNotContain} = await engine.run(sceneFactContain);
        console.log(eventNotContain);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124

    用例三: 判断数组为空或不为空

    const engine = new Engine();
        const sceneRuleEmpty = new Rule(
            {
                name: 'scene_rule_empty',
                conditions: {
                    all: [
                        {
                            fact: 'store',
                            path: '$.book.length',
                            operator: 'equal',
                            value: 0
                        }
                    ]
                },
                event: {
                    type: 'scene_rule_empty',
                    params: {
                        data: 'scene_rule_empty'
                    }
                }
            }
        );
        const sceneRuleNotEmpty = new Rule(
            {
                name: 'scene_rule_not_empty',
                conditions: {
                    all: [
                        {
                            fact: 'store',
                            path: '$.book.length',
                            operator: 'notEqual',
                            value: 0,
                        }
                    ]
                },
                event: {
                    type: 'scene_rule_not_empty',
                    params: {
                        data: 'scene_rule_not_empty'
                    }
                }
            }
        );
    
        engine.addRule(sceneRuleEmpty);
        engine.addRule(sceneRuleNotEmpty);
    
        const sceneFact = {
            "store": {
                "book": [],
                "bicycle": {
                    "color": "red",
                    "price": 19.95
                }
            }
        };
        const {events: eventsContain, almanac: almanacContain} = await engine.run(sceneFact);
        console.log(eventsContain);
    
        const sceneFactContain = {
            "store": {
                "book": [
                    {
                        "category": "reference",
                        "author": "Nigel Rees",
                        "title": "Sayings of the Century",
                        "price": 8.95
                    }
                ],
                "bicycle": {
                    "color": "red",
                    "price": 19.95
                }
            }
        };
        const {events: eventNotContain, almanac: almanacNotContain} = await engine.run(sceneFactContain);
        console.log(eventNotContain);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    四、规则引擎实践

    1、背景介绍:

    巡检项目需求,需要执行 (1)取数据 -> (2)业务规则判断出结果 -> (3)结果告知/预警 的主业务链路逻辑。
    其中: (2)业务规则判断出结果 步骤中,规则部分变更较为频繁,且不同的巡检指标处理时,各类规则可能差异较大,但是规则判断基本均为各类常见的if else语句,为了可拓展性与维护性方面考虑,引入规则引擎进行处理。项目由于使用技术为node技术,采用node规则引擎json-rule-engine来做技术实现。

    2、主链路设计:

    在这里插入图片描述

    3、模块代码实现

    const {Engine} = require("json-rules-engine");
    const {isEmpty, has} = require("lodash");
    
    /**
     * @desc 规则引擎服务
     */
    class RuleEngineService extends Service {
        constructor(ctx) {
            super(ctx);
    
            this.ruleEngine = new Engine();
            this.initCustomOperator();
        }
    
        /**
         * 注册自定的规则比较符
         */
        initCustomOperator() {
        	// TODO 这里添加自定义的判断操作符到规则引擎中
        }
    
        /**
         * 添加规则到规则引擎
         * @param ruleSetAry
         */
        addRule(ruleSetAry) {
            for (let rule of ruleSetAry) {
                this.ruleEngine.addRule(rule);
            }
        }
    
        /**
         * 移除规则引擎的指定规则
         * @param ruleSetAry
         */
        removeRule(ruleSetAry) {
            for (let rule of ruleSetAry) {
                this.ruleEngine.removeRule(rule);
            }
        }
    
        /**
         * 执行规则
         * @param ruleSetAry 规则集数组
         * @param factAry 规则因子数组
         */
        async exec(ruleSetAry, factAry) {
    
            let res = [];
    
            // 0、判空处理
            if (isEmpty(ruleSetAry) || isEmpty(factAry)) {
                return res;
            }
    
            // 1、引擎建立规则
            this.addRule(ruleSetAry);
            try {
                // 2、规则执行
                for (let fact of factAry) {
                    let factRuleResAry = [];
                    // 2.1 规则执行
                    try {
                        const {events, almanac} = await this.ruleEngine.run(fact);
    
                        factRuleResAry.push(events.map(({type, params}) => {
                            if (has(params, '_advice_parse_func') && !isEmpty(params._advice_parse_func)) {
                                const parser = new Function('fact', params._advice_parse_func);
                                params.advice.tip = parser(fact);
                            }
                            // 注意:如果满足多个条件,则这里会有多条记录信息
                            return {
                                rule_type: type,
                                rule_payload: params
                            };
                        }));
    
                    } finally {
                        // 2.2 清理执行完成的fact数据
                        for (let field in fact) {
                            this.ruleEngine.removeFact(field);
                        }
                    }
                    // 2.3 塞到结果的定义队列里面
                    res.push(
                        {
                            fact: fact,
                            ruleResult: factRuleResAry
                        }
                    );
                }
            } finally {
                // 3、清理规则
                this.removeRule(ruleSetAry);
            }
            return res;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
  • 相关阅读:
    Windows C++ 环境下 eigen、osqp、osqp-eigen安装教程
    权限系统(前后端分离)
    qt基础之d_ptr和q_ptr
    建站系列(三)--- 网络协议
    日志管理:Syslog日志采集
    o.s.web.servlet.PageNotFound : No mapping for GET /favicon.ico
    C/C++新手看过来----新手问题汇总分析
    mybatis中使用<choose><when><otherwise>标签实现根据条件查询不同sql
    跨域问题-笔记
    C++ 基于boost.asio封装的日志库,支持输出到文件、控制台和syslog远程日志,支持变长参数的日志输出
  • 原文地址:https://blog.csdn.net/Harvay/article/details/127734250