最近進行的一次技術選型(工作流引擎)及相關知識介紹

三國夢迴發表於2022-03-04

前言

最近有個新專案,需要實現類似工作流引擎的效果,如果不知道是啥,看完本文就懂了。
公司內其實也有些自研的,可能就是不像開源的這些那樣,還支援這個那個規範,都是基於需求定製開發的,擴充套件性稍微差點。
所以,這次其實幾個同事,分工調研了幾個開源的和公司內的,開源的包括activiti、flowable、camunda,我這邊主要調研了flowable、camunda,同事調研了activiti和公司內部的。

最終看下來,我們的需求,其實不需要用到這麼複雜的開源框架,公司內的一個框架更符合一些。開源的框架,會建很多表,表也不符合公司內的建表規範,所以還需要閱讀原始碼,去改造之類的,也比較麻煩。會引入很多jar包,總體來說,還是比較重。

文末有幾個引擎的對比,大家有興趣可以看看,也可以加我微信和我探討(只花了兩天時間,可能也瞭解得也比較粗略)。

最終來說,技術還是服務於需求的,不是因為框架牛逼就硬上,合適最重要。

先說說uml和omg

學過軟體工程的同學,肯定知道uml,全稱Unified Modeling Language,統一建模語言。建模,為啥要建模,因為軟體研發過程較為抽象,一個需求來了,肯定要先分析分析,建個模(通俗就是:畫個圖),但是每個人畫出來的圖都不一樣,比如uml裡用一個小人來表示使用者,有的人就不願意用小人。所以,為了業界內人士溝通交流更方便,就定義了一套標準,每種圖應該怎麼畫,包含了哪些部分。

比如uml包含了如下型別的圖,每種圖裡,都有固定的圖例來代表固定的意思(僅部分):

ok,大家明白了uml,我再說說omg是啥,omg是個標準化組織,致力於提出uml這樣類似的標準,和業界的公司進行討論交流,各公司的人、學術界的人、omg的人,共同討論,提出一個大家都能接受的方案,然後大家就按照這個標準來進行實現。

世界上的標準化機構很多,omg手裡拿出來的,現在廣為使用的,被iso採納的,有如下幾個。

主要就是uml和bpmn,注意,沒有xml(圖裡右上角那個是xmi)。

bpmn

圖形規範

bpmn(Business Process Model And Notation)是啥,也是一種建模語言,和uml類似,就是規定:xxx,用這個來表示,yyy,用那個來表示。

bpmn主要是對工作流進行建模,大家公司裡的oa系統,基本就是工作流的典範,比如下面這樣一個使用者請假流程,就是遵循bpmn規範的。

這個圖裡,哪些地方用空心圓,哪些地方是矩形,哪些地方用菱形,都是有講究,這就是bpmn規範裡定義的。

xml規範

bpmn對圖形有規範,對圖形背後的儲存格式也有定義,這個圖,最終會轉化為一份xml,這份xml也是遵循特定的schema的。

上圖這個xml,就是遵循omg官方的schema,裡面最外層是一個process元素,裡面的元素則只能是startEvent、sequenceFlow等。

這樣標準化了之後,業界各個廠商,就可以各自開發一套實現,只要這套實現,最終能生成上面這樣的xml,那就是符合bpmn的,拿這份bpmn檔案到其他廠商那裡,其他廠商的程式也能正確解析該檔案,因此就實現了互聯互通,這就是標準的力量。

bpmn版本歷史

主要是4個版本,現在業界基本都是基於2010年的最新版本2.0進行實現,也就是bpmn2.0

VERSION ADOPTION DATE URL
2.0 十二月 2010 https://www.omg.org/spec/BPMN/2.0/
1.2 一月 2009 https://www.omg.org/spec/BPMN/1.2/
1.1 一月 2008 https://www.omg.org/spec/BPMN/1.1/
1.0 三月 2007 https://www.omg.org/spec/BPMN/1.0/

