当 Spring Boot 遇上 Flowable 7:Forge 框架企业级工作流引擎架构全解析

本文以 Forge Admin 开源框架为例,深入剖析如何基于 Flowable 7 构建一套"声明式接入、事件驱动同步、双设计器前端"的企业级工作流平台。从三模块解耦架构到 @FlowStart 注解式发流,从 Redis Pub/Sub 事件总线到 AI 生成 BPMN,带你理解每一个设计决策背后的工程考量。

📌 仓库地址

欢迎 Star ⭐ 支持!项目持续更新中。


一、为什么不是"开箱即用"的 Flowable?

Flowable 自带的 Spring Boot Starter 能让你 5 分钟跑通一个流程,但当真正落地企业级场景时,你会遇到一连串问题:

  1. 业务代码与流程引擎深度耦合 --- 每次发起流程都要写一堆 runtimeService.startProcessInstanceByKey(...),业务层充斥着流程 API
  2. 状态同步全靠手写 --- 流程审批通过/驳回后,业务表状态怎么更新?写定时任务轮询?还是在每个审批接口里硬编码?
  3. 多租户隔离 --- Flowable 原生多租户机制和业务框架的多租户拦截器如何协同?
  4. 前端设计器 --- bpmn-js 原生面板对业务人员来说门槛太高,能不能做成钉钉审批那种拖拽式?
  5. 流程定义和业务表单绑定 --- 一个流程模型对应哪张业务表?表单字段在哪些节点可编辑?

Forge 框架的工作流模块正是为了解决这些问题而设计的。下面我们逐层拆解。


二、三模块解耦架构

Forge 将工作流拆分为三个 Maven 模块,遵循"引擎 / 插件 / SDK"的分离原则:

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                    业务模块 (forge-business)              │
│                                                         │
│   @FlowBind  @FlowStart  @FlowCallback                  │
│        │           │           │                        │
│   ┌────▼───────────▼───────────▼────┐                   │
│   │     forge-flow-client (SDK)      │                   │
│   │  · FlowClient (HTTP 门面)        │                   │
│   │  · FlowStartAspect (AOP 自动发流) │                   │
│   │  · FlowEventSubscriber (事件分发) │                   │
│   └────┬───────────────────────┬─────┘                   │
│        │ HTTP/RestTemplate     │ Redis Pub/Sub           │
│   ┌────▼───────────────────────▼─────┐                   │
│   │   forge-plugin-flow (引擎插件)    │                   │
│   │  · FlowInstanceServiceImpl       │                   │
│   │  · FlowTaskServiceImpl           │                   │
│   │  · FlowTaskEventListener         │                   │
│   │  · Flowable 7.0 引擎             │                   │
│   └──────────────────────────────────┘                   │
│                                                         │
│   forge-flow-server (独立微服务, 可选部署)                 │
└─────────────────────────────────────────────────────────┘
模块 定位 Flowable 依赖
forge-flow-client 业务侧 SDK,提供注解 + HTTP 门面 + 事件订阅 ❌ 无
forge-plugin-flow 引擎核心,封装 Flowable 全部能力 ✅ 7.0.1
forge-flow-server 独立微服务入口,薄 REST 层

关键设计 :业务模块只依赖 forge-flow-client,完全不引入 Flowable 的任何类。业务代码通过注解声明流程绑定,通过 HTTP 调用引擎服务,通过 Redis 接收事件回调。这意味着------你可以把流程引擎拆成独立微服务部署,业务模块完全无感知


三、声明式注解:让业务代码"零侵入"接入流程

这是整个设计中最核心的部分。三个注解,解决"绑定、发起、回调"三个核心问题。

3.1 @FlowBind --- 类级别绑定

less 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FlowBind {
    String modelKey();            // 绑定流程模型 key
    String businessType() default "";
}

标注在 Service 类上,声明"这个 Service 管理的业务走哪个流程模型"。后续的 @FlowStart 可以省略 modelKeyFlowEventSubscriber 也靠它来路由回调事件。

