八、BPMN 2.0流程图详解
BPMN 2.0的标准的出现是好事,用户不在被某个工作流开发商绑架或者在工作流中开发妥协,Activiti作为BPMN标准的一套解决方案,使得用户在选择工作流框架时可以平滑的迁移过渡。也有负面的不好的消息,就是BPMN标准是大量开会讨论和开发商妥协的结果(一般这是在做梦),所以用户在阅读BPMN规范会感觉到它太笨重了,Activiti开发工作流将用户体验放到第一位置,开发出了工作流设计插件。工作流官方推荐使用工作流设计插件。
8.1 事件(Event)
每个流程设计都有start event和end event,而在整个流程中发生的事件都是有event来表示。事件在设计面板中用圆圈表示,在BPMN 2.0中主要有两种事件:
- Catching:当流程执行到事件的时候, 它会等待被触发。而触发条件需要用户配置在这个圆圈图标的属性里面,和下面第二种圆圈图标外形上的区别是:Catching图标里面是空的,就是空圈。
- Throwing:当流程执行到事件的时候,它会立即触发,同样的触发器也需要配置在图标属性里面,和Catching图标不同是圆圈图标里面有东西是,黑色的。
总的来说,事件定义决定了事件的语义。如果没有事件定义,这个事件就不做什么特别的事情。 没有设置事件定义的开始事件不会在启动流程时做任何事情。如果给开始事件添加了一个事件定义 (比如定时器事件定义)我们就声明了开始流程的事件 "类型 " (这时定时器事件监听器会在某个时间被触发)。
8.1.1 定时器事件
定时器事件是根据指定的时间触发的事件。可以用于开始事件(start event), 中间事件(intermediate event)和边界事件(boundary event)。定时器事件必须含有下面一种属性的配置。
timeDate:指定ISO 8601格式的日期定时器激活。(至于ISO 8601日期格式可以详见百度:http://baike.baidu.com/view/931641.htm)
<timerEventDefinition> <timeDate>2016-08-23T18:13:00</timeDate> </timerEventDefinition>
timeDuration:定义定时器经过多少时间后激活。时间段也是取得ISO 8601格式,比如在一年三个月五天六小时七分三十秒内,可以写成P1Y3M5DT6H7M30S。
<timerEventDefinition> <timeDuration>P10D</timeDuration> </timerEventDefinition>
timeCycle:定义定时器重复间隔,在某些场景使用,比如周期性的启动流程,任务超时发送提醒。timeCycle的设置目前有两种方式:ISO 8601和Cron表达式(quartz任务调度框架提供的解决方案),activiti默认是使用ISO 8601。例如现在重复三次,每次间隔10小时:
1 <timerEventDefinition> 2 <timeCycle activiti:endDate="2016-08-22T16:42:11+00:00">R3/PT10H</timeCycle> 3 </timerEventDefinition>
<timerEventDefinition>
<timeCycle>R3/PT10H/${EndDate}</timeCycle>
</timerEventDefinition>
其中endDate是可选的配置,上面使用了两张方式加上了endDate, 定时器将会在指定的时间停止工作。
此外如果你使用Cron 表达式,可以这样写:
0 0/5 * * * ?
注意: 第一个数字表示秒,而不是像通常Unix cron中那样表示分钟。重复的时间周期能更好的处理相对时间,它可以计算一些特定的时间点 (比如用户任务的开始时间),而cron表达式可以处理绝对时间, 这对定时启动事件特别有用。
你可以使用表达式进行配置,在里面动态设置值,不过该值需要为ISO 8601或者(cron表达式)格式,
<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport"> <timerEventDefinition> <timeDuration>${duration}</timeDuration> </timerEventDefinition> </boundaryEvent>
定义器的执行有先决条件:async executor和job被启用定时器才会激活(例如在activiti.cfg.xml中配置了jobExecutorActivate或者asyncExecutorActivate为true)。
8.1.2 错误事件定时器
BPMN的错误是关于业务上面的异常处理,它和java代码上的异常是不同的,两者完全不同,比如这样配置一个错误事件:
1 <endEvent id="myErrorEndEvent"> 2 <errorEventDefinition errorRef="myError" /> 3 </endEvent>
8.1.3 信号事件
信号事件会引用一个命名的信号,所谓的信号作用在整个流程引擎全局范围内,会发送给所有活跃的处理器。信号事件在BPMN文件中是定义在signalEventDefinition中,其中的signalRef属性可以引用前面声明的signal,而signal在definitions的根节点中作为子元素,下面就是一个例子
1 <definitions... > 2 <!-- 声明signal --> 3 <signal id="alertSignal" name="alert" /> 4 5 <process id="catchSignal"> 6 <intermediateThrowEvent id="throwSignalEvent" name="Alert"> 7 <!-- signal event definition --> 8 <signalEventDefinition signalRef="alertSignal" /> 9 </intermediateThrowEvent> 10 ... 11 <intermediateCatchEvent id="catchSignalEvent" name="On Alert"> 12 <!-- signal event definition --> 13 <signalEventDefinition signalRef="alertSignal" /> 14 </intermediateCatchEvent> 15 ... 16 </process> 17 </definitions>
Throwing信号事件:在BPMN中配置或者用代码实现都可以发出信号,而使用代码可以这样子:
1 RuntimeService.signalEventReceived(String signalName); 2 RuntimeService.signalEventReceived(String signalName, String executionId);
这两个方法不同之处在于第一个方法发出全局的信号,第二个方法会指定execution发出信号。
Catching信号事件:被中间事件和边界事件捕获的事件。
前面第二个方法的executionId或者查询当前活跃的信号事件方法如下:
1 List<Execution> executions = runtimeService.createExecutionQuery() 2 .signalEventSubscriptionName("alert") 3 .list();
信号的作用范围:
默认的信号作用域是整个流程引擎,也就是说你可以throw一个信号在多个流程实例之间并发生作用。有时候我们需要作用范围仅仅是在发生事件的流程实例里,限制信号的作用范围,可以这样配置,不过它并不是BPMN2.0规范中的,是activiti独有的,其中activiti:scope的默认值是global。
1 <signal id="alertSignal" name="alert" activiti:scope="processInstance"/>
信号事件案例:这里我使用了Activiti Explorer在线流程图设计器设计了两张图,展示了信号交互。
第一张流程是从保险规则变动开始的,然后相关人员审批,如果同意后会发出保险条件发生改变的信号。
第二张流程中将在红框标识的地方会捕获(Catching)这个事件,使得保险合同在这时重新计算。
信号是通过广播传递给所有活跃的事件,但有时候我们并不是想要这种结果,譬如下图:
上面流程图的意思是执行“do something”任务时出现的错误,会被边界错误事件捕获, 然后使用信号传播给并发路径上的分支,进而中断"do something inparallel"任务, 但是,根据信号的广播含义,它也会传播给所有其他订阅了信号事件的流程实例,这就是我们不想要的。这时我们需要调用前面介绍触发信号的API的第二个方法进行手动关联。
8.1.4 消息事件
消息事件会引用已命名的消息。和信号不同的是,消息具有名称和内容,并且消息始终指定了单个的接收者。
消息事件定义在BPMN文件的messageEventDefinition元素中,其中messageRef属性值来自于message,至于message是配置在definitions的根元素里面。下面是一个例子:
<definitions id="definitions" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:activiti="http://activiti.org/bpmn" targetNamespace="Examples" xmlns:tns="Examples"> <message id="newInvoice" name="newInvoiceMessage" /> <message id="payment" name="paymentMessage" /> <process id="invoiceProcess"> <startEvent id="messageStart" > <messageEventDefinition messageRef="newInvoice" /> </startEvent> ... <intermediateCatchEvent id="paymentEvt" > <messageEventDefinition messageRef="payment" /> </intermediateCatchEvent> ... </process> </definitions>
抛出消息事件:Activiti作为嵌入式的引擎,它不会关注怎么接收消息,接收消息取决于你的环境和特定的平台,比如你可以连接到JMS消息队列或者执行WebService或REST请求,这是需要你的应用层架构中进行实现,Activiti只是其中一部分。
在你的应用里面收到消息,你需要处理它,如果是启动流程实例的消息,可以参考下面的API:
1 ProcessInstance startProcessInstanceByMessage(String messageName); 2 ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables); 3 ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey, Map<String, Object> processVariables);
这些API允许使用引用的消息进行启动流程实例。如果流程实例需要接收这些消息,首先你需要关联指定流程实例和消息,然后触发处于等待的流程,使用RunTimeService可以触发基于消息的流程。
1 void messageEventReceived(String messageName, String executionId); 2 void messageEventReceived(String messageName, String executionId, HashMap<String, Object> processVariables);
查询订阅消息事件的流程定义:
对于start event的消息,消息事件关联到指定的流程定义,消息的订阅可以使用ProcessDefinitionQuery查询。
1 ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery() 2 .messageEventSubscription("newCallCenterBooking") 3 .singleResult();
对于明确的消息是对应一个流程的,所以查询结果一般是0个或者1个,如果是流程定义更新,那么方法返回最新的流程定义。
如果是中间消息事件,订阅的消息关联到特定的流程,我们可以使用ExecutionQuery进行查询:
1 Execution execution = runtimeService.createExecutionQuery() 2 .messageEventSubscriptionName("paymentReceived") 3 .variableValueEquals("orderId", message.getOrderId()) 4 .singleResult();
下面的实例通过两个不同的消息进行启动流程实例:
在某些需要多个start event启动流程实例需要统一的处理方式的时候是有用处的。