【技術(shù)】Activiti進(jìn)行時(shí)——企業(yè)工作流生命周期貫通

圖1:一個(gè)典型的審批工作流程

最近做了一次對(duì)企業(yè)/云平臺(tái)級(jí)工作流引擎Activiti的調(diào)查:

  • TA,系出名門——由JBoss公司jBPM4引擎的原作者創(chuàng)立(JBoss公司無奈地已在jBPM5中改用了Drools內(nèi)核、走了其他路線);
  • TA,植根于Java開源社區(qū),擁有SpringSource、MuleSoft、Signavio等公司以及全球大量擁護(hù)者基于最新開發(fā)實(shí)踐的養(yǎng)分支持;
  • TA,“出場(chǎng)費(fèi)”要比IBM、Oracle等大廠的同類低很多,卻同樣支持BPMN 2.0國際標(biāo)準(zhǔn)、各大流行數(shù)據(jù)庫、集群擴(kuò)展等,包括對(duì)云平臺(tái)所需的多租戶特性也已有原生支持。

InfoQ上2012年4月就已有過一篇推薦Activiti的文章,本文則是筆者近2個(gè)月實(shí)戰(zhàn)Activiti的一篇簡(jiǎn)書&共享。

背景概述

目標(biāo) ------------------------------------

  1. 用戶能方便地修改流程定義的細(xì)節(jié)(例如改分歧條件、復(fù)制添加已有的類似節(jié)點(diǎn)),只要不涉及新的表單/服務(wù)/編程、就無需委托開發(fā)人員出馬。
  2. 能以REST服務(wù)方式調(diào)用,與現(xiàn)有業(yè)務(wù)系統(tǒng)銜接。比如這次的業(yè)務(wù)系統(tǒng)是.Net架構(gòu),而Activiti則是Java開源引擎,但因?yàn)樘峁┝薘EST API接口(且易于擴(kuò)展)、能以JSON/XML方式完成輕便的數(shù)據(jù)交互。
  3. 支持BPMN 2.0國際標(biāo)準(zhǔn)(具體后述)。標(biāo)準(zhǔn)統(tǒng)一之爭(zhēng)雖然往往夾雜著企業(yè)各自的便利/利益,但畢竟有其行業(yè)合理性與權(quán)威性(例如繁雜的醫(yī)療/金融/物流領(lǐng)域),在行業(yè)出現(xiàn)新的需求而升級(jí)標(biāo)準(zhǔn)時(shí)、只需升級(jí)符合新標(biāo)準(zhǔn)的引擎就能適應(yīng)需求。

成果 ------------------------------------

  • 打通基于REST API接口的 工作流生命周期(不足部分做定制補(bǔ)充)

    1. 流程設(shè)計(jì)模型:一覽、編輯(有模型編輯器)、部署(+允許啟動(dòng)者)-->流程定義
    2. 流程定義:一覽、掛起/激活、啟動(dòng)-->流程實(shí)例
    3. 流程實(shí)例:一覽、掛起/激活、查看(當(dāng)前狀態(tài))
    4. 任務(wù):一覽(我的任務(wù))、簽收/完成(表單變量-->流程變量)
    5. 監(jiān)聽器:觸發(fā)、處理(e.g.發(fā)送任務(wù)通知郵件)
  • 不包括

    1. 表單編輯器:Activiti并不帶表單編輯器(收費(fèi)版中有),內(nèi)置+外置兩種表單支持方式能滿足入門級(jí)業(yè)務(wù)需求,但多數(shù)需集成開發(fā)。
    2. 郵件模板編輯器:郵件模板的可視化編輯、也不在工作流引擎的負(fù)責(zé)范圍之內(nèi),需根據(jù)業(yè)務(wù)需求定制開發(fā)。

資料 ------------------------------------

<a id="reference" name="reference"></a>

調(diào)研、定制中以下一些資料較為有用,(感謝相關(guān)作者&)已有相關(guān)基礎(chǔ)的讀者可據(jù)此推斷本文的參考價(jià)值。

工作流概念+BPMN標(biāo)準(zhǔn)

如果希望直接將Demo跑起來邊看邊理解的話,可以跳到下一節(jié),否則就先隨我把概念部分混個(gè)眼熟。

工作流概念

制作了一張工作流在生命周期中不同階段/環(huán)節(jié)間切換的關(guān)系圖:


圖2:工作流生命周期
  • REST API:在此特指Activiti引擎的RESTful接口、以及另行擴(kuò)展的接口,提供所有工作流解析/處理的核心功能
  • 模型編輯器:在此特指Activiti Modeler,也可選用其他來做定制
  • 流程設(shè)計(jì)模型:工作流在此處于設(shè)計(jì)環(huán)節(jié),可被可視化地編輯調(diào)整(如圖1)。其中重要的元素,包括用戶任務(wù)(與表單畫面銜接,接受用戶的選擇/承認(rèn)/否決之類的輸入;是活動(dòng)節(jié)點(diǎn)中最主要的一種,參見BPMN2.0標(biāo)準(zhǔn))、監(jiān)聽器(響應(yīng)各種任務(wù)/活動(dòng)節(jié)點(diǎn)上的事件(e.g.已被簽收/已被辦理)、進(jìn)行不同處理(e.g.按照特定模板發(fā)送通知郵件))。
  • 流程定義:工作流在此處于“已部署”環(huán)節(jié),以一個(gè)“請(qǐng)假流程”為例的話、一旦被部署、有權(quán)限的用戶就可以啟動(dòng)/發(fā)起該流程申請(qǐng)請(qǐng)假了(誕生一個(gè)“流程實(shí)例”、見后),流程定義的內(nèi)容已不可被編輯(只可被掛起/激活),如需做修改、只能對(duì)流程設(shè)計(jì)模型做編輯、并再次部署。換句話說,流程定義、是處于流程從設(shè)計(jì)環(huán)節(jié)到運(yùn)行階段之間、相對(duì)穩(wěn)定的中間環(huán)節(jié)。
  • 流程實(shí)例:工作流至此已進(jìn)入“運(yùn)行中”階段。一個(gè)已經(jīng)被啟動(dòng)、進(jìn)入“運(yùn)行中”階段的工作流,就好比一個(gè)“狀態(tài)機(jī)”,可能處在不同活動(dòng)節(jié)點(diǎn)的不同狀態(tài)上(還可能并發(fā))、直至結(jié)束。當(dāng)工作流運(yùn)行到一個(gè)用戶任務(wù)節(jié)點(diǎn)(比如“總經(jīng)理/副總經(jīng)理承認(rèn)”)、而該任務(wù)的候選辦理者有多人時(shí),其中之一(比如副總經(jīng)理)就可以“簽收”,然后“辦理”該任務(wù)(也可以在界面上隱匿“簽收”、一律只顯示成“辦理”)。
  • 流程狀態(tài)跟蹤器:對(duì)“運(yùn)行中”階段的工作流當(dāng)前所處的活動(dòng)節(jié)點(diǎn)、狀態(tài)做出可視化標(biāo)識(shí)。使用官方組件Diagram Viewer會(huì)很方便,也可使用其他方法
  • 歷史流程實(shí)例:已經(jīng)結(jié)束的流程,仍可被查詢到(包括其中的用戶任務(wù)、選項(xiàng)輸入),這會(huì)有助于事后分析統(tǒng)計(jì)。
    注意:5.16版本開始還新增了“事件日志機(jī)制”,可采用大數(shù)據(jù)存儲(chǔ)分析(參見官方文檔)。

BPMN 2.0標(biāo)準(zhǔn)

<a id="bpmn2" name="bpmn2"></a>

先上一張諾貝爾頒獎(jiǎng)盛典籌備工作的設(shè)計(jì)模型圖:


圖3:The Nobel Prize Process Diagram

基于工業(yè)標(biāo)準(zhǔn)的工作流設(shè)計(jì),是滿足不同領(lǐng)域客戶復(fù)雜業(yè)務(wù)需求的有力保障。

BPMN2.0標(biāo)準(zhǔn)工作流的模型編輯與保存,可以在瀏覽器App形式的Activiti Modeler中進(jìn)行(并可以嵌入你的業(yè)務(wù)系統(tǒng)中),在此直接對(duì)照著Modeler的圖標(biāo)對(duì)主要的BPMN元素做簡(jiǎn)單介紹:

圖4:Activiti Modeler Shape Repository -- BPMN2.0
  • 開始事件(Start Event)
    <提醒>

    • 啟動(dòng)者變量(initiator):一個(gè)開始事件的initiator屬性中所保存的、只是一個(gè)“用于記錄啟動(dòng)者user_id的變量名稱”,可以算是個(gè)“變量聲明”屬性。當(dāng)一個(gè)用戶、例如"kermit"(官方范例用戶)、啟動(dòng)了這一工作流時(shí)(在流程設(shè)計(jì)模型被部署、成為一個(gè)“流程定義”后、它就能被某一用戶啟動(dòng)成為一個(gè)“流程實(shí)例”),"kermit"就會(huì)被記錄到在initiator中聲明的變量、比如"starter"中,那樣就會(huì)有一個(gè)流程變量starter被賦值為"kermit",并能在后續(xù)的活動(dòng)節(jié)點(diǎn)中(e.g.用戶任務(wù))被用于任務(wù)辦理人(assignee)屬性的賦值表達(dá)式(e.g.${starter})、或是其他的例如分歧條件表達(dá)式中(${starter=="kermit"})。詳見官方文檔
      開始事件的initiator屬性是唯一用于保存“變量名稱”的,在整個(gè)流程級(jí)別、或是任務(wù)級(jí)別、都存在著類似于candidateUsers/candidateStarterUsers的屬性,它們保存的直接就是允許啟動(dòng)流程/辦理任務(wù)的用戶ID(允許多個(gè))。詳見官方文檔
      注意:Activiti Modeler的當(dāng)前版本還不支持設(shè)置流程級(jí)別的candidateStarterUsers/candidateStarterGroups(允許啟動(dòng)者),Eclipse插件Activiti Designer中可以,但僅供開發(fā)者使用;因此需要通過擴(kuò)展REST API、在“流程設(shè)計(jì)模型::部署”、或是“流程定義::設(shè)置”環(huán)節(jié)進(jìn)行設(shè)置。
  • 活動(dòng)節(jié)點(diǎn)(Activity)
    <提醒>

    • 用語:Activity通常被翻譯成“節(jié)點(diǎn)”、包括了各種任務(wù)(Task)、順序流(Sequence Flow)、子流程(Sub-Process)等,一切具備生命周期狀態(tài)的元素。國內(nèi)也有使用“步驟”(Step)的,但其實(shí)Step的定義似乎應(yīng)是“節(jié)點(diǎn)(Activity)+網(wǎng)關(guān)(Gateway)”(@ultimus.com)
    • humanPerformer vs activiti:candidateUsers:這里將引入BPMN標(biāo)準(zhǔn)屬性與Activiti擴(kuò)展屬性的對(duì)比與互換的話題。
      1.語法上的不同:Activiti擴(kuò)展確有其化繁為簡(jiǎn)的優(yōu)勢(shì),請(qǐng)比較兩段定義("activiti:"打頭的即為擴(kuò)展),
      a. BPMN標(biāo)準(zhǔn)屬性
    <process>
        ...
        <userTask id='theTask' name='important task' >
            <potentialOwner>
              <resourceAssignmentExpression>
                <formalExpression>user(kermit), group(management)</formalExpression>
              </resourceAssignmentExpression>
            </potentialOwner>
        </userTask>
    </process>
    

    b. Activiti擴(kuò)展屬性

    <process>
        ...
        <userTask id="theTask1" name="my task" activiti:assignee="kermit" />
        <userTask id="theTask2" name="my task" activiti:candidateUsers="kermit, gonzo" />
        <userTask id="theTask3" name="my task" activiti:candidateGroups="management, accountancy" />
    </process>
    

    2.標(biāo)準(zhǔn)與擴(kuò)展沖突的解決:詳見官方文檔
    a.Activiti擴(kuò)展的前提是:總有簡(jiǎn)單的方法、轉(zhuǎn)換成標(biāo)準(zhǔn)方法
    b.Activiti擴(kuò)展的目標(biāo)是:最終把它們加入到下一版本的BPMN規(guī)范中, 或者至少可以引起對(duì)特定BPMN結(jié)構(gòu)的討論

  • 結(jié)構(gòu)(Structural)
    <非重點(diǎn)>

  • 網(wǎng)關(guān)(Gateway)
    <非重點(diǎn)>

  • 結(jié)束事件(End Event)
    <非重點(diǎn)>

  • 連接(Connecting Object)

    • 順序流(sequence flow):用于連接各種事件、活動(dòng)節(jié)點(diǎn)、網(wǎng)關(guān)的連線。除了網(wǎng)關(guān)會(huì)對(duì)流出的順序流有特殊限制之外(例如排他網(wǎng)關(guān))、BPMN缺省的流出順序流(若有復(fù)數(shù)個(gè)的話)是“并發(fā)”的、即同時(shí)發(fā)出。可以對(duì)順序流做條件式限制,即添加一個(gè)conditionExpression元素:
    <sequenceFlow id="flow" sourceRef="theStart" targetRef="theTask">
      <conditionExpression xsi:type="tFormalExpression">
        <![CDATA[${order.price > 100 && order.price < 250}]]>
      </conditionExpression>
    </sequenceFlow>
    

    注意:目前條件表達(dá)式只能使用UEL(統(tǒng)一表達(dá)式語言),詳見官方文檔

開發(fā)環(huán)境構(gòu)筑:Eclipse + Maven + Tomcat + Activiti

<a id="env" name="env"></a>

我所使用的開發(fā)環(huán)境:

  • Eclipse Java EE IDE for Web Developers, Luna
  • Apache Maven 3.2.3
  • Apache Tomcat 7
  • Activiti 5.16.3(5.16.4起設(shè)置方法有變化且官方文檔未同步更新):請(qǐng)下載開源代碼,而非其運(yùn)行版