3.2 @FlowStart --- 方法级别自动发起

java 复制代码
@Target(ElementType.METHOD)
public @interface FlowStart {
    String modelKey() default "";        // 默认取 @FlowBind 的值
    String businessKeySpEl();            // 必填,SpEL 表达式
    String titleSpEl() default "";
    String userIdSpEl() default "";
    String variablesSpEl() default "";   // 必须返回 Map<String,Object>
    boolean skipOnError() default true;
}

设计亮点:用 SpEL 表达式从方法参数和返回值中提取流程变量,业务方法完全不用关心流程 API。

AOP 切面 FlowStartAspect 的核心逻辑:

java 复制代码
@Around("@annotation(flowStart)")
public Object around(ProceedingJoinPoint pjp, FlowStart flowStart) throws Throwable {
    Object result;
    try {
        result = pjp.proceed();       // 先执行业务方法
    } catch (Throwable ex) {
        if (!flowStart.skipOnError()) tryStart(pjp, flowStart, null);
        throw ex;
    }
    tryStart(pjp, flowStart, result); // 业务成功后自动发起流程
    return result;
}

SpEL 上下文支持:#paramName(按参数名)、#p0/#p1(按位置)、#result(返回值),甚至可以通过 BeanResolver 引用 Spring Bean。

使用示例:

ini 复制代码
@FlowStart(
    businessKeySpEl = "'leave:' + #leave.id",
    titleSpEl       = "#leave.name + ' 的请假申请'",
    userIdSpEl      = "#leave.applicantId",
    variablesSpEl   = "{'days': #leave.days, 'type': #leave.leaveType}"
)
public Leave createLeave(Leave leave) {
    leaveMapper.insert(leave);
    return leave;  // 返回值供 SpEL 使用
}

业务方法只管存数据,流程自动发起------这就是"零侵入"的含义

3.3 @FlowCallback --- 事件回调

scss 复制代码
@FlowCallback(on = {
    FlowCallback.ON_COMPLETED,      // 流程通过
    FlowCallback.ON_REJECTED,       // 流程驳回
    FlowCallback.ON_CANCELED,       // 流程撤销
    FlowCallback.ON_TASK_CREATED,   // 任务创建
    FlowCallback.ON_TASK_COMPLETED, // 任务完成
    FlowCallback.ON_TASK_ASSIGNED   // 任务分配
})
public void handleFlowEvent(FlowEventContext context) {
    switch (context.getEvent()) {
        case FlowCallback.ON_COMPLETED -> markApproved(context.getBusinessKey());
        case FlowCallback.ON_REJECTED  -> markRejected(context.getBusinessKey(), context.getComment());
        case FlowCallback.ON_CANCELED  -> markCanceled(context.getBusinessKey());
    }
}

FlowEventContext 携带了完整的上下文信息:businessKeyprocessInstanceIdtaskNameassigneeIdcommentvariables 等。

事件传递链路

sql 复制代码
Flowable 引擎事件
    ↓
FlowTaskEventListener (引擎侧监听器)
    ↓
Redis Pub/Sub 频道: flow:event:all
    ↓
FlowEventSubscriber (业务侧订阅器)
    ↓
匹配 @FlowBind modelKey → 调用 @FlowCallback 方法

整个链路通过 Redis Pub/Sub 解耦,业务侧和引擎侧可以部署在不同进程。


四、引擎层核心设计:从发流到审批的完整链路

4.1 流程发起 - FlowInstanceServiceImpl

发流看似简单(一行 runtimeService.startProcessInstanceByKey),但企业级场景下要处理一堆边界问题:

scss 复制代码
// 1. 幂等锁:同一 businessKey 不能重复发起
ReentrantLock lock = acquireFlowStartLock(tenantId, businessKey, 5);

// 2. 先写业务关联表,再启动流程
//    原因:TASK_CREATED 事件监听器需要查到业务上下文
flowBusinessMapper.insert(flowBusiness);

