如何设计一个规则引擎

之前答应同事写一篇关于规则引擎的文章,本文结合工作中实际场景,列举了一些规则引擎设计方案

工单系统-规则引擎

一个通用的工单系统,一般会考虑以下几点问题:
1、前端可以通过界面来配置审批流程节点,每个节点可以通过条件来配置不同的流向
2、每个节点需要根据不同的条件展示不同的表单组件,或者根据规则设置默认值

简单来说上面的规则可以分为两种:
1、判断类型:根据不同条件处理不同的逻辑
2、赋值类型:为某个元素赋予默认值

判断类型

判断类型使用到最多的一种规则,比如合同审批流程,会根据合同金额、类型走不同的审批流程,而且每一个审批节点,也需要动态控制合同金额是否展示、编辑。

以下是一个简易版的合同审批流程:
如何设计一个规则引擎

在不同的节点可以根据条件来设置下一步的流向,比如:“合同金额大于5万”
如何设计一个规则引擎

对于表单数据的权限,我们可以设置3种权限,可编辑、只读、不可见,然后对3种权限设置规则
如何设计一个规则引擎

赋值类型

在每个审批流程节点,都会有对应的审批信息,比如一个报销审核信息,有些信息需要人工填写,有些信息则需要动态赋予一个默认值,赋值类型一般是在点击组件的时候才会加载,这样可以有效的避免一些无用的字段在业务接口中一次性返回,根据不同的场景加载需要的字段。比如下面:
如何设计一个规则引擎

设计思路:对应赋值类型的,前面用=开始,后面接对应的表达式。

或者数据通过接口获取,每个审批节点的审批人可能不同,一般先是一级负责人审批、再是二级负责人审批...,这样依次到总负责人审批完成。
所以在每个节点,可以在规则引擎中配置一个接口,动态获取审批人。

设计思路:后端定义好每个接口的枚举类型,然后返回给前端,前端将流程节点要获取审批人和枚举类型绑定,后端解析调用对应的接口。

解析规则

以上两种类型,赋值类型以‘=‘开头

=$交通金额$ * $交通发票数量$ + $餐饮金额$ * $餐饮发票数量$
$合同类型$ = ‘商务合同’ || ( $合同总金额$ > 1000000 && $合同总金额$ < 2000000 )

对应表达式解析,如果表达式中有自定义的表达式,可以自己实现解析算法,具体可参考我的这篇文章《逆波兰算法在规则引擎中的运用》
对于一般简单的表达式,也可以使用spring自带的解析器,具体使用如下:

// 1. 构建解析器
org.springframework.expression.ExpressionParser parser = new SpelExpressionParser();
// 2. 解析表达式
// 数字类型
Expression integerExpression = parser.parseExpression("100 * 2 + 400 * 1 + 66");
// boolean类型
Expression booleanExpression = parser.parseExpression("1>0 && 1<2 || (4%2=0)");

// 3. 获取结果
int integerResult = (Integer) integerExpression.getValue();
System.out.println(integerResult); // 结果:666

boolean booleanResult = (Boolean) booleanExpression.getValue();
System.out.println(booleanResult); // 结果:true

规则引擎框架

Drools

目前使用比较多的是开源规则引擎是Drools,Drools 是一个基于 Java 的开源的规则引擎,可以将复杂多变的规则从硬编码中解放出来,以规则脚本的形式存放在文件中,使得规则的变更不需要修正代码重启机器就可以立即在线上环境生效。

举个栗子,商城一个促销活动,如果商品金额小于100元,则不加积分
金额大于100小于500,加100积分
金额大于500,加200积分

package rules

import com.neo.drools.entity.Order

rule "zero"
    when
        $s : Order(amout <= 100)
    then
        $s.setScore(0);
        update($s);
end

rule "add100"
    when
        $s : Order(amout > 100 && amout <= 500)
    then
        $s.setScore(100);
        update($s);
end

rule "add200"
    when
        $s : Order(amout > 500)
    then
        $s.setScore(200);
        update($s);
end

将规则写到Drools脚本文件(score-rule.drl)中,然后通过java代码来执行。

public static final void main(String[] args) throws Exception{
    KieServices ks = KieServices.Factory.get();
    KieContainer kc = ks.getKieClasspathContainer();
    execute( kc );
}

public static void execute( KieContainer kc ) throws Exception{
    KieSession ksession = kc.newKieSession("score-rule");
    List<Order> orderList = getInitData();
    for (int i = 0; i < orderList.size(); i++) {
        Order o = orderList.get(i);
        ksession.insert(o);
        ksession.fireAllRules();
        addScore(o);
    }
    ksession.dispose();
}