bpmn 2.0的業界實現

實現還是挺多的,近10多個。現在大家比較用得多的,還是紅框的幾個,Activiti、Camunda、Flowable、jBPM。

這些實現,互相有些關係,就像log4j的維護人後來又建立了logback一樣。

目前主要就是在 Camunda/flowable 6/ activiti裡面去選擇。

flowable 內嵌模式快速瞭解

建立maven工程(文末有程式碼)

如果一上來,直接就開始比較各框架的差異,大家由於對其中任意一個都不瞭解,所以也沒法參照。這裡先講一下flowable框架(目前最先了解這個框架)。

flowable 引擎,支援兩種執行模式,一種是內嵌到業務服務中,我們們先講這種。

先建一個普通的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。比如,假設我們最終想建立一個員工請假流程,那麼,我們可以通過各種辦法(如flowable自帶的web-ui拖拽的方式或手動建立xml等),來建立一個下面這樣的,符合bpmn2.0規範的流程定義xml(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: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是不是比較抽象?這個xml就類似於一種標準格式,就像java的class檔案一樣,實現跨平臺的效果。

該xml對應的流程圖如下:

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

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

ok,引數準備好了,就準備傳給工作流了:

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

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

<userTask id="approveTask" name="Approve or reject request" activiti:candidateGroups="managers"/>

建立一個任務,任務有個標籤,就是candidate group,這裡是manager,可以猜得出,是給manager建了個審批任務。

manager查詢並審批任務

以下,基於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());
}

我們可把任務列印出來看看:

System.out.println("Which task would you like to complete?");
int taskIndex = Integer.valueOf(scanner.nextLine());
Task task = tasks.get(taskIndex - 1);
Map<String, Object> processVariables = taskService.getVariables(task.getId());
System.out.println(processVariables.get("employee") + " wants " +
    processVariables.get("nrOfHolidays") + " of holidays. Do you approve this?");

審批任務:

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,走不同分支。對應了:

回撥使用者程式碼--使用者開始休假

上面審批後,就會進入下一個節點:休假。

<serviceTask id="externalSystemCall" name="Enter holidays in external system"
             activiti:class="org.example.CallExternalSystemDelegate"/>

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

然後,基本流程就自己走完了。

flowable rest-api模式

簡介

上面那種,是其作為一個jar,內嵌到我們的程式裡,建立引擎對下。由我們業務程式去驅動引擎的執行。引擎和業務程式碼在同一個程式。

其實,flowable也可以作為一個獨立服務執行,提供rest-api出來,這樣的話,非java語言的開發者也可以使用該引擎了。

這個只需要我們下載官方的zip包,裡面有個rest的war包,我們丟到tomcat裡執行。

上傳工作流定義xml檔案,部署工作流

如果要實現上面java-api那樣的功能,我們就需要調介面來實現

下面就開始啟動工作流:

其他介面就不一一展示了。可以參考文件。

flowable-ui,通過web ui進行流程xml建模

上面手工建立xml,還是比較累的,我們可以通過其提供的web ui來建模,省點力氣。(不過也不是很好用,各種名詞比較費解,大家可能還是要自己做一套前端介面,呼叫自己的介面,來生成一個xml檔案)

上面的rest那一節,tomcat裡就部署了一個flowable-ui的。

就可以通過下面這樣的方式來建模。

其他方面

活躍程度:activiti是最活躍的,activiti (非常活躍,一天一個alpha版本)> camunda(一個月一個alpha版本) > flowable(幾個月或半年一個版本)
依賴:會引入37個jar包,當前最新的6.7.2版本
mysql:核心建表語句為7張,歷史表5張;程式執行後,結果有47張表,具體原因暫時沒去研究
持久層框架:寫mysql表時,使用mybatis

引擎對比

總結

demo程式碼:
https://gitee.com/ckl111/all-simple-demo-in-blog/tree/master/flowable-test

相關文章