// 3. 自动注入流程变量
variables.put("initiator", startUserId);
variables.put("initiatorLeader", orgService.getLeaderUserIdByLevel(...));
variables.put("regionCode", SessionHelper.getRegionCode());
variables.put("startUserOrgIds", orgService.getUserOrgIds(userId));

// 4. 按需部署:如果没有流程定义,自动从 FlowModel 部署 BPMN
deployModelOnDemand(modelKey);

// 5. 启动流程实例
processInstance = runtimeService.startProcessInstanceByKey(modelKey, businessKey, vars);

五个关键工程决策

决策 原因
幂等锁 + DB 唯一键 防止并发请求重复发流,5s 超时
先写业务表再启流程 TASK_CREATEDstartProcessInstanceByKey 同步触发,监听器要能查到上下文
自动变量注入 发起人、直属领导、组织、区划等变量自动填充,BPMN 网关条件可直接用
按需部署 业务侧不需要手动部署流程定义,首次发起自动部署
锁随事务释放 TransactionSynchronizationManager 确保锁在事务提交后才释放

4.2 任务审批 - FlowTaskServiceImpl

审批的核心不仅是"完成任务",还包括权限校验、变量注入、状态同步:

scss 复制代码
public void approve(taskId, userId, comment, signature, variables) {
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();

    // 节点级权限校验:这个节点允许审批吗?
    validateTaskAction(task, ACTION_APPROVE, comment, signature);

    // 校验必填变量
    validateRequiredVariables(task, variables);

    // 记录审批意见
    taskService.addComment(taskId, task.getProcessInstanceId(), comment);

    // 注入网关判断变量
    completeTask(task, mergeActionVariables(variables, true));
    // mergeActionVariables 会设置:
    //   approved = true
    //   approvalResult = "approve"
    // 网关条件直接读这两个变量做分支

    // 自动跳过重复审批人
    autoApproveRepeatedTasks(task.getProcessInstanceId());
}

自动跳过重复审批人是个很实用的功能:当流程经过多个节点,而某些节点的审批人是同一个人时,系统会自动帮 ta 审批通过,避免"自己审自己"。支持两种模式:

  • firstOnly - 同一实例中,一个人只在第一次出现时审批
  • consecutive - 连续节点为同一人时自动跳过

4.3 驳回与退回

Forge 实现了三种"往回走"的操作,语义各不同:

操作 发起者 语义 实现
驳回 (reject) 审批人 不同意,流程结束 complete + approvalResult=reject
退回 (returnTask) 审批人 退到指定节点重审 createChangeActivityStateBuilder
撤回 (withdraw) 发起人 发起人主动撤回 allowSubmitterWithdraw 控制

退回操作使用 Flowable 7 的状态迁移 API:

scss 复制代码
runtimeService.createChangeActivityStateBuilder()
    .processInstanceId(processInstanceId)
    .moveActivityIdsToSingleActivityId(currentActivityIds, targetActivityId)
    .changeState();

五、事件驱动同步:FlowTaskEventListener

这是引擎侧的"同步枢纽",实现了 FlowableEventListener,监听所有引擎事件并同步到业务表:

复制代码
TASK_CREATED       → 在 sys_flow_task 创建待办记录 + 发站内信
TASK_COMPLETED     → 更新任务状态 + 检测驳回
TASK_ASSIGNED      → 更新受理人
PROCESS_COMPLETED  → 读取 approvalResult 变量 → 更新 sys_flow_business 状态
PROCESS_CANCELLED  → 标记业务为已取消

设计要点

  1. 异步发布 - 事件发布用独立线程池 flowEventExecutor(core 2 / max 8 / queue 200),不阻塞引擎线程
  2. 幂等注册 - 在 ApplicationReadyEvent 时注册监听器,带防重复注册保护
  3. 站内信去重 - MessageService.sendIfAbsent 确保同一任务不会重复发通知
  4. 抄送触发 - TASK_CREATED 时根据节点配置的角色解析抄送人,触发 FlowCcService.sendCc
