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
<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 1Key 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
