Activiti and Springboot

Building

The basic Activiti runner

Maven dependencies

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.activiti</groupId>
<artifactId>spring-boot-starter-basic</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.185</version>
</dependency>

The Springboot runner

  • creates an in-memory H2 database
  • creates an Activiti process engine using that database
  • exposes all Activiti services as Spring Beans
  • configures tidbits here and there such as the Activiti async job executor, mail server, etc.
    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    public class MyApp {

    public static void main(String[] args) {
    SpringApplication.run(MyApp.class, args);
    }
    }

BPMN process definitions

Set BPMN 2.0 process definition into the src/main/resources/processes folder. All processes placed here will automatically be deployed (ie. parsed and made to be executable) to the Activiti engine.

Step 1: create the process:
Let’s keep things simple to start, and create a CommandLineRunner that will be executed when the app boots up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
CommandLineRunner init( final RepositoryService repositoryService,
final RuntimeService runtimeService,
final TaskService taskService) {
return new CommandLineRunner() {
public void run(String... strings) throws Exception {
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("applicantName", "John Doe");
variables.put("email", "john.doe@activiti.com");
variables.put("phoneNumber", "123456789");
runtimeService.startProcessInstanceByKey("hireProcess", variables);
}
};
}

So what’s happening here is that we have created a map of all the variables needed to run the process and pass it when starting process. If you’d check the process definition you’ll see we reference those variables using ${variableName} in many places (e.g. the task description).

1
activiti:expression="${resumeService.storeResume()}"

Step 2: create the process bean
The process bean would be:

1
2
3
4
5
6
@Component
public class ResumeService {
public void storeResume() {
System.out.println("Storing resume ...");
}
}

So, when running the application, you’d see:

1
2
3
4
5
6
7
8
9
10
11
  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.2.0.RELEASE)

2015-02-16 11:55:11.129 INFO 304 --- [main] MyApp: Starting MyApp on The-Activiti-Machine.local with PID 304 ...
Storing resume ...
2015-02-16 11:55:13.662 INFO 304 --- [main] MyApp: Started MyApp in 2.788 seconds (JVM running for 3.067)

The Activiti REST API

  • Maven dependencies

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.activiti</groupId>
    <artifactId>spring-boot-starter-rest-api</artifactId>
    <version>${activiti.version}}</version>
    </dependency>
  • Configuration
    The REST API is secured by basic auth, and won’t have any users by default. So we will aadd an admin user to the system as shown below (add this to the MyApp class).
    ⚠️ Don’t do this in a production system of course!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Bean
    InitializingBean usersAndGroupsInitializer(final IdentityService identityService) {

    return new InitializingBean() {
    public void afterPropertiesSet() throws Exception {
    Group group = identityService.newGroup("user");
    group.setName("users");
    group.setType("security-role");
    identityService.saveGroup(group);

    User admin = identityService.newUser("admin");
    admin.setPassword("admin");
    identityService.saveUser(admin);
    }
    };
    }

    You can test this via curl command:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    curl -u admin:admin -H "Content-Type: application/json" -d '{"processDefinitionKey":"hireProcess", "variables": [ {"name":"applicantName", "value":"John Doe"}, {"name":"email", "value":"john.doe@alfresco.com"}, {"name":"phoneNumber",  "value":"1234567"} ]}' http://localhost:8080/runtime/process-instances
    {
    "tenantId": "",
    "url": "http://localhost:8080/runtime/process-instances/5",
    "activityId": "sid-42BAE58A-8FFB-4B02-AAED-E0D8EA5A7E39",
    "id": "5",
    "processDefinitionUrl": "http://localhost:8080/repository/process-definitions/hireProcess:1:4",
    "suspended": false,
    "completed": false,
    "ended": false,
    "businessKey": null,
    "variables": [],
    "processDefinitionId": "hireProcess:1:4"
    }

Actuator endpoint

  • Maven dependencies

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.activiti</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>${activiti.version}</version>
    </dependency>

    This adds a Spring Boot actuator endpoint for Activiti. If we restart the application, and hit http://localhost:8080/activiti/, we get some basic stats about our processes. With some imagination that in a live system you’ve got many more process definitions deployed and executing, you can see how this is useful.

    The same actuator is also registered as a JMX bean exposing similar information.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    completedTaskCountToday: 0,
    deployedProcessDefinitions: [
    "hireProcess (v1)"
    ],
    processDefinitionCount: 1,
    cachedProcessDefinitionCount: 1,
    runningProcessInstanceCount: {
    hireProcess (v1): 0
    },
    completedTaskCount: 0,
    completedActivities: 0,
    completedProcessInstanceCount: {
    hireProcess (v1): 0
    },
    openTaskCount: 0
    }

Dedicated REST endpoint

To finish our coding, we will create a dedicated REST endpoint for our hire process, that could be consumed by for example a javascript web application. So most likely, we’ll have a form for the applicant to fill in the details we’ve been passing programmatically above. And while we’re at it, let’s store the applicant information as a JPA entity. In that case, the data won’t be stored in Activiti anymore, but in a separate table and referenced by Activiti when needed.

1
2
3
4
5
<dependency>
<groupId>org.activiti</groupId>
<artifactId>spring-boot-starter-jpa</artifactId>
<version>${activiti.version}</version>
</dependency>

