简介:面向Java后端与前端开发者的JEECG集成Flowable工作流引擎源码包,解决在JEECG平台快速引入流程审批与BPMN2.0标准流程管理等能力的问题。资源共15个文件,以7个Java文件为核心,配合yml数据库配置、xml权限与流程定义文件、vue组件、js/html前端文件及md说明文档,压缩包仅23KB,目录按backend与frontend划分,整体轻量清晰,已有711人浏览学习。内容涉及后端子工程创建、数据源连接配置、权限设置及Flowable当前用户获取逻辑的覆盖,重点解决Flowable与JEECG用户体系的对接;同时包含前端Flowable包接入、配置修改和Vue组件引入的完整示例,结合README说明可快速理解项目结构,适合具备一定Java与Vue基础、希望快速在JEECG项目落地工作流功能的开发者参考移植。
1. 为什么要在JEECG里集成Flowable:低代码平台的审批流短板
JEECG能快速搭建业务模块,但一旦涉及请假、合同会签、采购流转这类多节点审批,平台自带能力就不够用了。Flowable作为BPMN 2.0流程引擎,能把流程图变成可执行的审批流,所以不少团队拿JEECG源码做二次开发时,第一步就是集成Flowable。下面的集成指南面向正在做这个集成的后端开发者,讲清楚版本选型、依赖配置、用户体系打通、流程部署与待办处理,并给出自定义UserManager和启动流程的源码片段。结尾整理了实际踩过的五个坑,覆盖事务回滚、多数据源连错库、审批人认不出用户等翻车现场。读者可以照着思路改到自己工程里,新手按步骤走,熟手直接跳去看避坑章节。
2. 依赖与配置:让Flowable引擎在JEECG里平稳启动
2.1 版本对应关系:先选Flowable还是先选Spring Boot
Flowable的spring-boot-starter跟Spring Boot版本有严格的对应关系。常见做法是:基于Spring Boot 2.5.x的JEECG工程,用Flowable 6.7.x比较稳;Spring Boot 2.7.x可以上Flowable 6.8.x。版本不匹配的典型症状是启动时抛ClassNotFoundException或NoSuchMethodError,报错信息往往指向org.springframework.boot包或Flowable引擎内部的类,看起来像是代码问题,实际上是依赖版本打架。
我一般会把Flowable版本收进dependencyManagement统一管理,避免子模块各自引入不同版本。下面是pom里常用的做法:
<properties> <flowable.version>6.7.2</flowable.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>${flowable.version}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> </dependency> </dependencies>这段配置把flowable-spring-boot-starter收进依赖管理,业务模块只写groupId和artifactId,版本由根pom统一控制。好处是后续升级Flowable时只改一个属性,不会出现多个模块各自依赖不同版本导致引擎类冲突的问题。把starter放进dependencyManagement还有一层意义:Flowable的starter会往下传递mybatis依赖,而JEECG用的是mybatis-plus,两个ORM框架同时存在时,稍不注意就出现Mapper扫描互相覆盖。统一版本管理至少能保证传递依赖是可控的。
实际项目里我还遇到过一种更隐蔽的情况:Flowable的starter传递依赖了一个旧版本的spring-boot-autoconfigure,把JEECG工程里其他模块的自动配置类给覆盖了,表现是原本正常的mybatis-plus功能突然失效,控制台报一堆Mapper bean找不到。遇到这种问题别急着改业务代码,先执行mvn dependency:tree看看flowable相关依赖里带了哪些Spring Boot组件,把冲突的依赖排除掉。
2.2 application.yml里的Flowable开关:不是只加一行配置就行
Flowable的spring-boot-starter启动时会自动建表、自动注册引擎服务,看起来开箱即用,但直接扔进JEECG里往往会翻车。最关键的三个参数看表:
| 配置项 | 推荐值 | 作用 |
|---|---|---|
| flowable.database-schema-update | true | 启动时自动建表或升级表结构 |
| flowable.async-executor-activate | false | 是否激活异步执行器,集成初期先关 |
| flowable.jdbc-max-active-connections | 跟数据源池大小一致 | 引擎独立连接池上限 |
对应到application.yml里的写法:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/jeecg_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: "******" flowable: database-schema-update: true history-level: audit async-executor-activate: false jdbc-max-active-connections: 20 jdbc-max-idle-connections: 10这里把async-executor-activate关掉是刻意的。集成初期如果打开异步执行器,JobExecutor会启动独立的线程池,一旦事务边界没处理好,会出现“流程实例创建了但任务没生成”的诡异问题,而且这种问题日志里不会直接报错,查起来非常闹心。先关掉,让流程全部同步驱动,跑通后再考虑开异步。
history-level用audit,会记录流程实例、任务和活动数据,足够应付待办查询和审批历史,又没有full级别那么大的数据量。如果生产环境实在担心历史表膨胀,可以降到activity,但审批意见这类数据就查不到了,建议不要轻易降。
这里有两个容易忽略的点。一是database-schema-update=true意味着每次启动都会对比表结构并自动执行ALTER语句,多实例部署时会出现多个节点同时改表结构的情况,报错也不奇怪。多实例生产环境建议指定一个实例作为维护者,或者改用外部脚本升级。二是如果JEECG里已经配置了动态数据源连接池,Flowable自身的jdbc连接池参数不要直接抄文档默认值,它默认会创建自己的连接池,跟业务数据源争抢数据库连接数,流量稍大就出现连接池耗尽。
2.3 启动冒烟验证:建表日志和ACT_表检查
配置完成之后,正常启动JEECG工程,观察日志里是否出现Flowable的版本号打印,以及类似“Successfully applied database schema changes”的提示。启动成功后连数据库看以ACT_开头的表是否存在,重点检查三组:ACT_RE_*是流程定义,ACT_RU_*是运行时数据,ACT_HI_*是历史数据。如果这三个前缀的表都出现了,说明引擎初始化成功。
如果启动时直接报“Table ACT_GE_PROPERTY doesn't exist”或者建表语句执行失败,先确认数据源账号有没有DDL权限。没有权限时,可以手动执行Flowable提供的对应数据库类型SQL脚本,或者把schema-update改成false配合外部初始化脚本。有一点可以放心:JEECG自己的菜单初始化走的是它内部的SQL执行器,Flowable自动建表是独立的一条线,两边互不干扰,不用担心菜单被冲掉。
这一章做完,引擎已经能转起来。但你会立刻遇到一个现实问题:Flowable自己的用户表ACT_ID_*是空的,你在JEECG里创建的业务账号没法直接当审批人。这就是下一章要解决的事。
3. 用户体系打通:自定义UserManager,把JEECG账号变成Flowable审批人
3.1 默认UserManager为什么在JEECG里不可用
Flowable引擎自带一套用户和组的管理接口,数据落在ACT_ID_*表里。这套表由Flowable自己维护,跟JEECG的sys_user、sys_role没有任何关联。如果不做干预,在流程定义里写assignee="admin",引擎会去ACT_ID_USER里查admin,查不到的话任务就落到无主状态,待办列表里谁都不显示。更隐蔽的情况是:某个流程从旧系统迁移过来,旧的ACT_ID_USER里恰好有个同名用户,那么审批人会被错误地认定为那个人,而真实当前用户其实是JEECG里的同登录名账号。
常见的做法是不碰Flowable的用户表,而是实现自己的UserManager和GroupManager,把查询转发到sys_user和sys_role。好处是审批人数据始终只有一份,不用做两边同步,账号密码变更也天然一致;坏处是Flowable用户管理相关能力(比如流程设计器里的用户搜索)有一批方法要重写,好在大部分方法返回空集合就能正常跑,真正核心的只有少数几个。
3.2 实现UserManager:查询、校验、组关系三个核心方法
我实现的自定义UserManager继承Flowable的UserManagerImpl,重写关键查询方法。第一步先覆盖findUserById和checkPassword:
@Component public class JeecgUserManager extends UserManagerImpl { private final SysUserService sysUserService; public JeecgUserManager(SysUserService sysUserService) { this.sysUserService = sysUserService; } @Override public User findUserById(String userId) { // 统一用username作为Flowable用户标识 SysUser sysUser = sysUserService.getOne( new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, userId)); if (sysUser == null) { return null; } UserImpl user = new UserImpl(); user.setId(sysUser.getUsername()); user.setFirstName(sysUser.getRealname()); user.setEmail(sysUser.getEmail()); return user; } @Override public boolean checkPassword(String userId, String password) { // 登录认证在JEECG的Shiro/Sa-Token层已完成,这里放行 return true; } @Override public List<Group> findGroupsByUser(String userId) { // 把用户的角色列表映射为Flowable组 List<SysRole> roles = sysUserService.getRolesByUsername(userId); return roles.stream().map(role -> { GroupImpl group = new GroupImpl(); group.setId(role.getRoleCode()); group.setName(role.getRoleName()); group.setType("assignment"); return (Group) group; }).collect(Collectors.toList()); } }这里有个关键细节:findUserById返回的User对象,id用的是username而不是数据库主键。原因很简单——流程定义里写assignee时最直观的就是写登录账号;如果写主键,BPMN文件里全是数字或UUID,可读性太差,而且用户换绑账号后主键可能变化,老流程的assignee就对不上了。findGroupsByUser把sys_role映射成Flowable的组,组id直接用role_code,这样在BPMN里写candidateGroups="manager"就能对应JEECG里角色编码为manager的角色。
有人会问,findUserById直接按username查询,如果系统里登录名允许重复怎么办?这是一个真实存在的隐患。我的处理是:集成时要求登录名在sys_user表里唯一,否则流程引擎没法用登录名精确定位到人。如果你的历史数据里已经有重复登录名,那就只能额外维护一张流程用户映射表,把审批时使用的userKey映射到sys_user主键,虽然多一张表,但至少不会出现审批人张冠李戴。
3.3 GroupManager与候选组映射:让角色成为审批候选池
光有UserManager还不够,候选组查询走的是GroupManager。同样继承GroupManagerImpl并重写findGroupById和findGroupsByNativeQuery:
@Component public class JeecgGroupManager extends GroupManagerImpl { private final SysRoleService sysRoleService; public JeecgGroupManager(SysRoleService sysRoleService) { this.sysRoleService = sysRoleService; } @Override public Group findGroupById(String groupId) { SysRole role = sysRoleService.getByCode(groupId); if (role == null) { return null; } GroupImpl group = new GroupImpl(); group.setId(role.getRoleCode()); group.setName(role.getRoleName()); group.setType("assignment"); return group; } @Override public List<Group> findGroupsByNativeQuery(Map<String, Object> parameterMap) { // 生产环境建议按关键字过滤,不要全量返回 List<SysRole> roles = sysRoleService.list(); return roles.stream().map(role -> { GroupImpl group = new GroupImpl(); group.setId(role.getRoleCode()); group.setName(role.getRoleName()); group.setType("assignment"); return (Group) group; }).collect(Collectors.toList()); } }注意findGroupsByNativeQuery这里返回了全部角色,这是偷懒做法。如果系统角色数量多到几百个,这段代码会成为待办查询的瓶颈,因为Flowable解析候选组任务时需要拿用户所在组去匹配组ID。生产环境建议改成按关键字模糊查询sys_role的role_name,或者按用户ID反查部门再加部门对应的组,把数据量控制在几十条以内。
还有一步必须做:把这两个Manager注册到引擎配置里。只加@Component不会生效,Flowable不知道你的自定义实现存在。常见做法是注册一个ProcessEngineConfigurationConfigurer:
@Configuration public class FlowableEngineConfig { @Bean public ProcessEngineConfigurationConfigurer processEngineConfigurer( JeecgUserManager userManager, JeecgGroupManager groupManager) { return config -> { config.setUserManager(userManager); config.setGroupManager(groupManager); }; } }这个Configurer会在引擎初始化阶段执行,把默认的UserManager和GroupManager替换掉。验证是否生效很简单:应用启动后调用identityService.createUserQuery().userId("admin").singleResult(),看返回的User对象的id是不是JEECG里admin的username;或者直接观察日志里有没有对sys_user表的SQL查询,如果有,说明转发成功。
4. 部署、发起与审批:用源码把第一条业务流完整跑通
4.1 项目启动时自动部署BPMN流程
流程定义文件约定放在classpath的processes目录下。写一个CommandLineRunner,在Spring容器启动完成后自动扫描并部署:
@Component public class FlowableDeployer implements CommandLineRunner { private static final Logger log = LoggerFactory.getLogger(FlowableDeployer.class); private final RepositoryService repositoryService; public FlowableDeployer(RepositoryService repositoryService) { this.repositoryService = repositoryService; } @Override public void run(String... args) { String[] bpmnFiles = new String[]{ "processes/leave_v1.bpmn20.xml", "processes/contract_approval_v1.bpmn20.xml" }; for (String path : bpmnFiles) { boolean deployed = repositoryService.createDeploymentQuery() .deploymentName(path) .count() > 0; if (!deployed) { repositoryService.createDeployment() .addClasspathResource(path) .name(path) .deploy(); log.info("deployed: {}", path); } } } }这段代码先按deploymentName查一次,已部署过的流程定义不重复部署。这里有个容易踩的坑:流程文件内容改了但deploymentName没变,上面的判断会认为已存在而跳过部署,新改动不会生效。我习惯的做法是每次部署用的name带上版本号,比如leave_v1.bpmn20.xml、leave_v2.bpmn20.xml,改文件的同时改文件名,让引擎生成新版本定义;查询流程定义时再用processDefinitionKey取最新版本,避免历史版本干扰。
4.2 启动流程实例并绑定业务主键
启动流程实例最简单的方式是按流程定义key启动:
public String startLeaveProcess(String businessKey, String applyUser, int days) { Map<String, Object> variables = new HashMap<>(); variables.put("applyUser", applyUser); variables.put("days", days); variables.put("approved", false); ProcessInstance processInstance = runtimeService .startProcessInstanceByKey("leaveProcess", businessKey, variables); return processInstance.getId(); }businessKey在这里存的是业务表单主键,比如请假单在业务表里的id。这个设计很重要:审批完成回写业务表时,你需要靠businessKey把流程实例和业务记录关联起来,而不是靠变量。因为一个用户可能同时请假多次,如果只存用户名,流程和单据之间就是一对多的关系,回写时根本定位不到具体单子。
startProcessInstanceByKey第一个参数是BPMN里process元素的id属性值,不是文件名。很多人把这两个搞混,文件名可以随便起,但id必须全局唯一,引擎靠它定位流程定义。如果同一个key存在多个版本,这个方法默认启动最新版本;想指定老版本可以用startProcessInstanceById。变量里的approved字段是给条件表达式用的,BPMN里可以通过${approved}直接判断走向,这是后文审批回写的关键衔接。
启动之后,流程实例会停在第一个userTask节点上并生成一个待办任务。如果此时去taskService查询,会发现任务没有assignee,只有candidateGroups,这说明BPMN里设计成了候选组签收模式。候选组模式下,组内任何一个用户都可以通过claim来签收任务:
public void claimTask(String taskId, String username) { taskService.claim(taskId, username); }签收之后任务的assignee就从空变成该用户,待办列表查询也会命中。如果你的业务流程要求直接指定审批人,就在BPMN的userTask上写flowable:assignee表达式,比如${applyUser}或者具体的登录账号。
4.3 待办任务查询与审批操作的完整闭环
待办查询要同时照顾两种任务来源:直接分配给当前用户的任务,以及当前用户所在角色作为候选组的任务。
public List<Task> findTodoTasks(String username) { List<Task> assigned = taskService.createTaskQuery() .taskAssignee(username) .active() .orderByTaskCreateTime().desc() .list(); List<Task> candidate = taskService.createTaskQuery() .taskCandidateUser(username) .active() .orderByTaskCreateTime().desc() .list(); return Stream.concat(assigned.stream(), candidate.stream()) .distinct() .collect(Collectors.toList()); }注意这里加了active()过滤,已挂起流程的任务不会被查出来,避免流程暂停后还出现在待办里造成误操作。taskCandidateUser(username)的语义是“查询该用户直接或通过所在组可以签收的任务”,Flowable会内部调用GroupManager获取用户所在组,上一章自定义的GroupManager就在这里发挥作用。如果GroupManager没有正确注册,candidate查询会返回空列表,用户明明有角色权限,却看不到任何待办任务。
审批操作本质上是complete加流程变量:
public void completeTask(String taskId, boolean approved, String comment) { Map<String, Object> variables = new HashMap<>(); variables.put("approved", approved); if (StringUtils.hasText(comment)) { taskService.addComment(taskId, null, comment); } taskService.complete(taskId, variables); }这里有一个容易被忽视的细节:addComment存的是评论,不是流程变量。有些开发者把审批意见直接put进variables传给下一个节点,这样存在两个问题:一是意见数据会跟着流程变量一路传递,历史版本里可能残留;二是流程引擎的变量表是无序KV存储,查询审批记录时要从历史表中拼数据,很别扭。规范做法是评论走addComment进ACT_HI_COMMENT表,流程变量只放影响走向的判定值,比如approved这类。
最后补充一个查询层面的经验:待办列表接口一定要做分页,taskQuery支持listPage(firstResult, maxResults),不要一口气查全量。流程引擎的任务表本身就是高写入表,待办数超过几百条以后,全量list会让接口延迟明显上升,我在一个模拟项目X里就见过用户待办三千多条、接口四秒才返回的情况。
5. JEECG集成Flowable避坑指南:五个翻车现场与对应解法
5.1 坑一:启动时报ACT_GE_PROPERTY找不到
现象:应用启动到引擎初始化阶段,日志抛出表不存在的异常,指向ACT_GE_PROPERTY或ACT_GE_BYTEARRAY,有时还伴随“Cannot obtain JdbcConnection from DataSource”的错误。
原因分两种:一种是flowable.database-schema-update被设成了false,Flowable不会自动建表,而你没有手动执行过建表脚本;另一种是数据源账号只有DML权限没有DDL权限,建表语句被数据库拒绝。
解决:先确认配置里schema-update=true;再确认数据源账号有CREATE权限。如果生产环境不允许应用账号建表,需要DBA先执行Flowable对应数据库类型的建表SQL脚本,然后把schema-update改成false或validate。注意validate模式只校验表结构,不会往里写入任何初始数据,首次初始化时建表脚本必须手工完整执行一遍。
5.2 坑二:流程实例创建成功,但下一个节点的任务没生成
现象:startProcessInstanceByKey返回了流程实例ID,ACT_RU_EXECUTION里也有记录,但ACT_RU_TASK里查不到任何任务,待办接口返回空列表。
原因:最常见的是flowable.async-executor-activate=true,同时BPMN里某个节点或整个流程配了flowable:async="true"。再结合事务边界问题,流程节点进入等待状态但异步job没有执行成功,任务就一直不生成。这类问题非常隐蔽,它不会抛异常,只在日志里留下一行job执行失败的记录,很容易被忽略。
解决:集成初期把async-executor-activate关掉,把所有节点改成同步模式。跑通了再按需开放异步,并且给Flowable单独配置独立的线程池参数。另外检查调用startProcessInstanceByKey的方法上有没有@Transactional,如果外层事务回滚了,流程实例虽然打印出来但最终没落库,也会出现类似现象。
5.3 坑三:多数据源下Flowable连到了错误的库
现象:JEECG工程配置了主库和报表库两个数据源,启动后ACT_*表出现在报表库里,而主库里只有sys_*的JEECG表。流程数据跟业务数据被硬生生拆到了两个库,待办查询关联不到业务表。
原因:JEECG常见做法是用动态数据源组件在运行时切库,数据源从ThreadLocal里取。Flowable的自动配置在容器初始化时拿到的是某个默认DataSource,不一定是主数据源;如果动态数据源默认指向了报表库,Flowable建表就建到报表库去了。
解决:显式声明一个Flowable专用的DataSourceProvider,返回主数据源:
@Bean public FlowableDataSourceProvider flowableDataSourceProvider( @Qualifier("primaryDataSource") DataSource primaryDataSource) { return () -> primaryDataSource; }这样Flowable的持久层就不经过动态数据源的切库逻辑,始终使用主库。但要注意,如果项目中存在AOP切库拦截器,这个Provider返回的DataSource实例在使用前仍可能被代理包装,排查时看到连接URL不对,优先检查是不是切库AOP拦截了Flowable内部调用。
5.4 坑四:审批人认不出用户,候选任务消失
现象:流程定义里写了candidateGroups="manager",JEECG里也有一个角色编码为manager的用户组,但该角色下的用户登录后查不到待办任务;或者在流程跟踪图里看不到审批人姓名。
原因:GroupManager没有生效。常见的有两种:一是自定义GroupManager注册失败,Flowable还在用默认的ACT_ID_GROUP表,里面空空如也;二是sys_role的role_code和BPMN里的candidateGroups字符串大小写不一致,比如BPMN里写Manager,数据库里存manager,精确匹配直接落空。
解决:启动后先用identityService.createGroupQuery().groupId("manager").count()验证一下,返回0就说明GroupManager没接上。然后统一约定角色编码全小写,BPMN文件里也全小写,避免大小写不一致造成的玄学问题。用户侧同理,assignee统一用username小写,不要出现数据库里是Admin、流程里写admin的情况。
5.5 坑五:Linux服务器上流程追踪图中文全是方块
现象:本地Windows开发环境流程图中文正常,部署到Linux服务器后,流程追踪图上的节点名和审批意见中文全部变成豆腐块。
原因:Java2D在渲染图片时依赖系统字体库,Linux服务器通常没装中文字体,引擎找不到能显示中文的Font,只能画方块。
解决:服务器上安装fontconfig并部署一套中文字体文件,常见做法是把Windows下的宋体或黑体上传到/usr/share/fonts/chinese目录,然后执行fc-cache -fv刷新字体缓存。也可以在application.yml里通过flowable.font-name指定一个服务器已装的字体,比如flowable.font-name=WenQuanYi Zen Hei。这个问题不影响流程执行,只影响展示,但客户看到全是方块会直接认为系统坏了,优先级不低。
6. 进阶技巧:用Online表单发起流程,审批完成自动回写
6.1 在Online表单列表按钮里挂上发起入口
JEECG的Online表单生成的是动态业务表,最实用的做法是在列表页加一个“提交审批”按钮,把当前行的主键和表单数据通过后台接口发起流程。我通常会写一个通用方法,接收表单编码、数据ID、流程定义key三个参数,内部先查业务数据,再把主键当作businessKey启动流程:
public void startFromOnlineForm(String formCode, String dataId, String processKey) { Map<String, Object> formData = onlineFormService.queryDataById(formCode, dataId); Map<String, Object> variables = new HashMap<>(); variables.put("formCode", formCode); variables.put("dataId", dataId); variables.put("applyUser", LoginUtil.getLoginUser().getUsername()); runtimeService.startProcessInstanceByKey(processKey, dataId, variables); }6.2 审批完成后回写业务状态
流程走完后,业务表里那条单子需要把状态从“审批中”改成“已通过”或“已驳回”。我习惯在提交审批时给流程实例打一个自定义的业务状态变量,然后在流程的结束事件上用ExecutionListener统一收尾,或者在completeTask里判断当前任务是不是最后一个节点,是的话直接更新业务表。相比在每个审批节点里写业务代码,统一收尾的好处是新增流程时不用重复实现状态回写。
最后说一个我的习惯:凡是做Flowable集成,我拿到手的第一件事永远是先写一个冒烟测试,跑通“部署-发起-查任务-完成任务”这四步,再谈业务改造。这个习惯帮我避掉了很多后面才暴露的问题。希望帮到你。
本文还有配套的精品资源,点击获取