我們一起玩轉 Flowable 流程實例

語言: CN / TW / HK

今天我們繼續來聊聊流程實例。

部署之後的流程,這個還不能直接運行,例如我們部署了一個請假流程,現在 zhangsan 想要請假,他就需要開啟一個請假流程,lisi 想請假,他也需要開啟一個請假流程,這一個一個開啟的請假流程就是流程實例,而我們一開始部署的請假流程,則類似於一個模版,基於此模版,我們可以開啟很多個具體的流程實例。從這個角度來説,上篇文章我們定義的 ProcessDefinition 就類似於一個 Java 類,今天我們要介紹的 ProcessInstance 則相當於一個 Java 對象。

1. 捋清三個概念

首先我們需要先捋清三個概念:

  • 流程定義 ProcessDefinition
  • 流程實例 ProcessInstance
  • 執行實例 Execution

流程定義

流程定義 ProcessDefinition 這個好説,其實就是我們上篇文章中和大家介紹的內容。將一個流程 XML 文件部署到 flowable 中,這就是一個定義好的流程了,基於這個定義好的流程,我們可以開啟很多流程實例。

流程實例

流程實例 ProcessInstance 就是通過流程定義啟動的一個流程,他表示一個流程從開始到結束的最大的流程分支,在一個流程中,只存在一個流程實例,流程實例和流程定義的關係就類似於 Java 對象和 Java 類之間的關係。

執行實例

執行實例 Execution 稍微有點難以理解。

首先從類的關係上來看,ProcessInstance 就是 Execution 的子類。

流程實例通常是執行實例的根結點,即在一個流程中,出口和入口可以算是一個流程實例的節點,而中間的過程則是執行實例。

假如流程本身就是一條線,那麼流程實例和執行實例基本上是一樣的,但是如果流程中包含多條線,例如下圖:

這張圖中有並行網關,並行任務執行的時候,每一個並行任務就是一個執行實例,這樣大家就好理解了。

結論就是,在一個流程實例中,除了開始和結束之外,其他的都是執行實例。即使流程只有一條線,中間的也都是執行實例,只不過此時的執行實例等於流程實例而已。

好啦,三個基本概念先捋清楚。

2. 五種流程啟動方式

當我們將流程部署好之後,接下來啟動流程,我們有五種不同的方式去啟動一個流程。

  • 通過流程定義的 id 去啟動

首先就是通過流程定義的 id 去啟動一個流程,對應的方法名稱就是 RuntimeService#startProcessInstanceById,該方法有好幾個重載的方法,不同的重載方法只是傳遞的參數不同而已,其他基本上都是一樣的。

  • 通過流程的 key 去啟動

也可以通過流程定義的 key 去啟動一個流程,根據上篇文章​的介紹,大家知道,這個流程定義的 key 其實就是流程 XML 文件中的 id,這個對應的方法名是 RuntimeService#startProcessInstanceByKey。

  • 通過流程的 key+tenantId 去啟動

有這樣一種情況,例如我有兩個子系統 A 和 B,A 和 B 中都有一個請假流程的定義,現在當我想要啟動一個流程的時候,怎麼知道是啟動 A 的請假流程還是啟動 B 的請假流程呢?此時我們可以通過租户 ID 即 tenantId 去區分,所以,流程啟動就還有一個方法 RuntimeService#startProcessInstanceByKeyAndTenantId。

  • 通過流程的 message 去啟動

通過消息去啟動一個流程,對應的方法是 RuntimeService#startProcessInstanceByMessage。

  • 通過流程的 message+tenanId 去啟動

通過消息+租户 ID 去啟動一個流程,對應的方法是 RuntimeService#startProcessInstanceByMessageAndTenantId。

3. 簡單實踐

首先我們繪製一個簡單的流程圖,然後按照上篇文章所介紹的方式進行部署,流程圖如下:

流程 XML 文件如下:

<process id="leave" name="請假流程" isExecutable="true">
  <startEvent id="startEvent1" flowable:formFieldValidation="true" flowable:initiator="INITIATOR"></startEvent>
  <userTask id="sid-EF721F14-B1F1-4B3B-8018-608757EF5391" name="提交請假申請" flowable:assignee="${INITIATOR}" flowable:formFieldValidation="true">
    <extensionElements>
      <modeler:activiti-idm-initiator xmlns:modeler="http://flowable.org/modeler"><![CDATA[true]]></modeler:activiti-idm-initiator>
    </extensionElements>
  </userTask>
  <sequenceFlow id="sid-9C18B4D2-127C-40FD-BC81-1E947628D316" sourceRef="startEvent1" targetRef="sid-EF721F14-B1F1-4B3B-8018-608757EF5391"></sequenceFlow>
  <userTask id="sid-CC8E2905-C524-4C32-86DE-8C76102EBDF2" name="主管審批" flowable:assignee="zhangsan" 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-62E837FF-DF33-414C-AC21-2DA84E478856" sourceRef="sid-EF721F14-B1F1-4B3B-8018-608757EF5391" targetRef="sid-CC8E2905-C524-4C32-86DE-8C76102EBDF2"></sequenceFlow>
  <userTask id="sid-D033E54B-4388-46B1-A8F9-472ABD2E1435" name="經理審批" flowable:assignee="lisi" 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-7EDE624F-1D8F-4DF3-BB97-F8D9066A7A75" sourceRef="sid-CC8E2905-C524-4C32-86DE-8C76102EBDF2" targetRef="sid-D033E54B-4388-46B1-A8F9-472ABD2E1435"></sequenceFlow>
  <endEvent id="sid-FB77ACAC-DB24-4F44-9925-2FE2EAE09EF8"></endEvent>
  <sequenceFlow id="sid-21345500-1FCF-4356-9FB1-834C09BEA9CB" sourceRef="sid-D033E54B-4388-46B1-A8F9-472ABD2E1435" targetRef="sid-FB77ACAC-DB24-4F44-9925-2FE2EAE09EF8"></sequenceFlow>
</process>

這個 XML 文件我跟大家説一句,在啟動節點上我設置了 flowable:initiator="INITIATOR",相當於定義了流程發起人的變量為 INITIATOR,這個變量名是自定義的,定義好之後,將來我就可以在其他節點中就可以使用這個變量了。

很簡單的流程,其中:

  • 提交請假申請是由流程的發起人完成。
  • 主管是 zhangsan。
  • 經理是 lisi。

好了,先按照上篇文章我們介紹的方式部署流程。

接下來我們要啟動流程,假設我們用流程定義的 key 來啟動一個流程實例:

@SpringBootTest
public class RuTest {
    @Autowired
    RuntimeService runtimeService;

    private static final Logger logger = LoggerFactory.getLogger(RuTest.class);

    @Test
    void test01() {
        Authentication.setAuthenticatedUserId("wangwu");
        ProcessInstance pi = runtimeService.startProcessInstanceByKey("leave");
        logger.info("id:{},activityId:{}",pi.getId(),pi.getActivityId());
    }
}

啟動的代碼其實很簡單,當流程啟動成功之後,流程中的每一步都會記錄在 ACT_RU_EXECUTION 表中,同時,如果這個節點是一個用户任務節點(UserTask),那麼同時還會在 ACT_RU_TASK 表中添加一條記錄。

Authentication.setAuthenticatedUserId("wangwu"); 表示設置流程的發起人。

另外一種設置流程發起人的方式如下:

@Autowired
IdentityService identityService;
@Test
void test01() {
    identityService.setAuthenticatedUserId("wangwu");
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("leave");
    logger.info("id:{},activityId:{}", pi.getId(), pi.getActivityId());
}

對於我們上面的流程來説,啟動之後,就會進入到提交請假申請這個節點中,所以一共走了兩個節點,那麼 ACT_RU_EXECUTION 表中應該有兩條記錄了,如下圖:

再來看看 ACT_RU_TASK 表中的內容:

可以看到,該表中有一條記錄,這條記錄其實就是提交請假申請這個節點,現在流程就停在這一步了,需要用户手動操作,才會繼續向下走。

從這兩張表中我們也可以大致上看出來,EXECUTION 和 ProcessInstance 之間的關係,ACT_RU_EXECUTION 表中的每一條記錄就是一個 EXECUTION,多個 EXECUTION 對應同一個 PROC_INST_ID_,而 ACT_RU_TASK 表中的每一條 Task 記錄也都對應了一個 EXECUTION。

現在我們就先去查詢 wangwu 需要完成的 Task(wangwu 是流程的發起人):

