Activiti/BPMN 2.0 的 4 种网关

1.Activiti/BPMN 2.0 中主要有 4 种网关

网关类型 BPMN 符号 语义
排他网关 (Exclusive Gateway) 菱形 X 多条出口连线,按顺序评估条件,第一条为 true 的走。如果多条为 true,只走第一条(文档化的 XML 顺序)。必须有一条 true。
并行网关 (Parallel Gateway) 菱形 + 分叉 :所有出口全部并行执行,不评估条件。汇聚:等待所有进入的并行分支到达后,才继续往下走。
包容网关 (Inclusive Gateway) 菱形 O 分叉 :评估条件,走所有结果为 true 的出口。汇聚:等待所有"实际被激活"的进入分支到达。
事件网关 (Event-based Gateway) 菱形 五边形 基于事件(消息/定时器)做路由,谁先触发谁决定方向。

2.OA项目中具体业务场景

在 OA 审批流程(请假、加班、离职等)中,经常遇到一个需求:根据审批人的角色自动跳过某些节点。典型场景有三个:

  1. 发起人自动跳过第一个节点:如果发起人本身就具有第一个审批节点的角色(如发起人是 HR),就没必要自己审批自己,直接通过。
  2. 审批人兼岗自动跳过下一个节点:如果当前审批人同时拥有下一个节点的角色(如一个人既是 HR 又是 Manager),在当前节点审批通过后,下一个节点也自动通过。
  3. 最高角色自动结束流程:如果发起人是管理这种最高级别角色,整个流程不需要审批,自动结束。

演化过程

方案一:结束 n 个任务(最早的做法)

最早期的实现方式是:在 BPMN 图上画一个直链,审批通过时在代码中直接 completeTask() n 次,从而"跳过"中间节点。

java 复制代码
// 伪代码示意

taskService.complete(taskId1, variables); // 通过节点1

taskService.complete(taskId2, variables); // 立即通过节点2

问题

  • ACT_HI_TASKINST 中产生大量自动完成的"幽灵任务",历史数据不干净
  • 图的执行路径和实际业务逻辑脱节
  • 审计追溯时,难以区分"人工审批通过"和"自动跳过"
  • 跳过的逻辑全部隐藏在 Java 代码里,换一个人看不懂流程到底怎么走的

方案二:直连线 + 条件表达式(笔者项目中当前方案)

在 BPMN 图上,从一个 UserTask 伸出多条条件直连线,每条线上配置不同的条件表达式。Activiti 在完成任务时,根据流程变量评估条件,决定走哪条线。

复制代码

┌── 条件A(下一节点角色) ──→ [Node1]

[UserTask] ──┤

└── 条件B(跳过) ──→ [Node2]

条件表达式示例:

XML 复制代码
<sequenceFlow id="flow1" sourceRef="hrTask" targetRef="managerTask">

<conditionExpression xsi:type="tFormalExpression">

<![CDATA[${nextRole == 'manager'}]]>

</conditionExpression>

</sequenceFlow>

<sequenceFlow id="flow2" sourceRef="hrTask" targetRef="endEvent">

<conditionExpression xsi:type="tFormalExpression">

<![CDATA[${nextRole == 'end'}]]>

</conditionExpression>

</sequenceFlow>
核心组件
组件 职责
TaskListener 在任务创建/完成时触发,计算角色归属,设置流程变量
流程变量 NEXT_ROLE_KEYAPPROVER_ROLE_KEY 等,存储"下一步该往哪走"
条件直连线 在 BPMN 图上可视化分支,根据变量值路由
三个场景的具体实现

场景 1:发起人自动跳过第一个节点

java 复制代码
AutoCompleteFirstTaskForStarterListener(TaskListener,create 事件):


// 1. 获取当前任务的候选组(角色)

Set<String> candidateGroupIds = candidates.stream()

.filter(link -> link.getGroupId() != null)

.map(IdentityLink::getGroupId)

.collect(Collectors.toSet());

// 2. 获取流程发起人

String startUserId = getProcessInstanceStartUserId(processInstanceId);

// 3. 判断发起人是否属于任一候选组

boolean isInCandidateGroup = candidateGroupIds.stream()

.anyMatch(groupId -> userInGroup(startUserId, groupId));

// 4. 是 → 自动完成该任务,设置下一角色变量

if (isInCandidateGroup) {

completeTaskBo.getVariables()

.put(NEXT_ROLE_KEY, ApproverRoleEnum.MANAGER.getCode());

flowTaskService.completeTask(completeTaskBo);

}

场景 2:审批人兼岗自动跳过下一个节点

java 复制代码
MergeCandidateGroupTaskWithBusinessListener(TaskListener,create 事件):


// 1. 获取上一个任务的办理人

String previousAssignee = getPreviousTaskAssignee(delegateTask);

// 2. 判断该办理人是否在当前任务的候选组中

boolean isInCandidates = previousAssigneeInCurrentCandidates(

delegateTask, previousAssignee, accountSetsId);

// 3. 是 → 自动完成当前任务(同一个人的下一个节点自动通过)

if (isInCandidates) {

runMergedBusiness(delegateTask, taskId, processInstanceId, previousAssignee);

}

场景 3:最高角色自动结束流程

结合前两个 Listener,在自动完成任务时判断:

java 复制代码
String nextTaskId = flowTaskService.getCurrentTaskId(processInstanceId);

if (StrUtil.isBlank(nextTaskId)) {

// 没有下一个任务 → 流程自动结束,更新业务状态为"已完成"

}
变量流转示意

