工作流引擎架構設計

最近開發的安全管理平臺新增了很多工單申請流程需求,比如加白申請,開通申請等等。最開始的兩個需求,爲了方便,也沒多想,就直接開發了對應的業務代碼。

但隨着同類需求不斷增多,感覺再這樣寫可要累死人,於是開始了工作流引擎的開發之路。查找了一些資料之後,開發了現階段的工作流引擎,文章後面會有介紹。

雖然現在基本上能滿足日常的需求,但感覺還不夠智能,還有很多的優化空間,所以正好藉此機會,詳細瞭解了一些完善的工作流引擎框架,以及在架構設計上需要注意的點,形成了這篇文章,分享給大家。

什麼是工作流

先看一下維基百科對於工作流的定義:

工作流(Workflow),是對工作流程及其各操作步驟之間業務規則的抽象、概括描述。工作流建模,即將工作流程中的工作如何前後組織在一起的邏輯和規則,在計算機中以恰當的模型表達並對其實施計算。

工作流要解決的主要問題是:爲實現某個業務目標,利用計算機在多個參與者之間按某種預定規則自動傳遞文檔、信息或者任務。

簡單來說,工作流就是對業務的流程化抽象。WFMC(工作流程管理聯盟) 給出了工作流參考模型如下:

舉一個例子,比如公司辦公的 OA 系統,就存在大量的申請審批流程。而在處理這些流程時,如果每一個流程都對應一套代碼,顯然是不現實的,這樣會造成很大程度上的代碼冗餘,而且開發工作量也會驟增。

這個時候就需要一個業務無關的,高度抽象和封裝的引擎來統一處理。通過這個引擎,可以靈活配置工作流程,並且可以自動化的根據配置進行狀態變更和流程流轉,這就是工作流引擎。

簡單的工作流

那麼,一個工作流引擎需要支持哪些功能呢?

這個問題並沒有一個標準答案,需要根據實際的業務場景和需求來分析。在這裏,我通過一個工單流程的演進,從簡單到複雜,循序漸進地介紹一下都需要包含哪些基礎功能。

最簡單流程

最簡單的一個流程工單,申請人發起流程,每個節點審批人逐個審批,最終流程結束。

會籤

在這個過程中,節點分成了兩大類:簡單節點和複雜節點。

簡單節點處理邏輯不變,依然是處理完之後自動到下一個節點。複雜節點比如說會籤節點,則不同,需要其下的所有子節點都處理完成,才能到下一個節點。

並行

同樣屬於複雜節點,其任何一個子節點處理完成後,都可以進入到下一個節點。

條件判斷

需要根據不同的表單內容進入不同的分支流程。

舉一個例子,比如在進行休假申請時,請假一天需要直屬領導審批,如果大於三天則需要部門領導審批。

動態審批人

審批節點的審批人需要動態獲取,並且可配置。

審批人的獲取方式可以分以下幾種:

  1. 固定審批人

  2. 從申請表單中獲取

  3. 根據組織架構,動態獲取

  4. 從配置的角色組或者權限組中獲取

撤銷和駁回

節點狀態變更可以有申請人撤回,審批人同意,審批人駁回。那麼在駁回時,可以直接駁回到開始節點,流程結束,也可以到上一個節點。更復雜一些,甚至可以到前面流程的任意一個節點。

自動化節點

有一些節點是不需要人工參與的,比如說聯動其他系統自動處理,或者審批節點有時間限制,超時自動失效。

個性化通知

節點審批之後,可以配置不同的通知方式來通知相關人。

以上是我列舉的一些比較常見的需求點,還有像加簽,代理,腳本執行等功能,如果都實現的話,應該會是一個龐大的工作量。當然了,如果目標是做一個商業化產品的話,功能還是需要更豐富一些的。

但把這些常見需求點都實現的話,應該基本可以滿足大部分的需求了,至少對於我們系統的工單流程來說,目前是可以滿足的。

工作流引擎對比

既然這是一個常見的需求,那麼需要我們自己來開發嗎?市面上有開源項目可以使用嗎?

