简介面向需要在 JEECG 中引入 Flowable 工作流能力的 Java 开发者这套源码包提供了一整套可落地的集成方案覆盖后端与前端两条主线。后端包含新建子工程、数据库连接配置、权限设置以及覆盖 Flowable 获取当前用户逻辑的 Java 实现前端则给出 Flowable 包引入、配置文件调整与 Vue 组件封装并配套完整代码示例和配置说明能帮助开发者减少环境兼容性排查。压缩包共 15 个文件以 7 个 Java 源码文件为主辅以 yml 配置、Vue 组件、xml/html/js 文件及说明文档整体大小仅 23KB结构紧凑便于按模块快速对照。该资源已有 711 人学习适合具备一定 Java 与 Spring 开发经验、希望快速搭建 JEECG 工作流能力的开发者通过资源中的源码和配置可清晰定位后端关键类、权限控制逻辑与前端接入位置从而快速集成并继续扩展业务。1. 为什么做JEECG配Flowable一次不算成功的原型试水先说结论JEECG这类低代码平台自带的工作流模块应付固定审批链路还行一旦遇到会签、驳回、条件网关、动态指定办理人就会变成一场灾难。我去年接一个内部OA改造需求就是报销单走部门经理—财务—总经理三级审批其中财务环节要支持多人会签还用老工作流拼了两周最后改了十二版流程图都没跑通。后来换了Flowable同样是这个场景两天就串完了全部节点。那条真实的踩坑路径就是这篇JEECG集成Flowable指南的由来。这套方案适合那种已经在用JEECG做业务系统、却被内置工作流绑住手脚的团队也适合想给老项目注入正经BPM能力的开发者。下面的内容全部来自实际接过的模拟项目X版本是JEECG Boot 3.x配Flowable 6.7.2代码可以直接抄。2. 集成前置版本选型与环境准备先把坑标出来2.1 JEECG与Flowable版本匹配依赖冲突全避开JEECG Boot 3.0 系列基于 Spring Boot 2.x而 Flowable 6.7.2 同样兼容 Spring Boot 2.x这组搭配算是社区里验证最多的组合。如果非要用 Flowable 7.x注意它内部对 Spring Boot 版本的校验更严JEECG 的自动配置类有概率被扫描重复遇到BeanDefinitionStoreException的概率会明显上升。先在 pom.xml 里排除掉 JEECG 自带的旧版 mybatis-plus 和 jackson避免和 Flowable 的 mybatis 映射器冲突。我一般这样加依赖dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version6.7.2/version exclusions exclusion groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId /exclusion /exclusions /dependency这段配置的逻辑是让 Flowable 的 starter 不碰 mybatis-plus而 JEECG 自己的数据访问层照常使用 mybatis-plus二者各管各的 mapper。把 flowable 的 mapper 扫描路径和 JEECG 的 MapperScan 路径错开也很关键否则启动时 MyBatis 的 mapper 注册会互相覆盖。提示如果启动报Invalid value type for name processEngineName查一下是不是同时引入了 flowable-spring-boot-starter 和 flowable-engine 两个依赖二选一即可。2.2 建表与初始化配置自动部署别让流程表拖垮启动Flowable 启动时会自动检查数据库表结构如果不存在会按database-schema-update配置建表。JEECG 默认使用 MySQL建议直接用true让它自动建表省去手工执行 SQL 脚本的步骤。第一次启动会创建约 60 张以 ACT_ 开头的表建表 SQL 在 flowable-engine 的 jar 包内部不需要额外下载。application.yml 里的核心配置这样写flowable: database-schema-update: true async-executor-activate: false history-level: audit db-history-used: true check-process-definitions: falseasync-executor-activate这里我强烈建议先设成 false。JEECG 的默认数据库连接池参数比较保守如果打开异步执行器Flowable 会额外占用一个线程池去轮询 ACT_RU_JOB 表在集成初期很容易把连接池打满。等业务稳定后再开不迟。history-level设 audit 而不是 full可以少记录一部分变量更新历史减少数据量。关闭check-process-definitions这步容易被忽略。不关的话Flowable 每次启动都会去扫描 classpath 下的 BPMN 文件并重新部署导致流程定义版本号一直涨流程图改了没改都会多出一个新版本。关掉以后部署完全靠手动调用 RepositoryService。2.3 数据源设计共用还是独立两种方案怎么选Flowable 默认会和 JEECG 使用同一个数据源也就是共用 JEECG 配置的 datasource。这个方案实现最简单事务也能直接跨业务表和流程表但有一个隐患JEECG 的 Online 表单模块会定期做表结构对比如果 Flowable 的 ACT_ 表被 Online 模块扫描到会在系统日志里刷出一堆表不存在对应实体的告警不影响功能但很吵。另一种方案是给 Flowable 配独立数据源。做法是在配置类里单独定义 flowableDataSource再把 Flowable 的配置指向它。好处是 Online 表单的扫描范围不用改Flowable 的表单独放在一个库数据量大了也好清理。代价是跨库事务需要自己处理业务表写完要调流程接口时一旦流程步骤失败业务数据的回滚只能靠补偿逻辑。我实际做模拟项目X时选了共用数据源。理由很简单项目里的审批流核心诉求是业务数据与流程状态强一致交给同一个 Spring 事务管理器管着最安心。如果后续 ACT_HI_ 表膨胀到影响性能再单独拆库也不迟。下表是两种方案的整体对比维度共用数据源独立数据源集成成本低零配置中需额外配置事务一致性强一致需补偿性能隔离差互相影响好运维复杂度低中适用场景中小项目流程数据量大的系统3. 核心集成实战把Flowable引擎嵌入JEECG的Service层3.1 核心类封装让Spring容器接管Flowable的ProcessEngine流程引擎的初始化在 Spring Boot 自动配置里已经完成真正要做的封装是把 RuntimeService、TaskService、HistoryService 这些核心服务统一收口到一个 Service 类里避免 Controller 层到处 new 对象。JEECG 的 Service 层习惯是接口加实现类我这里按同样风格写了一个 FlowableWrapperService。Service public class FlowableWrapperService { private final RuntimeService runtimeService; private final TaskService taskService; private final HistoryService historyService; private final RepositoryService repositoryService; public FlowableWrapperService(RuntimeService runtimeService, TaskService taskService, HistoryService historyService, RepositoryService repositoryService) { this.runtimeService runtimeService; this.taskService taskService; this.historyService historyService; this.repositoryService repositoryService; } public ProcessInstance startProcess(String processKey, String businessKey, MapString, Object vars) { return runtimeService.startProcessInstanceByKey(processKey, businessKey, vars); } public void completeTask(String taskId, MapString, Object vars) { taskService.complete(taskId, vars); } public ListHistoricActivityInstance getHistoricActivities(String processInstanceId) { return historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); } }构造器注入是这里的关键点Flowable 在 Spring 容器里注册的各个 Service 都是单例直接注入即可。这样封装后Controller 层只需要调flowableWrapperService.completeTask()不需要知道 TaskService 怎么来的。把 RepositoryService 一并注入是因为流程部署和后期的版本查询都会用到它与其等用到时再补注入不如一次放全。参数方面startProcessInstanceByKey的 businessKey 建议传业务表主键不要传随机数后面查流程和业务数据的关联全靠它。vars 里放的是流程变量的初始值包括发起人、部门、金额这些审批规则要用的信息。3.2 Web接口落地用JEECG的Controller风格包一层流程接口JEECG 的 Controller 类约定继承 JeecgController接口返回 Result 对象。照这个风格写流程接口前端调起来是无缝的。下面是启动流程和审批任务的接口实现。RestController RequestMapping(/flowable/process) public class ProcessController extends JeecgControllerProcessController, FlowableWrapperService { Resource private FlowableWrapperService flowableWrapperService; PostMapping(/start) public ResultString start(RequestBody StartProcessReq req) { MapString, Object vars new HashMap(); vars.put(initiator, req.getUserId()); vars.put(approveAmount, req.getAmount()); ProcessInstance instance flowableWrapperService.startProcess( req.getProcessKey(), req.getBusinessKey(), vars); return Result.OK(instance.getProcessInstanceId()); } PostMapping(/complete) public ResultString complete(RequestBody CompleteTaskReq req) { MapString, Object vars new HashMap(); vars.put(approved, req.getApproved()); vars.put(comment, req.getComment()); flowableWrapperService.completeTask(req.getTaskId(), vars); return Result.OK(审批完成); } }StartProcessReq里至少要有 processKey、businessKey、userId、amount 四个字段。processKey 是 BPMN 文件里 process 元素的 id不是文件名businessKey 是业务数据主键。CompleteTaskReq里的 taskId 是流程任务ID前端在待办列表里拿到的就是它。这套接口风格的好处是前端可以复用 JEECG 的请求封装token 鉴权、日志切面全部自动生效。注意complete接口要加事务注解实际操作中不少人漏了这一步导致 Flowable 的任务完成和业务表更新不在同一个事务里一旦业务更新失败流程已经走到下一步数据就对不上了。3.3 流程部署与启动的参数设计businessKey到底怎么用流程部署这块JEECG 的常见做法是把 BPMN 文件放到项目的 resources/processes 目录然后启动时手动遍历部署。用 classpath 扫描的方式部署能控制部署时机避免自动部署带来的版本重复问题。Component public class ProcessDeployer implements ApplicationRunner { Resource private RepositoryService repositoryService; Override public void run(ApplicationArguments args) { String[] bpmnFiles new String[]{ processes/expense-approve.bpmn20.xml, processes/leave-approve.bpmn20.xml }; for (String file : bpmnFiles) { ClassPathResource resource new ClassPathResource(file); if (!resource.exists()) { continue; } try (InputStream is resource.getInputStream()) { repositoryService.createDeployment() .addInputStream(resource.getFilename(), is) .name(jeecg-deploy- System.currentTimeMillis()) .deploy(); } catch (IOException e) { throw new RuntimeException(流程部署失败: file, e); } } } }部署名称里带上时间戳是为了方便在 ACT_RE_DEPLOYMENT 表里区分每次部署的记录。每次启动项目会重新部署一次这不是问题因为同 key 的流程定义会生成新版本号旧版本流程实例不受影响。businessKey 的设计要结合 JEECG 的 Online 表单主键。Online 表单默认主键字段是 id类型是 varchar(32)所以 businessKey 直接传 Online 表单记录的 id 最合适。一个流程实例唯一对应一条业务数据反过来一条业务数据只允许有一个进行中的流程实例这点要在启动流程前用查询做校验不然用户重复点击提交按钮会创建多个流程实例。4. 表单与流程绑定用JEECG Online表单驱动Flowable的三种做法4.1 Online表单值怎么传给流程变量Online 表单提交的数据在 JEECG 里是一个 Mapkey 是表单字段名value 是用户填的值。把这些值传给 Flowable 最直接的方式是在提交表单的 Service 里加一个转换步骤。public void submitFormAndStartProcess(String formId, MapString, Object formData) { String businessKey formData.get(id).toString(); MapString, Object flowVars new HashMap(); flowVars.put(amount, new BigDecimal(formData.get(amount).toString())); flowVars.put(dept, formData.get(dept).toString()); flowVars.put(applicant, formData.get(applicant).toString()); if (special.equals(formData.get(approveType))) { flowVars.put(needFinanceApprove, true); } else { flowVars.put(needFinanceApprove, false); } flowableWrapperService.startProcess(expense-approve, businessKey, flowVars); }这里有几个隐藏细节。金额字段amount不要直接存字符串Flowable 的表达式里比较大小需要数值类型转成 BigDecimal 后BPMN 里的条件表达式amount 5000才能正确求值。needFinanceApprove这种布尔变量是用来驱动排他网关的如果 Online 表单里有单选字段建议直接映射成布尔值比在 BPMN 里写字符串判断更干净。流程变量不是越多越好。每多一个变量ACT_RU_VARIABLE 表就多一行历史表也会跟着记录。只传参与网关判断和会签设置的字段其余表单数据在业务表里查即可。4.2 会签与或签用Flowable的multiInstance表达JEECG用户选择JEECG 用户体系里没有原生的会签组概念所以要在 BPMN 里用 multiInstance 节点配合候选人列表实现。把一个 UserTask 配成多实例assignee 表达式用assigneeList变量接收 Online 表单传入的办理人 ID 列表。userTask idfinanceApprove name财务会签 flowable:assignee${assignee} multiInstanceLoopCharacteristics isSequentialfalse flowable:collectionassigneeList flowable:elementVariableassignee completionCondition${nrOfCompletedInstances nrOfInstances}/completionCondition /multiInstanceLoopCharacteristics /userTask这段 BPMN 的逻辑是assigneeList是流程变量类型为 List 每个元素会被赋给assignee从而生成对应数量的子任务。isSequentialfalse表示并行会签所有办理人同时看到任务。末尾的 completionCondition 是或签的关键——把nrOfCompletedInstances nrOfInstances改成 1只要一个人审批完整个节点就通过这是会签和或签的唯一区别。在 Java 侧启动流程时把 Online 表单里选择的用户 ID 列表塞进流程变量ListString assigneeList Arrays.asList(formData.get(financeUsers).toString().split(,)); flowVars.put(assigneeList, assigneeList);注意assignee的值必须是 JEECG 用户表里的真实用户 ID而不是用户名。Flowable 的任务查询是按 assignee 精确匹配的ID 和用户名不一致会导致用户在待办里看不到任务。4.3 流程状态回写用监听器把审批结果写回业务表流程走完后业务表里的审批状态字段要同步更新。最省事的方案是用 Flowable 的流程结束监听器在 process 元素上挂 executionListener监听 end 事件。Component(processEndListener) public class ProcessEndListener implements ExecutionListener { Resource private JdbcTemplate jdbcTemplate; Override public void notify(DelegateExecution execution) { String businessKey execution.getProcessInstanceBusinessKey(); MapString, Object vars execution.getVariables(); boolean approved (boolean) vars.getOrDefault(approved, false); String status approved ? 2_APPROVED : 3_REJECTED; jdbcTemplate.update( UPDATE biz_expense SET approval_status ? WHERE id ?, status, businessKey); } }直接在监听器里写 JdbcTemplate 更新业务表事务边界由 Flowable 的 command 拦截器管理。如果监听器抛异常整个流程事务会回滚包括前面的审批记录这一点既是优势也是隐患——监听器里千万别写外部接口调用或耗时操作。还有一条路径是让前端在流程结束后主动回调业务接口更新状态用 Flowable 的 historyService 判断流程是否结束。两种方式对比监听器更可靠不会因为用户没刷新页面而漏更新所以我在模拟项目X里用的是监听器方案。5. 避坑/常见问题集成后前两周最容易踩的五个坑5.1 启动就报XML解析失败BPMN文件里藏着看不见的字符现象Spring 容器启动到 Flowable 自动部署环节直接抛XMLException: content of type...但用 IDE 打开 BPMN 文件看不出任何问题。原因BPMN 文件是从在线流程设计器导出的文件头带了 UTF-8 BOM。Java 的 XML 解析器遇到 BOM 会把它当成非法字符Flowable 的文本解析在这一步不做兼容。解决用 Notepad 或 VS Code 把文件重新保存为 UTF-8 无 BOM 格式。如果是批量文件用下面的 Java 代码批量处理public void removeBom(Path filePath) throws IOException { byte[] bytes Files.readAllBytes(filePath); if (bytes.length 3 (bytes[0] 0xFF) 0xEF (bytes[1] 0xFF) 0xBB (bytes[2] 0xFF) 0xBF) { Files.write(filePath, Arrays.copyOfRange(bytes, 3, bytes.length)); } }这段代码先检查文件头三个字节是不是 EF BB BF是的话把 BOM 去掉重写。从那以后我每次从在线设计器导出 BPMN第一件事就是检查文件编码不再等启动报错。5.2 流程实例一到审批就卡住事务边界不对现象流程能启动待办任务也能查到但调用 complete 接口后任务没有消失业务表状态也没变日志里没有任何异常。原因JEECG 的 Service 层默认开启了事务completeTask 调用和业务表更新在同一个事务里。但 Flowable 内部的命令执行器有自己的事务传播规则如果外层事务把 Flowable 的更新操作也包进去且事务隔离级别不是 READ_COMMITTEDFlowable 的乐观锁版本号检查就会失败——表现为任务状态更新被覆盖。解决给 Flowable 操作单独标记事务传播行为让 Flowable 的命令在独立事务中执行。在 FlowableWrapperService 的 completeTask 方法上加Transactional(propagation Propagation.REQUIRES_NEW)。这样 Flowable 的更新先提交业务表更新后提交两者互不干扰可靠性反而更高。5.3 数据库连接池被耗尽Async Executor占用连接不释放现象系统运行两天后MySQL 的 max_connections 被撑满报Too many connections重启后恢复正常过两天又复现。原因Flowable 的 Async Executor 默认启用时会启动一个 Job 轮询线程每 10 秒扫一次 ACT_RU_JOB 表。这个轮询线程每次扫描都会从连接池拿连接如果某个 Job 因为流程定义被删而无法执行它会无限重试连接释放异常。解决跟我前面的配置一样把async-executor-activate设为 false。如果业务确实需要异步任务单独配置一个flowableAsyncExecutor的线程池参数把asyncExecutorAsyncJobAcquireWaitTime从默认的 10 秒调大到 60 秒降低轮询频率。同时排查 ACT_RU_JOB 表把异常 Job 的LOCK_EXP_TIME字段置空让执行器重新获取。5.4 历史表无限膨胀ACT_HI_* 该不该清理现象ACT_HI_PROCINST 和 ACT_HI_ACTINST 两张表三个月涨了 5GB导致查询流程历史越来越慢。原因Flowable 默认不清历史数据所有结束的流程实例、活动节点、变量变更都完整保留。JEECG 的流程查询页面会把 HIST 表全表排序数据量一大就慢。解决分两步走。第一定时清理已结束且超过 90 天的流程历史用 HistoryService 的批量删除public void cleanHistory(int daysRetention) { Date date new Date(System.currentTimeMillis() - daysRetention * 86400000L); ListHistoricProcessInstance list historyService.createHistoricProcessInstanceQuery() .finished() .finishedBefore(date) .list(); for (HistoricProcessInstance hpi : list) { historyService.deleteHistoricProcessInstance(hpi.getId()); } }第二把history-level从 audit 降到 activity可以少记录变量更新详情适合对变量历史要求不高的项目。5.5 多人审批时任务重复候选人表达式写错了现象会签节点的任务列表里出现多个相同任务同一个用户能审批两次。原因多实例节点的 assignee 表达式用了固定值比如flowable:assignee${assigneeList}这个表达式取到的是整个 List 而不是单个元素。Flowable 把这个 List 当成一个执行器赋值导致任务生成逻辑异常。解决检查 BPMN 里 multiInstanceLoopCharacteristics 的 elementVariable必须和 assignee 表达式里的变量名一致。标准写法是flowable:elementVariableassigneeassignee 表达式写${assignee}collection 写${assigneeList}。命名上的混乱是这问题最常见的来源。6. 进阶经验把流程日志接入JEECG的日志模块与权限联动6.1 用Flowable的HistoryService延展JEECG的操作日志JEECG 自带的操作日志表只记录用户调用接口的行为不包含流程节点的流转细节。把 Flowable 的历史活动和操作日志关联起来能给审批追溯提供更有力的数据支撑。做法是在审批接口里把 Flowable 的节点信息转成 JEECG 的日志格式写入系统日志表。public void appendFlowLog(String processInstanceId, String taskId, String userId, String action) { HistoricActivityInstance activity historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .taskId(taskId) .finished() .orderByHistoricActivityInstanceEndTime() .desc() .list() .get(0); SysLog log new SysLog(); log.setLogContent(流程[ processInstanceId ]节点[ activity.getActivityName() ]操作[ action ]); log.setLogType(2); log.setCreateBy(userId); sysLogService.save(log); }从从那以后我每次接流程项目都会保留日志关联的完整链路视图因为单纯看接口调用日志根本无法定位是谁卡住了流程。6.2 权限联动流程节点按钮与JEECG按钮权限互通审批按钮的显隐可以直接挂在流程节点上。JEECG 的按钮权限模型是按角色分配按钮编码Flowable 节点按流程定义编码区分。用一个映射表把节点编码映射到 JEECG 按钮权限编码前端加载待办详情时根据当前节点编码请求一次按钮权限接口返回的按钮列表就是用户在当前节点能做的操作。CREATE TABLE biz_flow_btn_map ( id VARCHAR(32) PRIMARY KEY, process_key VARCHAR(64) NOT NULL, node_key VARCHAR(64) NOT NULL, btn_code VARCHAR(64) NOT NULL );这样配置的好处是审批节点的通过和驳回按钮权限可以精确到角色甚至用户不需要在前端硬编码节点和按钮的对应关系。6.3 流程定义版本对比的快速验证最后分享一个对调试特别有帮助的技巧。流程定义改版后经常要确认新版流程图和旧版的行为差异直接在 Flowable 的流程设计器里看文件不够直观。用这个查询快速拿到两个版本的 BPMN XML 做 diffpublic void compareVersions(String processKey, long v1, long v2) { BpmnModel model1 repositoryService.getBpmnModel(processKey : v1); BpmnModel model2 repositoryService.getBpmnModel(processKey : v2); // 输出两个版本的节点序列 ListProcess processes1 model1.getProcesses(); ListProcess processes2 model2.getProcesses(); System.out.println(版本 v1 节点数: countNodes(processes1)); System.out.println(版本 v2 节点数: countNodes(processes2)); }用这个对比序列来定位新版本多出来的节点、少了哪些网关比逐个看流程图高效得多。从那以后我每次部署新版本前都强制走一遍流程定义版本对比确认改动范围和预期一致才发布。这个习惯帮我躲过了至少三次把排除节点重新加回来的低级错误希望也能帮到你。本文还有配套的精品资源点击获取