1. Overview

Using a rule engine is a great way to separate the business logic from our boilerplate code and protecting our application code from business changes.

In a previous article on Java Rule Engines, we mentioned the JSR 94 specification. The Jess Rule Engine holds particular importance as the reference rules driver implementation for JSR 94, so let’s take a look at it.

2. Jess Rule Engine

Jess is one of the earliest rule engines to be easily integrated with Java. Jess uses an enhanced implementation of the highly efficient Rete algorithm, making it much faster than a simple Java loop for most scenarios.

Rules can be executed from rulesets written in the native Jess Rules Language, an extended Lisp-based syntax, or from a more verbose XML format. We’ll use the native format.

There’s an Eclipse-based IDE for development (for older versions of Eclipse) and some excellent documentation on using and integrating Jess with Java. There’s even a REPL command-line interface where we can try out our ideas before creating a rules file.

As the reference rule engine for JSR 94, Jess is by definition JSR 94 compliant, although it’s no longer under active development.

2.1. A Quick Word About JSR 94

JSR 94 provides an API that we can use to give us independence from whichever rule engine we choose. We can plug any JSR 94 compliant rule engine into our code and run some rules without needing to change the way we interact with the rule engine in our application.

This doesn’t mean the rule engine’s underlying rules will look the same – we may have to rewrite those if we change the rule engine, but it does mean we won’t need to rewrite parts of our application to use the new rule engine. The only code changes we’ll need are to update the name of the driver and some rule file names.

2.2. The Jess JSR 94 Driver

Although there’s a reference rule engine driver for Jess included for JSR 94, Jess itself is not included, as it’s a licensed commercial product. The reference driver comes in the org.jcp.jsr94.jess package, but a newer driver is available in the jess.jsr94 package when we download Jess.

Let’s start by looking at Jess’s native Java integration before we move on to see how the JSR 94 layer changes this.

3. Provided Examples

Before we start integrating Jess to our code, let’s make sure we’ve downloaded it and made it available on our classpath. We’ll need to register for the free 30-day trial download unless we already have a license.

So, let’s download Jess, unpack the downloaded Jess71p2.jar, and run one of its examples to make sure we have a working version.

3.1. Standalone Jess

Let’s look in the Jess71p2/examples directory, where the jess directory holds some example rulesets. The pricing_engine directory shows an integration that can be executed via an ant build.xml script. Let’s change our directory to the pricing engine example and run the program via ant test:

cd Jess71p2/examples/pricing_engine
ant test

This builds and runs an example pricing ruleset:

Buildfile: Jess71p2\examples\pricing_engine\build.xml
...
test:
[java] Items for order 123:
[java] 1 CD Writer: 199.99
...
[java] Items for order 666:
[java] 1 Incredibles DVD: 29.99
[java] Offers for order 666:
[java] BUILD SUCCESSFUL
Total time: 1 second

3.2. Jess With JSR 94

Now that we have Jess working, let’s download JSR 94 and then unzip it to create a jsr94-1.0 directory with ant, doc, lib, and src directories inside.

unzip jreng-1_0a-fr-spec-api.zip

This gives us the JSR 94 API and Jess reference driver, but it doesn’t come with the licensed Jess implementation, so if we try running an example now, we’ll get the following error:

Error: The reference implementation Jess could not be found.

So, let’s add the Jess reference implementation, jess.jar, that came as part of the Jess71p2 we downloaded earlier and copy it to the JSR 94 lib directory, then run the example:

cp Jess71p2/lib/jess.jar jsr94-1.0/lib/
java -jar jsr94-1.0/lib/jsr94-example.jar

The example runs some rules to determine a customer’s remaining credit as invoices are paid:

Administration API Acquired RuleAdministrator: org.jcp.jsr94.jess.RuleAdministratorImpl@63947c6b
...
Runtime API Acquired RuleRuntime: org.jcp.jsr94.jess.RuleRuntimeImpl@68fb2c38
Customer credit limit result: 3000
...
Invoice 2 amount: 1750 status: paid
Released Stateful Rule Session.

4. Integrating Jess With Java

Now that we have Jess and JSR 94 downloaded and have run some rules both natively and via the JSR, let’s look at how to integrate a Jess ruleset into a Java program.

