在开源流程引擎 Flowable 里如何实现任意节点跳转?

在 Flowable 中实现任意节点跳转,首选官方 RuntimeService.createChangeActivityStateBuilder(),而不是修改运行时表,也通常不需要自己编写底层 Command。对普通用户任务,取得当前任务的 executionId 后调用 moveExecutionToActivityId(executionId, targetActivityId).changeState(),Flowable 会在同一命令事务中取消源节点运行时数据、调整 Execution 树、重建目标作用域,并通过 Agenda 启动目标节点。

但"任意"不等于"随便"。单个任务跳转、多个并行令牌合并、一个令牌拆成多分支、多实例实例退出、嵌入式子流程跨层和 Call Activity 跨流程,必须选择不同 API。目标节点是否合法、谁有权跳转、是否允许重复执行服务任务,也仍然是平台层的责任。

本文以 Flowable 8.0.0 为源码基线。Flowable 8 已切换到 Spring Framework 7、Spring Boot 4 和 Jackson 3,升级旧项目时应先核对运行环境。

一、核心结论与问题边界

先给结论:Flowable 已经内置状态迁移 API

一个普通节点跳转可以压缩成下面几行:

java 复制代码
Task task = taskService.createTaskQuery()
    .taskId(taskId)
    .singleResult();

runtimeService.createChangeActivityStateBuilder()
    .moveExecutionToActivityId(task.getExecutionId(), targetActivityId)
    .changeState();

这段代码表达的是:只移动当前任务对应的那个 Execution 到目标活动。如果当前节点处在会签或并行分支中,这种 execution 级 API 通常比 activityId 级 API 更精确。

图 1:Flowable 把节点跳转实现成公开 Builder API,底层仍然进入 Command、DynamicStateManager 与 Agenda。

最新源码里,调用链到底经过了什么

Flowable 8.0.0 的公开入口是 ChangeActivityStateBuilder。源码注释直接说明,它是"改变流程实例状态"的辅助构建器,通过 RuntimeService.createChangeActivityStateBuilder() 获得。

源码类 主要职责 跳转中的作用
RuntimeService 暴露流程实例运行时 API 创建状态迁移 Builder
ChangeActivityStateBuilderImpl 收集 execution、activity、变量和目标映射 描述"从哪里移动到哪里"
RuntimeServiceImpl 把 Builder 交给 CommandExecutor 进入引擎事务
ChangeActivityStateCmd 校验参数并取得 DynamicStateManager 状态迁移命令入口
DefaultDynamicStateManager 解析移动容器和活动结构 把业务请求转换成 Execution 集合
AbstractDynamicStateManager 删除旧状态、重建作用域和新执行 完成真正的执行树迁移
Agenda 安排 Continue Process 操作 执行目标 ActivityBehavior

源码调用顺序如下:

text 复制代码
RuntimeService.createChangeActivityStateBuilder()
  -> ChangeActivityStateBuilderImpl.changeState()
  -> RuntimeServiceImpl.changeActivityState(...)
  -> CommandExecutor.execute(new ChangeActivityStateCmd(...))
  -> DynamicStateManager.moveExecutionState(...)
  -> AbstractDynamicStateManager.doMoveExecutionState(...)
  -> Agenda.planContinueProcessOperation(newExecution)

ChangeActivityStateCmd 有两项关键校验:没有配置任何 move/enable 操作会直接报错;使用 activityId 级迁移或启用事件子流程时必须提供 processInstanceId。executionId 级迁移可以从 execution 自身解析流程实例,因此不强制设置流程实例 ID。

二、关键概念与能力差异

六类核心 API 应该怎么选

Flowable 的状态迁移 API 本质上处理三种令牌关系:一对一、多对一和一对多;每种关系又可以按 executionId 或 activityId 指定来源。

图 2:先判断要移动几个运行时令牌,再选择 executionId 还是 activityId。

