1. 程式人生 > >Activiti工作流引擎的使用

Activiti工作流引擎的使用

對於工作流引擎的解釋請參考百度百科:工作流引擎

1.1 我與工作流引擎

在第一家公司工作的時候主要任務就是開發OA系統,當然基本都是有工作流的支援,不過當時使用的工作流引擎是公司一些牛人開發的(據說是用一個開源的引擎修改的),名稱叫CoreFlow;功能相對Activiti來說比較弱,但是能滿足日常的使用,當然也有不少的問題所以後來我們只能修改引擎的程式碼打補丁。

現在是我工作的第二家公司,因為要開發ERP、OA等系統需要使用工作流,在專案調研階段我先搜尋資料選擇使用哪個開源工作流引擎,最終確定了Activiti5並基於公司的架構做了一些DEMO。

1.2 Activiti與JBPM5?

對於Activiti、jBPM4、jBPM5我們應該如何選擇,在InfoQ上有一篇文章寫的很好,從大的層面比較各個引擎之間的差異,請參考文章:縱觀jBPM:從jBPM3到jBPM5以及Activiti5

1.3 Activiti資料

2.初次使用遇到問題收集

因為Activiti剛剛退出不久所以資料比較空缺,中文資料更是少的可憐,所以開始的時候一頭霧水(雖然之前用過工作流,但是感覺差距很多),而且官方的手冊還不是很全面;所以我把我在學習使用的過程遇到的一些疑問都羅列出來分享給大家;以下幾點是我遇到和想到的,如果你還有什麼疑問可以在評論中和我交流再補充。

2.1 部署流程圖後中文亂碼

亂碼是一直纏繞著國人的問題,之前各個技術、工具出現亂碼的問題寫過很多文章,這裡也不例外……,Activiti的亂碼問題在流程圖中。

流程圖的亂碼如下圖所示:

Activiti工作流引擎使用

解決辦法有兩種:

2.1.1 修改原始碼方式

修改原始碼

org.activiti.engine.impl.bpmn.diagram.ProcessDiagramCanvas

在構造方法

public ProcessDiagramCanvas(int width, int height)
中有一行程式碼是設定字型的,預設是用 Arial 字型,這就是亂碼產生的原因,把字改為本地的中文字型即可,例如:
Font font = new Font("WenQuanYi Micro Hei", Font.BOLD, 11);

當然如果你有配置檔案讀取工具那麼可以設定在*.properties檔案中,我就是這麼做的:

Font font = new Font(PropertyFileUtil.get("activiti.diagram.canvas.font"), Font.BOLD, 11);

2.1.2 使用壓縮包方式部署

Activiti支援部署*.bpmn20.xml、bar、zip格式的流程定義。

使用Activit Deisigner工具設計流程圖的時候會有三個型別的檔案:

  • .activiti設計工具使用的檔案

  • .bpmn20.xml設計工具自動根據.activiti檔案生成的xml檔案

  • .png流程圖圖片

解決辦法就是把xml檔案和圖片檔案同時部署,因為在單獨部署xml檔案的時候Activiti會自動生成一張流程圖的圖片檔案,但是這樣在使用的時候座標和圖片對應不起來……

所以把xml和圖片同時部署的時候Activiti自動關聯xml和圖片,當需要獲取圖片的時候直接返回部署時壓縮包裡面的圖片檔案,而不是Activiti自動生成的圖片檔案

2.1.2.1 使用工具打包Bar檔案

右鍵專案名稱然後點選“Create deployment artifacts”,會在src目錄中建立deployment資料夾,裡面包含*.bar檔案.

2.1.2.2 使用Ant指令碼打包Zip檔案

這也是我們採用的辦法,你可以手動選擇xml和png打包成zip格式的檔案,也可以像我們一樣採用ant target的方式打包這兩個檔案。

<?xml version="1.0" encoding="UTF-8"?>
<project name="foo">

    <property name="workflow.definition" value="foo-common-core/src/main/resources/diagrams" />
    <property name="workflow.deployments" value="foo-common-core/src/main/resources/deployments" />

<target name="workflow.package.oa.leave">
		<echo>打包流程定義及流程圖::OA-請假</echo>
		<zip destfile="${workflow.deployments}/oa/leave.zip" basedir="${workflow.definition}/oa/leave" update="true"
			includes="*.xml,*.png" />
	</target>
