Monday, December 15, 2008

Writing a custom ClassLoader for jBPM

Within our jBPM process engine we're dealing with dependencies on libraries that are, well..., not completely stable. The code in the node handlers is calling our SOA layer through generated API classes, which automagically take care of several boiler plate tasks (such as security) and are deployed as jars along with our process engine. The SOA layer evolves, so in time a number of versions has come to exist.

We encountered a problem because we're running several process definitions within one engine deployment. This includes both entirely different processes as well as new versions of already running process definitions. Our base of automated business processes has grown over time, with older process implementations relying on the early SOA API and the newer process implementations taking advantage of the later API additions. There are dependencies to different versions of the API - and while strict backwards compatibility might have solved this issue for us, in practice this proved not quite feasible.

So what were the issues we were trying to solve?
  • There are different versions of the generated API classes corresponding to different versions of the SOA services. One deployment of jBPM must be able to run processes that rely on different versions next to each other.
  • We wanted to be able to configure the dependency per process definition, but also for versions of a definition, so that a new incarnation of a process may take advantage of a new (and hopefully improved) version of a web service.
  • Not only the correct version of the API classes needs to be used, also the corresponding web service endpoints has to be available to the code running a process instance.
The configuration had to be external to the process archive, so it can be adjusted at deploy time. We've settled for a simple XML format, which allows for all required information to be present using minimum complexity:

   
   
      
      
      
      
   
   
      
      
      
      
   
   
      
      
      
      
   
   
      
      
      
      
   

This custom configuration consists of the following:
  • One line indicating the directory in which all the API jars are deployed. Take care that this directory and the jars in it are not on the standard classpath, because then you're gonna be stuck with only one version, which is not compatible with all of the calling code.
  • At least one entry for each process definition. A single entry can be used for each separately deployed version of the definition (as for process2) or different entries for the different version ranges (indicated using the min_version and/or max_version attributes).
  • For each process definition (version range) the jar file and endpoint for each required web service is added. The name is used for querying by the client code.
The way to include the correct jars to the classpath of a given process instance (running a certain process definition version) is through a custom class loader - a mechanism that is made available in jBPM version 3.3.0.GA - just set the 'jbpm.classloader' property in jbpm.cfg.xml to 'custom' and indicate the custom class loader by setting its name in the 'jbpm.classloader.classname' property. This custom class loader itself is almost too simple to mention: it extends from java.net.URLClassloader, and in the constructor it determines the name and version of the process definition before reading the applicable jar file names (as URLs) from the custom configuration file.

We've put the actual reading from the XML file in a utility class; for reading from XML we could have gone completely overboard and set up a schema and compiled Java classes from it with JAXB. Instead we simply used the dom4j library and a couple of simple XPath expressions to accomplish the same.

Our utility class has the following interface:
public final class ConfigurationUtil {
   public static URL[] getJarsForProcessDefinition(String processId, int version) throws IOException {...}
   public static String getEndpointForProcessDefinition(
      String processId, int version, String serviceName) throws IOException {...}
}
The first method delivers everything needed by the custom class loader's super class constructor. The second method reuses the XML parsing facility and allows the last requirement mentioned in the issues above to be satisfied efficiently.

In all, writing a custom ClassLoader was not much of a task anymore once we figured out what kind of custom configuration was applicable to our situation...