ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JEECG集成Flowable:低代码平台工作流引擎落地指南

JEECG集成Flowable:低代码平台工作流引擎落地指南

简介:这是一份面向 JEECG 开发者的 Flowable 工作流引擎集成源码指南,适合已具备一定前后端基础、希望在 JEECG 敏捷开发平台中快速引入 BPMN2.0 工作流能力的项目团队。资源共 15 个文件、23KB,包含 7 个 Java 后端核心类、前端 Vue 组件与 JavaScript 配置、YAML 和 XML 配置文件及 HTML 示例页面,还附带 README 说明,能覆盖从后端子工程搭建、数据库连接配置、权限对接与当前用户逻辑覆盖,到前端依赖引入与组件调用的完整链路。已有 711 人学习下载。压缩包内代码示例与配置说明直接对应集成关键节点,可帮助开发者在实际项目中减少摸索成本、快速实现流程审批功能并定位常见问题,尤其适合作为团队内部技术预研或二次开发时的参考素材。

1. JEECG集成flowable:为什么低代码平台要补一个工作流引擎

用JEECG搭业务系统,表单、列表、权限、报表都能快速生成,但客户一旦提出"这个申请要走审批流程",事情就变了——这套需求没法靠拖拽表单解决,必须有一个真正的工作流引擎在背后跑节点、分任务、记历史。Flowable是Java生态里用得最多的BPM引擎之一,把它嵌进JEECG工程,正好补齐低代码平台的流程短板。下面按实际落地的顺序讲:先选型摆理由,再给依赖和配置,然后写服务封装、表单联动和待办查询,最后拆五个高频翻车点。适合正在做JEECG项目交付、被客户审批流程反复折腾的开发者。

2. 集成前的架构决策:选型依据、数据源归属与用户映射方案

2.1 对比Activiti和Camunda之后,为什么选择Flowable

一句话说清楚三个引擎的来龙去脉:Flowable和Camunda都是从Activiti某一代代码分出来的,Activiti继续在原有体系里维护。Camunda的产品化程度最高,自带了Cockpit流程监控台,界面和权限体系一应俱全,适合做独立部署的流程中台;Activiti保持精简路线,但社区活跃度近年来有所下降。Flowable夹在中间,API设计得更偏开发库,Engine拆分细,模块可以按需引入,和Spring Boot的集成封装也做得比较完整。

把这三个放到JEECG集成这个具体语境里对比,Camunda的问题在于引入了自己的界面层,和JEECG的菜单、权限体系重复,能保证不打架但维护成本高;Activiti的问题是历史负担重,一些老项目踩过的坑在新版本里还会遇到,排查资料相对分散。Flowable从依赖数量、API稳定性、资料密度三个维度看,都更适合作为嵌入模块写进JEECG。

选型没有必要追求"最强",要追求"最不容易出意外"。JEECG本身的定位是快速交付低代码项目,流程引擎追求的是集成路径短、踩坑有据可查。Flowable的官方文档里有专门的Spring Boot集成章节,社区里也常见把它和各类快速开发平台组合使用的案例,这两个条件在实际交付里非常重要。

2.2 嵌入还是独立部署:ProcessEngine生命周期与JEECG单体的匹配

集成前要做的第一个架构决策,是Flowable以什么形态跑起来。有人一提到工作流就想到单独部署一个服务,让JEECG通过HTTP调用流程接口。但一旦涉及流程和业务的联动,比如审批通过后修改业务状态、流程里要查业务表单数据,跨服务调用就要处理分布式事务和幂等补偿,复杂度会明显抬高。

我一般建议在JEECG单体架构下走嵌入模式。flowable-spring-boot-starter作为普通依赖放进JEECG进程,ProcessEngine和它派生的一组Service全部交给Spring容器管理。流程表和业务表落在同一个数据库、同一个事务里,审批完成后更新业务表就在同一个事务内提交,逻辑清楚,回滚也方便。

嵌入模式的代价是进程内存占用增加、后台任务和业务代码共享应用线程池。流量小时完全感觉不到,流量大时可以通过给流程引擎单独调参来隔离。如果团队已经有一套独立部署的流程引擎,或者有多套系统共用一套流程能力的需求,再考虑独立服务。否则嵌在JEECG里是性价比最高的方案,也最符合JEECG单体应用的部署习惯。

2.3 ACT_表落在哪里:共用数据源、表前缀与用户同步

Flowable启动后会自动建立一组ACT_开头的表,数量几十张,按职责分成定义表、运行表、历史表和身份表。这些表放哪里,决定了后续事务和备份的复杂度。JEECG支持多数据源,但你不一定想把流程引擎单独开一个库,因为流程表和业务表分库之后,一次审批操作要跨库提交,出问题就得靠补偿逻辑去追数据。

