快速入门规则引擎

规则引擎简介

背景

研究目的:网页活动项目中规则多样,规则使用的都是硬编码的方式,新增或修改规则比较麻烦,且逻辑不够清晰,扩展性不好。由此引入对规则的思考,查阅过业界的做法,了解到规则引擎这一利器,遂调研并学习记录一波。

概述

规则引擎起源于基于规则的专家系统,其提供一种可选的计算模型。与通常的命令式模型(由带有条件和循环的命令依次组成)不同,规则引擎基于生产规则系统。这是一组生产规则,每条规则都有一个条件(condition)和一个动作(action)可以将其看作是一组if-then语句。其通过输入一些基础事件,以推演或者归纳等方式,得到最终的执行结果。

比较学术的说法是:规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。

组成:

一般分为三个部分:

  • Fact: 用来传递数据,获取数据
  • LHS(Left Hand Side):可以理解为规则执行需要满足的条件。
  • RHS(Right Hand Sike):可以理解为规则执行后的返回对象。

优势

  • 声明式编程
  • 逻辑与数据分离
  • 速度及可测量性:Rete算法、Leaps算法等对系统数据匹配对象非常有效率
  • 易懂的规则:可以用接近自然语言的方式来编写规则,利于非技术人员编写规则
  • 规则变更快,适应庞大的逻辑和复杂的流程分支,减少if-else

应用场景

  • 日常活动配置,活动营销
  • 风控模型配置

常见的规则引擎

  • Drools

  • Easy Rule

  • RuleBook

  • 表达式引擎:

    • MVEL
    • FEL
    • simpleEL
    • groovy
    • Aviator

Drools 介绍

Drools 是一个基于Charles Forgy's的RETE算法的[注释1],易于访问企业策略、易于调整以及易于管理、基于java语言开发的开源业务规则引擎,符合业内标准,速度快、效率高,能够将Java代码直接嵌入到规则文件中。

官方地址:www.drools.org/

特点

1、简化系统架构,优化应用

2、提高系统的可维护性和维护成本

3、方便系统的整合

4、减少编写"硬代码"业务规则的成本和风险

5、社区活跃

5、学习成本高

基本概念

  • Fact:对象之间及对象属性之间的关系
  • rule:规则
  • module:if语句的条件

规则文件示例

ruby 复制代码
package act.lottery
import ink.wbc.drools.entity.User
​
rule "lottery"
       no-loop true
       when
               $user:User(money > 100)
       then
               System.out.println("满足抽奖条件");
               $user.setAllowLotteryFlag(1);
end
  • package:逻辑路径
  • import:导入的java包
  • rule:规则名称,唯一
  • no-loop:是否多次循环执行,默认false,true则只执行一次
  • when:条件语句
  • then:条件成立,执行逻辑
  • end:结束规则

使用示例

一、添加依赖

xml 复制代码
<!-- https://mvnrepository.com/artifact/org.drools/drools-compiler -->
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-compiler</artifactId>
    <version>7.10.0.Final</version>
</dependency>

二、创建kmodule.xml配置文件,指明规则文件的目录

xml 复制代码
注意:文件固定放在resource/META-INF下,名称固定为kmodule.xml
<?xml version="1.0" encoding="UTF-8" ?>
<kmodule xmlns="http://www.drools.org/xsd/kmodule">
    <!--
        name:指定kbase的名称,可以任意,但是需要唯一
        packages:指定规则文件的目录,需要根据实际情况填写,否则无法加载到规则文件,名字任意
        default:指定当前kbase是否为默认
    -->
    <kbase name="droolsDemo" packages="rules" default="true">
        <!--
            ksession:会话对象
            name:指定ksession名称,可以任意,但是需要唯一
            default:指定当前session是否为默认
        -->
        <ksession name="ksession-rule" default="true"/>
    </kbase>
</kmodule>

三、创建实体文件

kotlin 复制代码
public class User {
​
    private Integer money;
    private Integer allowLotteryFlag;
    
    // ... 省略
}

四、创建drl文件,表示规则文件

ruby 复制代码
package act.lottery
import ink.wbc.drools.entity.User
​
rule "lottery"
       no-loop true
       when
               $user:User(money > 100)
       then
               System.out.println("满足抽奖条件");
               $user.setAllowLotteryFlag(1);
end

五、测试

