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.
Environment: Spring Boot 2.2.11.RELEASE, Activiti 7.1.0.M6, MySQL.
Dependencies
<code><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></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><?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></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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.