本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本专栏第三篇已发布,尚未看过的小伙伴请移步这里:

  1. Flowable 开篇,流程引擎扫盲
  2. 通过 Flowable-UI 来体验一把 Flowable 流程引擎
  3. 搞懂 Flowable 中的流程定义和流程实例

今天这篇文章,松哥和大家梳理一下 Flowable 中常见的四种任务以及其对应的玩法。

1. ReceiveTask

1.1 使用场景

接受任务(Receive Task),接触过 Flowable 的小伙伴应该是见过或者听说过,它的图标如下图:

ReceiveTask 可以算是 Flowable 中最简单的一种任务,当该任务到达的时候,它不做任何逻辑,而是被动地等待用户 Trigger。

ReceiveTask 往往适用于一些不明确的阻塞,例如:一个复杂的计算需要等待很多条件,这些条件是需要人为来判断是否可以执行,而不是直接执行,这个时候,工作人员如果判断可以继续了,那么就 Trigger 一下使流程继续向下执行。

基于以上介绍,ReceiveTask 还有一个中文名字叫做等待任务,也就是说,流程走到 ReceiveTask 这个节点的时候,就卡住了,需要用户手动点一下,流程才会继续向下走。

1.2 实践

1.2.1 绘制流程图

我们绘制一个简单的流程图来看下 ReceiveTask 到底是啥样子,流程图如下:

ReceiveTask 图标上有一个信封。

小伙伴们绘制的时候,首先选择用户任务:

然后点击设置按钮,将用户任务切换为 ReceiveTask 即可:

绘制完成后,我们下载这个流程图对应的 XML 文件。

来看看,带 ReceiveTask 的流程图是下面这样的:

<process id="receiveTask_demo" name="接收任务测试流程" isExecutable="true"><documentation>接收任务测试流程</documentation><startEvent id="startEvent" flowable:formFieldValidation="true"></startEvent><sequenceFlow id="sid-9E7B327E-EFC8-4D29-8C6F-157D5E1B7A4E" sourceRef="startEvent" targetRef="todaySales"></sequenceFlow><receiveTask id="todaySales" name="统计今日销售额"></receiveTask><receiveTask id="sendMsg" name="发送今日销售业绩给老板"></receiveTask><endEvent id="endEvent"></endEvent><sequenceFlow id="s2" sourceRef="todaySales" targetRef="sendMsg"></sequenceFlow><sequenceFlow id="s3" sourceRef="sendMsg" targetRef="endEvent"></sequenceFlow>
</process>
复制代码

1.2.2 部署

这个松哥在之前的文章中已经反复介绍过多次了,这里就不再赘述了,大家参考我们之前的文章部署并启动上面这个流程。

1.2.3 分析

当流程启动之后,按照我们前面文章的分析,我们先去数据库中 ACT_RU_TASK 表进行查看,发现该表空空如也。也就是 ReceiveTask 并不会被记录在 ACT_RU_TASK 表中,他们只是单纯的被记录在 ACT_RU_EXECUTION 表中,因为在该表中,我们可以查看 ReceiveTask 的记录。

对于 ReceiveTask 的触发方式也很简单,如下:

@Test
void test10() {List<Execution> list = runtimeService.createExecutionQuery().activityId("todaySales").list();for (Execution execution : list) {runtimeService.trigger(execution.getId());}
}
复制代码

由于 ReceiveTask 的触发需要传入的参数是执行实例 ID 而不是流程实例 ID,所以我们要查询出来当前待触发的执行实例 ID。具体的查询方式就是根据 ReceiveTask 的节点名称去查询。

查询到执行实例 ID 之后,调用 trigger 方法完成触发,使得流程继续向下走。

好啦,现在流程进入到发送今日销售业绩给老板这个环节了,老办法继续查询并执行:

@Test
void test10() {List<Execution> list = runtimeService.createExecutionQuery().activityId("sendMsg").list();for (Execution execution : list) {runtimeService.trigger(execution.getId());}
}
复制代码

这个执行完层后,这个流程就结束了。现在我们去查看 ACT_RU_ACTINST 表已经空了,查看 ACT_RU_EXECUTION 表也空了。

2. UserTask

UserTask 看名字就知道,需要人工干预,而人工处理的方式有很多种,我们可以设置节点是由哪个用户处理,也可以设置是由哪个用户组来处理(相当于是由哪个角色来处理)。

现在,假设我有如下一个简单的流程图:

那么我该如何设置这个用户节点的处理人呢?

2.1 指定具体用户

第一种方式,是我们在绘制流程图的时候,可以选中这个节点,然后直接设置流程的处理人,像下面这样:

然后在打开的窗口中选择固定值,设置具体分配的用户是 javaboy,如下图:

好了,现在这个节点就固定的由一个名为 javaboy 的用户去处理了。

对应的 XML 文件如下:

<process id="demo01" name="demo01" isExecutable="true"><documentation>demo01</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><userTask id="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" flowable:assignee="javaboy" flowable:formFieldValidation="true"><extensionElements><modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete></extensionElements></userTask><sequenceFlow id="sid-71FB3A81-F753-419D-9A0A-2FC6E5361CED" sourceRef="startEvent1" targetRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3"></sequenceFlow><endEvent id="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></endEvent><sequenceFlow id="sid-DEBE03CD-F247-4EF3-BB67-ABBA94739B0A" sourceRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" targetRef="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></sequenceFlow>
</process>
复制代码

在上面这段 XML 中,小伙伴们看到 UserTask 节点中有一个 flowable:assignee="javaboy",这句话就是设置这个 UserTask 的处理人。

接下来,我们部署并启动这个流程(具体的部署启动方式可以参考本系列之前的文章),启动之后,我们可以在数据库的 ACT_RU_TASK 表中看到,这个 UserTask 的处理人是 javaboy,如下图:

现在我们可以通过 Java 代码去查询 javaboy 需要处理的 UserTask 了,如下:

@Autowired
TaskService taskService;
@Test
void test11() {List<Task> list = taskService.createTaskQuery().taskAssignee("javaboy").list();for (Task task : list) {logger.info("id:{},name:{}",task.getId(),task.getName());}
}
复制代码

这个查询,本质上其实就是去 ACT_RU_TASK 表中查询的,我们来看看执行的 SQL:

查询到这个任务之后,javaboy 有两种选择:

  1. 将这个任务指定给另外一个人,例如 zhangsan。
  2. 自己处理。

2.1.1 重新指定任务处理人

假设 javaboy 查询到自己的任务之后,想把这个任务交给 zhangsan 去处理,方式如下:

@Autowired
TaskService taskService;
@Test
void test11() {List<Task> list = taskService.createTaskQuery().taskAssignee("javaboy").list();for (Task task : list) {taskService.setAssignee(task.getId(),"zhangsan");}
}
复制代码

这行代码执行完成后,我们看数据库中的 ACT_RU_TASK 表,还是刚才那条记录,但是处理人变了,变成了 zhangsan:

小伙伴们看到,版本号从 1 变为 2 了,说明这条记录被更新过了,处理人则从 javaboy 变为了 zhangsan。

最后我们再来看下这个操作所执行的 SQL,来验证一下我们前面的结论:

小伙伴们注意看这里执行的 SQL,以及对应的参数,说明我们上面的分析是没有问题的。

2.1.2 自己处理

如果 javaboy 想自己处理这个任务也是可以的,方式如下:

@Autowired
TaskService taskService;
@Test
void test11() {List<Task> list = taskService.createTaskQuery().taskAssignee("javaboy").list();for (Task task : list) {taskService.complete(task.getId());}
}
复制代码

处理完成后,ACT_RU_TASK 表中的记录也会被自动删除掉(执行过的 UserTask 会被自动删除)。

这种方式是指定了具体的用户,很显然这种硬编码的方式使用起来很不方便,我们需要的是能够动态指定任务处理人的方式。

2.2. 通过变量设置

如果想动态指定 UserTask 的处理人,则可以通过变量来实现,具体方式如下:

在绘制流程图的时候,还是指定流程的具体处理人,但是在指定的时候,使用变量代替,如下图:

这里的 #{manager} 表示这个 UserTask 由一个名为 manager 的变量来指定,此时的 XML 文件则是下面这样:

<process id="demo01" name="demo01" isExecutable="true"><documentation>demo01</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><userTask id="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" flowable:assignee="#{manager}" flowable:formFieldValidation="true"><extensionElements><modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete></extensionElements></userTask><sequenceFlow id="sid-71FB3A81-F753-419D-9A0A-2FC6E5361CED" sourceRef="startEvent1" targetRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3"></sequenceFlow><endEvent id="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></endEvent><sequenceFlow id="sid-DEBE03CD-F247-4EF3-BB67-ABBA94739B0A" sourceRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" targetRef="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></sequenceFlow>
</process>
复制代码

小伙伴们看到,UserTask 节点中的 flowable:assignee="#{manager}" 就表示这个 UserTask 的处理人由 manager 变量指定。

对于这样的流程,我们在上一个节点处就需要指定下一个节点的处理人,对于当前案例来说,当然是要在流程启动的时候,指定这个 UserTask 的处理人,方式如下:

@Test
void test01() {Map<String, Object> variables = new HashMap<>();variables.put("manager", "javaboy");ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01",variables);logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

当流程启动成功之后,大家去查看 ACT_RU_TASK 表,就可以看到,有一个待处理的 UserTask,处理人是 javaboy,如下图:

能看到这条记录,就说明这个 UserTask 的处理人我们已经设置成功了。

接下来具体的处理逻辑,则参考 1.1 和 1.2 小节。

2.3. 通过监听器设置

当然,我们也可以通过监听器来设置任务的处理人。具体方式如下:

首先我们在绘制流程图的时候,不需要给 UserTask 分配用户,如下图:

然后我们为这个 UserTask 设置一个任务监听器,步骤如下:

首先点击 + 号,然后选择 CREATE 事件,最后再给出事件对应的实体类,如下:

当然这个实体类是我们项目中真实存在的一个类,如下:

public class MyTaskListener implements TaskListener {@Overridepublic void notify(DelegateTask delegateTask) {delegateTask.setAssignee("javaboy");}
}
复制代码

当这个 UserTask 创建的时候,就会触发这个监听器,为该 UserTask 设置处理人。

我们来看看这个流程图对应的 XML 文件是什么样子的:

<process id="demo01" name="demo01" isExecutable="true"><documentation>demo01</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><userTask id="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" flowable:formFieldValidation="true"><extensionElements><flowable:taskListener event="create" class="org.javaboy.flowableidm.MyTaskListener"></flowable:taskListener></extensionElements></userTask><sequenceFlow id="sid-71FB3A81-F753-419D-9A0A-2FC6E5361CED" sourceRef="startEvent1" targetRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3"></sequenceFlow><endEvent id="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></endEvent><sequenceFlow id="sid-DEBE03CD-F247-4EF3-BB67-ABBA94739B0A" sourceRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" targetRef="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></sequenceFlow>
</process>
复制代码

小伙伴们看到,event="create" class="org.javaboy.flowableidm.MyTaskListener" 就是我们设置的内容了。

现在我们部署并启动这个流程,当我们流程启动后,就可以在 ACT_RU_TASK 表中看到一条 javaboy 待处理的任务了。

2.4. 其他情况

最后再来说说一种特殊情况,就是这个 UserTask 由任务的发起人处理,任务是谁发起的,谁来处理人这个 UserTask。

这个首先需要在流程启动事件上设置任务的发起人变量名,如下,流程的启动节点,然后设置任务的发起人:

接下来,在给 UserTask 设置处理人的时候,设置处理人和任务的发起人的变量是同一个,如下图:

好啦,这就可以了。来看看对应的 XML 文件:

<process id="demo01" name="demo01" isExecutable="true"><documentation>demo01</documentation><startEvent id="startEvent1" flowable:initiator="INITATOR" flowable:formFieldValidation="true"></startEvent><userTask id="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" flowable:assignee="#{INITATOR}" flowable:formFieldValidation="true"><extensionElements><modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete></extensionElements></userTask><sequenceFlow id="sid-71FB3A81-F753-419D-9A0A-2FC6E5361CED" sourceRef="startEvent1" targetRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3"></sequenceFlow><endEvent id="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></endEvent><sequenceFlow id="sid-DEBE03CD-F247-4EF3-BB67-ABBA94739B0A" sourceRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" targetRef="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></sequenceFlow>
</process>
复制代码

startEvent 中有一个 flowable:initiator="INITATOR" 表示设置流程发起人的变量为 INITATOR。后续在 UserTask 中使用该变量即可。

将这个流程部署成功之后,按照如下方式启动流程:

@Test
void test01() {Authentication.setAuthenticatedUserId("javaboy");ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01");logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

Authentication.setAuthenticatedUserId("javaboy"); 表示设置流程的发起人为 javaboy。

前面都是针对单个任务处理人,有的时候,一个任务节点会存在多个候选人,例如 zhangsan 提交一个任务,这个任务即可以 lisi 处理,又可以 wangwu 处理,那么针对这种多个任务候选人的情况,我们该如何处理?

2.5 流程图指定多个候选人

首先我们还是使用之前旧的流程图,但是在为 UserTask 设置分配用户的时候,我们设置多个用户,如下图:

设置完成后,我们下载这个流程文件,来看下对应的 XML 文件,内容如下:

<process id="demo01" name="demo01" isExecutable="true"><documentation>demo01</documentation><startEvent id="startEvent1" flowable:initiator="INITATOR" flowable:formFieldValidation="true"></startEvent><userTask id="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" flowable:candidateUsers="javaboy,zhangsan,lisi" flowable:formFieldValidation="true"></userTask><sequenceFlow id="sid-71FB3A81-F753-419D-9A0A-2FC6E5361CED" sourceRef="startEvent1" targetRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3"></sequenceFlow><endEvent id="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></endEvent><sequenceFlow id="sid-DEBE03CD-F247-4EF3-BB67-ABBA94739B0A" sourceRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" targetRef="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></sequenceFlow>
</process>
复制代码

小伙伴们看到,UserTask 中的 flowable:candidateUsers="javaboy,zhangsan,lisi" 就表示这个 UserTask 由 javaboy、zhangsan 和 lisi 三个用户处理,用户名之间用 , 隔开。

2.6 查询任务处理人

接下来我们部署并启动上面这个流程,具体如何部署如何启动,这个在之前的文章中松哥已经和大家聊过了,这里不再赘述。

当流程启动成功之后,现在我们很容易想到像之前文章那样,去查询 javaboy 需要处理的 UserTask,如下:

List<Task> list = taskService.createTaskQuery().taskAssignee("javaboy").list();
for (Task task : list) {logger.info("id:{};name:{};taskDefinitionKey:{}", task.getId(), task.getName(), task.getTaskDefinitionKey());
}
复制代码

但是我们却发现这个 SQL 执行完成后,查询不到任何数据!为什么呢?我们来分析下。

经过前面几篇文章的介绍,现在小伙伴们都知道了,上面这个方法最终查询的是数据库中的 ACT_RU_TASK 表,查询的 SQL 如下:

那我们就去检查 ACT_RU_TASK 表以及它的 ASSIGNEE_ 字段,结果如下:

我们发现 ACT_RU_TASK 表中记录的 ASSIGNEE_ 字段值为 null!

为 null 这个其实也好理解,毕竟这个 UserTask 有多个人可以处理,但是只有一个字段,没法储存,肯定有其他存储方式。

好啦,不和大家卖关子了,这种有多个候选人的任务,我们应该按照如下方式来查询:

@Test
void test12() {List<Task> list = taskService.createTaskQuery().taskCandidateUser("javaboy").list();for (Task task : list) {logger.info("id:{};name:{};taskDefinitionKey:{}", task.getId(), task.getName(), task.getTaskDefinitionKey());}
}
复制代码

小伙伴们看到,这里应该调用 taskCandidateUser 方法进行处理。那么这个方法查询的是哪张表呢?我们来看下上面方法最终执行的 SQL,如下:

: ==>  Preparing: SELECT RES.* from ACT_RU_TASK RES WHERE RES.ASSIGNEE_ is null and exists(select LINK.ID_ from ACT_RU_IDENTITYLINK LINK where LINK.TYPE_ = 'candidate' and LINK.TASK_ID_ = RES.ID_ and ( LINK.USER_ID_ = ? ) ) order by RES.ID_ asc
: ==> Parameters: javaboy(String)
: <==      Total: 1
复制代码

小伙伴们看到,这里的查询涉及到两张表,分别是 ACT_RU_TASK 和 ACT_RU_IDENTITYLINK,两张表联合查询查出来的,那我们来看看 ACT_RU_IDENTITYLINK 表的内容:

小伙伴们看到,TYPE_ 为 candidate 的就表示这个 Task 的候选人,id 为 c5693038-3f42-11ed-b9e2-acde48001122 的 Task 一共有三个候选人,两张表联合查询,才可以查到这个 UserTask 该由谁来处理。

另外一种常见的需求就是,已经知道了要处理的流程实例了,但是不知道应该由谁来处理,此时通过查询 ACT_RU_IDENTITYLINK 表就可以确定一个流程实例都有哪些参与者,如下:

@Test
void test13() {List<ProcessInstance> list = runtimeService.createProcessInstanceQuery().list();for (ProcessInstance pi : list) {List<IdentityLink> identityLinksForProcessInstance = runtimeService.getIdentityLinksForProcessInstance(pi.getId());for (IdentityLink identityLink : identityLinksForProcessInstance) {logger.info("ProcessInstanceId:{},UserId:{}",identityLink.getProcessInstanceId(),identityLink.getUserId());}}
}
复制代码

我们来看看上面这个执行的 SQL,如下:

可以看到,其实就是通过查询 ACT_RU_IDENTITYLINK 表获取我们想要的数据。

2.7 认领任务

对于这种有候选人的任务,我们需要先认领,再处理。认领的本质,其实就是给 ACT_RU_TASK 表中,这个 UserTask 记录的 ASSIGNEE_ 字段设置上值。

认领任务的方式如下:

@Test
void test12() {List<Task> list = taskService.createTaskQuery().taskCandidateUser("javaboy").list();for (Task task : list) {taskService.claim(task.getId(),"javaboy");}
}
复制代码

认领之后,我们再来看 ACT_RU_TASK 表中的数据,如下:

可以看到,此时 ASSIGNEE_ 字段就有值了,同时 CLAIM_TIME 字段也记录了任务的认领时间。

再来看看任务认领执行的 SQL,基本上和我们所想的一致。

2.8 处理任务

认领后的任务该如何处理,这个就和我们上篇文章中介绍的方式一致了,如下:

@Test
void test11() {List<Task> list = taskService.createTaskQuery().taskAssignee("javaboy").list();for (Task task : list) {taskService.complete(task.getId());}
}
复制代码

具体原理上篇文章中已经介绍过了,这里就不再赘述了。

任务执行完成后,ACT_RU_IDENTITYLINK 表中的记录也会随之删除。

2.9 变量与监听器

前面这种方式设置的任务候选人我们是在绘制流程图的时候直接硬编码的,这显然不是一个好办法。如果能通过变量来传递任务的候选人,就会方便很多。

2.9.1 候选人变量

我们可以在绘制流程图的时候,用变量代替直接指定候选人,方式如下:

此时,生成的流程 XML 文件中,UserTask 节点的处理人也就变成了下面这个样子:

<process id="demo01" name="demo01" isExecutable="true"><documentation>demo01</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><userTask id="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" flowable:candidateUsers="${userIds}" flowable:formFieldValidation="true"></userTask><sequenceFlow id="sid-71FB3A81-F753-419D-9A0A-2FC6E5361CED" sourceRef="startEvent1" targetRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3"></sequenceFlow><endEvent id="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></endEvent><sequenceFlow id="sid-DEBE03CD-F247-4EF3-BB67-ABBA94739B0A" sourceRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" targetRef="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></sequenceFlow>
</process>
复制代码

UserTask 节点中的 flowable:candidateUsers="${userIds}" 就表示流程的处理人由 userIds 变量控制。

好了,接下来我们来启动流程,注意,此时启动流程需要传递 userIds 变量,如下:

@Test
void test01() {Map<String, Object> userIds = new HashMap<>();userIds.put("userIds", "javaboy,zhangsan,lisi");ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01",userIds);logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

多个用户之间,用英文 , 隔开。

好了,流程启动成功后,接下来的操作参考 3、4 小节,这里我就不再赘述了。

2.9.2 监听器

当然,我们也可以通过监听器来为 UserTask 设置多个候选处理人用户,首先我们创建一个监听器如下:

public class MyUserTaskListener implements TaskListener {@Overridepublic void notify(DelegateTask delegateTask) {delegateTask.addCandidateUser("javaboy");delegateTask.addCandidateUser("zhangsan");delegateTask.addCandidateUser("lisi");}
}
复制代码

然后在绘制流程图的时候,删除掉 UserTask 分配的用户,然后重新为 UserTask 设置一个监听器:

然后设置一个在创建 UserTask 的时候触发的监听器:

然后我们下载这个流程图对应的 XML 文件,如下:

<process id="demo01" name="demo01" isExecutable="true"><documentation>demo01</documentation><startEvent id="startEvent1" flowable:initiator="INITATOR" flowable:formFieldValidation="true"></startEvent><userTask id="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" flowable:formFieldValidation="true"><extensionElements><flowable:taskListener event="create" class="org.javaboy.flowableidm.MyUserTaskListener"></flowable:taskListener></extensionElements></userTask><sequenceFlow id="sid-71FB3A81-F753-419D-9A0A-2FC6E5361CED" sourceRef="startEvent1" targetRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3"></sequenceFlow><endEvent id="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></endEvent><sequenceFlow id="sid-DEBE03CD-F247-4EF3-BB67-ABBA94739B0A" sourceRef="sid-5F901234-AFF1-480E-9D66-2D196B910BA3" targetRef="sid-D0B9E5BF-8C1A-4F8F-B2C2-F423F5DC556D"></sequenceFlow>
</process>
复制代码

可以看到,在 userTask 节点中,通过 extensionElements 指定了额外的监听器。

好啦,这个流程现在就可以直接启动了,启动时也不需要额外的变量。

流程启动成功后,接下来的操作参考 3、4 小节,这里我就不再赘述了。

2.10 任务回退

当一个任务认领(Claim)之后,但是又不想处理,此时我们可以将任务退回。方式如下:

@Test
void test16() {List<Task> list = taskService.createTaskQuery().taskAssignee("javaboy").list();for (Task task : list) {taskService.setAssignee(task.getId(), null);}
}
复制代码

其实思路很简答,就是重新为任务设置处理人,且处理人为 null,这就是将任务回退了,接下来其他人可以重新认领该任务了。

2.11 修改任务候选人

2.11.1 增加

任务候选人也不是一成不变的,也可以动态修改,当一个流程启动之后,流程已经走到某一个 Task 了,此时我们想要修改该 Task 的候选人,也是可以的,方式如下:

@Test
void test17() {List<Task> list = taskService.createTaskQuery().taskCandidateUser("javaboy").list();for (Task task : list) {taskService.addCandidateUser(task.getId(),"wangwu");}
}
复制代码

添加完成后,查看 ACT_RU_IDENTITYLINK 表,我们发现 wangwu 已经添加进来了:

2.11.2 删除

如果想要删除一个候选人,方式如下:

@Test
void test18() {List<Task> list = taskService.createTaskQuery().taskCandidateUser("javaboy").list();for (Task task : list) {taskService.deleteCandidateUser(task.getId(), "wangwu");}
}
复制代码

删除成功之后,ACT_RU_IDENTITYLINK 表中对应的数据也就被清除掉了。

2.12 查询历史数据

如果一个流程执行结束了,我们还想查询这个流程曾经涉及到的参与者,可以通过如下方式查询:

@Test
void test14() {List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery().list();for (HistoricProcessInstance historicProcessInstance : list) {List<HistoricIdentityLink> links = historyService.getHistoricIdentityLinksForProcessInstance(historicProcessInstance.getId());for (HistoricIdentityLink link : links) {logger.info("userId:{},taskId:{},type:{},processInstanceId:{}",link.getUserId(),link.getTaskId(),link.getType(),link.getProcessInstanceId());}}
}
复制代码

这里最终其实就是去 ACT_HI_IDENTITYLINK 表中查询,对应的 SQL 如下:

上面这是查询一个流程的参与人,当然我们也可以查询一个 Task 的候选人与处理人,如下:

@Test
void test15() {List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().list();for (HistoricTaskInstance historicTaskInstance : list) {List<HistoricIdentityLink> links = historyService.getHistoricIdentityLinksForTask(historicTaskInstance.getId());for (HistoricIdentityLink link : links) {logger.info("userId:{},taskId:{},type:{},processInstanceId:{}", link.getUserId(), link.getTaskId(), link.getType(), link.getProcessInstanceId());}}
}
复制代码

查询对应的 SQL 如下:

和我们所想的基本一致。

前面松哥和大家分享的都是给 UserTask 设置处理人或者是候选用户,不过小伙伴们也知道,在我们为 UserTask 设置处理人的时候,除了设置单个的处理人,也可以设置 Group,就是某一个用户组内的所有用户都可以处理该 Task。

在 Flowable 中使用 Group 去归类某一类用户,但是这个实际上类似于我们在自己系统中平时所用的角色 Role。也就是说,我们可以按照角色去给每一个 UserTask 设置处理人。

接下来松哥就来和小伙伴们聊一聊这里的一些细节。

2.13 用户与用户组

首先我们先来看下用户组的一些基本操作。

2.13.1 添加组

组的属性相对来说少一些,添加方式和 user 比较像:

@Test
void test09() {GroupEntityImpl g = new GroupEntityImpl();g.setName("组长");g.setId("leader");g.setRevision(0);identityService.saveGroup(g);
}
复制代码

添加之后,组的信息保存在 ACT_ID_GROUP 表中,如下图:

组创建好之后,接下来还要给组添加用户,添加方式如下:

identityService.createMembership("zhangsan", "leader");
identityService.createMembership("lisi", "leader");
复制代码

这就是设置 zhangsan 和 lisi 是组长(注意用户和组的关联关系表中有外键,所以需要确保两个参数都是真实存在的)。

添加了关联关系之后,我们再去查看 ACT_ID_MEMBERSHIP 表,如下:

掉用如下方法可以删除关联关系:

identityService.deleteMembership("zhangsan","leader");
复制代码

2.13.2 修改组

如下,将 id 为 leader 的组名更新为主管,如下:

Group g = identityService.createGroupQuery().groupId("leader").singleResult();
g.setName("主管");
identityService.saveGroup(g);
复制代码

2.13.3 删除组

删除组方式如下:

identityService.deleteGroup("leader");
复制代码

删除组的同时,也会删除掉组和用户之间的关联关系,不过不用担心用户被删除。

2.13.4 查询组

可以根据 id 或者 name 或者组员信息等去查询组:

//根据 id 查询组信息
Group g1 = identityService.createGroupQuery().groupId("leader").singleResult();
System.out.println("g1.getName() = " + g1.getName());
//根据 name 查询组信息
Group g2 = identityService.createGroupQuery().groupName("组长").singleResult();
System.out.println("g2.getId() = " + g2.getId());
//根据用户查询组信息(组里包含该用户)
List<Group> list = identityService.createGroupQuery().groupMember("zhangsan").list();
for (Group group : list) {System.out.println("group.getName() = " + group.getName());
}
复制代码

2.14 设置候选组

在我们绘制流程图的时候,我们可以为 UserTask 设置一个候选组,方式如下:

从这个地方大家也可以看到,后选择是可以给多个的。

好了,设置完成后,我们下载流程图的 XML 文件,然后来看下这个地方与众不同之处:

<process id="demo01" name="测试流程" isExecutable="true"><documentation>测试流程</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><userTask id="sid-F2F3C468-79B9-447B-943F-7CD18CE9BECF" flowable:candidateGroups="leader" flowable:formFieldValidation="true"></userTask><sequenceFlow id="sid-79C79920-2AD8-48FE-A59C-CC4D23C1895D" sourceRef="startEvent1" targetRef="sid-F2F3C468-79B9-447B-943F-7CD18CE9BECF"></sequenceFlow><endEvent id="sid-2236991E-3643-4590-9001-E22C256CA584"></endEvent><sequenceFlow id="sid-51105EB7-07F6-4190-9B2E-8F1F20A307D1" sourceRef="sid-F2F3C468-79B9-447B-943F-7CD18CE9BECF" targetRef="sid-2236991E-3643-4590-9001-E22C256CA584"></sequenceFlow>
</process>
复制代码

小伙伴们看到,flowable:candidateGroups="leader" 就表示这个任务由一个候选用户组来处理,如果有多个候选的用户组,则不同用户组之间用 , 隔开。

当然,这是硬编码。如果想像候选用户一样,通过动态变量来传递用户组名称也是可以的,具体做法像下面这样:

这样,最终生成的 XML 文件则类似这样: flowable:candidateGroups="${g1}"

2.15 根据用户组查询任务

接下来,我们部署并启动一个流程,具体的部署和启动方式松哥在之前的文章中都已经和大家介绍过了,这里简单看下方法就行了:


@Test
void test01() {Map<String, Object> variables = new HashMap<>();variables.put("g1", "leader");ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01",variables);logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

这个就是流程启动的过程,注意启动的时候加了参数,用来描述下一个 UserTask 的处理组。

启动成功之后,我们可以在 ACT_RU_IDENTITYLINK 表中查看到用户组和 UserTask 之间的关系:

接下来我们可以按照查询候选人任务的方式,查询 zhangsan 需要完成的工作,如下:

@Test
void test19() {List<Task> list = taskService.createTaskQuery().taskCandidateUser("zhangsan").list();for (Task task : list) {logger.info("name:{},createTime:{}", task.getName(), task.getCreateTime());}
}
复制代码

这个查询的内部实现,我们可以拆分为两步:

  1. 查询出来 zhangsan 是属于哪个 group,这个查询执行的 SQL 如下:
SELECT RES.* from ACT_ID_GROUP RES WHERE exists(select 1 from ACT_ID_MEMBERSHIP M where M.GROUP_ID_ = RES.ID_ and M.USER_ID_ = ?) order by RES.ID_ asc
复制代码

这个查询中有一个参数,参数的值就是 zhangsan,上面这个 SQL 可以查询出来 zhangsan 这个用户属于 leader 这个分组,在接下来的查询中,会 zhangsan 和 leader 两个参数都会用到。

  1. 查询 zhangsan 或者 leader 的任务,执行 SQL 如下:
SELECT RES.* from ACT_RU_TASK RES WHERE RES.ASSIGNEE_ is null and exists(select LINK.ID_ from ACT_RU_IDENTITYLINK LINK where LINK.TYPE_ = 'candidate' and LINK.TASK_ID_ = RES.ID_ and ( LINK.USER_ID_ = ? or ( LINK.GROUP_ID_ IN ( ? ) ) ) ) order by RES.ID_ asc
复制代码

可以看到,这个查询里,有两个参数了,两个参数的值分别是 zhangsan 和 leader。

也就是说,虽然我们这里代码写的是按照 zhangsan 去查询,实际上查询的是 zhangsan 所属的用户组的 Task(这个逻辑也好理解,因为 zhangsan 所属的用户组的 Task 实际上也就是 zhangsan 的 Task)。

当然,我们也可以直接按照 group 去查询,如下:

@Test
void test20() {List<Task> list = taskService.createTaskQuery().taskCandidateGroup("leader").list();for (Task task : list) {logger.info("name:{},createTime:{}", task.getName(), task.getCreateTime());}
}
复制代码

这个查询原理跟上面的差不多,不过省事的是,这里一条 SQL 就搞定了(不需要根据用户名查询用户所属的分组了),如下:

SELECT RES.* from ACT_RU_TASK RES WHERE RES.ASSIGNEE_ is null and exists(select LINK.ID_ from ACT_RU_IDENTITYLINK LINK where LINK.TYPE_ = 'candidate' and LINK.TASK_ID_ = RES.ID_ and ( ( LINK.GROUP_ID_ IN ( ? ) ) ) ) order by RES.ID_ asc
复制代码

好啦,当这些任务查询出来后,接下来该如何执行,就和前面介绍的内容一样了,我这里就不再赘述了。

3. ServiceTask

ServiceTask 从名字上看就是服务任务,它的图标一般是像下面这样:

ServiceTask 一般由系统自动完成,当流程走到这一步的时候,不会自动停下来,而是会去执行我们提前在 ServiceTask 中配置好的方法。

3.1 实践

我们通过一个简单的例子来看一下 ServiceTask 要怎么玩。

假设我有如下一个简单的流程图:

中间这个就是一个 ServiceTask。

当流程执行到 ServiceTask 的时候,具体要做哪些事情?有三种不同的方式来设置这里的任务,我们分别来看。

3.1.1 监听类

首先我们可以设置一个监听类,这个监听类有一个硬性规定就是需要实现 JavaDelegate 接口,像下面这样:

public class MyServiceTask implements JavaDelegate {@Overridepublic void execute(DelegateExecution execution) {System.out.println("========MyServiceTask==========");}
}
复制代码

在这个监听类中我们可以完成一些操作,通过这个 execution 也可以获取到在流程节点之间传输的变量。

这个类定义好之后,接下来我们在流程定义的时候,配置这个类的全路径即可,如下图:

这个配置对应的 XML 内容如下:

  <process id="demo01" name="测试流程" isExecutable="true"><documentation>测试流程</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><sequenceFlow id="sid-33A78082-C2FD-48BE-8B87-99FB20F0B331" sourceRef="startEvent1" targetRef="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8"></sequenceFlow><serviceTask id="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8" flowable:class="org.javaboy.flowableidm.MyServiceTask"></serviceTask><endEvent id="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></endEvent><sequenceFlow id="sid-0698809E-0A6C-4B92-A167-AE96A8CB75F2" sourceRef="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8" targetRef="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></sequenceFlow></process>
复制代码

小伙伴们看到,在 ServiceTask 标签中的 flowable:class="org.javaboy.flowableidm.MyServiceTask" 就表示 ServiceTask 执行的服务类。

配置完成后,我们可以部署并启动这个流程,由于这个流程除了开始和结束,就这一个节点,所以流程一启动就自动结束了。不过在这个过程中,我们可以看到控制台打印出来了日志,说明这个 ServiceTask 确实是执行了。

3.1.2 委托表达式

我们也可以配置委托表达式。

委托表达式是指将一个实现了 JavaDelegate 接口的类注册到 Spring 容器中,然后我们在流程节点的配置中不用写完整的类名了,只需要写 Spring 容器中的 Bean 名称即可。

像下面这样:

@Component
public class MyServiceTask implements JavaDelegate {@Overridepublic void execute(DelegateExecution execution) {System.out.println("========MyServiceTask==========");}
}
复制代码

这个类注册到 Spring 容器中的默认名称是类名首字母小写,即 myServiceTask。

现在我们在流程图中,可以按照如下方式进行配置:

对应的 XML 文件如下:

<process id="demo01" name="测试流程" isExecutable="true"><documentation>测试流程</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><sequenceFlow id="sid-33A78082-C2FD-48BE-8B87-99FB20F0B331" sourceRef="startEvent1" targetRef="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8"></sequenceFlow><serviceTask id="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8" flowable:delegateExpression="${myServiceTask}"></serviceTask><endEvent id="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></endEvent><sequenceFlow id="sid-0698809E-0A6C-4B92-A167-AE96A8CB75F2" sourceRef="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8" targetRef="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></sequenceFlow>
</process>
复制代码

可以看到,flowable:delegateExpression="${myServiceTask}" 就表示执行的一个表达式。

测试过程同 2.1 小节,我就不再赘述了。

最后总结一下,委托表达式,一定是 JavaDelegate 接口的实现类,将这个实现类注册到 Spring 容器中,然后在使用的时候,根据 Bean 的名称从 Spring 容器中查找即可。

3.1.3 表达式

我们也可以使用表达式。

表达式就是一个普通类的普通方法,将这个普通类注册到 Spring 容器中,然后表达式中还可以执行这个类中的方法,类似下面这样,任意定义一个 Java 类:

@Component
public class MyServiceTask2 {public void hello() {System.out.println("========MyServiceTask2==========");}
}
复制代码

然后在流程图中按照如下方式进行配置:

表达式中有一部分内容隐藏了,完整的表达式是 ${myServiceTask2.hello()}

对应的 XML 文件如下:

<process id="demo01" name="测试流程" isExecutable="true"><documentation>测试流程</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><sequenceFlow id="sid-33A78082-C2FD-48BE-8B87-99FB20F0B331" sourceRef="startEvent1" targetRef="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8"></sequenceFlow><serviceTask id="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8" flowable:expression="${myServiceTask2.hello()}"></serviceTask><endEvent id="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></endEvent><sequenceFlow id="sid-0698809E-0A6C-4B92-A167-AE96A8CB75F2" sourceRef="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8" targetRef="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></sequenceFlow>
</process>
复制代码

可以看到,表达式的内容是 flowable:expression="${myServiceTask2.hello()}

测试方式同 2.1 小节,这里我不再赘述。

3.2 类中字段

可能有小伙伴注意到,我们在绘制流程图的时候,还可以为类设置一个字段。

例如我想给 ServiceTask 的执行类设置一个 username 字段,如下:

设置完成后,对应的 XML 如下:

<process id="demo01" name="测试流程" isExecutable="true"><documentation>测试流程</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><sequenceFlow id="sid-33A78082-C2FD-48BE-8B87-99FB20F0B331" sourceRef="startEvent1" targetRef="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8"></sequenceFlow><serviceTask id="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8" flowable:delegateExpression="${myServiceTask}"><extensionElements><flowable:field name="username"><flowable:string><![CDATA[javaboy]]></flowable:string></flowable:field></extensionElements></serviceTask><endEvent id="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></endEvent><sequenceFlow id="sid-0698809E-0A6C-4B92-A167-AE96A8CB75F2" sourceRef="sid-6FA66E2A-F8E6-4F10-8FA2-6450408E17D8" targetRef="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></sequenceFlow>
</process>
复制代码

可以看到,这里通过 extensionElements 节点描述了额外的信息。

接下来,我们就可以在 Java 类中访问到这个变量了,如下:

@Component
public class MyServiceTask implements JavaDelegate {Expression username;@Overridepublic void execute(DelegateExecution execution) {System.out.println("username.getExpressionText() = " + username.getExpressionText());System.out.println("username.getValue(execution) = " + username.getValue(execution));System.out.println("========MyServiceTask==========");}
}
复制代码

想要获取到 username 对应的值,上面这段代码中,松哥给大家演示了两种方式。

4. 脚本任务

个人感觉脚本任务和我们前面说的 ServiceTask 很像,都是流程走到这个节点的时候自动做一些事情,不同的是,在 ServiceTask 中,流程在这个节点中所做的事情是用 Java 代码写的,在脚本任务中,流程在这个节点中所做的事情则是用其他一些脚本语言如 JavaScript、Groovy、Juel 等写的。

脚本任务的图标如下图所示:

4.1 实践

写一个简单的例子我们来一起看下。

4.1.1 JavaScript 脚本

我们先来看用 JavaScript 写这个脚本。

假设我有如下流程图:

中间这个节点就是一个脚本任务。

选中该节点,我们先配置脚本语言是 JavaScript,如下图:

这里也可以使用简写的 js。

然后再点击右边的脚本,配置脚本,如下图:

上面这里我写了两行 JavaScript 脚本:

  1. 第一行表示流程执行到这里的时候,需要做一个简单的加法运算,a 和 b 两个变量则需要流程传入进来。
  2. 第二行表示往流程中存储一个名为 sum 的变量,变量值就是前面计算的结果,其中 execution 是一个内置变量。这个就类似于我们启动流程时候传入的变量一样。

在 ES6 中我们常用的 let 关键字这里并不支持,这个地方小伙伴们要注意。

配置完成之后,我们下载这个脚本来看下对应的 XML 文件是什么样子:

<process id="demo01" name="测试流程" isExecutable="true"><documentation>测试流程</documentation><startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent><sequenceFlow id="sid-33A78082-C2FD-48BE-8B87-99FB20F0B331" sourceRef="startEvent1" targetRef="sid-8D88DFF6-0F37-42FA-9F94-29FE30536094"></sequenceFlow><endEvent id="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></endEvent><sequenceFlow id="sid-0698809E-0A6C-4B92-A167-AE96A8CB75F2" sourceRef="sid-8D88DFF6-0F37-42FA-9F94-29FE30536094" targetRef="sid-A5F11956-15EA-4574-98D0-29A4E3DB5495"></sequenceFlow><scriptTask id="sid-8D88DFF6-0F37-42FA-9F94-29FE30536094" scriptFormat="JavaScript" flowable:autoStoreVariables="false"><script><![CDATA[var sum=a+b;
execution.setVariable("sum",sum);]]></script></scriptTask>
</process>
复制代码

小伙伴们看到,scriptTask 中内嵌了一个 script 节点,里边就是我们自己写的脚本内容。

好啦,接下来小伙伴们就可以部署并启动这个流程了,启动代码如下:

@Test
void test01() {Map<String, Object> variables = new HashMap<>();variables.put("a", 99);variables.put("b", 98);ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}
复制代码

大家注意启动的时候传递 a 和 b 两个变量。这个流程启动之后,直接就执行结束了,因为流程到达 scriptTask 并不会停止。

不过我们可以在 ACT_HI_VARINST 表中查看流程运行信息:

可以看到,相关的变量和变量值都保存着。

4.1.2 Groovy 脚本

看懂了 JavaScript 脚本,Groovy 就好懂了。不过 JavaScript 脚本估计大部分搞 Java 的小伙伴都懂,但是 Groovy 可能会比较陌生,我简单介绍下:

Groovy 是 Apache 旗下的一门基于 JVM 平台的动态/敏捷编程语言,在语言的设计上它吸纳了 Python、Ruby 和 Smalltalk 语言的优秀特性,语法非常简练和优美,开发效率也非常高(编程语言的开发效率和性能是相互矛盾的,越高级的编程语言性能越差,因为意味着更多底层的封装,不过开发效率会更高,需结合使用场景做取舍)。并且,Groovy 可以与 Java 语言无缝对接,在写 Groovy 的时候如果忘记了语法可以直接按 Java 的语法继续写,也可以在 Java 中调用 Groovy 脚本,都可以很好的工作,这有效的降低了 Java 开发者学习 Groovy 的成本。Groovy 也并不会替代 Java,而是相辅相成、互补的关系,具体使用哪门语言这取决于要解决的问题和使用的场景。

如果我们想要在流程中使用 Groovy 脚本,那么首先设置脚本格式为 Groovy:

然后设置脚本内容如下:

这段脚本表示流程执行到这个节点的时候输出一个 "hello groovy"(如果你熟悉 Groovy 脚本的话,就知道这段脚本其实也可以直接写 Java 代码,也能执行)。

另外说一句,使用 Groovy 脚本,千万别忘了加 Groovy 依赖,如下:

<dependency><groupId>org.codehaus.groovy</groupId><artifactId>groovy-all</artifactId><version>3.0.13</version>
</dependency>
复制代码

4.1.3 Juel 脚本

Juel 是 Java Unified Expression Language 的简称,它具有高性能,插件式缓存,小体积,支持方法调用和多参数调用,可插拔等多种特性,它是 JSP2.1 标准 (JSR-245) 中定义的一部分。尽管 EL 表达式是伴随着 JSP 而生,但现在已经可以在非 JS P应用中使用,相关的 API 放在 javax.el 包里面。

其实像我们之前写的 ${xxx} 这种表达式,其实就是 Juel 了。

来一个简单的例子看下。假设我们想在流程中使用 juel,首先设置脚本格式为 juel:

稀土掘金首页沸点课程返场5折直播活动商城APP邀请有礼插件探索稀土掘金搜索创作者中心vip会员5Java查序的头像梳理流程引擎 Flowable 四大常见任务相关推荐

  1. 稀土掘金首页沸点课程直播活动竞赛商城APP邀请有礼插件探索稀土掘金搜索创作者中心vip会员4Java查序的头像一篇文章带你玩转二叉树的层序遍历 | 十道题巩固练习

    题目描述 解题思路 由题可知,要求输出是按照二叉树每层的元素来做输出 我使用队列来对二叉树每层元素进行存储和输出 根据队列的长度可以判断出当前层的元素个数并遍历 首先去判断入参是否为 null 如果为 ...

  2. 小白学流程引擎-FLowable(一) —FLowable是什么

    小白学流程引擎-FLowable(一) | FLowable是什么 一.什么是流程引擎? 通俗的说,流程引擎就是多种业务对象在一起合作完成某件事情的步骤,把步骤变成计算机能理解的形式就是流程引擎. 流 ...

  3. Spring Boot 整合流程引擎 Flowable,so easy

    为啥想写 flowable 呢?原因很简单,因为最近在录的 tienchin 项目视频会用到,先写一篇文章和大家打打预防针,后面视频再细讲. 流程引擎,也算是一个比较常见的工具了,我们在日常的很多开发 ...

  4. 小白学流程引擎-FLowable(五) — BPMN2.0模型规范

    前言: 不用到处百度BPMN2的博客了,本篇文章带你系统掌握BPMN2规范的核心知识点.全文2万字,全覆盖BPMN2知识点,图文并茂,泡杯咖啡,慢慢细品- 一.BPMN是什么 BPMN(Busines ...

  5. 整理流程引擎Flowable的前端流程设计器Modeler

    1.Flowable Modeler 官方提供的设计器,不是基于vue,另外拆分整合困难. 2.bpmn-js GitHub地址:https://github.com/bpmn-io/bpmn-js ...

  6. Spring Boot 整合流程引擎 Flowable(附源码地址)

    一.导入依赖 flowable依赖: <dependency><groupId>org.flowable</groupId><artifactId>fl ...

  7. 淘宝爆款返场什么意思?淘宝爆款返场怎么报名?

    一.淘宝爆款返场什么意思? 淘宝爆款返场栏目定义:针对参加活动的爆款商品,奖励其在7天后手机端独立频道同活动价返场再售的活动形式.即参加聚划算.淘抢购等爆款商品再次促销,获得更多的展现. 二.淘宝爆款 ...

  8. 可以通过限定ip来限制用户重复登录么_王者荣耀2020皮肤返场投票活动地址 五周年限定皮肤返场投票开启公告...

    王者荣耀游戏中2020周年庆皮肤返场投票活动终于开启了,那么下面小编就跟大家介绍一下五周年限定皮肤返场投票开启公告以及详细的投票地址吧. 王者荣耀2020限定皮肤返场投票开启 亲爱的召唤师: 限定皮肤 ...

  9. flowable 流程表单_flowable 流程引擎总结

    最近公司使用Flowable开发了自己的OA系统,因此对Flowable的相关内容进行如下总结 一.Flowable 是什么 目前最新版是Flowable 6.4.2(2019年07月26日) Flo ...

  10. 流程引擎之Flowable简介

    背景 Flowable 是一个流行的轻量级的采用 Java 开发的业务流程引擎,通过 Flowable 流程引擎,我们可以部署遵循 BPMN2.0 协议的流程定义(一般为XML文件)文件,并能创建流程 ...

最新文章

  1. 【绝对靠谱】Vue生成二维码Qrcode,可插入二维码中心logo图标,可以设置二维码颜色大小等属性
  2. 人脸和宇宙是啥关系?看物理学家怎样用重整化群流模型重新理解视觉
  3. 太晚睡不着的落寞与开心(记近况)
  4. 文件上传利器SWFUpload使用指南
  5. angularjs 添加拦截器
  6. cursor用法java,Cursor的基本使用方法
  7. 作为一名通信老司机,我是如何看待翼龙通信无人机救灾的?
  8. [032] 微信公众帐号开发教程第8篇-文本消息中使用网页超链接(转)
  9. 工厂供电MATLAB仿真,工厂供电课程设计---基于MATLAB的电力电子系统仿真
  10. 第一章 ArcGis Server简介
  11. 第三篇:命名空间namespace的用法
  12. Origin2018安装与使用(整理中)
  13. 电子电路绘图与仿真软件
  14. 解决Ubuntu远程连接mysql连不上的问题
  15. ZOJ3551 Bloodsucker(概率dp)
  16. mac 查看 本地网络代理
  17. 创建工程文件(完整流程)
  18. nexmo 验证码的使用
  19. Kivy转apk——使用打包虚拟机(亲测~)
  20. 最常用的三角函数值和三角变形公式

热门文章

  1. 如何测试webservice接口
  2. kf真空接头标准尺寸_【真空】真空导入工艺详解!附具体操作步骤
  3. 完美世界国际版不用外挂多开的方法
  4. 袁亚湘院士上《开讲啦》变数学魔术啦!
  5. 618|Python购书攻略
  6. 《电路(邱关源)》第五版重难点记录(长期更新)
  7. 如何批量下载上海证券交易所上市公司年报
  8. 杭州/北京内推 | 蚂蚁集团数字身份及安全生态团队招聘学术实习生
  9. couchbase java 手册_couchBase在java中使用的基本知识
  10. Ember component