3. 规则语法入门:DRL语法基础、条件(when)与动作(then)编写、规则属性(salience、no-loop等)、规则文件结构
说实话,很多刚接触规则引擎的朋友,上来就盯着复杂的业务逻辑看。我个人的建议是——先把 DRL 语法啃透。语法是地基,地基不稳,后面写再多规则都是给自己挖坑。
DRL 全称是 Drools Rule Language。它是 Drools 规则引擎的原生语言。说白了,就是用一种接近自然语言的方式,把「如果…就…」的逻辑写出来。你想想看,业务人员读得懂,开发人员也维护得了,这才是规则引擎的初衷。
核心要点:DRL 文件本质上是一个「条件 → 动作」的映射集合。引擎会帮你自动匹配条件,执行动作。你只需要关心规则怎么写,不用关心引擎怎么跑。
3.1 规则文件结构:一个 DRL 文件长什么样
我们先看一个完整的 DRL 文件骨架。我习惯把它拆成三块:头部声明、全局变量、规则体。
package com.example.rules
import com.example.model.Order
import com.example.model.Customer
global com.example.service.DiscountService discountService
rule "新用户首单折扣"
salience 10
no-loop true
when
$o: Order(totalAmount > 100)
$c: Customer(isNew == true)
then
discountService.applyDiscount($o, 0.2);
System.out.println("新用户首单满100,打8折");
end
嗯,这里要注意几个点:
- package:和 Java 的包名类似,用来隔离规则集。不同包下的规则可以互相引用,但我建议一个 DRL 文件只放一个包。
- import:导入 Java 类。规则里用到的 Fact 对象,必须 import。
- global:全局变量。可以理解成规则里的「全局服务」。我曾在项目中用 global 传入了日志服务,方便规则执行时打日志。
- rule … end:规则体。一个 DRL 文件可以包含多个 rule。
我的习惯:每个 DRL 文件只放 10~15 条规则。超过这个数,文件会变得很难维护。按业务模块拆分,比如 order-discount.drl、risk-control.drl,比堆在一个文件里强得多。
3.2 条件(when)怎么写:匹配你想要的 Fact
when 部分就是规则的条件。引擎会在工作内存(Working Memory)里找所有符合条件的 Fact 对象。说白了,就是「从一堆数据里筛出我要的」。
最基本的写法是这样的:
when
$order: Order(amount > 500, status == "PAID")
then
// 动作
end
这里 $order 是一个绑定变量。你可以在 then 部分用它。条件里支持多种操作符:
| 操作符 | 说明 | 示例 |
|---|---|---|
> < >= <= |
数值比较 | amount > 100 |
== != |
等于 / 不等于 | status == "ACTIVE" |
matches |
正则匹配(字符串) | name matches "张.*" |
contains |
集合包含 | tagList contains "VIP" |
not |
取反 | not (amount > 1000) |
exists |
存在匹配 | exists Order(amount > 1000) |
多个条件之间默认是 与 关系。如果你想用「或」,可以用 || 或者 or:
when
$o: Order(amount > 1000 || customerLevel == "VIP")
then
// 高价值订单或VIP客户
end
我曾经踩过的坑:在条件里直接调用了 Java 方法,比如 order.isExpired()。这在语法上没问题,但每次规则匹配都会调用一次。如果这个方法里有数据库查询或远程调用,性能直接崩掉。记住,条件里尽量用字段直接比较,别搞复杂逻辑。
3.3 动作(then)怎么写:执行你的业务逻辑
then 部分就是规则命中后要执行的动作。这里可以写 Java 代码,也可以调用 global 服务,或者调用 Fact 对象的方法。
then
$order.setDiscount(0.15);
$order.setRemark("VIP客户自动折扣");
discountService.sendNotification($order);
update($order); // 通知引擎该对象已更新
end
这里有个关键点——update()。如果你修改了 Fact 对象的字段,并且希望引擎重新评估所有规则,必须调用 update()。否则引擎不知道数据变了,后续规则可能不会触发。
常用的动作操作:
- 直接赋值:
$obj.setField(value) - 调用服务:
globalService.doSomething($obj) - 插入新 Fact:
insert(new ResultObject(...))— 这会触发新的规则匹配 - 删除 Fact:
retract($obj)— 从工作内存中移除 - 更新 Fact:
update($obj)— 通知引擎重新评估
我个人的建议:then 部分尽量保持轻量。复杂的业务逻辑封装到 Java 服务里,规则只做「判断 + 调用」。这样规则文件干净,也方便单元测试。
3.4 规则属性:控制规则的行为
规则属性写在 rule 名称后面,when 前面。它们控制规则的执行顺序、是否重复触发、是否开启日志等。我挑几个最常用的讲。
3.4.1 salience:优先级
salience 的值越高,规则越先执行。默认是 0。你可以设成负数,表示低优先级。
rule "紧急规则"
salience 100
when
// ...
then
// 先执行
end
rule "普通规则"
salience 0
when
// ...
then
// 后执行
end
我在项目中遇到过一个问题:多条规则同时命中,但执行顺序错了,导致数据覆盖。后来给关键规则加了 salience,问题就解决了。
3.4.2 no-loop:防止死循环
no-loop 默认是 false。如果规则的动作里调用了 update(),并且条件仍然满足,这条规则会再次触发。这就可能形成死循环。
rule "防循环规则"
no-loop true
when
$o: Order(amount > 100)
then
$o.setAmount(50);
update($o); // 即使条件还满足,也不会再次触发本规则
end
嗯,这里要注意:no-loop true 只阻止当前规则重新触发自己。如果其他规则也匹配了同一个 Fact,它们还是会触发。
3.4.3 其他常用属性
| 属性 | 作用 | 示例 |
|---|---|---|
activation-group |
同一组内,只有一条规则会执行(互斥) | activation-group "payment" |
date-effective |
规则生效日期(之后才触发) | date-effective "2025-01-01" |
date-expires |
规则失效日期(之后不再触发) | date-expires "2025-12-31" |
enabled |
是否启用(true/false) | enabled false |
duration |
延迟执行(毫秒) | duration 3000 |
我曾经踩过的坑:给所有规则都设了很高的 salience,以为这样能「优先执行」。结果 salience 值之间差距太小,执行顺序还是乱的。我的经验是:salience 值之间至少差 10,比如 100、90、80,这样排序才可靠。
3.5 知识体系总览
下面这张图,把 DRL 语法的核心结构串起来了。你可以把它当成一张「语法地图」,写规则时对照着看。
3.6 实战建议:从零开始写你的第一条规则
如果你现在准备动手,我建议按这个步骤来:
- 先定义 Fact 类:比如
Order.java,字段用基本类型和 String,别搞复杂嵌套。 - 写一个最简单的规则:只匹配一个条件,then 里只打一行日志。先跑通再说。
- 加属性控制:试试 salience 和 no-loop,看看执行顺序的变化。
- 加多个条件:用
&&和||组合,感受一下引擎的匹配逻辑。 - 加动作:调用
update()或insert(),观察规则链式触发。
一个小技巧:调试规则时,可以在 then 里加 System.out.println("规则 " + drools.getRule().getName() + " 被触发");。这样你能清楚看到每条规则是否被命中,以及执行顺序。等上线前再删掉这些日志。
DRL 语法其实不复杂。你只要记住:when 是筛子,then 是动作,属性是控制器。把这三块搞明白,大部分业务场景都能覆盖。剩下的,就是在实战中慢慢积累经验了。