How to run Activiti BPMN with Spring Boot

This tutorial introduces you to Activiti BPMN and how to run it in a Spring Boot environment. We will demonstrate how to run a simple Process which includes user Tasks in it from a Spring Boot application..

Activiti is an open-source workflow engine written in Java that you can use to execute BPMN 2.0 compliant business processes. In this article we will mainly cover how run Activiti in a Spring Boot runtime.

Setting up the Project

Firstly, we will set up the project with Spring Boot initializr. In our example, we will use Spring Boot 2.6.11 API. Here is the list of dependencies we will need:

<?xml version="1.0" encoding="UTF-8"?>
<project
	xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.11</version>
		<relativePath/>

	</parent>
	<groupId>org.activiti</groupId>
	<artifactId>activiti-example</artifactId>
	<version>7.4.1</version>
	<name>Activiti :: Examples :: API Basic Process and Tasks Example No Bean</name>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.activiti</groupId>
			<artifactId>activiti-spring-boot-starter</artifactId>
			<version>7.4.1</version>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
	<repositories>
		<repository>
			<id>activiti-releases</id>
			<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url>
		</repository>
	</repositories>
</project>

As you can see, we are using Activiti version 7.4.1 through the activiti-spring-boot-starter. We also need to include in the repositories section the URL where Activiti releases are available.

In the next section, we will learn how to design the BPMN process using an online Process designer.

Designing the BPMN Process

In order to design the BPMN 2.0 process there are multiple options. You can try the an online Process designer http://demo.bpmn.io/

On the other hand, if you want to learn how to install a Business Process Designer as Eclipse plugin, then check this Activiti tutorial.

In our sample process, there’s an Human Task “Process Content” where the Task owner will choose if a Content is appropriate or not:

The decision will be taken from our example application as soon as the Task owner claims the Task.

Coding the Spring Boot application

The application logic is contained in the following @SpringBootApplication Class:

@SpringBootApplication
@EnableScheduling
public class DemoApplication implements CommandLineRunner {

    private Logger logger = LoggerFactory.getLogger(DemoApplication.class);

    private final ProcessRuntime processRuntime;

    private final TaskRuntime taskRuntime;

    private final SecurityUtil securityUtil;

    private final ObjectMapper objectMapper;

