从hello world开始逐步理解drools的核心元素

最近在思考构建一个服务编排(Service Orchestration)系统,考虑这个系统至少需要具备以下特征:

  1. 使用统一的方法定义服务功能单元
  2. 使用一种通用的方式将一个或多个服务的输出映射到下游服务的输入,映射时支持基础的数据转换与处理
  3. 支持以搭积木的方式将低层服务功能单元组织成更高层抽象的服务功能,直至一个完整的服务
  4. 用户编排服务时,具备较大的灵活性定制业务

1 drools介绍

Drools是一个基于Java的开源规则引擎(Rule Engine),说的更高大上一点,是一个业务规则管理系统(BRMS: Business Rule Management System)。它由JBoss(Red Hat旗下的一个开源项目)开发和支持。Drools采用易于理解的规则语言DRLDrools Rule Language)编写规则。DRL语言是一种声明性的语言,它明确的指定:"当满足某些条件时,执行某些操作",这种表达方式更接近业务语言表述,使得业务逻辑与底层代码进行解耦,变更业务规则无需改动代码,也使得它更易于理解、修改、测试和复用,更便于业务专家和开发人员相互协作。drools内部实现则基于Rete算法和逻辑编程的算法,确保引擎的高效,使得drools规则引擎在java环境下很受欢迎。

2 第一个例子:hello world

在spring boot环境下使用drools,需要在pom文件中添加以下依赖:

xml 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    
    <dependency>
        <groupId>org.kie</groupId>
        <artifactId>kie-spring</artifactId>
        <version>7.74.1.Final</version>
        <exclusions>
            <exclusion>
                <groupId>org.springframework</groupId>
                <artifactId>spring-tx</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

下面我们编写第一个规则文件,命名为test.drl(drools规则文件采用后缀.drl),它的内容为:

text 复制代码
package rules

rule "rule1"
    when

    then
        System.out.println("hello world");
end

这几乎是一个最简单的规则文件了。drools通过package来分类组织管理系统的规则文件,我们可以将不同业务、不同功能的规则文件放到不同的package下,这里需要注意的是,这里的package跟java语言中的package没有任何关系,它只是drools组织管理规则文件的一种方式,是一个命名空间,虽然大家可能在命名drools的package时采用java语言package一样的命名规范。drools的package名字一般与规则文件所放置的目录名相同。比如上面的test.drl文件,我就把它放在项目的src/main/resources目录下的rules子目录中。

drools规则文件中的rule end之间的部分就是我们定义的业务规则,一个规则文件中我们可以定义任意个这样的规则,我们一般会给每个规则取一个名字,上面的例子中规则的名字为rule1,规则名字加不加双引号都无所谓,效果都一样。每条规则有且仅有一个when then语句,从名字就知道,when后面跟一个条件,表示在什么情况下命中当前规则,上面的例子中,when后面是空的,表示不需要任何条件,这条规则总是命中的。then后面跟一系列的动作,可以修改某个状态的值,也可以调用某个方法。上面的例子中调用了System.out对象的pringln方法,从而输出hello world。drools规则可以直接使用java系统的类和对象,如果需要使用自定义的类和对象,需要在规则文件中先声明。

编写好了规则文件,我们就可以编写代码进行规则编译和执行了。基本代码如下所示:

java 复制代码
    // KieServices是drools系统服务的入口
    KieServices kieServices = KieServices.Factory.get();

    ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
    Resource[] resources = resourcePatternResolver.getResources("classpath*:rules/**/*.*");

    // 读取规则文件
    KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
    for (Resource file : resources) {
        kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH + file.getFilename(), "UTF-8"));
    }

    // 构建知识库和module
    KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
    kieBuilder.buildAll();
    KieModule kieModule = kieBuilder.getKieModule();

    // 获取指定module的container
    KieContainer kieContainer = kieServices.newKieContainer(kieModule.getReleaseId());

    // 从container中获取session
    KieSession kieSession = kieContainer.newKieSession();

    // 执行规则
    kieSession.fireAllRules();

    // 关闭session
    kieSession.dispose();

