flowable流程引擎初體驗,完成一個請假流程

flowable是一個用Java寫的輕量級商業流程引擎,用它可以部署BPMN2.0(在工業界被廣泛接受的XML標準)流程定義, 并且可以創建流程實例,驅動節點流轉,存儲相關的歷史數據等等??赡芨嗳讼仁锹犝f過activiti, 但是flowable實際上是activiti的主要成員在activiti上fork一個新的分支,添加了許多新的特性,也更加穩定實用,感興趣的可以去了解一下這段歷史。本文主要基于flowable官方文檔演示如何去用flowable來走通如下的一個請假流程。

1. 創建項目

首先我們創建一個maven項目,引入以下依賴:

  • 采用最新版本的flowable流程引擎,
  • flowable存儲數據默認采用h2內存數據庫,但我這里還是用熟悉的mysql
  • flowable內部采用SLF4J作為其日志框架
  • 在這里我們用log4j日志實現
    <dependency>
      <groupId>org.flowable</groupId>
      <artifactId>flowable-engine</artifactId>
      <version>6.3.1</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.40</version>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.21</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.21</version>
    </dependency>

Log4j需要一個屬性文件來進行相關配置,因此需要在src/main/resources添加具有以下內容的log4j.properties文件:

log4j.rootLogger=DEBUG, CA

log4j.appender.CA=org.apache.log4j.ConsoleAppender
log4j.appender.CA.layout=org.apache.log4j.PatternLayout
log4j.appender.CA.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n

2. 創建流程引擎

首先,我們需要做的是初始化一個流程引擎(ProcessEngine)實例。它是一個線程安全的對象在一個應用中也只需要初始化一次, 而它又由流程引擎配置(ProceeEngineConfiguration)創建,在ProcessEngineconfiguration中可以配置和調整省流程引擎。通常情況下ProcessEngineConfiguration是通過配置文件來配置的,但是也可以在程序中創建。當然,最少情況下的流程引擎配置也需要一個JDBC連接到數據庫。

public class HolidayRequest {
    public static void main(String[] args) {
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl("jdbc:mysql://localhost:3306/flowable?useUnicode=true&zeroDateTimeBehavior=convertToNull")
                .setJdbcUsername("root")
                .setJdbcPassword("123456")
                .setJdbcDriver("com.mysql.jdbc.Driver")
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);

        ProcessEngine processEngine = cfg.buildProcessEngine();
    }

}

在上面的代碼中,創建了一個獨立的配置對象,這里的‘獨立’是指引擎只由它自己創建和使用(如果是在Spring環境中,則要用SpringProcessEngineConfiguration來替代).該對象先配置了一個數據庫連接(注意先要創建flowable數據庫),最后的配置表示數據庫不存在的話就要去創建它。運行該段代碼后我們就可以看到數據庫中新增了許多表格,如下所示:

act_ru_: 表示的是一些流程實例運行(runtime)時的數據
act_hi_: 表示存儲的是一些歷史(history)數據
act_id_: 表示存儲的是一些與用戶身份(identity)相關的數據
act_re_: 表示的是一些流程定義(repository)相關的數據
act_ge_: 表示的是一些一般(general)數據
act_evt_log表示事件日志,act_prodef_info表示的是流程定義的一些信息。flowable還有一些其它模塊中的表,到后面介紹。

3. 部署流程定義

如開篇那張圖所示,我們需要實現的是一個很簡單的請假流程, 表示員工的請假首先需要經理審批,經理拒絕則發郵件通知到申請人并結束該該流程,經理同意的話先注冊到外部系統,然后通知給申請人進行銷假,然后結束。該流程定義可以作為一個模板,每個需要請假的員工可以創建一個流程實例。當員工提供一些信息(如姓名, 請假天數和描述)就可以開始該流程。下面解釋一下圖中每類元素的意思:

  • 開始事件:圖中用細線圓圈來表示,是流程實例的開始點
  • 箭頭:表示節點之間的流轉指向。
  • 用戶任務: 在圖中用左上角有人的圓角矩形表示,這些是需要用戶來操作的節點。圖中有兩個,第一個表示需要經理進行審批來同意或拒絕, 第二個表示用戶來確認銷假。
  • 排它網關: 用叉形符號填充的菱形表示,從該圖中出來的箭頭往往有多個,但只有一個滿足條件,流程會沿著滿足條件的方向流轉。
  • 自動化任務 : 左上角有齒輪形狀的的圓角矩形,表示自動執行的節點。圖中上面的表示請假被經理同意后自動注冊通知到外部系統,下面的表示請假被經理拒絕后自動發郵件通知給申請人。
  • 結束事件: 圖中用粗線圓圈表示,表示流程的結束。圖中上面的結束事件表示請假成功結束,下面的表示請假失敗結束。


Flowable引擎需要的是一個遵循BPMN2.0標準的XML文檔。BPMN2.0標準在工業界被廣泛認可, 它對流程中每個節點以及節點與節點之間的流轉定義了一套簡單清晰易用的標準,并且節點即可以是個人任務也可以是自動化任務等, 因此大家可以用都能理解的方式進行商業化的流程門交流。通常我們可以用一些工具來畫出流程圖然后生成BPMN2.0文檔,但是在這里我們為了熟悉該標準的格式不審采用手動定義的方式。在src/main/resources文件夾下添加名稱為holiday-request.bpmn20.xml內容如下的文件:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
             xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
             xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
             xmlns:flowable="http://flowable.org/bpmn"
             typeLanguage="http://www.w3.org/2001/XMLSchema"
             expressionLanguage="http://www.w3.org/1999/XPath"
             targetNamespace="http://www.flowable.org/processdef">

    <process id="holidayRequest" name="Holiday Request" isExecutable="true">

        <startEvent id="startEvent"/>
        <sequenceFlow sourceRef="startEvent" targetRef="approveTask"/>

        <userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>
        <sequenceFlow sourceRef="approveTask" targetRef="decision"/>

        <exclusiveGateway id="decision"/>
        <sequenceFlow sourceRef="decision" targetRef="externalSystemCall">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[
          ${approved}
        ]]>
            </conditionExpression>
        </sequenceFlow>
        <sequenceFlow  sourceRef="decision" targetRef="sendRejectionMail">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[
          ${!approved}
        ]]>
            </conditionExpression>
        </sequenceFlow>

        <serviceTask id="externalSystemCall" name="Enter holidays in external system"
                     flowable:class="com.hebaohua.workflow.delegate.CallExternalSystemDelegate"/>
        <sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>

        <userTask id="holidayApprovedTask" name="Holiday approved" flowable:assignee="${employee}"/>
        <sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>

        <serviceTask id="sendRejectionMail" name="Send out rejection email"
                     flowable:class="com.hebaohua.workflow.delegate.CallExternalSystemDelegate"/>
        <sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>

        <endEvent id="approveEnd"/>

        <endEvent id="rejectEnd"/>

    </process>

</definitions>

XML文件中最開始幾行是一些兼容BPMN2.0標準的約束,每個流程定義都一樣。
每一個activity都有一個id屬性作為唯一性標識, name屬性是為了增加可讀性的可選項。
連接activitys的有向箭頭稱之為sequence flow,有一個起始點和目標點。從排它網關出來的箭頭比較特別,這兩個都有一個條件標簽,表示只有滿足里面條件時才會走這條路線。條件表達式${approved}實際上是${approved == true}的縮寫,approved被稱之為流程變量。流程變量會隨著流程實例進行持久化存儲并且在整個流程實例的生命周期中都可以被用到, 因此我們必須在用到它之前的某個點對其進行賦值(本例中是當經理的用戶任務完成時對approved進行true或false賦值),而不是在流程實例一開始就對其賦值。
現在我們有了流程定義的BPMN2.0 XML文件,下一步就是要把它部署到引擎中。部署流程引擎也就意味著:

  • 流程引擎會把XML文件存儲在數據庫中,因此可以需要的時候查詢到它
  • 流程定義被解析為一個內部可執行的對象模型,所以我們可以以它為模板啟動一個流程實例
    部署流程定義到Flowable引擎需要用RepositoryService,可以在ProcessEngine對象中得到它。通過RepositoryService, 指定XML文件位置并調用deploy()方法后即可執行部署這個過程。
RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
  .addClasspathResource("holiday-request.bpmn20.xml")
  .deploy();

通過如下打印的日志可知,在部署的過程中分別在ACT_RE_PROCDEF, ACT_RE_DEPLOYMENT, ACT_GE_BYTEARRAY中插入了相關的記錄。


現在我們就可以通過RepositoryService創建的ProcessDefinitionQuery查詢到相關的流程定義信息了,如下代碼:

ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
  .deploymentId(deployment.getId())
  .singleResult();
System.out.println("Found process definition : " + processDefinition.getName());

4. 啟動流程實例

現在我們有了流程定義,以它為模板就可啟動一個流程實例了。
在啟動流程實之前,我們需要提供一些流程變量作為輸入。通常通過表單或REST API讓用戶來填寫這些信息,這里為了方便直接定定義相關變量放在Map中。
下一步就需要通過RuntimeService(同樣由processEngine產生)啟動流程了。如下代碼所示,通過key來啟動流程實例,這里的key對應BPMN2.0 XML文件中的process id屬性,本例中也就是holidayRequest。

RuntimeService runtimeService = processEngine.getRuntimeService();

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("employee", "jack");
variables.put("nrOfHolidays", 3);
variables.put("description", "回家看看");
ProcessInstance processInstance =
        runtimeService.startProcessInstanceByKey("holidayRequest", variables);

如下日志,可以看到插入了15條記錄,分別是ProcessInstance, Execution, IdentityLInk, Variable, Task相關的運行和歷史數據,Execution和Task是運行時的核心表。注意在這一步流程通過啟動事件已經到達manager審批結點了,因此會插入兩條HistoryActivityInstanceEntity記錄。

