Business Rules
Business Rules has been a powerful concept for decades. Do you remember Prolog? What about Drools? Business Rules engines allow us to write IF-THEN-ELSE conditions in a declarative way as opposite to the procedural way which is a common way of writing computer programs. With Business Rules you can focus on the logic and the outcome rather than how it is going to be processed by the engine. The Business Rules engine will produce the results in a most efficient way. Usually, it is an implementation of the Rete algorithm.
The other aspect when dealing with Business Rules is the possibility to change the rules dynamically without recompiling the programs that use those rules. An ideal scenario is having a separate authoring environment for the business representatives where they can create and change the rules. Once changed, the rules will be “magically” updated and consumed by the programs after that.
MuleSoft ESB provides out-of-the-box integration with Drools that allows you to add Business Rules in your flow. This article goes a step forward and shows you how to implement custom MuleSoft message processor that will support various Drools Business Rules formats. In addition the processor will be able to update the rules, if they change, dynamically without redeploying the application.
Drools
JBoss Drools is an open source Business Rules Management System written in Java. The native form of writing these rules is by using the Drools Rules Language (DRL) and its MVEL dialect. An example is worth a thousands of words. Here is a simple example how to classify the Customer’s purchase based on the amount of that purchase:
package com.interworks.labs.customer_rules_dt;
import com.interworks.labs.model.Customer;
//from row number: 1
//Silver
rule "Row 1 CustomerTypeDT"
dialect "mvel"
when
$customer : Customer( purchase >= 0 , purchase < 3000 )
then
$customer.setCustomerType( "Silver" );
end
//from row number: 2
//Gold
rule "Row 2 CustomerTypeDT"
dialect "mvel"
when
$customer : Customer( purchase >= 3000 , purchase < 10000 )
then
$customer.setCustomerType( "Gold" );
end
//from row number: 3
//Platinum
rule "Row 3 CustomerTypeDT"
dialect "mvel"
when
$customer : Customer( purchase >= 10000 , purchase < 999999 )
then
$customer.setCustomerType( "Platinum" );
end
When you write the rules this way you will focus on each particular rule and specify the conditions in a very natural way. Imagine if we have 100 rules and you have to write all of that logic in a procedural IF-THEN-ELSE fashion. It will be maintenance nightmare which is decoupled from the business.
MuleSoft ESB native Drools Support
Out-of-the-box MuleSoft offers integration with Drools:
You can easily embed rules in your flow with the following simple configuration:
<bpm:drools /> <bpm:rules rulesDefinition="CustomerType.drl" />
Nevertheless, there are many other Drools features that are very cool but not supported with this native integration. For example, besides DRL syntax, Drools supports many other formats like Excel based Decision Tables or Guided Decision Tables. These rule formats can be easily maintained by non-technical business users. In addition, Drools supports continuous integration of the rules with the programs that use those rules.
However, MuleSoft ESB is flexible enough and nothing stops us from writing custom MuleSoft Message Processor that will use the latest and brightest Drools features. So let’s go for it!
MuleSoft Custom Drools Message Processor
The latest Drools version at the moment of this writing is 7.6.Final. You can download all necessary deployment artifacts from this location:
Besides the engine, Drools comes with Business Rules authoring web application: Drools KIE Workbench. There are preconfigured Web application archives for deployment in various JEE containers: Wildfly, Tomcat etc. I’m using the Tomcat 8 WAR. Note that you need Maven installed and configured on the workstation. In order to deploy Drools KIE Workbench you should:
- Open the WAR file and the README.txt file within
- Configure the Tomcat container according the README.txt file (copy required libraries and configure Workbench users and roles)
- Copy the WAR into the Tomcat’s webapps folder and rename it to kie-drools-wb
- Start the Tomcat server
In addition, we shall configure the workstation’s Maven with credentials for accessing the Workbench’s local Maven repository. I’ve modified my user settings.xml by adding the following server configuration:
<server>
<id>guvnor-m2-repo</id>
<username>marjan</username>
<password>xxxxxxxxx</password>
<configuration>
<httpConfiguration>
<put>
<usePreemptive>true</usePreemptive>
</put>
</httpConfiguration>
</configuration>
</server>
In addition I’ve added the following Maven profile that points to the local Workbench’s Maven repository:
<profile>
<id>guvnor-m2-repo</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>guvnor-m2-repo</id>
<name>Guvnor M2 Repo</name>
<url>http://localhost:8080/kie-drools-wb/maven2/</url>
</repository>
</repositories>
</profile>
If you configure the Maven’s settings on a workstation different from the workstation the Workbench is running on then replace the localhost with the name of your server.
With the Drools KIE Workbench started and running, you can login and start creating your Business Rules. As a first step, I’ve uploaded my Customer model artifact, by navigation to Settings/Artifacts:
Ok, you are ready to create your first Workbench project. Navigate to Menu/Projects and press the Add Project button (we are not going to explain all of the steps to create Project and the Artifacts therein because that is not the focus of this article). For the purpose of this article, I’ve created the following Customer Type Guided Decision Table.
The customer-type business rules specified are self-explanatory.
Decision Tables can be easily changed by non-technical business users who will add new rows, modify the existing cells, etc. Once you are done with the configuration you can build and deploy the project into the Artifacts repository.
The deployed Drools Workbench projects are available as Maven artifacts to the programs that will use them.
Ok, the first part of the whole story has been completed: We have Business Rules deployed. The next step is using these rules from the MuleSoft ESB.
In this article we are using the latest Drools version. However, MuleSoft ESB 3.8 comes with Drools version 5 libraries. These libraries won’t work with our latest Drools version. In order to make the whole scenario work without class version problems, we need to “comment out” the libraries from the MuleSoft ESB runtime environment. For example, in the Anypoint studio environment, I’ve excluded the following jars in the folder plugins\org.mule.tooling.server.<<version>>\mule\lib\opt:
drools-api-5.0.1.jar.back
drools-compiler-5.0.1.jar.back
drools-core-5.0.1.jar.back
mvel2-2.0.10.jar.back
Note: Because of this runtime modification and the workstation’s specific Maven settings, the solution is suitable for on-premise deployments only.
With this modification applied, start (re-start) the Anypoint Studio and create a new Maven Mule Project. Add the following Maven dependencies in the pom file:
package com.interworks.labs.drools;
import org.kie.api.KieServices;
import org.kie.api.builder.KieScanner;
import org.kie.api.builder.ReleaseId;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.StatelessKieSession;
import org.mule.api.MuleEvent;
import org.mule.api.MuleException;
import org.mule.api.MuleMessage;
import org.mule.api.interceptor.Interceptor;
import org.mule.processor.AbstractInterceptingMessageProcessor;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class DroolsExecutor extends AbstractInterceptingMessageProcessor implements Interceptor, InitializingBean, DisposableBean {
private KieScanner kScanner;
private KieContainer kContainer;
private String packageName;
private String artifactId;
private String version;
private long scanInterval;
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getArtifactId() {
return artifactId;
}
public void setArtifactId(String artifactId) {
this.artifactId = artifactId;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public long getScanInterval() {
return scanInterval;
}
public void setScanInterval(long scanInterval) {
this.scanInterval = scanInterval;
}
@Override
public void afterPropertiesSet() throws Exception {
KieServices kieServices = KieServices.Factory.get();
ReleaseId releaseId = kieServices.newReleaseId(packageName, artifactId, version);
kContainer = kieServices.newKieContainer(releaseId);
if(scanInterval>0){
kScanner = kieServices.newKieScanner(kContainer);
kScanner.start(scanInterval);
}
}
@Override
public void destroy() throws Exception {
if(kScanner!=null)
kScanner.shutdown();
}
@Override
public MuleEvent process(MuleEvent event) throws MuleException {
MuleMessage muleMessage=event.getMessage();
Object payload=muleMessage.getPayload();
StatelessKieSession kSession = kContainer.newStatelessKieSession();
kSession.execute(payload);
return processNext(event);
}
}
At this point we have everything which is required to create our custom Drools Executor Message Processor. This Message Processor will continuously scan and monitor the specified Drools artifact and fetch the detected changes. Because scanning requires additional threads I’ve implemented also the Spring’s InitializingBean and DisposableBean interfaces. This implementation deals with Stateless sessions only, but it can easily be modified for different purposes (CEP, initial facts etc.).
package com.interworks.labs.drools;
import org.kie.api.KieServices;
import org.kie.api.builder.KieScanner;
import org.kie.api.builder.ReleaseId;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.StatelessKieSession;
import org.mule.api.MuleEvent;
import org.mule.api.MuleException;
import org.mule.api.MuleMessage;
import org.mule.api.interceptor.Interceptor;
import org.mule.processor.AbstractInterceptingMessageProcessor;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class DroolsExecutor extends AbstractInterceptingMessageProcessor implements Interceptor, InitializingBean, DisposableBean {
private KieScanner kScanner;
private KieContainer kContainer;
private String packageName;
private String artifactId;
private String version;
private long scanInterval;
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getArtifactId() {
return artifactId;
}
public void setArtifactId(String artifactId) {
this.artifactId = artifactId;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public long getScanInterval() {
return scanInterval;
}
public void setScanInterval(long scanInterval) {
this.scanInterval = scanInterval;
}
@Override
public void afterPropertiesSet() throws Exception {
KieServices kieServices = KieServices.Factory.get();
ReleaseId releaseId = kieServices.newReleaseId(packageName, artifactId, version);
kContainer = kieServices.newKieContainer(releaseId);
if(scanInterval>0){
kScanner = kieServices.newKieScanner(kContainer);
kScanner.start(scanInterval);
}
}
@Override
public void destroy() throws Exception {
if(kScanner!=null)
kScanner.shutdown();
}
@Override
public MuleEvent process(MuleEvent event) throws MuleException {
MuleMessage muleMessage=event.getMessage();
Object payload=muleMessage.getPayload();
StatelessKieSession kSession = kContainer.newStatelessKieSession();
kSession.execute(payload);
return processNext(event);
}
}
Let’s create simple demo flow.
Figure 4. Customer Type Flow
package com.interworks.labs.drools;
import org.kie.api.KieServices;
import org.kie.api.builder.KieScanner;
import org.kie.api.builder.ReleaseId;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.StatelessKieSession;
import org.mule.api.MuleEvent;
import org.mule.api.MuleException;
import org.mule.api.MuleMessage;
import org.mule.api.interceptor.Interceptor;
import org.mule.processor.AbstractInterceptingMessageProcessor;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class DroolsExecutor extends AbstractInterceptingMessageProcessor implements Interceptor, InitializingBean, DisposableBean {
private KieScanner kScanner;
private KieContainer kContainer;
private String packageName;
private String artifactId;
private String version;
private long scanInterval;
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getArtifactId() {
return artifactId;
}
public void setArtifactId(String artifactId) {
this.artifactId = artifactId;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public long getScanInterval() {
return scanInterval;
}
public void setScanInterval(long scanInterval) {
this.scanInterval = scanInterval;
}
@Override
public void afterPropertiesSet() throws Exception {
KieServices kieServices = KieServices.Factory.get();
ReleaseId releaseId = kieServices.newReleaseId(packageName, artifactId, version);
kContainer = kieServices.newKieContainer(releaseId);
if(scanInterval>0){
kScanner = kieServices.newKieScanner(kContainer);
kScanner.start(scanInterval);
}
}
@Override
public void destroy() throws Exception {
if(kScanner!=null)
kScanner.shutdown();
}
@Override
public MuleEvent process(MuleEvent event) throws MuleException {
MuleMessage muleMessage=event.getMessage();
Object payload=muleMessage.getPayload();
StatelessKieSession kSession = kContainer.newStatelessKieSession();
kSession.execute(payload);
return processNext(event);
}
}
Pay attention on the custom-interceptor definition. In particular, note that the Maven version specified is “LATEST”, i.e. the scanner in the class DroolsExecutor will fetch the latest Business Rules updates.
Start the flow from the Studio and test the exposed REST endpoint with Postman.
Ok, we have a workable solution. Without stopping the MuleSoft ESB, go back into the Drools Workbench and modify the Customer Type Decision table the following way:
Save the changes, build and deploy the model (overwrite the previous version).
Test with Postman with the same payload. This time the Customer is classified as “Silver” instead of “Gold”. The message processor has picked up the changes.
Cool, isn’t it? We have clear separation between developer’s and business roles. Non-technical business users can focus on the rules and push those rules into production with no downtime.
Because of its extensibility, MuleSoft ESB has proven itself as a powerful platform for easy integration with third-party systems even if the activities and connectors are not provided out of the box. Here at InterWorks we have long-standing experience with Java, Spring, and Integration in general that empowers us to implement various integration scenarios and working models that were not envisioned previously in a form suitable for our clients. In this particular case, we have managed to implement Drools Business Rules “hot” deployment within the started MuleSoft ESB flows.