开源规则引擎比较

规则引擎将复杂的业务逻辑从业务代码中剥离出来,可以显著降低业务逻辑实现难度;同时,剥离的业务规则使用规则引擎实现,这样可以使多变的业务规则变得可维护,配合规则引擎提供的良好的业务规则设计器,不用编码就可以快速实现复杂的业务规则。同样,即使是完全不懂编程的业务人员,也可以轻松上手使用规则引擎来定义复杂的业务规则。

分类

规则引擎整体分为下面几类:

  • 通过界面配置的成熟规则引擎,这种规则引擎相对来说就比较重,但功能全,比较出名的有:drools, urule
  • 基于jvm脚本语言,互联网公司会觉得drools太重了,然后会基于一些jvm的脚本语言自己开发一个轻量级的规则引擎,比较出名的有,groovy(开源风控radar),aviator,qlexpress
  • 基于java代码的规则引擎:基于jvm脚本语言会有一些语法学习的成本,所以就有基于java代码去做的规则引擎,比如通过一些注解实现抽象的方式去做到规则的扩展,比较出名的有: easy rules

下面将分别详细介绍三种优秀的规则引擎中drools、groovy、aviator和easy rules,并比较其优劣。

Drools

1. 简介

Drools是一个业务规则管理系统(BRMS)解决方案,它提供了一个核心业务规则引擎(BRE)、一个web创作和规则管理应用程序(Drools Workbench)、对符合级别3的决策模型符号(DMN)完整运行时支持。Drools是一个重量级的规则引擎,很多金融行业、电信行业的大公司都在使用它作为规则引擎。

规则模型包括:

  • 事实(Fact):对象之间及对象属性之间的关系
  • 规则(rule):是由条件和结论构成的推理语句,一般表示为if…then。一个规则的if部分称为LHS,then部分称为RHS。
  • 模式(module):就是指IF语句的条件。这里IF条件可能是有几个更小的条件组成的大条件。模式就是指的不能在继续分割下去的最小的原子条件。

2. RETE算法

Rete 匹配算法是一种进行大量模式集合和大量对象集合间比较的高效方法,通过网络筛选的方法找出所有匹配各个模式的对象和规则。

其核心思想是将分离的匹配项根据内容动态构造匹配树,以达到显著降低计算量的效果。Rete 算法可以被分为两个部分:规则编译和规则执行。当Rete算法进行事实的断言时,包含三个阶段:匹配、选择和执行,称做 match-select-act cycle。

RETE算法就是Drools引擎执行速度快的秘诀。个人认为,RETE算法有以下特点:

  • 不同规则之间的相同模式是可以共享节点和存储区的,所以做过的判断不需要重复执行,是典型的空间换时间的做法
  • 使用AlphaMemory和BetaMemory存储事实,当事实部分变化时,可以只计算变化的事实,提高了匹配效率
  • 事实只有满足当前节点,才会向下传递。不做无意义的匹配。

除了RETE算法,Drools还支持PHREAK等其他改进算法。

3. 如何使用

1)maven依赖


<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-compiler</artifactId>
    <version>${drools.version}</version>
</dependency>


2)kmodule.xml配置文件

根据drools要求创建resources/META-INF/kmodule.xml


<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://jboss.org/kie/6.0.0/kmodule">
    <!--
        name:指定kbase的名称,可以任意,但是需要唯一
        packages:指定规则文件的目录,需要根据实际情况填写,否则无法加载到规则文件
        default:指定当前kbase是否为默认
    -->
    <kbase name="myKbase1" packages="rules" default="true">
        <!--
            name:指定ksession的名称,可以任意,但需要唯一
            default:指定当前session是否为默认
        -->
        <ksession name="ksession-rule" default="true"/>
    </kbase>
</kmodule>


3)创建实体类Order


package com.ws.soon.entity;
import lombok.Data;
/**
订单
*/
@Data
public class Order {
    private Double  originalPrice; // 订单原始价格,即优惠前的价格
    private Double realPrice; // 订单真实价格,即优惠后的价格
}


4)创建规则文件

按照需求创建规则文件resources/rules/bookDiscount.drl


// 图书优惠规则
package book.discount
import com.ws.soon.entity.Order

// 规则一:所购图书总价在100元以下的没有优惠
rule "book_discount_1"
    when
        $order: Order(originalPrice < 100) // 匹配模式,到规则引擎中(工作内存)查找Order对象,命名为$order
    then
        $order.setRealPrice($order.getOriginalPrice());
        System.out.println("成功匹配到规则一,所购图书总价在100元以下无优惠");
