4、流程定义文件(BPMN)详解:流程元素、事件、任务、网关的 XML 结构
BPMN 2.0 的 XML 结构,说白了就是流程引擎能读懂的那张「图纸」。很多初学者对着 .bpmn 文件一头雾水,觉得那一堆尖括号比天书还难懂。其实它的套路非常固定,你只要掌握了核心的几个元素,剩下的全是排列组合。
我个人习惯把 BPMN 的 XML 结构拆成三块来看:流程骨架(definitions + process)、节点(事件/任务/网关)、连线(sequenceFlow)。今天我们就逐个击破。
4.1 文件根结构:definitions 与 process
每个合法的 BPMN 文件,最外层一定是 <definitions>。它就像集装箱,里面装着你所有的流程定义。
<definitions
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
targetNamespace="http://bpmn.io/schema/bpmn"
>
<process id="leaveApproval" name="请假审批流程" isExecutable="true">
<!-- 流程节点和连线写在这里 -->
</process>
<!-- 还可以放 BPMNDiagram(画布布局信息) -->
</definitions>
这里有个关键属性 isExecutable="true"。我在项目中遇到过好几次,流程部署后引擎不执行,排查半天发现是 isExecutable 没写或者写了 false。引擎默认认为这个流程只是「画图用的」,不会去解析它。嗯,这个坑踩过一次就忘不了。
targetNamespace 可以随便填,但建议用你的项目包名或域名,方便区分不同模块的流程定义。
4.2 事件(Event)的 XML 结构
事件是流程的「触发器」。BPMN 里事件分三种:开始、中间、结束。每种事件在 XML 里都有固定的标签。
4.2.1 开始事件
最简单的空开始事件,就一行:
<startEvent id="startEvent1" name="开始"></startEvent>
如果是定时开始事件,需要嵌套一个 <timerEventDefinition>:
<startEvent id="timerStart" name="每日定时触发">
<timerEventDefinition>
<timeCycle>0 0 8 * * ?</timeCycle>
</timerEventDefinition>
</startEvent>
你想想看,为什么定时器要写在事件定义里而不是单独配置?因为 BPMN 标准要求事件的行为必须内聚在事件节点内部,引擎解析时才能「自包含」地理解这个节点该做什么。
4.2.2 结束事件
<endEvent id="endEvent1" name="结束"></endEvent>
如果你想在结束时抛出一个消息,让其他流程订阅,可以这样写:
<endEvent id="msgEnd" name="发送完成通知">
<messageEventDefinition messageRef="leaveApprovedMsg" />
</endEvent>
messageRef 引用的 <message> 定义必须提前在 <definitions> 下的 <itemDefinition> 或 <message> 中声明,否则引擎会报引用错误。
4.3 任务(Task)的 XML 结构
任务是流程里「干活」的节点。最常用的是 用户任务(User Task) 和 服务任务(Service Task)。
4.3.1 用户任务
用户任务需要指定谁来处理。我习惯用 candidateUsers 或 candidateGroups:
<userTask id="approveTask" name="经理审批">
<extensionElements>
<activiti:candidateUsers>zhangsan,lisi</activiti:candidateUsers>
<activiti:candidateGroups>manager</activiti:candidateGroups>
</extensionElements>
</userTask>
这里要注意,extensionElements 是 BPMN 预留的扩展点。不同引擎(Activiti、Flowable、Camunda)的专属配置都塞在这里。我曾经接手过一个项目,他们把 candidateUsers 直接写在 <userTask> 属性里,结果从 Activiti 迁移到 Camunda 时全报错——因为 Camunda 不认那个属性名。
4.3.2 服务任务
服务任务用来调用 Java 类或表达式。比如自动计算请假天数:
<serviceTask id="calcDays" name="计算天数">
<extensionElements>
<activiti:class>com.example.CalculateDaysDelegate</activiti:class>
</extensionElements>
</serviceTask>
或者用表达式调用 Spring Bean:
<serviceTask id="sendEmail" name="发送邮件">
<extensionElements>
<activiti:expression>${emailService.send(execution)}</activiti:expression>
</extensionElements>
</serviceTask>
我个人更推荐用表达式方式,因为 Java 类方式每次修改都要重新编译部署,而表达式可以热更新。
4.4 网关(Gateway)的 XML 结构
网关控制流程的走向。最常用的是排他网关和并行网关。
4.4.1 排他网关(Exclusive Gateway)
排他网关就是「二选一」或「多选一」。它的 XML 很简单,但真正的逻辑写在连线的 conditionExpression 里:
<exclusiveGateway id="gateway1" name="天数判断" />
<sequenceFlow id="flow1" sourceRef="gateway1" targetRef="directorApprove">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${days > 3}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow2" sourceRef="gateway1" targetRef="managerApprove">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${days <= 3}]]>
</conditionExpression>
</sequenceFlow>
这里有个容易忽略的点:排他网关必须保证所有条件互斥,且至少有一个条件为 true。如果所有条件都不满足,引擎会抛出异常。我曾经在线上遇到过这种情况——新加了一个条件分支,但忘了更新默认分支的表达式,结果流程卡死在网关上。
4.4.2 并行网关(Parallel Gateway)
并行网关用来拆分和合并任务。比如审批通过后,同时通知人事和财务:
<parallelGateway id="forkGateway" name="并行分支" />
<parallelGateway id="joinGateway" name="并行合并" />
<sequenceFlow id="toHr" sourceRef="forkGateway" targetRef="notifyHr" />
<sequenceFlow id="toFinance" sourceRef="forkGateway" targetRef="notifyFinance" />
<sequenceFlow id="fromHr" sourceRef="notifyHr" targetRef="joinGateway" />
<sequenceFlow id="fromFinance" sourceRef="notifyFinance" targetRef="joinGateway" />
并行网关的合并节点会等待所有进入的连线都到达后,才继续往下走。如果某个分支一直没完成,流程就会卡住。所以并行网关一定要配合超时机制使用。
4.5 连线(SequenceFlow)的 XML 结构
连线是流程的「血管」,把各个节点串起来。它的结构很简单:
<sequenceFlow id="flow3" name="通过" sourceRef="approveTask" targetRef="endEvent1">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${approved == true}]]>
</conditionExpression>
</sequenceFlow>
注意 sourceRef 和 targetRef 引用的 ID 必须和节点 ID 完全一致,大小写敏感。我见过有人把 startEvent1 写成了 startevent1,结果流程部署成功但启动时报错——因为引擎找不到起始节点。
4.6 知识体系总览
下面这张图帮你把今天讲的所有元素串起来,方便你对照记忆:
4.7 常见元素属性速查表
为了方便你写代码时快速查阅,我把最常用的元素和属性整理成了一张表:
| 元素 | 必填属性 | 常用子元素 | 说明 |
|---|---|---|---|
| startEvent | id | timerEventDefinition / messageEventDefinition | 流程起点,可带触发器 |
| endEvent | id | terminateEventDefinition / messageEventDefinition | 流程终点,可终止或发消息 |
| userTask | id | extensionElements (candidateUsers / candidateGroups) | 人工处理节点 |
| serviceTask | id | extensionElements (class / expression / delegateExpression) | 自动执行节点 |
| exclusiveGateway | id | (无,逻辑在连线上) | 排他分支,条件互斥 |
| parallelGateway | id | (无,靠连线数量控制) | 并行分支,等待所有入线 |
| sequenceFlow | id, sourceRef, targetRef | conditionExpression | 节点间的连线 |
核心要点: BPMN 的 XML 结构其实就三个层次——definitions 包 process,process 里放节点和连线,节点通过 extensionElements 扩展引擎行为。你只要记住这个三层模型,写出来的 .bpmn 文件就不会跑偏。