In our example, we’ll start by executing a simple Jess rules file, hellojess.clp, from Java code, and then look at another rules file, bonus.clp, that will use and modify some of our objects.

4.1. Maven Dependency

There’s no Maven dependency available for Jess, so if we haven’t already done so, let’s download and unpack the Jess jar (jess.jar) and mvn install it to our local Maven repository:

mvn install:install-file -Dfile=jess.jar -DgroupId=gov.sandia -DartifactId=jess -Dversion=7.1p2 -Dpackaging=jar -DgeneratePom=true

We can then add it as a dependency in the usual way:

<dependency>
    <groupId>gov.sandia</groupId>
    <artifactId>jess</artifactId>
    <version>7.1p2</version>
</dependency>

4.2. Hello Jess Rules File

Next, let’s create the simplest of rules files to print out a message. We’ll save the rules file as hellojess.clp:

(printout t "Hello from Jess!" crlf)

4.3. Jess Rule Engine

Now, let’s create an instance of the Jess Rete rule engine, reset() it to its initial state, load up the rules in hellojess.clp, and run them:

public class HelloJess {
    public static void main(String[] args) throws JessException {
    Rete engine = new Rete();
    engine.reset();
    engine.batch("hellojess.clp");
    engine.run();
}

For this simple example, we’ve just added the potential JessException to our main method’s throws clause.

When we run our program, we’ll see the output:

Hello from Jess!

5. Integrating Jess to Java With Data

Now that everything is installed correctly and we can run rules, let’s see how we add data for the rule engine to process and how we retrieve the results.

First, we’ll need some Java classes to work with, and then a new ruleset that uses them.

5.1. Model

Let’s create some simple Question and Answer classes:

public class Question {
    private String question;
    private int balance;
    // getters and setters

    public Question(String question, int balance) {
        this.question = question;
        this.balance = balance;
    }
}

public class Answer {
    private String answer;
    private int newBalance;
    // getters and setters

    public Answer(String answer, int newBalance) {
        this.answer = answer;
        this.newBalance = newBalance;
    }
}

5.2. Jess Rule With Input and Output

Now, let’s create a simple Jess ruleset called bonus.clp that we’ll pass a Question to and receive an Answer from.

First, we import our Question and Answer classes and then use Jess’s deftemplate function to make them available to the rule engine:

(import com.baeldung.rules.jsr94.jess.model.*)
(deftemplate Question     (declare (from-class Question)))
(deftemplate Answer       (declare (from-class Answer)))

Note the use of parentheses, which denote Jess function calls.

Now, let’s use defrule to add a single rule avoid-overdraft in Jess’s extended Lisp format that gives us a bonus of $50 if the balance in our Question is below zero:

(defrule avoid-overdraft "Give $50 to anyone overdrawn"
    ?q <- (Question { balance < 0 })
    =>
    (add (new Answer "Overdrawn bonus" (+ ?q.balance 50))))

Here, the “*?”* binds an object to a variable q when the conditions on the right-hand side of the “*<-“* match. In this case, that’s when the rule engine finds a Question that has a balance less than 0.

When it does, then the actions to the right of the “=>” are triggered so the engine adds a new Answer object to the working memory. We give it the two required constructor arguments: “Overdrawn bonus” for the answer parameter and a (+) function to calculate the newAmount parameter.

5.3. Manipulating Data With the Jess Rule Engine

We can use add() to add a single object at a time to our rule engine’s working memory, or addAll() to add a collection of data. Let’s use add() to add a single question:

Question question = new Question("Can I have a bonus?", -5);
engine.add(data);

With all of our data in place, let’s execute our rules:

engine.run();

The Jess Rete engine will work its magic and return when all relevant rules have executed. In our case, we’ll have an Answer to inspect.

Let’s use a jess.Filter to extract our Answer from the rule engine into an Iterable results object*:*

Iterator results = engine.getObjects(new jess.Filter.ByClass(Answer.class));
while (results.hasNext()) {
    Answer answer = (Answer) results.next();
    // process our Answer
}

We don’t have any reference data in our simple example, but when we do, we can use a WorkingMemoryMarker and engine.mark() to mark the state of the rule engine’s working memory after adding the data. Then we can call engine.resetToMark with our marker to reset the working memory to our “loaded” state and efficiently reuse the rule engine for a different set of objects:

WorkingMemoryMarker marker;
// load reference data
marker = engine.mark();
// load specific data and run rules
engine.resetToMark(marker);

Now, let’s take a look at how we run this same ruleset using JSR 94.

6. Using JSR 94 to Integrate the Jess Rule Engine

JSR 94 standardizes how our code interacts with a rule engine. This makes it easier to change our rule engine without significantly changing our application if a better alternative comes along.

The JSR 94 API comes in two main packages:

  • javax.rules.admin – for loading drivers and rules
  • javax.rules – to run the rules and extract results

We’ll look at how to use the classes in both of these.

6.1. Maven Dependency

First, let’s add a Maven dependency for jsr94:

<dependency>
    <groupId>jsr94</groupId>
    <artifactId>jsr94</artifactId>
    <version>1.1</version>
</dependency>

6.2. Administration API

To start using JSR 94, we need to instantiate a RuleServiceProvider. Let’s create one, passing it our Jess rules driver:

String RULE_SERVICE_PROVIDER="jess.jsr94";
Class.forName(RULE_SERVICE_PROVIDER + ".RuleServiceProviderImpl");
RuleServiceProvider ruleServiceProvider = RuleServiceProviderManager.getRuleServiceProvider(RULE_SERVICE_PROVIDER);

Now, let’s get Jess’s JSR 94 RuleAdministrator, load our example ruleset into a JSR 94 RuleExecutionSet, and register it for execution with a URI of our choice:

RuleAdministrator ruleAdministrator = serviceProvider.getRuleAdministrator();

InputStream ruleInput = JessRunner.class.getResourceAsStream(rulesFile);
HashMap vendorProperties = new HashMap();

RuleExecutionSet ruleExecutionSet = ruleAdministrator
  .getLocalRuleExecutionSetProvider(vendorProperties)
  .createRuleExecutionSet(ruleInput, vendorProperties);

String rulesURI = "rules://com/baeldung/rules/bonus";
ruleAdministrator.registerRuleExecutionSet(rulesURI, ruleExecutionSet, vendorProperties);

The Jess driver doesn’t need the vendorProperties map we supplied to RuleAdministrator, but it’s part of the interface, and other vendors may require it.

Now that our rule engine provider, Jess, has been initialized and our ruleset has been registered, we are almost ready to run our rules.

Before we can run them, we need a runtime instance and a session to run them in. Let’s also add a placeholder, calculateResults(), for where the magic will happen, and release the session:

RuleRuntime ruleRuntime = ruleServiceProvider.getRuleRuntime();
StatelessRuleSession statelessRuleSession
  = (StatelessRuleSession) ruleRuntime.createRuleSession(rulesURI, new HashMap(), RuleRuntime.STATELESS_SESSION_TYPE);
calculateResults(statelessRuleSession);
statelessRuleSession.release();

6.3. Execution API

Now that we have everything in place, let’s implement calculateResults to supply our initial data, execute our rules in a stateless session, and extract the results:

List data = new ArrayList();
data.add(new Question("Can I have a bonus?", -5));
List results = statelessRuleSession.executeRules(data);

Since JSR 94 was written before JDK 5 came along, the API doesn’t use generics so let’s just use an Iterator to see the results:

Iterator itr = results.iterator();
while (itr.hasNext()) {
    Object obj = itr.next();
    if (obj instanceof Answer) {
        int answerBalance = ((Answer) obj).getCalculatedBalance());
    }
}

We’ve used a stateless session in our example, but we can also create a StatefuleRuleSession if we want to maintain state between invocations.

7. Conclusion

In this article, we learned how to integrate the Jess rule engine into our application by using Jess’s native classes and, with a bit more effort, by using JSR 94. We’ve seen how business rules can be separated into separate files that get processed by the rule engine when our application runs.

If we have rules for the same business logic, written for another JSR 94 compliant rule engine, then we can simply add in the driver for our alternative rule engine, and update the driver name our application should use, and no further code changes should be necessary.

There are more details at jess.sandia.gov for Embedding Jess in a Java Application, and Oracle has a useful guide for Getting Started With the Java Rule Engine API (JSR 94).

As usual, the code we looked at in this article is available over on GitHub.


» 下一篇: Java周报, 344