引言:

当我们定义完流程之后,流程定义在运行时会被实例化,因此我们要创建流程实例;

当流程实例在执行中,我们要控制和监视流程,以确保业务流程执行在监控之中;

当流程实例执行完毕,JBPM4会将其归档到"历史流程"中去,从而提高运行中流程实例的执行效率;

。。。。等等

这些都需要依赖JBPM4提供的Service API来帮助我们来实现:

其中包括:

  • 管理流程部署

  • 管理流程实例

  • 管理流程任务

  • 管理流程历史

  • 以及管理流程的一切

  • 。。。。。


一、流程定义、流程实例和执行的概念

流程定义是对业务过程步骤的描述,在JBPM4中它表现为若干"活动"节点通过"转移"线条串联。

例如:一个金融公司有一个信贷流程定义,如下:

wKiom1j41B_SYJ3iAAAunJW2pXU196.png

一个流程实例在其生命周期中,最典型的特征就是具有指向当前执行活动的指针——"执行"。

而流程实例支持"并行"执行,所以在同一个流程实例中的执行数量并非绝对唯一。

wKiom1j41djDPgahAAA2VrXETA4996.png

一般情况下,一个流程实例可以理解为一颗"执行树",当一个流程实例启动时,最初的执行处于这棵树的根节点位置,之后可以根据定义的需要产生子执行,即树枝。

JBPM使用树状执行结构的原因在于:这个概念实际上只有一条执行路径,所以子执行终归于(join)根执行,这样流程执行模型的实现和使用就更简单、更容易理解了。


二、流程引擎API

流程引擎对象——org.jbpm.api.ProcessEngine是JBPM4所有Service API之源。

在JBPM4中各种服务相互依存,但所有的Service API都从ProcessEngine中获得,ProcessEngine是由Configuration类创建的。

ProcessEngine是线程安全的,因此它可以保存在静态变量中,甚至JNDI命名服务中或者其他的重要位置,在应用中所有线程和请求都可以使用同一个ProcessEngine对象。

ProcessEngine processEngine = Configuration.getProcessEngine();

如果要指定其他位置的jbpm配置文件?

ProcessEngine processEngine = new Configuration()
.setResource("my-jbpm-configuration-file.xml").buildProcessEngine();

当然还有其他的setXxxx()方法可以获得jbpm的配置内容,例如从InputStream、xml字符串、InputSource、url、文件中等。

我们可以根据ProcessEngine得到如下的服务:

//流程资源服务的接口。提供对流程定义的部署、查询、删除等操作
RepositoryService getRepositoryService();

//流程执行服务的接口。提供启动流程实例、"执行"推进、设置流程变量等操作
ExecutionService getExecutionService();

//流程历史服务的接口。提供对流程历史库中流程流程实例、历史活动等记录的查询
HistoryService getHistoryService();

//人工任务服务的接口。提供对任务的创建、提交、查询、保存、删除等操作
TaskService getTaskService();

//身份认证服务的接口。提供对流程用户、用户组以及组成员关系的相关服务
IdentityService getIdentityService();

//流程管理控制服务的接口。提供异步工作(Job)相关的执行和查询操作
ManagementService getManagementService();

CommandService,这是一个特殊的Service API,例如:

ManagementService的实现ManagementServiceImple来说,则继承了AbstractServiceImple类,事实上以上6个Service API都继承了AbstractServiceImpl类:

wKiom1kCAejy62aVAAAkldABviQ632.jpg

而AbstractServiceImpl依赖CommandService,

wKioL1kCAkbjDaBlAABelepucSY410.jpg

看到以上代码,所谓的CommandService实际上就是"Command模式"的服务接口,就是讲客户端的请求全部封装在一个调用接口中,然后由这个接口去调用org.jbpm.api.cmd.Command接口的众多实现,例如:

wKiom1kCA2uhieV8AACmxhsbiAo840.jpg

这是典型的Command设计模式的应用


何为Command模式?为什么要采用Command模式?

抽象出待执行的动作以参数化某对象,可以用面向过程语言中的回调来理解这种参数化机制,所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用,可以说Command模式是回调机制的一个面向对象的替代品。
Command模式的目的即在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接受者可用一种与地址空间无关的方式表达,那么久可将负责该请求的命令对象传递给另一个不同的进程并在那里实现该请求。

Command模式的优势在于
    支持取消操作,Command的Execute操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中,可通过向后和向前遍历这一列表并分别调用Unexecute和Execute来实现次数不限的"取消"和"重做"。
    支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在Command接口中添加装载操作和存储操作,可以用来动态保持一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用Execute操作重新执行它们。
    用构建在原始操作上的高层操作构造一个系统,这样一种结构在支持事务的信息系统中很常见,一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command有一个公共的接口,使得用户可以用同一种方式调用所有的事务,同时使用该模式也易于添加新事务以扩展系统


三、利用API部署流程

RepositoryService提供了管理JBPM4发布资源的所有接口。

String deploymentid = repositoryService.createDeployment()
.addResourceFromClasspath("Order.jpdl.xml").deploy();

每次部署资源的内容都是以字节数组的形式,JPDL流程定义文件以拓展名.jpdl.xml被识别,其他资源包括任务表单、Java类、脚本等,如果不仅要部署.jpdl.xml流程定义文件,而且要部署一些列流程定义资源,则可以以流程定义归档的方式部署,流程引擎会自动识别归档中扩展名为.jpdl.xml的文件为流程定义文件。

在部署过程中,流程引擎会把一个ID分配给流程定义,这个ID格式为{key}-{version},即流程键和流程版本之间通过连字符链接。

如果流程定义未指定key,则会自动生成,同一个流程名称只能关联到一个可以


四、通过API删除已部署的流程

同样适用RepositoryService

repositoryService.deleteDeployment(deploymentId);

这个是物理删除,会在数据库中彻底销毁这条流程定义的记录。

异常情况:

1>要删除一个流程定义还存在还未完成的,则会抛出异常

2>如果希望级联删除一个已经发布的流程定义及其所产生的流程实例,可以使用RepositoryService的deleteDeploymentCascade方法



五、使用API发起新的流程实例

第一种:常规方法

ProcessInstance processInstance = executionService.startProcessInstanceByKey("ICL");

start...方法会查找key为ICL的最新版本流程定义,然后根据最新版本的ICL流程定义启动流程实例。当流程定义部署了一个新版本后,start...方法会自动切换到最新部署的流程定义版本。


如果想根据特定的流程定义版本发起流程实例,则可以使用流程定义的ID启动流程实例。如下:

ProcessInstance processInstance = executionService.startProcessInstanceById("ICL-1");


第二种:指定业务键发起流程实例

一个保险流程的实例必然需要和一个保险单号关联,以便将来在业务上的查询和索引,我们可以通过位新启动的流程实例分配一个业务键来做到。

一个业务键必须在此流程定义中是唯一的,例如:一个订单号、一个保险单号、一笔交易流水等。。。

ProcessInstance processInstance = 
executionService.startProcessInstanceByKey("ICL","CL92837");

"CL92837"是业务键,也许是流水号或是什么,总之只和特定的业务有关系。


业务键可以被用来创建流程实例的ID,格式为{processDefinitionKey}.{processInstanceKey}。

所以上面的代码会创建一个ID为"ICL.CL92837"的流程实例。

如果没有提供用户定义的业务键,数据库就会把流程定义主键作为Key。这样可以使用如下代码发起流程实例并获取流程实例ID:

ProcessInstance processInstance = executionService.startProcessInstanceByKey("ICL");
String pid = processInstance.getId();


第三种:指定变量发起流程实例

如果新的流程实例需要一些输入参数,则可以将这些参数放在流程变量中,然后在发起流程时传入流程变量对象,例如:

//创建并填充流程变量Map
Map<String,Object> variables = new HashMap<String,Object>();
variables.put("customerName","Alex Miler");
variables.put("type","Accident");
variables.put("amount",new Float(763.74));
//传入Map,带着流程变量发起流程实例
ProcessInstance processInstance = executionService.
startProcessInstanceByKey("ICL",variables);



六、唤醒一个等待状态的执行

当一个流程执行进入一个state活动时,执行(流程实例)会在到达state活动的时候进入等待状态——Wait State直到一个signal出现才能进入下一个步骤。

ExecutionService的signalExecution*方法可以用来发出signal.

为了获取正确的执行是给state活动分配一个事件监听器,定义如下(在*jpdl.xml中):

<state name="wait">
    <on event="start">
    	<event-listener class="org.jbpm.example.StartExternalWork"/>
    </on>
</state>

在事件监听器类StartExternalWork中,可以执行那些需要在此state活动中完成的工作,在这个事件监听器里,也可以通过execution.getId方法获得正确的执行ID。

有了这个执行ID,在state活动的工作完成之后,用它来发出signal,代码如下:

executionService.signalExecutionById(executionId);