ini 复制代码
public class DroolsMain {
​
    public static void main(String[] args) {
        KieServices kieServices = KieServices.Factory.get();
        //获取KieContainer对象
        KieContainer kieClasspathContainer = kieServices.getKieClasspathContainer();
        //从Kie容器对象中获取会话对象,用于和规则引擎交互
        KieSession kieSession = kieClasspathContainer.newKieSession();
​
        User user = new User();
        user.setMoney(200);
​
        //将数据提供给规则引擎
        kieSession.insert(user);
        //激活规则引擎,由Drools框架自动进行规则匹配,如果规则匹配成功则执行规则
        kieSession.fireAllRules();
        kieSession.dispose();
        System.out.println("是否允许抽奖:" + user.getAllowLotteryFlag().equals(1));
    }
}

Aviator

一个高性能、轻量级的java语言实现的表达式求值引擎,主要用于各种表达式的动态求值

官方地址:

特点

  • 支持大部分运算操作符,包括算术操作符、关系运算符、逻辑操作符、正则匹配操作符(=~)、三元表达式?: ,并且支持操作符的优先级和括号强制优先级。
  • 支持函数调用和自定义函数。
  • 支持正则表达式匹配
  • 自动类型转换
  • 支持传入变量,支持类似a.b.c的嵌套变量访问。
  • 性能优秀。
  • Aviator的限制,没有if else、do while等语句,没有赋值语句,仅支持逻辑表达式、算术表达式、三元表达式和正则匹配,没有位运算符。

示例

下载依赖

javascript 复制代码
        <dependency>
            <groupId>com.googlecode.aviator</groupId>
            <artifactId>aviator</artifactId>
            <version>5.2.7</version>
        </dependency>
Map<String, Object> map = new HashMap<String, Object>(8);
map.put("money", "200");
System.out.println(AviatorEvaluator.execute("'充值金额: '+ money", map));

自定义函数

typescript 复制代码
    static class AddFunction extends AbstractFunction {
        @Override
        public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
            double num1 = FunctionUtils.getNumberValue(arg1, env).doubleValue();
            double num2 = FunctionUtils.getNumberValue(arg2, env).doubleValue();
            return new AviatorDouble(num1 + num2);
        }
​
        @Override
        public String getName() {
            return "add";
        }
    }
​
    public static void main(String[] args) {
        // 注册自定义函数
        AviatorEvaluator.addFunction(new AddFunction());
        System.out.println(AviatorEvaluator.execute("add(100, 2003)"));
    }

编译表达式

typescript 复制代码
    /**
     * 自定义表达式   *   * @param param   * @return
     */
    public static Object customizeExpression(Map<String, Object> param) {
        String expression = "a+(b*c)";
        // 编译表达式   
        Expression compiledExp = AviatorEvaluator.compile(expression);
        return compiledExp.execute(param);
    }
​
    public static void main(String[] args) {
        Map<String, Object> param = new HashMap<>(3);
        param.put("a", 100);
        param.put("b", 200);
        param.put("c", 300);
        System.out.println(customizeExpression(param));
    }

扩展阅读 美团酒旅实时数据规则引擎应用实践

MVEL 表达式解析器

MVEL是基于java的表达式语言,但它是动态类型的,它是一种可嵌入的表达式语言和为Java平台提供运行时的语言。

官方文档:mvel.documentnode.com/

特点

  • 灵活,性能高,无类型限制
  • 资料少,更新维护少

Easy Rule

一个简单而强大的java规则引擎,集成mvel表达式和SpEL表达式

特点:

  • 轻量级框架和易于学习的API
  • 基于POJO的开发
  • 通过高效的抽象来定义业务规则,使用简单
  • 支持创建复合规则
  • 支持使用表达式语言(如MVEL[注释二]和SpEL[注释三])定义规则
  • 规则很复杂,规则编排不好

规则的定义

  • Name : 一个命名空间下的唯一的规则名称
  • Description : 规则的简要描述
  • Priority : 相对于其他规则的优先级,默认为Integer.MAX,值越小,优先级越高
  • Facts : 用来传递数据,类似于Map
  • Conditions : 为了应用规则而必须满足的一组条件
  • Actions : 当条件满足时执行的一组动作

对应的注解:

@Condition、@Fact、@Action

规则的组成

  • 简单规则

  • 复合规则:CompositeRule

    • UnitRuleGroup : 要么应用所有规则,要么不应用任何规则(AND逻辑)
    • ActivationRuleGroup : 执行第一个条件成立的规则对应的action(XOR逻辑)
    • ConditionalRuleGroup : 如果最高优先级的规则条件成立结果为true,则触发其余规则往下执行

组合规则示例:

typescript 复制代码
    public class LotteryCompositeRule extends UnitRuleGroup {
        public LotteryCompositeRule(Object... rules) {
            for (Object rule : rules) {
                addRule(rule);
            }
        }
​
        @Override
        public int getPriority() {
            return 0;
        }
​
        @Rule(name = "vipRule", description = "抽奖规则")
        public static class VipRule {
            @Condition
            public boolean isVip(@Fact("user") User user) {
                return user.getVipFlag() == 1;
            }
​
            @Action
            public void print(@Fact("user") User user) {
                System.out.println("当前玩家为VIP用户");
            }
​
            @Priority
            public int getPriority() {
                return 1;
            }
        }
​
        @Rule(name = "MoneyRule", description = "抽奖规则")
        public static class MoneyRule {
            @Condition
            public boolean isAllowLottery(@Fact("user") User user) {
                return user.getMoney() >= 100;
            }
​
            @Action
            public void print(@Fact("user") User user) {
                System.out.println("当前玩家充值为:" + user.getMoney());
                user.setAllowLotteryFlag(1);
            }
​
            @Priority
            public int getPriority() {
                return 2;
            }
        }
    }

创建规则的方式

  • fulent api:链式编程

    csharp 复制代码
        public static Rule fluentRule() {
            return new RuleBuilder().name("链式抽奖规则").description("fluent lottery rule").priority(2).when(f -> {
                User user = f.get("user");
                return user.getMoney() >= 100;
            }).then(f -> {
                System.out.println("满足抽奖条件");
                User user = f.get("user");
                user.setAllowLotteryFlag(1);
            }).build();
        }
  • 注解

    less 复制代码
        @Rule(name = "lotteryRule", description = "抽奖规则")
        public class LotteryRule {
            @Condition
            public boolean isAllowLottery(@Fact("user") User user) {
                return user.getMoney() > 100;
            }
    ​
            @Action
            public void allowLottery(@Fact("user") User user) {
                user.setAllowLotteryFlag(1);
            }
    ​
            @Priority
            public int getPriority() {
                return 1;
            }
        }
  • mvel表达式

    csharp 复制代码
        public static Rule mvelRule() {
            return new MVELRule().name("mvel lottery rule").description("mvel表达式创建规则").priority(3).when("user.getMoney() >=100")
                    .then("user.setAllowLotteryFlag(1)");
        }
  • spel表达式

    csharp 复制代码
        public static Rule spElRule() {
            return new SpELRule().name("SpEL lottery rule").description("SpEl表达式创建规则").priority(4).when("#{['user'].money > 2}")
                    .then("#{['user'].setAllowLotteryFlag(1)}");
        }
  • yaml形式

    yaml文件

java 复制代码
    name:"yaml rule"description:"yaml创建规则"priority:5condition:"user.getMoney() >= 100"actions:-"user.setAllowLotteryFlag(1);"