</project>


這樣當修改流程定義檔案後只要執行ant命令就可以打包了:

ant workflow.package.oa.leave

現在部署bar或者zip檔案檢視流程圖圖片就不是亂碼了,而是你的壓縮包裡面的png檔案。

2.2 使用引擎提供的Form還是自定義業務Form

2.2.1 引擎提供的Form

定義表單的方式在每個Task標籤中定義extensionElementsactiviti:formProperty即可,到達這個節點的時候可以通過API讀取表單元素。

Activiti官方的例子使用的就是在流程定義中設定每一個節點顯示什麼樣的表單哪些欄位需要顯示、哪些欄位只讀、哪些欄位必填。

但是這種方式僅僅適用於比較簡單的流程,對於稍微複雜或者頁面需要業務邏輯的判斷的情況就不適用了。

對於資料的儲存都是在引擎的表中,不利於和其他表的關聯、對整個系統的規劃也不利!

2.2.2 自定義業務Form

這種方式應該是大家用的最多的了,因為一般的業務系統業務邏輯都會比較複雜,而且資料庫中很多表都會有依賴關係,表單中有很多狀態判斷。

例如我們的系統適用jQuery UI作為UI,有很多javascript程式碼,頁面的很多操作需要特殊處理(例如:多個選項的互斥、每個節點根據型別和操作人顯示不同的按鈕);基本每個公司都有一套自己的UI風格,要保持多個系統的操作習慣一致只能使用自定義表單才能滿足。

2.3 業務和流程的關聯方式

這個問題在群裡面很多人都問過,這也是我剛剛開始迷惑的地方;

後來看了以下API發現RuntimeService有兩個方法:

2.3.1 startProcessInstanceByKey

javadoc對其說明:

startProcessInstanceByKey(String processDefinitionKey, Map variabes) 
          Starts a new process instance in the latest version of the process definition with the given key

其中businessKey就是業務ID,例如要申請請假,那麼先填寫登記資訊,然後(儲存+啟動流程),因為請假是單獨設計的資料表,所以儲存後得到實體ID就可以把它傳給processInstanceBusinessKey方法啟動流程。當需要根據businessKey查詢流程的時候就可以通過API查詢:

runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(processInstanceBusinessKey, processDefinitionKey);

議資料庫冗餘設計:在業務表設計的時候新增一列:PROCESS_INSTANCE_ID varchar2(64),在流程啟動之後把流程ID更新到業務表中,這樣不管從業務還是流程都可以查詢到對方!

特別說明: 此方法啟動時自動選擇最新版本的流程定義。

2.3.2 startProcessInstanceById

javadoc對其說明:

startProcessInstanceById(String processDefinitionId, String businessKey, Map variables) 
          Starts a new process instance in the exactly specified version of the process definition with the given id.

processDefinitionId:這個引數的值可以通過repositoryService.createProcessDefinitionQuery()方法查詢,對應資料庫:ACT_RE_PROCDEF;每次部署一次流程定義就會新增一條資料,同名的版本號累加。

特別說明: 此可以指定不同版本的流程定義,讓使用者多一層選擇。

2.3.3 如何選擇

建議使用startProcessInstanceByKey,特殊情況需要使用以往的版本選擇使用startProcessInstanceById

2.4 同步使用者資料

這個問題也是比較多的人詢問過,Activiti支援對任務分配到:指定人、指定組、兩者組合,而這些人和組的資訊都儲存在ACT_ID..表中,有自己的使用者和組(角色)管理讓很多人不知所措了;原因是因為每個系統都會存在一個許可權管理模組(維護:使用者、部門、角色、授權),不知道該怎麼和Activiti同步。

2.4.1 建議處理方式

Activiti有一個IdentityService介面,通過這個介面可以操控Activiti的ACT_ID_*表的資料,一般的做法是用業務系統的許可權管理模組維護使用者資料,當進行CRUD操作的時候在原有業務邏輯後面新增同步到Activiti的程式碼;例如新增一個使用者時同步Activiti User的程式碼片段:

/**
 * 儲存使用者資訊 並且同步使用者資訊到activiti的identity.User,同時設定角色
 * @param user
 * @param roleIds
 */
