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 审批流程(请假、加班、离职等)中,经常遇到一个需求:根据审批人的角色自动跳过某些节点。典型场景有三个:
- 发起人自动跳过第一个节点:如果发起人本身就具有第一个审批节点的角色(如发起人是 HR),就没必要自己审批自己,直接通过。
- 审批人兼岗自动跳过下一个节点:如果当前审批人同时拥有下一个节点的角色(如一个人既是 HR 又是 Manager),在当前节点审批通过后,下一个节点也自动通过。
- 最高角色自动结束流程:如果发起人是管理这种最高级别角色,整个流程不需要审批,自动结束。
演化过程
方案一:结束 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_KEY、APPROVER_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)) {
// 没有下一个任务 → 流程自动结束,更新业务状态为"已完成"
}
变量流转示意
以请假流程为例:
- 发起人(同时也是 HR)提交请假申请
- 流程启动,创建第一个任务(HR 审批节点)
AutoCompleteFirstTaskForStarterListener触发- 判断发起人在 HR 候选组中 → 自动完成
- 设置
NEXT_ROLE_KEY = 'manager'
- token 走到条件直连线
- 条件
nextRole == 'manager'→ 走 HR→Manager 线 - 条件
nextRole == 'end'→ 不走
- 条件
- 创建 Manager 审批任务
- 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 规范中不如网关语义明确,但在实际运行中行为一致且可靠。与其为了"标准"引入额外的复杂度,不如把精力花在让图清晰、代码可维护、数据干净上。