<提醒>

  • Maven的基本配置對(duì)初學(xué)者來說會(huì)多有挫折
    Maven本身是個(gè)好東西,開源項(xiàng)目很多對(duì)其他開源項(xiàng)目有大量的依賴引用,難免存在交叉引用、版本覆蓋、重復(fù)下載等繁瑣問題,而Maven工具&標(biāo)準(zhǔn)就是為了應(yīng)對(duì)這個(gè)而產(chǎn)生的(并成為Apache頂級(jí)項(xiàng)目)。
    可是,主要陷阱在于:

    • 國內(nèi)糟糕的網(wǎng)絡(luò)訪問情況...很多中央倉庫鏡像會(huì)出現(xiàn)超時(shí)
    • 部分依賴庫會(huì)不存在于中央倉庫中,需手動(dòng)下載后放入本地倉庫中
    • Maven可以用命令行方式運(yùn)行、也可用Eclipse插件方式來使用,但有時(shí)兩者的結(jié)果會(huì)不盡相同...

    解決方法:

    • 找到好文檔,從新建/導(dǎo)入一個(gè)Maven項(xiàng)目、編譯&運(yùn)行它開始
      使用Eclipse構(gòu)建Maven項(xiàng)目 (step-by-step)
      注意: 其中有些章節(jié)為【必須讀】有些【不必讀】
      2.Maven的安裝和配置
      2.2.配置
      2.2.1.修改默認(rèn)的本地倉庫位置:必須讀 重新設(shè)置本地倉庫位置,使得你可以更方便地 a.將需要手動(dòng)下載的依賴庫放置其中 b.備份已經(jīng)成功構(gòu)建的本地倉庫、用于環(huán)境移植
      2.2.2.修改默認(rèn)的中央倉庫鏡像:不必讀 通常是根據(jù)項(xiàng)目不同,修改項(xiàng)目pom.xml文件中的<repository>配置
      4.使用Maven來構(gòu)建Web項(xiàng)目
      4.1.創(chuàng)建Maven的web項(xiàng)目:不必讀 我們需要的是導(dǎo)入Activiti官方項(xiàng)目、而非新建,具體方法見后“導(dǎo)入已有項(xiàng)目”
      4.2.使用Maven添加項(xiàng)目依賴包:不必讀 導(dǎo)入項(xiàng)目的pom.xml中已經(jīng)配置好了,除非你有新的依賴包需求。
    • 使用國內(nèi)經(jīng)驗(yàn)者已有的“私服”:比如咖啡兔的Maven私服,具體POM設(shè)置見后“導(dǎo)入已有項(xiàng)目”
    • 小組開發(fā)時(shí),可考慮在局域網(wǎng)里搭一個(gè)“私服”(自己的中央倉庫):參考Nexus
    • <a id="import" name="import"></a>導(dǎo)入已有項(xiàng)目:Activiti-activiti-5.16.3\modules\activiti-webapp-explorer2項(xiàng)目
      • Eclipse中導(dǎo)入的方法:File > Import... > Maven::Existing Maven Projects > Next > 選中項(xiàng)目根目錄下的pom.xml文件即可。

      • Maven配置修改(pom.xml文件編輯):m2eclipse插件已經(jīng)裝好的情況下,pom.xml會(huì)自動(dòng)被在表單編輯器中打開(不過通常只需在文本方式下編輯、表單方式下輔助閱讀)
        a. 添加中央倉庫鏡像,比如咖啡兔的Maven私服(不過有時(shí)也會(huì)斷線、需添加其他鏡像)

         <repositories>
             ......
             <repository>
                 <id>kafeitu</id>
                 <url>http://maven.kafeitu.me/nexus/content/groups/public</url>
             </repository>
         </repositories>
        

        b. 添加依賴庫,比如數(shù)據(jù)庫驅(qū)動(dòng)(這里不需要修改,使用默認(rèn)h2數(shù)據(jù)庫即可、且無需下載安裝h2(已自帶,否則反而要改版本號(hào)))

         <dependencies>
             ......
             <dependency>
                 <groupId>com.h2database</groupId>
                 <artifactId>h2</artifactId>
             </dependency>
         </dependencies>
        
      • 數(shù)據(jù)庫連接修改:src/main/resources/db.properties
        (可選)給出一個(gè)改用Microsoft SQL Server的例子:

         db=mssql
         jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
         jdbc.url=jdbc:sqlserver://localhost:1433;databaseName=activiti
         jdbc.username=sa
         jdbc.password=
        

        不過相應(yīng)的,pom.xml中的數(shù)據(jù)庫驅(qū)動(dòng)依賴庫就也需要添改:

         <dependencies>
             ......
             <dependency>
                 <groupId>com.microsoft.sqlserver</groupId>
                 <artifactId>sqljdbc4</artifactId>
             </dependency>
         </dependencies>
        
      • 工作流引擎配置修改:src/main/webapp/WEB-INF/activiti-standalone-context.xml

         <bean id="demoDataGenerator" class="org.activiti.explorer.demo.DemoDataGenerator" init-method="init">
             ......
             <property name="createDemoUsersAndGroups" value="true" />
             <property name="createDemoProcessDefinitions" value="true" />
             <property name="createDemoModels" value="true" />
         </bean>
         <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
             ......
             <property name="databaseSchemaUpdate" value="true" />
         </bean>
        

        Activiti工作流引擎配置、目前(5.16.3)采用Spring框架方式加載,以實(shí)現(xiàn)不同擴(kuò)展的靈活配置。上手階段所需做的修改,主要就是如上屬性(分別用于自動(dòng)生成Demo數(shù)據(jù),以及比對(duì)引擎與數(shù)據(jù)庫之間的版本、自動(dòng)新建/升級(jí)數(shù)據(jù)庫),更多可參考官方文檔

      • Maven項(xiàng)目刷新:Project右鍵菜單 > Maven > Update Project...,對(duì)于Maven項(xiàng)目來說,單單F5在文件系統(tǒng)層面刷新同步是不夠的。

      • Maven項(xiàng)目編譯:

        • 命令行方式:較為簡(jiǎn)便,在你設(shè)置了PATH(包含%MAVEN_HOME%\bin)之后,便可在命令行窗口中、進(jìn)入到Maven項(xiàng)目的根目錄(pom.xml所在地)、然后執(zhí)行Maven的系列命令了,如:
          mvn compile
        • m2eclipse插件方式:更為簡(jiǎn)便,不過建議先搭建起調(diào)試環(huán)境(不同于.Net中attach到進(jìn)程的方式,見后),然后直接在運(yùn)行/調(diào)試前自動(dòng)編譯即可。
    • 導(dǎo)入已有項(xiàng)目:咖啡兔Demo,有了導(dǎo)入官方范例的基礎(chǔ),導(dǎo)入其他例子就很簡(jiǎn)單。
  • 基于Maven的Tomcat Web項(xiàng)目的斷點(diǎn)調(diào)試也會(huì)有一番周折
    解決方法:

定制補(bǔ)充REST API接口打通生命周天(周期)

經(jīng)歷過之前的準(zhǔn)備,便可以導(dǎo)入這1.5個(gè)人月調(diào)研開發(fā)的成果項(xiàng)目了,上圖,


圖5:測(cè)試入口界面(Web客戶端)

如果說activiti-explorer和kft-activiti-demo都是界面友好、前端與后臺(tái)并重的話,這次項(xiàng)目則純粹集中在后臺(tái)(封裝Activiti的引擎功能),因?yàn)榍岸私缑妫òū韱尉庉嬈鳌⑧]件末班編輯器)將由.Net頁面(一個(gè)企業(yè)內(nèi)部門戶網(wǎng)站)完成,銜接關(guān)系可參見圖2(生命周期)。

項(xiàng)目的完成可劃分為6個(gè)步驟:

  • Step1. 在官方范例activiti-explorer中嵌入activiti-rest
    <a id="combine_explorer_rest" name="combine_explorer_rest"></a>
    前文推薦了導(dǎo)入官方范例項(xiàng)目activiti-explorer作為入門(一分鐘入門+導(dǎo)入已有項(xiàng)目),在此第一步,我們就是在其基礎(chǔ)上、加上對(duì)REST API的支持,也就是嵌入另一個(gè)官方范例activiti-rest。
    對(duì)于缺少J2EE/Spring項(xiàng)目經(jīng)驗(yàn)的人可能會(huì)稍有障礙,其實(shí)兩個(gè)官方范例項(xiàng)目合并的主要破解點(diǎn)如下:

    • web.xml 中的Listener配置:src\main\webapp\WEB-INF\web.xml
      activiti-webapp-explorer2(activiti-explorer的項(xiàng)目名)中的Listener配置,分別是Spring框架的ContextLoaderListener和RequestContextListener;而當(dāng)你打開activiti-webapp-rest2(activiti-rest的項(xiàng)目名)則會(huì)發(fā)現(xiàn),Listener只有org.activiti.rest.common.servlet.ActivitiServletContextListener,這是一個(gè)位于activiti-common-rest依賴包中的類、只用來啟動(dòng)和回收一個(gè)ProcessEngine類的實(shí)例。

      結(jié)論:我們無需合并該Listener,因?yàn)閍ctiviti-explorer已經(jīng)采用Spring的Bean方式來加載ProcessEngine實(shí)例。

    • web.xml 中的Servlet配置:src\main\webapp\WEB-INF\web.xml
      官方范例(5.16.3為止)采用的是Restlet方式的REST實(shí)現(xiàn),因此用到的servlet是org.restlet.ext.servlet.ServerServlet。我們將來擴(kuò)展REST API將采用Spring MVC方式(后續(xù)),但對(duì)已有的REST API(5.16.3為止)、我們只能保留現(xiàn)有的Restlet方式入口,不過需注意,activiti-explorer范例與activiti-rest范例所用的org.restlet.application(Restlet方式下的入口類)并不相同,前者用的是一個(gè)只做了簡(jiǎn)單Router綁定的自定義類org.activiti.rest.explorer.application.ExplorerRestApplication、僅供modeler和diagram使用,后者(activiti-rest)用的則是包含較嚴(yán)格認(rèn)證機(jī)制的org.activiti.rest.service.application.ActivitiRestServicesApplication,也因此如官方文檔所述、標(biāo)準(zhǔn)REST API的調(diào)用都是需要經(jīng)過Basic認(rèn)證的。

      結(jié)論:合并兩個(gè)官方范例的org.restlet.application類,即使得activiti-explorer中的Restlet Application類、繼承org.activiti.rest.service.application.ActivitiRestServicesApplication,覆蓋其createInboundRoot()方法、補(bǔ)充入activiti-rest所需的RestServicesInit.attachResources(router);

  • Step2. 再加入kft-activiti-demo的配置
    如從“資料”中文章可以看到,咖啡兔的范例,貢獻(xiàn)了很多國內(nèi)項(xiàng)目級(jí)別的成熟經(jīng)驗(yàn),比如:

    • 中文化/國際化UTF-8
    • 對(duì)官方標(biāo)準(zhǔn)REST API的調(diào)用、和不足之處的補(bǔ)充范例
    • Spring MVC方式的REST API擴(kuò)展(盡管其結(jié)果也多以Spring MVC的ModelView方式返回、但入口與內(nèi)部處理都足以借鑒)

    這一步,我們所需做的就是把咖啡兔Demo中可借鑒的配置部分嵌入自己的項(xiàng)目中:

    • pom.xml
      最有價(jià)值的一段,就是借用咖啡兔的Maven私服(參見“導(dǎo)入已有項(xiàng)目”),否則在國內(nèi)網(wǎng)絡(luò)現(xiàn)狀下甚至可能寸步難行。
      其次便是對(duì)于Spring框架、持續(xù)層的支持等等,值得注意的是,activiti-explorer作為activiti官方范例、是繼承了其“上層”activiti-root的Maven配置的,而我們自己的項(xiàng)目、應(yīng)該并不會(huì)希望建立這種繼承,這種情況下,參照kft-activiti-demo的POM便是個(gè)safe的選擇。
    • web.xml
      咖啡兔Demo同樣使用Spring Bean方式加載,因此不需修改;同時(shí)因?yàn)榭Х韧肈emo啟用了Spring MVC架構(gòu)、而這是我們將來擴(kuò)展REST API時(shí)同樣可以用的,因此需要搬運(yùn)如下這一段:
      <servlet>
        <servlet-name>springServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>/WEB-INF/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>springServlet</servlet-name>
        <url-pattern>/</url-pattern>
      </servlet-mapping>
      
      其中spring-mvc.xml將負(fù)責(zé)具體的Spring MVC配置(后述)。
    • activiti-context.xml
      Spring框架缺省調(diào)用的上下文配置文件是applicationContext.xml,咖啡兔Demo是直接把Activiti引擎所需要的配置寫在其中的,activiti-explorer范例則是使用<import>把后臺(tái)、前端的配置分在兩個(gè)不同的文件中(activiti-standalone-context.xmlactiviti-ui-context.xml)。作為我們自己的項(xiàng)目,只需合并在一個(gè)activiti-context.xml中即可。
    • spring-mvc.xml
      不長(zhǎng),直接留用,只需把<context:component-scan>中的base-package改成自己的項(xiàng)目包就可以;定制擴(kuò)展的REST API將以一個(gè)個(gè)Spring MVC Controller的形式實(shí)現(xiàn),這里的配置將實(shí)現(xiàn)對(duì)項(xiàng)目包下帶有@Controller標(biāo)簽的類的自動(dòng)掃描、實(shí)例化。
  • Step3. REST API學(xué)習(xí)
    RESTful服務(wù)的好處,如前所述、主要是能夠使得業(yè)務(wù)系統(tǒng)所依賴的技術(shù)與工作流引擎所依賴的技術(shù)(Java)之間解耦。當(dāng)Activiti引擎作為一個(gè)RESTful接口的Web服務(wù)運(yùn)行時(shí)(輸入輸出可以都是JSON數(shù)據(jù)),瀏覽器、桌面、移動(dòng)客戶端、基于Java、.Net、Python都可與之交互。

    • URL風(fēng)格
      僅以獲取“流程設(shè)計(jì)模型一覽”為例,這是一個(gè)標(biāo)準(zhǔn)REST API中就有的功能(官方文檔),如果加上Spring Servlet的映射({servlet_mapping},在web.xml中已配置)等就形成完整的HTTP請(qǐng)求路徑:
      GET http://{host:port}/{app_name}/{servlet_mapping}/repository/models
      
      配置時(shí)則只需{servlet_mapping}之后的部分,例如
      GET repository/models              //表示獲取“一覽”
      GET repository/models/{modelId}    //表示獲取“某個(gè)個(gè)體”
      POST repository/models             //表示“新建”一個(gè)個(gè)體
      PUT repository/models/{modelId}    //表示“修改”某個(gè)個(gè)體
      
    • 官方的Restlet方式REST實(shí)現(xiàn)
      官方的REST API實(shí)現(xiàn)(后臺(tái))就在activiti-rest范例中。URL模式與實(shí)現(xiàn)方法之間的映射,位于src\main\java\org\activiti\rest\service\application\RestServicesInit.java中:
      router.attach("/repository/models", ModelCollectionResource.class);
      router.attach("/repository/models/{modelId}", ModelResource.class);
      ......
      
      進(jìn)一步打開ModelCollectionResource類,可見Restlet方式的注釋(Annotation)聲明:
      @Get("json")
      public DataResponse getModels() {......}
      
      @Post
      public ModelResponse createModel(ModelRequest request) {......}
      
      Restlet本身我們將不再沿用,但從映射所關(guān)聯(lián)到的各個(gè)方法體中我們可以查找到各個(gè)功能的幕后實(shí)現(xiàn)。
  • Step4. REST API擴(kuò)展
    對(duì)于標(biāo)準(zhǔn)REST API無法滿足的需求,就需要自行擴(kuò)展來實(shí)現(xiàn)了。擴(kuò)展會(huì)涉及到將來Activiti引擎升級(jí)時(shí)的兼容問題,不過實(shí)際開發(fā)中發(fā)現(xiàn)、REST接口層的代碼量相對(duì)來說是很薄的、即便其所用到的表層API發(fā)生了變化修改量也不會(huì)很大。

    • URL風(fēng)格
      擴(kuò)展API的{servlet-mapping}部分是與標(biāo)準(zhǔn)API有所區(qū)別的(比如一個(gè)是service另一個(gè)就是service-ext);其他URL風(fēng)格方面則盡量與官方REST API保持一致。
    • Spring MVC方式REST實(shí)現(xiàn)
      在Spring MVC方式中,REST URL模式與實(shí)現(xiàn)方法之間的映射,直接位于各個(gè)實(shí)現(xiàn)方法所在的類中,包括位于類的聲明、方法的聲明之上的注釋標(biāo)簽(@RequestMapping(...))。
      以“流程設(shè)計(jì)模型部署”為例,
      @Controller
      @RequestMapping(value = "/service-ext/repository/models")
      public class ModelController {
          ......
          @RequestMapping(value = "deploy/{modelId}")
          public String deploy(@PathVariable("modelId") String modelId, 
                  @RequestParam(value="starterUser", required=false) String starterUserId, 
                  @RequestParam(value="starterGroup", required=false) String starterGroupId, 
                  RedirectAttributes redirectAttributes)
          {......}
      }
      
      可以看到,deploy()方法所對(duì)應(yīng)的URL模式是/service-ext/repository/models/deploy/{modelId},其中{modelId}部分將被直接解析成方法參數(shù)modelId,另外有兩個(gè)方法參數(shù)starterUserIdstarterUserGroup則對(duì)應(yīng)的是URL中的QueryString部分(且是可選的),一個(gè)有效的HTTP請(qǐng)求可能是:
      GET http://localhost:8080/app/service-ext/repository/models/deploy/1111?starterUser=kermit
      
      更完整的Spring MVC語法文檔可參考這個(gè)鏈接,包括@ResponseBody的使用(方便的將對(duì)象返回值轉(zhuǎn)成JSON等格式)、produces="application/json"consumes="application/json"等等。
    • 官方代碼借鑒
      (略述)可根據(jù)與需求近似的REST API、找到與相關(guān)URL模式相映射的實(shí)現(xiàn)方法。activiti-engine核心包下的org.activiti.engine.impl.RepositoryServiceImpl等類是關(guān)鍵類,org.activiti.engine.impl.interceptor.CommandExecutor則是引擎采用的“Command設(shè)計(jì)模式”的執(zhí)行者,各種工作流操作都會(huì)被歸結(jié)為相應(yīng)的某種Command<Object>、在設(shè)置好特定的操作上下文CommandContext后、就由CommandExecutor負(fù)責(zé)執(zhí)行。一般我們并不需要修改到Command設(shè)計(jì)模式的領(lǐng)域,而只需參考activiti-rest包中是如何調(diào)用其他包的代碼就夠。
  • Step5. 測(cè)試界面(客戶端)
    如上,我們?cè)诤笈_(tái)每定制擴(kuò)展一個(gè)REST API(e.g.“流程設(shè)計(jì)模型部署”),相應(yīng)都需要在前端(Web)測(cè)試頁面(如圖5)中增加一組測(cè)試。
    注意:測(cè)試頁面其實(shí)相當(dāng)于是“業(yè)務(wù)系統(tǒng)”的角色,是可以與后臺(tái)(工作流引擎)分隔為兩個(gè)WebApp項(xiàng)目的。
    測(cè)試界面并未使用jquery-ui、blueprint等前端技術(shù),只是簡(jiǎn)單的JSP(其實(shí)純HTML/JS都完全可以,因?yàn)橹恍鐰jax通信+JSON包裝/解析)。給出一段調(diào)用“流程設(shè)計(jì)模型部署”API的HTML鏈接:

    <a id="deploy_link" 
         href="${ctx}/service-ext/repository/models/deploy/<%=deploy_model_id%>?starterUser=<%=starter_user_id%>">
         流程設(shè)計(jì)模型部署
    </a>
    