​
    public static Rule yamlRule() {
        SpELRuleFactory spELRuleFactory = new SpELRuleFactory(new YamlRuleDefinitionReader());
        File ruleFile = new File("src/main/resources/rules/lottery.yml");
        try {
            return spELRuleFactory.createRule(new FileReader(ruleFile));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

条件

接口为Condition,定义如下:

java 复制代码
public interface Condition {    /**     * Evaluate the condition according to the known facts.     *     * @param facts known when evaluating the rule.     *     * @return true if the rule should be triggered, false otherwise     */    boolean evaluate(Facts facts);  // ...}

evaluate方法用来判断规则是否被触发,Condition有几个实现类:

  • SpELCondition:利用 SPEL表达式引擎解析规则
  • MVELCondition:利用 MVEL表达式引擎解析规则
  • JexlCondition:利用 JEXL表达式引擎解析规则

规则引擎

RulesEngine接口的两种实现

  • DefaultRulesEngine:根据规则的自然顺序(默认为优先级)应用规则。
  • InferenceRulesEngine:执行所有rules,直到rules为空。

引擎参数:

  • skipOnFirstAppliedRule:告诉引擎当第一个规则被触发时跳过后面的规则。
  • skipOnFirstFailedRule:告诉引擎当第一个规则失败时跳过后面的规则。
  • skipOnFirstNonTriggeredRule:告诉引擎当一个规则没有被触发,那么跳过后面的规则。
  • priorityThreshold:设置优先级阈值,优先级低于它的将会被跳过执行。

这些参数在RulesEngineParameters中设置,然后将其赋值给RulesEngine。例如:

java 复制代码
RulesEngineParameters parameters = new RulesEngineParameters().skipOnFirstAppliedRule(true);DefaultRulesEngine rulesEngine = new DefaultRulesEngine(parameters);

创建规则引擎

带参数的方式:

java 复制代码
RulesEngineParameters parameters = new RulesEngineParameters()DefaultRulesEngine rulesEngine = new DefaultRulesEngine(parameters);

规则引擎执行

ini 复制代码
rulesEngine.fire(rules, facts);

监听器

规则监听器RuleListener,定义:

typescript 复制代码
  public interface RuleListener {
        // 在条件评估前调用    
        default boolean beforeEvaluate(Rule rule, Facts facts) {
            return true;
        }
​
        // 在条件评估执行后调用    
        default void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) {
        }
​
        // 在条件评估中发生异常时调用    
        default void onEvaluationError(Rule rule, Facts facts, Exception exception) {
        }
​
        // 条件执行前调用   
        default void beforeExecute(Rule rule, Facts facts) {
        }
​
        // 条件执行成功后调用    
        default void onSuccess(Rule rule, Facts facts) {
        }
​
        // 条件执行失败后调用    
        default void onFailure(Rule rule, Facts facts, Exception exception) {
        }

注册监听器

使用规则引擎的registerRuleListener方法即可,参数需要实现RuleListener接口,例如:

typescript 复制代码
    public class LotteryListener implements RuleListener {
        /**
         * * @param rule   * @param facts     * @param evaluationResult
         */
        @Override
        public void afterEvaluate(org.jeasy.rules.api.Rule rule, Facts facts, boolean evaluationResult) {
            if (!evaluationResult) {
                System.out.println("不满足抽奖条件");
            }
        }
    }
// 注册监听器
   rulesEngine.registerRuleListener(new LotteryListener());
​

示例

一、下载依赖

xml 复制代码
<dependency>  <groupId>org.jeasy</groupId>  <artifactId>easy-rules-core</artifactId>  <version>4.1.0</version></dependency>  <!-- 可选,支持mvel表达式 --><dependency>    <groupId>org.jeasy</groupId>    <artifactId>easy-rules-mvel</artifactId>    <version>4.1.0</version></dependency>

二、编写规则

less 复制代码
    @Rule(name = "lotteryRule", description = "抽奖规则")
    public class LotteryRule {
        @Condition
        public boolean isAllowLottery(@Fact("user") User user) {
            return user.getMoney() >= 100;
        }
​
        @Action
        public void allowLottery(@Fact("user") User user) {
            user.setAllowLotteryFlag(1);
        }
​
        @Priority
        public int getPriority() {
            return 1;
        }
    }

三、运行

ini 复制代码
    public static void main(String[] args) {
        User user = new User();
        user.setMoney(200);
        Facts facts = new Facts();
        facts.put("user", user);
        DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
        // 注册监听器        
        rulesEngine.registerRuleListener(new LotteryListener());
        // 注册规则        
        Rules rules = new Rules();
        rules.register(new LotteryRule());
        rulesEngine.fire(rules, facts);
        System.out.println("是否能够抽奖:" + user.getAllowLotteryFlag().equals(1));
    }

注释

参考地址

其他

例子地址

欢迎关注:吴编程

相关推荐
cup113 小时前
[Full Clock 技术复盘] 一、浏览器前端如何实现百毫秒级时间校准?时间 API 推荐、模拟 NTP 算法原理及局限
typescript·开源·api·时钟·时间同步
红尘散仙4 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记5 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪6 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
冬奇Lab6 小时前
每日一个开源项目(第118篇):SkillOpt - 像训练神经网络一样优化 LLM Agent 的技能
人工智能·开源·agent
basketball6166 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364576 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
妄想出头的工业炼药师7 小时前
GS slam mono
算法·开源
zhangxingchao7 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端