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());
在成功發布流程定義到工作流引擎後,我們可以啟動一個新的流程實例。流程定義和流程實例是一對多的關系,是一靜一動的關系。使用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());
在流程實例成功啟動後,流程第一步會是一個用戶的任務,他必須由用戶手動完成,而任務的獲取可以使用以下代碼實現:
// 查詢一個叫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,也就是說審批不同意,根據流程圖下一步是返回給請假者,請假者自己處理審批結果,
請假者可以重新提交修改的請假單,流程將會循環進入流程圖中的開始任務處。
流程定義在流轉中很可能被暫停,如果是流程定義被暫停了,那麼流程實例將不能夠被創建,同時工作流引擎將會拋出異常。暫停流程定義可以這樣實現:
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文檔。
在工作流引擎中查詢有兩種方式:工作流的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();
每一個流程實例在流程步驟中需要和使用變量。變量也會存儲在數據庫中,變量也能在表達式中使用(比如排他網關根據流程變量的取值決定流程的走向),在工作流之外其他的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);
目前工作流使用的表達式語言是UEL,所謂的UEL就是Unified Expression Language,他是javaee6的規范之一,為了讓最新的UEL的所有特性都用在activiti環境中,我們使用了JUEL的修改版。
表達式有兩種:值表達式(value-expression)和方法表達式(method-expression), 在表達式需要的地方,兩種表達式都可以使用。
${myVar} ${myBean.myProperty}
${printer.print()} ${myBean.addNewOrder('orderName')} ${myBean.doSomething(myVar, execution)}
表達式支持對象是beans, lists, arrays and maps.
在流程變量中,activiti已經定義了下面變量名,它們已經被使用:
業務流程是軟件工程中不可或缺的一部分,其中的邏輯應該被測試到。自從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 7 Task task = taskService.createTaskQuery().singleResult(); 8 assertEquals("My Task", task.getName()); 9 10 taskService.complete(task.getId()); 11 assertEquals(0, runtimeService.createProcessInstanceQuery().count()); 12 } 13 }
在Junit4中也有同樣的功能,必須使用類org.activiti.engine.test.ActivitiRule,通過該類的get方法我們可以獲取Service和流程實例。使用@Rule注解可以調用到org.activiti.engine.test.Deployment的功能,不過它加載xml文件是在類路徑下面,例如下面junit4的測試代碼:
public class MyBusinessProcessTest { @Rule public ActivitiRule activitiRule = new ActivitiRule(); @Test @Deployment public void ruleUsageExample() { RuntimeService runtimeService = activitiRule.getRuntimeService(); runtimeService.startProcessInstanceByKey("ruleUsage"); TaskService taskService = activitiRule.getTaskService(); Task task = taskService.createTaskQuery().singleResult(); assertEquals("My Task", task.getName()); taskService.complete(task.getId()); assertEquals(0, runtimeService.createProcessInstanceQuery().count()); } }
流程引擎是線程安全類,可以被多個線程單獨使用,在web環境中意味著流程引擎需要在項目啟動而創建流程引擎,在項目關閉而關閉流程引擎。下面的代碼簡單的創建了ServletContextListener 來初始化流程引擎和銷毀。
public class ProcessEnginesServletContextListener implements ServletContextListener { public void contextInitialized(ServletContextEvent servletContextEvent) { ProcessEngines.init(); } public void contextDestroyed(ServletContextEvent servletContextEvent) { ProcessEngines.destroy(); } }