Contents
Introduction
During my career in been involved in many project where the use of a business rules engine will be the perfect match, however most of the times the size of the project the constrains (time, money, etc) was decided not to use some of the well know and well positioned Rule framework like Drools, JRules, etc. In many of those projects we end creating a lightweight rule engine in order to solve the solution
The main inspiration of this project was to create a simple to use, light, standalone Java Rule Engine that can be use as simple as possible in any project where a rule engine design is necessary.
Looking for a good definition for a Rule Engine I found this Wiki Article that say:
A business rules engine is a software system that executes one or more business rules in a runtime production environment.
The rules might come from any aspect of the business like The rules might come from legal regulation (“An employee can be fired for any reason or no reason but not for an illegal reason”), company policy (“All customers that spend more than $100 at one time will receive a 10% discount”), or other sources. A business rule system enables these company policies and other operational decisions to be defined, tested, executed and maintained separately from application code.
Rule engines typically support: Rules, Facts, Priority (score), Mutual exclusion, Preconditions
The goal of EZ-Rules Framework is to support those features of a Rule Engine.
Our Rule Definition
There is a lot of information about how a Rule can be implemented, let’s mention our vision and implementation of a rule as resume. So in our framework a rule:
- Is Stateless
- Is annotated with @Rule
- Have a priority that define the order of executions of the rules inside a group
- Have one condition method and at least one action method that will define the behavior of the rule. See annotation @When and @Then
- Action methods inside the rule can be prioritize using the priority attribute in the @Then annotation
- Action method can have any number of parameters. We call those parameters Exchange (see documentation)
- Action method can have ExchangeManager class as parameter that allow the rule to manage the exchanges (see documentation)
- Action method can have parameters annotated as @Callback that allow to inject a callback context into the method (if a method is annotated as callback then is become a precondition to the rule to be execute, if the callback is missing the execution will fail).
- Can have a group associated using the annotation @Group
- A group have an strategy to run. An strategy can be:
- Execute all: Meaning the precondition of all rules in the group need to be accepted in order to execute the rule action
- Stop at first: Meaning that the first group get accepted will be executed and any of the rules after the group can be executed
- A group have an strategy to run. An strategy can be:
- Is thread safe
Getting Started
Build from Git Repository
EZ-Rules is an open source framework. In order to build it you will need Git, Maven and Java installed in your local computer. Alternatively you can use a develop tool (like Eclipse) that normally have support for Git.
To clone the repository from the command line use:
1 2 3 |
git clone https://github.com/rigregs/ez-rules.git cd ez-rules mvn clean package install |
This will install the lasted SNAPSHOT version in your local repository.
Using from maven repository
1 2 3 4 5 |
<dependency> <groupId>com.opnitech.utils</groupId> <artifactId>ez-rules-core</artifactId> <version>${ez-rules.version}</version> </dependency> |
Creating Rules
Before staring with rule definition samples let talk about some important pieces of the framework related with the rule declaration.
Annotations
@Rule
@Rule annotation allow to mark a POJO class as a Rule class.
1 2 3 4 |
@Rule public class SimpleRule { ... } |
The @Rule annotation allow the user to define the priority of the rule, using the priority parameter you can define the order of the rule evaluation
1 2 3 4 |
@Rule(priority = 1) public class SimpleRule { ... } |
@When
@When annotation allow to mark the conditional method that will decide if the rule need to be executed or not. This method can return 3 different values:
- boolean
- Boolean
- WhenEnum
Depending of the result of the “When” annotated method the rule will:
- Be executed if the method return true, WhenEnum.ACCEPT
- Skipped if the method return false, WhenEnum.REJECT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@When public boolean when() { return true; } ... @When public Boolean when() { return false; } ... @When public WhenEnum when() { return WhenEnum.ACCEPT; } |
@Then
@Then annotation allow to define the methods in the rule that will execute the rule action. This method should be void, you can have as many method in the rule annotated with this annotation as you need.
1 2 3 4 |
@Then public void then() throws Exception { ... } |
The @Then annotation allow the user to define the priority of the execution, using the priority parameter you can define the order of the “when” methods execution inside the rule. Important, the priority of this method doesn’t affect the priority of the Rule.
1 2 3 4 5 6 7 8 9 |
@Then(priority=1) public void then1() { ... } @Then(priority=2) public void then2() { ... } |
@Callback
@Callback annotation allow the user to pass a callback object to either the condition (@Then) of the action (@When) methods. This is useful to inject code into the Rule that allow an external call from inside the rule execution.
To declare a callback you simple need to annotate a class with the annotation
1 2 3 4 |
@Callback public class SimpleCallback { ... } |
To use the callback inside the rule you can do this:
1 2 3 4 5 6 7 8 9 |
@When public boolean when(@Callback SimpleCallback simpleCallback .....) { ... } @Then public boolean when(@Callback SimpleCallback simpleCallback .....) { ... } |
@GroupDefinition
@GroupDefinition annotation allow to mark a class as Group metadata. It allow to define the execution strategy (Execute All, Stop First Execute for now that is the default implementation). To define a group of rules you need first to define the group metadata using this annotation and after annotate the rule with the corresponding group definition (see @Group annotation.).
Sample of a Group Definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@GroupDefinition public class ValidGroupDefinition { // NOP } ... @GroupDefinition(ruleExecutionStrategy = ExecutionStrategyEnum.STOP_FIRST) public class ValidGroupDefinition { // NOP } ... @GroupDefinition(ruleExecutionStrategy = ExecutionStrategyEnum.ALL) public class ValidGroupDefinition { // NOP } |
@GroupKey
@GroupKey annotation allow to define a grouping key for groups and rules. A rule can be associated to a group, also a group can be contained in another group, this annotation is useful when you want to control the group key of a Rule or a Group at runtime.
Sample of how to use it:
1 2 3 4 5 6 7 8 9 10 |
@GroupDefinition public class SomeGroupDefinition { ... @GroupKey public String groupKey() { return "SOME_KEY"; } ... } |
@ParentGroupKey
@ParentGroupKey annotation allow to define a grouping key parent for groups. Using this annotation a group can specify what is the parent group. A null value mean that the group will be associated to the rule engine root.
1 2 3 4 5 6 7 8 9 10 |
@GroupDefinition public class SomeGroupDefinition { ... @GroupParentKey public String groupParentKey() { return "SOME_KEY"; } ... } |
@Group
@Group annotation allow group a rule into an specific group. The group annotation expect as parameter the group definition in order to know how to group the rules.
A group have a priority that will compete with the rules and groups at the same level. Also inside the group the priority will be use to decide the order of the execution.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Rule(priority = 1) @Group(SimpleGroupDefinition.class) public class SimpleGroupedRule1 { ... } ... @Rule(priority = 2) @Group(group = SimpleGroupDefinition.class) public class SimpleGroupedRule2 { ... } |
@Priority
@Priority annotation allow to define a dynamic way to define the rule priority. In order to dynamically calculate the priority of the rule you need to annotate a method of the rule with this annotation.
Important, priority method is called when the engine start and after that isn’t getting call anymore, isn;t possible to change the rule priority once the rule is fully initialize at this point.
1 2 3 4 5 6 7 8 9 10 |
@Rule public class PriorityRule extends AbstractRule<Boolean> { ... @RulePriority public int priority() { return 5; } ... } |
@Exchange
@Exchange annotation allow you to define a named context you want to inject in one parameter. The injection framework have two ways to inject the parameters in the @When and @Then annotated method:
- By Type: The engine will look in the type of the parameters and will inject the first exchange parameter that is assignable to the parameter type
- By Name: Using the @Exchange annotation you can specify wish exchange parameter you want to link to the parameter. This one is really useful when you want to match collection, maps etc where specifying the type contained is ambiguous some times.
1 2 3 4 5 6 7 8 9 |
@Rule public class SomeRule { ... @When public boolean when(@Exchange("param") List<Object> objects) { ... } ... } |
Exchanges
Exchanges is the mechanism to pass parameters to the rule. When a rule is execute you can pass a list of parameter that are dynamically binding to the rule flow method by type or by name. Also the rules can contribute back to the exchange using the EchangeManager interface.
Using the exchange manager a rule can execute CRUD operations to the global exchange in the rule execution context.
Example of exchange by type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Rule public class SomeRule { ... @When public boolean when(SomeClass someInstance) { // The value of someInstance wil be resolved by the first exchange assignable to the SomeClass type } @Then public boolean then(SomeClass someInstance) { // The value of someInstance wil be resolved by the first exchange assignable to the SomeClass type } ... } |
Example of exchange by name:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Rule public class SomeRule { ... @When public boolean when(@Exchange("param1") SomeClass someInstance) { // The value of someInstance wil be resolved by the first exchange assignable to the SomeClass type } @Then public boolean then(@Exchange("param2") SomeClass someInstance) { // Noticed that in this case the name of the parameter is different, we can have two exchanges with // the same type and decide where we want to inject any of the instances } ... } |
Example of exchange using the exchange manager:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Rule public class SomeRule { ... @When public boolean when(ExchangeManager exchangeManager) { // The value of someInstance wil be resolved by the first exchange assignable to the SomeClass type SomeClass someInstance = exchangeManager.resolveExchangeByName("param1"); } @Then public boolean then(ExchangeManager exchangeManager) { // Noticed that in this case the name of the parameter is different, we can have two exchanges with // the same type and decide where we want to inject any of the instances SomeClass someInstance = exchangeManager.resolveExchangeByName("param2); } ... } |
Executing Rules
Let’s show all pieces working together.
First create a simple rule that will receive a Boolean indicating if can be evaluated or not and a message that will get printed based if evaluated.
Creating the rule:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Rule public class EvaluateWithExchangeRule { @When public boolean canIEvaluate(Boolean answer) { //The evaluation parameter will be received via exchange, //if not defined then we'll assume false (then method won't be evaluated) return answer != null ? answer : false; } @Then public void evaluate(String message) { //The message will be receive as exchange and bind by type System.out.println(message); } } |
Main application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//Creating rule engine RulesEngine rulesEngine = new RulesEngine(); //Registering the rule, can be registered as a class type, class name or instance //in the case of dynamically grouped rule (using @GroupKey annotation) //is mandatory to register the rule as instance rulesEngine.registerExecutable(EvaluateWithExchangeRule.class); //Let's execute the rule, this first call should't print anything due the //Boolean parameter with match with the exchange parameter of the when //method and so the rule won't be evaluated rulesEngine.execute(Boolean.FALSE, "Hello World!"); System.out.println("****"); //Second call should print 'Hello World' due the //Boolean parameter with match with the exchange parameter of the when //method and so the rule will be evaluated rulesEngine.execute(Boolean.TRUE, "Hello World!"); System.out.println("****"); |
Console output:
1 2 3 |
**** Hello World! **** |
Conclusion
The rule engine we’re creating has been use already in two company. We advice the developer to try Drools or any other of the big and well established rule frameworks available as a first choice but if you need a lightweight rule engine framework you should really consider this library.
You can see the project in GitHub (see references for all the link) and you’re welcome to contribute with changes to the repo.
In the repository you’ll find 100+ unit test that will help you to understand how the engine work, also there is a sample project where you can see some examples with different features supported by the framework
References
Business rules engine: Business rules engine @ Wiki
Repository: EZ-Rules @ GitHub
Releases: EZ-Rules Releases @ GitHub
Continuous integration: Build Jenkins job @ CloudBees
Licensing: MIT License