迁移意图 推荐 API 典型场景
单个令牌 → 单个节点 moveExecutionToActivityId 普通驳回、只移动某个会签参与人
当前活动 → 单个节点 moveActivityIdTo 单实例流程的节点前进或后退
多个令牌 → 单个节点 moveExecutionsToSingleActivityId 收拢指定并行分支
多个活动 → 单个节点 moveActivityIdsToSingleActivityId 按活动批量收拢整个 Fork 区域
单个令牌 → 多个节点 moveSingleExecutionToActivityIds 从一个执行令牌人工拆出多个分支
当前活动 → 多个节点 moveSingleActivityIdToActivityIds 按活动批量创建多个目标分支

还有三类跨作用域 API:moveActivityIdToParentActivityId 用于从被调用流程返回父流程;moveActivityIdToSubProcessInstanceActivityId 用于从父流程进入新的被调用流程实例;enableEventSubProcessStartEvent 用于启用事件子流程启动事件。

普通用户任务如何安全跳到指定节点

应用层不应直接让前端提交一个 activityId 后立即执行。推荐把权限、目标计算、幂等和业务审计封装在服务中,再调用 Flowable API。

java 复制代码
@Transactional
public JumpResult jumpUserTask(JumpRequest request) {
    Task task = taskService.createTaskQuery()
        .taskId(request.taskId())
        .active()
        .singleResult();
    if (task == null) {
        throw new IllegalStateException("任务不存在或已经处理");
    }

    jumpPolicy.checkOperator(request.operatorId(), task);
    jumpPolicy.checkTarget(task, request.targetActivityId());
    idempotencyService.reserve(request.idempotencyKey());

    runtimeService.createChangeActivityStateBuilder()
        .moveExecutionToActivityId(
            task.getExecutionId(), request.targetActivityId())
        .processVariable("lastJumpOperator", request.operatorId())
        .processVariable("lastJumpReason", request.reason())
        .changeState();

    operationLog.save(ProcessOperation.jump(request, task));
    return new JumpResult(task.getProcessInstanceId(),
        task.getTaskDefinitionKey(), request.targetActivityId());
}

示例把操作者和原因放入流程变量只是为了说明 Builder 支持同步设置变量。生产系统中,完整操作日志仍应保存到业务审计表:流程变量会被覆盖,也不适合承担不可篡改的多次操作轨迹。

三、模型架构与运行机制

为什么 executionId 通常比 activityId 更安全

moveActivityIdTo(currentActivityId, targetActivityId) 看起来更简单,但它的来源是"流程实例内当前位于这个 activityId 的活动执行"。在多实例和并发结构中,一个 activityId 可能对应多个 execution。

Flowable 8 的 resolveMoveExecutionEntityContainers 会查询流程实例的子执行,按 activityId 筛选,再识别多实例根、父 execution 和多实例作用域。官方多实例测试也证明:在多实例活动中按 activityId 移动,可能迁移该活动下的全部相关实例;按 executionId 则可以只移动指定实例。

因此建议:

  • 由某条具体待办触发的驳回、跳转,优先使用 task.getExecutionId()
  • 管理员要整体收拢某个活动时,才使用 activityId 级 API。
  • 并行或会签页面必须展示"移动当前分支"还是"移动全部分支",不能共用一个模糊按钮。
  • 请求到达服务端后重新查询 Task 和 Execution,不能相信前端缓存的运行时 ID。

源码如何删除旧状态并创建新状态

DefaultDynamicStateManager.moveExecutionState 先把 Builder 中的请求解析为 MoveExecutionEntityContainer,再构造 ProcessInstanceChangeState 并调用 doMoveExecutionState。真正迁移时,源码大致执行以下步骤:

  1. 查找流程实例和当前活动的 Execution,设置本次迁移携带的流程变量。
  2. 解析目标 FlowElement、父子流程层级、多实例结构和 Call Activity 信息。
  3. 删除当前 execution 的子执行、任务、作业和事件订阅,并写入"Change activity to ..."删除原因。
  4. 如果离开子流程,向上删除不再需要的父作用域;如果进入子流程,重新创建嵌入式作用域层级。
  5. 为每个目标 FlowElement 创建新的 child execution,并设置局部变量。
  6. 重建目标节点的边界事件;异步任务创建 Job,普通节点交给 Agenda 继续执行。
  7. 处理待启用的事件子流程启动事件。

