Advanced scripting in Activiti: Custom Configuration Injection
The scripting task is probably one of the ‘oldest’ classes in the Activiti code base, but I think it is still underused by many. The (perceived?) downsides are of course performance (interpretation vs compilation) and less support from IDE perspective.
However, the benefits (imo) outweigh this:
- Scripts are defined in the process xml itself. No more worries about versioning and having to juggle with libs on the classpath.
- What we’ve seen in the past is that less technical skilled people dare to try scripts. But never Java.
Anyway, what few people know or have realized is that you can do really awesome and advance stuff in scripts in Activiti. Since such a script is executed within the process engine, you have access to everything the engine is capable of. Yes … everything… which makes it both a very powerful but also (potential) dangerous thing (if you don’t know what you’re doing).
Let me walk you through such an example. I like to call it ‘custom configuration injection’ as a concept, because it effectively allows you to add custom logic at runtime which alters process execution significantly. If you have a cooler name for it, please let me know.
All code can be found on my Github Page: https://github.com/jbarrez/activiti-advanced-scripting
The use case
Now what is this thing I want to do. Well, I want to have a process that, when executed
- Adds a ‘task completion event handler’ to every user task that is executed
- This event handler must fire a custom event off to a remote URL, where potentially a event processor is doing its stuff
So basically, we want to fire off custom events to some remote URL whenever a task gets completed. A good use case for this could be Business Intelligence reporting/Complex event processing, eg with something like Esper.
The first version
The first cut of this functionality can be found at https://github.com/jbarrez/activit-advanced-scripting/blob/master/src/test/resources/org/activiti/test/my-process.bpmn20.xml. When this process is executed, the following happens:
var config = Context.getProcessEngineConfiguration(); var bpmnParser = config.getBpmnParser();
We simply fetch the current ProcessEngineConfiguration instance. We fetch the BpmnParser instance from this configuration, as we will want to change the general user task parsing for the whole engine.
Next, we build the script:
var script = ""; script = script + "importPackage(java.net);"; script = script + "importPackage(java.io);"; script = script + "var url = new URL('http://localhost:8182/echo');"; script = script + "var connection = url.openConnection();"; script = script + "connection.setRequestMethod('POST');"; script = script + "connection.setDoOutput(true);"; script = script + "var outputStream = new BufferedOutputStream(connection.getOutputStream());"; script = script + "outputStream.write(new java.lang.String(\"{'eventType':'task-complete'}\").bytes);"; script = script + "outputStream.flush();"; script = script + "connection.connect();"; script = script + "var respCode = connection.getResponseCode();"; script = script + "if (respCode != 200) "; script = script + "println('Response code : ' + respCode);"; script = script + "outputStream.close();"; script = script + "connection.disconnect();";
This is obviously not the most efficient way to do this, but it sure shows the details of what happens. The message ‘eventType:task-complete’ is send to the localhost:8182 url through standard java.net and java.io classes.
The tricky part comes next:
var handler = new ExecuteScriptOnTaskCompleteBpmnParseHandler("JavaScript"); handler.setUserTaskCompleteScript(script); bpmnParser.getBpmnParserHandlers().addHandler(handler); // reset the deployment cache such that the new listener gets picked up on a new redeploy config.getProcessDefinitionCache().clear();
Here we add a BpmnParseHandler class to the engine configuration. The parse handler will add the execution of the script defined above to every receival of the ‘task complete event’ send out by the engine. This parse handler kicks in every time a user task is parsed, which effectively adds our ‘send-event-to-remote-service’ to every user task now happening in your Activiti environment!
There is a unit test to see how this works: https://github.com/jbarrez/activiti-advanced-scripting/blob/master/src/test/java/org/activiti/test/ExecuteScriptInProcessTest.java. In the test, we setup a very simple ‘echo service’ which simply prints out whenever such an event is received. If you run it in your IDE, you’ll see something like this:
But we can do better
But we can do better. Check the following code.
var handler = new ExecuteScriptOnTaskCompleteBpmnParseHandler("JavaScript"); handler.setUserTaskCompleteScript("http://localhost:8182/scripts/task-complete.js"); handler.setExecuteScriptInJob(true); bpmnParser.getBpmnParserHandlers().addHandler(handler); // Update the configuration to use the correct job handler var jobHandler = new ExecuteScriptJobHandler(); config.getJobHandlers().put(jobHandler.type,jobHandler);
This code does the same as in the previous section, ie. attaching a listener for ‘complete’ events to every user task. However, this implementation:
- Executes the script asynchronously
- Does not define the script in the process xml, but it is fetched from a remote url
- Updates the job handler configuration
If you ask me, that’s pretty awesome! So this means that the actual sending of a message to the remote service is not impacting the execution performance of your process instance. Obviously, from here you can go crazy and add persistent queues and all that fanciness. And on top of that, the script is always fetched from a remote server. If you want to update the logic that is executed, simply change the script that is returned. This means you can impact process execution AT RUNTIME without touching the actual process.
There is a unit test for this at https://github.com/jbarrez/activiti-advanced-scripting/blob/master/src/test/java/org/activiti/test/ExecuteScriptWithJobTest.java
If you run this test, you’ll see the following. Note that we host the completion script as static file called ‘task-complete.js’ on the test server.
In the test, you can see we have to execute the async job specifically to see the output of the test.
Caveat
On small caveat here: when the process engine reboots, the configuration will be reloaded from config file. Hence, the process from above that injects custom logic is not added. However, this can easily be done by using a ProcessEngineLifeCycleListener implementation that executes a process definition of a certain category after the engine has booted up. If you for example give all these processes ‘config-processes’ as category, they can easily be executed on bootup.
Conclusion
Scripting in BPMN 2.0 processes is a very powerful feature. It allows you to change process execution engine-wide in a matter of a few lines. Of course, all the code above can be done with Java. But the examples above use nothing more than standard BPMN 2.0 and the javascript engine that is bundled with every JDK install.
Thanks for reading. Happy coding!