Activiti Enterprise Developer Series - Service Tasks - Spring Bean Java Delegates

cancel
Showing results for 
Search instead for 
Did you mean: 

Activiti Enterprise Developer Series - Service Tasks - Spring Bean Java Delegates

gravitonian
Alfresco Employee
1 0 7,290

Table of Contents                   

 

Introduction

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:

 

  1. As a POJO, which is called a Java Delegate
  2. As a Spring Bean Java Delegate (what this article covers)
  3. As a Spring Bean method

 

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:

 

  1. POJO Java Delegate - inside a process instance you have one class instance per service task, between process instances you share class instances per service task
  2. Spring Bean Java Delegate - same Spring bean (i.e. class instance) is used within a process instance and between process instances
  3. Spring Bean method - same Spring bean (i.e. class instance) is used within a process instance and between process instances but there is no field injection and all data is passed in via method params, so thread-safe

 

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 second approach - Spring Bean Java Delegate.

Source Code

Source code for the Activiti Developer Series can be found here.

Prerequisites

Before starting with your service task implementation make sure to set up a proper Activiti Extension project.

Implementing a Hello World Spring Bean Java Delegate

This is pretty much the same thing as implementing a POJO Java Delegate, you just “Springify” it a bit so it can act as a Spring Bean and wire in other Spring Beans. Let’s start the usual way with a Hello World Spring Bean 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 Spring Bean Java Delegate will be a little bit different than the usual “Hello World”.

Coding the Java class

Here is the implementation of the Spring Bean 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;
import org.springframework.stereotype.Component;

