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.
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 1spring.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.
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.
