第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文件的核心结构串起来了。建议你保存下来,写规则时对照着看:

DRL文件结构总览 DRL文件 头部声明 规则体 可选部分 package / import / global rule ... end function / query / declare 属性部分(salience / no-loop / ...) when(条件 / LHS) then(动作 / RHS) 💡 规则体是DRL的核心,每个rule必须包含when和then,属性部分可选

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方法一样自然。