更常见的做法是让Flowable的表直接落在JEECG主库里。启动时把database-schema-update设为true,引擎自动建表,和业务表共用一个数据库实例。备份时把ACT_前缀的表和业务表一起处理即可,运维负担增加得很少。

用户同步问题同样关键。Flowable自带身份模块,也有一套用户表,但实际项目里基本不会使用它,因为审批人应当来自JEECG的机构和用户体系。用db-identity-used=false关掉Flowable身份模块,任务分配时直接写JEECG的用户ID,查询待办时用当前登录用户的ID过滤,用户体系就统一了。

3. 在JEECG工程里接入flowable:依赖、配置与第一个流程跑通

3.1 引入flowable-spring-boot-starter依赖并解决版本冲突

打开JEECG工程的pom.xml,加入flowable的Spring Boot Starter。多模块工程建议把工作流相关的代码和依赖收敛到一个独立模块里,比如workflow模块,不要让不需要流程的模块都引入引擎依赖。

<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>${flowable.version}</version> </dependency>

flowable.version定义在父pom的properties里,选版本时注意和Spring Boot大版本对齐。Spring Boot 2.x对应Flowable 6.x,Spring Boot 3.x对应Flowable 7.x,一般选当前大版本里面的最新稳定版。版本对齐这事不要拍脑袋,先看一眼工程里spring-boot的版本再定,否则Starter内部依赖会和现有依赖发生冲突。

加完依赖后最常出现jar包冲突问题。表现是服务启动到一半报BeanCreationException,或者运行期某个类找不到方法,日志里一堆NoSuchMethodError。排查方式是用mvn dependency:tree -Dincludes=org.flowable看依赖树,再针对冲突路径用exclude排除传递依赖。常见冲突对象有jackson-databind、mybatis、commons-lang3,优先保留JEECG里维护好的版本。

提示:依赖冲突是这类集成里最典型的玄学问题。看到NoSuchMethodError先别怀疑业务代码,大概率是同一个类在classpath里有多个版本,只有依赖树能告诉你真相。

3.2 数据源与引擎参数配置:database-schema-update、history-level怎么设

依赖就位后,在application.yml里加上flowable配置块。这里每个参数都直接影响集成后能否稳定运行,照着示例放进去之前,先理解它在当前场景里为什么这么设。

flowable: database-schema-update: true history-level: audit db-identity-used: false async-executor-activate: false check-process-definitions: false

database-schema-update为true,表示启动时自动建表或升级表结构。开发环境这么设最省事,省去手动执行建表脚本的工作;生产环境建议改成false或用脚本管理升级,避免引擎改动表结构引发事故。

history-level设audit,记录流程实例和活动实例,不记录变量每次变更的明细。之前有项目图省事设成full,上线三个月历史表涨到几个G,查询和备份都明显变慢。流程没有强制需求时,audit是成本和价值的平衡点。

db-identity-used为false,关掉Flowable的身份表,用户数据完全依赖JEECG。这一步不做,任务分配时会出现两套用户ID,待办查询对不上号,排查成本很高。

async-executor-activate为false,关闭Flowable的异步执行器。嵌入模式下后台作业扫描会占用连接池和线程,小项目机器配置有限,开着它容易把数据库连接耗尽。没有定时器任务或异步消息需求时,这个开关保持关闭。

还有一个容易忽略的资源定位配置。如果不希望BPMN文件强制放在classpath的processes目录下,可以自定义扫描路径,比如指定bpmn文件夹。它配合下一节的手动部署,是管理多模块流程文件最可靠的方式。

3.3 封装WorkflowService:用JEECG风格调用发起、审批与待办查询

依赖和配置完成后,Spring容器里已经注册好RuntimeService、TaskService、HistoryService等组件。但Controller直接操作这些组件会耦合引擎API,时间久了难以维护。常见做法是先封装一个WorkflowService,把发起、审批、待办三个操作收敛成业务方法。

@Service public class WorkflowService { @Resource private RuntimeService runtimeService; @Resource private TaskService taskService; @Resource private HistoryService historyService; public String startProcess(String processKey, String businessKey, Map<String, Object> variables) { ProcessInstance instance = runtimeService .startProcessInstanceByKey(processKey, businessKey, variables); return instance.getId(); } public List<Task> queryTodo(String assignee, int first, int max) { return taskService.createTaskQuery() .taskAssignee(assignee) .orderByTaskCreateTime().desc() .listPage(first, max); } public void completeTask(String taskId, Map<String, Object> variables) { taskService.complete(taskId, variables); } }

startProcess方法里的processKey要填BPMN文件中process元素的id,对应流程定义表里的KEY字段,不是文件名。businessKey参数存业务表主键,后续从流程实例反查业务数据就靠它。variables里放流程流转需要的字段,比如金额、审批人、部门ID。

