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 知识体系总览

下面这张图帮你把今天讲的所有元素串起来,方便你对照记忆:

BPMN XML 核心结构 definitions process 事件 (Event) 任务 (Task) 网关 (Gateway) 开始 / 结束 / 中间 定时 / 消息 / 信号 用户任务 服务任务 排他 / 并行 包含 / 事件 sequenceFlow 连接所有节点,可带 conditionExpression extensionElements 存放引擎专属配置(候选组、Java类等)

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 文件就不会跑偏。