第4章 DRL语法基础:从文件结构到第一个规则
DRL(Drools Rule Language)是规则引擎的核心语言。说白了,它就是你和规则引擎沟通的桥梁。我刚开始接触时也觉得语法有点怪,但摸清套路后会发现——其实就那么几个关键点。
这一章,我们就把DRL文件拆开揉碎,看看它到底长什么样。
4.1 DRL文件结构:骨架长这样
一个标准的DRL文件,结构其实很固定。我习惯把它想象成「三段式」:
- 头部声明:package、import、global等
- 规则体:一个或多个rule块
- 可选部分:function、query、declare等
来看个完整的例子:
package com.example.rules
import com.example.model.Order
import com.example.model.Customer
rule "VIP客户免运费"
when
$c: Customer(vipLevel >= 3)
$o: Order(customerId == $c.id, totalAmount > 100)
then
$o.setShippingFee(0);
System.out.println("VIP客户免运费已生效");
end
嗯,这里要注意——package和import必须写在最前面,规则引擎加载时会先解析它们。我曾经见过同事把import写在rule块里面,结果编译直接报错。
核心原则:DRL文件的解析顺序是自上而下的。package和import必须在所有规则之前。
4.2 package与import:命名空间的艺术
package的作用和Java里的包名类似。它用来隔离不同模块的规则。我建议你按业务模块来命名:
package com.mycompany.order.discount
package com.mycompany.order.shipping
package com.mycompany.user.verification
这样做的好处很明显——规则多了以后,你不会迷失在几百个规则文件里。
import则负责引入Java类。注意,它只引入类,不引入其他DRL文件。如果你需要跨文件引用规则,得用ruleflow-group或activation-group,这个后面会讲。
| 关键字 | 作用 | 示例 |
|---|---|---|
| package | 定义规则所属的命名空间 | package com.example.rules |
| import | 引入Java类或静态方法 | import com.example.model.Order |
| global | 声明全局变量(后面章节细讲) | global java.util.List resultList |
小技巧:如果你有大量import,可以像Java一样用通配符:import com.example.model.*。但别滥用,会影响编译性能。
4.3 rule关键字:规则的灵魂
每个规则都以rule开头,以end结束。规则名用双引号包裹,建议起得有意义——因为日志和监控里会直接显示这个名字。
rule "规则名称"
// 属性部分(可选)
salience 10
no-loop true
when
// 条件部分
then
// 动作部分
end
规则名可以重复吗?不可以。同一个KIE Base里,规则名必须唯一。我踩过这个坑——两个同名规则,后加载的会覆盖先加载的,而且没有任何警告。
4.4 when与then:条件与动作
when是规则的左半边(LHS),then是右半边(RHS)。
when部分:写条件,匹配工作内存中的Fact对象。条件可以组合:
when
$order: Order(totalAmount > 500, status == "NEW")
$customer: Customer(id == $order.customerId, age >= 18)
这里$order和$customer是绑定变量。你想想看,它们就像指针,指向匹配到的具体对象。在then里就能直接用。
then部分:写动作,通常是修改Fact、调用服务、打印日志等。注意,then里写的是Java代码:
then
$order.setDiscount(0.2);
$order.setStatus("DISCOUNT_APPLIED");
update($order); // 通知规则引擎对象已修改
System.out.println("订单 " + $order.getId() + " 已享受20%折扣");
避坑指南:在then里修改了Fact对象后,记得调用update()或modify()。我曾经漏掉这一步,结果规则明明改了数据,后续规则却感知不到——排查了整整一下午。
4.5 注释写法:好记性不如烂笔头
DRL支持三种注释方式,和Java几乎一样:
// 单行注释:解释这条规则是干嘛的
/*
* 多行注释:
* 可以写一大段说明
* 比如规则的历史变更记录
*/
/**
* Javadoc风格注释:
* 常用于规则文件头部,说明文件用途
*/
我个人习惯在每条规则前加一段单行注释,说明触发条件和预期效果。比如:
// 规则:新用户首单满100减20
rule "新用户首单优惠"
when
// ...条件
then
// ...动作
end
为什么强调注释?因为规则是业务人员也要看的。你写的规则,三个月后可能连你自己都忘了逻辑。注释就是给未来的自己留的便签。
4.6 知识体系总览
下面这张图,把DRL文件的核心结构串起来了。建议你保存下来,写规则时对照着看:
4.7 实战:写一个完整的DRL文件
理论说完了,咱们动手写一个。假设有个电商场景:订单金额超过1000元,且客户是VIP,就给打8折。
package com.example.order.discount
import com.example.model.Order
import com.example.model.Customer
// 规则:VIP大额订单打8折
rule "VIP大额订单折扣"
salience 10
no-loop true
when
// 匹配VIP客户
$customer: Customer(vipLevel >= 1)
// 匹配该客户的大额订单
$order: Order(
customerId == $customer.id,
totalAmount > 1000,
status == "NEW"
)
then
// 计算折扣金额
double discount = 0.2;
$order.setDiscount(discount);
$order.setStatus("DISCOUNT_APPLIED");
// 通知引擎对象已修改
update($order);
System.out.println("订单 " + $order.getId() +
" 已享受20%折扣,原价:" + $order.getTotalAmount());
end
个人经验:我建议你在then里加日志输出。规则跑起来后,日志就是你的眼睛。线上排查问题时,日志能帮你快速定位是规则没触发,还是触发了但动作没生效。
4.8 常见错误与避坑
最后,分享几个我踩过的坑:
- 漏写end:每个rule必须有一个end。少写了,编译直接报错。
- 规则名重复:同一个KIE Base里规则名必须唯一。后加载的会静默覆盖前一个。
- 忘记update:在then里修改Fact后,如果不调用update(),后续规则感知不到变化。
- import写错位置:import必须写在package之后、所有rule之前。
- 条件里用==比较字符串:DRL里字符串比较用
==,但前提是两边都是String类型。如果一边是null,会抛NPE。
避坑指南:我曾经在生产环境遇到一个诡异问题——规则有时触发有时不触发。排查了半天,发现是规则名写错了,两个规则用了同一个名字,后加载的覆盖了前一个。从那以后,我强制团队在规则名里加上模块前缀,比如"ORDER-DISCOUNT-VIP"。
好了,DRL语法基础就讲到这里。记住:package打头,import跟上,rule包住when和then,注释写清楚。这个套路熟了,写规则就跟写Java方法一样自然。