Introduction to the Evrete Rule Engine – Evrete规则引擎简介

最后修改: 2021年 10月 19日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

This article provides a first hands-on overview of Evette — a new open-source Java rule engine.

本文对Evette–一个新的开源Java规则引擎进行了首次实践性的概述。

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.

历史上,Evrete已被开发为Drools 规则引擎的轻型替代方案。它完全符合Java规则引擎规范,并使用经典的前向链式RETE算法,并在处理大量数据方面进行了一些调整和特色。

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.

它需要Java 8或更高版本,具有零依赖性,可无缝操作JSON和XML对象,并且允许功能接口作为规则的条件和行动

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.

它的大部分组件都可以通过服务提供者接口进行扩展,其中一个SPI实现将注释的Java类变成可执行的规则集。我们今天也要试一试。

2. Maven Dependencies

2.Maven的依赖性

Before we jump to the Java code, we need to have the evrete-core Maven dependency declared in our project’s pom.xml:

在进入Java代码之前,我们需要在项目的pom.xml中声明evrete-core的Maven依赖。

<dependency>
    <groupId>org.evrete</groupId>
    <artifactId>evrete-core</artifactId>
    <version>2.1.04</version>
</dependency>

3. Use Case Scenario

3.用例情景

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:

我们的领域数据模型将包括两个简单的类–CustomerInvoice

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.

顺便提一下,该引擎支持Java记录,并且允许开发人员将任意的类属性声明为功能接口

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.

再一次,我们将用流体规则生成器接口和注释的Java类来实现这些规则。让我们从规则构建器API开始。

4. Rule Builder API

4.规则生成器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.

规则构建器是为规则开发特定领域语言(DSL)的核心构建块。开发人员在解析Excel资源、纯文本或其他任何需要转化为规则的DSL格式时都会使用它们。

In our case, though, we’re primarily interested in their ability to embed rules straight into the developer’s code.

不过,在我们的案例中,我们主要对他们将规则直接嵌入到开发者的代码中的能力感兴趣。

4.1. Ruleset Declaration

4.1.规则集声明

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.

首先,我们创建了一个KnowledgeService的实例,它本质上是一个共享的执行器服务。通常,我们应该在每个应用程序中有一个KnowledgeService的实例

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.

由此产生的Knowledge实例是我们两个规则的预编译版本。我们这样做的原因与我们一般编译源代码的原因相同–确保正确性并更快地启动代码。

Those familiar with the Drools rule engine will find our rule declarations semantically equivalent to the following DRL version of the same logic:

熟悉Drools规则引擎的人会发现我们的规则声明在语义上等同于以下DRL版本的相同逻辑。

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

4.2.嘲弄测试数据

We will test our ruleset on three customers and 100k invoices with random amounts and randomly distributed among the customers:

我们将在三个客户和10万张随机金额的发票上测试我们的规则集,并在客户中随机分布。

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.

现在,sessionData变量包含了CustomerInvoice实例的混合,我们将把这些实例插入规则会话。

4.3. Rule Execution

4.3.规则执行

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:

我们现在需要做的是将所有100,003个对象(100k发票加上三个客户)送入一个新的会话实例,并调用其fire()方法。

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

5.注释的Java规则

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.

Evrete的注释的Java规则扩展模块可以满足这一要求。该模块实际上是一个 “展示型 “DSL,它完全依赖于库的核心API。

Let’s see how it works.

让我们看看它是如何工作的。

5.1. Installation

5.1.安装

Annotated Java Rules is an implementation of one of Evrete’s Service Provider Interfaces (SPI) and requires an additional evrete-dsl-java Maven dependency:

注释的Java规则是Evrete的服务提供商接口(SPI)之一的实现,需要额外的evrete-dsl-java Maven依赖。

<dependency>
    <groupId>org.evrete</groupId>
    <artifactId>evrete-dsl-java</artifactId>
    <version>2.1.04</version>
</dependency>

5.2. Ruleset Declaration

5.2.规则集声明

Let’s create the same ruleset using annotations. We’ll choose plain Java source over classes and bundled jars:

让我们使用注解来创建同样的规则集。我们将选择普通的Java源而不是类和捆绑的jar。

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:

这个源文件可以有任何名字,不需要遵循Java的命名规则。该引擎将在运行时按原样编译该源文件,我们需要确保。

  • 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

5.3.它是如何工作的

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.

作为普通的Java类,这样的规则集可以被扩展、重用,并打包成库。

5.4. Additional Features

5.4.附加功能

Simple examples are well-suited for introductions but leave many important topics behind. For Annotated Java Rules, those include:

简单的例子很适合做介绍,但却留下了许多重要的话题。对于注释的Java规则,这些包括。

  • 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

6.结语

In this article, we briefly tested a new Java rule engine. The key takeaways include:

在这篇文章中,我们简要地测试了一个新的Java规则引擎。主要的收获包括。

  1. Other engines may be better at providing ready-to-use DSL solutions and rule repositories.
  2. Evrete is instead designed for developers to build arbitrary DSLs.
  3. 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:

值得一提的是,本文没有涉及但在该库的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/.

官方文档位于https://www.evrete.org/docs/

Code samples and unit tests are available over on GitHub.

代码样本和单元测试可在GitHub上获得