2.4Hello World
Hello World如此经典,几乎每学习一门新技术都从它开始,它是一把打开技术之门的万能钥匙,因为通过Hello World能快速了解一门技术如何配置、运行,以及得到什么样的结果。
好吧,让我们一起开启探索Activiti的大门。
2.4.1最简单的流程定义
代码清单2-2是一个最简单的请假流程定义文件,简单到仅有开始节点和结束节点。
代码清单2-2最简单的请假流程定义文件leave.bpmn
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" #1-S
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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://www.kafeitu.me/activiti-in-action"> #1-E
<process id="leave" name="Leave"> #2
<startEvent id="startevent1" name="Start"></startEvent> #3
<endEvent id="endevent1" name="End"></endEvent> #4
<sequenceFlow id="flow1" name="" sourceRef="startevent1" #5
targetRef="endevent1"></sequenceFlow>
</process>
在解释这个文件的内容之前我们先来了解一下为了更好地理解代码含义本书所做的一些约定。
#号后面加数字用来表示代码清单的第几处(不是行号),其中1-S和1-E分别代表第1处标记的开始(Start)和结束(End)。
在详细讲解流程定义文件之前还要来看看对应的图片形式的流程定义,如图2-6所示。
在代码清单2-2中,1-S处的definitions标签表示BPMN 2.0规范中定义的开始,可以包含多个process标签;紧跟着是XMLSchema的定义,用来验证XML内容是否符合规范;最后一个属性targetNamespace是必需的,用来声明命名空间。为什么必须定义targetNamespace呢?命名空间是BPMN 2.0规范为了易于区分、归类流程定义所设,其值可以是任意文字,当然一般由公司或组织的名称定义,也可以更具体到一个项目,在本例中使用的是http://www.kafeitu.me/activiti-in-action,本书所有的targetNamespace也均使用来声明命名空间。
代码清单2-2中的#2处,定义了id属性为leave的process标签,以此来标记这是一个请假流程定义的开始。
代码清单2-2中的#3处,定义了流程的入口,即空启动事件startEvent,当流程启动时总是以startEvent开始。
代码清单2-2中的#4处,定义了流程的唯一结束出口,即空结束事件endEvent,流程线(flow)执行到endEvent时表示流程结束。可以定义多个结束事件。
代码清单2-2中的#5处,定义了一个sequenceFlow标签,用来描述各个流程节点之间的关系。图2-6中的箭头线就是sequenceFlow所定义的部分。sequenceFlow用sourceRef表示从哪里开始“流”到哪里。
2.4.2创建单元测试类
先来看看我们的Java类代码,如代码清单2-3所示,让程序运行起来,然后再解释代码的含义。
代码清单2-3 最简单的请假流程的Java类
public class VerySimpleLeaveProcessTest {
@Test
public void testStartProcess() throws Exception {
// 创建流程引擎,使用内存数据库
ProcessEngineprocessEngine = ProcessEngineConfiguration #1-S
.createStandaloneInMemProcessEngineConfiguration()
.buildProcessEngine(); #1-E
// 部署流程定义文件
RepositoryService repositoryService = processEngine.getRepositoryService(); #2
repositoryService.createDeployment() #3-S
.addClasspathResource(
"me/kafeitu/activiti/helloworld/sayhelloleave.bpmn.xml").deploy(); #3-E
// 验证已部署流程定义
ProcessDefinitionprocessDefinition = repositoryService #4-S
.createProcessDefinitionQuery().singleResult();
assertEquals("leavesayhello", processDefinition.getKey()); #4-E
// 启动流程并返回流程实例
RuntimeServiceruntimeService = processEngine.getRuntimeService(); #5
ProcessInstanceprocessInstance = runtimeService #6-S
.startProcessInstanceByKey("leavesayhello");
assertNotNull(processInstance);
System.out.println("pid=" + processInstance.getId() + ", pdid="
+ processInstance.getProcessDefinitionId()); #6-E
}
}
下面对代码清单2-3中所有标记处的作用依次进行解释。
#1-S至#1-E是通过编程方式创建一个流程引擎实例,即通过ProcessEngineConfiguration工具类的createStandaloneInMemProcessEngineConfiguration()方法创建一个使用H2内存数据库的流程引擎实例,默认的JdbcUrl为jdbc:h2:mem:activiti。当然,除了此方法之外还有其他创建引擎实例的方法,例如调用ProcessEngineConfiguration.createXXX(). buildProcessEngine(),在调用过程中还可以通过编程方式配置引擎的参数ProcessEngineConfiguration.createXXX().. setFoo(argument).buildProcessEngine()。
#2处紧接着使用刚刚创建的引擎实例获取RepositoryService,第1章列出的Activiti的七大Service接口都可以由ProcessEngine通过getXxxService()方法获取。
#3处使用RepositoryService部署位于classpath中的流程定义文件sayhelloleave.bpmn。
#4-S至#4-E处用来验证刚刚部署的流程是否成功。这里需要说明一下,在通过七大Service接口查询对象时均使用xxxService.createXxxQuery()方式创建查询对象。
#5处和#2处一样,通过引擎实例获取RuntimeService对象。
#6-S处使用runtimeService启动一个流程并返回流程实例。此外runtimeService和创建流程引擎的方式类似,也提供了多种启动流程的方式,可以使用runtimeService.startProcessInstanceXxx()启动流程实例。在启动的同时还可以设置流程变量,具体使用方法可以先参考API文档中的方法说明,以后的章节会陆续讲到根据不同的需求使用不同的启动方式。接下来验证流程是否启动成功,这仅是简单的null验证。读者可以自行扩展验证,以验证自己的猜测结果,这也是一种很好的学习方式。最后输出已启动的流程实例的ID和流程定义的ID。
2.4.3运行Hello World
可以将项目bpmn20-example导入至IDE,笔者使用的是Eclipse IDE for Java EE Developers(Indigo),选择此版本是考虑到后续章节中有基于Web的应用。本书的例子均使用Maven管理,在实例中已配置好依赖需要使用的仓库(Repository),通过http://www.eclipse.org/m2e/download/安装M2Eclipse插件,然后单击菜单:File->Import,选择对话框中的Maven->Exsiting Maven Project选项,最后选择本示例程序所在目录即可。
使用JUnit运行VerySimpleLeaveProcessTest之后得到的结果是:
pid=5,pdid=leavesayhello:1:4
pid即流程实例在数据中的id;pdid的值有些特殊,由一些列参数组合而成并以冒号分割,其中leavesayhello就是流程定义的key,1表示版本号,4表示流程定义在数据库中的id。
除了使用Eclipse运行测试用例外,还可以在命令行中输入mvn test进入本实例目录运行。
2.4.4添加业务节点
前面运行了最简单的例子来说明流程执行过程(严格来说不是一个流程,因为根本没有做任何事情),下面为这个例子添加一点实际业务使其可以正常工作起来。既然是请假流程就应该知道是哪个员工请了几天假(先处理不复杂的业务信息)。
首先需要为流程添加一个用户任务(userTask)来处理申请,根据申请内容决定运行申请还是驳回申请。下面先用如图2-7所示的流程图来表示需求。
图2-7带审批的请假流程
然后再来看看流程定义是如何设计的,以及用户任务(userTask)和脚本任务是如何定义的。支持领导审批的请假流程定义如代码清单2-4所示。
代码清单2-4支持领导审批的请假流程定义
<?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: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://www.kafeitu.me/activiti-in-action">
<process id="SayHelloToLeave" name="SayHelloToLeave">
<startEvent id="startevent1" name="Start"></startEvent>
<userTask id="usertask1" name="领导审批"> #1-S
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>deptLeader</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask> #1-E
<endEvent id="endevent1" name="End"></endEvent>
<sequenceFlow id="flow1" name="" sourceRef="startevent1"
targetRef="usertask1"></sequenceFlow>
<sequenceFlow id="flow2" name="" sourceRef="outputAuditResult"
targetRef="endevent1"></sequenceFlow>
<scriptTask id="outputAuditResult" name="输出审批结果" #2-S
scriptFormat="groovy">
<script><![CDATA[out:println "applyUser:" + applyUser + " ,days:" + days + ", approval:" + approved;]]></script>
</scriptTask> #2-E
<sequenceFlow id="flow3" name="" sourceRef="usertask1"
targetRef="outputAuditResult"></sequenceFlow>
</process>
</definitions>
此流程在2.4.1的基础上添加了新的userTask,并且增加了一个sequenceFlow定义。照例还是来解释一下此流程的定义。
#1处使用userTask来定义图2-7中的“领导审批”节点,其id为deptLeaderAudit,在其内部使用BPMN 2.0标准的用户任务分配定义元素设置了此任务由角色为deptLeader的人员处理,即有deptLeader角色的人员都可以处理此任务。
#2处定义了通过scriptTask来输出“领导审批”节点的处理结果。目前Activiti支持的scriptTask类型有Javascript和Groovy两种。本例使用可以运行在JVM上的脚本语言Groovy输出结果,语法简洁明了,读者比较容易理解。在scriptTask标签内部定义了脚本要处理的脚本内容,由script标签包裹并定义为CDATA类型数据。在流程运行的过程中Activiti会把脚本及流程变量转交给Groovy处理(需要添加Groovy的jar或依赖)。
这一步迈得好像有点大了,从一个空流程突然多出了两个不同的任务,不过这两个任务都比较简单。下面结合代码清单2-5的单元测试代码来讲解一下流程,相信读者能很快了解流程的运行过程以及任务的作用。
代码清单2-5SayHelloToLeaveTest.java
public class SayHelloToLeaveTest {
@Test
public void testStartProcess() throws Exception {
ProcessEngineprocessEngine = ProcessEngineConfiguration
.createStandaloneInMemProcessEngineConfiguration().buildProcessEngine();
RepositoryServicerepositoryService = processEngine.getRepositoryService();
String bpmnFileName = "me/kafeitu/activiti/helloworld/SayHelloToLeave.bpmn"; #1-S
repositoryService.createDeployment()
.addInputStream("SayHelloToLeave.bpmn ",this.getClass().getClassLoader()
.getResourceAsStream(bpmnFileName)).deploy(); #1-E
ProcessDefinitionprocessDefinition = repositoryService
.createProcessDefinitionQuery().singleResult();
assertEquals("SayHelloToLeave", processDefinition.getKey());
RuntimeServiceruntimeService = processEngine.getRuntimeService(); #2-S
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("applyUser", "employee1");
variables.put("days", 3);
ProcessInstanceprocessInstance = runtimeService.startProcessInstanceByKey(
"SayHelloToLeave", variables); #2-E
assertNotNull(processInstance);
System.out.println("pid=" + processInstance.getId() + ", pdid="
+ processInstance.getProcessDefinitionId());
TaskServicetaskService = processEngine.getTaskService(); #3-S
Task taskOfDeptLeader = taskService.createTaskQuery()
.taskCandidateGroup("deptLeader").singleResult();
assertNotNull(taskOfDeptLeader);
assertEquals("领导审批", taskOfDeptLeader.getName()); #3-E
taskService.claim(taskOfDeptLeader.getId(), "leaderUser"); #4
variables = new HashMap<String, Object>(); #5-S
variables.put("approved", true);
taskService.complete(taskOfDeptLeader.getId(), variables); #5-E
taskOfDeptLeader = taskService.createTaskQuery() #6-S
.taskCandidateGroup("deptLeader").singleResult();
assertNull(taskOfDeptLeader); #6-E
HistoryServicehistoryService = processEngine.getHistoryService(); #7
long count = historyService.createHistoricProcessInstanceQuery().finished() #8
.count();
assertEquals(1, count);
}
}
为了更好地理解代码,在开始讲解此代码清单之前先以情景模拟的方式来了解任务的签收与办理过程:公司的每个部门可能有多个领导(A是正手,B是副手),在将一项任务分配给“部门领导”角色之后所有的部门领导都会收到一条未签收状态的任务。假如公司约定请假流程的审核由B处理,在申请人申请请假之后A和B同时会看到一个需要处理的任务(Task),B签收任务并办理,此时A的待办任务列表中就少了一项任务。本来一项任务是属于一个角色或某几个候选人的,在执行了签收动作之后任务归签收人所有。在了解对任务的签收和办理的执行过程之后我们再来看看代码清单2-5的执行过程。
#1处和代码清单2-3的#3-E处一样用来部署流程,只不过变换了一种方式,不是直接部署bpmn20.xml而是部署以bpmn为扩展名的文件。原因解释一下:这个流程定义是通过Eclipse Designer创建的,默认扩展名为bpmn(实际内容是一样的),在Activiti 5.9以及之前的版本中流程引擎在读取流程定义文件时必须以bpmn20.xml结尾,所以这里需要变通一下,在部署流程的时候读取一个文件流并由foo.bpmn20.xml传入,结果和代码清单2-3中部署bpmn20.xml一样。从Activiti 5.10版本开始Activiti已经支持直接部署以bpmn扩展名结尾的流程定义文件。
#2处与代码清单2-3中的启动方式稍微有些差别,在调用启动流程的方法中传入了一个Map集合变量,这里设置了两个属性,applyUser表示申请人的名称,days表示请假的天数。这样Activiti在启动流程的时候会把这两个变量存入数据库中,以后就可以通过接口读取到节点。
#3处的任务是查询组(Group)deptLeader的未签收任务并验证任务的名称。
#4处通过taskService调用claim方法“签收”此任务归用户leaderUser所有。
#5处就是领导的处理结果。在流程启动时填写了请假人与请假天数来模拟实际场景,领导审批通过,也就是在#5-S处设置变量approval为true表示审批通过,在#5-E处完成任务的同时以流程变量的形式设置审批结果。
#6处是为了让读者更好地理解执行结果,因为任务已经办理完成,再次查询组deptLeader的任务已经为空。
#7处通过流程引擎对象获取历史记录查询接口。
#8处通过历史接口统计已经完成(finished)的流程实例数量,接着验证预期的结果。
细心的读者可能会问:明明流程图中有两个节点(领导审核和输出审批结果),为什么在代码中没有处理scriptTask呢?在讨论这个问题之前先看看运行结果:
pid=5, pdid=SayHelloToLeave:1:4
applyUser:employee1 ,days:3, approval:true
结果中第一行和代码清单2-3执行结果相同,第二行则不同。第二行的输出是scriptTask执行的结果,在流程定义中用Groovy脚本输出使用变量拼接的信息。现在可以来回答刚刚提出的问题:为什么代码没有处理scriptTask呢?原因很简单,对于scriptTask,流程引擎会自动处理,处理完成之后流转到下个节点,在本例中scriptTask之后就是结束事件了,流程结束,所以没有处理scriptTask。
就目前来说,除了userTask不能被自动处理之外,其他的任务均由流程引擎自动处理,无需人工参与。