第3章 教程
本教程将向你展示使用jpdl构建基本流程及管理运行时执行的API的使用方法。
教程的方式是解释一系列的例子,每个例子以特定的主题为焦点并包含大量的注释。这些例子在jBPM下载包的src/java.examples目录中也能找到。
最好的学习方法就是创建一个项目并且在给定实例的基础上试着修改。
Eclipse用户首先要做的是:下载jbpm-3.0-[version].zip 文件并解压到你的系统中。然后执行"File" --> "Import..." --> "Existing Project into Workspace",点击"Next"按钮,找到jBPM根目录点击"Finish"按钮。现在你的工作空间(workspace)中已经有一个jbpm.3 的项目了。现在就可以在src/java.examples/ 文件夹下找到教程的例子,打开这些例子时,可以使用 "Run" --> "Run As..." --> "JUnit Test" 方式来运行它们。
jBPM包含一个图形化设计工具来创作例子中的XML文件。你可以找到下载说明在 2.1下载包概述。你不需要这个图形化设计工具来完成本教程。
3.1 Hello World 实例
流程定义是一个定向图,由节点和流程转向组成。hello world流程中有3个节点,来看下如何将他们放在一起,我们先不用流程设计工具来开始一个简单流程。下面的图形显示了hello world流程的图示。
图 3-1 hello world 流程图
package org.zym;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.exe.Token;
import junit.framework.TestCase;
public class HelloWorld extends TestCase {
public void testHelloWorldProcess() {
// 这个方法显示一个流程定义和一个流程定义的执行。流程定义有三个节点:
// 未命名的start-state, state 's' 和 命名为 'end' 的 end-state 节点。
// 下一行代码解析 xml 文本到 ProcessDefinition 类,
// ProcessDefinition 是作为java对象的的流程的正规描述
ProcessDefinition processDefinition = ProcessDefinition
.parseXmlString("<process-definition>" + " <start-state>"
+ " <transition to='s' />" + " </start-state>"
+ " <state name='s'>" + " <transition to='end' />"
+ " </state>" + " <end-state name='end' />"
+ "</process-definition>");
// 下一行建立一个流程定义的执行,在创建后,流程执行有一个执行的主路径
// (=root token),定位在start-state节点位置。
ProcessInstance processInstance = new ProcessInstance(processDefinition);
// 创建流程执行的主路径 (=root token)
Token token = processInstance.getRootToken();
// 在创建流程定义后,确定主路径定位在 开始状态节点
assertSame(processDefinition.getStartState(), token.getNode());
// 让我们开始流程执行,离开开始状态节点通过它的缺省流程转向
token.signal();
// signal 方法将阻止直到流程执行进入一个等待状态
// 流程执行将已经进入了这第一个等待状态,状态 's' 节点。
// 所以执行的主路径现在定位在状态's'
assertSame(processDefinition.getNode("s"), token.getNode());
// 让我们发送另一个信号。假设执行离开状态's'通过缺省的流程转向
token.signal();
// 现在信息方法返回,因为这个流程实例已经到达结束节点(end-state)
assertSame(processDefinition.getNode("end"), token.getNode());
}
}
译者注:以上的代码是可执行的Junit测试用例程序,复制到eclipse中即可执行。与原文档的代码有所差异。但主测试方法是完全一样的。
3.2 Database 实例
jBPM的基本属性之一就是流程在等待状态时,能够将流程执行的状态在数据库中进行持久化。下一个实例将向你显示如何在jBPM数据库中存储一个流程实例。
实例也暗示了可能遇到的上下文。独立的方法被用来创建用户代码的不同部分。例如:Web应用中的一处代码开始流程并在数据库中持久化执行。后来,一个消息驱动bean从数据库中加载流程实例并恢复它的执行。
更多的关于jBPM持久性的信息能在 第7章 持久性 中找到。
package org.zym;
import java.util.List;
import junit.framework.TestCase;
import org.jbpm.JbpmConfiguration;
import org.jbpm.JbpmContext;
import org.jbpm.db.GraphSession;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.exe.Token;
public class HelloWorldDbTest extends TestCase {
static JbpmConfiguration jbpmConfiguration = null;
static {
// 实例配置文件,例如此文件能够在 src/config.files 文件夹下找到。
// 正常的情况下此配置文件的信息是在 jbpm.cfg.xml 资源文件中,
// 但这里我们放配置文件信息作为一个XML字符串
// 首先我们创建 JbpmConfiguration 静态变量。
// 一个 JbpmConfiguration 能够用于系统中的所有线程,那就是我们让它是static类型的原因。
jbpmConfiguration = JbpmConfiguration.parseXmlString(
"<jbpm-configuration>" +
// jbpm-context 上下文机制把jBPM核心引擎和jbpm使用的服务从环境中分开
" <jbpm-context>" +
" <service name='persistence' " +
" factory='org.jbpm.persistence.db.DbPersistenceServiceFactory' />" +
" </jbpm-context>" +
// 而且 jbpm 所使用的资源文件都参考 jbpm.cfg.xml 文件
" <string name='resource.hibernate.cfg.xml' " +
" value='hibernate.cfg.xml' />" +
" <string name='resource.business.calendar' " +
" value='org/jbpm/calendar/jbpm.business.calendar.properties' />" +
" <string name='resource.default.modules' " +
" value='org/jbpm/graph/def/jbpm.default.modules.properties' />" +
" <string name='resource.converter' " +
" value='org/jbpm/db/hibernate/jbpm.converter.properties' />" +
" <string name='resource.action.types' " +
" value='org/jbpm/graph/action/action.types.xml' />" +
" <string name='resource.node.types' " +
" value='org/jbpm/graph/node/node.types.xml' />" +
" <string name='resource.varmapping' " +
" value='org/jbpm/context/exe/jbpm.varmapping.xml' />" +
"</jbpm-configuration>"
);
}
public void setUp() {
jbpmConfiguration.createSchema();
}
public void tearDown() {
jbpmConfiguration.dropSchema();
}
public void testSimplePersistence() {
// 下面这3个方法调用中,所有数据都是通过数据库传递的。
// 在这个单元测试里,因为我们想要测试一个完整的流程环境,所以这3个方法在交互后被执行。
// 但实际上,这些方法代表了对服务器的不同的请求。
// 因为我们开始的是一个干净的、空的内存数据库,所以我们不得不先部署流程。
// 实际上,这个以前是由流程开发人员来完成的。
deployProcessDefinition();
// 假设我们想开始一个流程实例(=流程执行)当用户在web应用中提交一个表单时...
processInstanceIsCreatedWhenUserSubmitsWebappForm();
// 然后,异步消息到来流程继续执行
theProcessInstanceContinuesWhenAnAsyncMessageIsReceived();
}
public void deployProcessDefinition() {
// 这个测试显示流程定义和一个流程定义的执行。流程有三个节点:
// 未命名的开始状态、状态's'和一个命名为'end'的结束状态
ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(
"<process-definition name='hello world'>" +
" <start-state>" +
" <transition to='s' />" +
" </start-state>" +
" <state name='s'>" +
" <transition to='end' />" +
" </state>" +
" <end-state name='end' />" +
"</process-definition>"
);
// 查找上面配置的pojo持久性上下文生成器
JbpmContext jbpmContext = jbpmConfiguration.createJbpmContext();
try {
// 在数据库中部署流程定义
jbpmContext.deployProcessDefinition(processDefinition);
} finally {
// 拆毁 pojo 持久性上下文。
// 这里包含刷新SQL并插入流程定义到数据库中
jbpmContext.close();
}
}
public void processInstanceIsCreatedWhenUserSubmitsWebappForm() {
// 在方法里的代码能够放在struts-ation或JSF管理bean中。
// 查找上面配置的pojo持久性上下文生成器
JbpmContext jbpmContext = jbpmConfiguration.createJbpmContext();
try {
GraphSession graphSession = jbpmContext.getGraphSession();
ProcessDefinition processDefinition =
graphSession.findLatestProcessDefinition("hello world");
// 用从数据库中检索到的processDefinition,
// 我们能创建一个像在hello world例子中那样的流程定义执行(那个例子是持久性的)
ProcessInstance processInstance =
new ProcessInstance(processDefinition);
Token token = processInstance.getRootToken();
assertEquals("start", token.getNode().getName());
// 让我们开始流程执行
token.signal();
// 现在流程是在状态's'。
assertEquals("s", token.getNode().getName());
// 现在 processInstance 被存储在数据库中。所以流程执行的当前状态被存储在数据库中。
jbpmContext.save(processInstance);
// 下面的方法将从数据库中取出流程实例,并通过提供的另一个外部信号恢复执行
} finally {
// 拆毁pojo持久性上下文。
jbpmContext.close();
}
}
public void theProcessInstanceContinuesWhenAnAsyncMessageIsReceived() {
// 方法里的代码能够成为消息驱动Bean的内容.
// 查找上面配置的pojo持久性上下文生成器
JbpmContext jbpmContext = jbpmConfiguration.createJbpmContext();
try {
GraphSession graphSession = jbpmContext.getGraphSession();
// 首先,我们必须得到数据库中取出的流程实例.有几个选项是我们要知道的
// 什么样的流程实例我们正在处理的.在这个简单测试用例是最容易的,因为只是查找流程实例的所有列表.
// 那样就只给我们一个结果.所以让我们查看流程定义.
ProcessDefinition processDefinition =
graphSession.findLatestProcessDefinition("hello world");
// 我们搜索流程定义的所有流程实例
List processInstances =
graphSession.findProcessInstances(processDefinition.getId());
// 因为我们知道这个单元测试的上下文里,只有一个执行。在实际的环境中,processInstanceId
// 能够从到达的消息内容或从用户做的选择中进行提取。
ProcessInstance processInstance =
(ProcessInstance) processInstances.get(0);
// 现在我们继续执行,注意那个 processInstance 代理信号到达执行主路径(=root token)。
processInstance.signal();
// 在这个信号后,我们知道流程执行应该已到了结束状态
assertTrue(processInstance.hasEnded());
// 现在我们可以更新数据库中的执行状态
jbpmContext.save(processInstance);
} finally {
// 拆毁pojo持久性上下文。
jbpmContext.close();
}
}
}
译者注:以上的代码是可执行的Junit测试用例程序,复制到eclipse中即可执行。与原文档的代码有所差异。但主测试方法是完全一样的。
3.3 Context 实例:流程变量
流程变量包含流程执行期间的上下文信息。流程变量同java.util.Map类相似,映射变量命名到值,这个值是java对象。流程变量作为流程实例的一部分被持久化。为了保持简单,这个例子中我们只显示同变量一起工作的API,而没有持久性。
更多关于变量的信息能够在 第11章 上下文 中找到。
package org.zym;
import junit.framework.TestCase;
import org.jbpm.context.exe.ContextInstance;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ProcessInstance;
public class ProcessVaribleTest extends TestCase {
public void testProcessVaribles(){
// 这个例子还是从hello world流程开始,这次甚至没有修改.
ProcessDefinition processDefinition = ProcessDefinition.parseXmlString(
"<process-definition>" +
" <start-state>" +
" <transition to='s' />" +
" </start-state>" +
" <state name='s'>" +
" <transition to='end' />" +
" </state>" +
" <end-state name='end' />" +
"</process-definition>"
);
ProcessInstance processInstance =
new ProcessInstance(processDefinition);
// 从流程实例中取得同流程变量一起工作的上下文实例
ContextInstance contextInstance =
processInstance.getContextInstance();
// 在流程已经离开开始状态之前,我们将在流程实例的上下文中设置一些变量.
contextInstance.setVariable("amount", new Integer(500));
contextInstance.setVariable("reason", "i met my deadline");
// 从现在开始,这些变量同流程实例关联在一起.
// 流程变量现在可以通过显示的API被用户代码来访问.
// 流程变量也可以作为流程实例的一部分存储到数据库中.
processInstance.signal();
// 通过上下文实例是可以可以访问变量的
assertEquals(new Integer(500),
contextInstance.getVariable("amount"));
assertEquals("i met my deadline",
contextInstance.getVariable("reason"));
}
}
译者注:以上的代码是可执行的Junit测试用例程序,复制到eclipse中即可执行。与原文档的代码有所差异。但主测试方法是完全一样的。
3.4 Task 分配实例
下一实例中我们将显示你如何能够给用户分配任务。因为工作流引擎和组织结构模型是分开的,这样用来计算参与者的表达式语言总是会受到限制。所以,你不得不指定AssignmentHandler 的实现来计算任务的参与者。
package org.zym;
import junit.framework.TestCase;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.exe.Token;
import org.jbpm.taskmgmt.exe.TaskInstance;
public class TaskAssignmentTest extends TestCase {
public void testTaskAssignment() {
// 下面显示的流程基于hello world流程的.状态节点被任务节点替换.
// 任务节点在JPDL中代表在流程继续执行前的等待状态和生成将要完成的任务的一个结点
ProcessDefinition processDefinition = ProcessDefinition
.parseXmlString("<process-definition name='the baby process'>"
+ " <start-state>"
+ " <transition name='baby cries' to='t' />"
+ " </start-state>"
+ " <task-node name='t'>"
+ " <task name='change nappy'>"
+ " <assignment class='org.jbpm.tutorial.taskmgmt.NappyAssignmentHandler' />"
+ " </task>" + " <transition to='end' />"
+ " </task-node>" + " <end-state name='end' />"
+ "</process-definition>");
// 建立一个流程定义的执行
ProcessInstance processInstance = new ProcessInstance(processDefinition);
Token token = processInstance.getRootToken();
// 让我们开始流程执行,通过缺省的流程转向离开开始状态(start-state)
token.signal();
// 信号将阻塞流程执行,并进入等待状态,在本例中,也就是任务结点(task-node).
assertSame(processDefinition.getNode("t"), token.getNode());
// 当流程执行到任务结点时,任务"换尿布"被创建了,并且 NappyAssignmentHandler 被调用
// 来检查任务应该分配给谁. NappyAssignmentHandler 返回 'papa'.
// 在真实的环境中,任务将由 org.jbpm.db.TaskMgmtSession 类中的方法从数据库中被捕获.
// 因为我们不想将持久化的复杂性引入此例,我们只是处理流程实例的第一个任务实例
// (我们知道在测试环境中也只有一个)
TaskInstance taskInstance = (TaskInstance) processInstance
.getTaskMgmtInstance().getTaskInstances().iterator().next();
// 现在我们检查任务实例是否真正的赋给了 'papa'.
assertEquals("papa", taskInstance.getActorId());
// 现在我们假设爸爸已经完成了任务并标记任务完成了
taskInstance.end();
// 由于这是上一个任务要做的,任务触发继续流程实例执行.
assertSame(processDefinition.getNode("end"), token.getNode());
}
}
译者注:以上的代码是可执行的Junit测试用例程序,复制到eclipse中即可执行。与原文档的代码有所差异。但主测试方法是完全一样的。
译者增加* 由于此处使用了自定义的参与者赋值处理类,此类可以在根目录的examples下找到,此例的源代码如下:
/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jbpm.tutorial.taskmgmt;
import org.jbpm.graph.exe.*;
import org.jbpm.taskmgmt.def.*;
import org.jbpm.taskmgmt.exe.Assignable;
public class NappyAssignmentHandler implements AssignmentHandler {
private static final long serialVersionUID = 1L;
public void assign(Assignable assignable, ExecutionContext executionContext) {
assignable.setActorId("papa");
}
}
3.5 自定义动作实例
动作(Action)是绑定自定义的java代码到jbpm流程的一个机制。动作可以用它自己的结点进行关联(假设他们在流程的图示中是相关的)。或者动作要可以放在事件(event)上,例如:流程转向、离开结点或进入结点,如果那样的话,动作不会是图示的一部分,但在运行时流程执行击发事件执行时也会执行。
我们来看下我们例子中的动作的实现:MyActionHandler。这个动作实现程序并没有真正的做什么实际的事…,它只是设置了 isExecuted 这个布尔变量的值为真(true)。变量 isExecuted 是静态的,以至于能够在动作处理内部访问,同时来校验它的值。
更多的关于动作的信息可以在 10.5 动作 节中找到。
package org.jbpm.tutorial.action;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.exe.ExecutionContext;
// MyActionHandler 代表一个在流程执行期间被用户代码执行的类
public class MyActionHandler implements ActionHandler {
// 测试每个测试开始前,设置 isExecuted 值为 false
public static boolean isExecuted = false;
// 动作设置 isExecuted 值为 true,以至于当动作正在被执行时单元测试中能够显示
public void execute(ExecutionContext executionContext) throws Exception {
isExecuted = true;
}
}
译者注:以上的代码是可执行的Junit测试用例程序,复制到eclipse中即可执行。与原文档的代码有所差异。但主测试方法是完全一样的。
前面提到过,在每个测试开始前,我们将设置静态属性MyActionHandler.isExecuted的值为假(fasle)。
// 每个测试将要开始时会设置 MyActionHandler 的静态成员 isExecuted 的值为 false
public void setUp() {
MyActionHandler.isExecuted = false;
}
我们将在转向中开始一个动作。
package org.zym;
import junit.framework.TestCase;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.tutorial.action.MyActionHandler;
public class TransitionActionTest extends TestCase {
// 每个测试将要开始时会设置 MyActionHandler 的静态成员 isExecuted 的值为 false
public void setUp() {
MyActionHandler.isExecuted = false;
}
public void testTransitionAction() {
// 下一个流程是hello world流程的一个变体.
// 我们在状态's'到结束状态的转向上增加了一个动作.
// 这个测试的目的是展示下在 jBPM 流程中集成 java 代码时多么的容易
ProcessDefinition processDefinition = ProcessDefinition
.parseXmlString("<process-definition>"
+ "<start-state>"
+ "<transition to='s' />"
+ "</start-state>"
+ "<state name='s'>"
+ "<transition to='end'>"
+ "<action class='org.jbpm.tutorial.action.MyActionHandler' />"
+ "</transition>" + "</state>"
+ "<end-state name='end' />" + "</process-definition>");
// 开始一个新的流程定义的执行.
ProcessInstance processInstance = new ProcessInstance(processDefinition);
// 下一个信号将导致执行离开开始状态并进入状态's'.
processInstance.signal();
// 此处显示 MyActionHandler 还没有被执行
assertFalse(MyActionHandler.isExecuted);
// 而且执行的主路径被定位在状态 's' 处
assertSame(processDefinition.getNode("s"), processInstance
.getRootToken().getNode());
// 下一个信号将触发根令牌执行,令牌携动作执行流程转向,而且在调用信号方法时将执行动作
processInstance.signal();
// 此处我们可以看到在调用信号方法时 MyActionHandler 被执行了
assertTrue(MyActionHandler.isExecuted);
}
}
译者注:以上的代码是可执行的Junit测试用例程序,复制到eclipse中即可执行。与原文档的代码有所差异。但主测试方法是完全一样的。
下个例子显示了相同的动作,但现在这个动作分别被放在了进入结点和离开结点事件中。注意这个结点同转向相比已经有了更多的事件类型,而那个只有一个事件。因此置于一个结点上的动作应该放在一个事件元素上。
package org.zym;
import junit.framework.TestCase;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.tutorial.action.MyActionHandler;
public class NodeEventActionTest extends TestCase {
// 每个测试将要开始时会设置 MyActionHandler 的静态成员 isExecuted 的值为 false
public void setUp() {
MyActionHandler.isExecuted = false;
}
public void testNodeEventAction() {
ProcessDefinition processDefinition = ProcessDefinition
.parseXmlString("<process-definition>"
+ " <start-state>"
+ " <transition to='s' />"
+ " </start-state>"
+ " <state name='s'>"
+ " <event type='node-enter'>"
+ " <action class='org.jbpm.tutorial.action.MyActionHandler' />"
+ " </event>"
+ " <event type='node-leave'>"
+ " <action class='org.jbpm.tutorial.action.MyActionHandler' />"
+ " </event>" + " <transition to='end'/>"
+ " </state>" + " <end-state name='end' />"
+ "</process-definition>");
ProcessInstance processInstance = new ProcessInstance(processDefinition);
assertFalse(MyActionHandler.isExecuted);
// 下一个信号将导致流程执行离开开始状态并进入状态's'.
// 以至于进入状态's',此后开始执行动作
processInstance.signal();
assertTrue(MyActionHandler.isExecuted);
// 重置 MyActionHandler.isExecuted
MyActionHandler.isExecuted = false;
// 下一个信号将触发流程执行,离开状态's',所以这个动作将再次被执行
processInstance.signal();
// 瞧 :~
assertTrue(MyActionHandler.isExecuted);
}
}
译者注:以上的代码是可执行的Junit测试用例程序,复制到eclipse中即可执行。与原文档的代码有所差异。但主测试方法是完全一样的。
Now,chapter has been got! :)