Introduction

This 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 at least version 8 needs to be installed and configured
Gradle plugin needs to be installed into Eclipse
Gradle 4.x needs to installed and configured
Dependencies : Spring boot, Activiti, slf4j, h2 in-memory database

Creating and setting up Gradle project

Create gradle project in Eclipse called SpringBootActiviti using the following gradle dependencies

buildscript {
	ext {
    	springBootVersion = '1.5.9.RELEASE'
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

jar {
	baseName = 'SpringBootActiviti'
    version = '0.0.1'         

    manifest {
        attributes("Main-Class": "com.springboot.activiti.main.Application")
    }
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

//exclude default logback which comes with spring boot
configurations {
    all*.exclude module : 'spring-boot-starter-logging'
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.activiti:activiti-spring-boot-starter-basic:5.22.0")
    compile("com.h2database:h2")
    compile("org.slf4j:slf4j-api")
    compile("org.slf4j:slf4j-log4j12")
}

When referring to build directories, the tutorial assumes the standard Gradle build paths for your gradle project:

SpringBootActiviti/src/main/java – Java source directory
SpringBootActiviti/src/main/resources – Resource directory
SpringBootActiviti/src/test/java – Java test directory
SpringBootActiviti/src/test/resources – Resource test directory

Create main class

package com.springboot.activiti.main;

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

@SpringBootApplication(scanBasePackages = "com.springboot.activiti")
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.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 cmd prompt.

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

Create logger

Put below log4j.properties file under classpath resource SpringBootActiviti/src/main/resources

log4j.rootLogger=INFO, ACT

log4j.appender.ACT=org.apache.log4j.ConsoleAppender
log4j.appender.ACT.layout=org.apache.log4j.PatternLayout
log4j.appender.ACT.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n

Create Process Engine

Create a directory called processes under SpringBootActiviti/src/main/resources and in this directory we will put all bpmn files.

Let’s create (New -> Other -> Activiti -> Activiti Diagram) Onboarding.bpmn file to design activiti flow.

Now you will find palette on the right hand side of the opened Onboarding.bpmn file. Now put StartEvent diagram and while selected StartEvent diagram click on properties and on General tab rename the Id as startEvent.

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 -> Id – fullName, Name – Full Name, Type – string. Field2 -> Id – yearsOfExperience, Name – Years of Experience, Type – long.

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

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 -> Id – personalWelcomeTime, Name – Personal Welcome Time, Type – Date.

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="flow2" 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 &gt; 4" sourceRef="decision" targetRef="personalIntro"></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="flow3" sourceRef="personalIntro" targetRef="endEvent"></sequenceFlow>
    <sequenceFlow id="flow4" sourceRef="automatedIntro" 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="170.0" y="210.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
        <omgdc:Bounds height="55.0" width="105.0" x="250.0" y="200.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="decision" id="BPMNShape_decision">
        <omgdc:Bounds height="40.0" width="40.0" x="400.0" y="208.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="personalIntro" id="BPMNShape_personalIntro">
        <omgdc:Bounds height="80.0" width="136.0" x="353.0" y="330.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="automatedIntro" id="BPMNShape_automatedIntro">
        <omgdc:Bounds height="55.0" width="105.0" x="368.0" y="80.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endEvent" id="BPMNShape_endEvent">
        <omgdc:Bounds height="35.0" width="35.0" x="620.0" y="210.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
        <omgdi:waypoint x="205.0" y="227.0"></omgdi:waypoint>
        <omgdi:waypoint x="250.0" y="227.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
        <omgdi:waypoint x="355.0" y="227.0"></omgdi:waypoint>
        <omgdi:waypoint x="400.0" y="228.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="personalIntroPath" id="BPMNEdge_personalIntroPath">
        <omgdi:waypoint x="420.0" y="248.0"></omgdi:waypoint>
        <omgdi:waypoint x="421.0" y="330.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="48.0" width="100.0" x="371.0" y="269.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="automatedIntroPath" id="BPMNEdge_automatedIntroPath">
        <omgdi:waypoint x="420.0" y="208.0"></omgdi:waypoint>
        <omgdi:waypoint x="420.0" y="135.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="489.0" y="370.0"></omgdi:waypoint>
        <omgdi:waypoint x="637.0" y="369.0"></omgdi:waypoint>
        <omgdi:waypoint x="637.0" y="245.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
        <omgdi:waypoint x="473.0" y="107.0"></omgdi:waypoint>
        <omgdi:waypoint x="637.0" y="107.0"></omgdi:waypoint>
        <omgdi:waypoint x="637.0" y="210.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

Change server port

We don’t want tomcat server in spring boot application to be started on random port, so we will create application.properties file and put under classpath resource src/main/resources with below content.

server.port=9999

Create model class

package com.springboot.activiti.model;

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;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public long getYearsOfExperience() {
		return yearsOfExperience;
	}

	public void setYearsOfExperience(long yearsOfExperience) {
		this.yearsOfExperience = yearsOfExperience;
	}

	public Date getDate() {
		return date;
	}

	public void setDate(Date date) {
		this.date = date;
	}

}

Create below service class

package com.springboot.activiti.service;

import java.text.ParseException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.springboot.activiti.model.User;

@Service
public class OnboardingService {

	private static final Logger LOGGER = LoggerFactory.getLogger(OnboardingService.class);

	@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");

		LOGGER.info("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();
			LOGGER.info("Active outstanding tasks: [" + tasks.size() + "]");
			for (int i = 0; i < tasks.size(); i++) {
				Task task = tasks.get(i);
				LOGGER.info("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")) {
						LOGGER.info(
								"BEGIN [" + processInstance.getProcessDefinitionKey() + "] " + activity.getStartTime());
					}

					if (activity.getActivityType().equals("endEvent")) {
						endActivity = activity;
					} else {
						LOGGER.info("-- " + activity.getActivityName() + " [" + activity.getActivityId() + "] "
								+ activity.getDurationInMillis() + " ms");
					}
				}

				if (endActivity != null) {
					LOGGER.info("-- " + endActivity.getActivityName() + " [" + endActivity.getActivityId() + "] "
							+ endActivity.getDurationInMillis() + " ms");
					LOGGER.info(
							"COMPLETE [" + processInstance.getProcessDefinitionKey() + "] " + endActivity.getEndTime());
				}

			}

			// Re-query the process instance, making sure the latest state is
			// available
			processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId())
					.singleResult();

		}

	}
}