以请假流程为例:

  1. 发起人(同时也是 HR)提交请假申请
  2. 流程启动,创建第一个任务(HR 审批节点)
  3. AutoCompleteFirstTaskForStarterListener 触发
    • 判断发起人在 HR 候选组中 → 自动完成
    • 设置 NEXT_ROLE_KEY = 'manager'
  4. token 走到条件直连线
    • 条件 nextRole == 'manager' → 走 HR→Manager 线
    • 条件 nextRole == 'end' → 不走
  5. 创建 Manager 审批任务
  6. Manager 审批通过 → 流程结束
优势
  • BPMN 图可读:条件线画在图上,谁都能看明白分支逻辑
  • 历史数据干净 :跳过的节点根本不产生任务实例,ACT_HI_TASKINST 只记录真实经过的节点
  • 路由逻辑集中 :条件表达式写在 BPMN 的 conditionExpression 中,变量由 Listener 统一计算
  • 灵活适配:角色判断逻辑在 Java 代码中(查数据库、查角色交叠),不受 BPMN 条件表达式能力限制

方案三:网关 + 条件表达式(对比方案)

复制代码

┌── 条件A ──→ [Node1]

[UserTask] → [排他网关]

└── 条件B ──→ [Node2]

网关(Gateway)是 BPMN 2.0 标准的路由节点:

网关类型 语义
排他网关 (ExclusiveGateway) 按顺序评估条件,走第一条 true 的,必须有一条 true
并行网关 (ParallelGateway) 所有出口全部并行执行
包容网关 (InclusiveGateway) 走所有 true 的出口,等待所有激活分支到达再汇聚
直连线 vs 网关对比
维度 直连线 + 条件 排他网关
可视化 ✅ 同样能看到分支 ✅ 多了网关节点,理论上更标准
语义明确性 ⚠️ 依赖引擎实现行为(XML 顺序评估) ✅ BPMN 规范明确定义
无匹配路径时的行为 ⚠️ 不确定(版本相关,笔者项目中此时会报错) ✅ 必须有 default flow 兜底,否则抛异常
条件重新评估 ⚠️ 某些场景 token 可能不重新评估 ✅ 到达网关一定重新评估
代码复杂度 ✅ 和网关方案代码量一样 ❌ 多了一个 BPMN 节点,代码没少
与 TaskListener 配合 ✅ 天然配合 ✅ 同样需要 Listener 算变量
核心结论

从直连线升级到网关,在这个场景里没有实质性的收益。

原因很简单:无论是直连线还是网关,你都需要 TaskListener 来执行"查数据库 → 算角色 → 设置流程变量"的核心逻辑。变量算好了,直连线上的条件表达式和网关出口上的条件表达式没有任何区别------它们判断的是同一个变量。

网关真正的价值体现在:

  • 需要 default flow 兜底的场景(所有条件都不满足时走默认路径)
  • 需要包容网关"走多条路径"的场景(一个节点同时派生出多个并行分支)
  • 需要显式排他语义保证引擎无关性的场景(在多引擎间迁移流程定义)

但这些在我们的 OA 场景中都不构成核心需求。

附:包容网关的潜在价值

我们的方案中有一个场景其实可以受益于包容网关:审批人兼岗自动跳过

当前的实现方式是:完成任务 → 创建下一个任务 → Listener 检测到同一个人 → 自动 complete。这是一个先创建再销毁的过程,不优雅。

如果用包容网关,可以这样设计:

复制代码

[审批节点] → [包容网关]

├── 条件: 需要人工审批 → [下一个任务]

└── 条件: 兼岗跳过 → [自动完成]

包容网关会同时走两条满足条件的路径,然后汇聚到同一终点。不需要先创建任务再销毁,语义更干净。

但这个收益有限,不值得为此对全流程做一次大改。

总结

方案 优点 缺点 适用场景
结束 n 个任务 实现简单 历史数据脏、逻辑不可见、难以维护 快速原型
直连线 + 条件表达式 可视化好、历史干净、灵活 路径较多时线会交叉凌乱 OA 审批流程,角色动态判断
排他网关 + 条件表达式 BPMN 标准语义、兜底流明确 增加节点但代码量不变 需要引擎无关性的场景
包容网关 原生支持并行/多路 汇聚逻辑复杂,容易踩坑 一个节点同时触发多个动作

当前方案(直连线 + 条件表达式)是在"图的可读性"和"代码的灵活性"之间找到了一个很好的平衡点。它不需要额外地引入网关节点,也不需要结束 n 个任务来模拟跳过,是 OA 类流程中最务实的选择。


写在最后:BPMN 规范提供了很多"标准做法",但在实际落地时,"标准"不一定等于"最优"。Activiti 的条件直连线虽然在 BPMN 规范中不如网关语义明确,但在实际运行中行为一致且可靠。与其为了"标准"引入额外的复杂度,不如把精力花在让图清晰、代码可维护、数据干净上。

相关推荐
她的男孩8 小时前
低代码只能做单表 CRUD?我们一行代码没写,搭了个完整进销存
java·后端·架构
147API8 小时前
Claude Tag 进入 Slack 后,团队智能体需要哪些任务与审计字段
java·开发语言·数据库
dyonggan9 小时前
IDEA从零搭建SpringCloud Alibaba完整工程
java·spring cloud·intellij-idea
熬夜苦读学习9 小时前
QT_信号和槽
开发语言·qt
代码雕刻家9 小时前
编程语法细节
java·c语言·开发语言
j7~9 小时前
【C++】C++ 继承全解析:从基本语法到菱形继承的底层原理
开发语言·c++·继承·组合·虚继承·#暑假·七月创作之星博客挑战赛
SimonKing9 小时前
Agnes AI出桌面版了,可图可视频,免费用
java·后端·程序员
数行拙笔9 小时前
C++客户端---String类型
开发语言·c++·bootstrap
影寂ldy9 小时前
C# Task 进阶:WaitAll / WaitAny / WhenAll / WhenAny
开发语言·c#