1 Drools概述

1.1 什么是规则引擎

规则引擎是伴随着IT系统发展、业务复杂度提升而发展起来的, 将业务决策功能从代码实现中剥离的引擎系统。规则引擎用自身可识 别的语言来描述和编写业务规则,它接收输入参数(数据),通过预编译或预加载的规则推导出结果,供调用方使用或直接触发外部系统 接口,以对输入事件做出反馈(动作)。

规则引擎把业务规则的编写和修改工作从业务系统的开发人员身上转移到具体系统运营的业务人员身上,因而避免了从业务到代码再 到业务的长链路知识传递过程,降低了出错率。规则引擎还减少了因为业务变更而重新发布系统的次数,增加了系统的健壮性,提升了研发效率,缩短了从想法到实现的周期,从而提高了效益。

1.2 Drools是什么

Drools是业务规则管理系统(BRMS)的一种实现方式。它提供了核心业务规则引擎(BRE)、基于Web的规则编写和管理的控制台 (Drools Workbench)。它能运行基于DMN(决策模型和表示法)定义的决策模型,还能用来导入和运行遵从PMML(预言模型标记语言)的机器学习预测模型。

Drools是用Java实现的规则引擎,是由JBoss公司发起的100%开源项目,遵从Apache 2.0的宽松开源协议。

Drools社区的顶级项目有以下几个。

  • Drools Workbench:规则编写和管理的Web管控台。

  • Drools Expert:核心业务规则引擎。

  • Drools Fusion:复杂事件处理。

  • jBPM:流程引擎、规则流引擎。

  • OptaPlanner:约束求解器,轻量级规划调度引擎。

1.3 Drools的组件

Drools包含以下几个组件。

  • Business Central:业务中心,是规则编写和管理的Web控制台

  • KIE Server:规则执行的服务器,可以部署在任何Web服务器上

  • Asset Repository:一个Git库,用来保存编写好的规则和相关文件

  • Artifact Repository:一个制品库,用来保存编译打包好的制品,如kjar文件等。

这几个组件之间的关系如图所示。

image-20241015220432737

通常,用户会登录到Business Central中编写规则,将规则保存到Asset Repository中,再将规则编译、构建、发布成kjar文件保存 到Artifact Repository中, 最后通过Business Central创建KIE Server,将规则对应的kjar文件部署到KIE Server中运行,以供外部系统调用

1.4 Drools的核心概念

1.5.1 规则语言

Drools是以声明方式编写规则的,它目前支持两种规则语言:Java和MVEL(MVFLEX Expression Language)。Drools定义的规则保存在以DRL(Drools Rule Language)为扩展名的文件中,在Drools的领域内,这个带有规则定义的DRL文件通常被称为规则文件。一个简单的Drools规则定义通常由以下几部分组成

package 规则所需的包
​
rule 规则的名称
​
when 
    出发规则的条件,也成为规则左手边
then
    出发后规则所做的动作,也成为规则的右手边
end
  • package对规则的存放位置进行了定义,作用同Java里的package。

  • rule、when、then、end是规则语言的保留关键字

  • 规则的名称用来标识被定义的规则,同一个包下的规则名必须唯一

  • when定义了规则的触发条件

  • then定义了规则被触发后要做的动作

一个规则只能存放在一个规则文件中,不能跨多个规则文件存放,而一个规则文件中可以存放多个规则。简单来说,Drools的规则是由一个或多个“如果”(when)和“那么”(then)组成的,描述的是“如果满足这些条件,那么就做那些事情”。

1.5.2 事实对象

事实对象(fact)是Drools用来评估条件和执行结果的模型对象,也称为事实数据。

  • 事实对象可以简单地理解为Java的POJO类(Plain Old Java Object)

  • 事实对象可以有自己的函数,提供给规则引擎在“那么”的部分调用

  • 事实对象可以从数据库中加载

  • 事实对象不需要继承任何类或实现某些接口。

Drools要求这些事实对象必须遵从Java Beans的规范。事实对象根据其产生方式可以分为以下两种类型。

  • Stated fact(陈述事实对象):规则调用者提供给规则的事实对象

  • Inferred fact(推断事实对象):规则引擎根据调用者提供的陈述事实通过计算推导出的事实对象,推断事实可能会随着时间的变 化而改变。

比如,在一个商品促销的场景下,我们要根据顾客的会员级别和购买金额计算出顾客此次购物的折扣率,顾客的会员级别和此次购买金额是陈述事实,而根据会员级别和购买金额计算出的折扣率就是推断事实

1.5.3 决策引擎

决策引擎(Decision Engine)是Drools的核心,也可以称作Drools的“大脑”。决策引擎从生产内存(Production Memory)中加载定义的规则,再从工作内存(Working Memory)中读取事实对象,然后根据规则条件,用Phreak算法进行模式匹配 (Pattern Matching),也就是执行规则定义中的“如果”部分,如果匹配成功就把相应的动作(规则定义中的“那么”)部分放到议程 (Agenda)队列中

image-20241015221413067

  • 议程是规则引擎的触发事件队列,所有已经匹配的规则都会在议程中排队,等待规则引擎逐个执行。

  • 生产内存是保存编译后的Drools规则的位置。

  • 工作内存是事实对象(数据)的存放位置,外部提供给规则引擎的陈述事实和规则引擎产生或修改后的推断事实都保存在工作内存中

  • Phreak算法是新版Drools引入的、改进后的ReteOO算法,它会将编译后的规则组成Phreak网络,保存在生产内存中。

2 Drools入门

2.1 Hello Drools

这里用IDEA来做测试,可以先安装下插件

image-20241015222009722

引入maven依赖

