规则语言基础:语法、条件、动作与优先级

规则语言,说白了就是策略引擎的“母语”。你配置的每一条策略,最终都要翻译成规则引擎能理解的语法。我见过不少团队,业务逻辑写得天花乱坠,结果一上规则引擎就崩——十有八九是规则语法没写对。

这一节,我们就把规则语言的四个核心要素拆开揉碎:语法定义、条件表达式、动作定义、规则优先级。掌握了这些,你才算真正入了策略引擎的门。

3.1 规则语法定义:骨架要稳

每条规则,本质上就是一个“如果…那么…”的逻辑。但不同的规则引擎,语法细节差异很大。我个人习惯用 DRL(Drools Rule Language) 来举例,因为它的语法结构清晰,而且业界用得最广。

一个标准的规则长这样:

rule "订单金额超过1000元自动打折"
    when
        $order: Order(amount > 1000)
    then
        $order.setDiscount(0.9);
        System.out.println("订单已打9折");
end

你看,结构其实很简单:

  • rule "规则名称":给规则起个唯一的名字,方便排查问题。
  • when:条件部分,也叫 LHS(Left Hand Side)。
  • then:动作部分,也叫 RHS(Right Hand Side)。
  • end:规则结束。
小技巧: 规则名称最好带上业务含义,比如“双11满减规则_v2”。别用“rule_001”这种名字,三个月后你自己都看不懂。

我在项目中遇到过有人把规则写成了“when…then…when…then…”的嵌套结构,结果引擎直接报语法错误。记住:一条规则只有一个 when 和一个 then。

3.2 条件表达式:筛选的艺术

条件表达式是规则引擎的“眼睛”。它决定了哪些数据会被匹配,哪些会被忽略。

常见的条件表达式类型:

类型 示例 说明
比较运算 amount > 1000 大于、小于、等于、不等于
逻辑运算 amount > 1000 && status == "ACTIVE" 与、或、非
集合运算 items contains "手机" 包含、不包含、成员判断
正则匹配 phoneNumber matches "^1[3-9]\\d{9}$" 字符串模式匹配
范围判断 age in [18, 60] 闭区间、开区间

嗯,这里要注意:条件表达式不要写得太复杂。我曾经接手过一个项目,一条规则的条件部分写了 30 多行,嵌套了 5 层括号。结果每次规则评估都要卡好几秒。后来拆成 3 条规则,性能直接翻倍。

避坑指南: 条件表达式里尽量避免调用外部服务或数据库。规则引擎是内存计算,一旦引入 IO 操作,性能会断崖式下跌。我曾经见过有人把 HTTP 请求写在条件里,结果规则引擎直接变成了“慢查询生成器”。

3.3 动作定义:执行的力量

动作部分,就是规则匹配后要干的事。常见的动作包括:

  • 修改事实(Fact):更新业务对象的属性。
  • 插入新事实:向工作内存中添加新数据。
  • 删除事实:从工作内存中移除数据。
  • 调用外部服务:发送消息、记录日志、触发流程。
  • 中断规则链:使用 halt() 或 drop 停止后续规则执行。

看个实际例子:

rule "VIP用户自动升级"
    when
        $user: User(vipLevel == 1 && totalSpend > 50000)
    then
        $user.setVipLevel(2);
        $user.setUpgradeTime(new Date());
        insert(new VipUpgradeLog($user.getId(), "自动升级"));
        System.out.println("用户 " + $user.getName() + " 已升级为VIP2");
end

这个规则做了三件事:修改用户等级、记录升级时间、插入一条日志。动作可以组合,但不要在一个动作里塞太多逻辑。我建议:一个动作只做一件事,如果逻辑复杂,拆成多条规则。

核心原则: 动作部分应该保持“幂等性”。也就是说,同一条规则执行多次,结果应该是一样的。否则,一旦规则被重复触发,业务数据就会乱套。

3.4 规则优先级:谁先谁后

规则优先级,是策略引擎里最容易出坑的地方。你想想看,如果两条规则同时匹配了同一个数据,谁先执行?执行顺序不同,结果可能天差地别。

常见的优先级控制方式:

方式 说明 适用场景
显式优先级 给规则设置数字优先级,数值越小越优先 业务逻辑有明确的层级关系
规则顺序 按照规则在文件中的先后顺序执行 简单的线性流程
议程分组 将规则分组,组内按优先级,组间按分组顺序 复杂的多阶段业务
冲突解决策略 引擎内置的排序算法(如最近使用优先) 默认行为,不推荐依赖

在 DRL 中,显式优先级是这样写的:

rule "黑名单拦截"
    salience 100
    when
        $user: User(blacklisted == true)
    then
        $user.setBlocked(true);
        System.out.println("黑名单用户已拦截");
end

rule "VIP用户免审"
    salience 50
    when
        $user: User(vipLevel >= 3)
    then
        $user.setNeedReview(false);
        System.out.println("VIP用户免审");
end

这里 salience 100 比 salience 50 优先级高。所以黑名单拦截会先执行。如果用户既是黑名单又是 VIP,那他会先被拦截,不会进入免审流程——这符合业务预期。

避坑指南: 我曾经见过一个项目,所有规则的优先级都设成了 0。结果规则执行顺序完全不可控,同一个订单有时候打折有时候不打折。排查了三天才发现是优先级没设置。记住:显式设置优先级,不要依赖默认顺序。

3.5 知识体系总览

为了让你更直观地理解规则语言的四个核心要素,我画了一张结构图:

规则语言核心要素结构图 规则语言 语法定义 条件表达式 动作定义 规则优先级 rule...when...then...end 命名规范 比较/逻辑/集合 避免复杂嵌套 修改/插入/删除 保持幂等性 salience 显式设置 议程分组 核心原则 语法要规范 · 条件要简洁 · 动作要幂等 · 优先级要显式

这张图把四个要素的关系理清楚了。语法是骨架,条件是筛选器,动作是执行器,优先级是调度器。四者缺一不可。

3.6 实战建议

最后,给你几个我在项目中沉淀下来的建议:

  • 规则文件分模块管理:按业务领域拆分成多个 DRL 文件,别把所有规则塞到一个文件里。
  • 条件表达式写注释:复杂的条件一定要加注释,方便后来人维护。
  • 动作部分不要写业务逻辑:动作只负责“做什么”,不负责“怎么做”。具体的计算逻辑放到 Java 服务里。
  • 优先级留出扩展空间:比如用 10、20、30 这样的间隔,别用 1、2、3。这样以后插入新规则时不用改所有优先级。
我的习惯: 每次写完规则,我都会跑一遍单元测试,验证条件是否覆盖了所有边界情况。特别是空值、负值、超大值这些“刁钻”数据,最容易暴露问题。