end

// 规则二:所购图书总价在100~200的优惠20元
rule "book_discount_2"
    when
        $order: Order(originalPrice >= 100 && originalPrice < 200)
    then
        $order.setRealPrice($order.getOriginalPrice() - 20);
        System.out.println("成功匹配到规则二,所购图书总价在100~200元之间");
end

// 规则三:所购图书总价在200~300元的优惠50元
rule "book_discount_3"
    when
        $order: Order(originalPrice >= 200 && originalPrice < 300)
    then
        $order.setRealPrice($order.getOriginalPrice() - 50);
        System.out.println("成功匹配到规则三,所购图书总价在200~300元之间");
end

// 规则四:所购图书总价在300元及以上的优惠100元
rule "book_discount_4"
    when
        $order: Order(originalPrice >= 300)
    then
        $order.setRealPrice($order.getOriginalPrice() - 100);
        System.out.println("成功匹配到规则四,所购图书总价在300元及以上");
end


5)测试代码


package com.ws.soon.test; import com.ws.soon.entity.Order; import org.junit.Test; import org.kie.api.KieServices; import org.kie.api.runtime.KieContainer; import org.kie.api.runtime.KieSession; public class DroolsTest {    @Test    public void test() {        KieServices kieServices = KieServices.Factory.get();        // 获取Kie容器对象(默认容器对象        KieContainer kieContainer = kieServices.newKieClasspathContainer();        // 从Kie容器对象中获取会话对象(默认session对象        KieSession kieSession = kieContainer.newKieSession();        Order order = new Order();        order.setOriginalPrice(160d);        // 将order对象插入工作内存        kieSession.insert(order);        System.out.println("匹配规则前优惠后价格:" + order.getRealPrice());        // 匹配对象        // 激活规则,由drools框架自动进行规则匹配。若匹配成功,则执行        kieSession.fireAllRules();        // 关闭会话        kieSession.dispose();        System.out.println("优惠前价格:" + order.getOriginalPrice() + "\n优惠后价格:" + order.getRealPrice());   } }


4. 总结

Drools发展到今天,其实已经是一整套解决方案了。 如果只是想要简单使用,那就是只用到Drools的核心BRE,引入几个Maven依赖,编写Java代码和规则文件即可。但是如果要编排很复杂的工程,甚至整个业务都重度依赖,需要产品、运营同学一起来指定规则,则需要用到BRMS整套解决方案了,包括Drools Expert(BRE)、Drools Workbench(WEB)、DMN。

优点

  1. 功能完备
  2. 执行效率高
  3. 具有规则编排能力,可以应对复杂规则场景
  4. 社区活跃

缺点

  1. Drools相关的组件太多,需要逐个研究才知道是否需要
  2. Drools逻辑复杂,不了解原理,一旦出现问题排查难度高
  3. Drools需要编写规则文件,学习DSL语言成本较高

Groovy

1.简介

Groovy是构建在JVM上的一个轻量级却强大的动态语言, 它结合了Python、Ruby和Smalltalk的许多强大的特性.

Groovy就是用Java写的 , Groovy语法与Java语法类似, Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码, 相对于Java, 它在编写代码的灵活性上有非常明显的提升,Groovy 可以使用其他 Java 语言编写的库.

2.如何使用

1)maven依赖


<dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>${groovy.version}</version>
</dependency>


2)Groovy与java集成的方式

GroovyClassLoader 用 Groovy 的 GroovyClassLoader ,它会动态地加载一个脚本并执行它。GroovyClassLoader是一个Groovy定制的类装载器,负责解析加载Java类中用到的Groovy类。


GroovyClassLoader loader = new GroovyClassLoader();
Class groovyClass = loader.parseClass(new File(groovyFileName)); // 也可以解析字符串
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
groovyObject.invokeMethod("run", "helloworld");


GroovyShell GroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。您可使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。


GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate("println \"hello world\"");


GroovyScriptEngine GroovyShell多用于推求对立的脚本或表达式,如果换成相互关联的多个脚本,使用GroovyScriptEngine会更好些。GroovyScriptEngine从您指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也允许您传入参数值,并能返回脚本的值。


GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine(file.getAbsolutePath());
groovyScriptEngine.run("hello.groovy", new Binding())


3. 总结