and add the entity to the MyApp class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
class Applicant {

@Id
@GeneratedValue
private Long id;

private String name;

private String email;

private String phoneNumber;

// Getters and setters

We’ll also need a Repository for this Entity (put this in a separate file or also in MyApp). No need for any methods, the Repository magic from Spring will generate the methods we need for us.

1
2
3
public interface ApplicantRepository extends JpaRepository<Applicant, Long> {
// ..
}

And now we can create the dedicated REST endpoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class MyRestController {

@Autowired
private RuntimeService runtimeService;

@Autowired
private ApplicantRepository applicantRepository;

@RequestMapping(value="/start-hire-process",
method= RequestMethod.POST, produces= MediaType.APPLICATION_JSON_VALUE)
public void startHireProcess(@RequestBody Map<String, String> data) {

Applicant applicant = new Applicant(data.get("name"),
data.get("email"), data.get("phoneNumber"));
applicantRepository.save(applicant);

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("applicant", applicant);
runtimeService.startProcessInstanceByKey("hireProcessWithJpa",
variables);
}
}

Let’s restart the application and start a new process instance:

1
curl -u admin:admin -H "Content-Type: application/json" -d '{"name":"John Doe", "email": "john.doe@alfresco.com",  "phoneNumber":"123456789"}' http://localhost:8080/start-hire-process

We can now go through our process. You could create a custom endpoints for this too, exposing different task queries with different forms.
Let’s see which task the process instance currently is at:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl -u admin:admin -H "Content-Type: application/json" http://localhost:8080/runtime/tasks
{
"order": "asc",
"size": 1,
"sort": "id",
"total": 1,
"data": [{
"id": "14",
"processInstanceId": "8",
"createTime": "2015-02-16T13:11:26.078+01:00",
"description": "Conduct a telephone interview with John Doe. Phone number = 123456789",
"name": "Telephone interview"
...
}],
"start": 0
}

So, our process is now at the Telephone interview. In a realistic application, there would be a task list and a form that could be filled in to complete this task. Let’s complete this task (we have to set the telephoneInterviewOutcome variable as the exclusive gateway uses it to route the execution):
(When we get the tasks again now, the process instance will have moved on to the two tasks in parallel in the subprocess (big rectangle):)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
curl -u admin:admin -H "Content-Type: application/json" -d '{"action" : "complete", "variables": [ {"name":"telephoneInterviewOutcome", "value":true} ]}' http://localhost:8080/runtime/tasks/14

{
"order": "asc",
"size": 2,
"sort": "id",
"total": 2,
"data": [
{
...
"name": "Tech interview"
},
{
...
"name": "Financial negotiation"
}
],
"start": 0
}

Testing

Junit + Springboot: test “happy path” (while omitting @Autowired fields and test e-mail server setup).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {MyApp.class})
@WebAppConfiguration
@IntegrationTest
public class HireProcessTest {

@Test
public void testHappyPath() {

// Create test applicant
Applicant applicant = new Applicant("John Doe", "john@activiti.org", "12344");
applicantRepository.save(applicant);

// Start process instance
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("applicant", applicant);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
"hireProcessWithJpa", variables);

// First, the 'phone interview' should be active
Task task = taskService.createTaskQuery()
.processInstanceId(processInstance.getId())
.taskCandidateGroup("dev-managers")
.singleResult();
Assert.assertEquals("Telephone interview", task.getName());

// Completing the phone interview with success should trigger two new tasks
Map<String, Object> taskVariables = new HashMap<String, Object>();
taskVariables.put("telephoneInterviewOutcome", true);
taskService.complete(task.getId(), taskVariables);

List<Task> tasks = taskService.createTaskQuery()
.processInstanceId(processInstance.getId())
.orderByTaskName().asc()
.list();
Assert.assertEquals(2, tasks.size());
Assert.assertEquals("Financial negotiation", tasks.get(0).getName());
Assert.assertEquals("Tech interview", tasks.get(1).getName());

// Completing both should wrap up the subprocess, send out the 'welcome mail'
// and end the process instance
taskVariables = new HashMap<String, Object>();
taskVariables.put("techOk", true);
taskService.complete(tasks.get(0).getId(), taskVariables);

taskVariables = new HashMap<String, Object>();
taskVariables.put("financialOk", true);
taskService.complete(tasks.get(1).getId(), taskVariables);

// Verify email
Assert.assertEquals(1, wiser.getMessages().size());

// Verify process completed
Assert.assertEquals(1, historyService.
createHistoricProcessInstanceQuery().finished().count());

}

Ten minutes example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class TenMinuteTutorial {
public static void main(String[] args) {
// Create Activiti process engine
ProcessEngine processEngine = ProcessEngineConfiguration
.createStandaloneProcessEngineConfiguration()
.buildProcessEngine();
// Get Activiti services
RepositoryService repositoryService = processEngine.getRepositoryService();
RuntimeService runtimeService = processEngine.getRuntimeService();
// Deploy the process definition
repositoryService.createDeployment()
.addClasspathResource("FinancialReportProcess.bpmn20.xml")
.deploy();
// Start a process instance
String procId = runtimeService.startProcessInstanceByKey("financialReport")
.getId();
// Get the first task
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery()
.taskCandidateGroup("accountancy").list();
for (Task task : tasks) {
System.out.println("Following task is available for accountancy group: "
+ task.getName());
// claim it
taskService.claim(task.getId(), "fozzie");
}
// Verify Fozzie can now retrieve the task
tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();
for (Task task : tasks) {
System.out.println("Task for fozzie: " + task.getName());
// Complete the task
taskService.complete(task.getId());
}
System.out.println("Number of tasks for fozzie: "
+ taskService.createTaskQuery().taskAssignee("fozzie").count());
// Retrieve and claim the second task
tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
for (Task task : tasks) {
System.out.println("Following task is available for management group: "
+ task.getName());
taskService.claim(task.getId(), "kermit");
}
// Completing the second task ends the process
for (Task task : tasks) {
taskService.complete(task.getId());
}
// verify that the process is actually finished
HistoryService historyService = processEngine.getHistoryService();
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery()
.processInstanceId(procId).singleResult();
System.out.println("Process instance end time: "
+ historicProcessInstance.getEndTime());
}
}
}


Further information: