Java rules engine Easy Rules

Posted by lrdaramis on Thu, 11 Jun 2020 06:11:10 +0200

1. Overview of Easy Rules

Easy Rules is a Java rules engine inspired by a piece called< Should I use a Rules Engine? >The article of

The rule engine is to provide an optional calculation model. Unlike the usual imperative model, which consists of commands with conditions and cycles in turn, the rule engine is based on the production rule system. This is a set of production rules. Each rule has a condition and an action - in short, it can be regarded as a set of if then statements.

The beauty is that rules can be written in any order, and the engine decides when to calculate them in any way that makes sense for the order. A good way to consider it is for the system to run all rules, select the rules with established conditions, and then perform corresponding operations. The advantage of doing so is that many problems naturally fit into this model:

if car.owner.hasCellPhone then premium += 100;
if car.model.theftRating > 4 then premium += 200;
if car.owner.livesInDodgyArea && car.model.theftRating > 2 then premium += 300;

The rule engine is a tool that makes this computational model easier to program. It may be a complete development environment, or a framework that can work on traditional platforms. The production rule computing model is most suitable for solving only part of the computing problems, so the rule engine can be better embedded in the larger system.

You can build a simple rules engine yourself. All you need to do is create a set of objects with conditions and actions, store them in a collection, and then iterate through them to evaluate the conditions and perform the actions.  

Easy Rules provides Rule abstraction to create rules with conditions and actions, and the RuleEngine API, which runs through a set of rules to evaluate conditions and execute actions.  

Easy Rules is easy to use in two steps:

First of all, there are many ways to define rules

Mode 1: Notes

@Rule(name = "weather rule", description = "if it rains then take an umbrella")
public class WeatherRule {

    @Condition
    public boolean itRains(@Fact("rain") boolean rain) {
        return rain;
    }
    
    @Action
    public void takeAnUmbrella() {
        System.out.println("It rains, take an umbrella!");
    }
}

Mode 2: chain programming

Rule weatherRule = new RuleBuilder()
        .name("weather rule")
        .description("if it rains then take an umbrella")
        .when(facts -> facts.get("rain").equals(true))
        .then(facts -> System.out.println("It rains, take an umbrella!"))
        .build();

Mode 3: expression

Rule weatherRule = new MVELRule()
        .name("weather rule")
        .description("if it rains then take an umbrella")
        .when("rain == true")
        .then("System.out.println(\"It rains, take an umbrella!\");");

Mode 4: yml configuration file

For example: weather-rule.yml

name: "weather rule"
description: "if it rains then take an umbrella"
condition: "rain == true"
actions:
  - "System.out.println(\"It rains, take an umbrella!\");"
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
Rule weatherRule = ruleFactory.createRule(new FileReader("weather-rule.yml"));

Next, apply the rules

public class Test {
    public static void main(String[] args) {
        // define facts
        Facts facts = new Facts();
        facts.put("rain", true);

        // define rules
        Rule weatherRule = ...
        Rules rules = new Rules();
        rules.register(weatherRule);

        // fire rules on known facts
        RulesEngine rulesEngine = new DefaultRulesEngine();
        rulesEngine.fire(rules, facts);
    }
}

Getting started: Hello Easy Rules

<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-rules-core</artifactId>
    <version>4.0.0</version>
</dependency>

To create a maven project from a skeleton:

mvn archetype:generate \
    -DarchetypeGroupId=org.jeasy \
    -DarchetypeArtifactId=easy-rules-archetype \
    -DarchetypeVersion=4.0.0

By default, a HelloWorldRule rule is generated for us, as follows:

package com.cjs.example.rules;

import org.jeasy.rules.annotation.Action;
import org.jeasy.rules.annotation.Condition;
import org.jeasy.rules.annotation.Rule;

@Rule(name = "Hello World rule", description = "Always say hello world")
public class HelloWorldRule {

    @Condition
    public boolean when() {
        return true;
    }

    @Action
    public void then() throws Exception {
        System.out.println("hello world");
    }

}

2. Definition of rules

2.1. Definition rules

Most business rules can be represented by the following definitions:

  • Name: unique rule name under a namespace
  • Description: brief description of the rule
  • Priority: priority relative to other rules
  • Facts: facts, data to be processed immediately
  • Conditions: a set of conditions that must be met in order to apply rules
  • Actions: a set of actions to be executed when the conditions are met

Easy Rules provides an abstraction for each key point to define business rules.

In Easy Rules, the Rule interface represents rules

public interface Rule {

    /**
    * This method encapsulates the rule's conditions.
    * @return true if the rule should be applied given the provided facts, false otherwise
    */
    boolean evaluate(Facts facts);

    /**
    * This method encapsulates the rule's actions.
    * @throws Exception if an error occurs during actions performing
    */
    void execute(Facts facts) throws Exception;

    //Getters and setters for rule name, description and priority omitted.

}

The evaluate method encapsulates the Condition that must evaluate to TRUE to trigger a rule. The execute method encapsulates the actions that should be performed when the rule conditions are met. Conditions and actions are represented by the Condition and Action interfaces.

There are two ways to define rules:

  • By adding annotations to POJO classes
  • Programming through RuleBuilder API

You can add @ Rule annotation on a POJO class, for example:

@Rule(name = "my rule", description = "my rule description", priority = 1)
public class MyRule {

    @Condition
    public boolean when(@Fact("fact") fact) {
        //my rule conditions
        return true;
    }

    @Action(order = 1)
    public void then(Facts facts) throws Exception {
        //my actions
    }

