文章目录
提示:以下是本篇文章正文内容,Java 系列学习将会持续更新
一、网关
1.1 排他网关
排他网关(exclusive gateway)(也叫异或网关 XOR gateway,或者更专业的,基于数据的排他网关 exclusive data-based gateway),用于对流程中的决策建模。当执行到达这个网关时,会按照所有出口顺序流定义的顺序对它们进行计算。选择第一个条件计算为true的顺序流(当没有设置条件时,认为顺序流为true)继续流程。
- 使用排他网关时,只会选择一条顺序流。
- 当多条顺序流的条件都计算为 true 时,会且仅会选择在XML中最先定义的顺序流继续流程。
- 如果没有可选的顺序流,会抛出异常。
public class Test5 extends FlowableBootStudyApplicationTests {
@Resource
private RepositoryService repositoryService;
@Resource
private RuntimeService runtimeService;
@Resource
private TaskService taskService;
/**
* 部署流程
*/
@Test
public void deploy(){
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("请假流程-排他网关.bpmn20.xml")
.name("请假流程-排他网关")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println(deploy.getName());
}
/**
* 启动流程实例
*/
@Test
void runProcess(){
runtimeService.startProcessInstanceById("holiday-exclusive:1:c92d6bb0-4c91-11ee-9806-200db0c7aa5b");
}
/**
* 完成任务-提出请假申请
*/
@Test
void complete() {
Task task = taskService.createTaskQuery()
.processDefinitionId("holiday-exclusive:1:c92d6bb0-4c91-11ee-9806-200db0c7aa5b")
.taskAssignee("张三")
.singleResult();
Map<String,Object> variables = new HashMap<>();
variables.put("num", 6);
if(task != null) {
taskService.complete(task.getId(), variables);
}
}
/**
* 完成任务-审批请假申请
*/
@Test
void complete2() {
Task task = taskService.createTaskQuery()
.processDefinitionId("holiday-exclusive:1:c92d6bb0-4c91-11ee-9806-200db0c7aa5b")
.taskAssignee("主管") // 主管 或 总经理
.singleResult();
if(task != null) {
taskService.complete(task.getId());
}
}
}
注意:如果从网关出去的线所有条件都不满足的情况下会抛出系统异常!
但是,原来的任务还在没有丢 (如果没有排他网关,没有可走的分支时任务就会结束),我们可以重置流程变量。
@Test
public void setVariables(){
Map<String,Object> variables = new HashMap<>();
variables.put("num",4); // 给流程定义中的UEL表达式赋值
runtimeService.setVariables("3b987f19-4c92-11ee-84c8-200db0c7aa5b", variables); // 这里传流程实例executionId
}
1.2 并行网关
并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的。
- fork 分支:并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
- join 汇聚:所有到达并行网关,在此等待的进入分支, 直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关。
注意:如果同一个并行网关有多个进入和多个外出顺序流, 它就同时具有分支和汇聚功能。 这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。
与其他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。
①流程的部署和启动。
/**
* 部署流程
*/
@Test
public void deploy(){
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("请假流程-并行网关.bpmn20.xml")
.name("请假流程-并行网关")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println(deploy.getName());
}
/**
* 启动流程实例
*/
@Test
void runProcess(){
runtimeService.startProcessInstanceById("holiday-parallel:1:7593cd7a-4c96-11ee-9b26-200db0c7aa5b");
}
启动流程后,act_ru_task 表中有了一条待执行的任务,执行人是 “张三”。
②张三完成任务——提出请假申请。
@Test
void complete() {
Task task = taskService.createTaskQuery()
.processDefinitionId("holiday-parallel:1:7593cd7a-4c96-11ee-9b26-200db0c7aa5b")
.taskAssignee("张三")
.singleResult();
if(task != null) {
taskService.complete(task.getId());
}
}
进入并行网关后产生了两条分支。act_ru_task 表中有了两条待执行的任务,执行人分别是 “王经理” 和 “李经理”。
③完成其中一个分支任务——项目经理审批。
@Test
void complete() {
Task task = taskService.createTaskQuery()
.processDefinitionId("holiday-parallel:1:7593cd7a-4c96-11ee-9b26-200db0c7aa5b")
.taskAssignee("李经理")
.singleResult();
if(task != null) {
taskService.complete(task.getId());
}
}
流程没有结束,act_ru_task 表中还有一条待执行的任务,执行人分别是 “王经理”。
④完成所有分支任务——技术经理审批。
@Test
void complete() {
Task task = taskService.createTaskQuery()
.processDefinitionId("holiday-parallel:1:7593cd7a-4c96-11ee-9b26-200db0c7aa5b")
.taskAssignee("王经理")
.singleResult();
if(task != null) {
taskService.complete(task.getId());
}
}
完成所有分支任务后,流程结束!
1.3 包容网关
包含网关可以看做是排他网关和并行网关的结合体。 和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。 但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样。
包含网关的功能是基于进入和外出顺序流的:
- 分支:所有外出顺序流的条件都会被解析,结果为 true 的顺序流会以并行方式继续执行, 会为每个顺序流创建一个分支。
- 汇聚:所有并行分支到达包含网关,会进入等待状态, 直到每个分支都到达。 换句话说,包含网关只会等待被选中执行的顺序流。 在汇聚之后,流程会穿过包含网关继续执行。
①通过包容网关,进入两条分支。
/**
* 部署流程
*/
@Test
public void deploy(){
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("请假流程-包容网关.bpmn20.xml")
.name("请假流程-包容网关")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println(deploy.getName());
}
/**
* 启动流程实例
*/
@Test
void runProcess(){
runtimeService.startProcessInstanceById("holiday-inclusive:1:7381b94e-4c9a-11ee-8aff-200db0c7aa5b");
}
/**
* 完成任务-提出请假申请
*/
@Test
void complete() {
Task task = taskService.createTaskQuery()
.processDefinitionId("holiday-inclusive:1:7381b94e-4c9a-11ee-8aff-200db0c7aa5b")
.taskAssignee("张三")
.singleResult();
Map<String,Object> variables = new HashMap<>();
variables.put("num", 3);
if(task != null) {
taskService.complete(task.getId(), variables);
}
}
经过包容网关后,act_ru_task 表中有两条待执行的任务 (如果num>5则会有三条任务)。
②完成所有分支任务——王经理和李经理。
/**
* 完成任务-审批请假申请
*/
@Test
void complete2() {
Task task = taskService.createTaskQuery()
.processDefinitionId("holiday-inclusive:1:7381b94e-4c9a-11ee-8aff-200db0c7aa5b")
.taskAssignee("李经理") // 王经理和李经理
.singleResult();
if(task != null) {
taskService.complete(task.getId());
}
}
完成所有分支任务后,流程结束!
1.4 事件网关
事件网关允许根据事件判断流向。网关的每个外出顺序流都要连接到一个中间捕获事件。当流程到达一个基于事件网关,网关会进入等待状态,会暂停执行。与此同时,会为每个外出顺序流创建相对的事件订阅。
事件网关的外出顺序流和普通顺序流不同,这些顺序流不会真的 “执行”, 相反它们让流程引擎去决定执行到事件网关的流程需要订阅哪些事件。
- 事件网关必须有两条或以上外出顺序流;
- 事件网关后,只能使用
intermediateCatchEvent
类型(activiti 不支持基于事件网关后连接 ReceiveTask) - 连接到事件网关的中间捕获事件必须只有一个入口顺序流。
二、事件
事件 (event) 通常用于为流程生命周期中发生的事情建模。事件总是图形化为圆圈。在 BPMN 2.0 中,有两种主要的事件分类:捕获 (catching)
与抛出 (throwing)
事件。
- 捕获: 当流程执行到达这个事件时,会等待直到触发器动作。触发器的类型由其中的图标,或者说XML中的类型声明而定义。捕获事件与抛出事件显示上的区别,是其内部的图标没有填充(即是白色的)。
- 抛出: 当流程执行到达这个事件时,会触发一个触发器。触发器的类型,由其中的图标,或者说XML中的类型声明而定义。抛出事件与捕获事件显示上的区别,是其内部的图标填充为黑色。
2.1 定时器事件
flowable:
async-executor-activate: true # 开启定时任务
2.1.1 定时器启动事件
定时器启动事件(timer start event)在指定时间创建流程实例。在流程只需要启动一次,或者流程需要在特定的时间间隔重复启动时,都可以使用。
注意:
- 子流程不能有定时器启动事件。
- 定时器启动事件,在流程部署的同时就开始计时。不需要调用
startProcessInstanceByXXX
就会在时间启动。调用startProcessInstanceByXXX
时会在定时启动之外额外启动一个流程。 - 当部署带有定时器启动事件的流程的更新版本时,上一版本的定时器作业会被移除。这是因为通常并不希望旧版本的流程仍然自动启动新的流程实例。
定时器启动事件,用其中有一个钟表图标的圆圈来表示。
我们只需要部署该流程,没有启动流程实例的情况下,到 15:20:30 时自动帮助我们创建了一个流程实例。
2.1.2 中间计时器捕获事件
当第一个人工处理完成后,第二个人工处理的任务需要在 2023-09-07T15:20:30 之后执行。
2.1.3 边界计时器事件
注意:边界计时器事件也分为中断和非中断,默认是中断事件。设置了属性 cancelActivity="false"
的时候为非中断事件。
- 中断事件是中断当前的活动沿着事件触发。
人工处理1如果在定义的 dueDate 这个时间之前还没有处理,那么就会触发定时边界事件进入人工处理3,而人工处理1和2就不执行了。
- 非中断事件是不影响当前活动,并沿着事件触发。
人工处理1如果在定义的 dueDate 这个时间之前还没有处理,那么就会触发定时边界事件进入人工处理3,但不影响原本人工处理1和2的执行流程,相当于可以走两条分支。
2.2 消息事件
消息事件 (message event),是指引用具名消息的事件。消息具有名字与载荷。与信号不同,消息事件只有一个接收者。
2.2.1 消息启动事件
消息启动事件,也就是我们通过接收到某些消息后来启动流程实例,比如接收到了一封邮件,一条短信等。
①我们先定义一个消息。
②然后在消息开始节点出引用。
③部署后,通过发送消息来启动流程实例。
@Test
void runProcess(){
// runtimeService.startProcessInstanceById("message-event:1:a161b852-4d5c-11ee-892c-200db0c7aa5b");
runtimeService.startProcessInstanceByMessage("第一个消息");
}
发送消息后,可以发现 act_ru_task 中有了一条任务,说明流程实例成功启动了。
注意:发送消息发送的应该是消息的名称而不是消息的ID。如果名称错误,就会出现以下报错:
2.2.2 中间消息捕获事件
消息中间事件就是在流程运作中需要消息来触发的场景,案例演示,用户任务1
处理完成后,需要接收特定的消息之后才能进入到 用户任务2
中。
①启动流程实例,完成用户任务1
// 启动流程实例
@Test
void runProcess(){
runtimeService.startProcessInstanceById("message-event:1:4ed49371-4d5f-11ee-b5d5-200db0c7aa5b");
}
// 完成任务
@Test
void complete() {
Task task = taskService.createTaskQuery()
.processDefinitionId("message-event:1:4ed49371-4d5f-11ee-b5d5-200db0c7aa5b")
.taskAssignee("张三")
.singleResult();
if(task != null) {
taskService.complete(task.getId());
}
}
此时,用户任务1执行完成了,act_ru_task 表中没有任务信息。
②发布消息,触发中间事件。
/**
* 中间事件-发布消息
*/
@Test
void recevedMsg() {
// 需要拿到流程实例的执行编号
String executionId = "69a61d4d-4d5f-11ee-be8d-200db0c7aa5b";
runtimeService.messageEventReceived("第一个消息", executionId);
}
此时,通过了中间事件,act_ru_task 表中有了第二条任务信息。
2.2.3 边界消息事件
如果在消息触发时,用户任务1还没有完成,则会触发边界消息事件后执行用户任务3,而用户任务1和2就不执行了。
①当部署流程和启动实例后,等待执行用户任务1。
②当触发边界事件时,直接走了用户任务3的方向。
注意:这里的 executionId 一定是边界事件所在流程分支的实例ID。(只有在用户任务1的节点且未完成的情况下,才可以触发边界事件)
@Test
void recevedMsg() {
// 需要拿到流程实例的执行编号
String executionId = "5f44e48e-4d65-11ee-b739-200db0c7aa5b";
runtimeService.messageEventReceived("第一个消息", executionId);
}
2.3 错误事件
错误事件可以用做一个流程的开始事件或者作为一个任务或者子流程的边界事件,错误事件没有提供作用中间事件的功能,这一点和前面介绍的定时器事件和消息事件还有区别的。
2.3.1 异常启动事件
错误启动事件(error start event),可用于触发事件子流程(Event Sub-Process)。错误启动事件不能用于启动流程实例。错误启动事件总是中断。
两种方式触发错误开始事件的方式:
- 错误结束事件抛出错误(注:错误结束事件也选择的是同一个错误定义)
- 自动执行主流程的服务任务抛出错误,接下来子流程的错误开始事件捕获到错误后执行。
①在 FlowableUI 中没找到错误定义的选项,所以我们需要在流程文件中自己添加,并绑定错误事件。
②创建服务任务的执行类。
public class MyOneDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution delegateExecution) {
try { // 休眠10秒看效果
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("自动完成服务任务");
// 此处的errorCode和定义的error中的errorCode一致
throw new BpmnError("abc");
}
}
③部署并启动流程实例后,act_ru_task 表中还是空的,因为主流程中没有用户任务,此时正在自动执行服务任务。
④当服务任务抛出异常后,触发了子流程的异常启动事件,可以从 act_ru_task 和 act_ru_actinst 表中可以看出整个过程。
2.3.2 结束错误事件
接下来我们以第一种触发错误开始事件的方式为例讲解,需要在错误结束事件和异常启动事件上绑定同一个错误:
在 FlowableUI 中没找到错误定义的选项,所以我们需要在流程文件中自己添加。
当我们启动流程实例时,act_ru_task 表中有一条任务等待执行。
当主流程任务执行完,走向了结束错误事件,从而触发了子流程的异常启动事件。
2.3.3 边界错误事件
当子流程中没有发生特定错误时,不会触发边界错误事件,流程会走向用户任务1。
当子流程中发生特定错误时,会触发边界错误事件,流程会走向用户任务2。
2.4 信号事件
2.4.1 信号启动事件
自定义信号,并绑定到信号启动事件上。
通过释放对应的信号来触发流程的启动。
@Test
void runProcess(){
runtimeService.signalEventReceived("第一个信号");
}
注意:我们可以把信息的作用域由原来的 golbal
全局的调整为 processInstance
,测试后发现还是可以执行,说明在启动事件中信号的作用域是不起作用的。
<signal id="signal01" name="第一个信号" flowable:scope="global"></signal>
<signal id="signal01" name="第一个信号" flowable:scope="processInstance"></signal>
2.4.2 中间信号捕获事件
当我们启动事件后,会阻塞在这个消息获取中间事件处,等待相关信号后才会继续流转。
此时,对于两种信号作用域需要分情况讨论:
1、当定义的信号作用域为 global
时,我们发送 global 信号就可以捕获。
@Test
void sendSignal() {
runtimeService.signalEventReceived("第一个信号");
}
2、当定义的信号作用域为 processInstance
时,我们发送 global 信号就不会被捕获,而是需要我们在流程实例内部抛出信号。
2.4.3 中间信号抛出事件
信号中间抛出事件也就是在流程执行中的某个节点抛出了对应的信号,然后对应的信号中间捕获事件就会触发。
我们将信号的作用域定义为 processInstance
的,并且将这两个事件引用同一个信号。来查看该作用域的信号如何被捕获?
<signal id="signal01" name="第一个信号" flowable:scope="processInstance"></signal>
①当启动流程后,通过并行网关产生两条分支,第一条分支有一个待执行的任务,第二条分支需要等待捕获信号。
②当执行完任务1时,触发了该分支上的信号抛出事件,抛出的信号被第二条分支的信号捕获事件所捕获,成功走向的任务2的节点。
2.4.4 边界信号事件
①启动流程后,走到了用户任务1,等待执行或等待信号。
②捕获到了信号,走向了用户任务3,不用再执行任务1和2了。
2.5 结束事件
2.5.1 结束错误事件
当流程执行到达**错误结束事件(error end event)**时,结束执行的当前分支,并抛出错误。这个错误可以由匹配的错误边界中间事件捕获。如果找不到匹配的错误边界事件,将会抛出异常。
- 启动流程实例后,走到了用户任务1的节点,等待执行。执行时需要传递 num 变量,以决定后续的走向。
- 携带 num 通过排他网关:
- 如果 num=0,则子流程正常结束,接下来会走到用户任务2的节点。
- 如果 num>0,则子流程触发结束错误事件。并且被边界错误事件所捕获,最终走到了用户任务3的节点。
- 如果 num<0,则子流程异常报错,流程不会结束,异常也不会被捕获。
- 错误结束事件的作用就是在执行到错误结束的节点位置会抛出对应的错误,供需要获取的事件来处理。
2.5.2 结束终止事件
终止结束事件主要是对流程进行终止的事件,可以在一个复杂的流程中,如果某方想要提前中断这个流程,可以采用这个事件来处理,可以在并行处理任务中。
-
如果你是在流程实例层处理,整个流程都会被中断。
-
如果是在子流程中使用,那么当前作用和作用域内的所有的内部流程都会被终止,但是不会终止整个流程。
2.5.3 结束取消事件
取消结束事件 (cancel end event) 只能与 BPMN 事务子流程 (BPMN transaction subprocess) 一起使用。当到达取消结束事件时,会抛出取消事件,且必须由取消边界事件 (cancel boundary event) 捕获。取消边界事件将取消事务,并触发补偿 (compensation)。
结束取消事件我们只能在事务子流程中使用,在 FlowableUI 中暂时没有找到这个组件,所以在 Eclipse 中来绘制。
- 流程中定义了一个事务子流程和两个自动服务任务。事务子流程中定义了两个人工任务用一个排他网关连接,
flag<=0
的情况下会触发结束取消事件。 - 触发结束取消事件后,会被边界取消事件所捕获,从而执行外部的自动取消服务。
- 同时也会触发边界补偿事件,从而执行自动补偿服务。
2.6 补偿事件
通过补偿达到控制业务流程的目的就是补偿事件,比如我们正常的买机票的流程下订单购买,然后同时弹出支付流程页面。支付成功后就可以等待出票了,但是如果我们支付失败的话,这时要么重新支付,更换支付方式或者取消预订,这时取消预订就可以通过补偿事件来实现。
我们以下面的例子学习边界补偿事件和中间补偿投掷事件的使用:
预定机票的服务类:
public class PlaceOrderDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution delegateExecution) {
System.out.println(LocalDateTime.now().toString() + " : 正在预定机票中");
}
}
微信支付的服务类:
public class WechatPayDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution delegateExecution) {
System.out.println(LocalDateTime.now().toString() + " : 微信支付中");
boolean error = true;
if(error) {
System.out.println(LocalDateTime.now().toString() + " : 微信支付失败!");
throw new BpmnError("支付失败");
}
}
}
取消预订的服务类:
public class DeleteOrderDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution delegateExecution) {
System.out.println(LocalDateTime.now().toString() + " : 正在取消订单");
}
}
我们直接部署启动,整个流程执行的过程:
- 任务开始后会并行的执行机票预订和微信支付,然后在微信支付是抛出 pay-error 错误,同时错误边界事件会捕获到这个错误。
- 然后执行到中间补偿投掷事件,之后在机票预订的 边界补偿事件 被触发,对应的补偿触发器会执行对应的代码。
执行结果:
总结:
提示:这里对文章进行总结:
本文是对flowable的学习,学习了排他、并行、包含、事件这四种网关的使用,并且对事件进行了详细的学习和案例讲解,如定时器事件、消息事件、错误事件、信号事件和结束事件。之后的学习内容将持续更新!!!