scss 复制代码
// FlowAutoConfiguration 中的关键配置
@Bean
public TaskExecutor flowEventExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(8);
    executor.setQueueCapacity(200);
    executor.setThreadNamePrefix("flow-event-");
    return executor;
}

六、前端双设计器:专业 BPMN + 钉钉风格

Forge 前端实现了两套流程设计器,根据模型类型自动切换:

6.1 专业 BPMN 设计器(基于 bpmn-js)

bash 复制代码
components/bpmn/
├── FlowModeler.vue          # 核心设计器
├── CustomRenderer.js        # 自定义 SVG 渲染
├── flowable-moddle.json     # Flowable 命名空间扩展
├── NodePropertiesPanel.vue  # 属性面板
├── AutoLayout.js            # dagre 自动布局
└── InteractiveProcessDiagram.vue  # 运行时流程图

深度定制CustomRenderer.js 重写了所有节点的 SVG 渲染,用渐变色 + 图标替代 bpmn-js 默认样式:

css 复制代码
const COLORS = {
  start:       { from: '#34d399', to: '#10b981' },  // 绿
  end:         { from: '#f87171', to: '#ef4444' },  // 红
  userTask:    { from: '#60a5fa', to: '#3b82f6' },  // 蓝
  serviceTask: { from: '#a78bfa', to: '#8b5cf6' },  // 紫
}

Flowable 属性扩展 :通过 flowable-moddle.json 给 bpmn-js 注入 Flowable 专属属性,让设计器能直接编辑 assigneecandidateUsersformKeyallowApproverequireSignature 等属性。

6.2 钉钉风格设计器(从零实现)

bash 复制代码
components/flow-designer/
├── DingFlowDesigner.vue     # 主组件,API 与 FlowModeler 1:1 兼容
├── canvas/                  # 画布、节点渲染、边线、布局引擎
├── nodes/                   # 审批人/发起/结束/分支/服务/抄送节点
├── panel/                   # 各节点配置面板
├── converter/               # JSON ↔ BPMN XML 双向转换
└── viewer/                  # 只读运行时视图

这套设计器的核心是 JSON 树 ↔ BPMN XML 的双向转换器

arduino 复制代码
// converter/json-to-bpmn.js - 将钉钉风格 JSON 树转为 BPMN XML
// converter/bpmn-to-json.js - 将 BPMN XML 转为钉钉风格 JSON 树

两套设计器对外接口完全一致:

xml 复制代码
<!-- design.vue 根据模型类型自动切换 -->
<DingFlowDesigner v-if="isApprovalDesigner" :xml="bpmnXml" @change="handleBpmnChange" />
<FlowModeler v-else :xml="bpmnXml" @change="handleBpmnChange" />

业务人员用钉钉风格拖拽设计,专业开发者用 BPMN 设计器精细控制--同一份数据,两种编辑体验


七、实战:采购审批完整示例

Forge 内置了一个采购审批示例(forge-business 模块),完整演示了从发流到回调的全链路。

7.1 绑定流程模型

less 复制代码
@Slf4j
@Service
@RequiredArgsConstructor
@FlowBind(modelKey = "purchase_approval", businessType = "purchase")
public class SamplePurchaseOrderServiceImpl
        extends ServiceImpl<SamplePurchaseOrderMapper, SamplePurchaseOrder>
        implements SamplePurchaseOrderService {

    public static final String MODEL_KEY = "purchase_approval";
    public static final String BUSINESS_TYPE = "purchase";

    private final FlowClient flowClient;
    // ...
}

7.2 发起流程

ini 复制代码
public void submit(SamplePurchaseOrderSubmitDTO dto) {
    // 1. 保存业务数据
    SamplePurchaseOrder entity = savePurchaseOrder(dto);

    // 2. 构建流程变量
    String businessKey = "purchase:" + entity.getId();
    Map<String, Object> variables = new HashMap<>();
    variables.put("recordId", entity.getId());
    variables.put("orderNo", entity.getOrderNo());
    variables.put("amount", entity.getAmount());
    variables.put("deptLeaderId", orgService.getLeaderUserId(entity.getCreateBy()));
    variables.put("countersignUsers", engineeringManagerIds);

    // 3. 调用 FlowClient 发起流程
    FlowResult<String> result = flowClient.startProcess(
            MODEL_KEY, businessKey, BUSINESS_TYPE,
            flowTitle, variables,
            userId, userName, deptId, null);

    // 4. 回写流程实例 ID
    entity.setBusinessKey(businessKey);
    entity.setProcessInstanceId(result.getData());
    markInProcess(entity);
}

