Backend Development 18 min read

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

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

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)

<code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;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"&gt;
  &lt;process id="holiday" name="holiday" isExecutable="true"&gt;
    &lt;startEvent id="startevent1" name="Start"/&gt;
    &lt;endEvent id="endevent1" name="End"/&gt;
    &lt;userTask id="usertask1" name="部门经理审批" activiti:assignee="${mgr}"/&gt;
    &lt;userTask id="usertask2" name="总经理审批" activiti:assignee="${top}"/&gt;
    &lt;userTask id="usertask3" name="填写审批单" activiti:assignee="${assignee}"/&gt;
    &lt;sequenceFlow id="flow4" sourceRef="startevent1" targetRef="usertask3"/&gt;
    &lt;sequenceFlow id="flow5" sourceRef="usertask3" targetRef="usertask1"/&gt;
    &lt;sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"/&gt;
    &lt;sequenceFlow id="flow3" sourceRef="usertask2" targetRef="endevent1"/&gt;
  &lt;/process&gt;
  ...
&lt;/definitions&gt;</code>

Service Layer (HolidayService)

<code>@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);
    }
}
</code>

Controller Layer (HolidayController)

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

Configuration (application.yml excerpt)

<code>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
</code>

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)

<code>@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/**").permitAll();
    }
}
</code>

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.

workflowBackend DevelopmentBPMNSpring BootActivitiprocess-enginetask-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

login 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.