图 3:Flowable 不是修改旧 Task 的节点编号,而是取消旧状态并按目标 BPMN 重新进入。

这个过程解释了三个现象:跳转后的 taskId 通常是新的;目标用户任务的分配逻辑和任务监听器会重新执行;目标节点带有定时器、消息或信号边界事件时,相应运行时数据会重新创建。

四、核心场景与处理策略

前进跳转、后退跳转和跳过节点有区别吗

从引擎 API 看,moveExecutionToActivityId 并不要求目标一定在历史路径上,也不要求目标是前驱节点。因此前进、后退和跨节点在调用形式上相同。

区别来自业务语义:

  • 后退到已办节点:通常需要重新生成待办,并保留上一轮历史,不能修改旧历史记录。
  • 前进跳过审批:必须记录跳过原因,并检查被跳过节点是否承担必需控制。
  • 跳到服务任务:可能再次调用支付、发货或外部接口,必须有业务幂等。
  • 跳到结束事件:可能直接结束流程,需要同步业务单据状态。
  • 跳到网关:网关依赖令牌数量、入口路径和条件,不能只看目标 ID。

平台层应维护节点操作白名单,例如 canJumpFromcanJumpTo、允许角色、是否允许跨作用域、是否允许触发外部副作用。Flowable 负责执行迁移,不负责替企业决定迁移是否合理。

并行网关应该怎样移动令牌

并行网关最常见错误,是只把一个分支跳到 Join,却期待整个并行区域立即结束。Join 等待的是满足同步条件的令牌,不是某个任务的完成标志。

图 4:并行场景的核心不是节点名称,而是迁移前后应该保留几个 Execution。

java 复制代码
// 一个令牌拆为两个分支
runtimeService.createChangeActivityStateBuilder()
    .moveSingleExecutionToActivityIds(
        sourceExecutionId, List.of("legalTask", "financeTask"))
    .changeState();

// 两个活动的令牌收拢到并行区域之后
runtimeService.createChangeActivityStateBuilder()
    .processInstanceId(processInstanceId)
    .moveActivityIdsToSingleActivityId(
        List.of("legalTask", "financeTask"), "archiveTask")
    .changeState();

Flowable 官方 ChangeStateForGatewaysTest 同时覆盖了 activityId 和 executionId 两套一对多、多对一迁移。测试还演示了把多个分支移动到并行 Join,只有令牌数量和作用域正确时,网关才能继续流转。

五、数据、规则与状态设计

多实例会签怎样处理

多实例活动至少包含 MI Root、实例 execution、任务和计数变量。产品首先要区分三个动作:

产品动作 运行时意图 建议实现
当前审批人退回 只移动当前实例 使用当前 Task 的 executionId
整组会签退回 退出全部实例并回到前置节点 收集相关 execution 或按 activityId 整体迁移
会签中加人/减人 增减实例,不改变主路径 使用多实例增减 API,而不是节点跳转

Flowable 8 的解析代码会识别 isMultiInstanceRoot()、多实例父活动和同一父 execution 下的实例,并为不同父作用域建立独立移动容器。官方 ChangeStateForMultiInstanceTest 覆盖了顺序/并行多实例、嵌套多实例子流程和网关嵌套场景。

这并不意味着会签可以不测试。迁移后仍应校验 nrOfInstancesnrOfActiveInstancesnrOfCompletedInstances、completionCondition、局部变量和业务投票结果。引擎保证运行时结构迁移,企业要保证"本轮会签是否作废、重开还是保留已有票数"的业务语义。

