第12章 任务分配
jBPM的业务核心具有支持流程执行持久化的能力。这个属性对于任务管理和个人任务列表这种情况非常有用。jBPM允许指定一个软件片段去描述能够具有人员参与的等待状态任务的整个流程。
任务是流程定义的部分并且他们定义了在流程执行期间任务实例怎样被创建和分派。
任务可以在流程节点(task-nodes)和流程定义(process-definition)。那样的话任务节点代表一个将被用户和流程执行完成的任务应该等待直到参与者(actor)完成这个任务。当用户完成这个任务时,流程执行才能继续。当在任务节点(task-node)中被指定了多个任务时,默认的行为是等待完成所有的任务。
任务也能够被指定在流程定义中。在流程定义上指定的任务能够通过名称查找以及从任务节点内引用或用于动作内部。实际上,被命名的所有任务(也包含在任务节点里的)能够在流程定义中通过名称被查找到。
在整个流程定义中任务名称必须唯一。任务可以赋予一个优先级。这个优先级将被用作任务创建的每一个每个任务实例的初始优先级。TaskInstances后期可以改变这个优先级。
一个任务实例能够被分配一个actorId(java.lang.String)。所有任务实例被存储在数据库中的一个表里(JBPM_TASKINSTANCE)。通过使用一个给定的actorId查询这个表的所有任务实例,你能得到那个特定的用户的任务列表。
jBPM任务列表机制能够将其他的任务和jBPM的任务组合,即使那些任务和流程执行不相关。那样jBPM开发人员可以很容易地将jBPM-process-tasks和应用的任务组合在一个中央的任务列表仓库(task-list-repository)中。
任务实例生存期是容易理解的:在生成后,任务实例能够有选择的被开始。然后,任务实例能够被结束,那就意味着任务实例被标记这完成。
注意由于为了灵活性,分配(assignment)不再是生存同期的一部分。所以任务实例能够被分派或不分派。任务实例分配不会影响任务实例生存期。
任务实例典型的情况下是通过流程执行进入一个任务节点(使用TaskMgmtInstance.createTaskInstance(...)方法)来创建。然后,一个用户接口构件将使用TaskMgmtSession.findTaskInstancesByActorId(...)方法查询数据库获得用户列表。然后,从用户那里收集输入,UI构件调用TaskInstance.assign(String)、TaskInstance.start()或TaskInstance.end(...)方法。
任务实例用日期属性来维护它的状态:创建、开始和结束。那些属性可以被它们的各自TaskInstance上的getters访问。
现在,已经完成的任务实例使用一个结束日期标记,以至于他们不会再被随后的任务列表的查询捕获。但他们还保持在JBPM_TASKINSTANCE表中。
任务实例是参与者的任务列表中的条目。任务实例能够发信号(signalling)。一个发信号的任务实例是当它完成时,能够送信号到它的令牌来继续流程执行的一个任务实例,。任务实例能够阻塞(blocking),意味着相关的令牌(等于执行路径)在任务实例完成前不允许离开这个任务节点。缺省情况下任务实例是发信号(signalling)而非阻塞的(non-blocking)。
万一多个任务实例同这个任务节点相关联的,流程开发人员能够指定如何来完成任务实例去影响流程继续(continuation)。下列是能够赋予一个任务节点的signal-property的值的列表:
l last:这个是默认值。当上一个任务实例完成时继续进行执行。当在节点的入口上没有任务被生成时,执行继续。
l last-wait:继续进行执行当上一个任务实例被完成时。当在节点的入口上没有任务被生成时,执行在任务节点里等待直到任务被创建。
l first:继续进行执行当第一个任务实例完成时。当没有任务在这个节点的入口上创建时,执行继续。
l first-wait: 继续进行执行当第一个任务实例完成时。当没有任务在这个节点的入口上创建时,执行在任务节点里等待直到任务被创建。
l unsynchronized: 执行总是继续。不管任务被创建或仍然未完成。
l never: 执行从不继续,不管任务被创建或仍然未完成。
任务实例创建也许基于运行时的计算上。那样的话,增加一个ActionHandler在任务节点的节点进入(node-enter)事件上并设置属性create-tasks="false"。这里是一个这样的一个动作处理实现的例子:
public class CreateTasks implements ActionHandler { public void execute(ExecutionContext executionContext) throws Exception { Token token = executionContext.getToken(); TaskMgmtInstance tmi = executionContext.getTaskMgmtInstance(); TaskNode taskNode = (TaskNode) executionContext.getNode(); Task changeNappy = taskNode.getTask("change nappy");
// now, 2 task instances are created for the same task. tmi.createTaskInstance(changeNappy, token); tmi.createTaskInstance(changeNappy, token);
}
}
|
像例子中显示的那样任务能够被创建能够在在这个任务节点上指定。他们本来也应该被指定在流程定义里并且从TaskMgmtDefinition上捕获。TaskMgmtDefinition用任务管理信息扩展ProcessDefinition。
在完成时标记任务实例的API方法是TaskInstance.end()。可选择的,你可以指定一个转换在end方法中。这样的为期不远任务实例的完成会触发执行的继续,任务节点留下特殊的转换。
12.3. 分派
一个流程定义包含任务节点。一个任务节点包含0或多个任务。任务是一个静态的流程定义部分的描述。在运行时,任务引起任务实例的生成。在任务实例相当于一个个人任务列表的入口。
对于jBPM,任务分派的push (personal task list) 和 pull (group task list)模型(往下看)可以应用在组合中。流程能够计算一个任务并将这个任务推进他/她的任务列表中。或可选的,任务能够被分配给一个参与者池(pool of actors),在那种情况下池中的每个参与者都能够将这个任务推进他/她的任务列表中。
分派的任务实例通过
AssignmentHandler接口完成:
public interface AssignmentHandler extends Serializable { void assign( Assignable assignable, ExecutionContext executionContext );
}
|
当一个任务实例创建时分派处理程序实现被调用。在那个运行时,任务实例能够被分配给一个或多个参与者。AssignmentHandler实现应该调用Assignable方法(setActorId or setPooledActors)来分配一个任务。这个Assignable既是一个TaskInstance也是一个SwimlaneInstance(等于流程角色)。
public interface Assignable { public void setActorId(String actorId); public void setPooledActors(String[] pooledActors);
}
|
TaskInstances和SwimlaneInstances能够被分配给一个特定的用户或一例参与者池。分配TaskInstance给用户,需要调用Assignable.setActorId(String actorId)方法。而分配TaskInstance给候选参与者池,要调用Assignable.setPooledActors(String[] actorIds)方法。
每一个流程定义的任务能够同一个分派处理程序实现相关联来在运行时执行分派。
当多个流程任务应该被分配给相同的人或参与者组时,可以考虑使用swimlane。
为了允许重用AssignmentHandlers的生成,每个AssignmentHandler的使用可以在processdefinition.xml中配置。21.2 委托部分有更多关于如何给assignment handlers增加配置的信息。
管理任务实例分派的数据模型和参与者的泳道实例如下。每个TaskInstance有一个actorId和一个pooled actors集合。
图12-1 分派模型类图
actorId对任务负责,而池化的参与者集则代表一个他们是否处理这个任务的职责的候选人集合。actorId和pooledActors是可选的同时也是可以组合的。
个人任务列表表示所有已经分配给特定的人的任务实例。这可以使用TaskInstance上的属性actorId来显示。所以为了放一个TaskInstance到某人的个人任务列表上,你只需使用下列一个方法:
l 在流程的任务元素的actor-id属性上指定一个表达式
l 在你的代码的任何地方使用TaskInstance.setActorId(String)
l 在一个AssignmentHandler中使用assignable.setActorId(String)
一个给定的用户要捕获个人任务列表,使用TaskMgmtSession.findTaskInstances(String actorId)。
池化的参与者(pooled actors)表示任务实例的候选人。这就意味着任务被提供给许多用户并且一个候选人不得不介入并处理任务。用户不能立即他们的群组任务列表开始一个任务。那也就将导致一个潜在的多个人开始相同的任务的冲突的可能。为了限制这个冲突,用户可以只处理他们群组任务列表中的任务实例并且移动他们进入个人任务列表。用户只允许用他们个人任务列表上的任务来开始工作。
为了在某人的群组任务列表上放一个taskInstance ,你必须放置用户的actorId或一个groupIds到pooledActorIds中。指定这个池化的用户,使用下列中的一个:
l 在流程的task元素的pooled-actor-ids属性中指定一个表达式
l 在你的代码的任何地方使用TaskInstance.setPooledActorIds(String[])代码
l 在一个AssignmentHandler中使用assignable.setPooledActorIds(String[])
为给定的用户捕获群组任务列表,处理以下内容:制造一个包含用户的actorId和发表于这个用户的群组的所有的ids的集合。用TaskMgmtSession.findPooledTaskInstances(String actorId)或TaskMgmtSession.findPooledTaskInstances(List actorIds)你可以搜索不在个人任务列表中((actorId==null))的任务实例并且在池化的actorIds中可以 匹配。
这样做的背后的动机是我们想从jBPM任务分派中分离身份构件。jBPM只存储String作为actorIds而不需要知道用户间的关系、群组以及其他的身份信息。
actorId将总重载池化参与者。所以有一个actorId和pooledActorIds列表的taskInstance,将只需暴露在参与者的个人任务列表中。pooledActorIds允许用户通过删除taskInstance 的actorId属性来将一个任务实例放回到群组中。
12.4. 任务实例变量
任务实例可以有它自己的变量集和任务实例,也能“查看”流程变量。任务实例通常在一个执行路径(也就是令牌,token)上创建。这会创建一个父子关系在令牌和任务实例间,这同令牌他们自己的父子关系相类似。正常的范围规则应用在任务变量和相关的令牌的流程变量间。更多的关于范围的信息能够在11.4 变量范围部分找到。
这就意味着任务实例能够“看到”它自己的变量加上所有的它关联的令牌的变量。
控制器可以用于在任务实例范围和流程范围变量之间创建填入和提交变量。
12.5. 任务控制器
在任务实例创建时,任务控制器可能填入任务实例变量而且当任务实例完成时,任务控制器能够提交任务实例的数据进入流程变量。
注意不会强迫您使用任务管理器。任务实例也能“看到”和它的令牌相关的流程变量。使用任务控制器:
- 创建任务实例的变量的拷贝,防止流程完成前中间媒介更新任务实例变量影响流程变量,然后拷贝被提交给流程变量。
- 任务实例变量不是和流程变量一对一相关的。例如,假设流程变量'sales in januari'、'sales in februari'和'sales in march'。然后流程实例的表单也许需要显示3个月的销售平均值。
任务计划从用户那里收集输入。但有许多用户接口能够用于向用户表示这个任务。例如:web应用、swing应用、即时消息、邮件表单等等。所以任务控制器成为流程变量(流程上下文)和用户接口间的桥梁。任务控制器提供一个到用户接口应用的的流程变量的视图。
当任务实例被创建时,任务控制器负责提取信息从洛变量并创建任务变量。任务变量作为用户接口表单的输入。用户输入可以存储在任务变量中,当用户结束任务时,一个任务控制器负责更新流程变量基于任务实例数据。
图12-2 任务控制器
在一个简单的场景中,在表单参数和流程变量间有一对一的映射。任务控制器被指定在task元素里。那样的话,缺省的jBPM任务控制器可以被使用并且它获得元素内部变量元素列表。变量元素表示流程变量如何在任务变量中被复制。
下个例子显示你怎么才能基于流程变量创建独立的任务实例变量复本:
<task name="clean ceiling"> <controller> <variable name="a" access="read" mapped-name="x" /> <variable name="b" access="read,write,required" mapped-name="y" /> <variable name="c" access="read,write" /> </controller>
</task>
|
name属性引用了流程变量的名称。mapped-name是可选并且引用任务实例变量的名称。如果mapped-name属性被忽略了,mapped-name缺省使用name属性。注意mapped-name也用作web应用中的任务实例表单的域的标签。
Access属性指定是否在任务实例创建时复制变量,及是否要在任务结束时写回到流程变量。这个信息可以被用于用户接口来生成适当的表单控制。access属性是可选的而且缺省的访问是“读,写”。
一个task-node可以有多个任务,start-state有一个任务。
如果简单的流程变量和表单参数之间的一对一映射有太多限制的话,你也可以写自己的TaskControllerHandler实现。这是TaskControllerHandler接口:
public interface TaskControllerHandler extends Serializable { void initializeTaskVariables(TaskInstance taskInstance, ContextInstance contextInstance, Token token); void submitTaskVariables(TaskInstance taskInstance, ContextInstance contextInstance, Token token);
}
|
这里是如何在task中配置自定义的任务控制器实现:
<task name="clean ceiling"> <controller class="com.yourcom.CleanCeilingTaskControllerHandler">
-- 写你自己的任务控制器程序的配置—
</controller>
</task>
|
泳道(swimlane)是一个流程角色(role)。它是指定流程中通过相同的参与者(actor)来完成的多个任务的一个机制。所以在为给定的泳道创建第一个任务实例后,参与者将同一个泳道上随后所有任务记在流程中。泳道因此会有一个分派(assignment),而且所有引用泳道的任务不需要指定一个分派(assignment)。
当第一个任务在给定泳道上创建时,泳道的AssignmentHandler被调用。Assignable传递给AssignmentHandler的是SwimlaneInstance。重要的是要知道所有的在给定的泳道上的任务实例完成的分派将传播到泳道实例。这个行为缺省被实现的,因为获得任务的去实现某个流程角色的人将有特定的流程的知识。所以那个泳道所有随后的任务实例的分派为那个用户自动地完成。
泳道是从UML活动图中借用的术语。
泳道能够和开始任务相关联去获得流程启动程序。
一个任务能够在一个开始状态中被指定。那个任务同一个泳道相关联。当一个新的任务实例为这样一个任务创建时,当前的认证参与者将使用Authentication.getAuthenticatedActorId()方法被捕获并且那个参考者将被存储在开始任务的泳道中。
例如:
<process-definition>
<swimlane name='initiator' /> <start-state> <task swimlane='initiator' /> <transition to='...' /> </start-state>
...
</process-definition>
|
任务能让动作和他们相关联起来。有4种标准事件类型定义任务:
task-create、
task-assign、task-start和
task-end。
task-create在任务实例创建时被触发。
task-assign在任务实例正在被分配时被触发。注意在这个事件上被执行的动作里,你能使用executionContext.getTaskInstance().getPreviousActorId()访问上一个参与者。
task-start当TaskInstance.start()被调用时触发。这个可以用来显示用户真正地开始做这个任务实例。开始一个任务是可选的。
task-end当TaskInstance.end(…)被调用时触发。这个标记任务的完成。如果任务关联流程执行,那么这个调用可能触发流程执行的恢复。
既然任务能够让事件和动作同他们相关联,那么异常程序也能指定在任务上。更多的关于异常处理的信息,查看10.7 异常处理部分。
关于任务的定时器特殊的事情是那个任务定时器
cancel-event可以被定制。缺省的,任务上的定时器在任务结束(等于完成)时将被取消。但是用定时器上的cancel-event属性,流程开发人员可以定制那个,例如task-assign或task-start。cancel-event类型可以被组合通过指定它们在这个属性里的以逗号分隔的列表。
12.10. 定制任务实例
任务实例能够被定制。做这个最容易的方式是去创建一个TaskInstance的子类。然后创建一个org.jbpm.taskmgmt.TaskInstanceFactory实现并在jbpm.cfg.xml中配置它通过设置配置属性jbpm.task.instance.factory到完全限定类名。如果你使用一个TaskInstance的子类,也要为子类(使用hibernate extends="org.jbpm.taskmgmt.exe.TaskInstance")创建一个hibernate的映射文件。然后在hibernate.cfg.xml文件中增加映射文件到映射文件的列表。
用户、群组和权限的管理通常称为身份管理。jBPM包含一个可选的身份构件,它能够很容易地用公司自己的身份数据存储替换。
jBPM身份管理构件包含组织结构模型的知识。任务分派通常使用组织结构来完成。所以组织结构模型的前提是,描述用户、群组、系统和他们之间的关系。一般来说,权限和角色更多地被包含在组织结构模型中。各种学术研究数次的失败,验证了没有能够适用于每个组织的组织结构模型。
jBPM处理这个问题的方式是通过在流程中定义一个参与者当作真实的参与人。参与者通过它的叫做actorId的 ID被标识。jBPM只知道actorId而且它们用最大灵活性的java.lang.Strings来表示。所以任何的关于组织结构模型和数据结构的内容都是jBPM核心引擎范围之外的事。
作为jBPM的扩展我们将提供(未来)一个构件去管理简单的用户-角色(user-roles)模型。这种用与角色间的多对多关系和定义在J2EE和servlet规范中的是同一模型,而且它将在新的开发中将是一个起始点。对更细节的问题感兴趣相关的贡献的话,大家可以去检出jboss jbpm jira上面的问题跟踪。
注意当用户-角色模型用于servlet、ejb和portlet规范时,处理任务分派不是足够强大的。那个模型在用户和角色间是多对多的关系。这个不包括关于组(teams)和涉及流程的用户的组织架构的信息。
图12-3 身份模型类图
黄色的类是下面设计的表达式分派处理程序的相关类。
User代表一个用户或一个服务。Group是任何种用户的群组。Groups群组能在组(team)、商务单元(business unit)和整个公司间的关系模型中嵌套。Groups在层级群组间类型是有差别的,例如:发色的groups. Memberships代表用户和群间的多对多关系。一个membership用于代表公司是中的一个位置。Membership的名字能够用于显示用户完整填写在群组中的角色。
身份构件伴随着一个任务分派期间计算参与者一个计算表达式的实现。这有一个在流程定义中使用分派表达式的例子:
<process-definition>
...
<task-node name='a'> <task name='laundry'> <assignment expression='previous --> group(hierarchy) --> member(boss)' /> </task> <transition to='b' /> </task-node>
...
|
分派表达式的语法像下面这样:
first-term --> next-term --> next-term --> ... --> next-term
where
first-term ::= previous | swimlane(swimlane-name) | variable(variable-name) | user(user-name) | group(group-name)
and
next-term ::= group(group-type) | member(role-name) |
12.11.2.1. first-term
表达式从左到右进行解析。first-term在身份模型中指定一个User和Group。随后的名词从用户或群组中计算next-term。
previous意味着任务已经被分配到当前已经认证的参与者。这意味着参与者执行流程中的前一步。
swimlane(swimlane-name) 意味着用户或群组从指定的泳道实例上处理。
variable(variable-name)意味着用户或群组从指定的变量实例上处理。变量实例能够包含java.lang.String,那样的话用户或群组从身份构件处捕获。或者是变量实例包含一个User或Group对象。
user(user-name)意味着给定的用户被身份构件处理。
group(group-name)意味着给定的群组被身份构件处理。
12.11.2.2. next-term
group(group-type)获得用户的群组。意味着前一名词一定已经有了一个User,它用给定的group-type在所有的memberships中为用户搜索群组。
member(role-name)获得一个群组的执行给定角色的用户。previous terms必须有一个Group。这个名词用一个group的membership搜索用户,找到匹配给定的role-name 的membership的名称。
当你想为组织结构信息来使用你自己的数据源时(例如你自己公司的用户数据库或ldap系统),你可以仅是扯掉jBPM身份构件。唯一的事情需要你做的是确保你从hibernate.cfg.xml文件中删除了这些行。
<mapping resource="org/jbpm/identity/User.hbm.xml"/> <mapping resource="org/jbpm/identity/Group.hbm.xml"/> <mapping resource="org/jbpm/identity/Membership.hbm.xml"/> |
ExpressionAssignmentHandler依赖身份构件所以你就不能像原来那样使用它了。那样的话你想重新使用ExpressionAssignmentHandler并且绑定它到你的用户数据存储上,你可以从ExpressionAssignmentHandler上扩展并覆盖getExpressionSession方法。
protected ExpressionSession getExpressionSession(AssignmentContext assignmentContext); |