实习月记|QLExpress入门

大家好,我是“月更号主”——钢板兽~,长话短说,今天来更新一篇关于QLExpress的文章,介绍一些关于QLExpress的入门知识,希望可以帮助没接触过规则引擎的同学快速掌握规则引擎及QLExpress的概念及作用。

1. 初识QLExpress

规则引擎(Rules Engine)是一种将业务决策逻辑从应用程序代码中剥离出来的系统组件,其核心思想是"业务规则与程序代码分离"。通过预定义的规则语法,实现动态的业务逻辑管理和执行。

市面上有很多规则引擎,各有优点,适用场景也不同,下图为其他博客中调研的常见规则引擎。

Drools和QLExpress是功能相对比较强大的规则引擎,而后者相比前者的学习曲线比较缓和,所以QLExpress很适合当入门使用的规则引擎。

QLExpress是由阿里开发的一款轻量级Java规则引擎,具有以下核心特性(搬运自官方开源仓库):

  • 1、线程安全,引擎运算过程中的产生的临时变量都是threadlocal类型。

  • 2、高效执行,比较耗时的脚本编译过程可以缓存在本地机器,运行时的临时变量创建采用了缓冲池的技术,和groovy性能相当。

  • 3、弱类型脚本语言,和groovy,javascript语法类似,虽然比强类型脚本语言要慢一些,但是使业务的灵活度大大增强。

  • 4、安全控制,可以通过设置相关运行参数,预防死循环、高危系统api调用等情况。

  • 5、代码精简,依赖最小,250k的jar包适合所有java的运行环境,在android系统的低端pos机也得到广泛运用。

2. QLExpress四要素

个人觉得QLExpress最重要的四个要素是:表达式执行器、脚本表达式、上下文、操作数,掌握这4个最基本的要素,就可以使用QLExpress来实现一些简单的功能啦。

  • 表达式执行器:ExpressRunner

ExpressRunner是QLExpress的核心执行器,负责解析脚本表达式(Expression),并执行解析后的表达式。

ExpressRunner runner = new ExpressRunner();
  • 脚本表达式:Expression

脚本表达式即我们想实现的逻辑,这个逻辑是我们要从业务中提取出来真正核心的部分,通常很简单,它可以是简单的表达式,也可以是结构清晰的“类Json”脚本,我们也通常把脚本表达式叫做规则

runner.parseInstructionSet(expression)这个函数可以将表达式解析为指令集,这个方法也可以用来校验表达式的语法是否正确。

QLExpress的缓存机制可以将编译后的指令集缓存至ExpressRunner实例,避免重复编译。

  • 上下文:context

Context是表达式执行时的上下文环境,用于存储变量和共享数据,执行器会把上下文中的变量输入到解析后的脚本表达式中。

Context常用基于HashMap的默认实现:

DefaultContext<String, Object> context = new DefaultContext<>();

value为实际对象,key为value的“名称”,可以自己定义,方便在脚本表达式中调用。

  • 操作符:Operator

Operator是QLExpress中最基本的操作单元,QLExpress中内置的操作符类型覆盖了常用的算术运算符、比较运算符、逻辑运算符等。

这个操作符可以理解成:QLExpress要将脚本表达式中的内容解析成操作数和其他内容。