答案是肯定的,目前,市場上比較有名的開源流程引擎有 Osworkflow、Jbpm、Activiti、Flowable、Camunda 等等。其中:Jbpm、Activiti、Flowable、Camunda 四個框架同宗同源,祖先都是 Jbpm4,開發者只要用過其中一個框架,基本上就會用其它三個了。

Osworkflow

Osworkflow 是一個輕量化的流程引擎,基於狀態機機制,數據庫表很少。Osworkflow 提供的工作流構成元素有:步驟(step)、條件(conditions)、循環(loops)、分支(spilts)、合併(joins)等,但不支持會籤、跳轉、退回、加簽等這些操作,需要自己擴展開發,有一定難度。

如果流程比較簡單,Osworkflow 是一個很不錯的選擇。

JBPM

JBPM 由 JBoss 公司開發,目前最高版本是 JPBM7,不過從 JBPM5 開始已經跟之前不是同一個產品了,JBPM5 的代碼基礎不是 JBPM4,而是從 Drools Flow 重新開始的。基於 Drools Flow 技術在國內市場上用的很少,所有不建議選擇 JBPM5 以後版本。

JBPM4 誕生的比較早,後來 JBPM4 創建者 Tom Baeyens 離開 JBoss,加入 Alfresco 後很快推出了新的基於 JBPM4 的開源工作流系統 Activiti,另外 JBPM 以 hibernate 作爲數據持久化 ORM 也已不是主流技術。

Activiti

Activiti 由 Alfresco 軟件開發,目前最高版本 Activiti7。Activiti 的版本比較複雜,有 Activiti5、Activiti6、Activiti7 幾個主流版本,選型時讓人暈頭轉向,有必要先了解一下 Activiti 這幾個版本的發展歷史。

Activiti5 和 Activiti6 的核心 leader 是 Tijs Rademakers,由於團隊內部分歧,在 2017 年 Tijs Rademakers 離開團隊,創建了後來的 Flowable。Activiti6 以及 Activiti5 代碼已經交接給了 Salaboy 團隊,Activiti6 以及 Activiti5 的代碼官方已經暫停維護了。

Salaboy 團隊目前在開發 Activiti7 框架,Activiti7 內核使用的還是 Activiti6,並沒有爲引擎注入更多的新特性,只是在 Activiti 之外的上層封裝了一些應用。

Flowable

Flowable 是一個使用 Java 編寫的輕量級業務流程引擎,使用 Apache V2 license 協議開源。2016 年 10 月,Activiti 工作流引擎的主要開發者離開 Alfresco 公司並在 Activiti 分支基礎上開啓了 Flowable 開源項目。基於 Activiti v6 beta4 發佈的第一個 Flowable release 版本爲 6.0。

Flowable 項目中包括 BPMN(Business Process Model and Notation)引擎、CMMN(Case Management Model and Notation)引擎、DMN(Decision Model and Notation)引擎、表單引擎(Form Engine)等模塊。

相對開源版,其商業版的功能會更強大。以 Flowable6.4.1 版本爲分水嶺,大力發展其商業版產品,開源版本維護不及時,部分功能已經不再開源版發佈,比如表單生成器(表單引擎)、歷史數據同步至其他數據源、ES 等。

Camunda

Camunda 基於 Activiti5,所以其保留了 PVM,最新版本 Camunda7.15,保持每年發佈兩個小版本的節奏,開發團隊也是從 Activiti 中分裂出來的,發展軌跡與 Flowable 相似,同時也提供了商業版,不過對於一般企業應用,開源版本也足夠了。

以上就是每個項目的一個大概介紹,接下來主要對比一下 Jbpm、Activiti、Flowable 和 Camunda。只看文字的話可能對它們之間的關係還不是很清楚,所以我畫了一張圖,可以更清晰地體現每個項目的發展軌跡。

那麼,如果想要選擇其中一個項目來使用的話,應該如何選擇呢?我羅列了幾項我比較關注的點,做了一張對比表格,如下:

dALIuv

Flowable 應用舉例

如果選擇使用開源項目來開發自己的引擎,或者嵌入到現有的項目中,應該如何使用呢?這裏通過 Flowable 來舉例說明。

使用 Flowable 可以有兩種方式,分別是內嵌和獨立部署方式,現在來分別說明:

內嵌模式

創建 maven 工程

先建一個普通的 maven 工程,加入 Flowable 引擎的依賴以及 h2 內嵌數據庫的依賴,也可以使用 MySQL 數據庫來做持久化。

<!-- https://mvnrepository.com/artifact/org.flowable/flowable-engine -->
<dependency>
  <groupId>org.flowable</groupId>
  <artifactId>flowable-engine</artifactId>
  <version>6.7.2</version>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>1.4.192</version>
</dependency>

創建流程引擎實例

import org.flowable.engine.ProcessEngine;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;

public class HolidayRequest {

  public static void main(String[] args) {
    ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
      .setJdbcUrl("jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1")
      .setJdbcUsername("sa")
      .setJdbcPassword("")
      .setJdbcDriver("org.h2.Driver")
      .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);

    ProcessEngine processEngine = cfg.buildProcessEngine();
  }

}

接下來,我們就可以往這個引擎實例上部署一個流程 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:activiti="http://activiti.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" >

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

<!--        <userTask id="approveTask" />-->
        <userTask id="approveTask" />

        <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" 
                     activiti:class="org.example.CallExternalSystemDelegate"/>
        <sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>

<!--        <userTask id="holidayApprovedTask" />-->
        <userTask id="holidayApprovedTask" ${employee}"/>

        <sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>

        <serviceTask id="sendRejectionMail                     activiti:class="org.flowable.SendRejectionMail"/>
        <sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>

        <endEvent id="approveEnd"/>

        <endEvent id="rejectEnd"/>

    </process>

</definitions>

此 xml 是符合 bpmn2.0 規範的一種標準格式,其對應的流程圖如下:

接下來,我們就把這個文件傳給流程引擎,讓它基於該文件,創建一個工作流。

RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
  .addClasspathResource("holiday-request.bpmn20.xml")
  .deploy();

創建後,實際就寫到內存數據庫 h2 了,我們還可以把它查出來:

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

創建工作流實例

創建工作流實例,需要提供一些輸入參數,比如我們創建的員工請假流程,參數就需要:員工姓名、請假天數、事由等。

Scanner scanner= new Scanner(System.in);

System.out.println("Who are you?");
String employee = scanner.nextLine();

System.out.println("How many holidays do you want to request?");
Integer nrOfHolidays = Integer.valueOf(scanner.nextLine());

System.out.println("Why do you need them?");
String description = scanner.nextLine();


RuntimeService runtimeService = processEngine.getRuntimeService();

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("employee", employee);
variables.put("nrOfHolidays", nrOfHolidays);
variables.put("description", description);

參數準備好後,就可以傳給工作流了:

ProcessInstance processInstance =
    runtimeService.startProcessInstanceByKey("holidayRequest", variables);

此時,就會根據流程定義裏的:

<userTask id="approveTask" />

創建一個任務,任務有個標籤,就是 candidateGroups,這裏的 managers,可以猜得出,是給 managers 建了個審批任務。

查詢並審批任務

基於 manager 查詢任務:

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());
}

審批任務:

boolean approved = scanner.nextLine().toLowerCase().equals("y");
variables = new HashMap<String, Object>();
variables.put("approved", approved);
taskService.complete(task.getId(), variables);

這裏就是把全局變量 approved,設爲了 true,然後提交給引擎。引擎就會根據這裏的變量是 true 還是 false,選擇走不同分支。如下:

<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" 
             activiti:class="org.example.CallExternalSystemDelegate"/>

這裏有個 class,就是需要我們自己實現的:

最後,流程就走完結束了。

REST API 模式

上面介紹的方式是其作爲一個 jar,內嵌到我們的程序裏。創建引擎實例後,由我們業務程序去驅動引擎的運行。引擎和業務代碼在同一個進程裏。

