• Flowable 中的网关、流程变量以及历史流程


    今天这篇文章,松哥和大家梳理一下 Flowable 中的网关、流程变量以及历史流程的玩法。

    1. 三大网关

    Flowable 中网关类型其实也不少,常见的主要有三种类型,分别是:

    1. 排他网关
    2. 并行网关
    3. 包容网关

    这三个里边最常用的当然就是排他网关了,今天松哥就来和小伙伴们聊一聊这三种网关,一起来体验一把这三种网关各自的特征。

    1.1. 排他网关

    首先就是排他网关了,这个也叫互斥网关,长得像下图这样:

    排他网关可以有 N 个入口,但是只有一个有效出口。

    松哥举一个例子:

    假设我有一个请假流程,请假 1 天,组长审批,请假小于 3 天,项目经理审批,请假大于 3 天,总监审批,据此,我们可以绘制如下流程图:

    在这个流程图中,当流程从排他网关出来的时候,我们设置一个变量,根据变量的值,来决定下一个走哪一个 Task,例如组长审批,我们做如下配置:

    这个流条件表示当 days 这个变量的值小于等于 1 的时候,就会进入到组长审批这个 Task。

    按照类似的方式,我们来设置经理审批:

    最后,总监审批的条件如下:

    最终,我们来看下这个流程对应的 XML 文件,如下:

    1. <process id="demo01" name="测试流程" isExecutable="true">
    2. <documentation>测试流程</documentation>
    3. <startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent>
    4. <exclusiveGateway id="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5"></exclusiveGateway>
    5. <sequenceFlow id="sid-DF97CC8B-3AD5-447D-AE67-1082CAB7B189" sourceRef="startEvent1" targetRef="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5"></sequenceFlow>
    6. <userTask id="sid-B4CD08AF-52B5-44F2-AC45-B2F5E154A5F0" name="组长审批" flowable:formFieldValidation="true"></userTask>
    7. <userTask id="sid-07B7951C-4E76-4639-989C-407C610C5BA8" name="经理审批" flowable:formFieldValidation="true"></userTask>
    8. <userTask id="sid-1A81B40F-D8D4-4158-B0B9-26DB8FB7DD2E" name="总监审批" flowable:formFieldValidation="true"></userTask>
    9. <endEvent id="sid-0F56FE56-1A8C-4B47-8F0D-196700DDF7B8"></endEvent>
    10. <sequenceFlow id="sid-E4B4B580-F078-4BB9-B5D3-966E80737C4C" sourceRef="sid-B4CD08AF-52B5-44F2-AC45-B2F5E154A5F0" targetRef="sid-0F56FE56-1A8C-4B47-8F0D-196700DDF7B8"></sequenceFlow>
    11. <endEvent id="sid-F05670CB-A8F4-44A3-B53D-46CFB6F65581"></endEvent>
    12. <sequenceFlow id="sid-3EC62E5D-ACDA-480E-93B4-C24D8F6E9042" sourceRef="sid-07B7951C-4E76-4639-989C-407C610C5BA8" targetRef="sid-F05670CB-A8F4-44A3-B53D-46CFB6F65581"></sequenceFlow>
    13. <endEvent id="sid-52711414-1769-4EC3-9AE5-6BA426123095"></endEvent>
    14. <sequenceFlow id="sid-C81500B2-D1EA-429F-8402-A3D8C8CA0E29" sourceRef="sid-1A81B40F-D8D4-4158-B0B9-26DB8FB7DD2E" targetRef="sid-52711414-1769-4EC3-9AE5-6BA426123095"></sequenceFlow>
    15. <sequenceFlow id="sid-807C7B79-4AFA-4525-847F-4D0FE1C0F0F3" name="小于1天" sourceRef="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5" targetRef="sid-B4CD08AF-52B5-44F2-AC45-B2F5E154A5F0">
    16. <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days<=1}]]></conditionExpression>
    17. </sequenceFlow>
    18. <sequenceFlow id="sid-3D3DF742-BF47-4536-9EE9-747CD284A1BA" name="1-3天" sourceRef="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5" targetRef="sid-07B7951C-4E76-4639-989C-407C610C5BA8">
    19. <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days>1 && days<=3}]]></conditionExpression>
    20. </sequenceFlow>
    21. <sequenceFlow id="sid-2AD41E43-AFEC-47A1-B8D1-0B4299434BF8" name="大于3天" sourceRef="sid-C4E389D6-C507-4B8E-8469-2288AA5B44A5" targetRef="sid-1A81B40F-D8D4-4158-B0B9-26DB8FB7DD2E">
    22. <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days>3}]]></conditionExpression>
    23. </sequenceFlow>
    24. </process>
    25. 复制代码

    可以看到,在 sequenceFlow 标签中,有一个 conditionExpression 标签,这个标签的内容就是具体的条件了。

    现在,我们部署一下这个流程,然后按照如下方式来启动:

    1. @Test
    2. void test01() {
    3. Map<String, Object> variables = new HashMap<>();
    4. variables.put("days", 3);
    5. ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);
    6. logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
    7. }
    8. 复制代码

    注意,这个启动的时候,传入一个 days 变量,系统将来会根据这个变量来决定这个流程要走到哪一个 Task。流程启动成功之后,我们去观察 ACT_RU_TASK 表,就可以看到流程的执行是否和我们所预想的一致。

    1.2. 并行网关

    并行网关,从名字上大概也能看出来,这种网关一般用在并行任务上,并行网关如下图:

    并行网关一般是成对出现的,一个出现的并行网关用来分流,第二个出现的并行网关用来聚合。

    我画一个简单的并行网关的例子,如下图:

    小伙伴们看到,这是一个简化的生产笔记本的流程图,当屏幕和键盘都生产好之后,再进行组装,整个流程图中存在两个并行网关(成对出现)。

    在这个流程图中,连接线上是不需要设置条件的(不同于拍他网关),这里即使你设置了条件,这个条件也是不会生效的。

    我们来看下这个并行网关流程图对应的 XML 文件,如下:

    1. <process id="demo01" name="测试流程" isExecutable="true">
    2. <documentation>测试流程</documentation>
    3. <startEvent id="sid-4F7F76BA-526A-4D8C-B45A-02FC1C56CA47" flowable:formFieldValidation="true"></startEvent>
    4. <sequenceFlow id="sid-11130848-EA1F-458A-A45D-49CBC49428C8" sourceRef="sid-4F7F76BA-526A-4D8C-B45A-02FC1C56CA47" targetRef="sid-6D01D4BE-C475-4270-8745-92752EA2C038"></sequenceFlow>
    5. <parallelGateway id="sid-6D01D4BE-C475-4270-8745-92752EA2C038"></parallelGateway>
    6. <userTask id="sid-54DD6BFA-FE6C-4DE7-9038-3DEEAF85002C" name="生产屏幕" flowable:assignee="zhangsan" flowable:formFieldValidation="true">
    7. <extensionElements>
    8. <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
    9. </extensionElements>
    10. </userTask>
    11. <sequenceFlow id="sid-8DD3383C-45D1-4EAF-9A22-702A5B9D0869" sourceRef="sid-6D01D4BE-C475-4270-8745-92752EA2C038" targetRef="sid-54DD6BFA-FE6C-4DE7-9038-3DEEAF85002C"></sequenceFlow>
    12. <userTask id="sid-7797ED55-155F-4D17-8EA5-DE40434C421B" name="生产键盘" flowable:assignee="lisi" flowable:formFieldValidation="true">
    13. <extensionElements>
    14. <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
    15. </extensionElements>
    16. </userTask>
    17. <sequenceFlow id="sid-6E992E8B-CF71-411D-B537-42FEDF4F4209" sourceRef="sid-6D01D4BE-C475-4270-8745-92752EA2C038" targetRef="sid-7797ED55-155F-4D17-8EA5-DE40434C421B"></sequenceFlow>
    18. <sequenceFlow id="sid-8DCA9516-FFED-4781-9ACC-530DC6E63755" sourceRef="sid-7797ED55-155F-4D17-8EA5-DE40434C421B" targetRef="sid-98D3C336-9AD9-4964-9CCB-496C850EE40F"></sequenceFlow>
    19. <sequenceFlow id="sid-EE80AE42-D021-4B9F-A91E-BD37C512EE65" sourceRef="sid-54DD6BFA-FE6C-4DE7-9038-3DEEAF85002C" targetRef="sid-98D3C336-9AD9-4964-9CCB-496C850EE40F"></sequenceFlow>
    20. <userTask id="sid-4FFE361A-E2AF-4481-BACF-1E618E8C4A26" name="组装" flowable:assignee="javaboy" flowable:formFieldValidation="true">
    21. <extensionElements>
    22. <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
    23. </extensionElements>
    24. </userTask>
    25. <sequenceFlow id="sid-8CABC6E8-E36A-4814-B897-817D4A9F231C" sourceRef="sid-98D3C336-9AD9-4964-9CCB-496C850EE40F" targetRef="sid-4FFE361A-E2AF-4481-BACF-1E618E8C4A26"></sequenceFlow>
    26. <endEvent id="sid-BF02170B-8138-4867-AE01-E3B29505183D"></endEvent>
    27. <sequenceFlow id="sid-F72B2A15-913F-436E-8AD7-6A6FB190E197" sourceRef="sid-4FFE361A-E2AF-4481-BACF-1E618E8C4A26" targetRef="sid-BF02170B-8138-4867-AE01-E3B29505183D"></sequenceFlow>
    28. <parallelGateway id="sid-98D3C336-9AD9-4964-9CCB-496C850EE40F"></parallelGateway>
    29. </process>
    30. 复制代码

    现在我们把这个流程部署并启动。

    流程启动成功之后,我们发现在 ACT_RU_TASK 表中有两个需要执行的 Task,如下图:

    这两个 Task,如果只执行掉其中一个,那么还剩下另外一个 Task,如果两个都执行了,那么你就会看到一个新的 Task,如下图(两个并行任务执行完成后,进入到下一个任务):

    好啦,这就是并行网关。

    1.3. 包容网关

    包容网关,有时候也叫相容网关、兼容网关等,如下图:

    包容谁呢?包容排他网关和并行网关。也就是说,这种包容网关可以根据实际条件转为排他网关或者并行网关。

    举个栗子:

    假如说报销金额大于 500,zhangsan 审批,报销金额大于 1000,则需要 zhangsan 和 lisi 同时审批,且 zhangsan 和 lisi 审批无先后顺序。

    据此,我绘制如下流程图:

    在报销金额大于 500 上设置如下条件:

    大于 1000 上设置如下条件:

    接下来我们来部署好这个流程。

    部署好之后,我们首先来启动流程,第一次启动的时候,我们设置报销金额为 666,如下:

    1. @Test
    2. void test01() {
    3. Map<String, Object> variables = new HashMap<>();
    4. variables.put("money", 666);
    5. ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);
    6. logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
    7. }
    8. 复制代码

    流程启动之后,我们在 ACT_RU_TASK 表中可以看到,该 zhangsan 审批了,如下:

    zhangsan 审批之后,就是 wangwu 审批了,我就不演示了。

    假设我们启动流程的时候,报销金额为 2000,如下:

    1. @Test
    2. void test01() {
    3. Map<String, Object> variables = new HashMap<>();
    4. variables.put("money", 2000);
    5. ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);
    6. logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
    7. }
    8. 复制代码

    那么此时你就会看到,在 ACT_RU_TASK 表中,出现了两条记录,分别是 zhangsan 审批和 lisi 审批,此时这两个审批就是一个并行任务了:

    接下来就按并行任务的模式来,这两个人都审批了,才会进入到 wangwu 审批。

    这就是兼容网关的特点,即根据实际情况,会变成排他网关或者并行网关。

    好啦,三种常见的网关就和小伙伴们分享完啦,感兴趣的小伙伴赶紧试一试吧~

    2. 四种变量设置方式

    [TOC]

    在之前的文章中,松哥也有和小伙伴们使用过流程变量,然而没有和大家系统的梳理过流程变量的具体玩法以及它对应的数据表详情,今天我们就来看看 Flowable 中流程变量的详细玩法。

    2.1. 为什么需要流程变量

    首先我们来看看为什么需要流程变量。

    举一个简单的例子,假设我们有如下一个流程:

    这是一个请假流程,那么谁请假、请几天、起始时间、请假理由等等,这些都需要说明,不然领导审批的依据是啥?那么如何传递这些数据,我们就需要流程变量。

    2.2. 流程变量的分类

    整体上来说,目前流程变量可以分为三种类型:

    1. 全局流程变量:在整个流程执行期间,这个流程变量都是有效的。
    2. 本地流程变量:这个只针对流程中某一个具体的 Task(任务)有效,这个任务执行完毕后,这个流程变量就失效了。
    3. 临时流程变量:顾名思义就是临时的,这个不会存入到数据库中。

    在接下来的内容中,我会跟大家挨个介绍这些流程变量的用法。

    2.3. 全局流程变量

    假设我们就是上面这个请假流程,我们一起来看下流程变量的设置和获取。

    2.3.1 启动时设置

    第一种方式,就是我们可以在流程启动的时候,设置流程变量,如下:

    1. @Test
    2. void test01() {
    3. Map<String, Object> variables = new HashMap<>();
    4. variables.put("days", 10);
    5. variables.put("reason", "休息一下");
    6. variables.put("startTime", new Date());
    7. ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01", variables);
    8. logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
    9. }
    10. 复制代码

    我们可以在启动的时候为流程设置变量,小伙伴们注意到,流程变量的 value 也可以是一个对象(不过这个对象要能够序列化,即实现了 Serializable 接口),然后在启动的时候传入这个变量即可。

    我们在流程启动日志中搜索 休息一下 四个字,可以找到和流程变量相关的 SQL,一共有两条,如下:

    1. insert into ACT_HI_VARINST (ID_, PROC_INST_ID_, EXECUTION_ID_, TASK_ID_, NAME_, REV_, VAR_TYPE_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, BYTEARRAY_ID_, DOUBLE_, LONG_ , TEXT_, TEXT2_, CREATE_TIME_, LAST_UPDATED_TIME_) values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
    2. INSERT INTO ACT_RU_VARIABLE (ID_, REV_, TYPE_, NAME_, PROC_INST_ID_, EXECUTION_ID_, TASK_ID_, SCOPE_ID_, SUB_SCOPE_ID_, SCOPE_TYPE_, BYTEARRAY_ID_, DOUBLE_, LONG_ , TEXT_, TEXT2_) VALUES ( ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) , ( ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) , ( ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
    3. 复制代码

    从标名称上大概就能看出来,ACT_HI_VARINST 是存储流程执行的历史信息的,ACT_RU_VARIABLE 则是保存流程运行时候的信息的。

    我们打开 ACT_RU_VARIABLE 表来看一下:

    从表中我们可以看到,每一个流程变量都有对应的流程实例 ID,这就说明这些流程变量是属于某一个流程实例的,所以我们可以按照如下方式来查询流程变量:

    1. @Test
    2. void test01() {
    3. List<Execution> list = runtimeService.createExecutionQuery().list();
    4. for (Execution execution : list) {
    5. Object reason = runtimeService.getVariable(execution.getId(), "reason");
    6. logger.info("reason:{}", reason);
    7. }
    8. }
    9. 复制代码

    对应的查询 SQL 如下:

    1. : ==> Preparing: select * from ACT_RU_VARIABLE WHERE EXECUTION_ID_ = ? AND TASK_ID_ is null AND NAME_ = ?
    2. : ==> Parameters: 6fdd2007-4c3a-11ed-aa7e-acde48001122(String), reason(String)
    3. : <== Total: 1
    4. 复制代码

    可以看到,这个就是去 ACT_RU_VARIABLE 表中进行查询,查询条件中包含了变量的名称。

    当然,我们也可以直接查询某一个流程的所有变量,如下:

    1. @Test
    2. void test02() {
    3. List<Execution> list = runtimeService.createExecutionQuery().list();
    4. for (Execution execution : list) {
    5. Map<String,Object> variables = runtimeService.getVariables(execution.getId());
    6. logger.info("variables:{}", variables);
    7. }
    8. }
    9. 复制代码

    这个对应的查询 SQL 如下:

    1. : ==> Preparing: select * from ACT_RU_VARIABLE WHERE EXECUTION_ID_ = ? AND TASK_ID_ is null
    2. : ==> Parameters: 6fdd2007-4c3a-11ed-aa7e-acde48001122(String)
    3. : <== Total: 3
    4. 复制代码

    可以看到,这个跟上面的那个差不多,只不过少了 NAME_ 这个条件。

    2.3.2 通过 Task 设置

    我们也可以在流程启动成功之后,再去设置流程变量,步骤如下:

    首先启动一个流程:

    1. @Test
    2. void test01() {
    3. ProcessInstance pi = runtimeService.startProcessInstanceByKey("demo01");
    4. logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
    5. }
    6. 复制代码

    然后设置流程变量:

    1. @Test
    2. void test03() {
    3. Task task = taskService.createTaskQuery().singleResult();
    4. taskService.setVariable(task.getId(), "days", 10);
    5. Map<String, Object> variables = new HashMap<>();
    6. variables.put("reason", "休息一下");
    7. variables.put("startTime", new Date());
    8. taskService.setVariables(task.getId(),variables);
    9. }
    10. 复制代码

    查询到某一个 Task,然后设置流程变量,上面这段代码和小伙伴们演示了两种设置方式:

    • 逐个设置
    • 直接设置一个 Map

    上面这个设置流程变量的方式,本质上还是往 ACT_HI_VARINST 和 ACT_RU_VARIABLE 表中插入数据。具体的 SQL 也和前面的一样,我就不贴出来了。

    2.3.3 完成任务时设置

    也可以在完成一个任务的时候设置流程变量,如下:

    1. @Test
    2. void test04() {
    3. Task task = taskService.createTaskQuery().singleResult();
    4. Map<String, Object> variables = new HashMap<>();
    5. variables.put("reason", "休息一下");
    6. variables.put("startTime", new Date());
    7. variables.put("days", 10);
    8. taskService.complete(task.getId(),variables);
    9. }
    10. 复制代码

    底层涉及到的 SQL 都跟前面一样,我就不赘述了。

    2.3.4 通过流程设置

    由于是全局流程变量,所以我们也可以通过 RuntimeService 来进行设置,如下:

    1. @Test
    2. void test05() {
    3. Execution execution = runtimeService.createExecutionQuery().singleResult();
    4. runtimeService.setVariable(execution.getId(), "days", 10);
    5. Map<String, Object> variables = new HashMap<>();
    6. variables.put("reason", "休息一下");
    7. variables.put("startTime", new Date());
    8. runtimeService.setVariables(execution.getId(), variables);
    9. }
    10. 复制代码

    好啦,一共就是这四种方式。

    2.4. 本地流程变量

    第三小节我们说的全局流程变量是和某一个具体的流程绑定的,而本地流程变量则不同,本地流程变量和某一个 Task 绑定。

    2.4.1 通过 Task 设置

    假设我们启动流程之后,通过 Task 来设置一个本地流程变量,方式如下:

    1. @Test
    2. void test03() {
    3. Task task = taskService.createTaskQuery().singleResult();
    4. taskService.setVariableLocal(task.getId(), "days", 10);
    5. Map<String, Object> variables = new HashMap<>();
    6. variables.put("reason", "休息一下");
    7. variables.put("startTime", new Date());
    8. taskService.setVariables(task.getId(),variables);
    9. }
    10. 复制代码

    上面这段代码中,我设置了一个本地变量,两个全局变量,设置完成后,我们去 ACT_RU_VARIABLE 表中来查看一下具体的效果。

    大家看到,由于 days 是本地变量,所以它的 TASK_ID_ 有值,这个好理解,说明 days 这个变量和这个具体的 Task 是有关的。

    此时如果我们完成这个 Task,代码如下:

    1. @Test
    2. void test06() {
    3. Task task = taskService.createTaskQuery().singleResult();
    4. taskService.complete(task.getId());
    5. }
    6. 复制代码

    完成之后,再来查看 ACT_RU_VARIABLE 表,如下:

    我们发现本地变量 days 已经没有了。因为上一个 Task 都已经执行完毕了,这个时候如果还是按照第三小节介绍的方式去查询变量,就查不到 days 了。此时如果需要查询到曾经的 days 变量,得去历史表中查询了,方式如下:

    1. @Test
    2. void test07() {
    3. ProcessInstance pi = runtimeService.createProcessInstanceQuery().singleResult();
    4. List<HistoricVariableInstance> list = historyService.createHistoricVariableInstanceQuery().processInstanceId(pi.getId()).list();
    5. for (HistoricVariableInstance hvi : list) {
    6. logger.info("name:{},type:{},value:{}", hvi.getVariableName(), hvi.getVariableTypeName(), hvi.getValue());
    7. }
    8. }
    9. 复制代码

    这是流程本地变量的特点,当然相关的方法还有好几个,这里列出来给小伙伴们参考:

    • org.flowable.engine.TaskService#complete(java.lang.String, java.util.Map, boolean):在完成一个 Task 的时候,如果传递了变量,则可以通过第三个参数来控制这个变量是全局的还是本地的,true 表示这个变量是本地的。
    • org.flowable.engine.RuntimeService#setVariableLocal:为某一个执行实例设置本地变量。
    • org.flowable.engine.RuntimeService#setVariablesLocal:同上,批量设置。

    好啦,这就是本地流程变量。

    2.5. 临时流程变量

    临时流程变量是不存数据库的,一般来说我们可以在启动流程或者完成任务的时候使用,用法如下:

    1. @Test
    2. void test21() {
    3. Map<String, Object> variables = new HashMap<>();
    4. variables.put("reason", "休息一下");
    5. variables.put("startTime", new Date());
    6. ProcessInstance pi = runtimeService
    7. .createProcessInstanceBuilder()
    8. .transientVariable("days", 10)
    9. .transientVariables(variables)
    10. .processDefinitionKey("demo01")
    11. .start();
    12. logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
    13. }
    14. 复制代码

    上面这段代码涉及到的流程变量就是临时流程变量,它是不会存入到数据库中的。

    也可以在完成一个任务的时候设置临时变量,如下:

    1. @Test
    2. void test22() {
    3. Task task = taskService.createTaskQuery().singleResult();
    4. Map<String, Object> transientVariables = new HashMap<>();
    5. transientVariables.put("days", 10);
    6. taskService.complete(task.getId(), null, transientVariables);
    7. }
    8. 复制代码

    这个临时变量也是不会存入到数据库中的。

    好啦,关于流程变量,今天就和小伙伴们先说这么多~

    3. 历史流程

    [TOC]

    在之前的文章中松哥和小伙伴们聊过,正在执行的流程信息是保存在以 ACT_RU_ 为前缀的表中,执行完毕的流程信息则保存在以 ACT_HI_ 为前缀的表中,也就是流程历史信息表,当然这个历史信息表继续细分的话,还有好多种,今天我们就来聊一聊这个话题。

    假设我有如下一个流程:

    当这个流程执行完毕后,以 ACT_RU_ 为前缀的表中的数据均已清空,现在如果想查看刚刚执行过的流程信息,我们就得去以 ACT_HI_ 为前缀的表中。

    3.1. 历史流程信息

    历史流程信息查看,方式如下:

    1. @Test
    2. void test05() {
    3. List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery().finished().list();
    4. for (HistoricProcessInstance hpi : list) {
    5. logger.info("name:{},startTime:{},endTime:{}",hpi.getName(),hpi.getStartTime(),hpi.getEndTime());
    6. }
    7. }
    8. 复制代码

    调用的时候执行的 finished() 方法表示查询已经执行完毕的流程信息(从这里也可以看出,对于未执行完毕的流程信息也会保存在历史表中)。

    我们来看下这个查询对应的 SQL,如下:

    1. SELECT RES.* , DEF.KEY_ as PROC_DEF_KEY_, DEF.NAME_ as PROC_DEF_NAME_, DEF.VERSION_ as PROC_DEF_VERSION_, DEF.DEPLOYMENT_ID_ as DEPLOYMENT_ID_ from ACT_HI_PROCINST RES left outer join ACT_RE_PROCDEF DEF on RES.PROC_DEF_ID_ = DEF.ID_ WHERE RES.END_TIME_ is not NULL order by RES.ID_ asc
    2. 复制代码

    从这个 SQL 中可以看到,这个查询本质上就是查询的 ACT_HI_PROCINST 表。如下图:

    如果我们在查询的时候不限制流程是否执行完毕,那么我们的查询方法如下:

    1. @Test
    2. void test05() {
    3. List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery().list();
    4. for (HistoricProcessInstance hpi : list) {
    5. logger.info("name:{},startTime:{},endTime:{}",hpi.getName(),hpi.getStartTime(),hpi.getEndTime());
    6. }
    7. }
    8. 复制代码

    对应的查询 SQL 如下:

    1. SELECT RES.* , DEF.KEY_ as PROC_DEF_KEY_, DEF.NAME_ as PROC_DEF_NAME_, DEF.VERSION_ as PROC_DEF_VERSION_, DEF.DEPLOYMENT_ID_ as DEPLOYMENT_ID_ from ACT_HI_PROCINST RES left outer join ACT_RE_PROCDEF DEF on RES.PROC_DEF_ID_ = DEF.ID_ order by RES.ID_ asc
    2. 复制代码

    和前面的 SQL 相比,后面的 SQL 少了 WHERE RES.END_TIME_ is not NULL 条件,也就是说,判断一个流程是否执行完毕,就看它的 END_TIME_ 是否为空,不为空就表示流程已经执行结束了,为空就表示流程尚在执行中。

    3.2. 历史任务查询

    刚刚我们查询的是历史流程,接下来我们来看下历史任务,也就是查询一个流程中执行过的 Task 信息,如下表示查询所有的历史流程任务:

    1. @Test
    2. void test06() {
    3. List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().list();
    4. for (HistoricTaskInstance hti : list) {
    5. logger.info("name:{},assignee:{},createTime:{},endTime:{}",hti.getName(),hti.getAssignee(),hti.getCreateTime(),hti.getEndTime());
    6. }
    7. }
    8. 复制代码

    这个查询对应的 SQL 如下:

    1. SELECT RES.* from ACT_HI_TASKINST RES order by RES.ID_ asc
    2. 复制代码

    可以看到,历史任务表就是 ACT_HI_TASKINST,如下图:

    当然,这里还有很多其他的玩法,例如查询某一个流程已经执行完毕的历史任务,如下:

    1. @Test
    2. void test07() {
    3. List<HistoricProcessInstance> instanceList = historyService.createHistoricProcessInstanceQuery().list();
    4. for (HistoricProcessInstance hpi : instanceList) {
    5. List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().processInstanceId(hpi.getId()).finished().list();
    6. for (HistoricTaskInstance hti : list) {
    7. logger.info("name:{},assignee:{},createTime:{},endTime:{}", hti.getName(), hti.getAssignee(), hti.getCreateTime(), hti.getEndTime());
    8. }
    9. }
    10. }
    11. 复制代码

    这个里边的查询历史任务的 SQL 如下:

    1. SELECT RES.* from ACT_HI_TASKINST RES WHERE RES.PROC_INST_ID_ = ? and RES.END_TIME_ is not null order by RES.ID_ asc
    2. 复制代码

    可以看到,跟前面相比,多了两个条件:

    1. 流程实例 ID
    2. 流程结束时间不为 null

    从这里也可以看出来,这个 finish 方法的执行逻辑跟我们前面讲的是一样的。

    3.3. 历史活动查询

    历史任务就是各种 Task,历史活动则包括跟多内容,像开始/结束节点,连线等等这些信息都算是活动,这个在之前的文章中松哥已经和大家介绍过了。

    查询代码如下:

    1. @Test
    2. void test08() {
    3. List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery().list();
    4. for (HistoricActivityInstance hai : list) {
    5. logger.info("name:{},startTime:{},assignee:{},type:{}",hai.getActivityName(),hai.getStartTime(),hai.getAssignee(),hai.getActivityType());
    6. }
    7. }
    8. 复制代码

    这个查询对应的 SQL 如下:

    1. SELECT RES.* from ACT_HI_ACTINST RES order by RES.ID_ asc
    2. 复制代码

    可以看到,ACT_HI_ACTINST 表中保存了历史活动信息。

    3.4. 历史变量查询

    查询流程执行的历史变量,方式如下:

    1. @Test
    2. void test09() {
    3. HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().singleResult();
    4. List<HistoricVariableInstance> list = historyService.createHistoricVariableInstanceQuery().processInstanceId(pi.getId()).list();
    5. for (HistoricVariableInstance hvi : list) {
    6. logger.info("name:{},type:{},value:{}", hvi.getVariableName(), hvi.getVariableTypeName(), hvi.getValue());
    7. }
    8. }
    9. 复制代码

    这个查询对应的 SQL 如下:

    1. SELECT RES.* from ACT_HI_VARINST RES WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
    2. 复制代码

    可以看到流程的历史变量信息保存在 ACT_HI_VARINST 表中。

    3.5. 历史日志查询

    有的小伙伴看到日志这两个字可能会觉得奇怪,咦?流程执行还有日志吗?没听说过呀!

    其实历史日志查询就是前面那几种的一个集大成者,用法如下:

    1. @Test
    2. void test10() {
    3. HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().singleResult();
    4. ProcessInstanceHistoryLog historyLog = historyService.createProcessInstanceHistoryLogQuery(pi.getId())
    5. //包括历史活动
    6. .includeActivities()
    7. //包括历史任务
    8. .includeTasks()
    9. //包括历史变量
    10. .includeVariables()
    11. .singleResult();
    12. logger.info("id:{},startTime:{},endTime:{}", historyLog.getId(), historyLog.getStartTime(), historyLog.getEndTime());
    13. List<HistoricData> historicData = historyLog.getHistoricData();
    14. for (HistoricData data : historicData) {
    15. if (data instanceof HistoricActivityInstance) {
    16. HistoricActivityInstance hai = (HistoricActivityInstance) data;
    17. logger.info("name:{},type:{}", hai.getActivityName(), hai.getActivityType());
    18. }
    19. if (data instanceof HistoricTaskInstance) {
    20. HistoricTaskInstance hti = (HistoricTaskInstance) data;
    21. logger.info("name:{},assignee:{}", hti.getName(), hti.getAssignee());
    22. }
    23. if (data instanceof HistoricVariableInstance) {
    24. HistoricVariableInstance hvi = (HistoricVariableInstance) data;
    25. logger.info("name:{},type:{},value:{}", hvi.getVariableName(), hvi.getVariableTypeName(), hvi.getValue());
    26. }
    27. }
    28. }
    29. 复制代码

    这个里边,首先是查询基本的流程日志信息,这个本质上就是查询历史流程实例信息,对应的 SQL 如下:

    1. select RES.*, DEF.KEY_ as PROC_DEF_KEY_, DEF.NAME_ as PROC_DEF_NAME_, DEF.VERSION_ as PROC_DEF_VERSION_, DEF.DEPLOYMENT_ID_ as DEPLOYMENT_ID_ from ACT_HI_PROCINST RES left outer join ACT_RE_PROCDEF DEF on RES.PROC_DEF_ID_ = DEF.ID_ where PROC_INST_ID_ = ?
    2. 复制代码

    接下来我写了三个 include,每一个 include 都对应一句 SQL:

    includeActivities 对应的 SQL 如下:

    1. SELECT RES.* from ACT_HI_ACTINST RES WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
    2. 复制代码

    includeTasks 对应的 SQL 如下:

    1. SELECT RES.* from ACT_HI_TASKINST RES WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
    2. 复制代码

    includeVariables 对应的 SQL 如下:

    1. SELECT RES.* from ACT_HI_VARINST RES WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
    2. 复制代码

    最终查询完成后,调用 getHistoricData 方法可以查看这些额外的数据,List 集合中存放的 HistoricData 也分为不同的类型:

    • includeActivities 方法对应最终查询出来的类型是 HistoricActivityInstance。
    • includeTasks 方法对应最终查询出来的类型是 HistoricTaskInstance。
    • includeVariables 方法对应最终查询出来的类型是 HistoricVariableInstance。

    在遍历的时候通过类型判断去查看具体是哪一种变量类型。

    综上,这个历史日志查询其实就是一个集大成者。

    3.6. 历史权限查询

    这个是用来查询流程或者任务的处理人,例如查询流程的处理人,方式如下:

    1. @Test
    2. void test11() {
    3. HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().singleResult();
    4. List<HistoricIdentityLink> links = historyService.getHistoricIdentityLinksForProcessInstance(pi.getId());
    5. for (HistoricIdentityLink link : links) {
    6. logger.info("userId:{}",link.getUserId());
    7. }
    8. }
    9. 复制代码

    这个是查询流程对应的处理人,对应的 SQL 如下:

    1. select * from ACT_HI_IDENTITYLINK where PROC_INST_ID_ = ?
    2. 复制代码

    如果想查询任务的处理人,对应的方式如下:

    1. @Test
    2. void test12() {
    3. String taskName = "提交请假申请";
    4. HistoricTaskInstance hti = historyService.createHistoricTaskInstanceQuery().taskName(taskName).singleResult();
    5. List<HistoricIdentityLink> links = historyService.getHistoricIdentityLinksForTask(hti.getId());
    6. for (HistoricIdentityLink link : links) {
    7. logger.info("{} 任务的处理人是 {}",taskName,link.getUserId());
    8. }
    9. }
    10. 复制代码

    这个查询对应的 SQL 如下:

    1. select * from ACT_HI_IDENTITYLINK where TASK_ID_ = ?
    2. 复制代码

    和前面的相比,其实就多了一个查询条件 TASK_ID_

    3.7. 自定义查询 SQL

    和前面讲的很多查询类似,当我们弄懂了每一个历史查询的 API 操作的是哪一个数据表,就会发现,历史数据的查询,也可以自定义 SQL。

    举个例子和小伙伴们看下,例如查询某一个流程已经执行完毕的历史任务:

    1. @Test
    2. void test13() {
    3. List<HistoricProcessInstance> instanceList = historyService.createHistoricProcessInstanceQuery().list();
    4. for (HistoricProcessInstance hpi : instanceList) {
    5. List<HistoricTaskInstance> list = historyService.createNativeHistoricTaskInstanceQuery()
    6. .sql("SELECT RES.* from ACT_HI_TASKINST RES WHERE RES.PROC_INST_ID_ = #{pid} and RES.END_TIME_ is not null order by RES.ID_ asc")
    7. .parameter("pid",hpi.getId()).list();
    8. for (HistoricTaskInstance hti : list) {
    9. logger.info("name:{},assignee:{},createTime:{},endTime:{}", hti.getName(), hti.getAssignee(), hti.getCreateTime(), hti.getEndTime());
    10. }
    11. }
    12. }
    13. 复制代码

    flowable 底层是 MyBatis,所有 SQL 中参数的传递形式和 MyBatis 一致。

    3.8. 历史数据记录级别

    Flowable 需要记录哪些历史数据,有一个日志级别用来描述这个事情,默认有四种级别:

    • None: 这个表示不存储任何历史信息,好处是流程执行的时候效率会比较快,坏处是流程执行结束后,看不到曾经执行过的流程信息了。
    • Activity: 这个会存储所有流程实例和活动实例,在流程实例结束时,顶级流程实例变量的最新值将复制到历史变量实例中,不会存储详细信息。
    • Audit: 在 Activity 的基础上,还会存储历史详细信息,包括权限信息等。默认的日志记录级别即次。
    • Full: 这个是在 Audit 的基础上,还会存储变量的变化信息,这会记录大量的数据,也会导致流程执行变慢。

    一共就这四种级别,在 Spring Boot 项目中,如果我们想要配置这个日志记录的级别,其实非常方便,直接在 application.properties 中进行配置即可,如下:

    1. flowable.history-level=none
    2. 复制代码

    配置加了这个配置,我们随便启动一个流程,然后去查询 ACT_HI_ 系列的表,发现都是空的,没有数据。

    如果我们将历史日志记录的级别改为 activity,那么就会记录下来流程信息以及活动信息,但是像执行的 Task 这些信息都是没有的(ACT_HI_TASKINST),包括流程参与者的信息(ACT_HI_IDENTITYLINK)等都不会记录下来。

    如果我们将历史日志记录的级别改为 audit,则上面提到的这几种日志就都会记录下来。但是 ACT_HI_DETAIL 表还是空的,详细一个流程变量的变化过程不会被记录下来。

    如果我们将日志记录级别改为 full,那么将会记录下更多的信息。ACT_HI_DETAIL 表中会记录下流程变量的详细信息。

    整个过程我就不给小伙伴们演示了大家可以自行尝试。

    好啦,关于历史数据的查询,松哥先和小伙伴们聊这么多~下篇文章我们继续~

  • 相关阅读:
    力扣 215. 数组中的第K个最大元素
    C语言实现扫雷小游戏(更新中)
    计算机毕业设计JavaWeb医学院校大学生就业信息管理系统(源码+系统+mysql数据库+lw文档)
    自动驾驶学习笔记(五)——绕行距离调试
    系统架构师之软件工程
    hdu 6109 数据分割
    NPDP认证|制造业产品经理日常工作必备技能,快来学习提升吧!
    Java之Applet 使用 AudioClip接口播放音频学习笔记
    酷开系统游戏空间,开启大屏娱乐新玩法
    R语言fpc包的dbscan函数对数据进行密度聚类分析、plot函数可视化聚类图
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/128073967