再給出一段部署后、已進(jìn)入工作流運(yùn)行時(shí)階段(runtime)時(shí)、模擬用戶提交某份表單完成某個(gè)用戶任務(wù)的JS代碼:

<script type="text/javascript">
function onhandle(taskId){
    var request = {
        action: "complete",
        assignee: "${userId}",
        variables: [
            {name: "complete_key_001", value: "complete_value_001"},
            {name: "complete_key_002", value: "complete_value_002"},
            {name: "complete_key_003", value: "complete_value_003"}
        ]
    }
    var request_json = JSON.stringify(request);
    ajaxRequest("POST", "${ctx}/service-ext/runtime/tasks/complete/" + taskId, request_json, null);
}
function ajaxRequest(method, url, request, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    if(url.indexOf("/service/")>0){ 
        //standard activiti REST api requires 1.Auth(base64) 2.POST 3.JSON header 
        xhr.setRequestHeader("Authorization", "Basic " + base64.encode("kermit"+":"+""));
        if("POST" === method.toUpperCase() ){
            xhr.setRequestHeader("Content-Type","application/json");
        }
    }
    else if(url.indexOf("/service-ext/runtime/process-instances")>0)
        xhr.setRequestHeader("Content-Type","application/json");
    else if(url.indexOf("/service-ext/runtime/tasks")>0)
        xhr.setRequestHeader("Accept", "application/json");
    xhr.onload = (callback!=null) ? callback : function() {
        debugger;
    };
    xhr.send(request);
}
</script>
  • Step6. 貫通生命周期
    根據(jù)你所服務(wù)的“業(yè)務(wù)系統(tǒng)”的不同,會(huì)對(duì)REST服務(wù)的接口提出不同的需求,甚至?xí)绊懙焦ぷ髁魃芷诘?strong>責(zé)任人劃分:例如,流程設(shè)計(jì)模型究竟是能從官方Modeler直接兼容地保存到數(shù)據(jù)庫、只需一個(gè)id即可部署,還是需要采用其他的Modeler編輯、然后以BAR/ZIP包方式直接部署、因此參數(shù)也就變成了文件流;或者說流程實(shí)例被啟動(dòng)后,某個(gè)用戶名下的“我的(用戶)任務(wù)”、究竟是點(diǎn)擊后就“簽收+辦理”并彈出表單、還是分成兩步進(jìn)行。
    當(dāng)我們逐一完成如圖5的若干項(xiàng) 擴(kuò)展或標(biāo)準(zhǔn)REST API調(diào)用,圖2中展示的工作流生命周期也就能被打通了。工作流運(yùn)行階段、根據(jù)用戶輸入的不同、會(huì)進(jìn)行不同的流轉(zhuǎn),可以通過官方范例中已有的流程狀態(tài)跟蹤器“Diagram Viewer”查看進(jìn)度如下:
    圖6:運(yùn)行時(shí)階段的工作流狀態(tài)查看

    上圖紅線部分顯示了,“填寫申請(qǐng)”確認(rèn)提交后經(jīng)過了“直屬上級(jí)審批”、但在“人事審批”環(huán)節(jié)被否決,又重新進(jìn)入了“填寫申請(qǐng)”環(huán)節(jié)。

