Spring Boot Activiti Example

Introduction

Spring Boot activiti example shows the simplicity of embedding Business Process Management (BPM) into our application using Activiti. We will build a spring boot application that embeds standards-based Business Process Modeling Notation (BPMN) logic into our application.

Activiti has advanced process design tools for embedding more sophisticated BPM logic into our application. These tools include an Eclipse-based and Web-Based BPMN Editor to name a few.

Prerequisites

Chrome Postman to test the application
Activiti Eclipse BPMN 2.0 Designer plugin needs to be installed into Eclipse
Java 8 or 12, Gradle 5.6, Spring boot 2.1.8, Activiti 6.0.0, h2 1.4.199

Creating Gradle Project

Create gradle project in Eclipse called spring-activiti-integration. The updated build.gradle script is given below with the following required dependencies.

buildscript {
	ext {
    	springBootVersion = '2.1.8.RELEASE'
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
    }
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'

repositories {
    mavenCentral()
}
sourceCompatibility = 12
targetCompatibility = 12

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
    compile("org.activiti:activiti-spring-boot-starter-basic:6.0.0")
    compile("com.h2database:h2:1.4.199")
}

Create Main Class

Creating main class is enough to deploy the Spring Boot application into Tomcat server.

package com.roytuts.spring.activiti.integration;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.roytuts.spring.activiti.integration")
public class SpringActivitiApp {

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

}

You should be able to build the blank project. Please ensure that the overall state is “BUILD SUCCESS” before continuing.

Note: you won’t get import statements until you build the application so you should create the above class with main method only. Once build gets successful you would be able to import the required spring boot dependencies.

Execute command – gradle clean build on the project root directory from command line prompt.

You will see the required jar files get downloaded and finally you would get “BUILD SUCCESSFUL” message.

Creating Process Engine Workflow

Let’s create in Eclipse, (New -> Other -> Activiti -> Activiti Diagram) Onboarding.bpmn file under src/main/resources/processes folder to design activiti flow.

Now you will find palette on the right hand side of the opened Onboarding.bpmn file.

Click on Properties tab, then Process -> Id -> onboarding and Name -> Onboarding.

If properties tab not found in the Eclipse then you can open it from Window -> Show View -> Properties (or Other -> Properties).

Now put StartEvent diagram and while selected StartEvent diagram click on properties and on General tab rename the Id as startEvent.

spring boot activiti

Now create UserTask diagram connecting from StartEvent and while selected click on properties and on General tab rename the Name as Enter Data.

On Main config tab write managers for Candidate groups(comma separated).

On tab Form add two fields using New button. Field1 -> IdfullName, NameFull Name, Typestring. Field2 -> IdyearsOfExperience, NameYears of Experience, Typelong.

You can set also other properties according to your requirements.

spring boot activiti

Then create Exclusive Gateway connecting from above user task. While selected click on General tab in properties and rename the Id as decision.

spring boot activiti

Now create two tasks – UserTask and ScriptTask connecting from Exclusive Gateway.

While selected user task, on General tab rename Id as personalIntro and rename Name as Personal introduction and data entry.

Now click on the connector (which connects from Exclusive Gateway to user task) and on General tab rename Id as personalIntroPath and rename Name as Years of experience > 4.

While select script task, on General tab rename Id as automatedIntro and rename Name as Generic and automated data entry. Now click on the connector (which connects from Exclusive Gateway to script task) and on General tab rename Id as automatedIntroPath.

Now click on properties of Exclusive Gateway and on General tab choose Default flow as automatedIntroPath because we want script task to be executed when yearsOfExperience is less than 4 years(else block) but personalIntroPath should be executed when yearsOfExperience is greater than 4 years.

Now click on properties of Personal introduction and data entry and write managers for Candidate groups (comma separated) on Main config tab.

On Form tab create a field using New button. Field -> IdpersonalWelcomeTime, NamePersonal Welcome Time, Typedate.

Now click on properties of Generic and automated data entry and write below script on Main config tab. Select Script language as javascript.

var dateAsString = new Date().toString();
execution.setVariable("autoWelcomeTime", dateAsString);

Now create EndEvent from the palette and connectors from both Personal introduction and data entry and Generic and automated data entry. While selected click on General tab in properties and rename Id as endEvent.

The complete diagram looks as below image:

spring boot activiti onboarding


