2022年6月,由于某个项目建设的要求,需要从Excel中读取流程数据并且自动生成遵循BPMN标准的流程图,以用于作业处理,目前支持这些流程图的主流开源框架有Activiti、Flowable、Camunda。由于没有在网上搜索到现成的方案,于是自己通过分析BPMN文件结构,设计了一套实现方案,可能绘制的流程图并不是很完美,但在工作中比较好的实现了预期的效果,这里做个记录以备后续学习改进。
1、原理
以Camunda Modeler绘制的流程图为例,Camunda流程图遵从BPMN2.0标准,流程描述文档为xml格式,打开一个bpmn文件的文本格式,其中的xml结构主要有两部分:process和bpmndi:BPMNDiagram,其中process为各个节点的属性和前后关系。sequenceFlow中有描述sourceRef和targetRef,表示连接线的两端,exclusiveGateway中有描述incoming和outgoing,用于表示流入和流出。
要生成流程图,想办法按格式生成xml文件即可。
以下是一个流程图的样例:
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0mfbluk" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.9.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.1.0"><bpmn:process id="Process_0hmfwtd" isExecutable="true"><bpmn:startEvent id="StartEvent_1"><bpmn:outgoing>Flow_0w2j9lb</bpmn:outgoing></bpmn:startEvent><bpmn:sequenceFlow id="Flow_0w2j9lb" sourceRef="StartEvent_1" targetRef="Activity_0mwthrf" /><bpmn:sequenceFlow id="Flow_1ez6hz2" sourceRef="Activity_0mwthrf" targetRef="Activity_18ebup0" /><bpmn:userTask id="Activity_0mwthrf" name="节点1"><bpmn:incoming>Flow_0w2j9lb</bpmn:incoming><bpmn:outgoing>Flow_1ez6hz2</bpmn:outgoing></bpmn:userTask><bpmn:userTask id="Activity_18ebup0" name="节点2"><bpmn:incoming>Flow_1ez6hz2</bpmn:incoming><bpmn:outgoing>Flow_1jvovvr</bpmn:outgoing></bpmn:userTask><bpmn:sequenceFlow id="Flow_1jvovvr" sourceRef="Activity_18ebup0" targetRef="Activity_1u98wuw" /><bpmn:userTask id="Activity_1u98wuw" name="节点3"><bpmn:incoming>Flow_1jvovvr</bpmn:incoming><bpmn:outgoing>Flow_1doeb9v</bpmn:outgoing></bpmn:userTask><bpmn:task id="Activity_0my6802" name="节点4"><bpmn:incoming>Flow_1doeb9v</bpmn:incoming><bpmn:outgoing>Flow_0v77htt</bpmn:outgoing></bpmn:task><bpmn:sequenceFlow id="Flow_1doeb9v" sourceRef="Activity_1u98wuw" targetRef="Activity_0my6802" /><bpmn:exclusiveGateway id="Gateway_13kpjty"><bpmn:incoming>Flow_0v77htt</bpmn:incoming><bpmn:outgoing>Flow_0rx278g</bpmn:outgoing><bpmn:outgoing>Flow_0ivytvj</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:sequenceFlow id="Flow_0v77htt" sourceRef="Activity_0my6802" targetRef="Gateway_13kpjty" /><bpmn:sequenceFlow id="Flow_0rx278g" name="分支1" sourceRef="Gateway_13kpjty" targetRef="Activity_0u0ewsu" /><bpmn:userTask id="Activity_0u0ewsu" name="节点5"><bpmn:incoming>Flow_0rx278g</bpmn:incoming><bpmn:outgoing>Flow_0fvhke0</bpmn:outgoing></bpmn:userTask><bpmn:task id="Activity_076z7o7" name="节点6"><bpmn:incoming>Flow_0fvhke0</bpmn:incoming><bpmn:incoming>Flow_1hg2jlc</bpmn:incoming><bpmn:outgoing>Flow_19hca6k</bpmn:outgoing></bpmn:task><bpmn:sequenceFlow id="Flow_0fvhke0" sourceRef="Activity_0u0ewsu" targetRef="Activity_076z7o7" /><bpmn:exclusiveGateway id="Gateway_1meq3pj"><bpmn:incoming>Flow_19hca6k</bpmn:incoming><bpmn:outgoing>Flow_0om5nu7</bpmn:outgoing><bpmn:outgoing>Flow_101go1a</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:sequenceFlow id="Flow_19hca6k" sourceRef="Activity_076z7o7" targetRef="Gateway_1meq3pj" /><bpmn:task id="Activity_0slozxh" name="节点7"><bpmn:incoming>Flow_0om5nu7</bpmn:incoming><bpmn:outgoing>Flow_1hg2jlc</bpmn:outgoing></bpmn:task><bpmn:sequenceFlow id="Flow_0om5nu7" name="分支1" sourceRef="Gateway_1meq3pj" targetRef="Activity_0slozxh" /><bpmn:sequenceFlow id="Flow_1hg2jlc" sourceRef="Activity_0slozxh" targetRef="Activity_076z7o7" /><bpmn:task id="Activity_1qisdls" name="节点8"><bpmn:incoming>Flow_101go1a</bpmn:incoming><bpmn:incoming>Flow_1449zkx</bpmn:incoming><bpmn:outgoing>Flow_02gk645</bpmn:outgoing></bpmn:task><bpmn:sequenceFlow id="Flow_101go1a" name="分支2" sourceRef="Gateway_1meq3pj" targetRef="Activity_1qisdls" /><bpmn:exclusiveGateway id="Gateway_0flehn2"><bpmn:incoming>Flow_02gk645</bpmn:incoming><bpmn:outgoing>Flow_0tdyk61</bpmn:outgoing><bpmn:outgoing>Flow_154cjgf</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:sequenceFlow id="Flow_02gk645" sourceRef="Activity_1qisdls" targetRef="Gateway_0flehn2" /><bpmn:task id="Activity_0un6cyt" name="节点9"><bpmn:incoming>Flow_0tdyk61</bpmn:incoming><bpmn:outgoing>Flow_1449zkx</bpmn:outgoing></bpmn:task><bpmn:sequenceFlow id="Flow_0tdyk61" name="分支1" sourceRef="Gateway_0flehn2" targetRef="Activity_0un6cyt" /><bpmn:task id="Activity_05jsz8l" name="节点10"><bpmn:incoming>Flow_154cjgf</bpmn:incoming><bpmn:outgoing>Flow_15aey6d</bpmn:outgoing></bpmn:task><bpmn:sequenceFlow id="Flow_154cjgf" name="分支2" sourceRef="Gateway_0flehn2" targetRef="Activity_05jsz8l" /><bpmn:task id="Activity_1hwf9iy" name="节点11"><bpmn:incoming>Flow_15aey6d</bpmn:incoming><bpmn:outgoing>Flow_0cwzv60</bpmn:outgoing></bpmn:task><bpmn:sequenceFlow id="Flow_15aey6d" sourceRef="Activity_05jsz8l" targetRef="Activity_1hwf9iy" /><bpmn:task id="Activity_08zhct0" name="节点12"><bpmn:incoming>Flow_0cwzv60</bpmn:incoming><bpmn:outgoing>Flow_0b4ac3v</bpmn:outgoing></bpmn:task><bpmn:sequenceFlow id="Flow_0cwzv60" sourceRef="Activity_1hwf9iy" targetRef="Activity_08zhct0" /><bpmn:exclusiveGateway id="Gateway_1x0x5ad"><bpmn:incoming>Flow_0b4ac3v</bpmn:incoming><bpmn:outgoing>Flow_0dvvwp6</bpmn:outgoing><bpmn:outgoing>Flow_0josnvh</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:sequenceFlow id="Flow_0b4ac3v" sourceRef="Activity_08zhct0" targetRef="Gateway_1x0x5ad" /><bpmn:task id="Activity_1okc7tv" name="节点13"><bpmn:incoming>Flow_0dvvwp6</bpmn:incoming></bpmn:task><bpmn:sequenceFlow id="Flow_0dvvwp6" name="分支1" sourceRef="Gateway_1x0x5ad" targetRef="Activity_1okc7tv" /><bpmn:endEvent id="Event_1vpetqu"><bpmn:incoming>Flow_0josnvh</bpmn:incoming><bpmn:incoming>Flow_0ivytvj</bpmn:incoming></bpmn:endEvent><bpmn:sequenceFlow id="Flow_0josnvh" name="分支2" sourceRef="Gateway_1x0x5ad" targetRef="Event_1vpetqu" /><bpmn:sequenceFlow id="Flow_0ivytvj" name="分支2" sourceRef="Gateway_13kpjty" targetRef="Event_1vpetqu" /><bpmn:sequenceFlow id="Flow_1449zkx" sourceRef="Activity_0un6cyt" targetRef="Activity_1qisdls" /></bpmn:process><bpmndi:BPMNDiagram id="BPMNDiagram_1"><bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0hmfwtd"><bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"><dc:Bounds x="179" y="159" width="36" height="36" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_1eaprsv_di" bpmnElement="Activity_0mwthrf"><dc:Bounds x="270" y="137" width="100" height="80" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_0pu6rnq_di" bpmnElement="Activity_18ebup0"><dc:Bounds x="430" y="137" width="100" height="80" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_1l1fsdt_di" bpmnElement="Activity_1u98wuw"><dc:Bounds x="590" y="137" width="100" height="80" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_0my6802_di" bpmnElement="Activity_0my6802"><dc:Bounds x="750" y="137" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Gateway_13kpjty_di" bpmnElement="Gateway_13kpjty" isMarkerVisible="true"><dc:Bounds x="915" y="152" width="50" height="50" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_1h063h5_di" bpmnElement="Activity_0u0ewsu"><dc:Bounds x="1030" y="137" width="100" height="80" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_076z7o7_di" bpmnElement="Activity_076z7o7"><dc:Bounds x="1200" y="137" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Gateway_1meq3pj_di" bpmnElement="Gateway_1meq3pj" isMarkerVisible="true"><dc:Bounds x="1375" y="152" width="50" height="50" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_0slozxh_di" bpmnElement="Activity_0slozxh"><dc:Bounds x="1500" y="137" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_1qisdls_di" bpmnElement="Activity_1qisdls"><dc:Bounds x="1500" y="360" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Gateway_0flehn2_di" bpmnElement="Gateway_0flehn2" isMarkerVisible="true"><dc:Bounds x="1375" y="375" width="50" height="50" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_0un6cyt_di" bpmnElement="Activity_0un6cyt"><dc:Bounds x="1200" y="360" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_05jsz8l_di" bpmnElement="Activity_05jsz8l"><dc:Bounds x="890" y="360" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_1hwf9iy_di" bpmnElement="Activity_1hwf9iy"><dc:Bounds x="750" y="360" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_08zhct0_di" bpmnElement="Activity_08zhct0"><dc:Bounds x="590" y="360" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Gateway_1x0x5ad_di" bpmnElement="Gateway_1x0x5ad" isMarkerVisible="true"><dc:Bounds x="455" y="375" width="50" height="50" /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Activity_1okc7tv_di" bpmnElement="Activity_1okc7tv"><dc:Bounds x="270" y="360" width="100" height="80" /><bpmndi:BPMNLabel /></bpmndi:BPMNShape><bpmndi:BPMNShape id="Event_1vpetqu_di" bpmnElement="Event_1vpetqu"><dc:Bounds x="179" y="382" width="36" height="36" /></bpmndi:BPMNShape><bpmndi:BPMNEdge id="Flow_0w2j9lb_di" bpmnElement="Flow_0w2j9lb"><di:waypoint x="215" y="177" /><di:waypoint x="270" y="177" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_1ez6hz2_di" bpmnElement="Flow_1ez6hz2"><di:waypoint x="370" y="177" /><di:waypoint x="430" y="177" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_1jvovvr_di" bpmnElement="Flow_1jvovvr"><di:waypoint x="530" y="177" /><di:waypoint x="590" y="177" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_1doeb9v_di" bpmnElement="Flow_1doeb9v"><di:waypoint x="690" y="177" /><di:waypoint x="750" y="177" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0v77htt_di" bpmnElement="Flow_0v77htt"><di:waypoint x="850" y="177" /><di:waypoint x="915" y="177" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0rx278g_di" bpmnElement="Flow_0rx278g"><di:waypoint x="965" y="177" /><di:waypoint x="1030" y="177" /><bpmndi:BPMNLabel><dc:Bounds x="984" y="159" width="28" height="14" /></bpmndi:BPMNLabel></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0fvhke0_di" bpmnElement="Flow_0fvhke0"><di:waypoint x="1130" y="177" /><di:waypoint x="1200" y="177" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_19hca6k_di" bpmnElement="Flow_19hca6k"><di:waypoint x="1300" y="177" /><di:waypoint x="1375" y="177" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0om5nu7_di" bpmnElement="Flow_0om5nu7"><di:waypoint x="1425" y="177" /><di:waypoint x="1500" y="177" /><bpmndi:BPMNLabel><dc:Bounds x="1449" y="159" width="28" height="14" /></bpmndi:BPMNLabel></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_1hg2jlc_di" bpmnElement="Flow_1hg2jlc"><di:waypoint x="1550" y="137" /><di:waypoint x="1550" y="70" /><di:waypoint x="1250" y="70" /><di:waypoint x="1250" y="137" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_101go1a_di" bpmnElement="Flow_101go1a"><di:waypoint x="1400" y="202" /><di:waypoint x="1400" y="270" /><di:waypoint x="1550" y="270" /><di:waypoint x="1550" y="360" /><bpmndi:BPMNLabel><dc:Bounds x="1461" y="252" width="28" height="14" /></bpmndi:BPMNLabel></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_02gk645_di" bpmnElement="Flow_02gk645"><di:waypoint x="1500" y="400" /><di:waypoint x="1425" y="400" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0tdyk61_di" bpmnElement="Flow_0tdyk61"><di:waypoint x="1375" y="400" /><di:waypoint x="1300" y="400" /><bpmndi:BPMNLabel><dc:Bounds x="1324" y="382" width="28" height="14" /></bpmndi:BPMNLabel></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_154cjgf_di" bpmnElement="Flow_154cjgf"><di:waypoint x="1400" y="425" /><di:waypoint x="1400" y="490" /><di:waypoint x="1030" y="490" /><di:waypoint x="1030" y="400" /><di:waypoint x="990" y="400" /><bpmndi:BPMNLabel><dc:Bounds x="1201" y="472" width="28" height="14" /></bpmndi:BPMNLabel></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_15aey6d_di" bpmnElement="Flow_15aey6d"><di:waypoint x="890" y="400" /><di:waypoint x="850" y="400" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0cwzv60_di" bpmnElement="Flow_0cwzv60"><di:waypoint x="750" y="400" /><di:waypoint x="690" y="400" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0b4ac3v_di" bpmnElement="Flow_0b4ac3v"><di:waypoint x="590" y="400" /><di:waypoint x="505" y="400" /></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0dvvwp6_di" bpmnElement="Flow_0dvvwp6"><di:waypoint x="455" y="400" /><di:waypoint x="370" y="400" /><bpmndi:BPMNLabel><dc:Bounds x="399" y="382" width="28" height="14" /></bpmndi:BPMNLabel></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0josnvh_di" bpmnElement="Flow_0josnvh"><di:waypoint x="480" y="425" /><di:waypoint x="480" y="480" /><di:waypoint x="197" y="480" /><di:waypoint x="197" y="418" /><bpmndi:BPMNLabel><dc:Bounds x="325" y="462" width="28" height="14" /></bpmndi:BPMNLabel></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_0ivytvj_di" bpmnElement="Flow_0ivytvj"><di:waypoint x="940" y="202" /><di:waypoint x="940" y="290" /><di:waypoint x="197" y="290" /><di:waypoint x="197" y="382" /><bpmndi:BPMNLabel><dc:Bounds x="555" y="272" width="28" height="14" /></bpmndi:BPMNLabel></bpmndi:BPMNEdge><bpmndi:BPMNEdge id="Flow_1449zkx_di" bpmnElement="Flow_1449zkx"><di:waypoint x="1250" y="360" /><di:waypoint x="1250" y="310" /><di:waypoint x="1550" y="310" /><di:waypoint x="1550" y="360" /></bpmndi:BPMNEdge></bpmndi:BPMNPlane></bpmndi:BPMNDiagram>
</bpmn:definitions>
通过观察xml文件,得出几个重要的结论:
- process节点中描绘的是图形的相对关系,其中sequenceFlow为连接线,sourceRef和targetRef分别表示连接的两端节点编号;
- bpmndi:BPMNDiagram节点中描绘的是图形的绝对位置,通过坐标的方式定位图形位置。bpmndi:BPMNShape为节点的描述,bpmndi:BPMNEdge为连接线的描述,可以看出这两者共同组成了计算机中典型数据结构:“图”,shape和edge分别对应图的顶点和边,这种带有各种属性,并且可以随意跳转分支的图,可以视为加权网状图;
- 节点坐标位置为包围节点的外围矩形(示意图中的虚线框)的左上角位置,如下图所示:
(1) 矩形节点坐标示意图:
(2) 圆形节点坐标示意图:
(3) 网关节点坐标示意图:
2、方案
数据从Excel读取之后,每一行都对应一个节点,先根据节点顺序排列,一行最多显示10个节点(节点间距默认66,行间距默认300),依次遍历excel数据,达到10个则换行,设置坐标。
记录两份数据,通过map存储:节点坐标信息、sequenceFlow连接线信息,包括连接两端的source、target。
● 根据xml的结构可以看出,process节点中的基本信息可以比较容易的进行拼接,比较有难度的是bpmndi:BPMNDiagram,因为其中涉及到每个节点的坐标、节点间的连接线,还需要考虑到网关的跳转;
● 先根据节点坐标信息初始化每个节点的位置,并设置到“bpmndi:BPMNShape”中;
● 再根据sequenceFlow连接线信息将各个节点根据关系进行连接。连接时需要区分前后相对位置的关系,需要区分如下几种情况:
(1) source和target在一条水平线上(通过判断y坐标的相对位置确定),细分如下3种情况:
A、source的x小于target的x,且间距为正常的间距,则正常的向右连接即可;
B、source的x小于target的x,且间距大于正常的间距(大于或等于2倍正常间距),说明中间间隔了一个节点,则需要从上端绕行,以避免连续太过杂乱。具体绕行轨迹为向上、向右、向下,连接source的顶端与target的顶端;
C、source的x大于target的x,则需要向前绘制连接线,此时为了考虑图形的可读性,需要分向上、向左、向下绕行绘制连接线, 连接source的顶部和target的顶部。
(2) source和target在一条垂直线上(x坐标居中对齐),根据y的大小,分两种情况处理:
A、source.y < target.y, 直接进行向上或者向下的绘制即可,连接source的底部和target的顶部。
B、source.y > target.y,向上绘制连接线,连接source的顶部和target的底部。
(3) source和target的x、y都不相同,则需要判断source与target的相对位置,参考第(1)种情况里面的判断规则,有4种相对位置:
A、target在source的右上方,则分别向上、向右、向上连接source的顶部和target的底部
B、target在source的右下方,则分别向下、向右、向下连接source底部和target的顶部
C、target在source的左上方,则分别向上、向左、向下连接source的顶部和target的底部
D、target在source的左下方,则分别向下、向左、向下连接source的底部和target的顶部
最后还需要根据连接线的source和target,再次编辑各个节点的incoming和outgoing,最终全部添加到xml节点中,即可完成xml的拼装。
以下几种情况需要记录incoming和outgoing:
(1) source.y与target.y相同,source和target在同一个水平线上
A、source.x > target.x,
B、source.x < target.x 且source.x与target.x相距超过2个标准箭头的长度。
(2) source.y与target.y不相同,即source与target不在同一个水平线上
最终生成的xml文件符合BPMN2.0标准,可以使用相关的客户端工具(Camunda Modeler或者Flowable Modeler)进行编辑和查看。
3、实现
核心的处理逻辑在于节点分布和连线算法的实现,提前将节点和他们的位置分布计算出来,并且针对不同的节点相对位置进行连线的位置的计算,由于是实际工作项目,这里就不贴源代码出来了,理解了思路自己实现代码其实并没有太大难度。
从Excel导入有一些开源框架例如easyexcel(底层依赖poi)可以实现,最终绘制图形也是利用一些dom的开源组件例如dom4j写入xml文件即可。
当然,在具体的实现过程中也经历了很多版本的更新,最初的版本比较凌乱,最后的版本已经非常接近设计原图的样式了。
第一版的图形样式,连线有点乱,留个纪念:
第五版的图形样式,基本上和人工绘制的差不多了: