Step‑by‑Step Guide: Integrating Flowable BPMN with Spring Boot for Leave Approval Workflow
This article walks through the evolution of Java‑based workflow engines, compares Activiti, Flowable and Camunda, and provides a complete Spring Boot integration tutorial—including Maven setup, BPMN modeling, service task implementation, diagram generation, unit testing and common troubleshooting—using a concrete leave‑request example.
1. Workflow Engine Overview
Early Java workflow engines started with jBPM, created by Tom Baeyens at JBoss. After Baeyens left JBoss for Alfresco, he launched Activiti based on jBPM 4, while the original jBPM project abandoned the jBPM 4 code. This split later produced three major engines:
Activiti – now oriented toward cloud environments and integrates with Spring Cloud and Docker.
Flowable – focuses on a feature‑rich engine with many extension points, allowing developers to build custom functionality on top of the core engine.
Camunda – lightweight, offering a compact BPMN editor built on bpmn.io, ideal for embedded, highly customizable editors.
2. Core BPMN Concepts
Using a simple leave‑request process as an example, a BPMN diagram consists of four elements:
Events – start, end, intermediate, boundary, etc.
Sequence Flows – connections between elements, optionally guarded by condition expressions.
Tasks – user tasks, service tasks, script tasks, send tasks, receive tasks, etc.
Gateways – exclusive (XOR), inclusive (OR), parallel (AND), event‑based, etc.
Task Types Explained
User Task : waits for a human to complete the step (e.g., employee submits a leave request).
Service Task : executes Java code or calls a remote service automatically.
Script Task : runs an inline script when the flow reaches the node.
Send Task : sends a message to an external participant.
Gateway Types Explained
Exclusive Gateway (互斥网关): only one outgoing path is taken based on a condition.
Inclusive Gateway (相容网关): multiple outgoing paths may be taken if their conditions are satisfied.
Event Gateway : waits for a specific event before proceeding.
Parallel Gateway : splits or joins parallel branches.
3. Integrating Flowable with Spring Boot
3.1 Create a Spring Boot project and add dependencies
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.7.2</version>
</dependency>Because Flowable persists process state, add MySQL and MyBatis dependencies as well.
3.2 BPMN file placement
All BPMN files placed under src/main/resources/processes are automatically deployed at startup.
3.3 Example BPMN definition (ask_for_leave.bpmn20.xml)
<?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:flowable="http://flowable.org/bpmn"
targetNamespace="http://www.flowable.org/processdef">
<process id="ask_for_leave" name="ask_for_leave" isExecutable="true">
<startEvent id="start"/>
<userTask id="employeeTask" name="请假" flowable:assignee="#{leaveTask}">
<documentation>员工请假</documentation>
</userTask>
<sequenceFlow sourceRef="start" targetRef="employeeTask"/>
<userTask id="leaderTask" name="组长审批" flowable:assignee="#{zuzhangTask}"/>
<sequenceFlow sourceRef="employeeTask" targetRef="leaderTask"/>
<exclusiveGateway id="leaderGateway" name="组长审批网关"/>
<sequenceFlow sourceRef="leaderTask" targetRef="leaderGateway"/>
<sequenceFlow id="leaderApprove" sourceRef="leaderGateway" targetRef="managerTask" name="组长审批通过">
<conditionExpression xsi:type="tFormalExpression">${var:equals(checkResult,"通过")}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="leaderReject" sourceRef="leaderGateway" targetRef="failService" name="组长审批拒绝">
<conditionExpression xsi:type="tFormalExpression">${var:equals(checkResult,"拒绝")}</conditionExpression>
</sequenceFlow>
<serviceTask id="failService" name="发送失败提示" flowable:class="com.xx.process.service.LeaveFailService" flowable:exclusive="true"/>
<userTask id="managerTask" name="经理审批" flowable:assignee="#{manageTask}"/>
<exclusiveGateway id="managerGateway" name="经理审批网关"/>
<sequenceFlow sourceRef="managerTask" targetRef="managerGateway"/>
<...additional flows for manager approve/reject...>
<endEvent id="end"/>
</process>
</definitions>3.4 Service task implementation
@Slf4j
@Component
public class LeaveFailService implements JavaDelegate {
@Override
public void execute(DelegateExecution delegateExecution) {
log.info("审批不通过 {}", delegateExecution.getCurrentActivityId());
}
}The class implements JavaDelegate; Flowable invokes execute automatically when the service task is reached.
3.5 API to view the current diagram
@RestController
public class LeaveController {
@Autowired RuntimeService runtimeService;
@Autowired RepositoryService repositoryService;
@Autowired ProcessEngine processEngine;
@GetMapping("/pic")
public void showPic(HttpServletResponse resp, String processId) throws Exception {
ProcessInstance pi = runtimeService.createProcessInstanceQuery()
.processInstanceId(processId).singleResult();
if (pi == null) return;
List<Execution> executions = runtimeService.createExecutionQuery()
.processInstanceId(processId).list();
List<String> activityIds = new ArrayList<>();
for (Execution exe : executions) {
activityIds.addAll(runtimeService.getActiveActivityIds(exe.getId()));
}
BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
ProcessEngineConfiguration engConf = processEngine.getProcessEngineConfiguration();
ProcessDiagramGenerator generator = engConf.getProcessDiagramGenerator();
InputStream in = generator.generateDiagram(bpmnModel, "png", activityIds, new ArrayList<>(),
engConf.getActivityFontName(), engConf.getLabelFontName(), engConf.getAnnotationFontName(),
engConf.getClassLoader(), 1.0, false);
OutputStream out = resp.getOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
in.close();
out.close();
}
}The endpoint receives a process instance ID and returns a PNG showing the current active nodes.
3.6 Unit tests covering the whole flow
@SpringBootTest
@Slf4j
@ActiveProfiles("dev")
class ProcessApplicationTests {
@Autowired RuntimeService runtimeService;
@Autowired TaskService taskService;
static final String employeeId = "yuangongID_3";
static final String leaderId = "zuzhangId_3";
static final String managerId = "manageId_3";
@Test
void askForLeave() {
Map<String, Object> vars = new HashMap<>();
vars.put("leaveTask", employeeId);
ProcessInstance pi = runtimeService.startProcessInstanceByKey("ask_for_leave", vars);
runtimeService.setVariable(pi.getId(), "name", "javaboy");
runtimeService.setVariable(pi.getId(), "reason", "休息一下");
runtimeService.setVariable(pi.getId(), "days", 10);
log.info("创建请假流程 processId:{}", pi.getId());
}
@Test
void submitToLeader() {
List<Task> tasks = taskService.createTaskQuery().taskAssignee(employeeId).list();
for (Task task : tasks) {
Map<String, Object> map = new HashMap<>();
map.put("zuzhangTask", leaderId);
taskService.complete(task.getId(), map);
}
}
@Test
void leaderApprove() {
List<Task> tasks = taskService.createTaskQuery().taskAssignee(leaderId).list();
for (Task task : tasks) {
if ("组长审批".equals(task.getName())) {
Map<String, Object> map = new HashMap<>();
map.put("manageTask", managerId);
map.put("checkResult", "通过");
map.put("zuzhangTask", leaderId);
taskService.complete(task.getId(), map);
}
}
}
@Test
void managerApprove() {
List<Task> tasks = taskService.createTaskQuery().taskAssignee(managerId).list();
for (Task task : tasks) {
Map<String, Object> map = new HashMap<>();
map.put("checkResult", "通过");
taskService.complete(task.getId(), map);
}
}
@Test
void managerReject() {
List<Task> tasks = taskService.createTaskQuery().taskAssignee(managerId).list();
for (Task task : tasks) {
Map<String, Object> map = new HashMap<>();
map.put("checkResult", "拒绝");
taskService.complete(task.getId(), map);
}
}
}The tests simulate the full lifecycle: start the process, employee submits, leader approves, manager approves or rejects.
3.7 Common pitfalls
Diagram text appears as garbled characters : caused by missing default fonts on the host machine. Adding a Chinese font (e.g., "宋体") or configuring the engine font names resolves the issue.
Changing a BPMN file does not affect already‑running instances : once a process instance starts, its definition is persisted. Modifications only affect new instances; existing ones continue with the original definition.
4. Configuration to fix missing fonts
@Configuration
public class FlowableConfig implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
@Override
public void configure(SpringProcessEngineConfiguration cfg) {
cfg.setActivityFontName("宋体");
cfg.setLabelFontName("宋体");
cfg.setAnnotationFontName("宋体");
}
}Inject this configuration bean so that Flowable uses the specified font when generating PNG diagrams.
By following the steps above, developers can quickly set up a Flowable‑based leave‑approval workflow, understand each BPMN element, customize service tasks, visualize runtime state, and write automated tests to guarantee correctness.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