小結(jié)

本文記錄了近2個(gè)月期間對(duì)開源工作流引擎Activiti做調(diào)研、貫通整個(gè)工作流生命周期(如圖2)的過程,重點(diǎn)在于標(biāo)識(shí)出同樣沒有相關(guān)基礎(chǔ)的技術(shù)人員所可能碰到的陷阱、疑惑,希望能節(jié)省你的時(shí)間(少加幾晚上班陪陪家人~);如有不正確/并非最優(yōu)解的地方、也請(qǐng)有經(jīng)驗(yàn)者指正。
因?yàn)樯婕肮椭骼妫酥R(shí)點(diǎn)之外、完整源碼就不能共享了。同時(shí),因?yàn)闃I(yè)務(wù)系統(tǒng)中已包含自主開發(fā)的“表單編輯器”與“郵件模板編輯器”,這兩部分內(nèi)容(包括與引擎之間的具體銜接)不涵蓋在本文范圍之內(nèi)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,923評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,954評(píng)論 6 342
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,285評(píng)論 25 708
  • 不是要實(shí)現(xiàn)只是留做想念成為期盼
    朩旁閱讀 272評(píng)論 0 0
  • 順豐從一個(gè)寄居廣州一寓的小快遞,憑借良好的機(jī)遇期,優(yōu)質(zhì)的服務(wù),把握機(jī)遇的能力,一躍發(fā)展成為市值2300億的上市公司...
    LiveFuture閱讀 255評(píng)論 0 0