优点

  • 历史悠久、使用范围大
  • 和java兼容性强:无缝衔接java代码,即使不懂groovy语法也没关系,支持语法糖
  • 适应于反复执行的简单表达式,效率非常高

缺点

  • 支持System.exit等语法,有安全风险
  • 依赖于开发人员编写groovy

Aviator

1. 简介

Aviator的设计目标是轻量级和高性能 ,相对于Groovy的笨重,Aviator非常小,只依赖common-beanutil,不算依赖包就70K,不过Aviator的语法受限,它并不是一门完整的语言,只是语言的一小部分集合。Aviator是直接将表达式编译成Java字节码,交给JVM去执行。基于aviator的规则引擎可视化的架构是前端配置完数据之后存入到mysql数据库,然后aviator编译器从数据库中拉取数据进行运算。

  • 支持大部分运算操作符,包括算数运算符、关系运算符、逻辑操作符、正则匹配操作符、三元表达式,并且还致辞操作符的优先级以及括号的强制优先级。
  • 支持函数调用和自定义函数
  • 支持正则表达式匹配
  • 自动类型转换,当执行操作的时候,会自动判断操作数类型并做相应的转换,无法转换就抛异常
  • 支持传入变量,支持类似a.b.c的嵌套变量访问。
  • 性能优秀

2.如何使用

1)maven依赖


<dependency> <groupId>com.googlecode.aviator</groupId> <artifactId>aviator</artifactId> <version>${aviator.version}</version> </dependency>


2)示例

我们只需要将业务人员配置的规则转换成一个规则字符串,然后将该规则字符串保存进数据库中,当使用该规则时,只传递该规则所需要的参数,便可以直接计算出结果,我们开发人员无需再为这些规则编写任何代码。


public class AviatorExampleTwo { //规则可以保存在数据库中,mysql或者redis等等 Map<Integer, String> ruleMap = new HashMap<>(); public AviatorExampleTwo() { //秒数计算公式 ruleMap.put(1, "hour * 3600 + minute * 60 + second"); //正方体体积计算公式 ruleMap.put(2, "height * width * length"); //判断一个人是不是资深顾客 ruleMap.put(3, "age >= 18 && sumConsume > 2000 && vip"); //资深顾客要求修改 ruleMap.put(4, "age > 10 && sumConsume >= 8000 && vip && avgYearConsume >= 1000"); //判断一个人的年龄是不是大于等于18岁 ruleMap.put(5, "age >= 18 ? 'yes' : 'no'"); } public static void main(String[] args) { AviatorExampleTwo aviatorExample = new AviatorExampleTwo(); //选择规则,传入规则所需要的参数 System.out.println("公式1:" + aviatorExample.getResult(1, 1, 1, 1)); System.out.println("公式2:" + aviatorExample.getResult(2, 3, 3, 3)); System.out.println("公式3:" + aviatorExample.getResult(3, 20, 3000, false)); System.out.println("公式4:" + aviatorExample.getResult(4, 23, 8000, true, 2000)); System.out.println("公式5:" + aviatorExample.getResult(5, 12)); } public Object getResult(int ruleId, Object... args) { String rule = ruleMap.get(ruleId); return AviatorEvaluator.exec(rule, args); } } //公式1:3661 //公式2:27 //公式3:false //公式4:true //公式5:no


另外支持自定义函数功能

3.总结

优点

  • 规则配置门槛低,因此业务分析师很容易上手。
  • 系统支持规则热部署。
  • 轻量级和高性能。
  • 有前人在金融方面的使用,并发情况下,exexute方法线程安全。

缺点

  • 规则都要从表达式去运算,语法受限。且数值型数据仅支持Long和Double两种数据类型。
  • 不支持乘方运算,需要结合spring的el表达式来进行。

Easy-rules

1. 简介

Easy Rules提供了抽象Rule来创建带有conditions和actions的规则,RulesEngine API运行一系列规则来评估conditions和执行actions。

Easy Rules:使用步骤主要分成创建fact,创建rule,创建RulesEngine和fire四个步骤。

大多数业务规则可以表示为以下定义:

  • 名称:一种唯一的规则名称
  • 描述:对规则的简要描述
  • 优先级:相对于其他规则的优先级
  • 条件:设置规则执行时需要满足的条件,返回boolean
  • 操作:设置的条件满足时执行的操作

2. 如何使用

1)maven依赖


