BPMN边界事件
在BPMN2.0的事件分类中,边界事件被划分到中间事件中,BPMN2.0中将狭义的中间事件和边界事件,统称为中间事件。本书所称的中间事件为狭义的中间事件,即可以单独作为流程元素存在于流程中的事件为中间事件,而附属于某个流程元素(如任务、子流程等)的事件为边界事件。边界事件是Catching事件,会等待被触发,当边界事件被触发,当前的活动会被中断,并且当前的顺序流会发生转移。
BPMN2.0中定义了以下的边界事件:消息(Message)边界事件、定时器(Timer)边界事件、升级(Escalation)边界事件、错误(Error)边界事件、取消(Cancel)边界事件、补偿(Compensation)边界事件、条件(Conditional)边界事件、信号(Signal)边界事件、组合(Multiple)边界事件和并行组合(Parallel Multiple)边界事件。
定时器边界事件
定时器边界事件是附属在流程活动中的事件,当流程到达了流程活动时,定时器启动,当定时器边界事件被触发后,当前的活动会被中断,流程会从定时器边界事件离开流程活动。定时器边界事件使用在一些限时的业务流程中较为合适。假设当前有一个手机维修的流程,从接收到客户报障开始计算,先由初级工程负责修理手机,如果超过1个小时该工程师仍然未将手机修理好,就交由中级工程师负责修理,此时可以为初级工程师的流程任务加入定时边界事件。该业务流程如图11-9所示。
图11-9 含有定时器边界事件的维修业务流程
图11-9定义的流程开始后,任务会到达“初级工程师处理维修”的UserTask,这个UserTask有一个定时器边界事件,如果定时器边界事件触发,流程将会转到“中级工程师处理”的UserTask。图11-9对应的流程文件如代码清单11-21所示。
代码清单11-21:codes\11\11.5\boundary-event\resource\bpmn\TimerBoundaryEvent.bpmn
PT1M
代码清单11-21的粗体字代码,定义了一个定时器边界事件,在定时器事件定义中,使用了timeDuration元素,设置了该定时器事件将会在1分钟后触发,换言之,如果这个“初级工程师处理维修”的任务在1分钟内不完成的话,将会触发这个边界事件,流程会转向“中级工程师处理维修”的用户任务。需要注意的是,业务中定义的是初级工程师1个小时内处理不完就交给中级工程师处理,为了能更快到看到代码效果,案例中将初级工程师的修理时间设定为1分钟 。代码清单11-22加载该流程文件。
代码清单11-22:
codes\11\11.5\boundary-event\src\org\crazyit\activiti\TimerBoundaryEvent.java
// 创建流程引擎
ProcessEngineImpl engine = (ProcessEngineImpl) ProcessEngines
.getDefaultProcessEngine();
// 得到流程存储服务组件
RepositoryService repositoryService = engine.getRepositoryService();
// 得到运行时服务组件
RuntimeService runtimeService = engine.getRuntimeService();
// 获取流程任务组件
TaskService taskService = engine.getTaskService();
// 部署流程文件
repositoryService.createDeployment()
.addClasspathResource("bpmn/TimerBoundaryEvent.bpmn").deploy();
// 启动流程
runtimeService.startProcessInstanceByKey("tbProcess");
// 查询当前任务
Task currentTask = taskService.createTaskQuery().singleResult();
System.out.println("当前处理任务名称:" + currentTask.getName());
// 停止70秒
Thread.sleep(1000 * 70);
// 重新查询当前任务
currentTask = taskService.createTaskQuery().singleResult();
System.out.println("当前处理任务名称:" + currentTask.getName());
代码清单11-22中,启动流程后,执行任务查询,输出当前任务名称为“初级工程师处理维修”,代码清单11-22的粗体字代码,停止70秒后再进行任务查询,此时当前的任务已经变为“中级工程师处理维修”。需要注意的是,在程序运行的过程中,不同的硬件上可能会出现误差,为了能更准确的看到效果,1分钟后所执行的边界面事件,笔者在代码清单11-22中,给了70秒它去运行。运行代码清单11-22,输出结果如下:
当前处理任务名称:初级工程师处理维修
当前处理任务名称:中级工程师处理维修
由此可见,当超过规定的时间后流程仍然停留在UserTask,则定时器边界事件会触发,流程转向另外的UserTask。
BPMN2.0中实际上有两种定时器边界事件,一种是可中断的定时器边界事件,另外一种是不可中断的定时器边界事件,此处所说的可中断,并不是事件可中断,而是对于触发该事件的原来的执行流是否可中断。在定义boundary元素时,可以使用cancelActivity属性来设置该事件是否哪种事件,如果cancelActivity设置为true,则表示这是一个可中断的定时器边界事件,一旦这个边界事件被触发,那么原来的执行流将会被中断(Activiti实现为将执行流数据从数据库中删除),如果将cancelActivity设置为false,则表示这是一个不可中断的定时器边界事件,即使该边界事件被触发,原来的执行流仍然不会中断(数据仍然存在于执行流数据库中),原来的执行流当前的活动为该边界事件的id。
错误边界事件
错误边界事件依附在某个流程活动中,用于捕获子流程中抛出的错误,因此错误边界事件使用在嵌入子流程或者调用子流程中。
在使用错误边界事件时,可以使用错误事件定义加入errorRef的属性,该属性用于引用一个错误,在使用错误引用时,需要注意以下几点:
如果不使用该属性的话,那么这个错误边界事件将会捕获任何的错误事件而不管抛出的errorCode;
如果提供了该属性并且指向一个已经存在的“error”,那么该边界事件只会捕获与该“error”一样的errorCode;
如果errorRef属性引用了一个不存在的“error”,那么引用的字符串将会被当作errorCode。
下面将设计流程验证第三种情况,定义一个流程如图11-10所示。
图11-10 错误边界事件
在图11-10的流程中,当进入子流程后,会遇到一个ServiceTask,这个ServicTask会直接抛出BpmnError,图11-10对应的流程文件如代码清单11-23所示。
代码清单11-23:codes\11\11.5\boundary-event\resource\bpmn\ErrorBoundaryEvent.bpmn
代码清清单11-23的粗体字代码定义了一个错误边界事件,errorRef属性的值为“abc”,但是整份流程文件中并没有定义“abc”的error,因此“abc”将会作为一个errorCode来使用,抛出BpmnError的JavaDelegate对应的类为ThrowErrorDelegate,如代码清单11-24所示。
代码清单11-24:
codes\11\11.5\boundary-event\src\org\crazyit\activiti\ThrowErrorDelegate.java
public class ThrowErrorDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
String errorCode = “abc”;
System.out.println(“抛出错误,errorCode为:” + errorCode);
throw new BpmnError(errorCode);
}
}
ThrowErrorDelegate类中抛出的errorCode同样为“abc”,因此此处抛出的BpmnError会被错误边界事件捕获,编写代码直接启动流程,如代码清单11-25所示。
代码清单11-25:
codes\11\11.5\boundary-event\src\org\crazyit\activiti\ErrorBoundaryEvent.java
// 创建流程引擎
ProcessEngine engine = ProcessEngines
.getDefaultProcessEngine();
// 得到流程存储服务组件
RepositoryService repositoryService = engine.getRepositoryService();
// 得到运行时服务组件
RuntimeService runtimeService = engine.getRuntimeService();
// 获取流程任务组件
TaskService taskService = engine.getTaskService();
// 部署流程文件
repositoryService.createDeployment()
.addClasspathResource("bpmn/ErrorBoundaryEvent.bpmn").deploy();
// 启动流程
runtimeService.startProcessInstanceByKey("ebProcess");
// 进行任务查询
Task task = taskService.createTaskQuery().singleResult();
System.out.println(task.getName());
在代码清单11-25的最后会进行任务查询,将当前流程需要处理的任务名称输了出,如果错误边界事件被触发,那么将会打印“Error Task”,如果错误边界事件没有触发,将会打印“End Task”。由于在ThrowErrorDelegate类中抛出的errorCode为“abc”,因此运行代码清单11-25,将会输出“Error Task”,即错误边界事件会被触发。运行代码清单11-25,输出结果如下:
抛出错误,errorCode为:abc
Error Task
修改ThrowErrorDelegate类,将抛出的errorCode改为cde,则最终输出的结果为:
抛出错误,errorCode为:cde
抛出错误,errorCode为:cde
20:24:29,674 ERROR CommandContext - Error while closing command context
org.activiti.engine.delegate.BpmnError: No catching boundary event found for error with errorCode ‘cde’, neither in same process nor in parent process
信号边界事件
定时器边界事件的触发条件是时间条件符合要求,错误边界事件的触发条件是接收到抛出的错误,同样地,信号边界事件的触发条件是接收到信号,但是不一样的是,信号边界事件具有全局性,换言之,信号边界事件会进行全局范围的信号捕获。与定时器边界事件类似,信号边界事件同样存在可中断与不可中断两类,可以为boundaryEvent元素设置cancelActivity属性,如果设置为true,那么原来的执行流将会被中断,设置为false,则原来的执行流仍然存在。如果多个信号边界事件使用了相同的信号,当在某个地方发出信号时,即使在不同的流程实例中,这些信号边界事件均会捕获到该信号。
假设当前有一个签订合同的流程,会先进行合同的查看,然后再进行合同确认,如果在合同确认时接收到信息,合同的条款发生变更,那么就会对业务流程产生影响(不能再进行签订合同或者重新查看合同条款),业务流程如图11-11所示。
图11-11 签订合同流程
签订合同的流程如图11-11所示,在合同确认的UserTask中加入了信号边界事件,不管该流程定义有多少个流程实例,一旦在合同确认一步接收到信号,就会触发信号边界事件,流程会转向合同变更的UserTask。签订合同的流程文件内容如代码清单11-26所示。
代码清单11-26:codes\11\11.5\boundary-event\resource\bpmn\SignalBoundaryEvent.bpmn
代码清单11-26的粗体字代码,定义了一个信号边界事件,该事件引用了id为“contactChangeSignal”的信号,需要注意的是,该信号边界事件被定义为中断的边界事件(cancelActivity=true),会中断原来的执行流。代码清单11-27加载该流程文件并进行流程的处理。
代码清单11-27:
codes\11\11.5\boundary-event\src\org\crazyit\activiti\SignalBoundaryEvent.java
// 创建流程引擎
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 得到流程存储服务组件
RepositoryService repositoryService = engine.getRepositoryService();
// 得到运行时服务组件
RuntimeService runtimeService = engine.getRuntimeService();
// 获取流程任务组件
TaskService taskService = engine.getTaskService();
// 部署流程文件
repositoryService.createDeployment()
.addClasspathResource("bpmn/SignalBoundaryEvent.bpmn").deploy();
// 启动2个流程实例
ProcessInstance pi1 = runtimeService
.startProcessInstanceByKey("sbProcess");
ProcessInstance pi2 = runtimeService
.startProcessInstanceByKey("sbProcess");
// 查找第一个流程实例中签订合同的任务
Task pi1Task = taskService.createTaskQuery()
.processInstanceId(pi1.getId()).singleResult();
taskService.complete(pi1Task.getId());
// 查找第二个流程实例中签订合同的任务
Task pi2Task = taskService.createTaskQuery()
.processInstanceId(pi2.getId()).singleResult();
taskService.complete(pi2Task.getId());
// 此时执行流到达确认合同任务,发送一次信号
runtimeService.signalEventReceived("contactChangeSignal");
// 查询全部的任务
List tasks = taskService.createTaskQuery().list();
// 输出结果
for (Task task : tasks) {
System.out.println(task.getProcessInstanceId() + "---"
+ task.getName());
}
代码清单11-27中启动了两个流程实例,并将两个流程实例的查看合同任务完成,此时两个流程实例的执行流均到达“合同确认”的UserTask,使用代码清单11-27的粗体字代码发送一个信号,由于使用的signalEventReceived方法没有指定执行流,也就是使用该方法向全部的流程实例发送信号,需要触发使用了“contactChangeSignale”信号的边界事件,在此只发送了一次信号,案例中的两个流程实例中的信号边界事件均会捕获到该事件,相应的执行流均会到达“合同变更”的UserTask,运行代码清单11-27,输出结果如下:
5—合同变更
10—合同变更
根据输出结果可得知,即使只发送一次信号,两个流程实例的信号边界事件均捕获到该信号,流程转向“合同变更”的UserTask。信号边界事件可以使用在多种场合,例如在签订各种合同时,由于相关的政府政策的变化而导致合同中权利和义务发生变化,为了避免不必要的纠纷,可以在关键流程节点中加入信号边界事件。
补偿边界事件
在前面章节中,使用了取消结束事件和取消边界事件,当事务子流程被取消时,会触发事务子流程里面的补偿边界事件,这些补偿边界事件会依附在事务子流程的活动中,除了在事务子流程中可以使用取消事件来触发补偿边界事件外,还可以使用补偿中间事件来触发补偿边界事件。补偿中间事件是可以单独作为流程元素的Throwing事件,不需要附属于任何的流程活动。
与其他边界事件不一样的是,补偿边界事件会在流程活动完成后根据情况(事务取消或者补偿中间事件触发)而触发,例如在11.4.3小节中的取消边界事件的触发会导致事务子流程中补偿边界事件的触发,即使该补偿边界事件所依附的流程活动已经结束。在Activiti的实现中,当执行流到达附有边界事件的流程活动时,都会加入事件描述数据(ACT_RU_EVENT_SUBSCR表),边界事件所附的活动完成后,这些事件描述数据会被删除,但是补偿边界事件所产生的事件描述数据不会被删除(直到流程实例结束),因为即使活动完成后,这些补偿事件都有可能被触发。如果在一个流程中,一个附有补偿边界事件的活动被执行(完成)了若干次,那么当补偿边界事件触发后,这些补偿边界事件的执行次数将会与活动的执行(完成)次数相等。需要注意的是,补偿边界事件不支持依附在嵌套子流程中。
假设现在有一个银行转账的业务流程,流程启动后,需要进行转出银行扣款,再进行转入银行收款的任务,最后还需要提供一个验证的任务,当转出银行成功扣款和转入银行收款同时成功后,流程结束,如果其中一间银行操作失败,则为这些任务触发补偿事件,图11-12为该业务的流程图。
图11-12 转账流程
如果将转账流程作为某个业务流程的一部分,可以将转账流程作为事务子流程来处理,本例为了更加简洁,直接将转账单独作为一个普通的流程进行处理。图11-12中转出银行扣款和转入银行加款两个ServiceTask,均附有一个补偿边界事件,在验证转帐结果的的ServiceTask上,附有一个错误边界事件,该边界事件会抛出BpmnError,并且由补偿中间事件捕获,此时补偿中间事件会触发当前流程中的全部补偿边界事件。图11-12对应的流程文件如代码清单11-28所示。
代码清单11-28:
codes\11\11.5\boundary-event\resource\bpmn\CompensationBoundaryEvent.bpmn
代码清单11-28中的粗体字代码,①②定义了“转入银行加款”的ServiceTask及其补偿边界事件, ③④定义了“转出银行扣款”的ServiceTask及其补偿边界事件,⑤⑥定义了两个用于处理补偿的ServiceTask。①、③、⑤、⑥定义的ServiceTask对应的类仅仅只输出相应的文字,“转入银行加款”的类输出“转入银行接收款项...”,“转出银行扣款”的类输出“转出银行扣减款项...”,“转出银行取消”的类输出“转出银行取消扣减款项...”,“转入银行取消”的类输出“转入银行取消接收款项...”,这四个JavaDelegate类的实现在此不再赘述,只是简单的打印文字。代码清单11-28中⑦定义了一个用于验证转账结果的ServiceTask,⑧为该验证的ServiceTask添加错误边界事件,errorCode为“transferError”,代码清单11-29为验证转账结果的ServiceTask的JavaDelegate类。
代码清单11-29:
codes\11\11.5\boundary-event\src\org\crazyit\activiti\ValidateTransferDelegate.java
public class ValidateTransferDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
boolean result = (Boolean)execution.getVariable(“result”);
if (result) {
System.out.println(“转账成功”);
} else {
System.out.println(“转账失败,抛出错误”);
throw new BpmnError(“transferError”);
}
}
}
代码清单11-29中的ValidateTransferDelegate,在execute方法中,会从执行流中获取“result”参数,如果该参数为true,则输出“转账成功”,否则输出“转账失败”并抛出BpmnError,在实际应用中,不可能只会判断一个参数而决定是否触发补偿事件,本例为了能更加简洁去讲述补偿边界事件,此处只使用一个流程参数来决定是否触发补偿事件。代码清单11-30加载流程文件并运行流程。
代码清单11-30:
codes\11\11.5\boundary-event\src\org\crazyit\activiti\CompensationBoundaryEvent.java
// 创建流程引擎
ProcessEngine engine = ProcessEngines
.getDefaultProcessEngine();
// 得到流程存储服务组件
RepositoryService repositoryService = engine.getRepositoryService();
// 得到运行时服务组件
RuntimeService runtimeService = engine.getRuntimeService();
// 部署流程文件
repositoryService.createDeployment()
.addClasspathResource("bpmn/CompensationBoundaryEvent.bpmn").deploy();
// 初始化参数
Map vars = new HashMap();
vars.put("result", false);
runtimeService.startProcessInstanceByKey("cbProcess", vars);
代码清单11-30中,在流程启动时就将“result”参数设置为false,因此流程执行到“验证转账结果”的ServiceTask时,就会抛出BpmnError并且触发错误边界事件,执行流经过错误边界事件后转向补偿中间事件,补偿中间事件会触发流程中全部的补偿中间事件,那么定义的两个处理补偿的ServiceTask便会执行,运行代码清单11-30,输出如下:
转出银行扣减款项…
转入银行接收款项…
转帐失败,抛出错误
转入银行取消接收款项…
转出银行取消扣减款项…
在开始流程时,将流程参数设置为true,运行后输出如下:
转出银行扣减款项…
转入银行接收款项…
转账成功
根据以上的输出结果得知,在触发补偿中间事件后,在流程中全部的补偿边界事件均被触发。在使用补偿事件时,需要注意与取消事件进行区分,当前活动没有结束之前,不能使用补偿事件,补偿事件需要在其活动完成后才能触发,而取消事件可以在流程活动仍在进行时触发。