    @Action(order = 2)
    public void finally() throws Exception {
        //my final actions
    }

}

@Condition annotation specifies the rule condition
@Face annotation specifying parameters
@Action annotation specifies the action executed by the rule

RuleBuilder supports chain style definition rules, such as:

Rule rule = new RuleBuilder()
                .name("myRule")
                .description("myRuleDescription")
                .priority(3)
                .when(condition)
                .then(action1)
                .then(action2)
                .build();

Combination rule

CompositeRule consists of a set of rules. This is a typical implementation of composite design pattern.

Composition rules are an abstract concept because they can be triggered in different ways.

Easy Rules comes with three CompositeRule implementations:

  • UnitRuleGroup: either apply all rules or do not apply any rules (AND logic)
  • ActivationRuleGroup: it triggers the first applicable rule and ignores other rules in the group (XOR logic)
  • ConditionalRuleGroup: if the rule with the highest priority evaluates to true, the rest of the rules are triggered

Composite rules can be created from basic rules and registered as regular rules:

//Create a composite rule from two primitive rules
UnitRuleGroup myUnitRuleGroup = new UnitRuleGroup("myUnitRuleGroup", "unit of myRule1 and myRule2");
myUnitRuleGroup.addRule(myRule1);
myUnitRuleGroup.addRule(myRule2);

//Register the composite rule as a regular rule
Rules rules = new Rules();
rules.register(myUnitRuleGroup);

RulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.fire(rules, someFacts);

Each rule has a priority. It represents the default order in which registration rules are triggered. By default, a lower value indicates a higher priority. You can override the compareTo method to provide a custom priority policy.

2.2. Defining facts

In Easy Rules, the Fact API represents facts

public class Fact<T> {
     private final String name;
     private final T value;
}

Here's a chestnut:

Fact<String> fact = new Fact("foo", "bar");
Facts facts = new Facts();
facts.add(fact);

Or, it can be abbreviated like this

Facts facts = new Facts();
facts.put("foo", "bar");

You can inject Facts into condition and action methods with @ face annotation

@Rule
class WeatherRule {

    @Condition
    public boolean itRains(@Fact("rain") boolean rain) {
        return rain;
    }

    @Action
    public void takeAnUmbrella(Facts facts) {
        System.out.println("It rains, take an umbrella!");
        // can add/remove/modify facts
    }

}

2.3. Define rule engine

Easy Rules provides two kinds of RulesEngine interface implementation:

  • DefaultRulesEngine: apply rules according to their natural order
  • InferenceRulesEngine: continues to apply rules to known facts until no more rules apply

Create rule engine:

RulesEngine rulesEngine = new DefaultRulesEngine();

// or

RulesEngine rulesEngine = new InferenceRulesEngine();

Then, register the rules

rulesEngine.fire(rules, facts);

The rule engine has some configurable parameters, as shown in the following figure:

Here's a chestnut:

RulesEngineParameters parameters = new RulesEngineParameters()
    .rulePriorityThreshold(10)
    .skipOnFirstAppliedRule(true)
    .skipOnFirstFailedRule(true)
    .skipOnFirstNonTriggeredRule(true);

RulesEngine rulesEngine = new DefaultRulesEngine(parameters);

2.4. Define rule listener

By implementing the RuleListener interface

public interface RuleListener {

    /**
     * Triggered before the evaluation of a rule.
     *
     * @param rule being evaluated
     * @param facts known before evaluating the rule
     * @return true if the rule should be evaluated, false otherwise
     */
    default boolean beforeEvaluate(Rule rule, Facts facts) {
        return true;
    }

    /**
     * Triggered after the evaluation of a rule.
     *
     * @param rule that has been evaluated
     * @param facts known after evaluating the rule
     * @param evaluationResult true if the rule evaluated to true, false otherwise
     */
    default void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) { }

    /**
     * Triggered on condition evaluation error due to any runtime exception.
     *
     * @param rule that has been evaluated
     * @param facts known while evaluating the rule
     * @param exception that happened while attempting to evaluate the condition.
     */
    default void onEvaluationError(Rule rule, Facts facts, Exception exception) { }

    /**
     * Triggered before the execution of a rule.
     *
     * @param rule the current rule
     * @param facts known facts before executing the rule
     */
    default void beforeExecute(Rule rule, Facts facts) { }

    /**
     * Triggered after a rule has been executed successfully.
     *
     * @param rule the current rule
     * @param facts known facts after executing the rule
     */
    default void onSuccess(Rule rule, Facts facts) { }

    /**
     * Triggered after a rule has failed.
     *
     * @param rule the current rule
     * @param facts known facts after executing the rule
     * @param exception the exception thrown when attempting to execute the rule
     */
    default void onFailure(Rule rule, Facts facts, Exception exception) { }

}

3. Example

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.cjs.example</groupId>
    <artifactId>easy-rules-quickstart</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.jeasy</groupId>
            <artifactId>easy-rules-core</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.jeasy</groupId>
            <artifactId>easy-rules-support</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.jeasy</groupId>
            <artifactId>easy-rules-mvel</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>
</project>

4. Extension

The rule is essentially a function, such as y = f (x1, X2,..., xn)

Rule engine is to solve the problem of separating business code and business rules. It is a component embedded in the application program and realizes the separation of business decisions from the application code.

Another common way is Java+Groovy. Java embedded Groovy script engine splits business rules.

https://github.com/j-easy/easy-rules/wiki

Topics: Java Maven Apache Programming