@Autowired
TaskService taskService;
@Test
void test02() {
    List<Task> list = taskService.createTaskQuery().taskAssignee("wangwu").list();
    for (Task task : list) {
        logger.info("id:{};name:{};taskDefinitionKey:{}",task.getId(),task.getName(),task.getTaskDefinitionKey());
    }
}

根據前面的介紹,我們知道,這個查詢肯定是去 ACT_RU_TASK 表中進行查詢的,我們來看下執行的 SQL:

可以看到,這裏就是根據 ASSIGNEE_ 字段去查詢任務的。

查詢到任務之後,接下來去完成任務:

@Test
void test03() {
    List<Task> list = taskService.createTaskQuery().taskAssignee("wangwu").list();
    for (Task task : list) {
        taskService.complete(task.getId());
    }
}

這個表示查詢到 wangwu 的任務然後完成,這個方法執行完成之後,首先會在 ACT_RU_TASK 表中插入一條新的需要 zhangsan 完成的 Task,然後會更新 ACT_RU_EXECUTION 表中對應的執行實例信息,最後再從 ACT_RU_TASK 表中刪除需要 wangwu 完成的記錄,這些操作是在同一個事務當中完成的。

好了,現在再去執行 test02 的查詢方法,就會發現查不到了,因為沒有 wangwu 需要完成的 task 了,接下來應該去查詢 zhangsan 需要完成的 task。

當一個流程實例完成後,ACT_RU_TASK 和 ACT_RU_EXECUTION 表中的記錄都會被刪除,所以我們可以通過查詢 ACT_RU_EXECUTION 表中是否還有記錄,去判斷一個一個流程目前是處於執行狀態還是完成狀態,代碼如下:

@Test
void test04() {
    String pId = "9c8557dd-3727-11ed-9404-acde48001122";
    ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(pId).singleResult();
    if (pi == null) {
        logger.info("{} 流程執行結束", pId);
    }else{
        logger.info("{} 流程正在執行中", pId);
    }
}

最後,如果你想要去 ACT_RU_EXECUTION 表中查詢執行實例也是 OK 的,方式如下:

@Test
void test05() {
    List<Execution> list = runtimeService.createExecutionQuery().processInstanceId("6d0341c7-3729-11ed-8e4e-acde48001122").list();
    for (Execution execution : list) {
        logger.info("id:{};processInstanceId:{};name:{}",execution.getId(),execution.getProcessInstanceId(),execution.getName());
    }
}

查看執行的 SQL 如下:

: ==>  Preparing: SELECT RES.* , P.KEY_ as ProcessDefinitionKey, P.ID_ as ProcessDefinitionId, P.NAME_ as ProcessDefinitionName, P.VERSION_ as ProcessDefinitionVersion, P.DEPLOYMENT_ID_ as DeploymentId from ACT_RU_EXECUTION RES inner join ACT_RE_PROCDEF P on RES.PROC_DEF_ID_ = P.ID_ WHERE RES.PROC_INST_ID_ = ? order by RES.ID_ asc
: ==> Parameters: 6d0341c7-3729-11ed-8e4e-acde48001122(String)
: <==      Total: 2

可以看到,就是去 ACT_RU_EXECUTION  表中查詢的。

4. 刪除流程實例

如果我們想刪除一個流程實例,操作方式如下:

@Test
void test06() {
    runtimeService.deleteProcessInstance("65ab0b38-38f3-11ed-b103-acde48001122", "javaboy想刪除了");
}

注意這個是刪除正在執行的流程實例信息,並不會刪除歷史流程信息。

5. 獲取運行的活動節點

可以根據執行實例的 ID 去查詢活動節點的 ID,方式如下:

@Test
void test07() {
    List<Execution> list = runtimeService.createExecutionQuery().list();
    for (Execution execution : list) {
        List<String> activeActivityIds = runtimeService.getActiveActivityIds(execution.getId());
        for (String activeActivityId : activeActivityIds) {
            System.out.println("activeActivityId = " + activeActivityId);
        }
    }
}

這裏查詢的其實就是 ACT_RU_EXECUTION 表,查詢到的 activeActivityId 其實就是該表的 ACT_ID 字段,我們來看下查詢的 SQL:

好啦,流程實例先聊這麼多,下篇文章我們繼續~