第1章:流程定义解析——从XML到可执行模型
各位同学,咱们今天聊聊工作流引擎里最基础、也最关键的一环——流程定义解析。
说白了,就是怎么把一张画好的流程图,变成计算机能理解、能执行的数据结构。我做了这么多年低代码平台,发现很多新手一上来就扎进画界面、拖组件,结果到了流程引擎这块就卡住了。嗯,今天咱们就把这块硬骨头啃下来。
1.1 XML解析基础:流程定义的“通用语言”
为什么选XML?你想想看,工作流引擎要跨平台、跨语言,总得有个大家都认的格式吧。XML就是那个“通用语言”。
我在项目中遇到过,有人非要用JSON存流程定义,结果到了要加命名空间、要扩展属性的时候,改得那叫一个痛苦。XML的树形结构天然适合描述流程——节点是活动,边是流转,清清楚楚。
一个典型的流程定义XML长这样:
<process id="leave_approval" name="请假审批流程">
<startEvent id="start" name="开始" />
<userTask id="apply" name="提交申请" assignee="employee">
<extensionElements>
<property name="formKey" value="leave_form" />
</extensionElements>
</userTask>
<sequenceFlow id="flow1" sourceRef="start" targetRef="apply" />
<userTask id="manager_approve" name="经理审批" assignee="manager" />
<sequenceFlow id="flow2" sourceRef="apply" targetRef="manager_approve" />
<endEvent id="end" name="结束" />
<sequenceFlow id="flow3" sourceRef="manager_approve" targetRef="end" />
</process>
看到没?每个元素都有id,这是流程引擎识别节点的唯一标识。我建议你从一开始就养成好习惯——id用有意义的命名,别用uuid,否则调试的时候你会想哭。
1.2 DOM与SAX解析对比:选对工具,事半功倍
好,XML有了,怎么读?Java里主流就两种:DOM和SAX。咱们来掰扯掰扯。
| 对比维度 | DOM解析 | SAX解析 |
|---|---|---|
| 读取方式 | 一次性加载到内存,构建树 | 顺序扫描,事件驱动 |
| 内存占用 | 高(整个文档都在内存里) | 低(只处理当前节点) |
| 访问方式 | 随机访问,任意节点 | 只能顺序访问,不能回头 |
| 修改能力 | 支持增删改 | 只读 |
| 适用场景 | 小文件、需要频繁操作 | 大文件、只需读取 |
我个人习惯是:流程定义文件一般不大(几百KB顶天了),用DOM解析完全够用。而且你想想看,解析完我们还要构建模型、做校验,DOM的树结构直接映射到流程模型,多顺手。
但如果你要解析的是流程日志、历史数据这种动不动几十MB的文件,那还是老老实实用SAX吧。我记得有一次,一个客户导入了三年的流程日志,用DOM解析直接OOM了……嗯,从那以后我养成了先评估文件大小的习惯。
1.3 流程定义模型构建:从XML到Java对象
XML解析完了,拿到的是DOM节点或者SAX事件。接下来要干的事,就是把这些零散的信息组装成我们想要的流程模型。
我一般会设计这么几个核心类:
public class ProcessDefinition {
private String id;
private String name;
private List<FlowNode> nodes; // 所有节点
private List<SequenceFlow> flows; // 所有流转
private Map<String, FlowNode> nodeMap; // id到节点的映射,方便查找
}
public abstract class FlowNode {
private String id;
private String name;
private String type; // startEvent, userTask, endEvent等
}
public class SequenceFlow {
private String id;
private String sourceRef; // 来源节点id
private String targetRef; // 目标节点id
private String conditionExpression; // 条件表达式,用于排他网关
}
构建的过程其实就是一个遍历+映射的过程。用DOM解析的话,大概是这样:
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(xmlFile);
Element root = doc.getDocumentElement();
NodeList nodeList = root.getChildNodes();
ProcessDefinition processDef = new ProcessDefinition();
processDef.setId(root.getAttribute("id"));
processDef.setName(root.getAttribute("name"));
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (node instanceof Element) {
Element elem = (Element) node;
String tagName = elem.getNodeName();
if ("startEvent".equals(tagName)) {
// 构建开始节点
} else if ("userTask".equals(tagName)) {
// 构建用户任务节点
} else if ("sequenceFlow".equals(tagName)) {
// 构建流转
}
}
}
1.4 流程定义校验:别让错误跑上线
模型构建完了,别急着用。先校验!我在生产环境见过太多因为流程定义错误导致的诡异bug了。
校验什么?我总结了几条:
- 结构完整性:有没有开始节点?有没有结束节点?没有开始节点,流程怎么启动?没有结束节点,流程永远跑不完。
- 引用正确性:sequenceFlow的sourceRef和targetRef引用的节点id,在节点列表里是否存在?我曾经见过一个case,sourceRef写错了,结果流程走到一半就断了。
- 连通性:从开始节点出发,能不能到达所有节点?有没有死胡同?有没有永远走不到的节点?
- 网关约束:排他网关的分支条件是否互斥?并行网关的汇聚节点数量是否匹配?
校验代码大概长这样:
public class ProcessValidator {
public ValidationResult validate(ProcessDefinition processDef) {
ValidationResult result = new ValidationResult();
// 1. 检查开始和结束节点
if (processDef.getStartEvent() == null) {
result.addError("缺少开始节点");
}
if (processDef.getEndEvent() == null) {
result.addError("缺少结束节点");
}
// 2. 检查引用完整性
for (SequenceFlow flow : processDef.getFlows()) {
if (!processDef.containsNode(flow.getSourceRef())) {
result.addError("流转 " + flow.getId() + " 引用了不存在的源节点");
}
if (!processDef.containsNode(flow.getTargetRef())) {
result.addError("流转 " + flow.getId() + " 引用了不存在的目标节点");
}
}
// 3. 检查连通性(BFS/DFS遍历)
Set<String> reachableNodes = findReachableNodes(processDef);
for (FlowNode node : processDef.getNodes()) {
if (!reachableNodes.contains(node.getId())) {
result.addWarning("节点 " + node.getId() + " 不可达");
}
}
return result;
}
}
好了,这一章的内容就到这里。流程定义解析是整个工作流引擎的基石,这块做扎实了,后面的流程流转、任务分配、历史记录,都会顺很多。下一章咱们聊聊流程实例的创建和执行,到时候你会看到,今天构建的这个模型是怎么“活”起来的。