Activiti工作流学习-----基于5.19.0版本(4)
四、使用工作流开发
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 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()); } }
4.8 工作流在web应用中使用
流程引擎是线程安全类,可以被多个线程单独使用,在web环境中意味着流程引擎需要在项目启动而创建流程引擎,在项目关闭而关闭流程引擎。下面的代码简单的创建了ServletContextListener 来初始化流程引擎和销毁。
public class ProcessEnginesServletContextListener implements ServletContextListener { public void contextInitialized(ServletContextEvent servletContextEvent) { ProcessEngines.init(); } public void contextDestroyed(ServletContextEvent servletContextEvent) { ProcessEngines.destroy(); } }
posted on 2016-08-21 02:00 liujie037 阅读(5995) 评论(0) 编辑 收藏 举报