從零開始學習和改造activiti流程引擎的13天,自己記錄一下
day#1(11.13)
嘗試通過spring boot 整合最新版activiti 7,但是苦於官方的文件基本為空,無法完成spring boot的配置,最終按照activiti 6的文件,手工初始化ProcessEngine以及完成deploy測試。
在eclipse中安裝流程模型設計器,並畫簡單的流程。
day#2(11.14)
想要開啟activiti對資料庫操作的SQL日誌列印,研究了好一番功夫,終於得以實現。實現方式如下:
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="60 seconds" debug="false"> <!-- 控制檯輸出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss} %-5p [%c] - %m%n</pattern> </encoder> </appender> <logger name="org.springframework" level="ERROR" /> <logger name="org.mybatis" level="ERROR" /> <logger name="com.baomidou.mybatisplus" level="ERROR" /> <logger name="org.apache" level="ERROR" /> <!-- Activiti日誌 --> <logger name="org.activiti" level="ERROR" /> <logger name="org.activiti.engine.impl.persistence.entity" level="DEBUG" /> <!--myibatis log configure --> <logger name="com.ibatis" level="DEBUG" /> <logger name="com.ibatis.common.jdbc.SimpleDataSource" level="DEBUG" /> <logger name="com.ibatis.common.jdbc.ScriptRunner" level="DEBUG" /> <logger name="com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate" level="DEBUG" /> <logger name="java.sql.Connection" level="DEBUG" /> <logger name="java.sql.Statement" level="DEBUG" /> <logger name="java.sql.PreparedStatement" level="DEBUG" /> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration> logback.xml
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.8</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.22</version> </dependency> maven dependency
簡單的講,就是resources下放logback.xml,maven新增對logback的依賴即可,然後activiti執行的sql就會被自動列印,無需其他額外設定。
下載activiti 6 全部壓縮包(110MB左右),將官方的線上模型設計器執行起來,發現前端使用angularJS做的。對官方demo稍作研究後決定,完全替換官方的rest API,方法就是獲取官方的後端API原始碼,然後一個介面一個介面增加到自建spring mvc專案中。
新建spring mvc空專案,新建簡單controller,執行成功。
maven引入activiti engine,複製第一個API程式碼以及editor前端程式碼到自己的專案中,發現需要引入另一個jar包(activiti-app-logic)。maven引入之後,編譯能通過,但是無法啟動,原因是spring 啟動時檢查引入的jar包衝突。在maven中增加exclusion修復完jar包衝突之後,總算執行起來。
執行起來之後,為了避開身份認證,直接修改JS原始碼,終於得以直接執行activiti-app/editor/#/editor目錄。
day#3(11.15)
開始修復第一個API(GET rest/models),發現需要引用或完全替換activiti-app-logic包中的資料庫訪問層。考慮了一會,考慮到modeler只涉及3張表,決定用我們之前專案的hibernate框架完全重寫modeler的資料庫訪問層程式碼。
非常順利,很快實現獲取models,建立model,生成model縮圖,獲取model編輯器json資料,儲存模型(完成編譯)等介面。唯一麻煩的事就是需要從activiti-app-logic包中搬程式碼,解決各種依賴問題。
day#3(11.16)
先是修復了儲存模型的介面,並且進行了大量測試,各種複雜模型資料都可以順利儲存。
接下來體驗了modeler的大部分功能,發現非常的複雜,得需要完全理解activiti的基礎上才能開發出適用的流程引擎啊。接下來的幾天可能是學習activiti引擎了。
day#4(11.19/週一)
週末的時候花了些時間閱讀文件,完成了大部分內容的閱讀,特別是對BPMN2.0規範有了較深刻的認識。
上午繼續花了約2小時完成文件的粗略閱讀,接著制定了後面的計劃:
- (1)完成流程模型的部署(之前已完成模型建立和儲存);
- (2)發起流程;
- (3)檢視任務列表;
- (4)完成任務;
- (5)完成流程;
- (6)下一階段:複雜自定義流程。
接下來首先是研究官方demo,找到並體驗釋出流程以及任務列表的功能,然後就是從官方的原始碼中拷貝需要的程式碼到自建工程裡面。遇到了一點問題,就是官方demo中釋出流程的業務過於複雜不適合我們專案,但是一開始程式碼是按照官方demo寫的,所以執行起來之後總是報json解析錯誤,後來慢慢慢慢跟程式碼,發現可以完全移除官方釋出流程中的很多步驟,這樣才會更加適合我們的專案。然後就順利完成了模型的部署。
接下來為了後面的測試更加方便,自己新增了幾個rest介面將引擎下面的服務方法通過瀏覽器暴露,這樣便於後期的除錯。
為了從全新的簡單流程開始測試,就需要把之前建立的錯誤流程刪除,並且正式開發時也需要刪除流程的功能,所以接著就重新開發刪除流程的介面。
day#5(11.20)
清除之前的資料之後,準備建立一個較為真實的請假流程,流程圖如下:
流程圖畫好之後,發現流程圖及相關屬性亂碼。然後百度,一開始以為很好解決就直接從網上copy了一些程式碼,最後發現都沒有解決問題,然後才開始修改editor_json介面,然後定位到造成問題的原因是因為JsonNode物件在restcontroller返回時序列化造成的亂碼問題。嘗試將JsonNode替換為普通object,中文亂碼消失。但是普通java object有一個動態的model物件,如果使用fasterxml的JsonNode發現無法正常序列化,改換為fastjson的JSONObject物件,居然可以正常序列化!至此,中文亂碼問題已解決。
接下來便是發起流程。發起流程之後,獲取流程例項,獲取taskService,獲取當前task,嘗試設定task的owner和表單資料(通過流程變數),然後完成任務。
程式碼雖然簡單,但是發現任務的owner根本設定不上去,後來嘗試了各種方法,才發現問題所在:taskService.complete方法呼叫之後,當前Task物件已從資料庫刪除了,如果再用此task物件的id去資料庫查詢將查不到該task,這時(complete方法呼叫之後),其實當前流程例項下面包含的任務已經是新建立的task了(有不同的id),之前那個complete的task已經到history表裡面去了(owner也設定成功了的)。
由於我們將要開發方便普通工作人員使用的自定義流程設計器,所以,activiti官方的流程設計器的屬性編輯器基本不能用了,因為那個編輯器幾乎沒有人會用的。要替換這個編輯器,最重要的部分就是選擇assignee了,設計的原型圖如下:
設計思路:
(1)在填寫表單環節,增加自定義環節屬性:是否需要使用者指定下一環節的審批人。如果下一環節只有一個,並且業務需求確實是需要指定審批人,那麼在使用者提交表單之前,需要選擇下一環節審批人。資料傳到伺服器端之後,伺服器首先儲存任務的表單資料,然後完成任務,然後獲取下一個環節任務,然後設定下一環節的審批人(來自引數);
(2)如果下一環節審批人不需要流程發起人指定,那麼提交表單後,伺服器端處理邏輯:開始流程 -> 獲取第一個任務 -> 設定owner和表單資料 -> 完成任務。在下一環節的create event listener程式碼中早已設定好assignee的計算邏輯。只要進入listener,就會根據環節定義的審批人進行計算得到當前任務的指派者。
(3)下一環節的assignee已設定好,只要該使用者開啟任務並進行審批就可以讓流程運轉起來了。
day#6(11.21)
開始改造模型設計器:
(1)簡化工具箱,只保留空開始事件、使用者任務、並行閘道器、排他閘道器、空結束任務、文字備註;
(2)簡化流程屬性編輯器;
(3)簡化其他控制元件屬性編輯器;
(4)使用者任務控制元件增加自定義角色配置;
通過跟蹤分析js程式碼,發現工具箱資料來源來自stencilset_bpmn.json,然後備份好後刪除不需要的控制元件,執行,成功。
繼續通過跟蹤分析js程式碼,發現屬性編輯器的資料來源同樣來自stencilset_bpmn.json,然後修改英文為中文,執行,亂碼,同樣是fasterxml的JsonNode物件造成,替換為fastjson的JSONObject,順利解決中文亂碼問題。
首先修改User Task控制元件,將額外屬性移除,執行,發現設計圖上面多了兩個圖示,然後一個屬性一個屬性移除,終於發現"multiinstance_typepackage","isforcompensationpackage"這兩個屬性不能移除,說白了這就是activiti的這兩個屬性的預設值的bug。但是這兩個屬性不移除的話,編輯器上面就會顯示,這顯然是不符合需求的。於是開始找程式碼看哪裡用到這兩個屬性了。嘗試修改多個地方的程式碼之後,雖然可以實現但終覺不妥,後來突然發現屬性object有個popular屬性,將其修改為false,編輯器上就直接隱藏了,甚是方便。
後來有個員工要離職,工作交接花了3小時左右。
接著完成稍微複雜一點的流程設計,如圖:
先從簡單的控制元件開始吧。首先需要改造的就是排他網關出去的兩個條件順序流。思路:
(1)為每個順序流增加flowtype欄位,如果該欄位有值,那麼在儲存模型的時候,就將該欄位的值獲取出來並按照格式填充到conditionsequenceflow屬性中,並刪除模型的flowtype屬性;
(2)設定條件的表示式大致為:$(APPROVAL_RESULT=='"+flowtype+"'),這樣在上一環節完成審批時,會新增一個名稱為APPROVAL_RESULT的流程變數,從而實現條件的跳轉;
(3)後期再將flowtype的編輯器改為下拉列表,這樣使用者只需要選擇:“審批同意”、“審批拒絕”、“無條件”、“其他表單條件。。。”就可以完成條件的設定,無需輸入表示式了。
修改程式碼之後執行,直接成功。下班了,第二天可以測試環節是否可以自動跳轉了。
day#7(11.22)
由於找了很久的文件沒有找到UEL表示式如何判斷字串相等,所以索性改成判斷bool相等,因為有demo嘛。然後更新流程模型,更新程式碼。最終修復editor_json的程式碼如下:
1 public static void fix(JSONObject json) { 2JSONArray shapes = json.getJSONArray("childShapes"); 3JList<JSONObject> flows = JList.from(shapes).select(x -> (JSONObject) x).where(x -> { 4JSONObject stencil = x.getJSONObject("stencil"); 5if (!stencil.getString("id").equals("SequenceFlow")) { 6return false; 7} 8JSONObject properties = x.getJSONObject("properties"); 9String flowtype = properties.getString("flowtype"); 10if (StringHelper.isNullOrWhitespace(flowtype)) { 11return false; 12} 13if (flowtype.equals("agreed") || flowtype.equals("rejected")) { 14JSONObject condition = new JSONObject(); 15JSONObject expression = new JSONObject(); 16expression.put("staticValue", "${APPROVED==" + flowtype.equals("agreed") + "}"); 17expression.put("type", "static"); 18condition.put("expression", expression); 19properties.put("conditionsequenceflow", condition); 20} 21//properties.remove("flowtype"); 22return true; 23}); 24 } View Code
開始測試:
(1)填寫請假表單,complete流程開始後的預設任務(將流程推進到經理稽核環節),返回流程例項ID;
(2)查詢該流程例項下面的全部活躍任務;
(3)找到經理稽核的任務(只有這一個任務);
(4)根據經理稽核任務ID對請假進行稽核(approved=true/false),complete任務時傳入APPROVED流程變數(值來自於controller方法引數);
(5)再次根據流程例項ID查詢活躍任務,發現流程已經根據approved引數自動選擇分支進行流轉了。
(6)至此,順序流條件控制測試成功。
接下來開始擴充套件UserTask的自定義屬性了。
首先在stencilset_bpmn.json中增加一個complexassigneepackage,然後將此package新增到UserTask的屬性列表中,然後在properties.js中增加響應配置,以及建立相應的html模板程式碼和angularJS的controller。
下圖則是這個自定義屬性(複雜指派者屬性)的編輯器原型:
對應到伺服器端的資料結構為:
1 public class DataDto { 2private String displayText; 3private JList<InitiatorType> initiators; 4private JList<Long> roleIds; 5private JList<Long> departmentIds; 6private JList<Long> userIds; 7 }
前期為了快速測試流程的運轉流程,並不需要去開發複雜的前端互動介面,最簡單的方式,莫過於直接修改編輯器JS原始碼,然後直接從瀏覽器控制檯裡面將JS物件存入對應的屬性值裡面即可。這個構想進行的非常順利,可以順利的顯示和儲存這個DataDto的資料。
但是,這個自定義屬性在匯出流程模型為XML的時候,生成的XML卻不包含這個自定義屬性,於是開始各種百度谷歌官方文件,網上有一些例子,但大多都是複製來複制去,並且相比原文有些錯漏,而官方文件,不管是5還是6版的都沒有提及自定義屬性的事,最後在大概兩三小時的挫敗中終於找到了“原文連結”,雖然不是完全適用,但原文畢竟沒有錯漏,提供了關鍵示例程式碼,終於在XML中顯示出了自定義屬性。
然後測試釋出流程模型,沒有問題。然後修改UserTask的create event listener,從該listener中獲取環節定義中的自定義屬性,然後根據自定義屬性的值修改任務的候選人/組/指派者。
改好程式碼之後就開始進入測試,發現在UserTask的extensionelements節點中的資料不完整,被截斷了,然後修改XML增加CDATA包裹,再測試,無效,然後修改XML中屬性為URL編碼,結果可以正確解析了。
至此,自定義屬性已擴充套件成功。等著第二天來測試我們這個複雜的指派系統了吧。
day#8(11.23)
首先完成UserTask的create event listener的指派程式碼的編寫,非常順利。
然後從填寫表單開始測試,加上斷點,順利完成指派者、候選人、候選組的測試,非常成功。
下面是對activiti指派者/候選人測試的一些總結,官方文件中不曾提及的:
(1)當呼叫taskService.addCandidateUser和addCandidateGroup之後,可以通過TaskQuery查詢出任務;
(2)在上一步之後,如果呼叫了taskService.claim認領任務之後,再呼叫第一步的根據候選人/組進行查詢就查不出結果了,但是從該任務的identityLinks中仍然可以看到該任務的候選人/組並沒有被清空,還是以前的值,並且claim之後,任務的assignee就是認領者了;
(3)任務認領之後如果再次釣claim任務就會報錯;
(4)任務認領者可以是任何人,也就是說不一定非要是候選人或候選組裡面的人。
接下來便是開始開發表單了,另一個重要節點。
首先再次仔細閱讀了官方的關於表單的知識,沒有太大用處。然後又在網上找了一些自定義表單的文章,用處也不大。因為我的目標很明確,就是要搞清楚表單是如何儲存在流程模型XML中的,以及如何在任務詳情頁面呈現。
既然網上的資源質量有限,又只得把官方的demo執行起來,建立流程,建立表單,發起流程,一步一步測試,都可以跑通。為了知道表單定義在XML中的體現,直接下載已定義好的流程XML,發現表單資料在XML中只有form-properties列表和form-key兩個屬性,那表單的定義資料就一定在其他地方儲存了。
day#9(11.26)
花了近一半的時間開會以及投入到其他專案。
由於之前已經研究清楚,流程模型XML並不會包含表單資訊,因此表單資訊只能用另一張資料庫表來儲存。
為了讓流程設計器更加友好方便使用,我設計瞭如下的表單定製化介面。
如此,使用者只需要輸入表單設計器編輯好的表單key,程式便會自動載入表單資訊,然後使用者只需要勾選當前環節哪些欄位需要顯示,哪些欄位可以編輯。這可能是目前市面上最好的流程自定義表單設計器了。
思路清楚,接下來便是跟改造自定義指派的自定義屬性一樣,增加自定義表單屬性的編輯器。同樣,在前期,只通過程式碼生成XML,先不做介面。
表單設計器的環節自定義屬性開發很快完成。接著便是完成表單相關表/資料結構設計,儲存資料庫,開發很快完成。
day#10(11.27)
由於activiti官方將表單資料儲存到跟流程模型同一張表裡面,我覺得這並不是太好,還是新建一張表用來存放表單資料更好。
表單表包含簡單的key/name/css/fields/buttons欄位,其中fields和buttons都是json格式的資料,用json而不是子表是因為考慮到後期擴充套件表單域的靈活性。
然後就是開始開發自定義表單模組了,當然,為了更快讓整個流程跑起來,先就做個簡單版本的表單設計器吧,如下圖:
這個表單設計器雖然簡單,但是工具箱以及表單實時展示部分已經完成了非常容易擴充套件的架構方式,這樣對於優化上圖中的控制元件,以及增加複雜控制元件都會變得非常簡單。該模組採用vuejs渲染。
到下班前,已基本完成了工具箱及表單的呈現部分。
day#11(11.28)
花了點時間優化表單設計器的體驗及增加功能,然後就是儲存資料庫,以及從資料庫載入呈現表單,上午就結束了。
總的來講,表單設計器從介面設計到儲存資料庫到WEB前端,花了一個下午和一個上午,已經可以正常運行了。
接下來便是將表單設計器整合到流程設計器裡面。
本來想繼續用vuejs來渲染表單部分,這樣的話可以重用表單設計器的渲染程式碼,但是嘗試了一下發現根本不行,因為angularjs和vuejs都是實時渲染,angularjs的html只要被vuejs接管之後,我們看到的html已經是vuejs重新渲染生成的了,反之亦然。
所以,還是老老實實用angularjs做吧。
先花了一個小時左右把angularjs的文件大致看了下,發現遠沒有vuejs的文件好讀。比如,我想找一個如何渲染列表以及如何渲染動態屬性,硬是找了很久沒有找到。最後不得不在官方文件中通過找對應的例子一個一個完成渲染。
至此,流程設計器裡面的表單屬性編輯器已能基本上呈現出來了,在右側勾選屬性時就顯示,未勾選時則隱藏,從而做到實時的表單預覽。如下圖:
接下來便是將這個表單的資料存到資料庫,以及從資料庫載入資料並還原表單了。
day#12(11.29)
非常順利的完成了表單屬性的儲存以及從資料庫還原表單,然後將整個流程的所有使用者環節的資料進行了更新,發現現在的整個表單設計確實非常易用,簡單直觀!
接下來為了更好地進行後邊的測試,先把之前落下的順序流條件編輯器給優化一下。
按照官方提供的bool值模板,非常順利的完成了自定義的流程條件編輯器,最終介面如下:
接著,為了讓流程真正的跑起來,還需要對指派屬性進行改造。
首先是定義彈出開啟後加載資料的資料結構,然後生成一些假的資料方便測試。然後就是利用angularjs對前端進行繫結。最終實現效果如下圖:
實現的功能包括:
(1)部門跟使用者採用樹形資料結構生成,支援實時名稱過濾,且只要有下級節點,上級節點就一定會顯示;
(2)4個tab內容區點選複選框就會實時更新到右邊已選列表;
(3)已選列表點選X就會取消選中對應複選框;
(4)根據4個tab已選內容生成合理的顯示文字(紅色框框區);
(5)點選儲存將已選項儲存到資料庫。
所有這些編碼工作在一個下午完成。接下來第二天便是從資料庫恢復表單了。
day#13(11.30)
首先完成了從資料庫還原表單已選資料,接著刪除了UserTask屬性列表中的TaskListeners屬性編輯器,改為儲存流程模型時對所有UserTask預設新增我們的自定義任務指派管理器(在任務建立時執行)。這2步都完成的非常順利。
接下來便可以真正測試流程運轉了,在這個過程中還需要還原表單設計器完成的表單,以及控制顯示隱藏和可編輯性。
在任務詳情頁還原表單,以及新增簡單的表單認證,以及控制顯示隱藏表單欄位和按鈕,這些開發工作都進展的非常順利。
接下來便可以測試流程的運轉了:
(1)發起流程,同時將任務指派給當前使用者;
(2)使用者進入任務詳情,能夠顯示錶單,填寫表單,能夠正常提交和儲存草稿;
(3)提交表單後,新建立的任務順利指派給了下一級節點。
總的來講,整個流程的運轉跟預期的差不多,只是在審批人選擇拒絕時讓流程回到第一個節點時,流程發起者無法再次看到該任務,因為任務的指派者變成空了。所以之前的方案得改了。改造後,流程開始的第一個環節即必須設定指派者或候選人,這樣也更加靈活了,然後startProcessInstanceByKey之後就不需要設定指派者了,因為流程自動進入我們自定義的複雜指派者監聽器模組,該監聽器會自動根據流程定義對指派者進行設定,這樣,不管時流程剛剛開啟還是以後任何時候回退到該環節,該環節都一定會有指派者了。
OK,至此整個自定義流程已經全部完成了,接下來改下介面最後再完整的測試一遍吧。
最終的測試過程
第0步:設計流程圖和表單,以及準備工作
下面對部分環節進行說明:
(1)填寫請假單環節的指派者設定為流程發起人;
(2)部門經理審批設定為發起者所在部門負責人(個人);
(3)CEO審批設定為指定的個人(陳麗);
(4)HR備份指派了3個人作為候選人;
(5)財務備份選擇的是財務處管理員,一種系統角色。
表單配置還是跟之前的一樣:
下面是整個測試過程用到的一個簡單介面:
第1步:發起流程
測試例子都使用user05進行流程的發起。發起之後,可以看到指派給user05的任務列表:
第2步 填寫申請單
點選上一步的檢視任務詳情,進入任務詳情頁:
記住流程例項ID為:145001
填寫表單後,點選儲存草稿。然後再重新整理頁面,發現任務資料已儲存,證明儲存草稿按鈕工作正常。然後再點選提交。
第3步:部門經理審批
回到測試介面,輸入流程例項ID,載入該流程例項的活動任務,如下圖:
可以看到流程已經運轉到部門經理審批環節了,指派者也已經正確的設定為了user05-department_manager。點選檢視任務詳情,如下圖:
這裡我們點選拒絕。
第4步:重新提交申請表單
再次回到測試介面,載入流程例項的活動任務,如下圖:
可以看到流程已經正確退回到第一個填寫請假單的環節了,指派者也正確的設定為了流程發起人user05。點選檢視任務詳情,再次進入填寫表單介面,如下圖:
再次點選提交。
第5步:部門經理再次審批
再次回到測試介面,載入該流程例項的活動任務,如下圖:
36063-758603220.jpg"/>
再次看到流程已經運轉到部門經理審批環節。點選檢視任務詳情,如下如:
這次點同意。
第6步:CEO審批
回到測試介面,再次載入流程例項的活動任務,如下圖:
可以看到流程已經運轉到CEO審批環節。點選檢視任務詳情,如下圖:
然後點選同意按鈕。
第7步:HR備份
回到測試介面,再次載入流程例項的活動任務,如下圖:
可以看到指派為空,候選組為空,候選使用者有3個。這裡我就不去測試領取任務的功能了,直接點選檢視任務詳情,然後點提交(就不截圖了)。
第8步:財務備份
回到測試介面,再次載入流程例項的活動任務,如下圖:
可以看到指派者為空,候選使用者為空,候選組為一個角色的ID。點選檢視任務詳情,如下圖:
在任務詳情頁面可以看到指派者仍然為空,沒關係,直接點選提交。
第9步:完成
這時再次回到測試介面,輸入流程例項ID,可以看到已經沒有任何活動任務了,該流程例項已經順利完成了。
總結
整個13天的學習和改造activiti的工作中,沒有什麼大的技術難題,主要還是一個學習別人框架原始碼的一個過程。最痛苦的莫過於官方的文件實在太不全了,對activiti的改造幾乎全靠讀官方demo的前後端原始碼。
THE END