如果我们想在脚本表达式里写的逻辑,内置操作符没有覆盖怎么办?没关系,QLExpress支持自定义扩展操作符,自定义之后将操作符加入到runner执行器即可(具体可看官方文档:https://github.com/alibaba/QLExpress?tab=readme-ov-file)。


介绍完QLExpress的四个要素,对于没接触过QLExpress的人来说可能还是很难理解QLExpress到底是怎么“运作”的。别着急,我来举个例子。

我们想执行这段逻辑,当实际支付金额小于应付金额时,支付失败:

if(actuallyPay < shouldPay) {
    return Result.of("实际支付金额小于应付金额,支付失败");
} else {
    return Result.of("支付成功");
}

如果要用QLExpress来实现这段逻辑,我们应该确定几个事情。

  1. 确定上下文,把变量存到上下文中。这段逻辑实际上有三个“单位”:实际支付金额、应付金额、返回的消息,所以我们可以这样来设计context。
keyvalue解释
apactuallyPay实际支付
spshouldPay应付金额
mgmesssage返回消息,messge中有两条消息: failMessage:"实际支付金额小于应付金额,支付失败。 "successMessage:“支付成功”
  • 确定脚本表达式,脚本表达式中的对象可用context中的key代替,写成"ap.value < sp.value"。

  • 关于操作符,保证表达式所需要的操作符已经被覆盖(可以是 QLExpress内部覆盖,也可以自己拓展定义的),并且要有返回结果。

  • 执行器要结合核心逻辑使用,尽量保证逻辑可复用。

// 1.构建上下文
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("ap", actuallyPay.value);
context.put("sp", shouldPay.value);
context.put("mg", message);
// 2.脚本表达式
String expression = "ap.value < sp.value";
// 3.启动表达式执行器
ExpressRunner runner = new ExpressRunner();
if((Boolean) runner.execute(expression, context, null, true, false)) {
    return Result.of(context.get("mg").failMessage.toString);
} else {
    return Result.of(context.get("mg").successMessage.toString);
}

这个例子很简单,但是在实际应用中,通常可以使用结构清晰的“类Json”脚本来代替表达式,为功能新增定义统一结构的脚本,使得新增功能更加方便快捷。

3. QLExpress到底有什么用?

在看完上面的例子之后,该有人问了:原本只要几行代码能搞定的事儿,用了QLExpress之后,“硬是”写了十几行代码,这样做有什么意义吗?

关键就在逻辑可复用这几个字。

回到上面的例子,因为只有“支付失败”和“支付成功”两种情况,而且逻辑很简单,所以只需要4行代码,但是如果有几十种,甚至几百种情况并且每种情况的代码逻辑都很繁琐呢?我们需要硬编码出所有的情况,这样做太“笨”了。

而使用QLExpresson的话,则有以下几个优点:

  • 代码逻辑可复用,几十种、甚至几百种情况只需共用一段逻辑。

  • 脚本表达式expression是弱类型语言,每当需要新增情况时,我们只需要写一个“类Json”的脚本,不需要硬编码

使用QLExpress处理所有情况时,我们可以:

  1. 为所有情况定义一个规则Map集合,key为每种情况的标识,value为每种情况对应的脚本,也就是规则。有新增情况时,只需要新增规则(脚本)即可。

  2. 当需要执行核心逻辑时,会遍历集合中的所有规则,自动执行conditions为true的规则。所有规则的处理都复用同一段逻辑。


来举一个简单的订单场景,对于订单我们可能会有多种处理逻辑,比如根据订单的价格、状态等做出不同的动作。

  1. 定义规则(脚本)模版

一个简单的规则脚本中应该有条件(conditions)和执行动作(actions)。

"rule_payment_success": {
  "conditions": [
    "order.status == 'PAID'",
    "order.amount >= 100"
  ],
  "actions": [
    "order.level = 'VIP'",
    "order.addTag('HIGH_VALUE')",
    "result = 'NOTIFY_DELIVERY'"
  ]
}

在实际应用中,规则脚本中的逻辑应该尽可能地少,尽可能多地把代码抽到复用逻辑中。

  • 定义一个规则集合Map,用来保存规则名称和规则:
private final Map<String, Rule> rules = loadRules();

Rule这个对象用来保存Json脚本反序列化之后的数据:

public class Rule {
    List<String> conditions;
    List<String> actions;
}
  • 定义上下文:
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("order", order);
context.put("result", new String());
  • 执行表达式:
  1. 遍历 map 中的规则脚本。
  2. 执行 conditions,判断是否为 true。
  3. conditions 为 true 则执行 actions。
if (MapUtils.isEmpty(rules)) {
    return "DEFAULT_ACTION";
}

for (Rule rule : rules.values()) {
    boolean allConditionsMet = rule.conditions.stream()
            .allMatch(cond -> evaluateCondition(cond, context));
    if (allConditionsMet) {
        for (String action : rule.actions) {
            try {
                runner.execute(action, context, null, true, false);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return (String) context.get("result");
    }
}
return "DEFAULT_ACTION";

evaluateCondition方法用来评估规则的condtions是否满足:

private boolean evaluateCondition(String cond, DefaultContext<String, Object> context) {
    try {
        return (boolean) runner.execute(cond, context, null, true, false);
    } catch (Exception e) {
        return false;
    }
}

新增规则时,只需要追加规则配置,无需修改代码:

"rule_promotion_gift": {
  "conditions": [
    "order.use_promo_code == true",
    "order.amount > 500"
  ],
  "actions": [
    "result = 'ADD_FREE_GIFT'"
  ]
}

这个例子凸显出QLExpress的作用,它让新规则的增加变得高效,如果核心代码逻辑更加复杂,QLExpress的这种作用将更加明显。


这篇文章仅仅是对QLExpress做一个简单的介绍,QLExpress还有其他优秀的特性,比如缓存,QLExpresson会将比较耗时的脚本编译过程缓存在本地机器,我们也可以自定义缓存工具,让脚本的编译、执行更高效,日后有机会再来探究下这些功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值