completeTask的variables里建议加入approved变量,值表示通过或驳回,交给排他网关做分支判断。这个变量进入流程实例作用域后,设计流程图时直接在网关表达式里引用即可,不需要额外写判断代码。

queryTodo用taskAssignee精确匹配用户,适合单人任务。如果流程节点用的是候选组方式,查询要换成taskCandidateGroup或taskCandidateUser。这个细节直接决定待办数据是否正确,很多集成翻车都翻在这里。

4. 把流程和业务表单粘在一起:变量绑定、待办列表与权限过滤

4.1 用BusinessKey关联业务主键,避免流程变量塞满大字段

流程引擎和业务表单的连接点,就是发起流程时传入的那一批数据。不少开发者习惯把所有业务字段一股脑塞进流程变量,再扔一个几十K的JSON字符串,这是Flowable集成里最常见的坏习惯。流程变量会被持久化到运行表和历史表,大字段进入后,查询、备份、历史清理都会跟着变慢。

通用做法是在流程变量里只保留流程决策需要的最小字段集,业务表单的完整数据留在业务表,用businessKey建立关联。发起流程时传业务表主键,之后通过processInstanceBusinessKey可以反查流程实例,需要详情数据时回到业务表取。

Map<String, Object> vars = new HashMap<>(); vars.put("approvalType", "contract"); vars.put("amount", 86000); vars.put("assignee", userId); String instanceId = workflowService.startProcess("contract_approval", businessId, vars);

这段代码里的businessId对应业务表主键,amount用于流程里的金额网关判断,assignee用于指定第一个审批节点的处理人。合同附件、备注说明这类大字段不进variables,取用时按businessId回查业务表。这样变量表始终干净,历史表的压力也小。

4.2 待办/已办查询的常见写法和分页优化

待办列表是流程功能里请求量最高的接口。单人任务用taskAssignee,候选组任务用taskCandidateGroup,两种任务出现在同一页面时,需要用or条件组合查询。这段代码是实际项目里常用的一种写法,可以直接拿来参考。

public List<Task> queryTodoWithOr(String userId, String groupId, int first, int max) { return taskService.createTaskQuery() .or() .taskAssignee(userId) .taskCandidateGroup(groupId) .endOr() .orderByTaskCreateTime().desc() .listPage(first, max); }

or()和endOr()必须成对出现,中间的条件按or关系叠加。条件本身不能为null,否则会在查询阶段直接抛异常。分页用listPage传入first和max,配合前端分页组件使用即可。

已办查询走HistoryService,按办理人加上finished条件过滤已结束的任务:

public List<HistoricTaskInstance> queryDone(String userId, int first, int max) { return historyService.createHistoricTaskInstanceQuery() .taskAssignee(userId) .finished() .orderByHistoricTaskInstanceEndTime().desc() .listPage(first, max); }

这个查询在数据量上来之后会变慢,需要给Flowable历史表的常用字段建索引。重点覆盖ACT_HI_TASKINST的PROC_INST_ID_、TASK_DEF_KEY_、END_TIME_三列,再配合查询条件里的assignee列。索引建在引擎自动创建的表上不冲突,直接用数据库客户端执行即可。

4.3 结合JEECG数据规则过滤流程实例

JEECG的数据权限体系能把部门、角色范围内的数据过滤掉,但这个能力作用在业务表上,流程实例表不在它的直接管辖范围内。如果不加处理,任何登录用户都可能通过流程接口看到其他部门发起的流程。

常规做法是发起流程时把发起人所在部门写入流程变量,查询流程实例时,先从JEECG数据规则里拿到当前用户可见的部门集合,再拼到流程实例查询的条件里。Flowable对流程实例的变量过滤有原生支持:

String deptId = currentUser.getDeptId(); List<ProcessInstance> instances = runtimeService.createProcessInstanceQuery() .variableValueEquals("deptId", deptId) .list();

如果部门集合较多,可以用variableValueIn("deptId", deptIds)一次传入列表。变量过滤依赖ACT_RU_VARIABLE表,这张表和ACT_RU_EXECUTION表的关联字段需要建索引,否则流程数量上来后查询会明显变慢。这是接入阶段容易被忽视的调优点。

5. 集成flowable最容易翻车的五个地方:现象、原因与解决方案

5.1 启动后数据库连接池被后台任务占满,业务表查询变慢

现象:服务正常跑一段时间后,业务接口响应从几十毫秒涨到几秒,日志里大量出现无法获取连接,数据库连接数监控打满。

原因:Flowable的异步执行器默认开启,会在作业表里持续扫描定时任务和事件消息。嵌入模式下和JEECG业务共享数据库连接池,两边同时抢连接,池子不够用就出现上面的现象。

解决:集成初期先关闭异步执行器,把配置里的async-executor-activate设为false。同时让Flowable和JEECG复用同一个数据源,不要新建第二个DataSource指向同一个库。如果业务确实需要定时器任务,等基础跑通之后单独评估开启,并调大连接池的最大连接数。

5.2 部署了BPMN文件,流程定义列表却为空

现象:把BPMN文件放进了resources目录,服务也启动成功了,但流程定义列表里一条记录都查不到。

原因:Starter默认只在classpath的processes目录下扫描流程文件。多模块工程里,文件如果放在业务模块的资源目录,而Flowable在公共模块启动,扫描路径就命不中。另外,不配置resource-locations时,自定义目录里的文件不会被自动部署。

解决:不依赖自动部署,改为手动部署,部署路径自己控制。在应用启动监听器或者初始化方法里执行:

repositoryService.createDeployment() .addClasspathResource("bpmn/contract_approval.bpmn20.xml") .name("合同审批流程") .deploy();

如果文件比较多,也可以用自定义配置统一扫描:

flowable: resource-locations: classpath*:/bpmn/**/*.bpmn20.xml

注意两种方式不要混用。手动部署配合版本管理,上线时流程版本更可控,是我个人更倾向的方式。

5.3 审批通过以后,业务表状态纹丝不动

现象:流程顺利走到结束节点,页面上显示流程已完成,但打开业务表单,审批状态还停留在待审核。

原因:流程引擎只负责流转,审批完成不会自动改业务表。集成方少了一环更新业务状态的动作,这是最常见的工作流认知盲区。

解决:把状态更新逻辑挂到节点事件上。推荐在流程图的最后一个人工节点结束位置挂一个ExecutionListener,或者在流程末尾增加一个ServiceTask,在其中调用JEECG的业务Service,把状态改成已同意或已驳回。这段逻辑要放进Spring事务里,监听器抛出异常时整个流程回滚,保证流程数据和业务数据始终一致。

5.4 历史数据膨胀到影响查询,清理又怕丢轨迹

现象:流程上线几个月后,ACT_HI_ACTINST、ACT_HI_VARINST表越来越大,流程追踪页面打开要等好几秒,数据库备份也明显变慢。

原因:history-level设成full,导致每次流程变量变更都留下多版本记录。加上没有建立历史数据清理机制,只增不减,数据量积累到一定程度就拖垮查询。

解决:把新流程的history-level调整到audit,减少单实例的数据量。对已经堆积的历史数据,写一个定时任务,定期删除已结束流程的历史记录,调用HistoryService的removeHistoricProcessInstance。删除前必须确认流程实例已经结束,否则引擎会抛异常。保留时长按业务需求定,一般保留180天比较常见。

5.5 服务重启后,待办任务的处理人全部对不上

现象:发布新版本后,用户打开待办中心,任务数量看起来正常,但点进去发现处理人和当前登录用户对不上,有的任务别人能看自己看不了。

原因:任务分配时写死了另一套用户标识。例如流程设计器里配置了具体的用户账号,或者流程变量里放的是姓名而不是JEECG用户ID,导致待办查询按登录ID过滤时匹配不上。这个问题在重启后才暴露,是因为重启前任务已经分配完毕,用户没有往这个方向核对过。

解决:流程设计时不要写死用户,用户节点全部使用${assignee}这类表达式,由发起流程的代码动态传入JEECG用户ID,保持db-identity-used=false。发布新版本前做一次人工核对,用管理员账号跑一遍流程,确认任务能正确分配给目标用户,再对外发布。

6. 进阶技巧:用历史活动实例把流程足迹画回页面

流程跑通之后,业务方第一个追问就是"现在走到哪一步了、谁审的、审了多久"。这些数据在Flowable历史服务里都有现成接口,不需要自己建表记录。一组查询拿回活动节点的时间线和处理人,前端按顺序展示,就是一个完整的流程追踪模块。

public List<HistoricActivityInstance> trace(String processInstanceId) { return historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime().asc() .list(); }

返回的记录对应流程图里的每个节点,包含节点类型、名称、开始结束时间、办理人。前端拿到后按用户任务节点和网关节点分组展示,用户任务显示处理人和耗时,网关节点折叠不显示。这样一个审批进度面板就出来了,视觉效果和商用流程系统基本一致。

还有一个高频需求是当前节点高亮。BPMN模型在RepositoryService里完整存储,读取出来之后渲染到前端画布,根据trace结果找到当前活动节点打上标记。这一步主要工作量在前端,数据来源就是上面这个trace方法。如果前端团队不太熟悉BPMN渲染,建议先用表格形式展示进度,等稳定了再考虑画布高亮。

这套集成做下来,我最深的体会是:不要一上来就铺全量功能,先把一条最简流程从发起到审批走完,验证待办、已办、业务状态都对得上,再往上加并行网关、会签、子流程。每加一种流程结构,都回归一次数据一致性。流程引擎一旦上了生产,追数据的成本比设计阶段多花的时间高得多。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进