Drools足以实现大多数场景的需求。但是为了使用它,则需要去专门学习它的脚本规则,而且对于运营来说学习成本很大。

Java与Lua/JS

通过Java与Lua/JS等脚本语言的结合,
首先是在项目中添加需要的jar包,只需在maven中引入下面的包即可:

<dependency>
    <groupId>org.luaj</groupId>
    <artifactId>luaj-jse</artifactId>
    <version>3.0.1</version>
</dependency>

在这里我们使用的是Luaj,Luaj是Lua的一个Java实现,相比其他的native调用要更为稳定,并且本身也支持JSR223标准。

如果需要在Java中调用Lua脚本,只需创建一个Globals对象,载入脚本,然后就能在Java中去调用Lua脚本了,例如:

Globals globals =JsePlatform.standardGlobals();
LuaValue chunk =globals.load("print(\"hello luaj !!!\")");
chunk.call();

其中load方法内执行的就是一段Lua代码。

当然我们也可以去执行脚本中的某个具体方法,并且传入参数,例如这段Lua脚本:

function oneArgTest(str)
    print(#str)
end

这段脚本的作用是返回传入参数的长度,只需要将之前的Java代码加上下面一段:

LuaValue oneArgTest= globals.get(LuaValue.valueOf("oneArgTest"));
LuaValue results =oneArgTest.call("test func");

就能在Java中传入参数并执行该方法了。

我们甚至可以将Java对象也作为参数传入Lua脚本里,从而实现从Lua中对Java对象进行操作,以及调用Java对象中的方法,例如:

LuaValue luaValue =CoerceJavaToLua.coerce(test);

其中test是一个Java对象,通过CoerceJavaToLua.coerce方法将其转换成一个LuaValue对象,然后就能像之前的代码那样传入Lua脚本中,如果需要在脚本中调用test对象的方法,只需像这样:

test:getTestName()
test:setTestName("objecttest")

通过对象+”:”+方法名的形式调用。

当然这种Lua+Java混合调用的模式非常灵活,不过也带来了一个麻烦, 那就是有Java方法的Lua脚本只能在JVM中去执行,这为代码的调试带来了一定麻烦。

自定义规则

对于某些具体的业务,本身其业务逻辑就已经比较固定,所需要制定的规则也很有限。
比如一个系统需要频繁对不同数据源的进行报文过滤、替换等操作,在这种情况下我们可以去尝试自己开发一个规则引擎。

首先我们需要制定一个规则脚本,可以使用通用的json格式的规则脚本,用来对某个航班的数据报文进行加工处理,如下。

匹配航班为CZ8123,将CZ81替换为OQ23

{
      "conditions": [
        {"args":["flightNo","CZ8123"],"method": "equals"}
      ],
      "operations": [
        {"args":["flightNo","CZ81","OQ23"]," method ":"replace"}
      ]
}

在一个脚本中包含多个规则,每个规则独立执行。对于每一个规则,都包括两个部分,条件判断(conditions)与执行(operations)操作。条件与操作均支持多个,也就是说当满足所有的条件时,会执行后面的所有操作。每一个条件与操作都包含两部分,参数(args)与对应的方法(method),参数可以是固定值,也可以是报文的某一属性(脚本中以dyn,开头),又或是约定好的特定值(例如做时间判断时可以用now表示当前时间);方法则和Java代码中的方法名相对应。

规则解析部分就比较简单了,因为本身就是json格式,只需要分别读取条件与操作部分,然后去匹配传入参数与方法名,然后通过反射的方式去执行Java代码中的方法即可。

这个自定义的规则引擎看起来似乎没有前面两种解决方案强大,并且也有着很大的局限性,但是可以通过这个简单的规则脚本,为其编写一个前端页面,然后实现直接通过一个可视化的界面来配置规则。
成品大概是这样的:
如何设计一个规则引擎

大致思路就是在页面中实现DOM元素与JSON对象的互相转化,直接用JQuery就可以实现,再加上一个脚本的查找和更新功能就可以了。

这么一来,不仅代码不用写了,脚本也不用管了。直接在页面上用鼠标点几下就能完成一个规则的配置,而且不仅仅是研发人员,没有任何编程基础的运营与客服也能轻松上手。

以上几种方案,既有功能强大但实现复杂的,也有功能简单但是实现方便的,不管是用哪种方案,在面对需要频繁变动业务逻辑的场景时,大部分时候都能轻松的应对了。

本文来源于公众号《百川分享会》:baichuanshare

如何设计一个规则引擎

上一篇:git工作流及遇到的问题


下一篇:系统延迟及定时机制