Build a Spring Boot 2.2 + Activiti 7 Leave Approval Workflow from Scratch

This guide walks through setting up a Spring Boot 2.2.11 project with Activiti 7.1, configuring Maven dependencies, explaining Activiti table prefixes, defining core services, creating a BPMN leave request process, configuring security and datasource, and exposing REST APIs for deployment, execution, and task handling.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Build a Spring Boot 2.2 + Activiti 7 Leave Approval Workflow from Scratch

Environment: Spring Boot 2.2.11.RELEASE, Activiti 7.1.0.M6, MySQL.

Do not import the Activiti dependencies via <dependencyManagement> as shown below, because it causes various problems:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.activiti.dependencies</groupId>
            <artifactId>activiti-dependencies</artifactId>
            <version>7.1.0.M6</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

Correct way is to add the required dependencies directly under <dependencies>:

<dependencies>
    <dependency>
        <groupId>org.activiti.dependencies</groupId>
        <artifactId>activiti-dependencies</artifactId>
        <version>7.1.0.M6</version>
        <type>pom</type>
    </dependency>
    <dependency>
        <groupId>org.activiti</groupId>
        <artifactId>activiti-spring-boot-starter</artifactId>
        <version>7.1.0.M6</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

Activiti creates several tables; their prefixes indicate purpose:

ACT_RE_* : Repository tables storing process definitions and static resources.

ACT_RU_* : Runtime tables holding live process instances, variables, async jobs, etc. They are cleared when a process ends.

ACT_HI_* : History tables preserving completed instances, activities, tasks, etc.

ACT_GE_* : General tables used across different scenarios.

Data Table Classification

General data (ACT_GE_*)

Process definitions (ACT_RE_*)

Runtime instances (ACT_RU_*)

History data (ACT_HI_*)

Other tables

Core Classes

ProcessEngine : The central entry point to obtain all Activiti services.

TaskService : Operations on user tasks (complete, delete, delegate, etc.).

RepositoryService : Manages process definitions and deployments.

RuntimeService : Starts process instances and interacts with running executions.

HistoryService : Queries historical data.

Leave Process Definition (holiday.bpmn)

<?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.pack.org">
  <process id="holiday" name="holiday" isExecutable="true">
    <startEvent id="startevent1" name="Start"/>
    <endEvent id="endevent1" name="End"/>
    <userTask id="usertask1" name="部门经理审批" activiti:assignee="${mgr}"/>
    <userTask id="usertask2" name="总经理审批" activiti:assignee="${top}"/>
    <userTask id="usertask3" name="填写审批单" activiti:assignee="${assignee}"/>
    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"/>
    <sequenceFlow id="flow3" sourceRef="usertask2" targetRef="endevent1"/>
    <sequenceFlow id="flow4" sourceRef="startevent1" targetRef="usertask3"/>
    <sequenceFlow id="flow5" sourceRef="usertask3" targetRef="usertask1"/>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_holiday">
    <bpmndi:BPMNPlane bpmnElement="holiday" id="BPMNPlane_holiday">
      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="505.0" y="60.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="505.0" y="550.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
        <omgdc:Bounds height="55.0" width="105.0" x="470.0" y="290.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
        <omgdc:Bounds height="55.0" width="105.0" x="470.0" y="420.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask3" id="BPMNShape_usertask3">
        <omgdc:Bounds height="55.0" width="105.0" x="470.0" y="170.0"/>
      </bpmndi:BPMNShape>
      <!-- edges omitted for brevity -->
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

Configuration (application.yml)

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.pack.domain
server:
  port: 8080
spring:
  activiti:
    check-process-definitions: true
    db-history-used: true
    history-level: full
    database-schema-update: true
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/activiti?serverTimezone=GMT%2B8
    username: root
    password: xxxxxx
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimumIdle: 10
      maximumPoolSize: 200
      autoCommit: true
      idleTimeout: 30000
      poolName: MasterDatabookHikariCP
      maxLifetime: 1800000
      connectionTimeout: 30000
      connectionTestQuery: SELECT 1

spring.activiti.db-history-used enables the 25‑table history schema; without it only 17 tables are created and the process diagram cannot be displayed.

spring.activiti.history-level controls the granularity of stored historical data (none, activity, audit, full).

spring.activiti.check-process-definitions forces automatic deployment of process definitions.