5. 編寫JavaDelegate實現自動化任務

現在雖然任務到了第一個用戶任務節點,但該任務節點執行后就會走自動化任務,在XML文件中我們定義對應的類為com.hebaohua.flowable.delegate.CallExternalSystemDelegate, 如果找不到該類程序流程引擎就會報異常,所以我們先看看如何編寫這樣的一個自動化執行類。
很簡單,實現org.flowable.engine.delegate.JavaDelegate接口及其execute方法。通過得到的DelegateExecution對象我們可以得到流程中所有變量及相關信息,因此想要做什么就在這個方法里來實現。如下代碼只是一個簡單演示:

public class CallExternalSystemDelegate implements JavaDelegate {
    @Override
    public void execute(DelegateExecution execution) {
        System.out.println("Calling the external system for employee "
                + execution.getVariable("employee"));
    }
}

6. 查詢并完成任務

在一個實際的應用中,用戶通過登錄系統可以以界面的方式查看他們的任務列表,并且可以看到流程實例的數據(流程運行狀態,流程變量等), 以此來對該任務作相應處理。在本例中,我們通過API調用來模擬這個過程。
現在流程運行到了第一個用戶任務, 在上面的XML文件中我們指定的完成該用戶的為"managers"這個分組,首先我們可以通過TaskService創建TaskQuery對象來查詢managers分組下的任務。如下:

TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
System.out.println("You have " + tasks.size() + " tasks:");
for (int i=0; i<tasks.size(); i++) {
    System.out.println((i+1) + ") " + tasks.get(i).getName());
}

Task task = tasks.get(0);
Map<String, Object> processVariables = taskService.getVariables(task.getId());
System.out.println(processVariables.get("employee") + " wants " +
        processVariables.get("nrOfHolidays") + " of holidays. Do you approve this?");

返回:

You have 1 tasks:
1) Approve or reject request
jack wants 3 of holidays. Do you approve this?

現在managers可以完成這個任務了。同樣的在完成任務時,雖然現實中是通過用戶界面的表單來完成,在這里通過傳遞包含approved變量(這個變量很重要,決定后面流程的路線, 這里我們賦值為true)的map來進行。
variables = new HashMap<String, Object>(); variables.put("approved", true); taskService.complete(task.getId(), variables);
通過執行上面的代碼managers完成了該任務,細心的你肯定在打印出的日志中發現了如下紅框中的內容。對,沒錯,這就是自動化任務節點中打印出來的。也就是說,現在自動化任務節點已經完成,流程已經到達第二個用戶任務了。


同樣的,根據XML中的定義, 我們知道第二個用戶應該由jack自己(${employee}對就流程變量的值)來完成,通過以下代碼即可查出jack需要完成的任務。

List<Task> tasks = taskService.createTaskQuery().taskCandidateOrAssigned("jack").list();

用和managers同樣的方法,jack就可以完成任務。至此,流程已經到達結束事件,整個流程也就走完了。

7. 查詢歷史數據

Flowable為我們存儲了豐富的流程實例執行的歷史數據。例如我們想看到流程實例每個節點耗時情況,首先通過ProcessEngine獲得HistoryService對象并且創建一個查詢作如下限制:

  • 查詢待定流程實例的節點情況
  • 只查看已完成的節點
    結果會按照結束時間排序,也就是執行順序:
HistoryService historyService = processEngine.getHistoryService();
List<HistoricActivityInstance> activities =
  historyService.createHistoricActivityInstanceQuery()
   .processInstanceId(processInstance.getId())
   .finished()
   .orderByHistoricActivityInstanceEndTime().asc()
   .list();

for (HistoricActivityInstance activity : activities) {
  System.out.println(activity.getActivityId() + " took "
    + activity.getDurationInMillis() + " milliseconds");
}

輸出:

startEvent took 3 milliseconds
approveTask took 3234 milliseconds
decision took 16 milliseconds
externalSystemCall took 4 milliseconds
holidayApprovedTask took 5888 milliseconds
approveEnd took 2 milliseconds

也就是對應數據庫表act_hi_actinst中的如下記錄:

全文完。。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 關于Mongodb的全面總結 MongoDB的內部構造《MongoDB The Definitive Guide》...
    中v中閱讀 32,010評論 2 89
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,881評論 18 139
  • 周末沒能休息,期待已久的戶外也未能成行,而是被派到了大博中監考高三一模,早6:30就冒著春寒坐上車出發了。到了博中...
    lxy255028閱讀 889評論 1 3
  • 最初決定寫下這篇文章時,我內心是有猶豫的。又要不停的碼字了。雖然我跟那些寫手相比太渺小。但是對我這種沉浸在帶娃中不...
    溪邊萱草長閱讀 523評論 2 1
  • 好久沒有過這種感覺了:屋外下著雨,身邊沒有人,手頭沒有什么不能拖的事,就開始冷,從心里到身體。 可能是南方下雨...
    在我少年閱讀 459評論 0 0