第二種方式,Flowable 也可以作爲一個獨立服務運行,提供 REST API 接口,這樣的話,非 Java 語言開發的系統就也可以使用該引擎了。

這個只需要我們下載官方的 zip 包,裏面有個 rest 的 war 包,可以直接放到 tomcat 裏運行。

部署工作流

在這種方式下,如果要實現上面舉例的員工請假流程,可以通過調接口來實現:

啓動工作流:

其他接口就不一一展示了,可以參考官方文檔。

通過頁面進行流程建模

截止到目前,創建工作流程都是通過建立 xml 來實現的,這樣還是非常不方便的。因此,系統也提供了通過頁面可視化的方式來創建流程,使用鼠標拖拽相應組件即可完成。

但是體驗下來還是比較辛苦的,功能很多,名詞更多,有很多都不知道是什麼意思,只能不斷嘗試來理解。

開源 VS 自研

既然已經有成熟的開源產品了,還需要自研嗎?這算是一個老生常談的問題了。那到底應該如何選擇呢?其實並不困難,歸根結底就是要符合自身的業務特點,以及實際的需求。

開源優勢:

入門門檻低,有很多可以複用的成果。通常而言,功能比較豐富,周邊生態也比較完善,投入產出比比較高。一句話總結,投入少,見效快。

開源劣勢:

內核不容易掌控,門檻較高,通常開源的功能和實際業務並不會完全匹配,很多開源產品開箱即用做的不夠好,需要大量調優。一句話總結,入門容易掌控難。

自研優勢:

產品核心技術掌控程度高,可以更好的貼着業務需求做,可以定製的更好,基於上述兩點,通常更容易做到良好的性能表現。一句話總結,量身定製。

自研劣勢:

投入產出比略低,且對團隊成員的能力曲線要求較高。此外封閉的生態會導致周邊支持缺乏,當需要一些新需求時,往往都需要定製開發。一句話總結,啥事都要靠自己。

基於以上的分析,再結合我們自身業務,我總結了以下幾點可供參考:

  1. 開源項目均爲 Java 技術棧,而我們使用 Python 和 Go 比較多,技術棧不匹配

  2. 開源項目功能豐富,而我們業務相對簡單,使用起來比較重

  3. 開源項目並非開箱即用,需要結合業務特點做定製開發,學習成本和維護成本比較高

綜上所述,我覺得自研更適合我們現階段的產品特點。

工作流引擎架構設計

如果選擇自研,架構應該如何設計呢?有哪些比較重要的模塊和需要注意的點呢?下面來詳細說說。

BPMN

BPMN 全稱是 Business Process Model And Notation,即業務流程模型和符號。

可以理解成一種規範,在這個規範裏,哪些地方用空心圓,哪些地方用矩形,哪些地方用菱形,都是有明確定義的。

也就是說,只要是基於這個規範開發的系統,其所創建的流程就都是可以通用的。

其實,如果只是開發一個內部系統,不遵守這個規範也沒有問題。但要是做一個產品的話,爲了通用性更強,最好還是遵守這個規範。

流程設計器

對於工作流引擎來說,流程設計器的選型至關重要,它提供了可視化的流程編排能力,決定了用戶體驗的好壞。

目前主流的流程設計器有 Activiti-Modeler,mxGraph,bpmn-js 等,下面來做一個簡單介紹。

Activiti-Modeler

Activiti 開源版本中帶了 Web 版流程設計器,在 Activiti-explorer 項目中有 Activiti-Modeler,優點是集成簡單,開發工作量小,缺點是界面不美觀,用戶體驗差。

mxGraph

mxGraph 是一個強大的 JavaScript 流程圖前端庫,可以快速創建交互式圖表和圖表應用程序,國內外著名的 ProcessOne 和 draw.io 都是使用該庫創建的強大的在線流程圖繪製網站。

由於 mxGraph 是一個開放的 js 繪圖開發框架,我們可以開發出很炫的樣式,或者完全按照項目需求定製。

