版权声明:本文为博主原创文章,未经博主容许不得转载。关注公众号技术汇(ID: jishuhui_2015) 可联系到做者。html
『工单系统』从宏观上看,是一些状态流的转换,笔者认为,工单系统的实现便是对工做流(workflow)的实现,典型的应用有企业OA系统,各种CRM,ERP等。spring
对于工单系统的实现,其实能够结合实际业务去编写相应的业务代码,这样作的最大的好处是定制化程度高,运行业务流程高度自定义化。然而,物极必反,高度定制的业务流程将会失去必定的灵活性。编程
那么问题来了:如何权衡?架构
答曰:基于业内标准实现。app
标准=权威框架
当咱们发现设计出来的业务流程不符合标准,就不得不怀疑了。编程语言
是时候要祭出工做流的“杀手锏”了:BPMN2.0工具
至于BPMN2.0的基础介绍,我在此再也不赘述,用一句话归纳:BPMN2.0是IBM制定的一套完善的工做流开发标准,它包含了诸如事件,网关,顺序流,任务等各种基本元素。ui
BPMN2.0毕竟只是一套标准,还须要付诸实现,业内知名的工做流开发框架当属Activiti了,使用的编程语言是受众较多的Java,若是实际工做中有用到工做流,可使用此开发框架,若是主流开发框架不是Java,只能自造轮子了。spa
若是准备进入工做流的开发,不管是使用框架,仍是自研,建议阅读一下这份文档,对实际开发工做很是有帮助。
再回到文章开头提到的“工单系统”,说说笔者启动此项目前的一些我的想法。
工单的流程推进自己就是工做流的一种体现,因此理所固然是使用了BPMN2.0的标准。大概花了一个工做日去研读了上述提到的参考文档,愈发以为BPMN2.0标准的精妙之处,要囊括现有的业务需求,那是绰绰有余。
固然,在作系统架构的时候,一定是要结合实际业务需求的。仔细分析了自身的业务需求,发现都是一些“短流程”,工单任务不会超过2个,工单任务类型也只会有一种(即为userTask),流程分支也很少,总而言之,笔者面临的都是一些较为简单的工单流程。
若是直接使用Acviti这类庞大复杂的框架,一方面是实际的工单流程不算复杂,二来有大材小用之嫌,再者团队成员都不熟悉此标准,理解并对接起来有难度。
所以,笔者走上了基于BPMN2.0标准自研的道路。
倒不是有重造一个Activiti的雄心壮志,而其实质是对Activiti进行功能裁剪,只实现了一些必要的标准。
关于此工单系统的架构设计将会分三篇文章讲解,此篇文章将着重介绍我用到了哪些BPMN2.0标准元素。
Workflow Definition Language(如下简称WDL),意思是工做流定义语言,属于我的自创,并不是官方术语,只是想在团队内统一语言而已。
沿袭Activiti的设计实现,WDL也是基于XML的。
在工单系统里面,笔者实现了如下8种基本元素:
一、根节点,由一对definitions标签组成。
<definitions id="def" name="工做流程配置"> </definitions>
复制代码
二、流程定义节点,由一对process标签表示,与其下属子节点组成一个完整的流程。 值得一提的是,BPMN2.0标准中是容许subProcess(子流程)存在的,这个feature在此工单系统里并未实现。
<process id="verify_work" name="用户审核流程"> </process>
复制代码
三、空开始事件节点,一般表示一个流程的开始。
<startEvent id="start" name="开始事件"/>
复制代码
四、定时事件定义,不可单独存在,其效果是在其余事件的基础上加了一个定时器,典型的应用是下面将提到的定时边界事件。
<timerEventDefinition>
<!-- 时间点 或者 cron表达式 -->
<timeDuration>${duration}</timeDuration>
</timerEventDefinition>
复制代码
五、定时边界事件。
边界事件都是捕获事件,它会附在一个节点上,当节点运行时,事件会监听对应的触发类型。 当边界事件被捕获,节点就会中断运行,同时执行事件的后续流程。
定时边界事件能够理解为一个暂停等待警告的时钟。当流程执行到绑定了边界事件的环节, 会启动一个定时器。
当定时器触发时,环节就会中断,并沿着定时边界事件的外出连线继续执行。
<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="userTask">
<!-- cancelActivity表示是否会中断边界事件所依附的任务 -->
<timerEventDefinition>
<timeDuration>0 15 10 * * ? *</timeDuration>
</timerEventDefinition>
</boundaryEvent>
复制代码
六、消息(边界)事件,消息的接收和发送要在应用或架构的一层实现的,流程引擎则内嵌其中。
这个元素相较于标准,仍是有改动的。消息事件在工单系统中被界定为是一种回调通知的手段,通知的类型有REST和MQ两种方式,通知所携带的参数在params中可被定义,name是参数名。
message标签是惟一一个与process标签同级的标签,message就比如全局变量,能够被WDL中多个元素引用。
如下定义了一个消息体,并在消息边界事件中引用该消息体。
<message id="newInvoice" name="newInvoiceMessage" type="REST | MQ">
<params>
<param name="target">${target}</param>
<param name="a">${a}</param>
<param name="x">${x}</param>
</params>
</message>
<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<messageEventDefinition messageRef="newInvoice"/>
</boundaryEvent>
复制代码
七、顺序流程节点,在表现形式上是一个单向箭头,所以须要定义两端的元素。起始元素用sourceRef属性定义,指向元素用targetRef属性定义,其值都是元素的id属性值。由此也能够看出,顺序流上的基本元素一般都须要有id属性进行标识的,并且最好不要重复,避免混淆。
<sequenceFlow id="flow1" sourceRef="ss" targetRef="tt" />
复制代码
八、 条件流程节点,意思知足某种条件才通向对应的顺序流。
<sequenceFlow id="flow1" sourceRef="exclusiveGw" targetRef="task1">
<conditionExpression>${condition}</conditionExpression>
</sequenceFlow>
复制代码
九、用户任务,表示须要工做人员实际操做推进的任务节点。
BPMN2.0标准中有很丰富的任务类型,诸如脚本任务,Java服务任务,邮件任务等等,Activiti也扩展出来了Mule任务,Camel任务等。
而在工单系统中,只须要用到用户任务,搭配其余事件,便可很好的知足业务需求。
<userTask id="task" name="verify_task"/>
复制代码
十、排他性网关节点,就像程序的if-else的判断,链接排他性网关的众多分支,最终只会走向其中一个分支。
<exclusiveGateway id="xgid" name="Request approved" default="sf"/> <!-- default表示默认流程 -->
复制代码
十一、并行性网关节点,和排他性网关是对立的,链接并行性网关的众多分支将会同时执行。
<parallelGateway id="pgid" name="gname"/>
复制代码
十二、空结束事件节点,一般做为一个流程的结束。
<endEvent id="end" name="结束事件"/>
复制代码
补充说明:
在上述的定时事件,消息定义,顺序流等元素均用到了同一种取值方式,即咱们常见的${value}形式,并不是用到了spring相关的解析手段,而是受到Activiti的启发,使用的是JUEL工具对表达式进行解析和执行。
在WDL中,不须要太复杂的表达式,支持简单的取值和逻辑运算便可,如:${name},${approved==true}。
为了加深对上述元素的理解,笔者挑了5个具备表明性的实例,展现其WDL的内容,实例配图均来自这份文档。
须要注意的是,definitions下面的子节点不讲究前后顺序,不必定要按流程走向书写WDL。我的习惯是先定义节点元素,而后用顺序流(sequenceFlow)进行链接。
这种状况比较简单,只有一个开始节点和一个任务节点。
<definitions id="def" name="工做流程配置">
<process id="verifyCredit" name="verify credit">
<startEvent id="start" name="开始"/>
<userTask id="unkown" name=""/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="unkown">
<conditionExpression>${condition}</conditionExpression>
</sequenceFlow>
</process>
</definitions>
复制代码
这是一个典型的单支顺序流程。
<definitions id="def" name="工做流程配置">
<process id="pid" name="my process">
<startEvent id="start" name="开始"/>
<userTask id="write" name="Write monthly financial report"/>
<userTask id="verify" name="Verify monthly financial report"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="write"/>
<sequenceFlow id="flow2" sourceRef="write" targetRef="verify"/>
<sequenceFlow id="flow2" sourceRef="verify" targetRef="end"/>
<endEvent id="end"/>
</process>
</definitions>
复制代码
此处出现了多分支状况,而且定义了一个defaultFlow,相似于:
if (conditionA) {
doTask1
} else if (conditionB) {
doTask3
} else {
doTask2
}
复制代码
不难看出,排他性网关通常都会伴随一个defaultFlow,如下是WDL内容。
<definitions id="def" name="工做流程配置">
<process id="pid" name="my process">
<startEvent id="start" name="开始"/>
<userTask id="t1" name="Task1"/>
<userTask id="t2" name="Task2"/>
<userTask id="t3" name="Task3"/>
<exclusiveGateway id="xgid" name="Exclusive Gateway" default="t2"/>
<sequenceFlow id="flow1" sourceRef="xgid" targetRef="t1">
<conditionExpression>${conditionA}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow2" sourceRef="xgid" targetRef="t2"/>
<sequenceFlow id="flow1" sourceRef="xgid" targetRef="t3">
<conditionExpression>${conditionB}</conditionExpression>
</sequenceFlow>
</process>
</definitions>
复制代码
到此处为止,算是能够看到一个完整的流程定义了,有开始和结束,各个任务节点,以及分支。
<definitions id="def" name="工做流程配置">
<process id="verifyCredit" name="verify credit">
<startEvent id="start" name="开始"/>
<userTask id="verifyCreditHistory" name="Verify credit history"/>
<sequenceFlow id="verify_flow" sourceRef="start" targetRef="verifyCreditHistory"/>
<exclusiveGateway id="approve" name="approve or not"/>
<sequenceFlow id="end_flow" sourceRef="verifyCreditHistory" targetRef="approve">
<userTask id="contact" name="Contact customer for further information"/>
<sequenceFlow id="disapprove_flow" sourceRef="approve" targetRef="contact">
<conditionExpress>${approve==false}</conditionExpress>
</sequenceFlow>
<sequenceFlow id="end_flow" sourceRef="contact" targetRef="end1">
<endEvent id="end1"/>
<sequenceFlow id="approve_flow" sourceRef="contact" targetRef="end2">
<conditionExpress>${approve==true}</conditionExpress>
</sequenceFlow>
<endEvent id="end2"/>
</process>
</definitions>
复制代码
这种状况是包含了一个定时边界事件,若是cancelActivity="false",那么状况就变得较为复杂了,由于有两处结束节点,cancelActivity="true"的时候,则只在一处结束。
<definitions id="def" name="工做流程配置">
<process id="verifyCredit" name="verify credit">
<startEvent id="start" name="开始"/>
<userTask id="firstLineSupport" name="First line support"/>
<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
<timerEventDefinition>
<timeDuration>2017-02-12 12:00:00</timeDuration>
</timerEventDefinition>
</boundaryEvent>
<sequenceFlow id="flow1" sourceRef="start" targetRef="firstLineSupport"/>
<sequenceFlow id="flow2" sourceRef="firstLineSupport" targetRef="end1"/>
<endEvent id="end1"/>
<userTask id="secondLineSupport" name="Second line support"/>
<sequenceFlow id="flow3" sourceRef="firstLineSupport" targetRef="secondLineSupport"/>
<sequenceFlow id="flow4" sourceRef="secondLineSupport" targetRef="end2"/>
<endEvent id="end2"/>
</process>
</definitions>
复制代码
到此,工单系统所需的基础知识就讲解完毕了。总体感受,BPMN2.0仍是简单易懂的,而且能覆盖到绝大多数工单流程,其能成为业内标准,也是自有一番道理的。