<properties>
    <logbok.version>1.5.0</logbok.version>
    <lombok.version>1.18.30</lombok.version>
    <drools.version>7.63.0.Final</drools.version>
    
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
​
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-bom</artifactId>
            <version>${drools.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
​
<dependencies>
    <dependency>
        <groupId>org.drools</groupId>
        <artifactId>drools-engine-classic</artifactId>
    </dependency>
​
    <dependency>
        <groupId>org.drools</groupId>
        <artifactId>drools-model-compiler</artifactId>
    </dependency>
</dependencies>

规则文件如下[hello.drl]:

package io.adrainty.drools;
​
rule "hello drools"
when
    $message: String()
then
    System.out.println($message);
end

在resource资源目录下创建kmodule.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://www.drools.org/xsd/kmodule">
    <!--
        This is a minimal kmodule.xml. Ref Drools documentation,
        see http://docs.jboss.org/drools/release/7.59.0.Final/drools-docs/html_single/index.html#_creatingandbuildingakieproject
    -->
</kmodule>

创建一个Java文件来执行这个文件吧

public class DroolsHelloWorld {
​
    public static void main(String[] args) {
        KieServices kieServices = KieServices.Factory.get();
        KieContainer kContainer = kieServices.getKieClasspathContainer();
        KieBase kieBase = kContainer.getKieBase();
        KieSession session = kieBase.newKieSession();
​
        session.insert("Hello Drools!");
        session.fireAllRules();
    }
​
}

整个目录如图所示:

image-20241015223812814

执行后可以看到控制台输出了我们想要的内容:

image-20241015223853504

2.2 规则语法解读

我们之前的规则语法如下:

package io.adrainty.drools;
​
rule "hello drools"
when
    $message: String()
then
    System.out.println($message);
end

该文件说明如下:

  • 该规则文件保存在io.adrainty.drools包下,只有一个名称为“hello drools”的规则。在这个包下不允许再有名称为“hello drools”的规则,因为Drools规定在同样的包下规则的名称必须唯一。

  • 规则的名称为“hello drools”

  • hello drools规则中when部分定义的是$message:String(),其含义是:如果在当前的工作内存中存在一个String型的事实对象,就把这个事实对象和变量$message绑定。

  • hello drools规则中then部分没有定义对事实数据的操作,只是借助绑定的$message变量,把匹配的String类型的事实对象的内容输出到控制台。

2.3 Drools的模式匹配

为什么$message:String()就能判断出工作内存中是否存在一个String类型的事实对象?Drools是怎么把这个选中的事实对象绑定到$message:上的呢Drools规则语言是以声明方式来编写规则的,它以函数式编程的方式进行模式匹配。

举个例子,假如有一个Customer对象的定义

public class Customer {
    private String id;
    private String loyaltyLevel;
    private String name;
}

那么,就有:

  • 匹配所有Customer的模式为:Customer()

  • 匹配金卡会员的模式为:Customer(loyaltyLevel == "gold")

  • 匹配金卡会员,获取会员级别和顾客名字并绑定到变量的模式为: Customer($status: loyaltyLevel == "gold", $name: name)

  • 匹配金卡会员,获取顾客本身并绑定到变量的模式为:$customer: Customer(loyaltyLevel == "gold")

  • 通过绑定变量匹配金卡会员的模式为:$customer: Customer(loyaltyLevel == $name)

image-20241015224511342

模式匹配绑定变量前的“$”可以省略,如果省略,则在引用此变量时也不需要在变量前加“$”

规则“如果”部分的规则触发条件可以是多个事实对象的模式匹配组合。假设我们添加了如下Order对象定义:

public class Order {
    private String customerId;
    private int guant;
}

则匹配所有金卡会员顾客中所下订单数量大于100的模式为Customer(loyaltyLevel == "gold",id: id) && Order(customerId == id, quant > 100)

Drools中常用条件约束符有&&、||、and和or。

2.4 规则工程解读

Hello Drools是一个基于Maven的Java工程,在工程管理的pom.xml文件中引入了如下依赖:

 <dependency>
     <groupId>org.drools</groupId>
     <artifactId>drools-engine-classic</artifactId>
</dependency>

<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-model-compiler</artifactId>
</dependency>
  • 引入的drools-engine-classic依赖实现了Drools规则引擎的运行

  • 引入的drools-model-compiler依赖实现了把不同类型的规则定义(文本、Excel表等)转换成规则引擎所能识别的、可以执行的规则

src/main/resources/META-INF/kmodule.xml中定义了如何加载规则文件(DRL),内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://www.drools.org/xsd/kmodule">
    ...
</kmodule>

在Hello Drools的项目中采用默认方式加载规则文件,没有多个kbase和不同种类的ksession,因而不需要明确指定kbase、 ksession的加载项。

在Main方法中,我们以JUnit方式启动规则引擎

public class DroolsHelloWorld {

    public static void main(String[] args) {
        // 通过KieServices的工厂方法获取到KieServices的对象实例kieServices
        KieServices kieServices = KieServices.Factory.get();
        // kieServices实例根据当前JVM环境的类路径(classpath)找到kmodule.xml,进行规则运行环境的初始化并生成kContainer。
        KieContainer kContainer = kieServices.getKieClasspathContainer();
        // 从kContainer中获取kieBase。
        KieBase kieBase = kContainer.getKieBase();
        // 用kieBase创建新的session(KieSession会话)
        KieSession session = kieBase.newKieSession();

        // 触发规则
        session.insert("Hello Drools!");
        session.fireAllRules();
    }

}

3 Drools规则语言

一个完整的规则语言由一下几部分组成

package 规则所属的包

import 导入规则的依赖

function 函数定义(可选)

query 查询定义(可选)

declare 类型声明(可选)

global 全局变量定义(可选)

rule 规则1的名称
when 
	出发规则的条件,也成为规则左手边
then
	出发后规则所做的动作,也成为规则的右手边
end

rule 规则2的名称
...
  • 一个DRL文件可以包含单个或多个规则、查询和函数。

  • 可以在DRL文件中定义规则、查询和函数所需的资源,例如规则的依赖、全局变量、类型声明等。

  • 规则所属的包名必须放到DRL文件的顶部

  • 规则的定义通常放到DRL文件的末尾。

  • 对DRL文件的其他部分没有强制的顺序要求。

接下来我们分别看一下各个部分

3.1 包定义

包是Drools存放资产的文件夹,用法同Java语言里的包。包包含规则的定义文件(以DRL为扩展名)、数据对象、决策表等其他类型的资产。包同时是每组规则的命名空间,相同命名空间下的规则名必须唯一

一个规则库可以有多个包。Drools遵从Java的规范,将规则的定义与包内资产的声明存放在同一个文件中,不同包之间的规则或对所依赖Java库的引用,需要先通过import导入当前的命名空间中,或者不导入,而通过全路径的方式使用

规则库是指规则编译后的,可以独立部署和运行的kjar包(如customer-rules.jar)。kjar是KIE Jar的缩写,我们可以把kjar理解为包含规则定义的Jar

3.2 依赖导入

Drools规则语言的import与Java中的import语句类似,用于导入外部依赖项,需要在DRL文件中导入所依赖对象的完整路径和类型才可以在规则文件中使用。导入的格式为:

import packageName.objectName;

如果有多个依赖,建议分多行导入,Drools引擎默认已经导入java.lang包。

示例:

import io.adrainty.drools;
import java.util.List;

3.3 函数

在编写规则的时候,为了重用多次出现的规则代码逻辑,我们可以把这部分逻辑抽取出来放到规则依赖的Java库中,也可以直接在规则文件中定义可重用的函数,使用示例如下:

package com.example;

...
function String hello(String yourName) {
    return "Hello " + yourName + " !";
}

rule "规则1"
when
	...
then
	System.out.println( hello{ "AdRainty" } )
end
...

如果引用的函数与当前的规则在不同的包下,可以通过如下方式导入后使用:

import function com.example.hello;

3.4 查询

查询用来从决策引擎的工作内存中查找出符合指定条件的事实数据。我们可以在DRL文件中以如下方式定义查询:

query "年龄小于18岁的人"
	$person : Person{ age < 18 }
end

以上查询的名称为“年龄小于18岁的人”,它能从工作内存中查找出所有年龄小于18岁的Person对象。

QueryResults results = ksession.getQueryResults("年龄小于18岁的人");
System.out.println("有" + result.size() + "人年龄小于18岁");

for (QueryResultRow row: results) {
    Person person = (Person) row.get("person");
    System.out.println(person.getName());
}

以上代码从已创建好的ksession中通过查询名“年龄小于18岁的人”获取该查询的结果,保存在QueryResults对象中,再通过QueryResultsRow对象迭代查找,获取返回集中的每个Person对象

查询的名称在规则工程中是全局的,因此在一个规则工程中查询的名称必须唯一,不能重复。

3.5 类型声明

我们可以在Java中定义规则所需要的数据类型,也可以直接在DRL文件中声明。在DRL文件中还可以声明枚举类型,类型之间可以继承。

3.5.1 普通数据类型定义

以下是在DRL文件中声明Person类型并在规则中使用的示例:

import java.util.Date

declare Person
	name: String
	dateOfBirth: Date
	address: Address
end

rule "使用DRL内声明的类型"
	when 
		$p: Person( name -- "Jonkey" )
	then
		Person person = new Person();
		person.setName( "Benny" );
		insert( person )
end

以上在DRL中声明的Person类型,规则引擎会将其解析成如下的Java对象

public class Person implements Serializable {
    private String name;
    private java.util.Date dateOfBirth;
    private Address address;
    
    // 无参和全参构造函数
    // getters和setters
    // equals和hashCode, toString
}

3.5.2 枚举类型定义

以下是枚举类型DaysOfWeek的定义与使用示例,枚举项之间用逗号分隔

declare enum DayOfWeek 
	SUN("Sunday"),Mon("Monday"),TUE("Tuesday").....;
  fullName: String
end

rule "使用枚举类型"
when 
	$emp: Employee( dayOff == DayOfWeek.MON )
then
	...
end

3.5.3 类型的继承

import com.example.Person

declare Person
end

declare Student extends Person 
	school: String
end

3.5.4 全局变量

规则用到的全局变量需要在DRL文件中声明

global com.example.Person systemAdmin;

rule "使用全局变量"
    when
    then
        systemAdmin.setName("Jonkey");
end

在上面的规则文件中,我们用global关键字定义了类型为com.example.Person的全局变量systemAdmin,并在规则的执行部分对全局变量进行赋值,Java使用如下:

Person admin = new Person();
KieSession kieSession = kiebase.newKieSession();
kieSession.setGlobal("systemAdmin", admin);

在以上的示例中,我们用全局变量进行数据传递。全局变量也可以被规则用于调用对外部服务

global com.example.EmailService emailService;

rule "使用全局服务"
    when 
    then
    	emailService.send("aaa@bb.com", "email context")
end

3.6 规则属性

我们可以为指定规则添加额外的属性以描述该规则的特定行为, 格式如下

rule "规则名"
	// 属性1
	// 属性2
	when 
		// 条件
	then
		// 动作
end

属性

说明

使用实例

salience

规则优先级,整型,值越大优先级越高。在规则的激活队列中,优先级高的规则会先执行

salience 10

enabled

规则是否生效的开关标识符,布尔类型,true代表开启,false代表关闭

enabled true

date-effective

规则的生效时间,字符串类型的时间描述,只有当前系统时间在生效时间之后,规则才有效

date-effective "16-Oct-2021"

date-expires

规则的失效时间,字符串类型的时间描述,当前系统时间在生效时间之后,规则失效

date-expires "16-Oct-2021"

no-loop

规则防自身触法标识,布尔类型,指定在规则的执行部分对事实对象的修改不再触法规则本身。事实对象修改后,规则自身满足了再次触法的条件,设置此标识后,自身的规则将不会被再次触法,防止非期望的无穷递归

no-loop true

agenda-group

规则的议程组,字符串类型,是对已经激活的规则执行部分的分组,分组后的规则只有在回去焦点后才能执行

agenda-group "GroupName"

activation-group

规则的激活组,字符串类型,是对激活规则的分组。在同一激活组中,只有一个规则被激活,其余的规则在被激活规则执行后将不会被激活,该属性经常与salience结合使用

activation-group "GroupName"

duration

规则激活后被执行的延迟时间,长整型,单位为毫秒。如果规则满足触法的条件,则在该属性定义的时间段后被执行

duration 100000

timer

规则的定时调度,可以是周期调度或定时调度</br>周期的调度格式:timer(int:<initial delay><repeat interval>?)</br>定时调度格式:timer(cron: <cron expression>)

timer(int: 30s)</br>timer(int: 30s 5m)</br>timer(cron: * 0/15 * * * ?)

calendars

基于quartz的规则定时调度

calendars "* * 0-7,18-23 ? * *"

lock-on-active

规则组的防自身触法标识,布尔类型,在规则所在的组(规则的议程组或激活组)被激活、修改了事实数据后,该组的所有规则都不会被自身组内修改而触法

lock-on-active true

ruleflow-group

规则流分组,字符串类型,在规则流的使用场景下,只有该规则组内的规则才有机会被激活

ruleflow-group "GroupName"

dialect

规则实现的方言,字符串类型,目前支持的只有Java和Mvel

dialect "JAVA"

3.7 规则的条件

规则的条件用于判断该规则是否需要触发。只有满足了规则的触发条件,其动作部分才会执行。规则的条件通常是由一个或多个模式和约束组成的

3.7.1 空条件

如果规则的条件部分为空,Drools会把空条件当作true,该规则的动作部分会在决策引擎接收到fireAllRules()调用的时候执行,相当于eval(true),eval是Drools的表达式计算函数,它会对传递给它的表达式进行计算,比如eval(1+2),结果是3。

3.7.2 复合条件

当规则的条件部分由多个条件组合而成(称为多个模式)时,条件之间可以通过复合条件的关键字连接,如and、or、not等。如果条件之间没有提供复合关键字,则Drools会默认将条件之间的复合关系 视为and

3.7.3 条件运算符

Drools不但支持Java中的标准运算符,还针对规则添加了扩展运算符

  • ()子属性访问

// 子属性没有分组访问方式
Person( name == "jonkey", address.city == "shenzhen", address.country == "cn")

// 分组访问方式
`Person( name == "jonkey", address.(city == "shenzhen", country == "cn"))
  • #转换成子类型

// 将address转换成address的子类型LongAddress
Person(name == "jonkey", address#LongAdress.country = "cn")

// 将address转换成address的全路径类型org.domain.LongAddress
Person(name == "jonkey", address#org.domain.LongAdress.country = "cn")

// 多次子类型转换
Person(name == "jonkey", address#LongAdress.country#DetailedCountry.population > 10000000)
  • !.非空访问

Person( $streetName: address!.street )

// 相当于下面格式
Person( address != null, $streetName: address.street )
  • []集合元素访问

Person(childList[0].age == 18)
  • matched与not matches: 匹配和不匹配正则表达式

Person (country matched "(USA)?\\S*CN")
  • contains与not contains:集合中包含和不包含元素

PamilyTree(countries contains "CN")
  • memberOf与not memberOf:元素是否属于集合

PamilyTree(person memberOf $europeanDescendants)
  • soundslike:英文发音是否几乎相同

// 匹配名字是Jon或John
Person(firstName soundslike "John")
  • str:字符串判断

Message(routingValue str[startsWith] "R1")
Message(routingValue str[endsWith] "R2")
Message(routingValue str[length] 17)
  • in与notin:值是否在集合中

Person($color: favoriteColor)
Color(type in ("red", "blue", $color))

3.8 规则的动作

规则的动作是规则触发后要执行的动作。常见的动作有对工作内存中的事实对象进行插入、修改、删除操作。在规则的动作部分不宜放过多的逻辑,如果逻辑过多,可以适当考虑将规则拆分为多个规则,以降低规则的复杂度。

3.8.1 规则动作中常用的方法

Drools提供了如下不需要借助drools变量而可以直接使用的方法来操作工作内存。

  • set: 设置属性值

set<field>(<value>)

$application.setApproved(false)
  • update: 通知决策引擎实时数据已经变更

通过set修改了事实数据的属性值,需要通过调用update来通知决策引擎,事实数据已经改变,需要重新进行规则匹配以触发新的规则执行。

update (<Object>)

loanApplication.setAmount(100);
update(loanApplication)
  • modify:修改属性值并通知决策引擎

modify(<fact-expression>) {
	<expression>,
	<expression>,
	...
}

modify(loanApplication) {
	setAmount(100),
	setApproved(true)
}
  • insert:将事实对象插入工作内存中

insert(new <oBJECT>);

insert(new Applicant());
  • delete:从工作内存中删除事实对象

delete(<oBJECT>);

delete(applicant);
  • insertLogical: 用insert向工作内存中插入事实对象后,如果该对象不再需要,我们要显式调用delete将其从工作内存中删除。Drools提供了insertLogical方法。用insertLogical插入工作内存中的对象,在后续不再满足当初插入该事实对象的规则条件时,决策引擎会自动将其删除

insertLogical(new <oBJECT>);

insertLogical(new Applicant());

3.8.2 借助drools变量引用的规则动作方法

除了常用的规则动作外,Drools还提供了基于drools变量引用的规则动作方法。

  • drools.getRule().getName():返回当前触发规则的名称。

  • drools.getMatch():返回当前触发规则的匹配信息,可以用来作为日志输出或辅助调试。

  • drools.getKieRuntime().halt():在外部通过fireUntilHalt()激活决策引擎的规则评估后,引擎一直处于活动状态,需要显式调用drools.getKieRuntime().halt()方法才能停止引擎的当前活动。

  • drools.getKieRuntime().getAgenda():返回KIE会话议程的引用,通过该引用可以进一步获取规则激活组、议程组和规则流组。

  • drools.getKieRuntime().setGlobal():设置全局变量。

  • drools.getKieRuntime().getGlobal():获取全局变量。

  • drools.getKieRuntime().getGlobals():获取所有的全局变量。

  • drools.getKieRuntime().getEnvironment():返回Drools运行的环境变量。

  • drools.getKieRuntime().getQueryResults(query) :返回指定查询的结果

3.9 注释

Drools规则中的注释与Java语言中的注释相同:用双反斜线//标识单行注释,用/*…*/标识多行注释,决策引擎会忽略被标识为注释的内容。

3.10 错误提示

Drools提供了标准格式的错误提示,以便于我们对规则进行调试和除错。错误提示的格式如图3-1所示,各块的说明如下。

image-20241020230639049

  1. 出错码。

  2. 出错的行号和列号。

  3. 错误问题描述。

  4. 错误所在的DRL文件中的位置(rule,function,query)

  5. 错误所在的DRL文件中的匹配模式。