The complete XML source of the above bpmn file:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
  <process id="onboarding" name="Onboarding" isExecutable="true">
    <startEvent id="startEvent" name="Start"></startEvent>
    <userTask id="usertask1" name="Enter Data" activiti:candidateGroups="managers">
      <extensionElements>
        <activiti:formProperty id="fullName" name="Full Name" type="string"></activiti:formProperty>
        <activiti:formProperty id="yearsOfExperience" name="Years of Experience" type="long"></activiti:formProperty>
      </extensionElements>
    </userTask>
    <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="usertask1"></sequenceFlow>
    <exclusiveGateway id="decision" name="Exclusive Gateway" default="automatedIntroPath"></exclusiveGateway>
    <sequenceFlow id="flow3" sourceRef="usertask1" targetRef="decision"></sequenceFlow>
    <userTask id="personalIntro" name="Personal introduction and data entry" activiti:candidateGroups="managers">
      <extensionElements>
        <activiti:formProperty id="personalWelcomeTime" name="Personal Welcome Time" type="date"></activiti:formProperty>
      </extensionElements>
    </userTask>
    <sequenceFlow id="personalIntroPath" name="Years of experience > 4" sourceRef="decision" targetRef="personalIntro">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${yearsOfExperience > 4}]]></conditionExpression>
    </sequenceFlow>
    <scriptTask id="automatedIntro" name="Generic and automated data entry" scriptFormat="javascript" activiti:autoStoreVariables="false">
      <script>var dateAsString = new Date().toString();
execution.setVariable("autoWelcomeTime", dateAsString);</script>
    </scriptTask>
    <sequenceFlow id="automatedIntroPath" sourceRef="decision" targetRef="automatedIntro"></sequenceFlow>
    <endEvent id="endEvent" name="End"></endEvent>
    <sequenceFlow id="flow4" sourceRef="automatedIntro" targetRef="endEvent"></sequenceFlow>
    <sequenceFlow id="flow5" sourceRef="personalIntro" targetRef="endEvent"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_onboarding">
    <bpmndi:BPMNPlane bpmnElement="onboarding" id="BPMNPlane_onboarding">
      <bpmndi:BPMNShape bpmnElement="startEvent" id="BPMNShape_startEvent">
        <omgdc:Bounds height="35.0" width="35.0" x="223.0" y="370.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
        <omgdc:Bounds height="55.0" width="105.0" x="450.0" y="360.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="decision" id="BPMNShape_decision">
        <omgdc:Bounds height="40.0" width="40.0" x="600.0" y="368.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="personalIntro" id="BPMNShape_personalIntro">
        <omgdc:Bounds height="65.0" width="171.0" x="535.0" y="160.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="automatedIntro" id="BPMNShape_automatedIntro">
        <omgdc:Bounds height="55.0" width="105.0" x="719.0" y="360.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endEvent" id="BPMNShape_endEvent">
        <omgdc:Bounds height="35.0" width="35.0" x="850.0" y="250.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
        <omgdi:waypoint x="258.0" y="387.0"></omgdi:waypoint>
        <omgdi:waypoint x="450.0" y="387.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="555.0" y="387.0"></omgdi:waypoint>
        <omgdi:waypoint x="600.0" y="388.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="personalIntroPath" id="BPMNEdge_personalIntroPath">
        <omgdi:waypoint x="620.0" y="368.0"></omgdi:waypoint>
        <omgdi:waypoint x="620.0" y="225.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="54.0" width="100.0" x="620.0" y="271.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="automatedIntroPath" id="BPMNEdge_automatedIntroPath">
        <omgdi:waypoint x="640.0" y="388.0"></omgdi:waypoint>
        <omgdi:waypoint x="719.0" y="387.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
        <omgdi:waypoint x="824.0" y="387.0"></omgdi:waypoint>
        <omgdi:waypoint x="867.0" y="385.0"></omgdi:waypoint>
        <omgdi:waypoint x="867.0" y="285.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
        <omgdi:waypoint x="706.0" y="192.0"></omgdi:waypoint>
        <omgdi:waypoint x="781.0" y="193.0"></omgdi:waypoint>
        <omgdi:waypoint x="867.0" y="193.0"></omgdi:waypoint>
        <omgdi:waypoint x="867.0" y="250.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

Creating VO Class

We are creating below VO or DTO class that will be used to parse a request JSON object from the clients or end users.

package com.roytuts.spring.activiti.integration.vo;

import java.util.Date;

import com.fasterxml.jackson.annotation.JsonFormat;

public class User {

	private String name;
	private long yearsOfExperience;
	@JsonFormat(pattern = "dd/MM/yyyy")
	private Date date;

	//getters and setters
}

Creating Service Class

The service class responsible for processing business logic in the business workflow.

package com.roytuts.spring.activiti.integration.service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.activiti.engine.FormService;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.form.FormData;
import org.activiti.engine.form.FormProperty;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.impl.form.DateFormType;
import org.activiti.engine.impl.form.LongFormType;
import org.activiti.engine.impl.form.StringFormType;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ParseException;
import org.springframework.stereotype.Service;

import com.roytuts.spring.activiti.integration.vo.User;

@Service
public class OnboardingService {

	@Autowired
	private RuntimeService runtimeService;
	@Autowired
	private TaskService taskService;
	@Autowired
	private FormService formService;
	@Autowired
	private HistoryService historyService;

