Activiti权威指南
上QQ阅读APP看书,第一时间看更新

4.4 元素解析

4.4.1 元素解析入口

代码清单4-8中,元素解析的运行环境(加载和验证流程文档信息)准备完毕,开始调用convertToBpmnModel方法解析元素,该方法的具体实现如代码清单4-11所示。

代码清单4-11 BpmnXMLConverter.java

以上是流程文档中元素解析的全过程,该方法内部使用XMLStreamReader迭代器遍历元素,其解析处理步骤总结如下。

(1)第2行实例化BpmnModel类,该类负责存储所有元素解析之后的结果。

(2)解析流程文档命名空间。

因为STAX解析流程文档的顺序是按照流程文档中元素定义的先后顺序自上而下解析的,所以首先解析父元素definitions,该元素对应的解析器为DefinitionsParser,该元素可以定义一系列的命名空间URI,形如<definitionsxmlns:activiti="http://activiti.org/bpmn" ></definitions>,因为流程文档中的元素名称是由开发者定义的,为了避免命名冲突,需要引入命名空间对元素加以区分。

(3)解析外围元素。

什么是外围元素呢?首先需要搞清楚这个概念,平时定义的流程元素,例如开始节点等元素均作为process元素的子元素存在,这里所说的外围元素指的是作为definitions元素的子元素同时还作为process元素的兄弟节点存在,形如消息元素<definitions><message id="newInvoice"></message></definitions>,最常见的外围元素有message、signal等,流程文档中定义的大部分外围元素是没有先后顺序之分的,既可以在process元素之上,也可以在process元素之下,因为外围元素种类不多而且不容易变动,所以该类型的元素解析器均在当前类中进行实例化,有关外围元素的解析处理逻辑可以查看对应的解析器。例如消息元素的解析:messageFlowParser.parse(xtr, model),该parse方法需要如下两个输入参数:①xtr参数:程序可以根据该参数值从流程文档中解析元素的属性值;②model参数:BpmnModel实例对象,元素解析完毕,可以直接将元解析结果存储到该元素对应的属性承载类实例对象中,然后再将其添加到BpmnModel实例对象中。因为外围元素的种类不多,平时开发中也不经常使用,为了减少风险,增加可控度,外围元素的定义以及解析不建议修改和扩展。

(4)第38~57行开始解析元素,主要解析事件、网关、活动等元素信息。

第43行首先判断activeProcess对象是否为空,如果该对象为空,表示流程文档中没有定义子元素,所以就不需要进行解析;如果该对象不为空,则第44行首先根据xtr. getLocalName()方法获取元素名称,然后根据元素名称从convertersToBpmnMap集合中查找该元素对应的解析器,例如xtr.getLocalName()值为userTask,则对应的解析器为UserTaskXMLConverter,从这里的处理逻辑就可以看出convertersToBpmnMap集合选用Map数据结构的好处,如果使用List数据结构,势必存在如下两个问题:①需要循环遍历解析器集合才能查找到适配当前元素的解析器;②客户端向该集合添加元素解析器时,可能会造成同一个元素的解析器有多个;如果同一个元素对应多个解析器,那么引擎该如何选择解析器,即使多个解析器都可以正常使用,引擎又该以哪一个解析器的解析结果为准?如果使用Map数据结构就不会出现以上问题,客户端可以直接根据key值(元素名称)查找元素对应的解析器,而且相同的key值只能存储一个,所以如果想要使用自定义元素解析器,只需要根据key值覆盖引擎默认的元素解析器即可。

(5)解析通用元素。

流程文档中通用元素的种类非常多,例如文档元素"documentation"、扩展元素"extensionElements"等,这些元素通常作为流程定义三大要素的子元素存在,试想一下,如果每个元素都在自己的解析处理逻辑中对通用元素进行解析,势必会造成相同的解析代码分散在各个模块中,无形之中增加了项目维护的复杂度,如果通用元素的解析规则变了,则需要修改每个模块中对应的解析逻辑,工作量非常大,如果将通用元素的解析功能抽离出来进行统一管理维护,则以上这些问题就不会出现。虽然暂时还没有看到具体元素的解析过程,但是已经看到了大量类似xxx.parse方法的调用,因为元素解析是基于STAX迭代器方式,所以首先会获取元素类型,然后再根据元素类型委托不同的解析器进行解析,上文细讲解过这样设计的好处,相信结合该方法的处理逻辑对开闭原则的掌握会更加深入。

由于Activiti中的元素类型非常多,从而导致元素解析器非常庞大,因为元素的解析处理步骤大体类似,每个都进行讲解,工作量极大,而且可能会有事倍功半的效果,所以接下来重点讲解通用元素以及流程元素(如连线元素)的解析处理逻辑,其他元素的解析逻辑可以参考该案例自行学习。

4.4.2 解析根元素

根元素definitions对应的解析器为DefinitionsParser,根元素中可以定义多个流程元素process(建议每个流程文档只定义一个process元素,这样可以减少开发过程中的维护成本),根元素definitions的定义以及解析过程如代码清单4-12所示。

代码清单4-12 DefinitionsParser.java和根元素的定义

根元素的解析处理流程总结如下。

(1)第11行获取根元素的targetNamespace属性值并将其设置到model对象中。

(2)第13~17行遍历所有命名空间的URI,包括获取URI的前缀以及值,最终第16行将其添加到model对象中。

(3)第18~30行进行属性解析。

(4)第28行验证黑名单列表,如果解析的属性不在黑名单列表中,则将其作为扩展属性进行处理,并通过第29行操作将其添加到model对象中。

注意

definitions元素至少需要包含xmlns和targetNamespace两个属性声明, targetNamespace可以是任意值,该属性可以用来对流程定义模板进行分类(项目开发中的一个小技巧)。

上述代码中进行了黑名单列表验证,接下来详细分析其处理逻辑。对于definitions元素来说,默认的黑名单属性集合defaultAttributes在第5~8行中进行初始化,该集合中的元素有如下3个:typeLanguage、expressionLanguage、targetNamespace,其处理流程如下。

• 遍历definitions元素中的所有属性。对于definitions元素来说,除了定义黑名单中的属性,还分别定义了exporterVersion、id、name、exporter4个属性,因此以上所述的7个属性都会进行遍历。

• 封装属性信息。ExtensionAttribute类承载了定义的属性名称name、属性值value、命名空间namespace、命名空间前缀namespacePrefix信息。

• definitions元素的扩展属性判断。

BpmnXMLUtil.isBlacklisted()方法的处理逻辑如代码清单4-13所示。

代码清单4-13 BpmnXMLUtil.java

该方法的处理逻辑如下。

• 第2行如果blackLists集合为空,则返回false;如果blackLists集合不为空,第3行循环遍历该集合,第4行循环遍历blackList,第5行如果blackAttribute对象中的name与attribute对象的name相同,则第6~7行开始继续比对两个对象的命名空间值是否相等,如果相等第8行返回true,第9~10行如果两个对象的命名空间均为空也返回true。

•isBlacklisted方法执行完毕后直接返回处理结果。

(5)经过isBlacklisted方法处理之后,如果存在扩展属性,则通过model.addDefinition-sAttribute方法将其添加到model对象中,方便后续获取,addDefinitionsAttribute方法内部使用Map数据结构存储扩展信息,该方法的实现相对来说比较简单,本书不过多讲解。看到这里可能会有疑问:哪些元素支持扩展属性呢?目前,Activiti中支持该特性的元素有3个:根元素definitions、任务节点userTask和流程元素process。

4.4.3 流程内元素解析入口

流程元素process的解析过程可以参考ProcessParser类的相关实现,接下来暂且将关注点放到process元素的子元素解析过程中,在代码清单4-11中,第44行根据元素名称从集合中查找到该元素对应的解析器,因为集合convertersToBpmnMap在BpmnXMLConverter类加载的时候已经初始化,关于这一点上文也详细讲解过,查找到元素对应的解析器之后,直接委托解析器的基类BaseBpmnXMLConverter中的convertToBpmnModel方法解析元素,该方法的相关定义如代码清单4-14所示。

