Table of Contents
Service tasks are one of the fundamental building blocks of any process. They allow you to implement complex business logic, make calculations, talk to external systems and services, and more. In Activiti there are a number of ways in which a service task can be implemented:
What implementation approach you choose depend on the use-case. If you don’t need to use any Spring beans in your implementation then use a POJO Java Delegate. If your service task implementation needs to use, for example out-of-the-box Spring beans, then use the Spring Bean Java Delegate. These two approaches uses a “one operation per service task” implementation. If you need your implementation to support multiple operations, then go with the Spring bean method implementation.
There is also a runtime twist to this, most likely there will be multiple process instances calling the same service task implementation. And the same service task implementation might be called from multiple service tasks in a process. The implementation behaves differently depending on approach:
What this basically means is that if you use a third party class inside a Java Delegate, then it needs to be thread safe as it can be called by multiple concurrent threads. If you use a Spring bean approach then the same thing applies, if you inject beans they need to all be thread safe. With the Spring bean approach you can also change the bean instantiation scope to be PROTOTYPE, which means an instance will be created per service task.
This article cover the first approach - POJO Java Delegate.
Source code for the Activiti Developer Series can be found here.
Before starting with your service task implementation make sure to set up a proper Activiti Extension project.
Let’s start the usual way with a Hello World Java Delegate. However, we are going to take the opportunity to check process IDs and object instance IDs while we are at it, so the log message from the Java Delegate will be a little bit different than the usual “Hello World”.
Here is the implementation of the Java Delegate class:
package com.activiti.extension.bean;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorldJavaDelegate implements JavaDelegate {
private static Logger logger = LoggerFactory.getLogger(HelloWorldJavaDelegate.class);
@Override
public void execute(DelegateExecution execution) throws Exception {
logger.info("[Process=" + execution.getProcessInstanceId() +
"][Java Delegate=" + this + "]");
}
}
The class has been created in the com.activiti.extension.bean package, but it does not have to be located in this package. It is just good practice to put the class in this package if it ever needs to be converted to a Spring Java Delegate in the future. Then this package is scanned automatically by Activiti and any beans are instantiated.
A Java Delegate needs to implement the org.activiti.engine.delegate.JavaDelegate interface, which contains the execute method. This methods takes one parameter of type org.activiti.engine.delegate.DelegateExecution, which is how we get access to the process instance that is invoking this Java Delegate. As said in the introduction, a Java Delegate object instance for a Service Task is shared between process instances, so the DelegateExecution parameter is our way of finding out information about the active process instance.
Notice how we use an external class here for logging and that the logger.info method could be called concurrently by multiple threads. So it needs to be thread safe,which it is. Same thing applies for other class members that we define and use in the execute method. We can do whatever we want inside the execute method as all threads have independent call stacks.
Now to test the Java Delegate implementation create a process model looking like this:
Both of these Service Tasks uses the Java Delegate that we implemented:
In BPMN 2.0 XML it will look like this:
<serviceTask id="sid-05C898E5-B1B0-4A1A-89D0-5639FAE5A3BE"
name="Service Task 1 (Java Delegate D1)"
activiti:class="com.activiti.extension.bean.HelloWorldJavaDelegate">
When we run this process it will print similar logs to the following:
11:17:31,894 [http-nio-8080-exec-7] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=70005][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@7ce61257]
11:17:31,896 [http-nio-8080-exec-7] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=70005][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@2394529d]
If we let the process instance stay at the User Task and start another process, then we will see logs such as follows for the second process instance:
11:20:07,035 [http-nio-8080-exec-5] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=70015][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@7ce61257]
11:20:07,036 [http-nio-8080-exec-5] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=70015][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@2394529d]
What we can see here is that when inside one process instance each Service Task will have its unique version of the Java Delegate object instance (i.e. 7ce61257 and 2394529d). However, when starting a second process instance, it will reuse the Java Delegate instances from the first process instance.
When using the activiti:class attribute, things are not thread safe even if each service task has its own Java Delegate instance. This is because Java Delegate instances are shared between process instances. And this could potentially lead to library code used in the Java Delegate implementation being called concurrently, and as a result it needs to be thread-safe, as the log library we used in our example. Same thing for any class member variables, access needs to be synchronized.
So use thread-safe libraries and avoid class members.
Most of the time when you implement a Java Delegate you would want to use some process variable values. Process variables are typically set when the process instance is started, through the API, or by different activities in the process. They are stored in the database for each process instance.
So how do you pass these process variable values into the Java Delegate? It is not necessary to pass any value, you can access all process instance variables via the DelegateExecution object and the getVariable method.
There is one process variable that you can access immediately to try this out, and that is the initiator:
String initiator = (String)execution.getVariable("initiator");
logger.info("Initiator of the process has user ID = " + initiator);
To print all process variables for the process instance you can do this:
logger.info("--- Process variables:");
Map<String, Object> procVars = execution.getVariables();
for (Map.Entry<String, Object> procVar : procVars.entrySet()) {
logger.info(" [" + procVar.getKey() + " = " + procVar.getValue() + "]");
}
It is equally easy to create a new process variable via the setVariable method on the DelegateExecution object:
execution.setVariable("greeting", "Hello World!");
When you create new variables they are created in a scope, similar to how you create variables in a Java program. For example, if you create a variable inside a Java class method it is scoped to that method and not accessible outside of it. Variables in the Activiti workflow engine are scoped at for example process instance level, sub-process instance level, user task instance level etc. If you are creating a variable that already exist in the scope, then it will be overwritten with the new value.
However, there is a difference between Java and Activiti, when we create a variable with the setVariable method it will traverse the scope hierarchy until it reaches the top root scope and set the variable there, unless there is a local variable with the same name in some scope on the way to the root scope, in that case this local variable is overwritten.
So when we created the variable greeting it was set on the root process instance scope. This is because service tasks, in contrast to user tasks, have the same scope as the process instance they are created in.
If you want to be sure to set a variable at the scope you are at, then you should use the setVariableLocal method instead:
execution.setVariableLocal("greetingLocal", "Hello World Local!");
This has no effect on a service task as it does not have its own scope like a user task, unless it’s in an external process invoked via <callActivity id="someProcessId".
Let’s say we now have the following implementation:
public void execute(DelegateExecution execution) throws Exception {
logger.info("[Process=" + execution.getProcessInstanceId() +
"][Java Delegate=" + this + "]");
logger.info("[ActivityName=" + execution.getCurrentActivityName() +
"][ActivityId=" + execution.getCurrentActivityId() + "]");
String initiator = (String)execution.getVariable("initiator");
logger.info("Initiator of the process has user ID = " + initiator);
execution.setVariable("greeting", "Hello World!");
execution.setVariableLocal("greetingLocal", "Hello World Local!");
logger.info("--- Process variables:");
Map<String, Object> procVars = execution.getVariables();
for (Map.Entry<String, Object> procVar : procVars.entrySet()) {
logger.info(" [" + procVar.getKey() + " = " + procVar.getValue() + "]");
}
}
This will print a log looking something like this:
08:58:55,896 [http-nio-8080-exec-1] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=97501][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@132faf11]
08:58:55,896 [http-nio-8080-exec-1] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [ActivityName=Service Task 1 (Java Delegate D1)][ActivityId=sid-05C898E5-B1B0-4A1A-89D0-5639FAE5A3BE]
08:58:55,896 [http-nio-8080-exec-1] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - Initiator of the process has user ID = 1
08:58:55,897 [http-nio-8080-exec-1] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - --- Process variables:
08:58:55,897 [http-nio-8080-exec-1] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [initiator = 1]
08:58:55,897 [http-nio-8080-exec-1] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [greeting = Hello World!]
08:58:55,897 [http-nio-8080-exec-1] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [greetingLocal = Hello World Local!]
Important. as this is not a user task you cannot use the TaskService and get a task instance, such as:
TaskService taskService = execution.getEngineServices().getTaskService();
Task task = taskService.createTaskQuery().singleResult();
String taskId = task.getId();
You will get a NullPointerException on line 3.
Now, what happens if we move one of the Service tasks into a sub-process:
This now produces the following log:
09:21:20,862 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=100011][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@44749768]
09:21:20,862 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [ActivityName=Service Task 1 (Java Delegate D1)][ActivityId=sid-05C898E5-B1B0-4A1A-89D0-5639FAE5A3BE]
09:21:20,862 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - Initiator of the process has user ID = 1
09:21:20,862 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - --- Process variables:
09:21:20,862 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [initiator = 1]
09:21:20,862 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [greeting = Hello World!]
09:21:20,862 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [greetingLocal = Hello World Local!]
09:21:20,864 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=100011][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@5183bd3f]
09:21:20,864 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [ActivityName=Service Task 2 (Java Delegate D1)][ActivityId=sid-69D06583-0BF7-43A6-8C28-2A65EEF1508C]
09:21:20,864 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - Initiator of the process has user ID = 1
09:21:20,864 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - --- Process variables:
09:21:20,865 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [initiator = 1]
09:21:20,865 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [greeting = Hello World!]
09:21:20,865 [http-nio-8080-exec-4] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [greetingLocal = Hello World Local!]
If you have a normal sub-process like in our example above with <subProcess id="subProcess1", it is modelled as a part of the parent process (in the same BPMN and as a child of the XML element).
If the sub-process you want to call is subject to change, independent of the parent-process, you can use a <callActivity id="someProcessId". This sub-process is called an external process (not part of the parent BPMN, but lives in it's own BPMN). This allows you to redeploy the child-process without the need to alter the parent-process.
A normal sub-process is treated as a direct child of the process-instance, so you can access the parent's process variables from within the sub-process, as you have seen above. On the contrary, a call-activity does not have access to the parent variables and is a "process instance" itself. You can expose variables from the parent-process to the call-activity by using the in/out declarations.
This might sound like a weird question, one right? No, behind the scenes all variables are fetched and cached when you call the getVariable method for the first time. This might not always be ideal, if you for example have loads of variables. To just fetch the specified variable you need to use another method signature that takes an extra parameter:
String initiator = (String)execution.getVariable("initiator", false);
In this case the false parameter says that we do not want to fetch all variables. Just the one we actually specified, initiator in this case.
Now, let’s say you wanted to use the Java Delegate from multiple service tasks in your process but have the implementation behave a little bit different depending on from which service task it is invoked. Basically you want configure the implementation for each service task.
This can be done easily using Class fields and field injection. Let’s set a separate greeting for each service task. This can be done as follows, first click on Class fields for the service task:
Then set them up, for Service Task 1:
And for Service Task 2:
The BPMN 2.0 looks like this:
<serviceTask id="sid-05C898E5-B1B0-4A1A-89D0-5639FAE5A3BE"
name="Service Task 1 (Java Delegate D1)"
activiti:class="com.activiti.extension.bean.HelloWorldJavaDelegate">
<extensionElements>
<activiti:field name="greeting">
<activiti:string><![CDATA[Hello from Service Task 1]]></activiti:string>
</activiti:field>
</extensionElements>
</serviceTask>
We can now access this field in the Java Delegate implementation via an org.activiti.engine.delegate.Expression:
public class HelloWorldJavaDelegate implements JavaDelegate {
private static Logger logger = LoggerFactory.getLogger(HelloWorldJavaDelegate.class);
private Expression greeting;
public void setGreeting(Expression greeting) {
this.greeting = greeting;
}
@Override
public void execute(DelegateExecution execution) throws Exception {
logger.info("[Process=" + execution.getProcessInstanceId() +
"][Java Delegate=" + this + "]");
logger.info("[ActivityName=" + execution.getCurrentActivityName() +
"][ActivityId=" + execution.getCurrentActivityId() + "]");
String greetingText = (String) greeting.getValue(execution);
logger.info("The greeting set for this service task is: " + greetingText);
}
}
The field value is injected through a public setter method on your Java Delegate class, following the Java Bean naming conventions (e.g. field <activiti:field name="greeting"> has setter setGreeting(…)).
The output from this implementation looks like this:
10:46:50,956 [http-nio-8080-exec-10] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=102511][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@6618bfad]
10:46:50,956 [http-nio-8080-exec-10] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [ActivityName=Service Task 1 (Java Delegate D1)][ActivityId=sid-05C898E5-B1B0-4A1A-89D0-5639FAE5A3BE]
10:46:50,957 [http-nio-8080-exec-10] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - The greeting set for this service task is: Hello from Service Task 1
10:46:50,958 [http-nio-8080-exec-10] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [Process=102511][Java Delegate=com.activiti.extension.bean.HelloWorldJavaDelegate@588a2f17]
10:46:50,958 [http-nio-8080-exec-10] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - [ActivityName=Service Task 2 (Java Delegate D1)][ActivityId=sid-69D06583-0BF7-43A6-8C28-2A65EEF1508C]
10:46:50,959 [http-nio-8080-exec-10] INFO com.activiti.extension.bean.HelloWorldJavaDelegate - The greeting set for this service task is: Hello from Service Task 2
So as you might have figured out, using class fields are thread safe as they are set on the class when it is first initiated. And there is one class instance per service task. As usual, Java Delegate object instances are shared between process instances.
When using the activiti:class attribute, using field injection is always thread safe. For each service task that references a certain class, a new Java Delegate instance will be instantiated and fields (org.activiti.engine.delegate.Expression) will be injected once when the instance is created. Reusing the same class in multiple service tasks and process definitions is no problem.
So far the service tasks have been defined in the process definition in the default way, which means that they will be called synchronously in the same transaction and thread as the transaction and thread the process was started in.
So when the process instance execution reaches Service Task 1, it will have to stop and wait for the Java Delegate implementation to complete, and then the same for Service Task 2. When a service task contains long-running logic, like the invocation of an external Web Service or the construction of a large PDF document, this may not be the desired behaviour.
Activiti provides a solution for these cases in the form of async continuations. From a BPMN 2.0 XML perspective, the definition of an asynchronous service task (or another type of task) is easy. You only have to add an async attribute to the service task configuration:
<serviceTask id="sid-05C898E5-B1B0-4A1A-89D0-5639FAE5A3BE"
name="Service Task 1 (Java Delegate D1)"
activiti:async="true"
activiti:class="com.activiti.extension.bean.HelloWorldJavaDelegate">
When we define a service task with the async attribute set to true, the execution of the
service task logic will be executed in a separate transaction and thread.