Spring Security Configuration (allow all requests)

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/**")
            .permitAll();
    }
}

HolidayService (core business logic)

@Service
public class HolidayService {

    private static final Logger logger = LoggerFactory.getLogger(HolidayService.class);

    @Resource
    private ProcessEngine processEngine;
    @Resource
    private RepositoryService repositoryService;
    @Resource
    private RuntimeService runtimeService;
    @Resource
    private TaskService taskService;

    /** Deploy the BPMN file */
    public void createDeploy() {
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("processes/holiday.bpmn")
                .addClasspathResource("processes/holiday.png")
                .name("请假申请单流程")
                .key("holiday")
                .category("InnerP")
                .deploy();
        logger.info("流程部署id: {}", deployment.getId());
        logger.info("流程部署名称: {}", deployment.getName());
    }

    public List<ProcessDefinition> queryProcessDefinitionByKey(String processDefinition) {
        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
        List<ProcessDefinition> list = query.processDefinitionKey(processDefinition).list();
        list.forEach(pd -> {
            logger.info("------------------------------------------------");
            logger.info("流程部署id:{}", pd.getDeploymentId());
            logger.info("流程定义id:{}", pd.getId());
            logger.info("流程定义名称:{}", pd.getName());
            logger.info("流程定义key:{}", pd.getKey());
            logger.info("流程定义版本:{}", pd.getVersion());
            logger.info("------------------------------------------------");
        });
        return list;
    }

    public void deleteDeployment(String deploymentId) {
        repositoryService.deleteDeployment(deploymentId, true);
    }

    public void startProcessInstanceById(String processDefinitionId) {
        ProcessInstance pi = runtimeService.startProcessInstanceById(processDefinitionId);
        logger.info("流程定义ID: {}", pi.getProcessDefinitionId());
        logger.info("流程实例ID: {}", pi.getId());
    }

    public void startProcessInstanceAssignVariables(String processDefinitionId, Map<String, Object> variables) {
        ProcessInstance pi = runtimeService.startProcessInstanceById(processDefinitionId, variables);
        logger.info("流程定义ID: {}", pi.getProcessDefinitionId());
        logger.info("流程实例ID: {}", pi.getId());
        logger.info("BussinessKey: {}", pi.getBusinessKey());
    }

    public List<Task> queryTasks(String assignee) {
        TaskQuery query = taskService.createTaskQuery();
        return query.taskAssignee(assignee).orderByTaskCreateTime().asc().list();
    }

    public void executionTask(Map<String, Object> variables, String instanceId) {
        Task task = taskService.createTaskQuery().processInstanceId(instanceId).singleResult();
        if (task == null) {
            logger.error("任务【{}】不存在", instanceId);
            throw new RuntimeException("任务【" + instanceId + "】不存在");
        }
        taskService.complete(task.getId(), variables);
    }
}

HolidayController (REST API)

@RestController
@RequestMapping("/holidays")
public class HolidayController {

    @Resource
    private HolidayService holidayService;

    @GetMapping("")
    public R lists(String key) {
        return R.success(holidayService.queryProcessDefinitionByKey(key));
    }

    @GetMapping("/_deploy")
    public R createDeploy() {
        holidayService.createDeploy();
        return R.success();
    }

    @GetMapping("/start")
    public R startProcess(String userId, String processDefinitionId) {
        Map<String, Object> vars = new HashMap<>();
        vars.put("assignee", userId);
        holidayService.startProcessInstanceAssignVariables(processDefinitionId, vars);
        return R.success();
    }

    @GetMapping("/tasks")
    public R myTasks(String userId) {
        List<Task> list = holidayService.queryTasks(userId);
        List<Map<String, Object>> result = list.stream().map(task -> {
            Map<String, Object> map = new HashMap<>();
            map.put("id", task.getId());
            map.put("assignee", task.getAssignee());
            map.put("createTime", task.getCreateTime());
            map.put("bussinessKey", task.getBusinessKey());
            map.put("category", task.getCategory());
            map.put("dueDate", task.getDueDate());
            map.put("desc", task.getDescription());
            map.put("name", task.getName());
            map.put("owner", task.getOwner());
            map.put("instanceId", task.getProcessInstanceId());
            map.put("variables", task.getProcessVariables());
            return map;
        }).collect(Collectors.toList());
        return R.success(result);
    }

    @GetMapping("/apply")
    public R fillApply(@RequestParam Map<String, Object> variables) {
        String instanceId = (String) variables.remove("instanceId");
        if (StringUtils.isEmpty(instanceId)) {
            return R.failure("未知任务");
        }
        holidayService.executionTask(variables, instanceId);
        return R.success();
    }
}

Testing Steps

Deploy the process (the BPMN file is auto‑deployed on startup). Verify act_re_procdef table.

Query process definitions: GET /holidays?key=holiday.

Start a leave request: GET /holidays/start?userId=10000&processDefinitionId=....

Query tasks for the user: GET /holidays/tasks?userId=10000.

Fill the approval form (assign next assignee, days, explain):

GET /holidays/apply?instanceId=...&mgr=10001&days=2&explain=Vacation

.

Repeat step 4 for the next assignee (department manager) and then for the general manager using mgr and top parameters.

After the final approval, the runtime tables are empty and history tables (e.g., act_hi_actinst) contain the completed records.

All steps demonstrate a complete end‑to‑end Activiti leave‑approval workflow integrated with Spring Boot.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

workflowBPMNSpring BootmysqlActivitiProcess Engine
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.