子流程和 Call Activity 如何跨层跳转

嵌入式 SubProcess 与 Call Activity 不是一回事。嵌入式子流程仍在同一个流程实例和定义中,Flowable 的普通 activity/execution 迁移可以在进入或退出时重建对应作用域。

Call Activity 会启动独立的被调用流程实例。Flowable 为此提供专门 API:

java 复制代码
// 从被调用流程退出,回到父流程节点
runtimeService.createChangeActivityStateBuilder()
    .processInstanceId(childProcessInstanceId)
    .moveActivityIdToParentActivityId("childTask", "parentReview")
    .changeState();

// 从父流程节点进入新的被调用流程实例
runtimeService.createChangeActivityStateBuilder()
    .processInstanceId(parentProcessInstanceId)
    .moveActivityIdToSubProcessInstanceActivityId(
        "parentReview", "childTask", "callActivity")
    .changeState();

第二个 API 还有指定子流程定义版本的重载。Flowable 官方 Call Activity 测试验证了父子流程间迁移、calledElement 表达式、变量解析和指定定义版本。跨流程跳转必须使用这些 API,不能把子流程 activityId 当作父流程普通节点直接移动。

六、工程实现与系统集成

历史、监听器和操作日志会发生什么

状态迁移使用"取消源活动并启动目标活动"的语义,而不是把源任务正常 complete。Flowable 网关测试会断言 ACTIVITY_CANCELLED 事件;动态状态管理器删除旧执行时也传入 Change activity to ... 原因。

企业项目需要特别检查:

  • 源任务的 delete/取消监听器是否会执行,是否错误触发正常完成逻辑。
  • 源活动历史是否闭合,目标活动是否生成新一轮历史。
  • 目标节点的 create/assignment 监听器是否具有幂等性。
  • 异步服务任务是否生成新 Job,失败重试会不会重复外部副作用。
  • 流程变量和局部变量是否要继承、覆盖或重新计算。

审批意见、操作人、源节点、目标节点、理由、流程定义版本和幂等键应保存到独立业务审计表。历史表适合还原引擎轨迹,但不能替代企业操作日志。

并发、权限和事务怎样设计

节点跳转与正常完成可能同时发生。应用服务应在调用前检查活动任务,在引擎事务内再次依赖实体版本做乐观锁控制;遇到 FlowableOptimisticLockingException 时返回"任务状态已变化",不要自动重复跳转。

建议至少增加以下控制:

  • 服务器根据当前模型、历史路径和角色重新计算允许目标。
  • 接口使用幂等键,阻止重复点击和网关重放。
  • 前端只展示候选目标,不拥有最终授权。
  • 流程库与业务库同源时使用本地事务;异库时使用 Outbox 或可靠事件。
  • 跳到 Service Task、Send Task、结束事件前执行高风险节点检查。
  • 每次迁移记录快照,包括活动 executionId 集合和迁移后的任务集合。

七、安全、性能与治理要求

低代码平台如何封装成可配置能力

在云程低代码开发平台这类产品中,不应把 ChangeActivityStateBuilder 直接暴露给页面。设计器可以为节点配置"允许退回范围、是否可跳过、是否允许跨子流程、操作角色";运行时策略层把用户动作转换为 executionId/activityId 集合;Flowable 适配层选择一对一、多对一或一对多 API;审计层统一记录结果。

一个实用的候选目标策略是:默认只允许历史经过的人工节点;管理员纠偏可使用显式白名单;多实例、并行和 Call Activity 只开放专用操作。这样既利用 Flowable 强大的状态迁移能力,又不会把底层技术能力变成不受控的"万能跳转"。

八、案例实践与迁移路线

必须覆盖哪些集成测试