	public void onboard(final User user) throws ParseException {
		ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("onboarding");
		System.out.println(
				"Onboarding process started with process instance id [" + processInstance.getProcessInstanceId()
						+ "], key [" + processInstance.getProcessDefinitionKey() + "]");
		while (processInstance != null && !processInstance.isEnded()) {
			List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
			System.out.println("Active outstanding tasks: [" + tasks.size() + "]");
			for (int i = 0; i < tasks.size(); i++) {
				Task task = tasks.get(i);
				System.out.println("Processing Task [" + task.getName() + "]");
				Map<String, Object> variables = new HashMap<>();
				FormData formData = formService.getTaskFormData(task.getId());
				for (FormProperty formProperty : formData.getFormProperties()) {
					if (StringFormType.class.isInstance(formProperty.getType())) {
						variables.put(formProperty.getId(), user.getName());
					} else if (LongFormType.class.isInstance(formProperty.getType())) {
						variables.put(formProperty.getId(), user.getYearsOfExperience());
					} else if (DateFormType.class.isInstance(formProperty.getType())) {
						variables.put(formProperty.getId(), user.getDate());
					}
				}
				taskService.complete(task.getId(), variables);
				HistoricActivityInstance endActivity = null;
				List<HistoricActivityInstance> activities = historyService.createHistoricActivityInstanceQuery()
						.processInstanceId(processInstance.getId()).finished().orderByHistoricActivityInstanceEndTime()
						.asc().list();
				for (HistoricActivityInstance activity : activities) {
					if (activity.getActivityType().equals("startEvent")) {
						System.out.println(
								"BEGIN [" + processInstance.getProcessDefinitionKey() + "] " + activity.getStartTime());
					}
					if (activity.getActivityType().equals("endEvent")) {
						endActivity = activity;
					} else {
						System.out.println("-- " + activity.getActivityName() + " [" + activity.getActivityId() + "] "
								+ activity.getDurationInMillis() + " ms");
					}
				}
				if (endActivity != null) {
					System.out.println("-- " + endActivity.getActivityName() + " [" + endActivity.getActivityId() + "] "
							+ endActivity.getDurationInMillis() + " ms");
					System.out.println(
							"COMPLETE [" + processInstance.getProcessDefinitionKey() + "] " + endActivity.getEndTime());
				}
			}
			// Re-query the process instance, making sure the latest state is
			// available
			processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId())
					.singleResult();
		}
	}

}

Creating REST Controller

This REST controller class is responsible for handling request/response from clients.

package com.roytuts.spring.activiti.integration.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ParseException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.roytuts.spring.activiti.integration.service.OnboardingService;
import com.roytuts.spring.activiti.integration.vo.User;

@RestController
public class OnboardingController {

	@Autowired
	private OnboardingService onboardingService;

	@PostMapping("/onboard")
	public String startOnboarding(@RequestBody final User user) throws ParseException {
		onboardingService.onboard(user);
		return "Onboarding completed successfully";
	}

}

Deploying Application

Run the main class to deploy the application into embedded Tomcat server.

Running the main class will give you the following output and it is obvious that the process engine has started the process with id onboarding.

[Extra info : processDefinitionId = onboarding | processDefinitionName = Onboarding |  | id = decision |  | activityName = Exclusive Gateway | ]

Testing the Application

Test the application using Postman or any REST Client Tool.

Scenario – Experience greater than 4 years

URL - http://localhost:8080/onboard
Method - POST

Request Body

{
 "name":"Soumitra Roy",
 "yearsOfExperience":10,
 "date":"08/09/2017"
}

Response – Onboarding completed successfully

Console Log

BEGIN [onboarding]
-- Start [startEvent] 0 ms
-- Enter Data [usertask1] 89 ms
-- Exclusive Gateway [decision] 0 ms
-- Personal introduction and data entry [personalIntro] 235 ms
-- End [endEvent] 0 ms
COMPLETE [onboarding]

Scenario – Experience less than or equal to 4 years

URL - http://localhost:8080/onboard

Method – POST

Request Body

{
 "name":"Rushikesh Fanse",
 "yearsOfExperience":2,
 "date":"24/01/2018"
}

Response – Onboarding completed successfully

Console Log

BEGIN [onboarding]
-- Start [startEvent] 6 ms
-- Enter Data [usertask1] 365 ms
-- Exclusive Gateway [decision] 34 ms
-- Generic and automated data entry [automatedIntro] 1810 ms
-- End [endEvent] 1 ms
COMPLETE [onboarding]

Source Code

download source code

Thanks for reading.

1 thought on “Spring Boot Activiti Example

  1. Very nice article to get started with activiti and spring boot. One thing that it’s missing I this is the condition on when to flow from exclusive gateway to the user task and the service task. Only the name of the path has been set. When I run the above code, I get Processing Task [Personal introduction and data entry] even when the yearsOfExperience is less than or equal to 4. To solve this, in the diagram, with the connector from the exclusive gateway to the user task selected, under Properties > Main Config > Condition, I added ${yearsOfExperience > 4}. Then everything works fine.

Leave a Reply

Your email address will not be published. Required fields are marked *