一种基于脚本语言的规则引擎

image.png

楠木.png

基于脚本语言的规则引擎

前言

在日常开发中,开发者经常要面对这样一种场景:业务需求逻辑繁杂,且会不断变化。

通常的实现方式就是基于if else的代码堆砌,这样的代码会让开发者感觉乏味,缺少技术含量。

维护这样的代码也会让人日渐疲惫和迷茫。

我们思考是否能提供一套方案,将动态的业务规则从主流程中分离出来。

用户可以自行编写业务规则,系统将业务规则转换成代码执行,将开发者从if else中解放出来。

案例

我们假设业务场景

场景1:金融风控

场景2:交易促销

综合以上场景,我们定义业务模型的特性:

  • 有着固定的输入参数和输出格式
  • 中间规则部分要求能够配置化,尽可能灵活

需求模型

规则数据模型

我们将业务场景做了一下数据模型抽象

我们将规则解构成 变量 + 条件 + 结果,以便能够实现业务规则与实现代码的互相转换。

系统模型

我们将系统简单划分成规则执行器和规则管理两个模块。

  • 规则执行器

    负责选定需要执行的规则对象(包括规则的执行顺序)解析执行,并返回最终结果。

  • 规则管理

    对开发和业务开放,负责业务规则和实现代码的结构化解析与互换。

实现

我们将系统分为以下几个层面去实现:

动态代码脚本的执行,需要一个脚本语言执行器。

业务规则与代码脚本的互相转换,需要将业务规则数据结构化。

另外,让业务方能够在系统中定义自己所需的算法,委托系统执行的能力也是必须的。

脚本语言执行器

在JVM环境下,脚本语言可以选用 Groovy ,Scala,JRuby 等。本方案则是采用了另外一种方式,借用阿里的开源工具QLExpress实现。

对于 Groovy 和 QLExpress,我们有一些特性上的对比

执行引擎 执行类型 表达式语言 性能 特性
Groovy 编译型 支持 java语法兼容
QLExpress 解释型 支持 功能扩展性强

根据调查结果,我们需要的是一个支持表达式语言,轻量,灵活,且功能扩展性强的引擎。

因此我们选择 QLExpress。

除了上面提到的特性,QLExpress 还

  • 能绑定 java 类或者对象的 method 并且在脚本中执行
  • 可以自定义扩展操作符
  • 支持集合类参数
  • 与 spring 无缝集成

这些特性都是我们实现规则引擎的必要的条件

QLExpress 的执行方式:

ExpressRunner runner = new ExpressRunner();
// 业务的上下文参数
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
// 规则脚本
String express = "a + b * c";
// 规则结果
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
复制代码

业务对接系统只需要:

  1. 提供上下文参数(这些参数是一般不会变化)
  2. 处理规则结果返回

如此,我们做到了将业务流程与业务规则分离,开发人员通过服务调用的方式,将大量业务逻辑交由规则引擎系统处理。

数据结构化

根据定义规则的数据模型,我们将规则拆分为变量,条件,结果三个维度。

简单条件,可以由变量和逻辑运算符组成;规则的条件也可以由多个简单条件组合而成。规则结果也可以由变量来表示。

变量的来源可以是业务提供的上下文参数,可以是定义的一套算法,算法需要支持业务使用方自定义。

我们用一个例子来解释这些概念吧。

有一个交易订单的业务,在用户下单操作后,希望用户同时满足条件

  • 用户所在地为A市
  • 用户没有购买过当前商品
  • 下单时间为晚上

时,希望给当前订单8折优惠。

我们将下单业务的输入参数简单定义为:用户 ID ,商品 ID 。为了构造规则条件,我们还需要定义一些中间变量。

  • 变量1 = 根据用户 ID 获取用户所在地
  • 变量2 = 根据用户 ID 和商品 ID 获取用户购买记录
  • 变量3 = 判断当前时间为晚上

有了三个变量作为条件,我们可以再定义:变量4 = 8折,作为最终返回结果。

因此这个规则可以等价为:

if 变量1等于A市 并且 变量2等于0 并且 变量3等于 true

​ return 变量4

因此,我们将一条具体的业务规则转换成了对一条逻辑关系记录和若干变量的管理。

实现了数据的结构化。

定义绑定方法

在业务规则需求场景中,编码的实现过程通常是使用上下文参数或者通过参数进行计算后产生的临时变量作为规则的组成要素,参与规则的业务逻辑运算。

因此需要业务能将自定义的方法预先注册进脚本执行器,脚本执行器在执行到相关的代码时候,能够调用方法,得到业务结果。

我们通过 QLExpress 绑定java类或者对象 method 的特性,将业务方法注册进 QLExpress 执行器。

public void functionABC(Long a,Integer b,String c){
	System.out.println("functionABC");
}
    
    
ExpressRunner runner = new ExpressRunner();
runner.addFunctionOfServiceMethod("abc", singleton,"functionABC",new Class[]{Long.class,Integer.class,String.class},null);
String exp = "abc(a,b,c)";
IExpressContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a",1L);
context.put("b",2);
context.put("c","3");
runner.execute(exp, context, null, false, false);
}
复制代码

QLExpress执行绑定java类对象方法的底层逻辑与java程序中调用是一样的,所以我们也可以绑定远程方法。

我们知道,dubbo可以做到*像调用本地方法一样调用远程服务,*因此我们能将绑定什么样的方法的决定权交给业务,增加方案的开放度。

总结

本文提供了一个针对规则类需求的一套轻量级解决方案。

将规则需求数据进行结构化解析,然后转换成脚本语言,通过执行脚本语言实现业务规则需求。

这套方案能够在满足业务主体流程稳定的同时,通过提供一定程度的扩展能力,满足业务规则灵活变化的需要。目前已经在政采云的商家交易和报销费控业务中得到了应用。

展望未来

在业务间的需求差异巨大的背景下,如何在满足业务的灵活定制的同时,又能将能力扩展为通用服务,节省开发成本,需要在后续的迭代中逐步完善。

目前规则逻辑只支持单一路径,后面可以考虑多条路径甚至路径嵌套规则的实现。

规则执行器在执行规则脚本的过程中,如何优化其中间变量,也是可以优化的方向。

未来会继续从技术及业务两方面入手,将系统建设的更加易用、高效。

推荐阅读

JVM系列文章第二章-类文件到虚拟机

Dapr 实战(一)

Dapr 实战(二)

DS 版本控制核心原理揭秘

DS 2.0 时代 API 操作姿势

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 [email protected]

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

image.png

猜你喜欢

转载自juejin.im/post/7078456167797620766