Sunday, December 10, 2006

Dynamically Generated Classes as JBoss Rules Facts (Edson Tirelli)

As most users know, JBoss Rules is designed to use standard POJO JavaBeans as facts. This design directive intends, among other things, to:

  • make integration faster: as there is no need to replicate your Business Model in an engine proprietary business model
  • improve performance: as the engine is design to work as optimized as possible when running over the POJO beans. Also, no need to copy attribute values between mapped structures.

In real world, though, supporting POJO beans may not be enough to achieve above goals.

As we all know, Java, as a platform, provides several ways of developing applications that put dynamic resources at good use. And this is using both platform standard features and a whole lot of tools out there in the internet, both open and proprietary.

For instance, we often are asked about "How to use dynamically generated class' beans as facts?". This usually happens in companies that create applications that allow users to define part or the whole of the business model without touching java code. These applications usually have an embedded rules engine, and you may want the engine to reason over these dynamically generated business models.

And the good news is: you can do that with JBoss Rules, and 3.0.5 made it even easier!

The only issue is to make sure you are using a tool to generate your beans that will truly generate standard JavaBeans. I mean, a tool that:

  1. allows you to state specifically what is the package and class name for the generated class: this is mandatory in order to write efficient rules, as the first constraint the engine will apply over your facts are the class type of the facts your rules will reason over
  2. generates a no-argument default constructor (as per the JavaBeans spec)
  3. correctly generates the getXXX() methods for your properties (as per the JavaBeans spec)
  4. ideally will allow you to define what are your bean's key properties and will automatically generate equals()/hashCode() methods for it using these properties (allowing for consistent reasoning based on equality instead of only identity)
  5. ideally will generate fast accessors for your properties, allowing your beans to be high performing

Most bean generation tools will allow that. Just to name a few open source tools (it is not intended to be a full list):

  • ASM: this is by far my preferred framework. I bit lower level than other tools, but allows you to do anything you want (you are writing bytecode after all), and it is as fast as one can be (from my experience).
  • BCEL: another popular framework from Apache.
  • CGLIB: a higher level framework that is used in a lot of projects.

Above frameworks allows you to create a dynamic POJO JavaBean Business Model and use it as your rules business model. Above is just a small list and I'm sure there are a lot of frameworks out there that allow you to do it.

Unfortunately, one also popular framework that fails to work well with JBoss Rules is Apache DynaBeans.
I'm not saying that DynaBeans are bad to all, as there are some use cases for it described in its documentation. What I'm saying is simply that DynaBeans are bad for integration with JBoss Rules. You can read more about it in Mark's post.

So, you have your Business Model (that happens to be dynamically generated), that complies at least with requirements 1-3 listed above, and you want to use it when writing business rules. What you need to do?

You simply must make your business model available for the engine. JBoss Rules will need your classes in 2 distinct situations, that may occur in sequence or not:

  1. Rules compilation
  2. Rules execution

Let’s talk about how to use them in each of the above steps.

It is important to note that all this is closely related to how Class Loaders work in the Java platform, but since you are using dynamically generated classes, I assume you have knowledge of java Class Loader architecture.

1. Rules Compilation

When you write a rules file, whatever the format you use (DRL, DSL, Decision Tables), JBoss Rules needs to compile it before using. When compiling a rule base, the classes you use as facts must be "available".

For instance, if you write a rule base like this:

package org.drools.sample;

import org.drools.sample.facts.Person;
import org.drools.sample.facts.Cheese;

rule "Likes Cheese"
when
Person( $likes : likes )
Cheese( type == $likes )
then
// do some stuff
end


You must have classes Person and Cheese "available" for compilation. The concept of "available" varies though according to the compiler you are using. Also, Person or Cheese or even both classes may be dynamically generated classes. The engine does not care about it, but the compiler certainly does.

JBoss Rules uses JCI (Java Compiler Interface) as an abstraction layer for the compiler layer. JBoss Rules is integrated and tested with two underling compilers: JDT (default) and JANINO.

They will generate the same results, but from a compilation requirements perspective, they are a bit different, so let’s talk about how to make compilation works with each of them.

1.1. Janino

Janino
is not the default compiler for JBoss Rules but you can activate it either by using a PackageBuilderConfiguration.setCompiler() method or by setting the "drools.compiler=JANINO" system property.

For JCI+Janino to compile your rule base, it is enough to have dynamically generated classes available into your context class loader. So, for instance, if your dynamically generated classes were loaded into the same ClassLoader that loaded your Main application class, only thing you need to do is to call, before creating your PackageBuilder:

Thread.currentThread().setContextClassLoader( Main.class.getClassLoader() );


This will ensure that your PackageBuilder will use the provided class loader to find the classes for compilation. That will obviously succeed as the given class loader is the one that loaded your dynamic classes.

1.2. JDT

JDT is the default compiler for JBoss Rules. If you don't set the "drools.compiler" property, nor change it using the PackageBuilderConfiguration.setCompiler() method, it will be used to compile your rules.

Although, JCI+JDT have an additional requirement: to compile your rule base, the context class loader must be able to provide the actual classes’ bytecode, not only the loaded classes. In other words, you must provide a custom class loader that can provide an input stream for the byte code of each class used in your rule base.

In the above rule example, your custom class loader must return the input stream to the byte code of Person and Cheese classes, does not matter if the classes were dynamically generated or not.

For dynamically generated classes, it is enough for the custom class loader to implement the method:

public InputStream getResourceAsStream(final String name);


I have written a simple example that has a ClassLoader that uses a simple HashMap to store dynamically generated classes bytecodes and return them when the appropriate getResourceAsStream method is called.

So, what you need to do is similar to what you do in Janino:

Thread.currentThread().setContextClassLoader( myCustomClassLoader );


But, your custom class loader must comply with the above requirement.

2. Rules Execution

Rules execution may happen immediately after rules compilation or not. For example, you may compile a rule base and serialize it at build time. At runtime you simply deserialize the rule base and execute it.

The only thing you need to do at execution time is to make sure the same ClassLoader hierarchy used to load your rule base is used to load your fact classes. Again, this is not related to dynamically generated classes, but the problem of multiple ClassLoader shows up more frequently when using dynamically generated classes, because this one of the situations when people usually use multiple ClassLoaders inside the same application.

Just to understand the problem, remember that in java, a class C loaded by loader L1 is different from the same class C loaded by loader L2. So, if you load above rule base with class loader L1 and assert into working memory a Cheese class instance loaded by class loader L2, your rule will never match and obviously never fire.

So make sure your rule base does not load your classes in a different class loader than your application is using to load it.

Example

I created a very simple example of JBoss Rules using dynamically generated classes as facts and committed it here. It is not intended to be a general purpose solution, but rather a didactic example of one possible solution.

I am using ASM as the bytecode generation framework and kept the API functional but simple in order to not overload the example.

The example uses a Customer bean that is a regular POJO class and two dynamically generated classes: Order and LineItem.

Feel free to drop questions you may have to the JBoss Rules users list.

[]s
Edson