使用 Drools 定义和执行业务规则
本指南演示了您的 Quarkus 应用程序如何使用 Drools 添加智能自动化功能,并使用 Drools 规则引擎为其提供动力。
先决条件
要完成本指南,您需要
-
大约 15 分钟
-
一个 IDE
-
已安装 JDK 17+ 并正确配置了
JAVA_HOME
-
Apache Maven 3.9.9
-
如果您想使用它,可以选择 Quarkus CLI
-
如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置
简介
Drools 是一组专注于智能自动化和决策管理的项目,最值得注意的是提供了一个前向链接和后向链接的基于推理的规则引擎、DMN 决策引擎和其他项目。规则引擎是创建专家系统的基本构建块,在人工智能中,专家系统是一种模拟人类专家决策能力的计算机系统。您可以在 Drools 网站上阅读更多信息。
Drools 允许使用 2 种不同的编程风格定义规则:一种是更传统的风格,基于 KieBase 的概念,KieBase 充当业务规则的存储库,而 KieSession 存储运行时数据并针对它们进行评估;另一种是使用 Rule Unit 作为单个抽象,它封装了一组规则的定义以及将要匹配这些规则的事实。
Drools Quarkus 扩展完全支持这两种风格,本文档解释了如何使用这两种风格,并概述了每种风格的优缺点。
将传统的 Drools 编程模型与 Quarkus 集成
第一个示例演示了如何使用传统的 Drools 风格定义一组规则,以及如何通过 Quarkus 在 REST 端点中公开它们的评估。
此示例项目的域模型仅由两个类组成,一个是贷款申请
public class LoanApplication {
private String id;
private Applicant applicant;
private int amount;
private int deposit;
private boolean approved = false;
public LoanApplication(String id, Applicant applicant, int amount, int deposit) {
this.id = id;
this.applicant = applicant;
this.amount = amount;
this.deposit = deposit;
}
}
和提出申请的申请人
public class Applicant {
private String name;
private int age;
public Applicant(String name, int age) {
this.name = name;
this.age = age;
}
}
规则集由批准或拒绝申请的业务决策以及将所有已批准的申请收集到列表中的最后一个规则组成。
global Integer maxAmount;
global java.util.List approvedApplications;
rule LargeDepositApprove when
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount <= maxAmount )
then
modify($l) { setApproved(true) }; // loan is approved
end
rule LargeDepositReject when
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount > maxAmount )
then
modify($l) { setApproved(false) }; // loan is rejected
end
// ... more loans approval/rejections business rules ...
rule CollectApprovedApplication when
$l: LoanApplication( approved )
then
approvedApplications.add($l); // collect all approved loan applications
end
我们想要实现的目标是将这些规则的评估放入微服务中,并通过使用 Quarkus 开发的 REST 端点公开它们。为此,只需在您的项目的依赖项中添加 Drools Quarkus 扩展即可。
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-quarkus</artifactId>
</dependency>
此时,可以获得对 KieSession 的引用,该 KieSession 评估先前定义的规则,并在 REST 端点中使用它,如下所示
@Path("/find-approved")
public class FindApprovedLoansEndpoint {
@Inject
KieRuntimeBuilder kieRuntimeBuilder;
@POST()
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public List<LoanApplication> executeQuery(LoanAppDto loanAppDto) {
KieSession session = kieRuntimeBuilder.newKieSession();
List<LoanApplication> approvedApplications = new ArrayList<>();
session.setGlobal("approvedApplications", approvedApplications);
session.setGlobal("maxAmount", loanAppDto.getMaxAmount());
loanAppDto.getLoanApplications().forEach(session::insert);
session.fireAllRules();
session.dispose();
return approvedApplications;
}
}
其中 KieRuntimeBuilder
接口的实现由 Drools 扩展自动生成并可注入,并且允许使用单个语句获得 Drools 项目中定义的任何 KieBases 和 KieSessions 的实例。
在这里,LoanAppDto
是一个简单的 POJO,用于向同一个 KieSession 提交多个贷款申请
public class LoanAppDto {
private int maxAmount;
private List<LoanApplication> loanApplications;
public int getMaxAmount() {
return maxAmount;
}
public void setMaxAmount(int maxAmount) {
this.maxAmount = maxAmount;
}
public List<LoanApplication> getLoanApplications() {
return loanApplications;
}
public void setLoanApplications(List<LoanApplication> loanApplications) {
this.loanApplications = loanApplications;
}
}
因此,例如,尝试使用一组贷款申请调用该端点
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d
'{"maxAmount":5000,"loanApplications":[
{"id":"ABC10001","amount":2000,"deposit":1000,"applicant":{"age":45,"name":"John"}},
{"id":"ABC10002","amount":5000,"deposit":100,"applicant":{"age":25,"name":"Paul"}},
{"id":"ABC10015","amount":1000,"deposit":100,"applicant":{"age":12,"name":"George"}}
]}'
https://:8080/find-approved
规则引擎将根据我们之前配置的业务规则对它们进行评估,仅返回根据这些规则可以批准的一个
[{"id":"ABC10001","applicant":{"name":"John","age":45},"amount":2000,"deposit":1000,"approved":true}]
转移到规则单元编程模型
规则单元是 Drools 中引入的一个新概念,它封装了一组规则以及将要匹配这些规则的事实。它带有一个名为数据源的第二个抽象,定义了事实插入的来源,实际上充当了类型化的入口点。有两种类型的数据源
-
DataStream:一个仅追加的数据源
-
订阅者仅接收新的(可能还有过去的)消息
-
无法更新/删除
-
在“反应式流”术语中,流也可能是热的/冷的
-
-
DataStore:用于可修改数据的数据源
-
订阅者可以通过对事实句柄进行操作来对数据存储进行操作
-
为了在我们的 quarkus 应用程序中使用规则单元,需要添加第二个依赖项。
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ruleunits-engine</artifactId>
</dependency>
本质上,规则单元由 2 个严格相关的部分组成:要评估的事实的定义和评估它们的规则集。第一部分是用 POJO 实现的,对于贷款示例,可能类似于以下内容
package org.loans;
import org.drools.ruleunits.api.DataSource;
import org.drools.ruleunits.api.DataStore;
import org.drools.ruleunits.api.RuleUnitData;
public class LoanUnit implements RuleUnitData {
private int maxAmount;
private DataStore<LoanApplication> loanApplications;
public LoanUnit() {
this(DataSource.createStore(), 0);
}
public LoanUnit(DataStore<LoanApplication> loanApplications, int maxAmount) {
this.loanApplications = loanApplications;
this.maxAmount = maxAmount;
}
public DataStore<LoanApplication> getLoanApplications() {
return loanApplications;
}
public void setLoanApplications(DataStore<LoanApplication> loanApplications) {
this.loanApplications = loanApplications;
}
public int getMaxAmount() {
return maxAmount;
}
public void setMaxAmount(int maxAmount) {
this.maxAmount = maxAmount;
}
}
在这里,我们没有使用我们引入的 LoanAppDto
来编组/解组 JSON 请求,而是直接绑定表示规则单元的类。两个相关的区别是它实现了 RuleUnitData
接口,并使用 DataStore
代替包含要批准的贷款申请的普通 List
。第一个只是一个标记接口,用于通知引擎此类是规则单元定义的一部分。DataStore
的使用是必要的,以便让规则引擎根据更改做出反应,从而触发新规则并触发其他规则。在该示例中,规则的结果修改了贷款申请的已批准属性。相反,maxAmount
值可以被视为规则单元的配置参数,并保持原样:它将在规则评估期间自动处理,其语义与全局变量相同,并从 JSON 请求传递的值自动设置,如第一个示例中所示,因此您仍然可以在规则中使用它。
规则单元的第二部分是包含属于此单元的规则的 drl 文件。
package org.loans;
unit LoanUnit; // no need to using globals, all variables and facts are stored in the rule unit
rule LargeDepositApprove when
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount <= maxAmount ] // oopath style
then
modify($l) { setApproved(true) };
end
rule LargeDepositReject when
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount > maxAmount ]
then
modify($l) { setApproved(false) };
end
// ... more loans approval/rejections business rules ...
// approved loan applications are now retrieved through a query
query FindApproved
$l: /loanApplications[ approved ]
end
此规则文件必须声明相同的包和一个与 Java 类实现 RuleUnitData
接口的名称相同的单元,以便声明它们属于同一个规则单元。
该文件也已使用新的 OOPath 表示法重写:正如预期的那样,这里数据源充当类型化的入口点,并且 oopath 表达式以其名称作为根,而约束在方括号中,如下面的示例所示。
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount <= maxAmount ]
或者,您仍然可以使用旧的 DRL 语法,将数据源的名称指定为入口点,其缺点是在这种情况下,即使引擎可以从数据源的类型推断出来,您也需要再次指定匹配对象的类型,如下所示。
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount <= maxAmount ) from entry-point loanApplications
最后请注意,将所有已批准的贷款申请收集到全局 List
中的最后一个规则已被一个简单地检索它们的查询所取代。使用规则单元的优势之一是它清楚地定义了计算的上下文,换句话说,就是要传递给规则评估的输入事实。同样,查询定义了此评估期望的输出。
这种对计算边界的清晰定义也允许 Drools 自动生成一个执行查询并返回其结果的类,以及一个将规则单元作为输入、将其传递给前一个查询执行器并将其作为输出返回的 REST 端点。
您可以根据需要拥有任意数量的查询,并且对于每个查询,都会生成一个不同的 REST 端点,其名称与从 camel case(如 FindApproved
)转换为破折号分隔(如 find-approved
)的查询名称相同。
一个更全面的例子
在这个更全面和完整的示例中,我们将使用一些简单的规则来扩充基本的 Quarkus 应用程序,以推断家庭自动化设置状态的潜在问题。
我们将定义一个 Drools 规则单元和 DRL 格式的规则。
我们将规则单元连接到标准的 Quarkus CDI bean 中,以便在 Quarkus 应用程序中使用(例如,通过 Kafka 连接 MQTT 消息等)。
先决条件
要完成本指南,您需要
-
少于 15 分钟
-
一个 IDE
-
已安装 JDK 17+ 并正确配置了
JAVA_HOME
-
Apache Maven 3.9.3+
-
Docker
-
安装 GraalVM 如果你想在 native 模式下运行
创建 Maven 项目
首先,我们需要一个新的 Quarkus 项目。要创建一个新的 Quarkus 项目,您可以参考 Quarkus 和 Maven 指南
当您配置好 Quarkus 项目后,您可以通过将以下依赖项添加到您的 pom.xml
中,将 Drools Quarkus 扩展添加到您的项目中
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-quarkus</artifactId>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ruleunits-engine</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
编写应用程序
让我们从应用程序域模型开始。
此应用程序的目标是推断家庭自动化设置状态的潜在问题,因此我们创建必要的域模型来表示传感器、设备和房屋内其他事物的状态。
灯设备域模型
package org.drools.quarkus.quickstart.test.model;
public class Light {
private final String name;
private Boolean powered;
public Light(String name, Boolean powered) {
this.name = name;
this.powered = powered;
}
// getters, setters, etc.
}
CCTV 安全摄像头域模型
package org.drools.quarkus.quickstart.test.model;
public class CCTV {
private final String name;
private Boolean powered;
public CCTV(String name, Boolean powered) {
this.name = name;
this.powered = powered;
}
// getters, setters, etc.
}
在 WiFi 中检测到的智能手机域模型
package org.drools.quarkus.quickstart.test.model;
public class Smartphone {
private final String name;
public Smartphone(String name) {
this.name = name;
}
// getters, setters, etc.
}
Alert 类来保存潜在检测到的问题的信息
package org.drools.quarkus.quickstart.test.model;
public class Alert {
private final String notification;
public Alert(String notification) {
this.notification = notification;
}
// getters, setters, etc.
}
接下来,我们在 Quarkus 项目的 src/main/resources/org/drools/quarkus/quickstart/test
文件夹中创建一个规则文件 rules.drl
。
package org.drools.quarkus.quickstart.test;
unit HomeRuleUnitData;
import org.drools.quarkus.quickstart.test.model.*;
rule "No lights on while outside"
when
$l: /lights[ powered == true ];
not( /smartphones );
then
alerts.add(new Alert("You might have forgot one light powered on: " + $l.getName()));
end
query "AllAlerts"
$a: /alerts;
end
rule "No camera when present at home"
when
accumulate( $s: /smartphones ; $count : count($s) ; $count >= 1 );
$l: /cctvs[ powered == true ];
then
alerts.add(new Alert("One CCTV is still operating: " + $l.getName()));
end
在此文件中,有一些示例规则来决定房屋的总体状态是否被认为是不合适的,从而触发必要的 Alert
(s)。
规则单元是 Drools 8 中引入的一个核心范例,可以帮助用户封装规则集以及将要匹配这些规则的事实;您可以在 Drools 文档中阅读更多信息。
这些事实将插入到 DataStore
中,这是一个类型安全的入口点。为了使一切正常工作,我们需要同时定义 RuleUnit 和 DataStore。
package org.drools.quarkus.quickstart.test;
import org.drools.quarkus.quickstart.test.model.Alert;
import org.drools.quarkus.quickstart.test.model.CCTV;
import org.drools.quarkus.quickstart.test.model.Light;
import org.drools.quarkus.quickstart.test.model.Smartphone;
import org.drools.ruleunits.api.DataSource;
import org.drools.ruleunits.api.DataStore;
import org.drools.ruleunits.api.RuleUnitData;
public class HomeRuleUnitData implements RuleUnitData {
private final DataStore<Light> lights;
private final DataStore<CCTV> cctvs;
private final DataStore<Smartphone> smartphones;
private final DataStore<Alert> alerts = DataSource.createStore();
public HomeRuleUnitData() {
this(DataSource.createStore(), DataSource.createStore(), DataSource.createStore());
}
public HomeRuleUnitData(DataStore<Light> lights, DataStore<CCTV> cctvs, DataStore<Smartphone> smartphones) {
this.lights = lights;
this.cctvs = cctvs;
this.smartphones = smartphones;
}
public DataStore<Light> getLights() {
return lights;
}
public DataStore<CCTV> getCctvs() {
return cctvs;
}
public DataStore<Smartphone> getSmartphones() {
return smartphones;
}
public DataStore<Alert> getAlerts() {
return alerts;
}
}
测试应用程序
我们可以创建一个标准的 Quarkus 和 JUnit 测试,以根据特定的一组场景检查规则单元和已定义规则的行为。
package org.drools.quarkus.quickstart.test;
@QuarkusTest
public class RuntimeTest {
@Inject
RuleUnit<HomeRuleUnitData> ruleUnit;
@Test
public void testRuleOutside() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", true));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isNotEmpty().anyMatch(kv -> kv.containsValue(new Alert("You might have forgot one light powered on: living room")));
}
@Test
public void testRuleInside() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", true));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
homeUnitData.getCctvs().add(new CCTV("security camera 1", false));
homeUnitData.getCctvs().add(new CCTV("security camera 2", true));
homeUnitData.getSmartphones().add(new Smartphone("John Doe's phone"));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isNotEmpty().anyMatch(kv -> kv.containsValue(new Alert("One CCTV is still operating: security camera 2")));
}
@Test
public void testNoAlerts() {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
homeUnitData.getLights().add(new Light("living room", false));
homeUnitData.getLights().add(new Light("bedroom", false));
homeUnitData.getLights().add(new Light("bathroom", false));
homeUnitData.getCctvs().add(new CCTV("security camera 1", true));
homeUnitData.getCctvs().add(new CCTV("security camera 2", true));
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
List<Map<String, Object>> queryResults = unitInstance.executeQuery("AllAlerts");
assertThat(queryResults).isEmpty();
}
}
将规则单元与 Quarkus CDI bean 连接起来
我们现在可以将规则单元连接到标准的 Quarkus CDI bean 中,以便在 Quarkus 应用程序中通用。
例如,稍后这可能有助于通过 Kafka 连接 MQTT 的设备状态报告,使用适当的 Quarkus 扩展。
我们创建一个简单的 CDI bean,以使用以下内容抽象出规则单元 API 的使用
package org.drools.quarkus.quickstart.test;
@ApplicationScoped
public class HomeAlertsBean {
@Inject
RuleUnit<HomeRuleUnitData> ruleUnit;
public Collection<Alert> computeAlerts(Collection<Light> lights, Collection<CCTV> cameras, Collection<Smartphone> phones) {
HomeRuleUnitData homeUnitData = new HomeRuleUnitData();
lights.forEach(homeUnitData.getLights()::add);
cameras.forEach(homeUnitData.getCctvs()::add);
phones.forEach(homeUnitData.getSmartphones()::add);
RuleUnitInstance<HomeRuleUnitData> unitInstance = ruleUnit.createInstance(homeUnitData);
var queryResults = unitInstance.executeQuery("AllAlerts");
List<Alert> results = queryResults.stream()
.flatMap(m -> m.values().stream()
.filter(Alert.class::isInstance)
.map(Alert.class::cast))
.collect(Collectors.toList());
return results;
}
}
可以使用此 CDI bean 相应地重构相同的测试场景。
package org.drools.quarkus.quickstart.test;
@QuarkusTest
public class BeanTest {
@Inject
HomeAlertsBean alerts;
@Test
public void testRuleOutside() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", true), new Light("bedroom", false), new Light("bathroom", false)),
Collections.emptyList(),
Collections.emptyList());
assertThat(computeAlerts).isNotEmpty().contains(new Alert("You might have forgot one light powered on: living room"));
}
@Test
public void testRuleInside() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", true), new Light("bedroom", false), new Light("bathroom", false)),
List.of(new CCTV("security camera 1", false), new CCTV("security camera 2", true)),
List.of(new Smartphone("John Doe's phone")));
assertThat(computeAlerts).isNotEmpty().contains(new Alert("One CCTV is still operating: security camera 2"));
}
@Test
public void testNoAlerts() {
Collection<Alert> computeAlerts = alerts.computeAlerts(
List.of(new Light("living room", false), new Light("bedroom", false), new Light("bathroom", false)),
List.of(new CCTV("security camera 1", true), new CCTV("security camera 2", true)),
Collections.emptyList());
assertThat(computeAlerts).isEmpty();
}
}