1. Introduction
This article provides a first hands-on overview of Evrete — a new open-source Java rule engine.
Historically, Evrete has been developed as a lightweight alternative to the Drools Rule Engine. It is fully compliant with the Java Rule Engine specification and uses the classical forward-chaining RETE algorithm with several tweaks and features for processing large amounts of data.
It requires Java 8 and higher, has zero dependencies, seamlessly operates on JSON and XML objects, and allows functional interfaces as rules' conditions and actions.
Most of its components are extensible through Service Provider Interfaces, and one of these SPI implementations turns annotated Java classes into executable rulesets. We will give it a try today as well.
2. Maven Dependencies
Before we jump to the Java code, we need to have the evrete-core Maven dependency declared in our project's pom.xml:
<dependency>
<groupId>org.evrete</groupId>
<artifactId>evrete-core</artifactId>
<version>2.1.04</version>
</dependency>
3. Use Case Scenario
To make the introduction less abstract, let's imagine that we run a small business, today is the end of the financial year, and we want to compute total sales per customer.
Our domain data model will include two simple classes — Customer and Invoice:
public class Customer {
private double total = 0.0;
private final String name;
public Customer(String name) {
this.name = name;
}
public void addToTotal(double amount) {
this.total += amount;
}
// getters and setters
}
public class Invoice {
private final Customer customer;
private final double amount;
public Invoice(Customer customer, double amount) {
this.customer = customer;
this.amount = amount;
}
// getters and setters
}
On a side note, the engine supports Java Records out of the box and allows the developers to declare arbitrary class properties as functional interfaces.
Later in this introduction, we will be given a collection of invoices and customers, and logic suggests we need two rules to handle the data:
- The first rule clears each customer's total sales value
- The second rule matches invoices and customers and updates each customer's total.
Once again, we'll implement these rules with fluid rule builder interfaces and as annotated Java classes. Let's start with the Rule builder API.
4. Rule Builder API
Rule builders are central building blocks for developing domain-specific languages (DSL) for rules. Developers will use them when parsing Excel sources, plain text, or whichever other DSL format needs to be turned into rules.
In our case, though, we're primarily interested in their ability to embed rules straight into the developer's code.
4.1. Ruleset Declaration
With rule builders, we can declare our two rules using fluent interfaces:
KnowledgeService service = new KnowledgeService();
Knowledge knowledge = service
.newKnowledge()
.newRule("Clear total sales")
.forEach("$c", Customer.class)
.execute(ctx -> {
Customer c = ctx.get("$c");
c.setTotal(0.0);
})
.newRule("Compute totals")
.forEach(
"$c", Customer.class,
"$i", Invoice.class
)
.where("$i.customer == $c")
.execute(ctx -> {
Customer c = ctx.get("$c");
Invoice i = ctx.get("$i");
c.addToTotal(i.getAmount());
});
First, we created an instance of KnowledgeService, which is essentially a shared executor service. Usually, we should have one instance of KnowledgeService per application.
The resulting Knowledge instance is a pre-compiled version of our two rules. We did this for the same reasons we compile sources in general — to ensure correctness and launch the code faster.
Those familiar with the Drools rule engine will find our rule declarations semantically equivalent to the following DRL version of the same logic:
rule "Clear total sales"
when
$c: Customer
then
$c.setTotal(0.0);
end
rule "Compute totals"
when
$c: Customer
$i: Invoice(customer == $c)
then
$c.addToTotal($i.getAmount());
end
4.2. Mocking Test Data
We will test our ruleset on three customers and 100k invoices with random amounts and randomly distributed among the customers:
List<Customer> customers = Arrays.asList(
new Customer("Customer A"),
new Customer("Customer B"),
new Customer("Customer C")
);
Random random = new Random();
Collection<Object> sessionData = new LinkedList<>(customers);
for (int i = 0; i < 100_000; i++) {
Customer randomCustomer = customers.get(random.nextInt(customers.size()));
Invoice invoice = new Invoice(randomCustomer, 100 * random.nextDouble());
sessionData.add(invoice);
}
Now, the sessionData variable contains a mix of Customer and Invoice instances that we will insert into a rule session.
4.3. Rule Execution
All we will need to do now is to feed all the 100,003 objects (100k invoices plus three customers) to a new session instance and call its fire() method:
knowledge
.newStatelessSession()
.insert(sessionData)
.fire();
for(Customer c : customers) {
System.out.printf("%s:\t$%,.2f%n", c.getName(), c.getTotal());
}
The last lines will print the resulting sales volumes for each customer:
Customer A: $1,664,730.73
Customer B: $1,666,508.11
Customer C: $1,672,685.10
5. Annotated Java Rules
Although our previous example works as expected, it does not make the library compliant with the specification, which expects rule engines to:
- “Promote declarative programming by externalizing business or application logic.”
- “Include a documented file-format or tools to author rules, and rule execution sets external to the application.”
Simply put, that means that a compliant rule engine must be able to execute rules authored outside its runtime.
And Evrete's Annotated Java Rules extension module addresses this requirement. The module is, in fact, a “showcase” DSL, which relies solely on the library's core API.
Let's see how it works.
5.1. Installation
Annotated Java Rules is an implementation of one of Evrete's Service Provider Interfaces (SPI) and requires an additional evrete-dsl-java Maven dependency:
<dependency>
<groupId>org.evrete</groupId>
<artifactId>evrete-dsl-java</artifactId>
<version>2.1.04</version>
</dependency>
5.2. Ruleset Declaration
Let's create the same ruleset using annotations. We'll choose plain Java source over classes and bundled jars:
public class SalesRuleset {
@Rule
public void rule1(Customer $c) {
$c.setTotal(0.0);
}
@Rule
@Where("$i.customer == $c")
public void rule2(Customer $c, Invoice $i) {
$c.addToTotal($i.getAmount());
}
}
This source file can have any name and does not need to follow Java naming conventions. The engine will compile the source as-is on the fly, and we need to make sure that:
- our source file contains all the necessary imports
- third-party dependencies and domain classes are on the engine's classpath
Then we tell the engine to read our ruleset definition from an external location:
KnowledgeService service = new KnowledgeService();
URL rulesetUrl = new URL("ruleset.java"); // or file.toURI().toURL(), etc
Knowledge knowledge = service.newKnowledge(
"JAVA-SOURCE",
rulesetUrl
);
And that's it. Provided that the rest of our code remains intact, we'll get the same three customers printed along with their random sales volumes.
A few notes on this particular example:
- We've chosen to build rules from plain Java (the “JAVA-SOURCE” argument), thus allowing the engine to infer fact names from method arguments.
- Had we selected .class or .jar sources, the method arguments would have required @Fact annotations.
- The engine has automatically ordered rules by method name. If we swap the names, the reset rule will clear previously computed volumes. As a result, we will see zero sales volumes.
5.3. How It Works
Whenever a new session is created, the engine couples it with a new instance of an annotated rule class. Essentially, we can consider instances of these classes as sessions themselves.
Therefore, class variables, if defined, become accessible to rule methods.
If we defined condition methods or declared new fields as methods, those methods would also have access to class variables.
As regular Java classes, such rulesets can be extended, reused, and packed into libraries.
5.4. Additional Features
Simple examples are well-suited for introductions but leave many important topics behind. For Annotated Java Rules, those include:
- Conditions as class methods
- Arbitrary property declarations as class methods
- Phase listeners, inheritance model, and access to the runtime environment
- And, above all, use of class fields across the board — from conditions to actions and field definitions
6. Conclusion
In this article, we briefly tested a new Java rule engine. The key takeaways include:
- Other engines may be better at providing ready-to-use DSL solutions and rule repositories.
- Evrete is instead designed for developers to build arbitrary DSLs.
- Those used to author rules in Java might find the “Annotated Java rules” package as a better option.
It's worth mentioning other features not covered in this article but mentioned in the library's API:
- Declaring arbitrary fact properties
- Conditions as Java predicates
- Changing rule conditions and actions on-the-fly
- Conflict resolution techniques
- Appending new rules to live sessions
- Custom implementations of library's extensibility interfaces
The official documentation is located at https://www.evrete.org/docs/.
Code samples and unit tests are available over on GitHub.