执行上面的代码就会输出下面的信息:

text 复制代码
hello world

不过,粗一看代码,这里出现了一堆drools系统相关的新类和对象,让人有点迷惑。像KieServicesKieFileSystemKieBuilderKieModuleKieContainerKieSession可能之前从没见过,其实还有几个drools系统的基础类和概念,上面没有出现,如KieRepositoryKieBaseKieProjectClasspathKieProjectFactsAgenda等,例子中没有出现,是因为都使用了默认值。不要着急,下面我试图慢慢解释。

首先,我们看到这些类的前缀都是KieKie其实是由Knowledge Is Everything一词的首字母组成的。而Kie其实是一个比较庞大的系统,它提供了一整套的工具,从创建和管理规则和流程,到执行这些规则和流程,再到展示执行结果。大家使用比较多的droolsjBPM(衍生出ActivitiCamundaFlowable等众多流行BPMBusiness Process Management)工具)都是其中的一个子项目。我们这里不介绍Kie,Kie系统的主要项目结构如下:

整个drools规则引擎系统以KieServices为统一管理的服务中心和入口,我们通过它可以创建和获取各种相关的组件和资源。KieServices其实是一个单例,一个项目只要一个实例就可以了。我们可以从KieServices开放的接口获取对应的资源,如KieContainer,而KieContainer实际是由KieProject创建的,KieProject会从KieRepository中获取KieModule,而KieModule会包含一个或多个KieBaseKieBase表示知识库,也就是我们定义的drools规则等相关资源,一个KieBase可以包括一个或多个KieSession,我们实际是通过KieSession实例进行交互,向KieSession提供规则实际的输入数据对象(drools将这些输入数据叫做Facts),然后触发规则引擎的执行动作的(执行的动作在drools中叫做Agenda)。而KieModule只是一些静态的资源,它描述了包含的规则(KieBase),需要创建的KieSession。而KieProject根据KieModule创建的引擎实例就是KieContainerKieContainer中包含了根据KieModule描述要求而创建的KieSession实例。KieFileSystem代表以文件形式存在的规则,是drools获取规则的一种介质。

drools决策引擎(可以理解为就是KieSession实例)的结构如下所示:

上图中的Production Memory就是KieBase的实例,它包含了我们定义的规则等信息。Working Memory是应用程序与drools引擎数据交互的界面。应用程序将规则输入数据存入Working Memory中,然后触发规则引擎的执行,KieSession检测到Working Memory中的对象发生了变,会触发Pattern Matcher检查Production Memory中所有规则的when条件,如果有符合条件的规则,则会生成Agenda,也就是触发规则then动作。Agenda往往会修改Working Memory中的对象的值或执行对象的方法。应用程序从Working Memory中获取相应的对象,从而获取规则引擎的结果。

KieSession分为无状态KieSession和有状态KieSession,无状态KieSession在每次触发执行后(kieSession.fireAllRules()),都会清除Working Memory中的数据,所以即使不调用kieSession.dispose()方法,前后两次执行KieSession引擎也不会相互影响。有状态KieSession则在执行完后不会清除状态数据,下一次执行还能看到上次修改的状态数据,如果不需要上次遗留的状态数据,需要执行kieSession.dispose()方法手动清除。

3 第二个例子

上面的hello world例子过于简单,很多地方用的是默认值,KieModuleKieBaseKieSession的关系也没有清晰的体现,应用程序与KieSession的交互也没有得到充分体现,下面通过一个包含多一些元素的例子进一步加以说明。

这一次我们先创建一个名字叫kmodule.xml的文件,放在项目src/main/resources/META-INF目录下,文件内容为:

xml 复制代码
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://www.drools.org/xsd/kmodule">
    <kbase name="KBase1" default="true" packages="rules.package1">
        <ksession name="KSession1_1" type="stateful" default="true"/>
        <ksession name="KSession1_2" type="stateless" default="false"/>
    </kbase>
    <kbase name="KBase2" default="false" packages="rules.package2, rules.package3">
        <ksession name="KSession2_1" type="stateful" default="false">
        </ksession>
    </kbase>
</kmodule>

这个文件清晰的描述了它包含两个KieBase,名字分别为KBase1和KBase2,并为KieBase配置了两个参数:default为true表示这个KieBase是默认KieBase,当通过KieContainer获取KieBase时,如果没提供KieBase名字,则获取的是默认KieBase,当然,默认KieBase只能有一个;packages表示KieBase的规则文件的package名字列表,多个package名字用逗号分隔。KBase1包含了两个KieSession,KSession1_1和KSession1_2,也配置了两个参数:type表示KieSession是无状态的还是有状态的,default表示是否是默认KieSession。整个KieModule中只能有一个默认KieSession。

然后我们根据上面的kmodule.xml的配置,在rules.package1下创建一个drl规则文件,文件内容为:

text 复制代码
package rules.package1

// 引入指定类,这样规则文件中能使用这些类
import movee.drools.Student;
import movee.drools.Trophy;

// 全局变量
global movee.drools.Collage collage;

/*
 * 定义一个函数,可以在rule中调用
 */
function Integer add(Integer num){
    return num + 1;
}

/*
 * query,应用程序可以根据query名字查询引擎中的数据,尤其是引擎生成的数据
 */
query "queryTrophy"(Integer scoreLevel)
    $trophy:Trophy(score >= scoreLevel)
end

rule "ExcellentStudents"
    // 这条规则不能循环执行
    no-loop true
    when
        // $student是Working Memory中匹配到条件的Student对象的名字,后面可以通过这个名字操作匹配到的对象
        // 名字不一定需要以$符号开头,只是惯例如此
        $student: Student(score >= collage.getExcellentLevelScore())
    then
        System.out.println("ExcellentStudents:" + $student.getName());
        
        // 调用Fact对象的方法
        $student.award();
        
        // 调用函数
        Integer excellentNum = add(collage.getExcellentStudentNum());
        
        // 改变Fact对象的值
        collage.setExcellentStudentNum(excellentNum);
        
        // 创建新对象,并插入Working Memory中
        Trophy trophy = new Trophy($student.getName(), $student.getScore());
        insert(trophy);
end

rule "PassStudents"
    when
        $student: Student(score >= collage.getPassLevelScore() && score < collage.getExcellentLevelScore())
    then
        System.out.println("PassStudents:" + $student.getName());
end

rule "UnPassStudents"
    when
        $student: Student(score < collage.getPassLevelScore())
    then
        System.out.println("UnPassStudents:" + $student.getName());
end

这个规则文件增加了很多元素,新增元素和用法都添加了注释,不详细解释了。不过,我们可以看到规则then中的action跟编写java程序并没有什么区别。

然后根据规则文件的内容,我们编写相应的类和代码,关键部分也添加了注释,也不再解释了。代码如下:

java 复制代码
package movee.drools;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {

    private String name;
    private Integer score;

    public void award() {
        System.out.println(name + ": you are very good and get a trophy");
    }

}
java 复制代码
package movee.drools;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Collage {

    private Integer excellentLevelScore;
    private Integer passLevelScore;
    private Integer excellentStudentNum;

}
java 复制代码
package movee.drools;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Trophy {

    private String studentName;
    private Integer score;

}
java 复制代码
    KieServices kieServices = KieServices.Factory.get();
    KieContainer kieContainer = kieServices.getKieClasspathContainer();

    // 获取session
    KieSession kieSession = kieContainer.newKieSession("KSession1_1");

    // 插入三个Fact对象
    kieSession.insert(new Student("john", 95));
    kieSession.insert(new Student("tom", 99));
    kieSession.insert(new Student("bob", 80));

    // 设置全局变量
    Collage collage = new Collage(90, 60, 0);
    kieSession.setGlobal("collage", collage);

    // 执行规则
    kieSession.fireAllRules();

    // 根据规则文件的query查询
    QueryResults queryResults = kieSession.getQueryResults("queryTrophy", 90);
    log.info("queryResults: {}", new Gson().toJson(queryResults.toList()));

    // 获取引擎处理后的结果
    log.info("excellentStudentNum of collage: {}", collage.getExcellentStudentNum());

    kieSession.dispose();

执行后,输出内容如下:

text 复制代码
2023-09-02T00:21:59.822+08:00  INFO 17103 --- [  restartedMain] o.d.c.kie.builder.impl.KieContainerImpl  : Start creation of KieBase: KBase1
2023-09-02T00:22:00.098+08:00  INFO 17103 --- [  restartedMain] o.d.c.kie.builder.impl.KieContainerImpl  : End creation of KieBase: KBase1
ExcellentStudents:john
john: you are very good and get a trophy
ExcellentStudents:tom
tom: you are very good and get a trophy
PassStudents:bob
2023-09-02T00:22:00.127+08:00  INFO 17103 --- [  restartedMain] movee.drools.DroolsApplication           : queryResults: [{"scoreLevel":90,"$trophy":{"studentName":"john","score":95}},{"scoreLevel":90,"$trophy":{"studentName":"tom","score":99}}]
2023-09-02T00:22:00.127+08:00  INFO 17103 --- [  restartedMain] movee.drools.DroolsApplication           : excellentStudentNum of collage: 2

4 更多一点了解

好了,到此,我们可以介绍一些抽象和琐碎的内容了

4.1 kmodule.xml中kbase和ksession支持的属性

4.1.1 kbase的属性

属性名 默认值 说明
name none KieBase的名称,这个属性是强制的,必须设置
includes none 意味着本KieBase将会包含所有include的KieBase的rule、process定义。非强制属性。
packages all package列表,多个package以逗号分隔。默认情况下将包含resources目录下面(子目录)的所有规则文件。
default false 取值true/false,是否默认KieBase,如果是默认的话,不用名称就可以查找到该KieBase,但是每一个module最多只能有一个KieBase。
equalsBehavior identity 取值identity/equality 顾名思义就是定义"equals"(等于)的行为,这个equals是针对Fact(事实)的,当插入一个Fact到Working Memory中的时候,Drools引擎会检查该Fact是否已经存在,如果存在的话就使用已有的FactHandle,否则就创建新的。而判断Fact是否存在的依据通过该属性定义的方式来进行的:设置成 identity,就是判断对象是否存在,可以理解为用==判断,看是否是同一个对象;
eventProcessingMode cloud 取值cloud/stream 当以云模式编译时,KieBase将事件视为正常事实,而在流模式下允许对其进行时间推理。
declarativeAgenda disabled 取值disabled/enabled,这是一个高级功能开关,打开后规则将可以控制一些规则的执行与否。

4.1.2 ksession的属性

属性名 默认值 说明
name none KieSession的名称,该值必须唯一,也是强制的,必须设置。
type stateful 取值stateful/stateless, 定义该session到底是有状态(stateful)的还是无状态(stateless)的。
default false 取值true/false, 定义该session是否是默认的,如果是默认的话则可以不用通过session的name来创建session,在同一个module中最多只能有一个默认的session。
clockType realtime 取值realtime/pseudo, 定义时钟类型,用在事件处理上面,在复合事件处理上会用到,其中realtime表示用的是系统时钟,而pseudo则是用在单元测试时模拟用的。
beliefSystem simple 取值simple/defeasible,/jtms,定义KieSession使用的belief System的类型。

4.2 drl文件的结构

4.2.1 drl文件结构

drl文件完整结构包括以下元素(第二个例子的drl文件已经包含了所有结构元素):

关键字 描述
package 包名,只限于逻辑上的管理,同一个包名下的查询或者函数可以直接调用
import 用于导入类或者静态方法
global 全局变量
function 自定义函数
query 查询
rule end 规则体

4.2.2 rule语法结构

rule完整语法结构如下:

text 复制代码
rule "ruleName"
    attributes
    when
        LHS 
    then
        RHS
end

4.2.2.1 rule支持的属性

第二个例子的drl文件的attributes部分只包括了一个属性,drools的rule支持下面这些属性:

属性名 说明
salience 指定规则执行优先级
dialect 指定规则使用的语言类型,取值为java或mvel
enabled 指定规则是否启用
date-effective 指定规则生效时间
date-expires 指定规则失效时间
activation-group 激活分组,具有相同分组名称的规则只能有一个规则触发
agenda-group 议程分组,只有获取焦点的组中的规则才有可能触发
timer 定时器,指定规则触发的时间
auto-focus 自动获取焦点,一般结合agenda-group一起使用
no-loop 防止死循环,防止自己更新规则再次触发
lock-on-active no-loop增强版本。可防止别人更新规则再次出发

4.2.2.2 rule的LHS部分(when条件)

配置Fact属性时支持的条件操作符:

符号 说明
大于
< 小于
>= 大于等于
<= 小于等于
== 等于
!= 不等于
contains 检查一个Fact对象的某个属性值是否包含一个指定的对象值
not contains 检查一个Fact对象的某个属性值是否不包含一个指定的对象值
memberOf 判断一个Fact对象的某个属性是否在一个或多个集合中
not memberOf 判断一个Fact对象的某个属性是否不在一个或多个集合中
matches 判断一个Fact对象的属性是否与提供的标准的Java正则表达式进行匹配
not matches 判断一个Fact对象的属性是否不与提供的标准的Java正则表达式进行匹配

Fact属性多个条件支持 &&、||、!、()进行条件组合

LHS多个条件支持通过and、or、not、()进行条件组合

4.2.2.3 rule的RHS部分(then动作)

drools提供了一些内置方法,包括:

方法 功能说明
insert 向Working Memory插入新的Fact对象,并触发规则重新评估计算
update 更新Working Memory中Fact对象的值,并触发规则重新评估计算
modify 也是更新Working Memory中Fact对象的值,只是更新的语法有区别,并触发规则重新评估计算
retract 删除Working Memory中的Fact对象,并触发规则重新评估计算
相关推荐
明月_清风8 小时前
Foundry Invariant Testing 实战:ERC-4626 + Handler + Ghost Variable
后端·web3·solidity
明月_清风8 小时前
Foundry Invariant Testing:让测试自动寻找复杂状态下的 Solidity Bug
后端·web3·solidity
EXI-小洲8 小时前
Spring AI (第二章)大模型对话上下文记忆
java·人工智能·spring
扬大平仔8 小时前
# 小深:用 AgentScope Java 2.0 Harness 做私人助手(上)mysql
java·开发语言·mysql
java porter8 小时前
我开源了一个 Spring AI Agent 项目
后端
阿里云基础软件8 小时前
一句话看透 JVM,SysOM 诊断 Skill 新增 Java 应用诊断能力
java·开发语言·jvm·人工智能·操作系统·sysom 诊断 skill
Wang's Blog8 小时前
Java框架快速入门: Spring Security+OAuth2之自动化集成测试
java·spring·自动化
136096757238 小时前
AgentScope 2.0 学习笔记:RAG 进阶——Top-K 调优与幻觉测试(让 Agent 敢说「不知道」)
后端
yume_sibai9 小时前
07-Rust 异步编程完全指南(async/await + Tokio + Future + 并发原语 + 异步流)
开发语言·后端·rust
Sylvia33.9 小时前
板球实时数据接入实战:从Frames模型到多赛制适配
java·python·websocket·网络协议·架构