工作流引擎架構設計

alwaysbeta發表於2023-01-12

原文連結: 工作流引擎架構設計

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

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

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

什麼是工作流

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

工作流(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。只看文字的話可能對它們之間的關係還不是很清楚,所以我畫了一張圖,可以更清晰地體現每個專案的發展軌跡。

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

Activiti 7 Flowable 6 Camunda JBPM 7
流程協議 BPMN2.0、XPDL、PDL BPMN2.0、XPDL、XPDL BPMN2.0、XPDL、XPDL BPMN2.0
開源情況 開源 商業和開源版 商業和開源版 開源
開發基礎 JBPM4 Activiti 5 & 6 Activiti 5 版本 5 之後 Drools Flow
資料庫 Oracle、SQL Server、MySQL Oracle、SQL Server、MySQL、postgre Oracle、SQL Server、MySQL、postgre MySQL,postgre
架構 spring boot 2 spring boot 1.5 spring boot 2 Kie
執行模式 獨立執行和內嵌 獨立執行和內嵌 獨立執行和內嵌 -
流程設計器 AngularJS AngularJS bpmn.js -
活躍度 活躍 相對活躍 相對活躍 -
表數量 引入 25 張表 引入 47 張表 引入 19 張表 -
jar 包數量 引入 10 個 jar 引入 37 個 jar 引入 15 個 jar -

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" name="Holiday Request" isExecutable="true">

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

<!--        <userTask id="approveTask" name="Approve or reject request"/>-->
        <userTask id="approveTask" name="Approve or reject request" activiti: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"
                     activiti:class="org.example.CallExternalSystemDelegate"/>
        <sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>

<!--        <userTask id="holidayApprovedTask" name="Holiday approved"/>-->
        <userTask id="holidayApprovedTask" name="Holiday approved" activiti:assignee="${employee}"/>

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

        <serviceTask id="sendRejectionMail" name="Send out rejection email"
                     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" name="Approve or reject request" activiti:candidateGroups="managers"/>

建立一個任務,任務有個標籤,就是 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" name="Enter holidays in external system"
             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. 增加節點自動化能力,異常處理機制,提高系統的穩定性

以上就是本文的全部內容,如果覺得還不錯的話歡迎點贊轉發關注,感謝支援。


參考文章:

推薦閱讀:

相關文章