SpringBoot集成Flowable工作流引擎:告别XML,实现可视化流程设计与执行
如果你正在开发一个OA、ERP或审批系统,需要处理“提交申请→部门审批→领导审核→财务处理→归档完成”这类多环节、多角色的业务流程,那么你很可能正在寻找一个工作流引擎。但当你真正开始调研时,会发现一个普遍存在的矛盾:工作流引擎本身很强大,但定义和修改流程的成本却高得惊人。
传统的做法是,你需要手动编写或修改复杂的BPMN 2.0 XML文件。就像下面这样,一个简单的请假流程,其XML定义就长达上百行,充斥着各种命名空间和坐标定义:
<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" ...> <process id="Leave" name="LeaveProcess" isExecutable="true"> <userTask id="leaveTask" name="请假" flowable:assignee="${leaveTask}"/> <userTask id="managerTask" name="经理审核"/> <exclusiveGateway id="managerJudgeTask"/> <!-- ... 更多节点和连线定义 --> </process> <!-- 冗长的图形布局定义 --> <bpmndi:BPMNDiagram id="BPMNDiagram_process"> <bpmndi:BPMNPlane bpmnElement="Leave" id="BPMNPlane_process"> <!-- 每个节点的坐标和大小 --> <bpmndi:BPMNShape bpmnElement="leaveTask" id="BPMNShape_leaveTask"> <omgdc:Bounds height="79.99999999999999" width="100.0" x="304.60807973558974" y="122.00000000000001"/> </bpmndi:BPMNShape> <!-- ... --> </bpmndi:BPMNPlane> </bpmndi:BPMNDiagram> </definitions>这不仅容易出错,而且每次业务逻辑调整(比如增加一个会签节点、修改审批人规则)都需要开发人员介入,严重拖慢了业务迭代速度。产品经理画好的流程图,到开发这里变成了一堆难以维护的XML代码,沟通成本和维护成本直线上升。
这篇文章要解决的核心问题,就是如何打破这个瓶颈。我们将实现一个更优雅的解决方案:在SpringBoot项目中集成工作流引擎(以Flowable为例),并引入一个可视化的流程设计器——bpmn-js。这样,业务人员或产品经理可以通过拖拽的方式设计流程,系统自动生成标准的BPMN XML,后端引擎无缝执行。开发者的角色从“XML编写员”转变为“引擎集成和业务扩展者”,真正实现业务与技术的解耦。
本文将分为上下两篇,上篇聚焦于环境搭建、引擎集成与核心概念贯通,下篇将深入前端流程设计器整合与高级特性实战。读完上篇,你将能独立搭建一个具备工作流能力的SpringBoot后端服务,并深刻理解其运行机制。
1. 为什么是SpringBoot + Flowable + bpmn-js?
在开始动手之前,我们需要做一个清晰的技术选型判断。工作流领域有几个常见的选项:Activiti、Flowable、Camunda,以及较新的Temporal。它们的核心区别是什么?我们为什么选择这个组合?
Activiti vs. Flowable:两者同宗同源,Flowable是原Activiti核心团队创建的分支。目前,Flowable在社区活跃度、对BPMN 2.0标准的支持完整性以及Spring Boot的集成友好度上,通常被认为是更优的选择。它提供了更模块化、更清晰的API,并且维护积极。对于大多数国内的OA、审批类项目,Flowable是一个成熟且稳妥的选择。
Temporal:这是一个新兴的、基于事件驱动的“工作流即代码”平台。它更适用于微服务架构下的复杂、长时、可恢复的业务流程(Saga模式),其编程模型与传统的基于BPMN图的引擎有本质不同。如果你的场景是传统的、以表单和人工审批为核心的流程图式工作流,那么BPMN引擎(如Flowable)更合适。
bpmn-js:这是由Camunda团队开源的一个基于Web的BPMN 2.0流程图查看与编辑库。它功能强大、外观专业,是业界事实上的标准前端BPMN设计器。将其集成到我们自己的系统中,可以让我们拥有一个媲美专业工作流软件的可视化流程设计能力。
因此,SpringBoot + Flowable + bpmn-js这个组合,恰好覆盖了从后端流程引擎执行、到前端可视化设计的完整链路,既能保证引擎的稳定与强大,又能极大提升流程定义环节的效率和体验。
2. 核心概念:工作流引擎到底在做什么?
在写第一行代码前,必须理解几个核心概念,否则很容易在后续开发中迷失方向。
流程定义(Process Definition): 这是流程的“蓝图”或“模板”。通常就是一个BPMN 2.0的XML文件。它定义了流程有哪些步骤、步骤之间的顺序、每个步骤由谁处理、有哪些分支条件等。一个流程定义可以启动无数次。
流程实例(Process Instance): 这是流程定义的“一次具体运行”。例如,“请假流程定义”是一个模板,而“张三在2023年10月26日发起的那次请假申请”就是一个流程实例。每个实例有自己独立的状态、变量和任务。
任务(Task): 流程实例运行到一个需要人工或系统处理的节点时,就会产生一个任务。最常见的是用户任务(User Task),需要指定一个办理人(Assignee)去完成它,比如“经理审批”。
网关(Gateway): 用于控制流程的分支与合并。排他网关(Exclusive Gateway)就像if-else,根据条件选择一条路径;并行网关(Parallel Gateway)则允许所有分支同时进行。
运行时服务与任务服务: 这是你与引擎交互的主要API。
RuntimeService: 负责流程实例的启动、挂起、删除以及流程变量的管理。TaskService: 负责对流程中产生的任务进行操作,如查询任务、完成任务、设置任务办理人等。
数据库持久化: Flowable引擎的所有元数据(流程定义)、运行时数据(流程实例、任务、变量)和历史数据都保存在数据库中。启动时,它会自动检查并创建所需的表(约60张)。强烈建议为工作流引擎使用独立的数据库或Schema,避免与业务表混杂。
理解了这些,再看上面的XML,你就会明白:<process>定义了一个模板,<userTask>定义了任务节点,<sequenceFlow>定义了连线,而conditionExpression定义了流转条件。引擎的工作就是解析这个模板,并根据你的API调用,驱动一个实例按照这个模板的规则一步步执行下去。
3. 环境准备与项目初始化
我们使用当前(撰写时)比较稳定的Spring Boot 2.7.x和Flowable 6.7.0进行演示。请确保你的开发环境已安装JDK 8+、Maven 3.6+和MySQL 5.7+。
第一步:使用Spring Initializr创建项目你可以通过 https://start.spring.io 或IDE(如IntelliJ IDEA)的创建向导,生成一个Spring Boot项目。关键依赖选择:
- Spring Web: 提供Web MVC能力。
- Spring Data JPA: 简化数据库操作(也可用MyBatis-Plus,根据习惯选择)。
- MySQL Driver: 数据库驱动。
第二步:手动添加Flowable依赖在生成的pom.xml中,添加Flowable的Spring Boot Starter依赖。注意,Spring Initializr可能没有直接提供Flowable选项,需要手动添加。
<!-- pom.xml --> <dependencies> <!-- Spring Boot 基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Flowable 核心依赖 --> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter-process</artifactId> <version>6.7.0</version> </dependency> <!-- 数据库 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency> <!-- 其他工具 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这里我们选择flowable-spring-boot-starter-process,它包含了流程引擎的核心模块。如果你还需要表单、内容引擎等,可以引入对应的starter。
第三步:配置数据库连接在application.yml或application.properties中配置数据库连接。Flowable启动时会自动检测数据源并创建表。
# application.yml spring: datasource: url: jdbc:mysql://localhost:3306/flowable_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 对于业务表,JPA可以自动更新。Flowable表由其自身管理。 show-sql: true # Flowable 相关配置 (可选,有默认值) flowable: # 是否在启动时自动部署classpath下的流程定义文件 async-executor-activate: false # 关闭历史数据记录级别,可提升性能,调试时可设为full history-level: audit重要提醒: 请提前在MySQL中创建名为flowable_demo的数据库(名字可自定)。第一次启动应用时,控制台会输出大量creating table...的日志,这表明Flowable正在自动建表。完成后,检查数据库,你会看到约60张以ACT_为前缀的表,这些表就是引擎的“大脑”。
4. 第一个流程定义:从XML到可执行引擎
现在,我们来创建一个最简单的请假流程定义。按照约定,我们将BPMN XML文件放在src/main/resources/processes/目录下,Flowable会自动扫描并部署它。
第一步:创建流程定义XML文件在src/main/resources/processes/目录下创建leave-process.bpmn20.xml文件。注意后缀必须是.bpmn20.xml或.bpmn,这是Flowable的识别约定。
<?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" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath"> <!-- 定义一个流程,id是引擎识别的关键,name是给人看的 --> <process id="leaveProcess" name="请假流程" isExecutable="true"> <!-- 开始事件 --> <startEvent id="startEvent" name="开始申请"/> <!-- 用户任务:员工提交请假申请 --> <userTask id="submitLeaveTask" name="提交请假申请" flowable:assignee="${applicantId}"> <documentation> 员工提交请假单,填写请假类型、时间、事由。 </documentation> </userTask> <!-- 用户任务:部门经理审批 --> <userTask id="deptApproveTask" name="部门经理审批" flowable:candidateGroups="dept_manager"> <documentation> 部门经理审批员工的请假申请。 </documentation> </userTask> <!-- 排他网关:根据审批结果决定流向 --> <exclusiveGateway id="decisionGateway" name="审批决策"/> <!-- 用户任务:HR备案(仅当审批通过时) --> <userTask id="hrRecordTask" name="HR备案" flowable:assignee="hr_staff"> <documentation> 经理审批通过后,HR进行备案记录。 </documentation> </userTask> <!-- 结束事件:审批通过流程结束 --> <endEvent id="endEventApproved" name="请假通过"/> <!-- 结束事件:审批驳回流程结束 --> <endEvent id="endEventRejected" name="请假驳回"/> <!-- 定义流程的顺序流(连线) --> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="submitLeaveTask"/> <sequenceFlow id="flow2" sourceRef="submitLeaveTask" targetRef="deptApproveTask"/> <!-- 从审批任务到决策网关 --> <sequenceFlow id="flowToDecision" sourceRef="deptApproveTask" targetRef="decisionGateway"/> <!-- 决策网关到HR备案(条件:审批通过) --> <sequenceFlow id="flowToHr" sourceRef="decisionGateway" targetRef="hrRecordTask"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${approvalResult == 'APPROVE'}]]> </conditionExpression> </sequenceFlow> <!-- 决策网关到驳回结束(条件:审批驳回) --> <sequenceFlow id="flowToReject" sourceRef="decisionGateway" targetRef="endEventRejected"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${approvalResult == 'REJECT'}]]> </conditionExpression> </sequenceFlow> <!-- HR备案到最终结束 --> <sequenceFlow id="flowToEnd" sourceRef="hrRecordTask" targetRef="endEventApproved"/> </process> </definitions>关键点解析:
flowable:assignee="${applicantId}": 这是一个流程变量的表达式。在启动流程时,我们需要传入一个名为applicantId的变量,其值将作为这个任务的办理人。这实现了动态任务分配。flowable:candidateGroups="dept_manager": 指定这个任务的候选组。这意味着所有属于“dept_manager”这个组的用户都可以看到并认领这个任务。这比写死一个用户ID更灵活,更符合实际业务(审批人是角色,不是具体某个人)。${approvalResult == 'APPROVE'}: 这是网关的条件表达式。当流程执行到decisionGateway时,引擎会检查流程变量approvalResult的值,决定下一步走哪条路径。
第二步:启动应用,验证部署启动你的SpringBoot应用。观察日志,如果看到类似下面的信息,说明流程定义已成功部署:
INFO o.f.s.b.a.FlowableJobConfiguration: - Starting async job executor INFO o.f.s.b.a.FlowableProcessConfiguration: -Deploying BPMN 2.0 process definition: leaveProcess (请假流程)你也可以通过Flowable提供的REST API或内置的管理控制台(如果引入了flowable-spring-boot-starter-ui依赖)来查看已部署的流程定义。但为了更贴近实际开发,我们直接通过代码和Service来验证。
5. 核心服务注入与流程实例操作
Flowable通过自动配置,已经将核心服务注入到了Spring容器中。我们可以在Service或Controller中直接使用它们。
第一步:创建一个Service类来封装工作流操作
package com.example.workflow.service; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.*; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; import java.util.List; import java.util.Map; @Service @Slf4j public class LeaveWorkflowService { // 核心服务:流程运行时服务 @Autowired private RuntimeService runtimeService; // 核心服务:任务服务 @Autowired private TaskService taskService; // 核心服务:流程仓库服务(用于查询流程定义等) @Autowired private RepositoryService repositoryService; /** * 启动一个请假流程实例 * @param applicantId 申请人ID * @param businessKey 业务主键(例如:请假单ID),用于关联业务数据 * @return 流程实例ID */ @Transactional public String startLeaveProcess(String applicantId, String businessKey) { // 1. 设置流程变量 Map<String, Object> variables = new HashMap<>(); variables.put("applicantId", applicantId); // 用于设置第一个任务的办理人 variables.put("businessKey", businessKey); // 关联业务数据 // 2. 启动流程实例 // 参数1:流程定义的Key,即XML中process节点的id属性 // 参数2:业务主键 // 参数3:流程变量 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leaveProcess", businessKey, variables); String processInstanceId = processInstance.getId(); log.info("请假流程启动成功。流程实例ID: {}, 业务Key: {}", processInstanceId, businessKey); // 3. 自动完成第一个任务(提交申请) // 在实际业务中,提交申请可能是一个表单操作,这里为了演示,自动完成。 Task firstTask = taskService.createTaskQuery() .processInstanceId(processInstanceId) .taskAssignee(applicantId) .singleResult(); if (firstTask != null) { taskService.complete(firstTask.getId()); log.info("自动完成提交申请任务,任务ID: {}", firstTask.getId()); } return processInstanceId; } /** * 查询某个用户的待办任务 * @param userId 用户ID * @return 待办任务列表 */ public List<Task> getTodoTasks(String userId) { // 查询指定办理人的任务 List<Task> assigneeTasks = taskService.createTaskQuery() .taskAssignee(userId) .orderByTaskCreateTime().desc() .list(); // 查询指定候选组的任务(用户可能属于多个组) List<Task> candidateGroupTasks = taskService.createTaskQuery() .taskCandidateGroup("dept_manager") // 示例:查询部门经理组的任务 .orderByTaskCreateTime().desc() .list(); // 合并列表(实际中可能需要去重和权限过滤) assigneeTasks.addAll(candidateGroupTasks); return assigneeTasks; } /** * 完成一个审批任务 * @param taskId 任务ID * @param approvalResult 审批结果 (APPROVE/REJECT) * @param comment 审批意见 */ @Transactional public void completeApproveTask(String taskId, String approvalResult, String comment) { // 1. 添加审批意见(可选) if (comment != null && !comment.trim().isEmpty()) { taskService.addComment(taskId, null, comment); } // 2. 设置流程变量,用于网关判断 Map<String, Object> variables = new HashMap<>(); variables.put("approvalResult", approvalResult); // 3. 完成任务,引擎会根据变量自动流转到下一个节点 taskService.complete(taskId, variables); log.info("任务 {} 已完成,审批结果: {}", taskId, approvalResult); } /** * 查询流程实例当前状态(可视化节点) * 这里返回流程定义的XML,前端bpmn-js可以渲染。 * 更复杂的场景可以返回引擎生成的流程图图片。 */ public String getProcessDefinitionModel(String processDefinitionKey) { // 获取最新版本的流程定义 org.flowable.engine.repository.ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery() .processDefinitionKey(processDefinitionKey) .latestVersion() .singleResult(); if (processDefinition == null) { throw new RuntimeException("流程定义不存在"); } // 获取BPMN XML资源 return new String(repositoryService.getProcessModel(processDefinition.getId())); } }代码逻辑深度解析:
startProcessInstanceByKey: 这是启动流程的入口。使用processDefinitionKey(即XML中<process id="leaveProcess">的id)来定位流程模板。businessKey至关重要,它是连接工作流实例和业务数据(如请假单表的主键)的桥梁。taskService.createTaskQuery(): 这是Flowable提供的Fluent API,用于构建复杂的任务查询。你可以链式调用.taskAssignee()、.processInstanceId()、.taskCandidateGroup()等方法来过滤任务。查询结果支持分页和排序。taskService.complete(taskId, variables): 完成任务并传递变量。这是驱动流程向前流转的关键操作。当任务完成时,引擎会计算下一个节点,如果遇到网关,就会使用这里传入的variables(如approvalResult)进行条件判断。- 事务管理: 我们为启动和完成任务的方法添加了
@Transactional注解。这是因为Flowable的很多操作(如更新任务状态、插入历史记录、更新流程变量)都需要在数据库事务中完成,确保数据一致性。
第二步:创建REST API控制器供前端调用
package com.example.workflow.controller; import com.example.workflow.service.LeaveWorkflowService; import lombok.RequiredArgsConstructor; import org.flowable.task.api.Task; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @RestController @RequestMapping("/api/workflow/leave") @RequiredArgsConstructor public class LeaveWorkflowController { private final LeaveWorkflowService workflowService; @PostMapping("/start") public ResponseEntity<Map<String, String>> startProcess(@RequestParam String applicantId, @RequestParam String businessKey) { String processInstanceId = workflowService.startLeaveProcess(applicantId, businessKey); Map<String, String> result = new HashMap<>(); result.put("processInstanceId", processInstanceId); result.put("message", "流程启动成功"); return ResponseEntity.ok(result); } @GetMapping("/tasks") public ResponseEntity<List<Map<String, Object>>> getTasks(@RequestParam String userId) { List<Task> tasks = workflowService.getTodoTasks(userId); List<Map<String, Object>> taskList = tasks.stream().map(task -> { Map<String, Object> map = new HashMap<>(); map.put("taskId", task.getId()); map.put("taskName", task.getName()); map.put("processInstanceId", task.getProcessInstanceId()); map.put("createTime", task.getCreateTime()); return map; }).collect(Collectors.toList()); return ResponseEntity.ok(taskList); } @PostMapping("/complete") public ResponseEntity<String> completeTask(@RequestParam String taskId, @RequestParam String approvalResult, @RequestParam(required = false) String comment) { workflowService.completeApproveTask(taskId, approvalResult, comment); return ResponseEntity.ok("任务处理完成"); } @GetMapping("/model") public ResponseEntity<String> getProcessModel() { String bpmnXml = workflowService.getProcessDefinitionModel("leaveProcess"); return ResponseEntity.ok().header("Content-Type", "application/xml").body(bpmnXml); } }6. 运行与验证:从API调用看引擎运转
现在,让我们通过一系列HTTP请求,来验证整个工作流引擎是否按预期工作。你可以使用Postman、cURL或任何你喜欢的API测试工具。
第一步:启动流程实例假设员工zhangsan提交了一个请假单,业务单号为LEAVE-20231027001。
POST http://localhost:8080/api/workflow/leave/start?applicantId=zhangsan&businessKey=LEAVE-20231027001预期响应:
{ "processInstanceId": "37501", "message": "流程启动成功" }后台发生了什么?
- 引擎根据
leaveProcess这个key找到流程定义。 - 创建一个新的流程实例,并将
businessKey和流程变量存入数据库。 - 实例从
startEvent开始,流转到submitLeaveTask。因为该任务的办理人通过变量${applicantId}被设置为zhangsan,所以引擎为zhangsan创建了一个任务。 - 我们的Service代码紧接着查询并自动完成了这个任务(模拟提交动作)。
- 流程继续流转到
deptApproveTask。这个任务的候选组是dept_manager,因此它现在是一个组任务,等待该组的成员来认领。
第二步:部门经理查询待办任务假设部门经理的用户ID是manager_li,并且他属于dept_manager组。
GET http://localhost:8080/api/workflow/leave/tasks?userId=manager_li预期响应:
[ { "taskId": "5002", "taskName": "部门经理审批", "processInstanceId": "37501", "createTime": "2023-10-27T10:00:00.000+00:00" } ]经理manager_li看到了这条待办。在实际系统中,他需要先“认领”这个任务,使其成为他的个人任务(taskService.claim(taskId, userId)),然后再处理。这里为了简化,我们假设查询到的任务可以直接处理。
第三步:部门经理审批(通过)经理处理任务,同意请假。
POST http://localhost:8080/api/workflow/leave/complete?taskId=5002&approvalResult=APPROVE&comment=同意,注意工作交接。后台发生了什么?
taskService.complete被调用,任务状态更新为已完成。- 引擎驱动流程实例移动到
decisionGateway(排他网关)。 - 网关计算条件表达式
${approvalResult == 'APPROVE'},由于我们传入的变量值是APPROVE,条件成立。 - 流程沿
flowToHr线流转到hrRecordTask(HR备案任务)。 - 该任务的办理人被固定设置为
hr_staff,因此为hr_staff创建了一个新任务。
第四步:HR备案(通过)HR人员hr_staff查询自己的任务并完成。
GET http://localhost:8080/api/workflow/leave/tasks?userId=hr_staffPOST http://localhost:8080/api/workflow/leave/complete?taskId=5003&approvalResult=APPROVE完成后,流程流转到endEventApproved,整个流程实例结束。
第五步:部门经理审批(驳回)如果经理在第三步选择了驳回:
POST http://localhost:8080/api/workflow/leave/complete?taskId=5002&approvalResult=REJECT&comment=驳回,项目期间请假不予批准。流程会从网关直接流向endEventRejected,流程实例同样结束,但不会产生HR备案任务。
通过这一系列操作,你可以清晰地看到流程是如何被API驱动,并严格按照BPMN XML定义来流转的。数据库中的ACT_RU_TASK(运行时任务表)、ACT_HI_TASKINST(历史任务表)等表会完整记录这些轨迹。
7. 常见问题与排查思路
在集成和工作流开发过程中,你几乎一定会遇到下面这些问题。提前了解,可以节省大量排查时间。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动应用时,Flowable表没有自动创建。 | 1. 数据库连接配置错误。 2. 数据库用户权限不足。 3. flowable.database-schema-update配置为false(默认是true)。 | 1. 检查application.yml中的数据库URL、用户名、密码。2. 检查MySQL是否允许远程连接或本地连接。 3. 查看启动日志,是否有数据库相关的错误。 | 1. 修正配置,确保网络和权限通畅。 2. 手动执行Flowable提供的建表SQL(在其jar包的 org/flowable/db/create目录下)。 |
| 流程定义XML文件修改后,重启应用不生效。 | 1. 文件没有放在resources/processes/目录下,或文件名后缀不对。2. 流程定义的 id属性重复或冲突。3. Flowable的缓存机制。 | 1. 确认文件路径和名称。 2. 检查不同XML文件中 <process id="...">是否唯一。3. 清理数据库的 ACT_RE_DEPLOYMENT和ACT_RE_PROCDEF表(危险操作,仅测试环境)。 | 1. 遵循命名规范。 2. 确保 process id唯一。3. 在测试时,可以配置 flowable.process-definition-cache-limit=0禁用缓存,或通过repositoryService的API进行强制清理和重新部署。 |
| 启动流程实例时,报错“no processes deployed with key 'xxx'”。 | 1. 流程定义Key拼写错误。 2. 流程定义XML有语法错误,导致部署失败。 3. 流程定义的 isExecutable属性为false。 | 1. 核对startProcessInstanceByKey中的key与XML中的process id是否完全一致。2. 查看启动日志,寻找流程部署时的XML解析错误。 3. 检查XML中 <process isExecutable="true">。 | 1. 仔细核对Key,区分大小写。 2. 使用在线的BPMN 2.0验证工具或IDEA插件检查XML语法。 3. 确保 isExecutable="true"。 |
| 任务查询不到,或者办理人/候选组不匹配。 | 1. 流程变量没有正确设置或传递。 2. 任务查询条件错误。 3. 用户-组关系没有在引擎中定义。 | 1. 调试查看runtimeService.getVariables(),确认流程变量值。2. 使用 taskService.createTaskQuery().list()查看所有任务,逐步添加过滤条件调试。3. Flowable有一套独立的身份管理表( ACT_ID_*),需要你同步或维护用户/组信息。 | 1. 确保启动和完成任务时,变量被正确放入Map并传递。 2. 熟练掌握TaskQuery的API。 3. 对于简单系统,可以忽略其身份模块,直接用业务系统的用户ID作为 assignee或candidateUser。对于复杂系统,需要集成或实现UserGroupManager接口。 |
| 流程走到网关后没有按预期路径执行。 | 1. 条件表达式语法错误或求值失败。 2. 流程变量类型不匹配(如字符串与布尔值比较)。 3. 条件表达式引用的变量在网关处不存在或为null。 | 1. 检查XML中conditionExpression的写法,确保是合法的JUEL表达式。2. 在完成任务前,打印出所有流程变量,确认其值和类型。 3. 使用 runtimeService.getVariable()确认变量是否存在。 | 1. 条件表达式尽量简单,如${result == 'yes'}。2. 对于复杂逻辑,考虑在Service层计算好结果,再以简单的变量形式传入。 3. 设置默认变量值,或在表达式中处理null情况,如 ${result != null && result == 'yes'}。 |
| 生产环境性能问题。 | 1. 历史数据过多,未做归档或清理。 2. 流程变量过大或过多。 3. 异步执行器配置不当。 | 1. 监控ACT_HI_*历史表的大小。2. 检查流程实例的变量表( ACT_RU_VARIABLE,ACT_HI_VARINST)。3. 查看异步作业( ACT_RU_JOB)是否有积压。 | 1. 配置合适的历史级别(flowable.history-level),非调试环境设为audit或activity。2. 定期归档或清理历史数据。 3. 优化流程设计,避免存储大对象作为流程变量。 4. 调整异步执行器线程池大小。 |
8. 最佳实践与工程建议
将工作流引擎集成到生产系统,远不止让流程跑通那么简单。以下是一些来自实战的经验总结,能帮你避开很多坑。
1. 数据库隔离与规划
- 专用数据库/模式: 务必为Flowable创建独立的数据库或Schema。60多张表与业务表混在一起,会给备份、迁移和性能优化带来巨大麻烦。
- 版本管理: 流程定义文件(BPMN XML)应该像代码一样进行版本管理(Git)。每次修改都应视为一次新版本的发布。Flowable支持同一Key下多个版本的流程定义,默认使用最高版本。
2. 业务键(Business Key)是生命线
- 强关联: 启动流程实例时,务必传入一个有意义的
businessKey,例如订单号、请假单ID。这是连接工作流世界和业务世界最重要的桥梁。 - 查询API: 充分利用
runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(businessKey)来通过业务键查询流程实例,而不是去记复杂的流程实例ID。
3. 流程变量的使用策略
- 最小化原则: 只将驱动流程流转所必需的数据作为流程变量(如审批结果
approvalResult、下一节点处理人nextAssignee)。不要把整个业务实体对象塞进去。 - 类型明确: 尽量使用简单类型(String, Integer, Boolean)。使用复杂对象(如DTO)会涉及序列化,可能带来版本兼容性问题。
- 作用域清晰: 理解流程实例变量(全局)和任务局部变量的区别。通常,驱动网关和分支的变量应设为流程实例变量。
4. 异常处理与事务
- 事务一致性: 工作流操作(如
taskService.complete)通常需要和你的业务更新操作(如更新请假单状态)放在同一个@Transactional方法中,确保同时成功或失败。 - 边界情况: 考虑流程中途出现异常(如审批人账号被禁用)的情况。Flowable提供了边界事件(Boundary Event)和补偿(Compensation)机制来处理,在复杂流程中需要设计进去。
5. 历史数据与审计
- 历史级别配置:
flowable.history-level有四个级别:none(不记录)、activity(记录节点)、audit(默认,记录节点和变量)、full(最详细,记录所有细节包括变量更新)。生产环境通常用audit,在满足审计需求与性能间取得平衡。 - 自定义历史: 如果默认的历史表(
ACT_HI_*)不能满足你的业务报表需求,可以在关键节点通过执行监听器(Execution Listener)或任务监听器(Task Listener)将数据写入你自己的业务历史表。
6. 与业务系统的集成模式
- 松耦合事件驱动: 这是推荐的模式。工作流引擎通过监听器(Listener)或消息事件(Message Event)触发业务系统的动作,而不是在流程定义中硬编码调用业务Service。例如,当流程到达“发送通知”节点时,抛出一个事件,由专门的消息服务去处理。
- 用户与权限对接: Flowable有自己的身份表(
ACT_ID_*),但大多数系统已有自己的用户体系。通常的做法是:不启用Flowable的身份模块,而是在查询任务时,通过taskCandidateUser()或taskCandidateGroupIn()等方法,直接传入从你自己系统查询到的用户ID和角色列表。
至此,我们已经完成了SpringBoot集成Flowable工作流引擎的后端核心部分。你现在拥有了一个可以部署、可以启动流程、可以处理任务、可以查询状态的完整后端服务。然而,我们仍然在手动编写和维护那个复杂的BPMN XML文件,这并没有解决我们开头提出的根本矛盾。
这就是为什么我们需要bpmn-js。在下篇中,我们将把焦点转移到前端,详细介绍如何集成这个强大的可视化流程设计器,实现从“画图”到“生成可执行流程定义”的无缝衔接,最终构建一个业务人员友好、开发效率极高的完整工作流平台。请关注下篇:《SpringBoot集成工作流引擎,bpmnjs流程编辑器(下)—— 可视化设计与全栈整合》。