Create REST controller

package com.springboot.activiti.controller;

import java.text.ParseException;

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

import com.springboot.activiti.model.User;
import com.springboot.activiti.service.OnboardingService;

@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";
	}

}

Running the application

Run the main class Application.java file.

Test the application using postman

Request –

URL - http://localhost:9999/onboard
Method - Post
Body - 
{
	"name":"Soumitra Roy",
	"yearsOfExperience":10,
	"date":"08/09/2017"
}

Response – Onboarding completed successfully

Output on console

[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - Onboarding process started with process instance id [5], key [onboarding]
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - Active outstanding tasks: [1]
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - Processing Task [Enter Data]
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - BEGIN [onboarding] Wed Jan 24 12:00:32 IST 2018
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - -- Start [startEvent] 13 ms
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - -- Enter Data [usertask2] 172 ms
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - -- Exclusive Gateway [decision] 13 ms
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - Active outstanding tasks: [1]
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - Processing Task [Personal introduction and data entry]
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - BEGIN [onboarding] Wed Jan 24 12:00:32 IST 2018
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - -- Start [startEvent] 13 ms
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - -- Enter Data [usertask2] 172 ms
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - -- Exclusive Gateway [decision] 13 ms
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - -- Personal introduction and data entry [personalIntro] 115 ms
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - -- End [endEvent] 0 ms
[http-nio-9999-exec-7] INFO  com.db.activiti.service.OnboardingService  - COMPLETE [onboarding] Wed Jan 24 12:00:32 IST 2018

Request

URL - http://localhost:9999/onboard
Method - Post
Body -
{
	"name":"Rushikesh Fanse",
	"yearsOfExperience":2,
	"date":"24/01/2018"
}

Response – Onboarding completed successfully

Output on console

[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - Onboarding process started with process instance id [18], key [onboarding]
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - Active outstanding tasks: [1]
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - Processing Task [Enter Data]
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - BEGIN [onboarding] Wed Jan 24 12:01:35 IST 2018
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - -- Start [startEvent] 1 ms
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - -- Exclusive Gateway [decision] 0 ms
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - -- Enter Data [usertask2] 45 ms
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - -- Generic and automated data entry [automatedIntro] 3320 ms
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - -- End [endEvent] 1 ms
[http-nio-9999-exec-1] INFO  com.db.activiti.service.OnboardingService  - COMPLETE [onboarding] Wed Jan 24 12:01:39 IST 2018

Thanks for reading.

Tags:

I am a professional Web developer, Enterprise Application developer, Software Engineer and Blogger. Connect me on Roy Tutorials | TwitterFacebook Google PlusLinkedin | Reddit

1 thought on “Spring Boot Activiti Example

  1. Hi,

    It’s a very nice guide for starting with Activiti. Thanks for the post.

    I recently started working with activiti. I needed to configure Mail task with its server configuration in a Spring Boot app.

    Any idea on how do we do that? Any help would be appreciated.

    Thanks in advance.

Leave a Reply

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