Defining and executing business rules with Drools
This guide demonstrates how your Quarkus application can use Drools to add intelligent automation and power it up with the Drools rule engine.
先决条件
完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
JDK 17+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.9
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
简介
Drools is a set of projects focusing on intelligent automation and decision management, most notably providing a forward-chaining and backward-chaining inference-based rule engine, DMN decisions engine and other projects. A rule engine is a fundamental building block to create an expert system which, in artificial intelligence, is a computer system that emulates the decision-making ability of a human expert. You can read more information on the Drools website.
Drools allows defining rules with 2 different programming styles: one more traditional based on the concepts of a KieBase acting as a repository of business rules and a KieSession storing and evaluating the runtime data against them, and the other using a Rule Unit as a single abstraction that encapsulates the definitions of both a set of rules and the facts against which those rules will be matched.
Both these styles are fully supported in the Drools Quarkus extension and this document explains how to use both, outlining the pros and cons of each one.
Integrating the traditional Drools programming model with Quarkus
This first example demonstrates how to define a set of rules using the traditional Drools style and how to expose their evaluation inside a REST endpoint through Quarkus.
The domain model of this sample project is made only by two classes, a loan application
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;
}
}
and the applicant who requested it
public class Applicant {
private String name;
private int age;
public Applicant(String name, int age) {
this.name = name;
this.age = age;
}
}
The rules set is made of business decisions to approve or reject an application plus one last rule collecting all the approved applications into a list.
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
The goal that we want to achieve is putting the evaluation of these rules in a microservice, exposing them in a REST endpoint developed with Quarkus. To do so it is enough to add the Drools Quarkus extension among the dependencies of your project.
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-quarkus</artifactId>
</dependency>
and at this point it is possible to obtain a reference to the KieSession evaluating the formerly defined rules and use it in a REST endpoint as it follows:
@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;
}
}
where an implementation of the KieRuntimeBuilder
interface is automatically generated and made injectable for you by the Drools extension and allows to obtain with a single statement an instance of any KieBases and KieSessions defined in your Drools project.
Here the LoanAppDto
is a simple POJO used to submit multiple loan application to the same 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;
}
}
thus trying for example to invoke that endpoint with a set of loan applications
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"}}
]}'
http://localhost:8080/find-approved
the rule engine will evaluate them against the business rules we have configured before, returning the only one that in this case can be approved according to them
[{"id":"ABC10001","applicant":{"name":"John","age":45},"amount":2000,"deposit":1000,"approved":true}]
Moving to the rule unit programming model
A rule unit is a new concept introduced in Drools encapsulating both a set of rules and the facts against which those rules will be matched. It comes with a second abstraction called data source, defining the sources through which the facts are inserted, acting in practice as typed entry-points. There are two types of data sources:
-
DataStream: an append-only data source
-
subscribers only receive new (and possibly past) messages
-
cannot update/remove
-
stream may also be hot/cold in “reactive streams” terminology
-
-
DataStore: data source for modifiable data
-
subscribers may act upon the data store, by acting upon the fact handle
-
In order to use rule units in our quarkus application it is necessary to add a second dependency.
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-ruleunits-engine</artifactId>
</dependency>
In essence a rule unit is made of 2 strictly related parts: the definition of the fact to be evaluated and the set of rules evaluating them. The first part is implemented with a POJO, that for the loan example could be something like the following:
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;
}
}
Here instead of using the LoanAppDto
that we introduced to marshall/unmarshall the JSON requests we are binding directly the class representing the rule unit. The two relevant differences are that it implements the RuleUnitData
interface and uses a DataStore
instead of a plain List
containing the loan applications to be approved. The first is just a marker interface to notify the engine that this class is part of a rule unit definition. The use of a DataStore
is necessary to let the rule engine to react accordingly to the changes by firing new rules and triggering other rules. In the example, the consequences of the rules modify the approved property of the loan applications. Conversely, the maxAmount
value can be considered a configuration parameter of the rule unit and left as it is: it will automatically be processed during the rules evaluation with the same semantic of a global, and automatically set from the value passed by the JSON request as in the first example, so you will still be allowed to use it in your rules.
The second part of the rule unit is the drl file containing the rules belonging to this unit.
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
This rules file must declare the same package and a unit with the same name of the Java class implementing the RuleUnitData
interface in order to state that they belong to the same rule unit.
This file has also been rewritten using the new OOPath notation: as anticipated, here the data source acts as a typed entry-point and the oopath expression has its name as root while the constraints are in square brackets, like in the following example.
$l: /loanApplications[ applicant.age >= 20, deposit >= 1000, amount <= maxAmount ]
Alternatively you can still use the old DRL syntax, specifying the name of the data source as an entry-point, with the drawback that in this case you need to specify again the type of the matched object, even if the engine can infer it from the type of the datasource, as it follows.
$l: LoanApplication( applicant.age >= 20, deposit >= 1000, amount <= maxAmount ) from entry-point loanApplications
Finally note that the last rule collecting all the approved loan applications into a global List
has been replaced by a query simply retrieving them. One of the advantages in using a rule unit is that it clearly defines the context of computation, in other terms the facts to be passed in input to the rule evaluation. Similarly, the query defines what is the output expected by this evaluation.
This clear definition of the computation boundaries allows Drools to also automatically generate a class executing the query and returning its results, together with a REST endpoint taking the rule unit as input, passing it to the former query executor and returning its as output.
You can have as many query as you want and for each of them it will be generated a different REST endpoint with the same name of the query transformed from camel case (like FindApproved
) to dash separated (like find-approved
).
A more comprehensive example
In this more comprehensive and complete example, we will augment a basic Quarkus application with a few simple rules to infer potential issues with the status of a home automation setup.
We will define a Drools Rule Unit and the rules in the DRL format.
We will wire the Rule Unit into a standard Quarkus CDI bean, for use in the Quarkus application (for instance, wiring MQTT messages from Kafka, etc.).
先决条件
要完成这个指南,你需要:
-
less than 15 minutes
-
一个编辑器
-
JDK 17+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.3+
-
Docker
-
GraalVM installed if you want to run in native mode
创建Maven项目
First, we need a new Quarkus project. To create a new Quarkus project, you can reference the Quarkus and Maven Guide
When you have your Quarkus project configured, you can add the Drools Quarkus extensions to your project by adding the following dependencies to your pom.xml
:
<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>
编写应用程序
Let’s start from the application domain model.
This application goal is to infer potential issues with the status of a home automation setup, so we create the necessary domain models to represent status of sensors, devices and other things inside the house.
Light device domain model:
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 security camera domain model:
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.
}
Smartphone detected in WiFi domain model:
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 class to hold information of the potential detected problems:
package org.drools.quarkus.quickstart.test.model;
public class Alert {
private final String notification;
public Alert(String notification) {
this.notification = notification;
}
// getters, setters, etc.
}
Next, we create a rule file rules.drl
inside the src/main/resources/org/drools/quarkus/quickstart/test
folder of the Quarkus project.
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
In this file there are some example rules to decide whether the overall status of the house is deemed inappropriate, triggering the necessary Alert
(s).
Rule Unit a central paradigm introduced in Drools 8 that helps users to encapsulate the set of rules and the facts against which those rules will be matched; you can read more information in the Drools documentation.
The facts will be inserted into a DataStore
, a type-safe entry point. To make everything work, we need to define both the RuleUnit and the 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;
}
}
测试应用程序
We can create a standard Quarkus and JUnit test to check the behaviour of the Rule Unit and the defined rules, accordingly to a certain set of scenarios.
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();
}
}
Wiring the Rule Unit with Quarkus CDI beans
We can now wire the Rule Unit into a standard Quarkus CDI bean, for general use in the Quarkus application.
For example, this might later be helpful to wire device status reporting through MQTT via Kafka, using the appropriate Quarkus extensions.
We create a simple CDI bean to abstract away the Rule Unit API usage with:
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;
}
}
The same test scenarios can be refactored using this CDI bean accordingly.
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();
}
}