    public DemoApplication(ProcessRuntime processRuntime,
                           TaskRuntime taskRuntime,
                           SecurityUtil securityUtil,
                           ObjectMapper objectMapper) {
        this.processRuntime = processRuntime;
        this.taskRuntime = taskRuntime;
        this.securityUtil = securityUtil;
        this.objectMapper = objectMapper;
    }

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

    }

    @Override
    public void run(String... args) {
        securityUtil.logInAs("system");

        Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(0, 10));
        logger.info("> Available Process definitions: " + processDefinitionPage.getTotalItems());
        for (ProcessDefinition pd : processDefinitionPage.getContent()) {
            logger.info("\t > Process definition: " + pd);
        }

    }

    @Scheduled(initialDelay = 1000, fixedDelay = 5000)
    public void processText() {

        securityUtil.logInAs("system");

        LinkedHashMap content = pickRandomString();

        SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yy HH:mm:ss");

        logger.info("> Starting process to process content: " + content + " at " + formatter.format(new Date()));

        ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
                .start()
                .withProcessDefinitionKey("categorizeHumanProcess")
                .withName("Processing Content: " + content)
                .withVariable("content", objectMapper.convertValue(content, JsonNode.class))
                .build());
        logger.info(">>> Created Process Instance: " + processInstance);


    }

    @Scheduled(initialDelay = 1000, fixedDelay = 5000)
    public void checkAndWorkOnTasksWhenAvailable() {
        securityUtil.logInAs("bob");

        Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10));
        if (tasks.getTotalItems() > 0) {
            for (Task t : tasks.getContent()) {

                logger.info("> Claiming task: " + t.getId());
                taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(t.getId()).build());

                List<VariableInstance> variables = taskRuntime.variables(TaskPayloadBuilder.variables().withTaskId(t.getId()).build());
                VariableInstance variableInstance = variables.get(0);
                if (variableInstance.getName().equals("content")) {
                    LinkedHashMap contentToProcess = objectMapper.convertValue(variableInstance.getValue(), LinkedHashMap.class);
                    logger.info("> Content received inside the task to approve: " + contentToProcess);

                    if (contentToProcess.get("body").toString().contains("activiti")) {
                        logger.info("> User Approving content");
                        contentToProcess.put("approved",true);
                    } else {
                        logger.info("> User Discarding content");
                        contentToProcess.put("approved",false);
                    }
                    taskRuntime.complete(TaskPayloadBuilder.complete()
                            .withTaskId(t.getId()).withVariable("content", contentToProcess).build());
                }


            }

        } else {
            logger.info("> There are no task for me to work on.");
        }

    }


    @Bean
    public Connector tagTextConnector() {
        return integrationContext -> {
            LinkedHashMap contentToTag = (LinkedHashMap) integrationContext.getInBoundVariables().get("content");
            contentToTag.put("tags", singletonList(" :) "));
            integrationContext.addOutBoundVariable("content", contentToTag);
            logger.info("Final Content: " + contentToTag);
            return integrationContext;
        };
    }

    @Bean
    public Connector discardTextConnector() {
        return integrationContext -> {
            LinkedHashMap contentToDiscard = (LinkedHashMap) integrationContext.getInBoundVariables().get("content");
            contentToDiscard.put("tags", singletonList(" :( "));
            integrationContext.addOutBoundVariable("content", contentToDiscard);
            logger.info("Final Content: " + contentToDiscard);
            return integrationContext;
        };
    }


    private LinkedHashMap pickRandomString() {
        String[] texts = {"hello from london", "Hi there from activiti!", "all good news over here.", "I've tweeted about activiti today.",
                "other boring projects.", "activiti cloud - Cloud Native Java BPM"};
        LinkedHashMap<Object,Object> content = new LinkedHashMap<>();
        content.put("body",texts[new Random().nextInt(texts.length)]);
        return content;
    }

}

As you can see, within this application there are two @Scheduled activities:

The method processText will create a new Process Instance with some random content for the process variable “content”

The method checkAndWorkOnTasksWhenAvailable will claim the active Tasks and complete them, based on the value of the “content” process variable.

We also need a Configuration Bean which contains the list of Users and Roles you can use to access the Spring Boot application:

@Configuration
public class DemoApplicationConfiguration {

    private Logger logger = LoggerFactory.getLogger(DemoApplicationConfiguration.class);

    @Bean
    public UserDetailsService myUserDetailsService() {

        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();

        String[][] usersGroupsAndRoles = {
                {"bob", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"john", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"hannah", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"other", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"},
                {"system", "password", "ROLE_ACTIVITI_USER"},
                {"admin", "password", "ROLE_ACTIVITI_ADMIN"},
        };

        for (String[] user : usersGroupsAndRoles) {
            List<String> authoritiesStrings = asList(Arrays.copyOfRange(user, 2, user.length));
            logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
            inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]),
                    authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList())));
        }


        return inMemoryUserDetailsManager;
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

Finally, to set the SecurityContext in the Activiti Engine, we will add a Component Class, which contains a SecurityContextImpl:

@Component
public class SecurityUtil {

    private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    @Autowired
    private UserDetailsService userDetailsService;

    public void logInAs(String username) {

        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (user == null) {
            throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
        }
        logger.info("> Logged in as: " + username);
        SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return user.getAuthorities();
            }

            @Override
            public Object getCredentials() {
                return user.getPassword();
            }

            @Override
            public Object getDetails() {
                return user;
            }

            @Override
            public Object getPrincipal() {
                return user;
            }

            @Override
            public boolean isAuthenticated() {
                return true;
            }

            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {

            }

            @Override
            public String getName() {
                return user.getUsername();
            }
        }));
        org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
    }
}

You can run the application as follows:

mvn install spring-boot:run

You will observe that every 5 seconds a new Process Instance starts and available Tasks are claimed accordingly:

spring boot activiti tutorial

The Spring Boot project is available under the Activiti examples: https://github.com/Activiti/Activiti/tree/develop/activiti-examples/activiti-api-basic-full-example-nobean

Leave a comment