用例 必查结果
普通 B → A 后退 旧任务取消,A 新任务创建,历史产生新一轮
普通 A → C 前进 B 未创建,跳过理由被记录
带边界定时器的节点 源 Timer 消失,目标 Timer 按模型创建
单个并行分支移动 其他分支保持活动,Join 等待关系正确
多分支收拢 所选 execution 被取消,只留下目标令牌
单个会签实例退回 其他实例和 MI 计数符合业务策略
整组会签退回 MI Root、任务和变量无残留
进入/退出嵌入式子流程 父作用域、局部变量和边界事件正确
Call Activity 跨流程 父子实例关系、版本和历史正确
完成与跳转并发 只有一方提交,另一方得到状态冲突
目标异步服务任务 Job、重试和业务幂等正确

不要只断言待办数量。还要检查 Execution 树、任务、作业、定时器、事件订阅、变量、历史活动、历史任务和业务单据状态。

1. 常见错误

  • 直接修改 ACT_RU_EXECUTION.ACT_ID_ 或复制旧版 Activiti 的自定义跳转代码。
  • 在会签或并行场景中默认使用 moveActivityIdTo,误把全部实例一起迁移。
  • 只移动一个分支到 Join,却期待网关自动越过其他分支。
  • 把旧 Task 的 taskDefinitionKey 改成目标节点,继续复用 taskId。
  • 允许前端提交未校验的任意目标 ID。
  • 跳到服务任务前没有检查外部副作用和幂等。
  • 把流程历史表当作业务操作日志。
  • 捕获引擎异常后仍提交业务状态。
  • 只测试普通节点,没有测试定时器、事件、子流程和并发。

九、平台落地、测试与选型

标准答案:Flowable 任意节点跳转怎么做

**普通任务跳转:**查询当前 Task,取得 executionId,调用 runtimeService.createChangeActivityStateBuilder().moveExecutionToActivityId(executionId, targetActivityId).changeState()

**并行或多实例:**先明确要移动一个令牌、多个令牌还是拆出多个令牌,再分别选择 moveExecutionToActivityIdmoveExecutionsToSingleActivityIdmoveSingleExecutionToActivityIds;需要按活动批量操作时使用对应 activityId 版本。

**跨父子流程:**嵌入式子流程可由动态状态管理器重建作用域;Call Activity 应使用 moveActivityIdToParentActivityIdmoveActivityIdToSubProcessInstanceActivityId

Flowable 8.0.0 底层会通过 ChangeActivityStateCmdDynamicStateManager 取消旧状态、重构 Execution 树、重建边界事件,并由 Agenda 启动目标节点。企业仍需在 API 外增加权限、目标白名单、幂等、业务审计和完整集成测试。

相关推荐
Behavior5 天前
一条指令,让 Claude Code 每天定时帮你追热点:动态工作流Workflows从 0 到 1 全流程
claude·workflow·工作流引擎
每天都是不一样的太阳8 天前
别让 AI Agent 先画靶再射箭:一套「结论忠于数据」的证据链工作流
agent·工作流引擎
怕浪猫10 天前
Agent 编排 Agent:DeepSeek Harness 的子代理与工作流系统有多强
agent·工作流引擎·deepseek
腾讯云开发者10 天前
一位教授与WorkBuddy的几个月,看看擦出了什么样的火花
工作流引擎
愚农搬码12 天前
Flowable会签实现:多实例任务的底层原理
工作流引擎
玹外之音21 天前
别做"AI 翻译"了,做"AI 本地化":跨境电商 Listing 系统的工程实践
aigc·工作流引擎·aiops
梦想很大很大21 天前
Workrun 进度更新:从理念到解决具体问题
agent·workflow·工作流引擎
愚农搬码1 个月前
BPMN.js 自定义工具栏、节点图标和右键菜单
工作流引擎
大龄码农有梦想1 个月前
仿钉钉流程模型如何转换成 BPMN 2.0?
工作流引擎·流程引擎·bpmn.js·oa·流程审批·仿钉钉流程设计器·bpmn流程设计器