规则语言DRL:语法结构、包声明、import、全局变量、函数、查询

好,咱们今天来啃一块硬骨头——DRL语法。说实话,我刚接触Drools那会儿,看着这堆规则文件也是一脸懵。但后来我发现,只要把它的骨架摸清楚,写规则就跟填表格一样简单。

DRL的全称是Drools Rule Language,说白了就是Drools自己的一套规则描述语言。它不像Java那么啰嗦,也不像YAML那么死板。我个人习惯把它看作「带逻辑的配置文件」。

一、DRL的语法结构

一个完整的DRL文件,通常长这样:

package com.example.rules

import com.example.model.Order
import com.example.model.Customer

global com.example.service.NotificationService notificationService

function boolean isVipCustomer(Customer c) {
    return c.getTotalSpent() > 10000;
}

query "findHighValueOrders"
    $order: Order(amount > 5000)
end

rule "VIP客户优先调度"
    when
        $c: Customer(vip == true)
        $o: Order(customerId == $c.id, status == "pending")
    then
        $o.setPriority(1);
        notificationService.notify("VIP订单已优先调度");
        update($o);
end

嗯,这里要注意,DRL文件里每个部分都有它存在的意义。咱们一个一个拆开讲。

二、包声明(package)

包声明,说白了就是给规则文件起个命名空间。跟Java里的package一个道理。

package com.energystorage.rules.dispatch

我在项目中遇到过一个问题:两个团队各自写了规则,结果包名冲突了,规则互相覆盖。后来我们强制规定,包名必须跟模块路径一致,比如调度模块就用dispatch,告警模块就用alert

避坑指南:包名不要用中文,不要用特殊字符。我曾经见过有人用空格,结果Drools直接报错,查了半天。

三、import(导入)

import的作用,就是把Java类引入到规则里。你想想看,规则里要操作订单、电池、调度任务,这些对象总得有个来源吧?

import com.energystorage.model.Battery
import com.energystorage.model.DispatchTask
import java.util.List

我个人习惯把import分成三组:

  • 模型类:Battery、Order、Customer这些实体
  • 工具类:List、Map、Date这些
  • 服务类:全局变量里要用到的服务

为什么要分组?因为规则文件一多,import乱成一锅粥,你根本不知道哪个类是从哪来的。

四、全局变量(global)

全局变量,是DRL里一个很特别的存在。它允许你在规则里引用外部的Java对象,比如日志服务、数据库连接、消息队列。

global com.energystorage.service.LogService logService
global com.energystorage.service.AlertService alertService

但是!这里有个大坑——全局变量不能在规则里被修改。你只能用它,不能改它。为什么?因为Drools的规则引擎是并发执行的,多个规则同时修改一个全局变量,那不乱套了?

核心要点:全局变量是只读的。如果你想在规则里修改数据,请用Fact对象,然后调用update()方法。

我曾经有个同事,非要在规则里给全局变量赋值,结果跑出来的结果每次都不一样。排查了两天才发现,是全局变量被多个规则线程同时写了。

五、函数(function)

函数,就是DRL里的「小工具」。当你在多个规则里重复写同一段逻辑时,就该把它抽成函数了。

function boolean isPeakHour(int hour) {
    return hour >= 10 && hour <= 14 || hour >= 18 && hour <= 21;
}

function double calculateChargeCost(double price, double quantity) {
    return price * quantity * 0.95; // 充电有5%损耗
}

你想想看,如果没有函数,每个规则里都要写一遍hour >= 10 && hour <= 14,万一哪天高峰时段变了,你得改多少个地方?

我个人习惯把函数放在DRL文件的顶部,紧跟在import后面。这样阅读规则时,先看到工具函数,再看具体规则,思路很清晰。

小技巧:函数里可以调用Java的静态方法。比如Math.max()String.format(),这些都能直接用。

六、查询(query)

查询,是DRL里一个被很多人忽略的功能。它允许你在规则引擎的工作内存里,按条件查找Fact对象。

query "findBatteriesBySoc"
    $b: Battery(soc > 80)
end

query "findOrdersByCustomer"
    $c: Customer()
    $o: Order(customerId == $c.id)
end

查询有什么用?我举两个实际场景:

  • 调试时:你想看看当前工作内存里有哪些电池的SOC超过80%,直接调这个查询就行
  • 外部调用时:Java代码里可以调用ksession.getQueryResults("findBatteriesBySoc"),拿到结果列表

嗯,这里要注意,查询不会触发规则。它只是「看一眼」,不会修改任何数据。所以你可以放心地用。

七、知识体系总览

说了这么多,咱们用一张图把DRL的骨架串起来:

DRL规则文件结构 package 包声明 命名空间,防止规则冲突 import 导入 引入Java类:模型、工具、服务 global 全局变量 只读引用外部服务,不可修改 function 函数 复用逻辑,减少重复代码 query 查询 查找工作内存中的Fact对象 rule 规则(when → then) 条件匹配 → 执行动作,核心业务逻辑所在

这张图把DRL的六个核心部分串起来了。从上到下,依次是包声明、import、全局变量、函数、查询,最后是规则。每一层都有它自己的职责,缺一不可。

八、实战中的避坑指南

最后,分享几个我踩过的坑:

  • 包名别乱写:我曾经把包名写成com.energy.storage,结果跟另一个模块的包名冲突了。后来统一改成com.energystorage.dispatch,再没出过问题。
  • import别偷懒:有人喜欢用import com.energystorage.model.*,但Drools不支持通配符导入。老老实实一行一个类。
  • 全局变量别乱改:记住,全局变量是只读的。想改数据?用Fact对象加update()。
  • 函数别写太复杂:函数里不要写超过20行的逻辑。太复杂了,不如拆成Java工具类。
  • 查询别滥用:查询适合调试和外部调用,但不要在规则里频繁调查询,性能会受影响。

好了,DRL的语法结构就讲到这里。记住这六个核心部分,写规则就跟搭积木一样简单。下一节,咱们会深入规则的条件部分——when,看看怎么写出精准的匹配条件。


公众号:蓝海资料掘金营,微信deep3321