楊光
(西安慶安航空電子有限公司信息開發室,陜西西安 710077)
現代企業的辦公自動化OA 系統具有復雜的流程審簽業務,以公文審簽為例,起草人員編寫公文,部門負責人審核,公司領導批準后,公文會下發到各個部門和員工。若審批人員不同意,則可以駁回流程,在起草人員修改后重新發起流程,進入審批狀態。
在傳統的工作流中,任務的指派人員角色單一,只能按照人員或角色指派任務。隨著人員或角色的復雜度的逐步提高,單一指派任務的方式已經不能完全滿足系統的需要。文中在任務單一指派的基礎上,提出了重構人員身份數據表的方法,可以將任務同時按人員或角色指派,提高了系統的靈活性。
工作流引擎ProcessEngine 類是Activiti 框架的核心類,通過它可以獲得Activiti 所需的所有Service類,以及生成流程運行時的各種實例、數據、監控以及管理流程的正常運行[1]。
官方API 提供了多種獲取ProcessEngine 的方法,這里使用名為activiti.cfg.xml 的配置文件來獲取,如圖1 所示[2]。

圖1 工作流引擎的配置文件activiti.cfg.xml
通過ProcessEngine 對象可以生成各個Service類,各類的作用如表1 所示。

表1 各Service類及作用
業務流程建模與標注(Business Process Model and Notation,BPMN)是描述流程的基本符號,包括圖元組合成一個業務流程圖(Business Process Diagram)的方式[3],如圖2 所示。

圖2 描述流程的基本符號
IDE 安裝Activiti 插件后,可以提供繪制流程的簡單方法,繪制的流程圖是后綴名為bpmn 的文件,這類文件本質上是一個xml格式的文件[4]。
在xml 格式文件中,包含代表各類元素的標簽。IDE 提供繪制流程圖的方式,可以自動生成xml 格式的文件。流程圖中各個基本符號的屬性都可以轉換為xml文件中的各類標簽和標簽的屬性[5]。
Activiti 的后臺有數據庫的支持,所有的表名都以act_開頭,表名的第二部分是表示表用途的兩個字母標識,用途與服務的API 對應[6]。
Activiti 的組織機構表包括act_id_group(用戶組信息表)、act_id_info(用戶擴展信息表)、act_id_membership(用戶與用戶組對應信息表)、act_id_user(用戶信息表)。
鑒于OA 系統部門、人員、角色關系的復雜性,這4 張表不能完全滿足OA 系統對用戶、人員、部門、角色關系的要求,因此需要調整表的結構或者重新設計表。部門表可以使用act_id_group 表,并補充相應字段。用戶表可以使用act_id_user,再增加人員表person_info,與用戶表建立關聯關系。act_id_membership 可以作為人員-部門-角色表,通過補充人員id、部門id、角色code、角色名稱等字段,可以體現人員所在部門及人員在該部門的角色。通過這種方式,可以很好地對Activiti 組織機構表進行重構,以滿足OA 系統復雜的用戶、人員、部門、角色、權限等要求[8]。
進行數據庫的二次開發以后,由于沒有破壞原有的數據表結構,因此在流程中可以使用調整后的數據庫結構進行處理,包括流程變量、個人任務、組任務。所以二次開發可以完全兼容原數據庫及方法。
在OA 系統進行文件分發時,首先由起草人編寫文件,編寫完成后,部門負責人進行審核。如果部門負責人及后續的公司領導均審批通過,則根據公文的分發范圍進行分發;如果審批不通過,則直接退回到起草人階段重新編寫。
使用Eclipse 的Activiti 插件繪制公文審簽流程圖,如圖3 所示。

圖3 公文審簽流程圖
為了滿足OA 系統越來越復雜的人員和角色設計,文中提出了可以分別按照人員和角色分配任務的方法。
在部門負責人審核任務中,由于起草人分屬不同的部門,所以部門負責人是變量,設置屬性中的Assignee 為變量${deptLeaderId},且部門負責人是單人。在起草人編寫任務完成時,根據該人員查詢所在部門的負責人id,將流程變量設置為部門負責人id,下一步的任務處理人即為部門負責人,如圖4所示。

圖4 部門負責人審核任務的屬性
在公司領導審批任務中,審批人為公司領導的角色,與流程發起人無關。因此,可以直接將公司領導審批任務的屬性Candidate groups 設置為company Leader 角色,如圖5 所示。

圖5 公司領導審批任務的屬性
首先使用Spring 框架注入流程引擎Process Engine 的實例,其次通過流程引擎創建倉庫Service,并通過倉庫Service 創建部署對象[9]。通過部署對象可以加載流程圖,并為流程圖指定名稱,如圖6所示。

圖6 部署流程定義
通過runtimeService 啟動流程實例,流程實例啟動后會生成流程實例id(processInstanceId),該數據會伴隨流程執行的整個過程,如圖7 所示。

圖7 啟動流程實例
完成任務有多種方法,其中之一是使用taskService的complete 方法。如圖8 所示,首先可以查詢出任務的id,其次使用taskService 的complete 方法,傳入參數任務id,即可將任務完成。完成任務后,觀察數據庫可以發現,歷史表中會出現該任務的信息,而正在執行的表中該任務會消失。

圖8 任務完成
對于待辦和已辦列表的查詢,文中提出了同時根據人員id 和角色id 查詢,合并查詢結果集的方法。
以待辦列表的查詢為例,若人員id為“RY00001”,需要查詢該人員的待辦列表,則按照以下步驟,如圖9 所示。