官方網站:http://jgraph.github.io/mxgrap

bpmn-js

bpmn-js 是 BPMN2.0 渲染工具包和 Web 模型。bpmn-js 正在努力成爲 Camunda BPM 的一部分。bpmn-js 使用 Web 建模工具可以很方便的構建 BPMN 圖表,可以把 BPMN 圖表嵌入到你的項目中,容易擴展。

bpmn-js 是基於原生 js 開發,支持集成到 vue、react 等開源框架中。

官方網站:https://bpmn.io/

以上介紹的都屬於是功能強大且完善的框架,除此之外,還有其他基於 Vue 或者 React 開發的可視化編輯工具,大家也可以根據自己的實際需求進行選擇。

流程引擎

最後來說說流程引擎,整個系統的核心。引擎設計的好壞決定了整個系統的穩定性,可用性,擴展性等等。

整體架構如圖所示,主要包括一下幾個部分:

一、流程設計器主要通過一系列工具創建一個計算機可以處理的工作流程描述,流程建模通常由許多離散的節點步驟組成,需要包含所有關於流程的必要信息,這些信息包括流程的起始和結束條件,節點之間的流轉,要承擔的用戶任務,被調用的應用程序等。

二、流程引擎主要負責流程實例化、流程控制、節點實例化、節點調度等。在執行過程中,工作流引擎提供流程的相關信息,管理流程的運行,監控流程的運行狀態,並記錄流程運行的歷史數據。

三、存儲服務提供具體模型及流程流轉產生的信息的存儲空間,工作流系統通常需要支持各種常見的數據庫存儲。

四、組織模型不屬於工作流系統的建設範圍,但流程設計器在建模的過程中會引用組織模型,如定義任務節點的參與者。還有就是在流程流轉的過程中同樣也需要引用組織模型,如在進行任務指派時,需要從組織模型中確定任務的執行者。

工作流引擎內部可以使用平臺自身的統一用戶組織架構,也可以適配第三方提供的用戶組織架構。

五、工作流引擎作爲一項基礎支撐服務提供給各業務系統使用,對第三方系統開放標準的 RESTful 服務

後記

下面來說說我現在開發的系統支持到了什麼程度,以及未來可能的發展方向。由於畢竟不是一個專門的工單系統,工單申請也只是其中的一個模塊,所以在整體的功能上肯定和完整的工作流引擎有很大差距。

第一版

第一版並沒有流程引擎,開發方式簡單粗暴,每增加一個流程,就需要重新開發對應的表和業務代碼。

這樣做的缺點是非常明顯的:

  1. 每個流程需要單獨開發,工作量大,開發效率低

  2. 流程功能相近,代碼重複量大,冗餘,不利於維護

  3. 定製化開發,缺少擴展性#

第二版

第二版,也就是目前的版本。

隨着工單流程逐漸增多,工作量逐漸增大,於是開始對流程進行優化,開發了現階段的工作流引擎。

在新增一個工單流程時,需要先進行工作流配置,配置其基礎信息,自定義字段,狀態和流轉這些信息。還支持配置自動化節點,可以根據條件由程序自動完成相關操作並審批。

配置好之後,後端無需開發,由統一的引擎代碼進行處理,包括節點審批流轉,狀態變更等。只需要開發前端的創建和查詢頁面即可,相比於第一版,已經在很大程度上提高了開發效率。

目前版本需要優化的點:

  1. 缺少可視化流程設計器,無法做到拖拽式設計流程

  2. 節點之間狀態流轉不夠靈活

  3. 缺少分佈式事物支持,以及異常處理機制

下一個版本

針對以上不足,下一個版本準備主要優化三點,如下:

  1. 需要支持可視化流程設計器,使流程設計更加簡單,靈活

  2. 根據流程配置自動生成前端頁面,做到新增一種類型的工單,無需開發

  3. 增加節點自動化能力,異常處理機制,提高系統的穩定性

參考文章:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/z2lbTDl5G0fcwlGB7jCMAg