还有第二种方式:(不建议采用

因为这种方式使得流程客户端实现和业务逻辑绑定的比较紧,如果不是特殊业务场景需要,不建议采用

//发起或获取流程实例
		ProcessInstance processInstance = 
				executionService.startProcessInstanceById(processDefinitionId);
		//或者
		ProcessInstance processInstance = 
				executionService.signalExecutionById(executionId);
		//如上述假设,我们知道当前流程实例在名为"external work"的活动中等待
		Execution execution = processInstance.findActiveExecutionIn("external work");
		
		//获取执行ID
		String executionId = execution.getId();



七、任务服务API

TaskService的主要目的是提供对任务列表的访问操作。

示例:

List<Task> taskList = taskService.findPersonalTasks("alex");

一般来说,任务会有一个表单,显示在一些用户界面接口中(例如JSP),这个表单需要读/写与任务相关的流程数据,一般是通过任务变量的方式,示例:

for (Task task : taskList) {
	String taskId = task.getId();
	Set<String> variableNames = taskService.getVariableNames(taskId);
	//读取任务变量
	Map<String, Object> variables = new HashMap<String, Object>();
	variables = taskService.getVariables(taskId, variableNames);
	//设置"键-值"形式的任务变量
	variables.put("category", "small");
	variables.put("lires", 923874893);
	//将变量存入任务
	taskService.setVariables(taskId, variables);
}
//TaskService 也用来完成任务,通过以下4中方式:
//根据指定的任务ID完成任务
taskService.completeTask(taskId);
//根据指定的任务ID,同时设入变量,完成任务
taskService.completeTask(taskId,variables);
//指定outcome,即下一步的转移路径,完成任务
taskService.completeTask(taskId,outcome);
//指定下一步的转移路径,同时设入变量,完成任务
taskService.completeTask(taskId,outcome,variables);

这些API允许设入变量Map,在任务完成前将作为流程变量同步到流程实例中。


在这些API中还有一个参数"outcome",这个参数可以用来决定任务完成后流向哪个流出"转移",任务完成后,流程将"何去何从",需要遵循如下的规则

1>如果任务拥有一个没有名称的流出转移:

a>taskService.getOutcomes(taskId);//返回包含一个null值的集合
b>taskService.completeTask(taskId);//会经过这个流出转移
c>taskService.completeTask(taskId,null);//会经过这个流程转移
d>taskService.completeTask(taskId,"anyvalue");//会抛出一个异常

2>如果任务拥有一个已命名为myName的流出转移:

a>taskService.getOutcomes(taskId);//返回包含这个流出转移的名称集合
b>taskService.completeTask(taskId);//会经过这个流出转移
c>taskService.completeTask(taskId,null);//会抛出一个异常,因为此任务没有无名称的流出转移
d>taskService.completeTask(taskId,"anyvalue");//会抛出一个异常
e>taskService.completeTask(taskId,"myName");//会经过这个流出转移

3>如果任务拥有多个流出转移,而其中一个没有名称,其他都有名称:

a>taskService.getOutcomes(taskId);//返回包含一个null值和其他流出转移名称的集合
b>taskService.completeTask(taskId);//会经过没有名称的流出转移
c>taskService.completeTask(taskId,null);//会经过没有名称的流出转移
d>taskService.completeTask(taskId,"anyvalue");//会抛出一个异常
e>taskService.completeTask(taskId,"myName");//会经过名称为myName的流出转移

4>如果任务拥有多个流出转移,且每个流出转移都拥有唯一的名称

a>taskService.getOutcomes(taskId);//返回包含所有流出转移名称的集合
b>taskService.completeTask(taskId);//会抛出一个异常,因为没有无名称的流出转移
c>taskService.completeTask(taskId,null);//会抛出一个异常,因为没有无名称的流出转移
d>taskService.completeTask(taskId,"anyvalue");//会抛出一个异常
e>taskService.completeTask(taskId,"myName");//会经过名称为myName的流出转移

注意:

    除非用户被分配到这个任务上,否则不能办理此任务,当然,如果实现了“代理人”机制除外。

    任务可以拥有多个候选人,候选人可以是单个用户也可以是用户组,用户可以接收候选人是自己的任务,接收任务的意思是用户会被流程引擎设置为任务的分配者,接收任务是个“排他”操作,因此在任务被“接收——分配”之后,其他的用户就不能接收并办理此任务了。

    用户接收任务后,一般需要客户端应用程序界面显示任务表单,并引导用户完成任务。


八、历史服务API

    在流程实例执行的过程中,会不断触发事件,通过这些事件,已完成流程实例的历史信息会被收集到流程历史数据表中,而HistoryService API提供了对这些历史信息的访问服务。

List<HistoryProcessInstance> history=
	historyService.createHistoryProcessInstanceQuery()
	//查询ID为"ICL-1"的流程定义
	.processDefinitionId("ICL-1")
	//返回的结果集按开始时间正序排列
	.orderAsc(HistoryProcessInstanceQuery.PROPERTY_STARTTIME)
	.list();

而历史的活动实例被作为HistoryActivityInstance保存到历史活动数据表中,通过如下API 查询:

List<HistoryActivityInstance> history =
		historyService.createHistoryActivityInstanceQuery()
		//查询ID为"ICL-1"的流程定义
		.processDefinitionId("ICL-1")
		//名称为"a"的活动实例
		.activityName("a")
		.list();


九、管理服务API

ManagementService即管理服务,通常用来管理Job(异步工作),

接口定义如下:

//执行指定ID的Job
void executeJob(String jobId);

//获取Job查询接口
JobQuery createJobQuery();

//删除Job
boolean deleteJob(long jobId);


JobQuery接口的功能比较丰富,定义如下:

//查询所有的消息型Job
JobQuery messages();

//查询所有的定时器Job
JobQuery timers();

//查询属于指定流程实例的Job
JobQuery processInstanceId(String processInstanceId);

//查询由于异常回滚而产生的Job
JobQuery exception(boolean hasException);

//将查询结果根据指定属性正序排列
JobQuery orderAsc(String property);

//将查询结果根据指定属性逆序排列
JobQuery orderDesc(String property);

//用于支持查询结果分页
JobQuery page(int firstResult, int maxResults);

//执行查询,返回Job列表
List<Job> list();

//执行查询,返回单个Job
Job uniqueResult();

//执行查询,返回结果集数量
long count();


十、查询服务API

查询服务API一般是基于在主要的JBPM概念实体上创建查询对象来实现的,包括流程实例、任务、流程历史等,例如:

/*
返回指定流程定义的所有未挂起的流程实例,结果集支持分页,获取前50条记录
*/
List<ProcessInstance> results = 
	//获取流程实例查询对象
	executionService.createProcessInstanceQuery()
	//指定流程定义ID
	.processDefinitionId("ICL-1")
	//设定"未挂起"为过滤条件
	.notSuspended()
	//分页
	.page(0, 50)
	//查询执行,获取结果集列表
	.list();

当然对任务的查询还可以使用类似的查询对象来实现:

List<Task> myTasks = taskService
	//获取任务查询对象
	.createTaskQuery()
	//指定流程实例ID
	.processInstanceId("pid")
	//分配给Alex的任务
	.assignee("Alex")
	//分页
	.page(100, 120)
	//根据日期逆向排序
	.orderDesc(TaskQuery.PROPERTY_DUEDATE)
	//查询执行,获得结果集列表
	.list();


十一、利用JBPM Service API完成流程实例

如何使用API完成流程定义、发起、执行整个流程并查询该流程实例的历史记录。

wKioL1kkPdSQAzmkAAAZl9JaESA906.jpg

对应的jPDL文件如下:

<?xml version="1.0" encoding="UTF-8"?>

<process name="process" xmlns="http://jbpm.org/4.4/jpdl">
   <start name="start" g="232,35,48,48">
      <transition name="to state" to="state" g="-49,-22"/>
   </start>
   <!-- state活动是等待活动,需要收到一个外部的执行信号才能流转通过 -->
   <state name="state" g="210,115,92,52">
      <transition name="to task" to="task" g="-45,-22"/>
   </state>
   <!-- task是等待活动, 分配个Alex,办理完成提交任务后才能流转通过-->
   <task assignee="Alex" name="task" g="210,199,92,52">
      <transition name="to end" to="end1" g="-50,-22"/>
   </task>
   <end name="end1" g="232,283,48,48"/>
</process>

单元测试如下(省略setUp/tearDown):

//使用执行服务:根据已部署流程定义的名称process,发起流程实例
ProcessInstance processInstance = executionService
		.startProcessInstanceByKey("process");
//获取流程实例ID
String pid = processInstance.getId();
//获取当前活动的执行对象
Execution executionInState = processInstance
		.findActiveExecutionIn("state");
//断言当前活动即为state
assertNotNull(executionInState);
//使用执行服务:发出执行信号结束当前活动,继续流程的执行
executionService.signalExecutionById(executionInState.getId());
//使用执行服务:从持久化层中获取"最新"的流程实例对象
processInstance = executionService
		.findProcessInstanceById(pid);
//获取当前活动的执行对象
Execution executionInTask = processInstance
		.findActiveExecutionIn("task");
//断言当前活动即为task
assertNotNull(executionInTask);
//使用任务服务:获取用户Alex的任务,即task活动产生的任务
Task task = taskService.findPersonalTasks("Alex").get(0);
//使用任务服务:完成任务
taskService.completeTask(task.getId());
//使用历史服务:创建历史任务查询
HistoryTask historyTask = historyService
		.createHistoryTaskQuery()
		.taskId(task.getId()).uniqueResult();
//断言上一步完成的任务已成为历史,即可通过历史任务查询
assertNotNull(historyTask);
//断言该流程实例已经结束
assertProcessInstanceEnded(pid);
//使用历史服务:创建历史流程实例查询
HistoryProcessInstance historyProcessInstance = 
		historyService.createHistoryProcessInstanceQuery()
		.processInstanceId(pid).uniqueResult();
//断言该流程实例已经成为历史,即可通过历史流程实例查询
assertNotNull(historyProcessInstance);