圖9 查詢任務
1)使用processEngine.getTaskService().createTask Query().taskAssignee("RY00001").list()語句獲得個人任務taskList1。
2)根據人員的id 可以查詢其所屬的角色,角色的id 為"JS00001",使用processEngine.getTaskService().createTaskQuery().taskCandidateGroup("JS00001").list()語句獲得個人任務taskList2。
3)使用List 的addAll 方法,將個人任務1 與個人任務2 合并,即可得到該人員的任務,taskList=taskList1.addAll(taskList2)。
使用上述方法查詢人員的任務列表時,由于需要將2 個以上的List 合并,且無法根據分頁進行查詢,所以任務數量會增加,從而導致數據重組的速率下降,影響系統性能[10]。經過實驗得出,當一個人員具有3 個角色,且個人任務為7 條,第一個角色的任務為4 條,第二個角色的任務為6 條,第三個角色的任務為3 條,將3 類任務進行合并,并進行數據重組時,查詢耗時為4 718 ms,已經超過了用戶的可忍受等待時間,影響了用戶的體驗,如圖10 所示。

圖10 任務列表查詢時間
使用面向切面的編程SpringAOP 以及非關系型數據庫Redis 可以有效地解決這個問題[11],具體如下所示:
1)使用SpringAOP 面向切面編程,每次用戶完成任務時,使用后置增強獲取人員id。利用切面來完成獲取人員id并將人員的任務存入Redis緩存。利用SpringAOP 的目的是使得將任務存入Redis 的操作與業務進行解耦。首先定義切面類TaskAspect,在切面類中定義切面的增強類型是后置增強@After,后置增強的方法是*com.*.taskExecute(..)。
在該方法中,查詢出該人員的待辦和已辦任務列表,并將其序列化為JSON 字符串[12]。
2)使用Java 調用Redis 的API,將該人員的待辦和已辦任務列表的JSON 字符串存入Redis 中。其中,key 可以是人員的id+任務類型(已辦和待辦),value 可以是任務列表的JSON 字符串。
3)每次查詢人員的待辦和已辦任務列表時,直接從Redis 中取值,并反序列化為任務列表[13-14]。
由于Redis 是非關系型數據庫,適合于作為緩存,讀取速度快,因此,從Redis 取值查詢人員的待辦和已辦列表時,可以有效降低查詢時間[15-20]。
實驗表明,使用Redis 作為緩存,在相同的查詢條件下,可以將任務的查詢時間從4 718 ms 降低至43 ms,如圖11 所示。

圖11 從Redis緩存中查詢任務列表時間
4)使用Redis 緩存任務列表的JSON 字符串的方法,可以有效降低查詢已辦和待辦任務列表的時間。然而,這樣會引入另一個問題,當用戶發起流程或者處理流程時,由于需要將用戶的已辦和待辦任務列表全部查出,并序列化為JSON 字符串,存入Redis,因此當用戶的任務列表數據量過大時,查詢和轉換的時間也會增加,這樣導致用戶點擊發起流程按鈕或點擊審批流程按鈕時,會產生長時間的等待,如圖12所示。

圖12 用戶操作時將任務列表緩存入Redis的時間
實驗表明,在相同的條件下,當用戶操作時,直接將任務列表緩存入Redis,會導致用戶的等待時間為3 209 ms,帶來了不良的操作體驗。
針對出現的此問題,文中提出了如下解決方式:
首先修改用戶操作后將任務列表緩存入Redis的方式,從同步修改為異步。當用戶操作時,仍然使用SpringAOP 面向切面編程,此時將執行操作的用戶id 及操作類型存入Redis 緩存。由于獲取用戶id及操作類型的耗時極短,因此用戶發起流程和完成任務的操作需要極短的時間,操作后會在Redis 中形成操作用戶的隊列。
操作用戶的隊列采用“FIFO+定時任務”的方式,每隔1 s 從Redis 中取出任務列表有變化的用戶id,將該用戶對應的任務列表查詢出來,并序列化后存入Redis 緩存。通過這種方式,將查詢任務列表的時間放在了后臺,提升了用戶的體驗。
以SpringBoot 的定時任務為例來說明定時任務的實現方法。
首先在SpringBoot 的啟動類上增加@Enable Scheduling 的注解,用來啟動定時任務;之后,在需要執行定時任務方法的類上增加注解@Component,納入容器進行管理;通過cron 表達式可以定義多種形式的定時任務,每隔1 s 執行一次,在需要執行定時任務的方法上增加注解@Scheduled(cron="*/1 * * ** ?")。這個定時任務的方法所完成的功能就是從Redis 中取出任務列表有變化的用戶id 隊列的隊頭,并且將該用戶的任務列表查詢出來,序列化后存入Redis,最后將該用戶從隊列中刪除。通過這種方式,可以將用戶操作時等待的時間轉化為后臺執行程序的時間,提高用戶的體驗。
實驗表明,在相同條件下,使用異步的方式,可以將用戶操作等待時間從3 209 ms 降低為36 ms,性能提高了兩個數量級,如圖13 所示。

圖13 用戶操作時將用戶id緩存入Redis的時間
文中研究了Activiti 工作流框架在OA 系統中的應用,提出了重構人員身份數據表的方法和同時根據人員和角色進行工作流的流轉方法,并優化了待辦和已辦任務列表的查詢。通過這些研究,可以使得復雜信息化系統中的工作流配置更加靈活,同時使得待辦和已辦任務列表的查詢效率大幅提高。該研究對于構建大型流程信息化系統具有一定的實際意義。