@Component("helloWorld")
public class HelloWorldSpringJavaDelegate implements JavaDelegate {
private static Logger logger = LoggerFactory.getLogger(HelloWorldSpringJavaDelegate.class);

@Override
public void execute(DelegateExecution execution) throws Exception {
logger.info("[Process=" + execution.getProcessInstanceId() +
"][Spring Java Delegate=" + this + "]");
}
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

 

There are two things here that differentiate a Spring Bean Java Delegate implementation from a POJO Java Delegate implementation. The first is that it is annotated with org.springframework.stereotype.Component, setting it up to be discovered and  registered as a Bean in the Spring application context. The second thing, which is very important, is that the class needs to be defined in the com.activiti.extension.bean package, or any sub-package, for the Spring component scanning to find it.

 

Note here that the Spring Bean will have the name helloWorld, as specified via the @Component annotation. If we don’t specify a name then the classname will be used with first letter in lower-case.

 

The rest of the implementation is the same as for the POJO Java Delegate, so read through the implementation section for it.

Testing the Spring Bean Java Delegate

Now to test the Java Delegate implementation create a process model looking like this:

 

And the Service Task is connected to the Spring Bean Java Delegate implementation via the Delegate expression property:

 

In BPMN 2.0 XML it will look like this:

 

<serviceTask id="sid-7C83EB30-8E02-400B-BEEF-CAE34BFB6FFD"
name="Service Task 1 (Spring Bean Java Delegate)"
activiti:delegateExpression="${helloWorld}">
‍‍‍

 

Note here that instead of using activiti:class we use activiti:delegateExpression to tell Activiti about the Spring Bean name. An activiti:delegateExpression can be used when the expression resolves to a Java object instance that implements the JavaDelegate interface.

 

When we run this process it will print similar logs to the following:

 

02:43:54,236 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldJavaDelegate  - [Process=102534][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@3e39eeb0]

02:43:54,238 [http-nio-8080-exec-9] INFO  com.activiti.extension.bean.HelloWorldJavaDelegate  - [Process=102534][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@3e39eeb0]

 

We can immediately see the difference from when using POJO Java Delegates, there is only one instance of the Spring Bean used by both service tasks. This is the standard way the Spring container works, it creates Singletons. 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:

 

02:46:20,065 [http-nio-8080-exec-2] INFO  com.activiti.extension.bean.HelloWorldJavaDelegate  - [Process=102542][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@3e39eeb0]

02:46:20,070 [http-nio-8080-exec-2] INFO  com.activiti.extension.bean.HelloWorldJavaDelegate  - [Process=102542][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@3e39eeb0]

 

And we can see that the initial Spring Bean instance is used also by the second process instance. So we need to make sure classes that we use are thread-safe. And that access to class members are serialized.

Spring Bean Java Delegates and Thread Safety

When using the activiti:delegateExpression attribute, the thread-safety of the delegate instance will depend on how the expression is resolved. If the delegate expression is reused in various tasks and/or process definitions, and the expression always returns the same instance, then it is not thread-safe. Let’s look at a few examples to clarify.

 

Suppose the expression is ${helloWorld}, like in our example above, which resolves to a shared single Spring Bean JavaDelegate instance. Then using this expression in different tasks and/or process definitions, and having it always resolve to the same instance, will cause problems unless we are careful. In this case, thread safe libraries must be used, and member variables cannot be used unless access is synchronized.

 

However, if the expression looks something like this ${helloWorldFactory.createDelegate(someVariable)}, where helloWorldFactory is a Spring Bean factory that creates a new instance each time the expression is resolved. Then there is no problem with regards to thread-safety. Same thing if we re-implement the Java Delegate class with Spring bean scope PROTOTYPE, more on this further on in this article.

Using Process Variables in the Spring Bean Java Delegate

In a Spring Bean Java Delegate we access process variables in the same way as in a POJO Java Delegate, so read its docs.

Using Field Injection in the Spring Bean Java Delegate

Now, let’s say you wanted to use the Spring Bean 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-7C83EB30-8E02-400B-BEEF-CAE34BFB6FFD"
name="Service Task 1 (Spring Bean Java Delegate)"
activiti:delegateExpression="${helloWorld}">

<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:

 

@Component("helloWorld")
public class HelloWorldSpringJavaDelegate implements JavaDelegate {
private static Logger logger = LoggerFactory.getLogger(HelloWorldSpringJavaDelegate.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() +
"][Spring Java Delegate=" + this + "]");

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:

 

09:58:57,566 [http-nio-8080-exec-14] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - [Process=107501][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@f787778]

09:58:57,566 [http-nio-8080-exec-14] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - The greeting set for this service task is: Hello from Service Task 1

 

09:58:57,567 [http-nio-8080-exec-14] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - [Process=107501][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@f787778]

09:58:57,567 [http-nio-8080-exec-14] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - The greeting set for this service task is: Hello from Service Task 2

 

So when we used the POJO Java Delegate we could understand why this would work as there is one class instance per service task in a process definition. How come it works for Spring Bean Java Delegates too, when only a single instance of the bean is used by all service tasks?

 

A class field is stateless and thus can be shared between all service tasks and process instances. The secret is in the greeting.getValue(execution) call. Multiple calls to <Expression>.getValue(<DelegateExecution>) can happen concurrently without problems, Activiti will figure out what value to return depending on current process instance.

Field Injection and Thread Safety

Field Injection is not thread-safe when using singleton Spring Bean Java Delegates. You could have multiple parallel execution paths in a process instance, and multiple process instances running in parallel, calling the same Java Delegate instance. And as there is only one instance of the delegate and X number of threads trying to inject an org.activiti.engine.delegate.Expression, then this will lead to race conditions.

 

To solve this problem we could rewrite the Spring Bean Java delegate to use an expression and passing the needed data to the delegate via method arguments. Or we could change our example to create a bean per service task instance instead, using the PROTOTYPE scope:

 

@Component("helloWorld")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class HelloWorldSpringJavaDelegate implements JavaDelegate {‍‍‍

 

The the logs would then look like this:

 

12:41:18,400 [http-nio-8080-exec-11] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - [Process=112501][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@fe8507a]

12:41:18,400 [http-nio-8080-exec-11] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - The greeting set for this service task is: Hello from Service Task 1

12:41:18,401 [http-nio-8080-exec-11] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - [Process=112501][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@6f6558f4]

12:41:18,401 [http-nio-8080-exec-11] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - The greeting set for this service task is: Hello from Service Task 2

 

So this looks better, one instance per service task. If we start another process instance it will look like this:

 

12:46:20,078 [http-nio-8080-exec-11] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - [Process=112509][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@1168625f]

12:46:20,078 [http-nio-8080-exec-11] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - The greeting set for this service task is: Hello from Service Task 1

12:46:20,079 [http-nio-8080-exec-11] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - [Process=112509][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@641c0070]

12:46:20,079 [http-nio-8080-exec-11] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - The greeting set for this service task is: Hello from Service Task 2

 

As we can see, we got four new instances created for our delegate, making it all thread safe. This might not be the most efficient solution if the production environment will have thousands of process instances running in parallel. See the Spring Bean Method approach for another solution.

 

Note. As of Activiti version 5.21, the process engine configuration can be configured in a way to disable the use of field injection on delegate expressions, by setting the value of the delegateExpressionFieldInjectionMode property (which takes one of the values in the org.activiti.engine.imp.cfg.DelegateExpressionFieldInjectionMode enum).

 

Following settings are possible:

 

  • DISABLED: fully disables field injection when using delegate expressions. No field injection will be attempted. This is the safest mode, when it comes to thread-safety.
  • COMPATIBILITY: in this mode, the behaviour will be exactly as it was before version 5.21: field injection is possible when using delegate expressions and an exception will be thrown when the fields are not defined on the delegate class. This is of course the least safe mode with regards to thread-safety, but it can be needed for backwards compatibility or can be used safely when the delegate expression is used only on one task in a set of process definitions (and thus no concurrent race conditions can happen).
  • MIXED: Allows injection when using delegateExpressions but will not throw an exception when the fields are not defined on the delegate. This allows for mixed behaviours where some delegates have injection (for example because they are not singletons) and some don’t.

 

The default mode for Activiti version 5.x is COMPATIBILITY.

Injecting Beans into Spring Bean Java Delegates

When we use Spring we most likely want to use other Spring Beans in our Java Delegate implementation. This can be Spring beans that we create and out-of-the-box Spring beans.

 

Let’s create a super simple bean that we can call from our delegate:

 

package com.activiti.extension.bean.service;

import org.springframework.stereotype.Service;

@Service
public class HelloWorldService {
public String greeting() {
return "Hello World from Service!";
}
}‍‍‍‍‍‍‍‍‍‍

 

Note that it is located in the com.activiti.extension.bean package, this means it will be scanned by Activiti and registered in the Spring application context. Now when this is done we can update our Spring Bean Java Delegate to wire in this bean plus an out-of-the-box bean called UserService:

 

package com.activiti.extension.bean;

import com.activiti.domain.idm.User;
import com.activiti.extension.bean.service.HelloWorldService;
import com.activiti.service.api.UserService;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.Expression;
import org.activiti.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component("helloWorld")
public class HelloWorldSpringJavaDelegate implements JavaDelegate {
private static Logger logger = LoggerFactory.getLogger(HelloWorldSpringJavaDelegate.class);

/**
* Expression representing the greeting class field
*/

private Expression greeting;

@Autowired
HelloWorldService simpleService;

@Autowired
UserService userService;

public void setGreeting(Expression greeting) {
this.greeting = greeting;
}

@Override
public void execute(DelegateExecution execution) throws Exception {
logger.info("[Process=" + execution.getProcessInstanceId() +
"][Spring Java Delegate=" + this + "]");

String greetingText = (String) greeting.getValue(execution);
logger.info("The greeting set for this service task is: " + greetingText);

logger.info("Injected Spring Bean greeting: " + simpleService.greeting());

User user = userService.findUser(Long.parseLong((String)execution.getVariable("initiator")));
String username = user.getFirstName() + " " + user.getLastName();
logger.info("Initiator is: " + username);
}
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

 

Running this produces a log similar to this:

 

02:03:21,181 [http-nio-8080-exec-3] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - [Process=115001][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@1251052b]

02:03:21,181 [http-nio-8080-exec-3] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - The greeting set for this service task is: Hello from Service Task 1

02:03:21,182 [http-nio-8080-exec-3] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - Injected Spring Bean greeting: Hello World from Service!

02:03:21,184 [http-nio-8080-exec-3] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - Initiator is: null Administrator

02:03:21,185 [http-nio-8080-exec-3] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - [Process=115001][Spring Java Delegate=com.activiti.extension.bean.HelloWorldSpringJavaDelegate@1251052b]

02:03:21,185 [http-nio-8080-exec-3] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - The greeting set for this service task is: Hello from Service Task 2

02:03:21,185 [http-nio-8080-exec-3] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - Injected Spring Bean greeting: Hello World from Service!

02:03:21,186 [http-nio-8080-exec-3] INFO  com.activiti.extension.bean.HelloWorldSpringJavaDelegate  - Initiator is: null Administrator

 

Accessing the HelloWorldService works nicely, mostly because it just returns a static String literal. But if you are calling methods that manipulate data you have to make them thread safe. Using the out-of-the-box UserService was also no problem, note that the Admin user does not have a first name and hence null.

 

Note. the order in which beans are read does not matter, the scanner will collect all bean info first and then create them in necessary order, just like it would work with XML based configuration of beans. The important thing is that all beans are in packages that are scanned.

Calling Spring Bean Java Delegates Asynchronously

See the POJO Java Delegate docs.