7.3 接收回调

scss 复制代码
@FlowCallback(on = {
    FlowCallback.ON_TASK_CREATED,
    FlowCallback.ON_TASK_COMPLETED,
    FlowCallback.ON_COMPLETED,
    FlowCallback.ON_REJECTED,
    FlowCallback.ON_CANCELED
})
@Transactional(rollbackFor = Exception.class)
public void handleFlowEvent(FlowEventContext context) {
    String businessKey = context.getBusinessKey();
    String event = context.getEvent();

    switch (event) {
        case FlowCallback.ON_TASK_CREATED ->
            // 进入"申请人修改"节点时,标记为待修改
            handleTaskCreated(context);
        case FlowCallback.ON_TASK_COMPLETED ->
            // 检测是否被驳回
            handleTaskCompleted(context);
        case FlowCallback.ON_COMPLETED ->
            // 流程通过,标记为已审批
            markApproved(businessKey);
        case FlowCallback.ON_REJECTED ->
            // 驳回,记录原因
            markRejected(businessKey, context.getComment());
        case FlowCallback.ON_CANCELED ->
            // 撤销
            markCanceled(businessKey);
    }
}

7.4 流程模型自举

最巧妙的是 ensureFlowModel() 方法--业务模块可以自己保证流程模型存在

scss 复制代码
private void ensureFlowModel() {
    // 1. 用代码构建 BPMN XML
    String bpmnXml = SamplePurchaseOrderFlowBpmn.build();

    // 2. 检查模型是否存在
    FlowModelVO model = flowClient.getModelByKey(MODEL_KEY);

    if (model == null) {
        // 3. 不存在则创建
        flowClient.createModel(modelRequest);
    }

    // 4. 未部署则自动部署
    if (!isDeployed(model)) {
        flowClient.deployModel(model.getId());
    }
}

这意味着业务模块可以独立交付--不需要运维人员手动在管理界面建流程模型,首次启动时自动完成。

7.5 状态机流转

scss 复制代码
DRAFT(草稿)
  ↓ submit
IN_PROCESS(审批中)
  ↓ ON_TASK_CREATED (进入修改节点)
NEED_MODIFY(待修改)
  ↓ resubmit
IN_PROCESS(审批中)
  ↓ ON_COMPLETED          ↓ ON_REJECTED       ↓ ON_CANCELED
APPROVED(已通过)    REJECTED(已驳回)    CANCELED(已撤销)

八、AI 驱动 BPMN 生成

Forge 集成了 AI 能力,可以根据自然语言描述自动生成 BPMN 流程定义:

less 复制代码
// FlowBpmnGenerateService - AI 生成 BPMN
public SseEmitter streamGenerate(FlowBpmnGenerateRequest request) {
    SseEmitter emitter = new SseEmitter(120_000L);

    // 构建富上下文 Prompt
    String prompt = buildPrompt(request,
        assigneeTypes,        // 审批人类型
        formTypes,            // 表单类型
        multiInstanceTypes,   // 会签类型
        completionConditions, // 完成条件
        timeoutActions,       // 超时动作
        enabledSpelTemplates, // SpEL 模板
        enabledForms,         // 可用表单
        currentNodeConfigs    // 当前节点配置
    );

    // 调用 AI 客户端,流式返回
    aiClient.streamChat(prompt, agentCode("flow_bpmn_builder"))
        .onProgress(chunk -> emitter.send(SseEmitter.event().name("chunk").data(chunk)))
        .onComplete(result -> emitter.send(SseEmitter.event().name("complete").data(result)))
        .onError(err -> emitter.send(SseEmitter.event().name("error").data(err.getMessage())));

    return emitter;
}

