Recently I was thinking about a technical refactoring of rule-based selection, which I wanted to implement through the rule engine, and it was a good opportunity to take a closer look at the rule engine. This article takes a closer look at the use of the rules engine Easy-Rules. Project address: github.com/j-easy/easy…
Introduction to the
Easy Rules is a simple but powerful Java Rules engine that provides the following features:
- Lightweight framework and easy to learn API
- Pojo-based development
- Support for creating composite rules from original rules
- Support for defining rules through expressions such as MVEL, SPEL, and JEXL
Begin to use
Introduction of depend on
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-core</artifactId>
<version>4.1.0</version>
</dependency>
Copy the code
Only core module dependencies are introduced above. If you need other module contents, you can introduce corresponding dependencies.
Define the rules
introduce
Most business rules can be represented by the following definitions:
- Name: unique rule name in the rule namespace
- Description: Brief description of the rule
- Priority: indicates the priority of the rule
- Facts: A set of known facts when a rule is triggered
- Conditions: A set of conditions that need to be met in order to apply the rule, given some facts
- Actions: Set of actions to be performed when a condition is met (possibly adding/removing/modifying facts)
Easy Rules provide an abstraction for each key point in defining business Rules. The Rules in Easy Rules are represented by the Rule interface:
public interface Rule extends Comparable<Rule> {
/** * This method encapsulates the condition of the rule. *@returnTrue if the rule can be applied based on the facts provided, false */ otherwise
boolean evaluate(Facts facts);
/** * This method encapsulates the operation of the rule. *@throwsIf an error occurs during an operation, an exception */ is thrown
void execute(Facts facts) throws Exception;
//Getters and setters for rule name, description and priority omitted.
}
Copy the code
The evaluate() method encapsulates the condition that must be true to trigger a rule. The execute() method encapsulates what should be done if a rule condition is met. Conditions and operations are represented by the Condition and Action interfaces.
Rules can be defined in two different ways:
- Through the
POJO
Add a comment to declare - through
RuleBuilder
API programming
These are the most common ways to define rules, but you can also implement the Rule interface or extend the BasicRule class if desired.
Define rules using annotations
Easy Rules provides the @rule annotation to convert POJOs into Rules.
@Rule(name = "my rule", description = "my rule description", priority = 1)
public class MyRule {
@Condition
public boolean when(@Fact("fact") fact) {
// Rule conditions
return true;
}
@Action(order = 1)
public void then(Facts facts) throws Exception {
// Operation 1 when the rule is true
}
@Action(order = 2)
public void finally(a) throws Exception {
// Operation 2 when rule is true}}Copy the code
The @condition annotation marks a method that evaluates a rule Condition. The method must be public, can have one or more @FACT annotated arguments, and returns a Boolean type. There is only one method that can be marked with the @condition annotation.
The @Action annotation marks the method by which the Action is performed. Rules can have more than one Action. You can use the order attribute to perform operations in a specified order.
useRuleBuilder
Define the rules
RuleBuilder allows you to define rules using streaming apis.
Rule rule = new RuleBuilder()
.name("myRule")
.description("myRuleDescription")
.priority(3)
.when(condition)
.then(action1)
.then(action2)
.build();
Copy the code
In this case, condition is an instance of the condition interface, and action1 and action2 are instances of the Action interface.
Combination rules
Easy Rules allow you to create complex Rules from original Rules. A CompositeRule consists of a group of rules. Composition rules are an abstraction because they can be triggered in different ways. Easy Rules provides three CompositeRule implementations.
UnitRuleGroup
A unit rule group is a combination of rules that can be used as a unit, with all or none of the rules applied.ActivationRuleGroup
: Activate rule group Triggers the first applicable rule and ignores other rules in the group. Rules are first sorted in their natural order (priority by default) within the group.ConditionalRuleGroup
: Conditional rule group takes the rule with the highest priority as the condition. If the rule with the highest priority evaluates to true, the remaining rules are triggered.
Composite rules can be created from the original rules and registered as regular rules.
// Create a composite rule from two original rules
UnitRuleGroup myUnitRuleGroup =
new UnitRuleGroup("myUnitRuleGroup"."unit of myRule1 and myRule2");
myUnitRuleGroup.addRule(myRule1);
myUnitRuleGroup.addRule(myRule2);
// Register composite rules like regular rules
Rules rules = new Rules();
rules.register(myUnitRuleGroup);
RulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.fire(rules, someFacts);
Copy the code
Rule priority
Each rule in Easy Rules has a priority. This represents the default order in which registration rules are triggered. By default, a lower value indicates a higher priority. To override this behavior, you should override the compareTo() method to provide a custom priority policy.
- If it’s inheritance
BasicRule
, you can specify the priority in the constructor, or override itgetPriority()
Methods. - If you use POJO to define rules, you can pass
@Rule
annotationspriority
Property to specify the priority, or to use@Priority
Annotation marks a method. The method has to bepublic
, no parameter but return type isInteger
. - If you are using
RuleBuilder
Define rules that can be usedRuleBuilder#priority()
Method specifies the priority.
Rules API
A set of rules in Easy Rules is represented by the Rules API. It can be used as follows:
Rules rules = new Rules();
rules.register(myRule1);
rules.register(myRule2);
Copy the code
Rules represents the namespace for registered Rules, so each registered rule in the same namespace must have a unique name.
Rules are compared using the Rule#compareTo() method, so the implementation of Rule should properly implement the compareTo() method to ensure that there is a unique Rule name in a single space.
Define the fact
A Fact in Easy Rules is expressed by Fact:
public class Fact<T> {
private final String name;
private final T value;
}
Copy the code
A fact has a name and a value, and neither can be null. The Facts API, on the other hand, represents a set of Facts and acts as a namespace for the Facts. This means that in a Facts instance, Facts must have unique names.
Here is an example of how to define a fact:
Fact<String> fact = new Fact("foo"."bar");
Facts facts = new Facts();
facts.add(fact);
Copy the code
You can also use a shorter version to create named facts with the PUT method, as shown below:
Facts facts = new Facts();
facts.put("foo"."bar");
Copy the code
You can use the @FACT annotation to inject facts into rules’ conditions and action methods. In the following rule, the rain fact is injected into the RAIN parameter of the itRains method:
@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}}Copy the code
Arguments of type Facts will be injected with all known Facts.
Note:
- If the fact of injection is missing in the condition method, the engine logs a warning and assumes that the condition is computed as
false
. - If there is no injected fact in the action method, the action is not executed and thrown
org.jeasy.rules.core.NoSuchFactException
The exception.
Defining the rule Engine
Easy Rules provides two implementations of the RulesEngine interface:
DefaultRulesEngine
: Applies rules according to their natural order (priority by default).InferenceRulesEngine
: Keep applying rules to known facts until no more rules are available.
Creating a rule Engine
You can use constructors to create rules engines.
RulesEngine rulesEngine = new DefaultRulesEngine();
// or
RulesEngine rulesEngine = new InferenceRulesEngine();
Copy the code
Registered rules can be triggered as follows.
rulesEngine.fire(rules, facts);
Copy the code
Rule engine parameters
The Easy Rules engine can be configured with the following parameters:
parameter | type | The default value |
---|---|---|
rulePriorityThreshold | int | MaxInt |
skipOnFirstAppliedRule | boolean | false |
rulePriorityThreshold | int | false |
skipOnFirstFailedRule | boolean | false |
skipOnFirstNonTriggeredRule | boolean | false |
skipOnFirstAppliedRule
: When a rule is successfully applied, the remaining rules are skipped.skipOnFirstFailedRule
: When a rule fails, the remaining rules are skipped.skipOnFirstNonTriggeredRule
: If a rule is not triggered, the remaining rules are skipped.rulePriorityThreshold
: When the priority exceeds the specified threshold, the remaining rules are skipped.
You can specify these parameters using the RulesEngineParameters API:
RulesEngineParameters parameters = new RulesEngineParameters()
.rulePriorityThreshold(10)
.skipOnFirstAppliedRule(true)
.skipOnFirstFailedRule(true)
.skipOnFirstNonTriggeredRule(true);
RulesEngine rulesEngine = new DefaultRulesEngine(parameters);
Copy the code
If you want to get parameters from your engine, you can use the following code snippet:
RulesEngineParameters parameters = myEngine.getParameters();
Copy the code
This allows the engine parameters to be reset after they have been created.
Define rule listeners
You can listen for rule execution events via the RuleListener API:
public interface RuleListener {
/** * Triggered before evaluating the rule. * *@paramRule The rule being evaluated *@paramFacts Known prior to the evaluation rule *@returnReturn true if the rule should be evaluated, false */ otherwise
default boolean beforeEvaluate(Rule rule, Facts facts) {
return true;
}
/** * Triggers ** after evaluating the rule@paramRule Rules after evaluation *@paramFacts Known facts after the evaluation rules *@paramEvaluationResult */
default void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) {}/** * triggered when a runtime exception causes a condition evaluation error@paramRule Rules after evaluation *@paramFacts Known at the time of assessment *@paramException Exception that occurs when the condition is evaluated */
default void onEvaluationError(Rule rule, Facts facts, Exception exception) {}/** * Triggered before the rule action is executed. * *@paramRule Indicates the current rule *@paramFacts known facts when performing rule operations */
default void beforeExecute(Rule rule, Facts facts) {}/** * Triggers ** after the rule operation has successfully executed@paramRule t Indicates the current rule *@paramFacts known facts when performing rule operations */
default void onSuccess(Rule rule, Facts facts) {}/** * Triggers ** when the rule operation fails@paramRule Indicates the current rule *@paramFacts are known facts when performing rule operations *@paramException Exception that occurs during rule operation */
default void onFailure(Rule rule, Facts facts, Exception exception) {}}Copy the code
This interface can be implemented to provide custom behavior that can be executed before/after each rule. To register a listener, use the following code snippet:
DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.registerRuleListener(myRuleListener);
Copy the code
Any number of listeners can be registered, and they will be executed in the order they were registered.
Note: When using a composition rule, listeners are invoked around the composition rule.
Define rules engine listeners
The RulesEngineListener API can be used to listen for rule engine execution events:
public interface RulesEngineListener {
/** * triggers ** before executing the rule set@paramRules Sets of rules to trigger@paramFacts Facts before rules are triggered */
default void beforeEvaluate(Rules rules, Facts facts) {}/** * triggers ** after the rule set is executed@paramRules Sets of rules to trigger@paramFacts Facts before rules are triggered */
default void afterExecute(Rules rules, Facts facts) {}}Copy the code
RulesEngineListener allows us to provide custom behavior before/after triggering the entire rule set. You can register listeners as follows.
DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.registerRulesEngineListener(myRulesEngineListener);
Copy the code
Any number of listeners can be registered, and they will be executed in the order they were registered.
Expression language (EL) support
Easy Rules supports defining Rules with MVEL, SpEL, and JEXL.
EL provider considerations
EL providers have some differences in behavior. For example, when a fact is missing in a condition, MVEL throws an exception, and SpEL ignores it and returns false. Therefore, you should be aware of these differences before choosing which EL to use in Easy Rules.
Define rules programmatically
Conditions, actions and rules respectively by MVELCondition/SpELCondition/JexlCondition, MVELAction/SpELAction/JexlAction and MVELRule/SpELRule/JexlRule said. Here is an example of a rule defined using MVEL:
Rule ageRule = new MVELRule()
.name("age rule")
.description("Check if person's age is > 18 and marks the person as adult")
.priority(1)
.when("person.age > 18")
.then("person.setAdult(true);");
Copy the code
Define rules through the rule description file
Can use description file defines the rules, the use of MVELRuleFactory/SpELRuleFactory/JexlRuleFactory to create rules from the descriptor file. Here is an example of an MVEL rule defined in YAML format in alcohol-rule-yml:
name: "alcohol rule"
description: "children are not allowed to buy alcohol"
priority: 2
condition: "person.isAdult() == false"
actions:
- "System.out.println("Shop: Sorry, you are not allowed to buy alcohol");"
Copy the code
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
MVELRule alcoholRule = ruleFactory.createRule(new FileReader("alcohol-rule.yml"));
Copy the code
You can also create multiple rules from a single file.
---
name: adult rule
description: when age is greater than 18. then mark as adult
priority: 1
condition: "person.age > 18"
actions:
- "person.setAdult(true);"
---
name: weather rule
description: when it rains, then take an umbrella
priority: 2
condition: "rain == true"
actions:
- "System.out.println("It rains, take an umbrella!" );"
Copy the code
You can load these rules into the Rules object as follows.
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
Rules rules = ruleFactory.createRules(new FileReader("rules.yml"));
Copy the code
Easy Rules also supports loading Rules from JSON descriptors. The specific reference documents are not expanded here.
Error handling in rule definitions
Engine behavior about incorrect expressions in conditions
For any runtime exceptions (missing facts, input errors in expressions, and so on) that may occur during the condition evaluation, the engine logs a warning and assumes that the condition evaluation is false. You can use RuleListener#onEvaluationError to listen for evaluation errors.
Engine behavior regarding incorrect expressions in operations
For any runtime exceptions (missing facts, typing errors in expressions, and so on) that may occur while performing the operation, the operation will not be performed and the engine will log an error. You can use RuleListener#onFailure to listen for operation execution exceptions. When a rule fails, the engine moves to the next rule unless the skipOnFirstFailedRule parameter is set.
The actual chestnuts
Ben uses Easy Rules to implement the FizzBuzz application. FizzBuzz is a simple app that requires counting from 1 to 100, and:
- If the number is a multiple of 5, print “fizz”
- If the number is a multiple of 7, print “buzz”
- If the number is a multiple of 5 and 7, print “fizzbuzz”
- Otherwise print the number itself
public class FizzBuzz {
public static void main(String[] args) {
for(int i = 1; i <= 100; i++) {
if (((i % 5) = =0) && ((i % 7) = =0))
System.out.print("fizzbuzz");
else if ((i % 5) = =0) System.out.print("fizz");
else if ((i % 7) = =0) System.out.print("buzz");
elseSystem.out.print(i); System.out.println(); } System.out.println(); }}Copy the code
We will write a rule for each requirement:
@Rule
public class FizzRule {
@Condition
public boolean isFizz(@Fact("number") Integer number) {
return number % 5= =0;
}
@Action
public void printFizz(a) {
System.out.print("fizz");
}
@Priority
public int getPriority(a) {
return 1; }}Copy the code
@Rule
public class BuzzRule {
@Condition
public boolean isBuzz(@Fact("number") Integer number) {
return number % 7= =0;
}
@Action
public void printBuzz(a) {
System.out.print("buzz");
}
@Priority
public int getPriority(a) {
return 2; }}Copy the code
public class FizzBuzzRule extends UnitRuleGroup {
public FizzBuzzRule(Object... rules) {
for(Object rule : rules) { addRule(rule); }}@Override
public int getPriority(a) {
return 0; }}Copy the code
@Rule
public class NonFizzBuzzRule {
@Condition
public boolean isNotFizzNorBuzz(@Fact("number") Integer number) {
return number % 5! =0 || number % 7! =0;
}
@Action
public void printInput(@Fact("number") Integer number) {
System.out.print(number);
}
@Priority
public int getPriority(a) {
return 3; }}Copy the code
Here are some explanations of these rules:
FizzRule
andBuzzRule
Quite simply, they check if the input is a multiple of 5 or 7, and print the result.FizzBuzzRule
It’s a combination rule. throughFizzRule
andBuzzRule
To create. The base class is selected asUnitRuleGroup
Either satisfy and apply these two rules, or apply nothing at all.NonFizzBuzzRule
It’s the rule when it’s neither a multiple of 5 nor a multiple of 7.
Note that we have set the priority so that the rules fire in the same order as the example in the Java example.
We then have to register these rules into a rule set and use a rules engine to trigger them:
public class FizzBuzzWithEasyRules {
public static void main(String[] args) {
// Create a rules engine
RulesEngineParameters parameters = new RulesEngineParameters().skipOnFirstAppliedRule(true);
RulesEngine fizzBuzzEngine = new DefaultRulesEngine(parameters);
// Create a rule
Rules rules = new Rules();
rules.register(new FizzRule());
rules.register(new BuzzRule());
rules.register(new FizzBuzzRule(new FizzRule(), new BuzzRule()));
rules.register(new NonFizzBuzzRule());
// Trigger rules
Facts facts = new Facts();
for (int i = 1; i <= 100; i++) {
facts.put("number", i); fizzBuzzEngine.fire(rules, facts); System.out.println(); }}}Copy the code
Note that we have set the skipOnFirstAppliedRule parameter to skip subsequent rules if the rule is successfully applied.