四、使用工作流開發 org.activiti.engine.ProcessEngine提供的Service作用在工作流引擎上面,如果所示是模仿一個公司簡單的審批流程,你可以下載這個Demo:Activiti unit test template玩玩。 發佈這個流程圖可以通過RepositorySer ...
四、使用工作流開發
org.activiti.engine.ProcessEngine提供的Service作用在工作流引擎上面,如果所示是模仿一個公司簡單的審批流程,你可以下載這個Demo:Activiti unit test template玩玩。
發佈這個流程圖可以通過RepositoryService進行,在資料庫中存儲的這些靜態數據是這些:
<?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:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://activiti.org/bpmn20" id="definitions"> <process id="vacationRequest" name="Vacation request" isExecutable="true"> <startEvent id="request" activiti:initiator="employeeName"> <extensionElements> <activiti:formProperty id="numberOfDays" name="Number of days" type="long" required="true"></activiti:formProperty> <activiti:formProperty id="startDate" name="First day of holiday (dd-MM-yyy)" type="date" datePattern="dd-MM-yyyy hh:mm" required="true"></activiti:formProperty> <activiti:formProperty id="vacationMotivation" name="Motivation" type="string"></activiti:formProperty> </extensionElements> </startEvent> <sequenceFlow id="flow1" sourceRef="request" targetRef="handleRequest"></sequenceFlow> <userTask id="handleRequest" name="處理休假單" activiti:candidateGroups="management"> <documentation>${employeeName} would like to take ${numberOfDays} day(s) of vacation (Motivation: ${vacationMotivation}).</documentation> <extensionElements> <activiti:formProperty id="vacationApproved" name="Do you approve this vacation" type="enum" required="true"> <activiti:value id="true" name="Approve"></activiti:value> <activiti:value id="false" name="Reject"></activiti:value> </activiti:formProperty> <activiti:formProperty id="managerMotivation" name="Motivation" type="string"></activiti:formProperty> </extensionElements> </userTask> <sequenceFlow id="flow2" sourceRef="handleRequest" targetRef="requestApprovedDecision"></sequenceFlow> <exclusiveGateway id="requestApprovedDecision" name="Request approved?"></exclusiveGateway> <sequenceFlow id="flow3" name="同意" sourceRef="requestApprovedDecision" targetRef="sendApprovalMail"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${vacationApproved == 'true'}]]></conditionExpression> </sequenceFlow> <manualTask id="sendApprovalMail" name="發送郵件"></manualTask> <sequenceFlow id="flow4" sourceRef="sendApprovalMail" targetRef="theEnd1"></sequenceFlow> <endEvent id="theEnd1"></endEvent> <sequenceFlow id="flow5" name="不同意" sourceRef="requestApprovedDecision" targetRef="adjustVacationRequestTask"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${vacationApproved == 'false'}]]></conditionExpression> </sequenceFlow> <userTask id="adjustVacationRequestTask" name="修改休假單" activiti:assignee="${employeeName}"> <documentation>Your manager has disapproved your vacation request for ${numberOfDays} days. Reason: ${managerMotivation}</documentation> <extensionElements> <activiti:formProperty id="numberOfDays" name="Number of days" type="long" required="true"></activiti:formProperty> <activiti:formProperty id="startDate" name="First day of holiday (dd-MM-yyy)" type="date" datePattern="dd-MM-yyyy hh:mm" required="true"></activiti:formProperty> <activiti:formProperty id="vacationMotivation" name="Motivation" type="string"></activiti:formProperty> <activiti:formProperty id="resendRequest" name="Resend vacation request to manager?" type="enum" required="true"> <activiti:value id="true" name="Yes"></activiti:value> <activiti:value id="false" name="No"></activiti:value> </activiti:formProperty> </extensionElements> </userTask> <sequenceFlow id="flow6" sourceRef="adjustVacationRequestTask" targetRef="resendRequestDecision"></sequenceFlow> <exclusiveGateway id="resendRequestDecision" name="Resend request?"></exclusiveGateway> <sequenceFlow id="flow7" name="重新請求處理" sourceRef="resendRequestDecision" targetRef="handleRequest"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${resendRequest == 'true'}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="flow8" name="放棄休假" sourceRef="resendRequestDecision" targetRef="theEnd2"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${resendRequest == 'false'}]]></conditionExpression> </sequenceFlow> <endEvent id="theEnd2"></endEvent> </process> <bpmndi:BPMNDiagram id="BPMNDiagram_vacationRequest"> <bpmndi:BPMNPlane bpmnElement="vacationRequest" id="BPMNPlane_vacationRequest"> <bpmndi:BPMNShape bpmnElement="request" id="BPMNShape_request"> <omgdc:Bounds height="35.0" width="35.0" x="1.0" y="61.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="handleRequest" id="BPMNShape_handleRequest"> <omgdc:Bounds height="60.0" width="100.0" x="102.0" y="49.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="requestApprovedDecision" id="BPMNShape_requestApprovedDecision"> <omgdc:Bounds height="40.0" width="40.0" x="237.0" y="58.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="sendApprovalMail" id="BPMNShape_sendApprovalMail"> <omgdc:Bounds height="60.0" width="100.0" x="391.0" y="49.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="theEnd1" id="BPMNShape_theEnd1"> <omgdc:Bounds height="35.0" width="35.0" x="641.0" y="61.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="adjustVacationRequestTask" id="BPMNShape_adjustVacationRequestTask"> <omgdc:Bounds height="60.0" width="100.0" x="391.0" y="165.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="resendRequestDecision" id="BPMNShape_resendRequestDecision"> <omgdc:Bounds height="40.0" width="40.0" x="541.0" y="174.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNShape bpmnElement="theEnd2" id="BPMNShape_theEnd2"> <omgdc:Bounds height="35.0" width="35.0" x="641.0" y="177.0"></omgdc:Bounds> </bpmndi:BPMNShape> <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1"> <omgdi:waypoint x="36.0" y="78.0"></omgdi:waypoint> <omgdi:waypoint x="102.0" y="79.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2"> <omgdi:waypoint x="202.0" y="79.0"></omgdi:waypoint> <omgdi:waypoint x="237.0" y="78.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3"> <omgdi:waypoint x="277.0" y="78.0"></omgdi:waypoint> <omgdi:waypoint x="320.0" y="77.0"></omgdi:waypoint> <omgdi:waypoint x="391.0" y="79.0"></omgdi:waypoint> <bpmndi:BPMNLabel> <omgdc:Bounds height="14.0" width="100.0" x="277.0" y="78.0"></omgdc:Bounds> </bpmndi:BPMNLabel> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4"> <omgdi:waypoint x="491.0" y="79.0"></omgdi:waypoint> <omgdi:waypoint x="523.0" y="78.0"></omgdi:waypoint> <omgdi:waypoint x="641.0" y="78.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5"> <omgdi:waypoint x="257.0" y="98.0"></omgdi:waypoint> <omgdi:waypoint x="257.0" y="190.0"></omgdi:waypoint> <omgdi:waypoint x="303.0" y="190.0"></omgdi:waypoint> <omgdi:waypoint x="391.0" y="195.0"></omgdi:waypoint> <bpmndi:BPMNLabel> <omgdc:Bounds height="14.0" width="100.0" x="261.0" y="151.0"></omgdc:Bounds> </bpmndi:BPMNLabel> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="flow6" id="BPMNEdge_flow6"> <omgdi:waypoint x="491.0" y="195.0"></omgdi:waypoint> <omgdi:waypoint x="541.0" y="194.0"></omgdi:waypoint> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="flow7" id="BPMNEdge_flow7"> <omgdi:waypoint x="561.0" y="214.0"></omgdi:waypoint> <omgdi:waypoint x="561.0" y="329.0"></omgdi:waypoint> <omgdi:waypoint x="149.0" y="329.0"></omgdi:waypoint> <omgdi:waypoint x="152.0" y="109.0"></omgdi:waypoint> <bpmndi:BPMNLabel> <omgdc:Bounds height="14.0" width="100.0" x="321.0" y="309.0"></omgdc:Bounds> </bpmndi:BPMNLabel> </bpmndi:BPMNEdge> <bpmndi:BPMNEdge bpmnElement="flow8" id="BPMNEdge_flow8"> <omgdi:waypoint x="581.0" y="194.0"></omgdi:waypoint> <omgdi:waypoint x="641.0" y="194.0"></omgdi:waypoint> <bpmndi:BPMNLabel> <omgdc:Bounds height="14.0" width="100.0" x="581.0" y="194.0"></omgdc:Bounds> </bpmndi:BPMNLabel> </bpmndi:BPMNEdge> </bpmndi:BPMNPlane> </bpmndi:BPMNDiagram> </definitions>
工作流引擎將會將xml轉化成可執行的java對象和資料庫記錄,即使重啟,工作流引擎仍然知道這些數據。發佈可以這樣書寫:
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine(); RepositoryService repositoryService = processEngine.getRepositoryService();
//載入xml流程定義文件 repositoryService.createDeployment() .addClasspathResource("org/activiti/test/VacationRequest.bpmn20.xml") .deploy(); Log.info("Number of process definitions: " + repositoryService.createProcessDefinitionQuery().count());
4.1 啟動流程實例
在成功發佈流程定義到工作流引擎後,我們可以啟動一個新的流程實例。流程定義和流程實例是一對多的關係,是一靜一動的關係。使用RuntimeService可以操作相關的流程,有很多方式啟動流程實例,在下麵的代碼中,我們使用了在流程定義的key來啟動它,同時我們在啟動過程中也添加了流程變數在流程實例中,流程變數大到整個流程實例的作用範圍,小到局部任務節點,流程實例具有的流程變數也是和其他流程變數之間區分的差別。流程變數是一個典型的Map結構:
1 Map<String, Object> variables = new HashMap<String, Object>(); 2 variables.put("employeeName", "Kermit"); 3 variables.put("numberOfDays", new Integer(4)); 4 variables.put("vacationMotivation", "I'm really tired!"); 5 6 RuntimeService runtimeService = processEngine.getRuntimeService();
//vacationRequest是開發者在流程定義xml中事先定義好的。
7 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("vacationRequest", variables);
8 // Verify that we started a new process instance
9 Log.info("Number of process instances: " + runtimeService.createProcessInstanceQuery().count());
4.2 完成任務
在流程實例成功啟動後,流程第一步會是一個用戶的任務,他必須由用戶手動完成,而任務的獲取可以使用以下代碼實現:
// 查詢一個叫management的用戶組的組任務 TaskService taskService = processEngine.getTaskService(); List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("management").list(); for (Task task : tasks) { Log.info("Task available: " + task.getName()); }
在查詢出了任務後,流程實例需要繼續開展往往需要我們完成這些任務,完成任務的代碼:
1 Task task = tasks.get(0); 2 3 Map<String, Object> taskVariables = new HashMap<String, Object>(); 4 taskVariables.put("vacationApproved", "false"); 5 taskVariables.put("managerMotivation", "We have a tight deadline!"); 6 //調用complete方法完成任務 7 taskService.complete(task.getId(), taskVariables);
流程實例將會進入下一步,在例子代碼中,我們將流程變數vacationApproved置為了false,也就是說審批不同意,根據流程圖下一步是返回給請假者,請假者自己處理審批結果,
請假者可以重新提交修改的請假單,流程將會迴圈進入流程圖中的開始任務處。
4.3 流程暫停和激活
流程定義在流轉中很可能被暫停,如果是流程定義被暫停了,那麼流程實例將不能夠被創建,同時工作流引擎將會拋出異常。暫停流程定義可以這樣實現:
1 repositoryService.suspendProcessDefinitionByKey("vacationRequest"); 2 try { 3 runtimeService.startProcessInstanceByKey("vacationRequest"); 4 } catch (ActivitiException e) { 5 e.printStackTrace(); 6 }
為了重新激活流程定義,調用方法repositoryService.activateProcessDefinitionXXX即可。
也有可能流程實例被暫停了,在暫停的時候,流程不能繼續開展(比如在完成任務拋出異常),沒有作業會執行,暫停一個流程實例調用RuntimeService的runtimeService.suspendProcessInstance方法,激活
流程實例執行runtimeService.activateProcessInstanceXXX方法即可。有深入瞭解的同學可以去看我後期的博客,同時Activiti作為一個開源項目,大家也可以直接深入源碼進行學習和查看官方的api文檔。
4.4 工作流的查詢
在工作流引擎中查詢有兩種方式:工作流的api查詢和mybatis的sql查詢,activiti的查詢API設計非常優雅,你可以連續的加上不同的限制條件和排序條件(它們在邏輯上都是And形式),例如下麵這段代碼:
1 List<Task> tasks = taskService.createTaskQuery() 2 .taskAssignee("kermit") 3 .processVariableValueEquals("orderId", "0815") 4 .orderByDueDate().asc() 5 .list();
有時候你需要更加強大的查詢,比如OR條件查詢以及其他無法使用API進行描述的查詢。對於這些情況,activiti建議你使用原生的sql查詢,查詢對象(比如TaskQuery)已經定義了返回不同的對象,比如Task, ProcessInstance, Execution等,使用原生sql查詢需要SQL知識和Activiti表結構知識(比如查詢你至少知道表名是什麼吧),activiti幫我們做了很多,比如下麵代碼:
//拼裝sql語句 List<Task> tasks = taskService.createNativeTaskQuery() .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T WHERE T.NAME_ = #{taskName}") .parameter("taskName", "gonzoTask") .list(); long count = taskService.createNativeTaskQuery() .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T1, " + managementService.getTableName(VariableInstanceEntity.class) + " V1 WHERE V1.TASK_ID_ = T1.ID_") .count();
4.5 工作流中的變數
每一個流程實例在流程步驟中需要和使用變數。變數也會存儲在資料庫中,變數也能在表達式中使用(比如排他網關根據流程變數的取值決定流程的走向),在工作流之外其他的service提供服務調用後存儲輸入和輸入結果。
一個流程實例擁有的變數叫做流程變數,執行對象也能擁有變數,不過變數只有當前任務才能擁有,流程繼續執行時就無法再次獲取和存儲上一次執行對象的變數。原則上流程變數數量是沒有限制的,每一個變數都存儲在表ACT_RU_VARIABLE中。
所有的startProcessInstanceXXX方法都提供了變數傳參的方法,比如:
ProcessInstance startProcessInstanceByKey(String processDefinitionKey, Map<String, Object> variables);
變數也可以在流程執行對象處添加,比如RuntimeService的API:
1 void setVariable(String executionId, String variableName, Object value); 2 void setVariableLocal(String executionId, String variableName, Object value); 3 void setVariables(String executionId, Map<String, ? extends Object> variables); 4 void setVariablesLocal(String executionId, Map<String, ? extends Object> variables);
在流程執行對象中設置的變數是局部變數(記住流程實例是由多個樹形結構的執行對象組成),局部變數僅僅是在對應的執行對象中可見,在我們不想讓變數傳播影響到流程實例這一層次的話,可以考慮使用局部變數,又比如在並行網關那裡需要對一個變數賦予新值而不會影響另外其他流程執行的路徑時也會考慮使用局部變數。
在任務對象上對局部變數的存取的API如下所示:(調用TaskService)
1 Map<String, Object> getVariables(String executionId); 2 Map<String, Object> getVariablesLocal(String executionId); 3 Map<String, Object> getVariables(String executionId, Collection<String> variableNames); 4 Map<String, Object> getVariablesLocal(String executionId, Collection<String> variableNames); 5 Object getVariable(String executionId, String variableName); 6 <T> T getVariable(String executionId, String variableName, Class<T> variableClass);
變數經常被用在短語、表達式、執行對象或者任務,任務監聽器、腳本等,譬如execution(執行對象)的API:
1 execution.getVariables(); 2 execution.getVariables(Collection<String> variableNames); 3 execution.getVariable(String variableName); 4 5 execution.setVariables(Map<String, object> variables); 6 execution.setVariable(String variableName, Object value);
由於歷史版本原因,在執行任何上述方法的時候,activiti預設是將所有的變數從資料庫取出來,意味著資料庫存有10個變數,現在你需要取出名叫myVariable的變數,但是其餘的9個也會被取出來並且緩存起來。這並不是很差,因為後期你取變數就不會再從資料庫取出,當然如果你有大量的變數或者在查詢方面你想進一步控制資料庫,這時全部取出就不怎麼合適了。從Activiti5.17版本開始,新添加了的方法支持是否全部查詢:
1 Map<String, Object> getVariables(Collection<String> variableNames, boolean fetchAllVariables); 2 Object getVariable(String variableName, boolean fetchAllVariables); 3 void setVariable(String variableName, Object value, boolean fetchAllVariables);
4.6 表達式
目前工作流使用的表達式語言是UEL,所謂的UEL就是Unified Expression Language,他是javaee6的規範之一,為了讓最新的UEL的所有特性都用在activiti環境中,我們使用了JUEL的修改版。
表達式有兩種:值表達式(value-expression)和方法表達式(method-expression), 在表達式需要的地方,兩種表達式都可以使用。
- Value expression: 用法和Spring環境相同,所有可以從Spring中讀取值。
${myVar}
${myBean.myProperty}
- method-expression:執行指定的方法,參數可有可無。activiti依靠圓括弧()區別表達式是method-expression,例如:
${printer.print()}
${myBean.addNewOrder('orderName')}
${myBean.doSomething(myVar, execution)}
表達式支持對象是beans, lists, arrays and maps.
在流程變數中,activiti已經定義了下麵變數名,它們已經被使用:
- execution:它含有當前正在執行的執行對象的信息。
- task:它含有當前任務的信息。
- authenticatedUserId:當前被驗證的用戶ID,如果沒有用戶被驗證,當前值為空。
4.7 工作流的單元測試與技巧
業務流程是軟體工程中不可或缺的一部分,其中的邏輯應該被測試到。自從activiti能夠嵌入Java應用中後,為業務流程書寫單元測試變得平常簡單了。Activiti支持Junit3和Junit4的測試風格,在junit3中,org.activiti.engine.test.ActivitiTestCase需要被繼承。ActivitiTestCase中protected的方法能夠創建流程引擎和相關的Services,預設的,創建的流程引擎會從類路徑下麵載入activiti.cfg.xml,為了更加靈活的載入,你需要重寫方法:getConfigurationResource()。在多個測試單元測試中如果讀取的配置文件相同,流程引擎會被緩存起來。通過繼承,你可以使用註解@Deployment在方法上面,在執行單元測試方法前,會將該測試類同一目錄的testClassName.testMethod.bpmn20.xml文件載入和發佈,在測試方法結束時候將會刪除流程實例,任務等,而且@Deployment也支持自定義載入文件,可以查看源代碼分析,這裡就不贅述了。
1 public class MyBusinessProcessTest extends ActivitiTestCase { 2 3 @Deployment 4 public void testSimpleProcess() { 5 runtimeService.startProcessInstanceByKey("simpleProcess"); 6