Activiti and Springboot
Building
The basic Activiti runner
Maven dependencies
1 | <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
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 |
|
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 |
|
So, when running the application, you’d see:
1 | . ____ _ __ _ _ |
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
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
14curl -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 | <dependency> |
and add the entity to the MyApp
class:
1 |
|
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 | public interface ApplicantRepository extends JpaRepository<Applicant, Long> { |
And now we can create the dedicated REST endpoint:
1 |
|
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 | curl -u admin:admin -H "Content-Type: application/json" http://localhost:8080/runtime/tasks |
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 | curl -u admin:admin -H "Content-Type: application/json" -d '{"action" : "complete", "variables": [ {"name":"telephoneInterviewOutcome", "value":true} ]}' http://localhost:8080/runtime/tasks/14 |
Testing
Junit + Springboot: test “happy path” (while omitting @Autowired fields and test e-mail server setup).
1 |
|
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
56public 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: