1. 项目概述:当RuoYi遇上Flowable
去年接手某集团OA系统升级项目时,技术选型阶段我们团队在Activiti和Flowable之间反复权衡。最终选择Flowable 7作为流程引擎,主要基于其原生支持的Spring Boot Starter特性以及与RuoYi框架的无缝集成能力。这套组合拳在实际落地过程中,既展现了强大的企业级流程管理能力,也让我们踩遍了集成路上的各种"暗坑"。
RuoYi作为国内流行的快速开发框架,其权限体系和代码生成器能大幅降低基础模块开发成本。而Flowable 7作为Activiti的分支演进版本,在性能优化和API设计上都有显著提升。两者结合特别适合需要快速实现复杂业务流程的中大型项目,比如我们实施的采购审批、费用报销等场景。但要注意的是,官方文档对某些细节的说明并不充分,这也是本文重点分享实战经验的原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与基础集成
2.1 依赖配置的玄机
在pom.xml中引入依赖时,很多教程只告诉你要加flowable-spring-boot-starter,但没说明版本陷阱。经过实测,推荐使用以下配置:
xml复制<!-- Flowable核心 -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
<version>7.0.0</version>
</dependency>
<!-- 表单设计支持 -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-form-spring-configurator</artifactId>
<version>7.0.0</version>
</dependency>
特别注意:RuoYi默认的MyBatis版本可能与Flowable存在冲突。我们遇到过一个诡异问题——流程实例启动时报SQL语法错误,最终发现是MyBatis 3.5.6与Flowable 7.0.0的兼容性问题。解决方案是统一升级到MyBatis 3.5.7+。
2.2 数据库初始化策略
Flowable需要28张核心表来存储流程数据,官方提供两种初始化方式:
- 自动建表(适合开发环境):
yaml复制flowable:
database-schema-update: true
- 手动执行SQL(生产环境必备):
在resources/db目录下存放flowable的建表脚本,通过RuoYi的DataSource配置初始化。我们推荐生产环境采用这种方式,因为:
- 可以自定义表前缀(如flw_)
- 能控制索引创建策略
- 避免启动时自动执行DDL的风险
踩坑记录:某次测试环境升级时,由于没关闭auto-update,导致流程定义表被意外修改,造成历史数据无法关联。建议即使开发环境也尽量使用手动SQL模式。
3. 流程设计与模型部署
3.1 可视化设计器集成
RuoYi本身没有流程设计器,我们通过两种方案实现:
方案A:嵌入Flowable Modeler
java复制// 在SecurityConfig中放行设计器路径
@Override
protected void configure(HttpSecurity http) {
http.authorizeRequests()
.antMatchers("/modeler/**").permitAll();
}
优点:官方原生支持
缺点:界面风格与RuoYi不统一
方案B:定制Vue前端
基于bpmn-js开发的设计器组件,关键集成代码:
javascript复制import BpmnModeler from 'bpmn-js/lib/Modeler'
export default {
mounted() {
this.bpmnModeler = new BpmnModeler({
container: '#canvas'
})
}
}
实测效果:与RuoYi风格统一,但开发成本较高。我们最终选择方案A快速上线,后续逐步替换为方案B。
3.2 模型部署的三种姿势
- Classpath部署(适合静态流程)
java复制repositoryService.createDeployment()
.addClasspathResource("processes/leave.bpmn20.xml")
.deploy();
- 动态字符串部署(适合流程版本管理)
java复制String bpmnXml = generateDynamicXml();
repositoryService.createDeployment()
.addString("process_v1.bpmn20.xml", bpmnXml)
.deploy();
- 数据库存储部署(企业级推荐)
java复制// 从ruoyi_sys_process表读取模型
ProcessDefinition definition = processMapper.selectLatest();
repositoryService.createDeployment()
.addBytes(definition.getName(), definition.getBytes())
.tenantId(definition.getTenantId())
.deploy();
性能提示:部署时务必添加tenantId参数,否则在多租户环境下会出现流程混淆问题。我们曾因此导致A公司的审批流程跑到B公司去,引发严重事故。
4. 核心业务逻辑实现
4.1 自定义表单与流程绑定
RuoYi的动态表单需要与Flowable表单关联,关键代码示例:
java复制// 启动流程时绑定表单
StartFormData formData = formService.getStartFormData(processDefinitionId);
Map<String, String> formValues = new HashMap<>();
formValues.put("leaveDays", "3");
formService.submitStartFormData(processDefinitionId, formValues);
// 任务节点表单处理
TaskFormData taskForm = formService.getTaskFormData(taskId);
formService.submitTaskFormData(taskId, formService.getRenderedTaskForm(taskId));
实际项目中我们扩展了FormService,使其支持RuoYi表单引擎的JSON Schema格式:
java复制public class CustomFormServiceImpl extends FormServiceImpl {
@Override
public Object getRenderedStartForm(String processDefinitionId) {
// 转换Flowable表单为RuoYi格式
return FormConverter.convert(super.getStartFormData(processDefinitionId));
}
}
4.2 审批链路的三种模式
- 固定审批人模式
xml复制<userTask id="leaderApproval" name="部门领导审批"
flowable:assignee="${applyUserId}_leader"/>
- 动态候选人模式
java复制taskService.addCandidateUser(taskId, userId);
taskService.addCandidateGroup(taskId, roleId);
- 会签模式(最复杂)
xml复制<userTask id="multiSign" name="会签">
<multiInstanceLoopCharacteristics
isSequential="false"
flowable:collection="assigneeList"
flowable:elementVariable="singleAssignee">
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
血泪教训:会签节点的collection参数必须实现Serializable接口!我们曾因忘记序列化导致流程实例崩溃。
5. 生产环境调优实战
5.1 性能优化四板斧
- 历史数据归档策略
yaml复制flowable:
history-level: audit # 生产环境推荐级别
async-executor-activate: true
- 批量操作API使用
java复制// 错误示范:循环提交
tasks.forEach(task -> taskService.complete(task.getId()));
// 正确做法:批量处理
List<String> taskIds = tasks.stream().map(Task::getId).collect(Collectors.toList());
taskService.bulkComplete(taskIds);
- 查询优化技巧
java复制// 避免全表扫描
historyService.createHistoricTaskInstanceQuery()
.taskTenantId(tenantId)
.orderByTaskCreateTime().desc()
.listPage(0, 50);
- 缓存配置示例
java复制@Bean
public ProcessEngineConfigurationImpl processEngineConfiguration(DataSource dataSource) {
SpringProcessEngineConfiguration config = new SpringProcessEngineConfiguration();
config.setProcessDefinitionCache(new DefaultProcessDefinitionCache(200));
return config;
}
5.2 高可用部署方案
我们的生产环境架构:
code复制 +-----------------+
| Nginx LB |
+--------+--------+
|
+---------------+---------------+
| |
+-------+-------+ +-------+-------+
| App Server 1 | | App Server 2 |
| (RuoYi+Flow) | | (RuoYi+Flow) |
+-------+-------+ +-------+-------+
| |
+---------------+---------------+
|
+--------+--------+
| MySQL Cluster |
+-----------------+
| Redis Sentinel |
+-----------------+
关键配置项:
yaml复制flowable:
async-executor:
lock-wait-time: 5m
max-async-jobs-due-per-acquisition: 100
6. 典型问题排查手册
6.1 流程卡死问题
现象:流程实例状态为RUNNING但无活跃任务
排查步骤:
- 检查ACT_RU_EXECUTION表
sql复制SELECT * FROM ACT_RU_EXECUTION WHERE PROC_INST_ID_='流程实例ID';
- 查看未完成的任务
java复制List<HistoricActivityInstance> activities = historyService
.createHistoricActivityInstanceQuery()
.unfinished()
.processInstanceId(processInstanceId)
.list();
- 常见修复方案:
java复制// 方案1:触发异步作业
managementService.executeJob(jobId);
// 方案2:重置流程变量
runtimeService.setVariable(executionId, "retryCount", 0);
6.2 表单数据丢失
根本原因:Flowable默认将表单变量存储在ACT_HI_VARINST表,但RuoYi可能使用自己的表结构
解决方案:
- 自定义变量处理器
java复制public class RuoYiVariableType implements VariableType {
@Override
public void setValue(Object value, ValueFields valueFields) {
// 存储到ruoyi_form_data表
formMapper.insert(new FormData(valueFields));
}
}
- 注册自定义类型
java复制configuration.getVariableTypes().addType(new RuoYiVariableType(), 0);
7. 扩展开发技巧
7.1 与RuoYi权限体系集成
改造Flowable的UserTaskAssigneeResolver:
java复制public class RuoYiAssigneeResolver implements UserTaskAssigneeResolver {
@Override
public String resolveAssignee(String assigneeExpression, DelegateExecution execution) {
// 关联RuoYi的用户体系
return SysUserUtils.translateAssignee(assigneeExpression);
}
}
权限校验增强:
java复制@Around("execution(* org.flowable.engine.impl.TaskServiceImpl.*(..))")
public Object checkPermission(ProceedingJoinPoint pjp) {
String taskId = (String) pjp.getArgs()[0];
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (!PermissionUtils.hasPermission(task.getTenantId())) {
throw new FlowablePermissionException();
}
return pjp.proceed();
}
7.2 流程版本管理方案
我们的版本控制策略:
- 每次部署生成版本快照
java复制Deployment deployment = repositoryService.createDeployment()
.addBytes(processName, bpmnBytes)
.enableDuplicateFiltering()
.deploy();
// 记录版本关系
processVersionMapper.insert(new ProcessVersion(
deployment.getId(),
businessKey,
getNextVersion(businessKey))
);
- 流程实例关联版本
java复制ProcessInstance instance = runtimeService.startProcessInstanceByKey(
"leaveProcess",
variables,
versionContext.getVersionId()
);
8. 监控与运维体系
8.1 健康检查端点
自定义HealthIndicator实现:
java复制@Component
public class FlowableHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
long jobCount = managementService.createJobQuery().count();
return Health.up()
.withDetail("jobs", jobCount)
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
8.2 审计日志集成
通过事件监听器记录操作日志:
java复制public class RuoYiFlowableEventListener implements FlowableEventListener {
@Override
public void onEvent(FlowableEvent event) {
if (event instanceof FlowableActivityEvent) {
// 记录到ruoyi_sys_log表
logMapper.insert(convertToSysLog(event));
}
}
}
日志关联实现:
java复制MDC.put("processInstanceId", execution.getProcessInstanceId());
Logger.info("流程节点触发:{}", execution.getCurrentActivityId());
经过半年多的生产验证,这套RuoYi+Flowable7的组合已稳定支撑日均3000+流程实例的运行。最大的体会是:企业级工作流落地不能只关注功能实现,更需要建立完整的生命周期管理体系。特别是在流程版本控制、性能监控、异常恢复等方面,需要根据业务特点持续优化。
