How to Build a Leave Approval Workflow with Activiti 7 and Spring Boot

This guide walks through setting up a Spring Boot 2.2.11 environment with Activiti 7.1, configuring Maven dependencies, explaining the Activiti database tables, defining a BPMN leave‑request process, implementing service and controller layers, and demonstrating API calls to deploy, start, query, and complete the workflow.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Build a Leave Approval Workflow with Activiti 7 and Spring Boot

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

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>

The Activiti tables are prefixed as follows:

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

ACT_RU_* : runtime tables storing active process instances, variables, and async tasks; cleared after process completion.

ACT_HI_* : history tables storing completed process data.

ACT_GE_* : general tables used across scenarios.

Core Classes

ProcessEngine : entry point to obtain services such as RepositoryService, RuntimeService, TaskService, and HistoryService.

TaskService : operations on task nodes (complete, delete, delegate, etc.).

RepositoryService : manages process definitions and deployments.

RuntimeService : starts and controls running process instances.

HistoryService : queries historical execution data.

BPMN 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="flow4" sourceRef="startevent1" targetRef="usertask3"/>
    <sequenceFlow id="flow5" sourceRef="usertask3" targetRef="usertask1"/>
    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"/>
    <sequenceFlow id="flow3" sourceRef="usertask2" targetRef="endevent1"/>
  </process>
  ...
</definitions>

Service Layer (HolidayService)

@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 process */
    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 key) {
        List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
            .processDefinitionKey(key).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 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) {
        return taskService.createTaskQuery().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);
    }
}

Controller Layer (HolidayController)

@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> m = new HashMap<>();
            m.put("id", task.getId());
            m.put("assignee", task.getAssignee());
            m.put("createTime", task.getCreateTime());
            m.put("bussinessKey", task.getBusinessKey());
            m.put("category", task.getCategory());
            m.put("dueDate", task.getDueDate());
            m.put("desc", task.getDescription());
            m.put("name", task.getName());
            m.put("owner", task.getOwner());
            m.put("instanceId", task.getProcessInstanceId());
            m.put("variables", task.getProcessVariables());
            return m;
        }).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();
    }
}

Configuration (application.yml excerpt)

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

Key Properties

spring.activiti.db-history-used : when true, all 25 Activiti tables (including history) are created; otherwise only 17 tables are generated and history diagrams cannot be displayed.

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

spring.activiti.check-process-definitions : if false, process definitions must be deployed manually.

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();
    }
}

Testing Steps

Deploy the process via /holidays/_deploy and verify act_re_procdef table.

Start a leave request with /holidays/start (provide userId and processDefinitionId).

Query the assigned tasks for a user with /holidays/tasks.

Submit approval data using /holidays/apply (parameters: mgr, top, explain, days, etc.).

Observe task progression through manager and director approvals and verify history tables ( act_hi_actinst, etc.).

The workflow completes after the final approval, and the article encourages readers to follow and share.

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.

workflowBPMNActivitiProcess Enginebackend-developmentspring-boottask-service
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.