<!--easy rules核心库--> <dependency> <groupId>org.jeasy</groupId> <artifactId>easy-rules-core</artifactId> <version>${easy.rules.version}</version> </dependency> <!--规则定义文件格式,支持json,yaml等--> <dependency> <groupId>org.jeasy</groupId> <artifactId>easy-rules-support</artifactId> <version>${easy.rules.version}</version> </dependency> <!--支持mvel规则语法库--> <dependency> <groupId>org.jeasy</groupId> <artifactId>easy-rules-mvel</artifactId> <version>${easy.rules.version}</version> </dependency>


2) 调用方式 easy-rules提供了很多种调用方式。使用方式大多都只是个范式,在官网很容易看懂。这里只列举一下各种形式。 注解方式


@Rule(name = "my rule1", description = "my rule description", priority = 1) public class MyRule1 { @Condition public boolean when(@Fact("type") Integer type) { return type == 1; } @Action(order = 1) public void execute1(Facts facts) throws Exception { log.info("MyRule1 execute1, facts={}", facts); } @Action(order = 2) public void execute2(Facts facts) throws Exception { log.info("MyRule1 execute2, facts={}", facts); } }


流式API


Rule weatherRule = new RuleBuilder() .name("weather rule") .description("if it rains then take an umbrella") .when(facts -> facts.get("rain").equals(true)) .then(facts -> System.out.println("It rains, take an umbrella!")) .build();


表达式方式 支持 MVEL , SpEL and JEXL


Rule weatherRule = new MVELRule() .name("weather rule") .description("if it rains then take an umbrella") .when("rain == true") .then("System.out.println(\"It rains, take an umbrella!\");");


规则描述文件


name: adult rule description: when age is greater than 18, then mark as adult priority: 1 condition: "person.age > 18" actions: - "person.setAdult(true);" name: weather rule description: when it rains, then take an umbrella priority: 2 condition: "rain == true" actions: - "System.out.println(\"It rains, take an umbrella!\");"


easyrules自定义规则通过json和yml文件来实现,可以适应变化的需求,有效避免重复修改代码逻辑,是一个很好的if-else替代品;并且它的condition判断可以通过调用方法来进行处理,这对业务的可扩展性也有很大的帮助,后续可以将规则存入数据库,存储内容可以以json字符串格式存储,只是在使用规则的时候需要将规则字符串转换为json文件,需要注意的是,**json文件中的规则需要以数组形式存

3)规则组合

规则的组合,和规则之间的逻辑关系。 在easy-rules中,规则可以被组合成为 CompositeRule ,并且提供了三种逻辑关系:

  • UnitRuleGroup:规则之间是 AND 的关系,所有规则是一个整体,要么应用所有规则,要么不应用任何规则
  • ActivationRuleGroup:规则之间是 XOR 的关系,只会执行第一个命中的规则,其它规则均忽略
  • ConditionalRuleGroup:具有最高优先级的规则作为 触发条件 ,如果满足触发条件,则会继续执行其它规则

4) 规则引擎

easy-rules提供了两种规则引擎实现:

  • DefaultRulesEngine:根据规则的自然顺序(默认为优先级)应用规则。
  • InferenceRulesEngine:持续对已知事实应用规则,直到不再应用规则为止。

3. 总结

easy-rules的代码结构设计的很清晰,代码也很清爽。项目本身很轻量级,基本上只提供了一个规则判断和行为执行的框架,相当于是对计算过程的抽象。但也正是因为轻量级,easy-rules几乎不包含规则编排等功能,如果规则的条件本身是很复杂的,那么我们只能自己对这些条件进行编排,对于easy-rules来说,它只是一条单一的规则。

优点

  • 轻量级框架和易于学习的API
  • 基于POJO的开发与注解的编程模型
  • 定义抽象的业务规则并轻松应用它们
  • 支持从简单规则创建组合规则的能力
  • 支持使用表达式语言(如MVEL和SpEL)定义规则的能力

缺点

  • 实现规则需要写的类较多,规则文件多了以后维护起来较为复杂
  • 所有的事实都要放到Facts对象里,稍微影响性能

最后

选择一款合适的规则引擎需要综合多方面的因素考量,如:

  • 业务规则的复杂度
  • 效率和性能(实时风控?)
  • 是否轻量化
  • 学习和维护成本

如果

参考资料

[博客]《常见开源规则引擎对比分析

[博客]《drools -Rete算法

[博客]《Drools规则引擎

[博客]《基于Aviator的规则引擎》