public void saveUser(User user, List<Long> roleIds, boolean synToActiviti) {
	accountManager.saveEntity(user);
	String userId = user.getId().toString();

	if (synToActiviti) {
		List<org.activiti.engine.identity.User> activitiUsers = identityService.createUserQuery().userId(userId).list();
		if (activitiUsers.size() == 1) {
			//更新資訊
			org.activiti.engine.identity.User activitiUser = activitiUsers.get(0);
			activitiUser.setFirstName(user.getName());
			activitiUser.setLastName("");
			activitiUser.setPassword(user.getPassword());
			activitiUser.setEmail(user.getEmail());
			identityService.saveUser(activitiUser);

			// 刪除使用者的membership
			List<Group> activitiGroups = identityService.createGroupQuery().groupMember(userId).list();
			for (Group group : activitiGroups) {
				identityService.deleteMembership(userId, group.getId());
			}

			// 新增membership
			for (Long roleId : roleIds) {
				Role role = roleManager.getEntity(roleId);
				identityService.createMembership(userId, role.getEnName());
			}

		} else {
			org.activiti.engine.identity.User newUser = identityService.newUser(userId);
			newUser.setFirstName(user.getName());
			newUser.setLastName("");
			newUser.setPassword(user.getPassword());
			newUser.setEmail(user.getEmail());
			identityService.saveUser(newUser);

			// 新增membership
			for (Long roleId : roleIds) {
				Role role = roleManager.getEntity(roleId);
				identityService.createMembership(userId, role.getEnName());
			}
		}
	}

}

刪除操作也和這個類似!

不管從業務系統維護使用者還是從Activiti維護,肯定要確定一方,然後CRUD的時候同步到對方,如果需要同步多個子系統那麼可以再呼叫WebService實現。

2.5 流程圖設計工具用什麼

Activiti提供了兩個流程設計工具,但是面向物件不同。

  • Activiti Modeler,面向業務人員,使用開源的BPMN設計工具Signavio,使用BPMN描述業務流程圖

  • Eclipse Designer,面向開發人員,Eclipse的外掛,可以讓開發人員定製每個節點的屬性(ID、Name、Listener、Attr等)

2.5.1 我們的方式

可能你會驚訝,因為我們沒有使用Activiti Modeler,我們認為用Viso已經能表達流程圖的意思了,而且專案經理也是技術出身,和開發人員也容易溝通。

目前這個專案是第一個使用Activiti的,開始我們在需求調研階段使用Viso設計流程圖,利用泳道流程圖設計和客戶溝通,確定後由負責流程的開發人員用Eclipse Designer設計得到bpmn20.xml,最後部署。

2.6 Eclipse Designer存在的問題

這個外掛有一個很討厭的Bug一直未修復,安裝了外掛後Eclipse的複製和粘帖快捷鍵會被更換為(Ctrl+Insert、Shift+Insert);Bug描述請見:

所以最後我們只能單獨開一個安裝了Eclipse Designer的Eclipse專門用來設計流程圖,這樣就不影響正常使用Eclipse JAVAEE了。

3.配置

3.1 整合Spring

對於和Spring的整合Activiti做的不錯,簡單配置一些Bean代理即可實現,但是有兩個和事務相關的地方要提示:

  • 配置processEngineConfiguration的時候屬性transactionManager要使用和業務功能的同一個事務管理Bean,否則事務不同步。

  • 對於實現了org.activiti.engine.delegate包中的介面的類需要被事務控制的實現類需要被Spring代理,並且新增事務的Annotation或者在xml中配置,例如:

    /**
     * 建立繳費流程的時候自動建立實體
     *
     * @author HenryYan
     */
    @Service
    @Transactional
    publicclass CreatePaymentProcessListener implementsExecutionListener {
       ....
    }

?

4.使用單元測試

單元測試均使用Spring的AbstractTransactionalJUnit4SpringContextTests作為SuperClass,並且在測試類新增:

@ContextConfiguration(locations = { "/applicationContext-test.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
?

雖然Activiti也提供了測試的一些超類,但是感覺不好用,所以自己封裝了一些方法。

4.1 驗證流程圖設計是否正確

4.2 業務物件和流程關聯測試

5.各種狀態的任務查詢以及和業務物件關聯

我們目前分為4中狀態:未簽收、辦理中、執行中、已完成。

查詢到任務或者流程例項後要顯示在頁面,這個時候需要新增業務資料,最終結果就是業務和流程的並集,請參考6.2

5.1 未簽收(Task)

此類任務針對於把Task分配給一個角色時,例如部門領導,因為部門領導角色可以指定多個人所以需要先簽收再辦理,術語:搶佔式

對應的API查詢:

/**
 * 獲取未簽收的任務查詢物件
 * @param userId    使用者ID
 */
@Transactional(readOnly = true)
publicTaskQuery createUnsignedTaskQuery(String userId) {
    TaskQuery taskCandidateUserQuery = taskService.createTaskQuery().processDefinitionKey(getProcessDefKey())
            .taskCandidateUser(userId);
    returntaskCandidateUserQuery;
}
?

5.2 辦理中(Task)

此類任務資料類源有兩種:

  • 簽收後的,5.1中籤收後就應該為辦理中狀態

  • 節點指定的是具體到一個人,而不是角色

對應的API查詢:

/**
 * 獲取正在處理的任務查詢物件
 * @param userId    使用者ID
 */
@Transactional(readOnly = true)
publicTaskQuery createTodoTaskQuery(String userId) {
    TaskQuery taskAssigneeQuery = taskService.createTaskQuery().processDefinitionKey(getProcessDefKey()).taskAssignee(userId);
    returntaskAssigneeQuery;
}
?

5.3 執行中(ProcessInstance)

說白了就是沒有結束的流程,所有參與過的人都應該可以看到這個例項,但是Activiti的API沒有可以通過使用者查詢的方法,這個只能自己用hack的方式處理了,我目前還沒有處理。

從表ACT_RU_EXECUTION中查詢資料。

對應的API查詢:

/**
 * 獲取未經完成的流程例項查詢物件
 * @param userId    使用者ID
 */
@Transactional(readOnly = true)
publicProcessInstanceQuery createUnFinishedProcessInstanceQuery(String userId) {
    ProcessInstanceQuery unfinishedQuery = runtimeService.createProcessInstanceQuery().processDefinitionKey(getProcessDefKey())
            .active();
    returnunfinishedQuery;
}
?

5.4 已完成(HistoricProcessInstance)

已經結束的流程例項。

從表ACT_HI_PROCINST中查詢資料。

/**
 * 獲取已經完成的流程例項查詢物件
 * @param userId    使用者ID
 */
@Transactional(readOnly = true)
publicHistoricProcessInstanceQuery createFinishedProcessInstanceQuery(String userId) {
    HistoricProcessInstanceQuery finishedQuery = historyService.createHistoricProcessInstanceQuery()
            .processDefinitionKey(getProcessDefKey()).finished();
    returnfinishedQuery;
}

?

5.5 查詢時和業務關聯

提示:之前在業務物件添加了PROCESS_INSTANCE_ID欄位

思路:現在可以利用這個欄位查詢了,不管是Task還是ProcessInstance都可以得到流程例項ID,可以根據流程例項ID查詢實體然後把流程物件設定到實體的一個屬性中由Action或者Controller輸出到前臺。

6.UI及截圖

結合實際業務描述一個業務從開始到結束的過程,對於迷惑的同學看完豁然開朗了;這裡使用請假作為例子。

6.1 單獨一個列表負責申請

這樣的好處是申請和流程辦理分離開處理,列表顯示未啟動流程的請假記錄(資料庫PROCESS_INSTANCE_ID為空)。

申請介面的截圖:

Activiti工作流引擎使用

6.2 流程狀態

Activiti工作流引擎使用

6.3 流程跟蹤

圖片方式顯示當前節點:

Activiti工作流引擎使用

列表形式顯示流程流轉過程:

Activiti工作流引擎使用

6.3.1 當前節點定位JS

Javascript思路:先通過Ajax獲取當前節點的座標,在指定位置新增紅色邊框,然後載入圖片。

7.開啟Logger

  1. 新增log4j的jar
  2. 設定log4j.logger.java.sql=DEBUG

8.結束

之前就想寫這篇文章,現在終於完成了,花費了幾個小時,希望能節省你幾天的時間。

請讀者仔細閱讀Activiti的使用者手冊和Javadoc。