前端通过 SSE(Server-Sent Events)实时接收生成进度:

javascript 复制代码
// flow-generator.js
const response = await fetch('/api/flow/ai-generator/stream-generate', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` },
    body: JSON.stringify(data),
    signal: controller.signal  // 支持中断
});

const reader = response.body.getReader();
// 解析 event: progress | chunk | complete | error

Prompt 工程:不是简单地把用户描述扔给大模型,而是注入了完整的平台上下文(可用审批人类型、表单字段、会签规则等),让 AI 生成的 BPMN 直接可用,不需要二次调整。


九、核心设计总结

问题 Forge 的解法
业务代码耦合流程 API @FlowStart AOP + SpEL,零侵入发流
流程状态同步 FlowTaskEventListener → Redis Pub/Sub → @FlowCallback
多租户隔离 引擎侧事件携带 tenantId,回调时切换租户上下文
流程模型管理 @FlowBind 绑定 + ensureFlowModel() 自举
前端设计器门槛 双设计器:bpmn-js(专业)+ 钉钉风格(业务友好)
重复审批 autoApproveRepeatedTasks 自动跳过
流程定义部署 按需部署,首次发起自动 deploy
驳回/退回/撤回 三种语义分离,各自独立实现
AI 辅助设计 富上下文 Prompt + SSE 流式生成

架构分层回顾

less 复制代码
业务层    @FlowBind / @FlowStart / @FlowCallback  ← 声明式,零 Flowable 依赖
SDK层     FlowClient / FlowStartAspect / FlowEventSubscriber  ← HTTP + Redis
引擎层    FlowInstanceService / FlowTaskService / EventListener  ← Flowable 7
前端层    FlowModeler / DingFlowDesigner  ← 双设计器,同一份数据
AI层      FlowBpmnGenerateService  ← 自然语言 → BPMN

十、写在最后

企业级工作流从来不是"引入一个引擎"就能解决的问题。Flowable 提供了强大的流程执行能力,但在它和业务系统之间,还需要一整套抽象层来处理声明式接入、事件同步、多租户、设计器适配等问题。

Forge 框架的工作流模块试图给出一份参考实现--不一定是唯一答案,但每一个设计决策都有明确的工程动机。如果你也在做类似的技术选型,希望本文能给你一些启发。

如果这篇文章对你有帮助,欢迎到仓库点个 Star ⭐

后续会继续分享:

  • Flowable 多租户深度实践
  • 动态表单与流程节点字段权限控制
  • 钉钉风格设计器的 JSON ↔ BPMN 转换算法
  • Flowable 性能优化与历史数据归档

敬请关注,我们下篇见! 🚀

相关推荐
java小白小1 小时前
SpringBoot(15):日志框架选型与配置——Logback 从入门到生产级
后端
HackTwoHub1 小时前
ARL灯塔重构版:支持APP/小程序/WEB资产同步扫描
人工智能·安全·web安全·网络安全·小程序·重构·自动化
KaMeidebaby1 小时前
卡梅德生物技术快报|小 RNA 适配体合成 + 多方法亲和力表征全流程标准化操作手册
前端·网络·数据库·人工智能·算法
是Dream呀1 小时前
基于深度学习的人类行为识别算法研究
人工智能·深度学习·算法
庄周de蝴蝶1 小时前
两次考试,总算压线通过系统分析师
后端·架构
孫治AllenSun2 小时前
【MDC】使用 MDC 封装一个 LogMdcUtil 工具类
java·spring boot·后端
人工智能培训2 小时前
破解数据发展瓶颈 激活数字经济新动能
大数据·数据库·人工智能·深度学习·神经网络·机器学习·生成对抗网络
阿里云大数据AI技术2 小时前
MaxCompute 多模态检索:原生向量能力开启多模态 AI 大数据处理新范式
人工智能
神奇小汤圆2 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
后端