代码清单4-14 BaseBpmnXMLConverter.java

以上便是process元素的子元素的解析全过程,在解析元素的所有属性之前,Activiti做了全局功能架构,主要包括如下内容。

(1)第3~8行解析公共属性值,常用的元素属性包括但不限于id、name、async、exclusive、default、isForCompensation几种。

(2)元素属性解析。

元素公共属性解析完毕,第10行开始调用convertXMLToElement(xtr, model)方法执行具体元素的解析工作,因为convertXMLToElement(xtr, model)方法是当前类BaseBpmnXMLConverter中定义的一个抽象方法,所以最终会调用具体的元素解析器完成元素解析工作,该方法解析完毕之后返回BaseElement实例对象。换言之,元素以及属性解析完毕之后会将其解析结果封装为BaseElement实例对象。

(3)第11~36行后置赋值操作。

根据元素对应的属性承载类的类型进行赋值,该操作主要区分网关和活动(引用流程、子流程以及所有的Task节点)两大要素,所有的节点均需要添加id、name值,如果元素是网关类型则需要填充defaultFlow、asynchronous、notExclusive属性值,“活动”类型则需要填充asynchronous、notExclusive、forCompensation、defaultFlow属性值。

(4)第38~43行如果元素类型为DataObject,则需要判断activeSubProcessList参数值是否为空,如果不为空,则将其作为子流程中的元素进行添加,否则添加到activeProcess对象中。

(5)第44~47行判断当前正在解析元素的父级元素,如果activeSubProcessList参数值不为空,则将其添加到activeSubProcessList对象中,否则添加到activeProcess对象中。

4.4.4 解析连线

上面提到BaseBpmnXMLConverter类中的convertXMLToElement(xtr, model)是抽象方法,该方法需要交给具体的子类去实现,为何这样设计?因为流程文档中的每个元素都对应一个解析器以实现自身属性解析功能,不同的解析器内部实现不同,但都在父类的convertXMLToElement方法中进行统一调用,该方法作为通用模板方法存在,由于BpmnXMLConstants接口定义了流程文档中所有的元素以及属性字段,为了便于统一管理,所有的元素解析器均直接或者间接地实现该接口。

下面详细讲解sequenceFlow元素的解析处理流程,该元素的解析器SequenceFlow-XMLConverter中convertXMLToElement方法的相关实现如代码清单4-15所示。

代码清单4-15 SequenceFlowXMLConverter.javaconvertXMLToElement方法

convertXMLToElement方法的处理逻辑如下所示。

(1)第2行实例化sequenceFlow元素对应的属性承载类,该类承载了连线元素所有属性值的获取和存储工作,连线元素解析完毕,所有的属性值都会封装到SequenceFlow实例对象中。

(2)第3行获取元素在XML中的坐标信息。

获取元素在流程文档中的行号和列号是非常必要的,这样做的好处就是当元素解析失败时,会将该元素在流程文档中的行号和列号暴露给客户端,方便客户端定位和排查问题。

(3)第4~7行解析属性信息。

对于sequenceFlow元素来说,常用的属性有sourceRef、targetRef、name、skipExpression,将这些属性信息获取并填充到sequenceFlow对象中。

(4)第8行开始解析当前元素的子元素。

调用parseChildElements方法解析sequenceFlow元素中的子元素,对于sequenceFlow元素来说,常用的子元素有文档元素documentation、扩展元素extensionElements(包括执行监听器以及用户自定义元素)。

4.4.5 获取元素坐标

在上述代码中,第3行委托BpmnXMLUtil.addXMLLocation()方法获取连线元素在XML文档中的行号和列号信息,具体实现如代码清单4-16所示。

代码清单4-16 BpmnXMLUtil.java获取元素文本结束位置的行号和列号

首先根据xtr对象获取Location实例对象,然后调用location对象的getLineNumber和getColumnNumber方法分别获取到行号和列号,获取完毕赋值到BaseElement实例对象中(对于连线来说该实例对象为sequenceFlow)。

注意

BpmnXMLUtil类被JVM加载时会触发该类中的静态代码块。