alartin's profileWindows Live 共享空间PhotosBlogListsMore Tools Help

Windows Live 共享空间

No list items have been added yet.
January 22

Facelet: JSF的最佳拍档

阅读这篇文章之前,我们先回顾一下,为什么我们需要JSF。很显然,我们不满意基于页面这种杂乱的Web视图设计体系。JSF就是提供一种基于组件,具有事件模型和组件模型的方式让我们象开发Swing图形界面一样开发Web程序。很美妙?不是么?

那么我们为什么还需要Facelet呢?我想这一定是大家最关心的问题。

文章列表:

  1. Inside Facelet: part one
  2. Inside Facelet: part two
  3. Facelet: best with JSF
  4. Improving JSF by Dumping JSP

JBPM术语:ProcessInstance和ProcessDefinition

概述

ProcessInstance是ProcessDefinition的一个执行实例,想象一下对于订票流程,每个客户的订票动作都会根据订票流程定义而创建一个流程实例,也就是执行实例ProcessInstance.创建执行实例很简单:ProcessInstance(ProcessDefinition def)。当一个ProcessInstance被创建后,负责执行主路径的token也被创建,这个token就是根token(root token),根token此时位于流程定义的开始状态start state.

JBPM术语:事件Event

概述

Event反映的是流程执行中的各个时刻。在流程执行中JBPM引擎会在计算下一个状态的时候触发各种事件。一个事件通常和流程定义中的一个元素相关联,比如流程定义本身,节点或者转移。大部分的元素能够触发不同类型的事件,比如一个节点可以触发节点进入事件,节点离开事件。事件其实是和动作连接在一起的。每个事件维护一个动作列表。当JBPM引擎触发一个事件的时候,该事件维护的动作列表中的动作将被执行。

事件类型

在JBPM中事件类型是写死在事件类中的,共有16种:

  1. EVENTTYPE_TRANSITION = "transition"; // 转移
  2. EVENTTYPE_BEFORE_SIGNAL = "before-signal"; // 发信号前
  3. EVENTTYPE_AFTER_SIGNAL = "after-signal"; // 发信号后
  4. EVENTTYPE_PROCESS_START = "process-start"; // 处理开始状态
  5. EVENTTYPE_PROCESS_END = "process-end"; // 处理结束状态
  6. EVENTTYPE_NODE_ENTER = "node-enter"; // 进入节点
  7. EVENTTYPE_NODE_LEAVE = "node-leave"; // 离开节点
  8. EVENTTYPE_SUPERSTATE_ENTER = "superstate-enter"; // 进入超级状态
  9. EVENTTYPE_SUPERSTATE_LEAVE = "superstate-leave"; // 离开超级状态
  10. EVENTTYPE_SUBPROCESS_CREATED = "subprocess-created"; // 子流程创建
  11. EVENTTYPE_SUBPROCESS_END = "subprocess-end"; // 子流程结束
  12. EVENTTYPE_TASK_CREATE = "task-create"; // 任务创建
  13. EVENTTYPE_TASK_ASSIGN = "task-assign"; // 任务分派
  14. EVENTTYPE_TASK_START = "task-start"; // 任务启动
  15. EVENTTYPE_TASK_END = "task-end"; // 任务结束
  16. EVENTTYPE_TIMER = "timer"; // 定时器

JBPM术语:超级状态Superstates

概述

一个超级状态就是一组节点。超级状态可以以回归方式嵌套。超级状态能够给流程定义带来层次效果。例如一个应用可能要把流程中的所有节点按阶段进行分组。动作可以和超级状态的事件相关联。超级状态能够让一个token在给定的时间上位于多个嵌套的节点中。这会带来很多方便,例如你可以方便的查询流程的执行是否在启动阶段。在JBPM中你可以将任意的节点们组成一个超级状态。

超级状态的转移

超级状态的所有转移都通过超级状态中的节点们的token来完成。转移也可以到达超级状态。此时,token被重定向到超级状态中的第一个节点。超级状态外的节点可以拥有指向超级状态内节点的转移,当然同时超级状态内的节点也可以拥有指向超级状态外的节点的转移,甚至指向超级状态本身的转移。超级状态甚至可以有自引用。

超级状态的事件

超级状态拥有两个特定的事件:进入超级状态,离开超级状态。无论超级状态

JBPM: 上下文

概述

上下文Context其实就是处理流程变量的地方。流程变量使用键值对来维护流程实例相关的信息。上下文必须要存储到数据库中。

访问变量

我们通过上下文实例(ContextInstance)来和流程变量打交道,上下文实例就是一个中央接口,位于org.jbpm.context.exe包中,从流程实例ProcessInstance中获得上下文实例:

ProcessInstance processInstance = ...;
ContextInstance contextInstance = (ContextInstance) processInstance.getInstance(ContextInstance.class);

常见的对上下文实例的操作包括:设置变量,获得变量:

void ContextInstance.setVariable(String variableName, Object value);
void ContextInstance.setVariable(String variableName, Object value, Token token);
Object ContextInstance.getVariable(String variableName);
Object ContextInstance.getVariable(String variableName, Token token);
变量的名字是String类型的。默认情况下,JBPM支持多种变量值的类型:
  • java.lang.String
  • java.lang.Boolean
  • java.lang.Character
  • java.lang.Float
  • java.lang.Double
  • java.lang.Long
  • java.lang.Byte
  • java.lang.Short
  • java.lang.Integer
  • java.util.Date
  • byte[]
  • java.io.Serializable
  • 使用hibernate持久化的类

Null值也支持,也可持久化。其他类型的也可以存储在流程变量中,只是会抛出异常。当流程变量的值为hibernate存储的类的时候需要配置JBPM.

流程变量的生命周期

流程变量无需在流程文件中声明,在运行时,你可以将任意Java对象设为变量。如果该变量不存在的话,变量会被创建,就象Map一样。

删除变量也很简单:

ContextInstance.deleteVariable(String variableName);
ContextInstance.deleteVariable(String variableName, Token token);
JBPM现在支持自动改变类型。这意味着你可以使用不同类型的值来覆盖一个变量。当然你应该严格控制类型改变的次数,毕竟这需要额外的数据库通讯。
流程变量的持久化
流程变量是流程实例的一部分。将流程实例保存在数据库中,意味着数据库和流程实例同步。流程变量的创建更新删除是通过流程实例的更新来实现的。
流程变量的范围
每个token,也就是每个流程执行的路径都有一套自己的变量。请求变量总是在token上发生的。流程实例拥有一个token树。在请求变量的时候,当你没有指定token参数的话,默认的是root token作为参数。
变量的查询是在给定的token的父token们中递归查找的。类似编程语言中的变量范围。
如果你在一个token上设置一个并不存在的变量,那么这个变量将在root token上创建。这意味这每个变量的默认范围是流程范围。
如果你需要限定变量的范围,则需要显式地指定Token:
ContextInstance.createVariable(String name, Object value, Token token);
变量的过载overloading
变量的过载指的是流程执行的每个路径都可以有相同一套名字的变量,也就是说不同的token中变量的名字可以一样,值和类型可以不一样。token之间是独立的。这在流程跨越同一个转移,启动不同的并发路径时很有用。
区别不同执行路径的就靠它们各自的这些变量。
变量的重载overriding
 
 

JBPM: 任务的管理 Task Management

概述

JBPM的核心就是持久化流程。持久化流程中最重要的特性就是管理人们的任务和任务列表。JBPM允许我们设定程序为人工参与进入等待状态。

任务

任务是流程定义的一部分。任务负责定义流程执行中任务实例task instance如何必须被创建和指派。所以任务也就是任务定义。任务可以在任务节点中定义,也可以在流程定义中定义。不过通常的方法是在任务节点中定义一个或者多个任务。这意味着任务节点task node代表着一个用户的人工任务,流程执行必须等待直到参与者actor(也就是用户)完成这项人工任务。当参与者actor完成人工任务后,流程执行将继续。如果一个任务节点中指定了多个任务,默认的行为是必须流程执行必须等待所有任务都完成

流程定义中也能定义任务。在流程定义中的任务能够通过名字查找,在任务节点中被引用,或者内部动作inside actions中使用。事实上,无论任务在流程定义中定义还是在任务节点中定义,只要任务有名字,都能够在流程定义中查找。需要注意的是:任务的名字在整个流程定义中必须唯一的。

任务可以有优先级。优先级在这个任务的每个任务实例中作为初始优先级。任务实例task instance能够在日后修改这个优先级。

任务实例 task instance

一个任务实例能够被指派给一个参与者ID, actorId(一个字符串)。所有的任务实例都在JBPM的JBPM_TASKINSTANCE表中存储。我们可以通过这张表查询给定actorId的所有任务实例,通过这种方法你可以获得特定用户的任务列表。JBPM任务列表机制能够将JBPM任务和其他任务结合起来,甚至能够结合那些和流程执行不相关的任务。通过这种方式,JBPM的开发者能够容易的将JBPM流程任务和其他系统的任务在一个同一的任务列表仓库同一管理。

任务实例的生命周期

任务实例的生命周期非常直观:创建后,任务实例就可以开始了,然后,可以被结束,这意味着任务实例被标记为完成。注意:为了灵活性,任务的指派并不是任务实例生命周期的一部分。所以任务实例可以被指派了,也可以尚未指派,这丝毫不影响任务实例的生命周期。任务实例通常是在流程执行进入任务节点的时候被创建。创建的方法是TaskMgmtInstance.createTaskInstance()。然后,用户界面组件可以通过TaskMgmtInstance.findTaskInstanceByActorId()方法查询任务列表。用户界面组件可以调用TaskInstance.assign(String), TaskInstance.start(), TaskInstance.end()等方法。

任务实例通过日期属性来维护它的状态:create, start, end. 可以通过TaskInstance相应getter方法访问。目前完成了的任务实例将会通过结束日期被标记为完成,因此对于后续查询任务列表的时候,完成了的任务实例不会被包含在任务列表中,不过它们仍旧在JBPM_TASKINSTANCE表中

任务实例和图的执行

从某种意义上说,任务实例是参与者actor任务列表上的项目。任务实例可以信号化,信号化的任务实例可以在任务结束的时候向token发出signal,让流程执行继续下去。任务实例也可以拦截化/阻止化,这意味着相关的token在任务未完成前不能离开任务节点。任务实例默认是信号化的

在一个任务节点中有多个任务实例的情况下,流程的开发者可以指定任务实例的完成将如何影响流程执行的继续。通过任务节点的signal属性的各种值来控制这一点:

  • last: 这是默认的。当最后一个任务完成时,流程执行将继续。当节点的入口处没有任务被创建的时候,流程执行将继续。
  • last-wait:当最后一个任务完成时,流程执行将继续。当节点的入口处没有任务被创建的时候,流程执行将在任务节点中等待直到任务被创建为止。
  • first:当第一个任务完成时,流程执行将继续。当节点的入口处没有任务被创建的时候,流程执行将继续。
  • first-wait:当第一个任务完成时,流程执行将继续。当节点的入口处没有任务被创建的时候,流程执行将在任务节点中等待直到任务被创建为止。
  • unsynchronized: 流程执行将永远继续下去,无论任务是否被创建或者仍未完成。
  • never: 流程执行将不再继续,无论任务是否被创建或者仍未完成,和unsynchronized相对。

任务实例也可以通过运行时计算而创建。在这种情况下,在任务节点的进入节点事件中添加一个ActionHandler,并且将属性create-task设为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, 提供了任务管理信息
完成一个任务实例很简单,调用TaskInstance.end()方法就可以了。你也可以在这个方法中指定一个转移transition,这样的话,任务实例完成
后将触发流程执行继续下去,离开任务节点,走向指定的转移。
任务指派
流程定义中包含了任务节点。一个任务节点可以包含零个或多个任务。什么是任务呢?任务其实就是流程定义中静态描述的一部分(是说任务仅仅是个描述而已)。在运行时,任务实例
将根据任务这个描述创建。而一个任务实例可以说就是一个人的任务列表中的一项。任务实例总是要被指派的。
JBPM的任务指派模型分为PULL和PUSH.
任务指派的数据模型
image 
任务实例或泳道实例的参与者ID actorId负责这个任务。而其中的PooledActors, 参与者候选池维护一系列任务的候选者,每个候选者都有可能负责这个任务,当候选者获得这个任务
后,他就为这个任务负责。需要注意的是:参与者ID和参与者候选池是可选的,也可以组合在一起使用。
个人的任务列表
个人任务列表负责维护指派给特定个人的任务,通过任务实例的参与者ID就可以看出。所以要想将任务实例放入个人任务列表中,只需下列方法的一种:
  • 在流程定义中的task元素的actor-id属性中指定表达式
  • 也可以在代码的任何地方设置TaskInstance.setActorId(String)方法
  • 也可以在一个AssignmentHandler中调用Assignable.setActorId(String)方法

我们可以通过TaskMgmtSession.findTaskInstances(String actorId)来获取特定人的任务列表。

组的任务列表

参与者候选池指的是任务实例的候选者。这意味着这个任务可以提供给多个用户,其中一个候选者可以被设置接受这个任务。候选池中的参与者可以看作是同一个组的,如果这个任务是提供给候选池的,用户必须先将任务拿到个人的任务列表中,才能执行任务。

TODO

背后的动机是我们希望将Identity组件从JBPM的任务指派中剥离。JBPM应该只关注actorId这些String类型的对象,而不应该关注用户,组或者其他识别信息之间的关系。

actorId总是覆盖参与者候选池中的actor(pooled actor)。所以如果任务实例既有actorId,也有参与者候选池的话,那么这个任务实例只出现在个人任务列表中。不过,参与者候选池有重要作用,一个用户可以将任务实例放回到组中重新供参与者候选人获取,这些只需要将任务实例的actorId属性去掉即可。

任务实例变量

任务实例可以拥有自己的一套变量,并且任务实例能够“看见”流程变量。任务实例是在一条执行路径token中创建的。需要注意的是:token和在这个token中创建的任务实例之间也是父子关系,这和token和其子token是父子关系是一样的。任务实例的变量和token相关的流程变量之间的范围规则同样适用。这意味着一个任务实例能够看到自己的一套变量,加上和它相关的token的所有变量。

任务控制器能够用来在任务实例范围和流程范围变量之间传递和提交变量。

任务控制器

 image

任务控制器本质上是流程变量和用户界面程序的桥梁。从这张图片可以看出,任务是以任务实例的方式赋给特定的用户的,任务控制器任务实例负责流程变量和任务变量之间的传递。用户只能通过任务实例中的变量和流程打交道,因此用户界面接口中的输入输出都是流程变量<->任务控制器<->任务实例<->用户界面。因此实际上用户在界面中输入的都是以任务变量的形式保存,当任务结束后通过任务控制器来将任务变量转化为流程变量。

最简单的情况下,流程变量和用户的表单参数是一对一的。任务控制器在任务元素中指定。JBPM提供一个默认的任务控制器,它维护一个变量元素列表。变量元素指示如何将流程变量拷贝到任务变量。

<task name="clean ceiling">
  <controller>
    // name属性指的是流程变量的名字,mapped-name属性指的是任务变量的名字,如果没有设定mapped-name,则任务变量名和流程变量名一致
    // 需要注意的是:mapped-name任务变量名就是任务实例表单中字段的标签
    // access属性指定变量传递的形式,read指的是任务实例创建时,从流程变量中拷贝到任务变量
    // write指的是任务实例完成时,从任务变量中拷回到流程变量中,默认的是read,write
    <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>
需要注意的是:一个任务节点可以有多个任务,而一个开始状态start-state只能有一个任务
如果你觉得流程变量和表单参数一对一的关系无法满足需求的话,你也可以实现自己的任务控制器TaskControllerHandler。
public interface TaskControllerHandler extends Serializable {
  void initializeTaskVariables(TaskInstance taskInstance, ContextInstance contextInstance, Token token);
  void submitTaskVariables(TaskInstance taskInstance, ContextInstance contextInstance, Token token);
}
<task name="clean ceiling">
  <controller class="com.yourcom.CleanCeilingTaskControllerHandler">
    // 此处配置实现的任务控制器
  </controller>
</task>

JBoss Seam 页面流释疑(1)

Seam的页面流pageflow是很容易让人迷惑的。

这里首先要弄清楚术语,Web程序通常都是在不同页面间来回跳转来实现应用逻辑的,所以通常的说法是页面导航Page Navigation, 这是最普遍和广泛的说法。因此对于页面导航Page Navigation的约定,被称为页面导航规则Page Navigation Rule。这几个术语是独立于语言和具体Web框架的。

例如,在JSF开发中,我们使用outcome来实现页面导航。outcome本身就是一个字符串,我们通过某个方法返回的outcome字符串来判断下一步的逻辑步骤。当然这个工作需要通过页面和outcome字符串的匹配设置来完成。例如在Seam的猜数字应用中,我们也可以使用JSF的outcome来实现页面导航:

<navigation-rule>
    <from-view-id>/numberGuess.jsp</from-view-id>
        
    <navigation-case>
        <from-outcome>guess</from-outcome>
        <to-view-id>/numberGuess.jsp</to-view-id>
        <redirect/>
    </navigation-case>

    <navigation-case>
        <from-outcome>win</from-outcome>
        <to-view-id>/win.jsp</to-view-id>
        <redirect/>
    </navigation-case>
        
    <navigation-case>
        <from-outcome>lose</from-outcome>
        <to-view-id>/lose.jsp</to-view-id>
        <redirect/>
    </navigation-case>

</navigation-rule>

我们可以看到JSF页面导航配置的三要素:

  1. 从哪个地方来:from-view-id指明从numberGuess.jsp页面来
  2. 某个可能的产出(outcome)是什么:from-outcome指明这个产出是win
  3. 这个可能的产出对应到哪里去:to-view-id指明如果产出是win的话,那么导向win.jsp页面

但是你可能立刻就能发现这种处理方式的弊病来:

  1. 这种方式需要在代码中处理导航逻辑。尽管我们配置页面导航比较容易,但是实际上,我们增加了代码中的复杂性(有可能得不偿失啊)。
  2. 自由度太大。开发人员可以随意设置outcome的值和其导向页面这样当页面导航复杂度增加的时候管理起来非常困难。
  3. 约束性太小。这个缺点和第二个缺点有一定联系,约束性太小意味着,开发人员往往需要查看所有的页面文件*.jsp和整个配置文件navigation.xml才能明白全部的页面导航规则

这种方式的页面导航被称为无状态的页面导航。

需要强调的是:对于这种无状态的页面导航方式,除了JSF导航(使用navigation.xml)外,Seam还支持一种自己实现的无状态页面导航(使用pages.xml或者某个页面的xxx.page.xml):

<page view-id="/admin/admin.xhtml">
        <navigation>
            <rule if-outcome="ship">
                <redirect view-id="/admin/ship.xhtml"/>
            </rule>
            <rule if-outcome="accept">
                <redirect view-id="/admin/accept.xhtml"/>
            </rule>
        </navigation>
    </page>

这里Seam也使用了类似JSF的页面导航三要素:

  1. 从哪里来:page元素的view-id属性指明从/admin/admin.xhtml页面来
  2. 某个可能的产出(outcome): navigation元素中嵌套的rule元素的if-outcome属性指明可能的一个outcome是ship
  3. 到哪里去:rule元素中嵌套的redirect元素的view-id属性指明如果outcome属性是ship的话,导向ship.xhtml页面

如果,在你的Seam应用程序中你准备使用无状态的页面导航的话,你有两种选择:

  1. 使用JSF导航:配置navigation.xml
  2. 使用Seam导航: 配置pages.xml或者某个xxx.page.xml

我们将在后面的文章中比较这两种选择。

Seam不仅仅只支持这种无状态的页面导航!更棒的是,Seam支持有状态的页面导航

提醒:在Seam中,Seam实现的页面导航被成为页面流Pageflow

  1. JSF页面导航被称为页面导航,而页面导航是广泛的术语
  2. Seam实现的页面导航被成为页面流Pageflow
  3. 页面流可以是无状态的页面导航,Seam自己实现的无状态的页面导航
  4. 页面流也可以是有状态的页面导航,Seam通过JBPM实现的有状态的页面导航

在我们讲解有状态的页面导航之前,我们先回顾一下无状态页面导航的弊病包括:虽然页面配置简单,但是代码中要控制页面导航规则;无法很容易的看清楚导航规则,因为约束小。

Seam通过使用JBPM来帮助我们支持创建有状态的页面导航

需要注意的是:Seam在两种层次上使用JBPM:

  1. 一种是在页面导航中使用JBPM;
  2. 另一种是在业务流程管理中使用JBPM.

这两者的层次是不一样的,这也是容易导致Seam初学者迷惑的地方,在这里我们先弄清楚这一点:JBPM是独立的流程管理引擎,在Seam中它被使用在两个不同的地方:页面导航和流程管理中。

实际上,Seam开发人员利用JBPM强大的定制功能,专门为页面导航定制了流程管理。Seam的使用者应该记住在Seam有状态的页面导航中page元素是JBPM节点Node的一种!这从侧面证明了JBPM强大的功能!

OK, 我们接着说JBPM在Seam中的使用。

首先看一下JBPM在页面导航中的作用:用来实现有状态的页面导航。

JBPM在这里用来定义单个对话中的页面流。一个Seam对话指的是单个用户相对短时期内的交互

我们再来看一下JBPM在流程管理中的作用:用来进行流程管理。

业务流程可以跨越多个用户,多个对话。其流程被持久化到JBPM数据库中,所以它是长时期的。当然,管理多用户活动当然比管理单个用户活动复杂的多,包括复杂的任务管理和多路径并发管理。

你可能在你的应用程序中同时在这两个地方都使用JBPM!

OK,让我们回到有状态页面导航的主题上来。

还是使用猜数字应用。这回我们需要用xxx.jpdl.xml文件来配置有状态的页面导航(pageflow.jpdl.xml):

<pageflow-definition name="numberGuess">
    
   <start-page name="displayGuess" view-id="/numberGuess.jsp">
      <redirect/>
      <transition name="guess" to="evaluateGuess">
      	<action expression="#{numberGuess.guess}" />
      </transition>
   </start-page>
   
   <decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
      <transition name="true" to="win"/>
      <transition name="false" to="evaluateRemainingGuesses"/>
   </decision>
   
   <decision name="evaluateRemainingGuesses" expression="#{numberGuess.lastGuess}">
      <transition name="true" to="lose"/>
      <transition name="false" to="displayGuess"/>
   </decision>
   <process-state name="cheat">
      <sub-process name="cheat"/>
      <transition name="displayGuess"/>
   </process-state>
   <page name="win" view-id="/win.jsp">
      <redirect/>
      <end-conversation />
   </page>
   
   <page name="lose" view-id="/lose.jsp">
      <redirect/>
      <end-conversation />
   </page>
   
</pageflow-definition>
我们可以看到有状态的页面流是通过JBPM的JPDL(xxx.jpdl.xml)来配置的,几个要素:
  1. 页面流定义: pageflow-definition元素指明页面流名字
  2. 起始点:起始点包括两种可能的元素:start-page元素或者start-state元素(区别我们稍后介绍)指明页面流的起始点
  3. 页面:page元素指明页面流中的各个页面
  4. 转移:transition元素代替了无状态页面流的产出outcome作用,这里的转移将在具体页面中使用(例如在guessNumber.jspx文件中):<h:commandButton value="Guess" action="guess"/>,这个按钮激发的动作action名为guess, 而这个guess就是xxx.jpdl.xml文件中配置的一个转移(注意和无状态的页面流区别,这里的动作action指的是转移,而不是传统意义上的动作或方法调用)
  5. 动作:在转移元素中的action元素指明这个转移中发生的事件(和JBPM的转移的概念一致),还是上面的例子:<action expression="#{numberGuess.guess}" />,在xxx.jpdl.xml文件中转移guess中有个动作,这个动作是调用numberGuess对象的guess方法,并且这个转移guess将导向evaluateGuess这个决策。所以逻辑是这样的: 用户 -> 点击Guess按钮 ->Guess按钮的action激发guess转移 ->guess转移激发numberGuess对象的guess方法 -> guess转移导向evaluateGuess决策
  6. 决策:decision元素
  7. 流程状态:process-state元素包含子流程元素sub-process

January 10

JBoss Seam: Seam应用框架

Seam Application Framework, 听起来挺吓人的,也很容易让人犯糊涂,Seam本身不就是个Web框架么,那里来的应用框架. 其实称之为Seam Component Template Framework更加合适. Seam 组件模板框架.

原因很简单, 这个名词看起来吓人,其实就干一件事情, 把Seam组件开发再简化(尽管通过注释等方法, 开发Seam组件非常简单了). 怎么再简化呢? 大家可以想一想Struts的Dynamic Action历史就明白了. 也就是通过配置来创建基本的Seam组件, 你可以不用写类了! 在componets.xml文件中配置.例如下面的配置信息能够创建一个基本的实现CRUD功能的Seam组件:

<framework:entity-home name="personHome" 
                       entity-class="eg.Person" 
                       entity-manager="#{personDatabase}">
    <framework:id>#{param.personId}</framework:id>
</framework:entity-home>
如果你讨厌通过XML配置来替代编程的话(反正我讨厌), 你也可以通过类继承来实现:
@Stateful
@Name("personHome")
public class PersonHome extends EntityHome<Person> implements LocalPersonHome {
    @RequestParameter String personId;
    @In EntityManager personDatabase;
    
    public Object getId() { return personId; }
    public EntityManager getEntityManager() { return personDatabase; }
    
}

一头雾水吧?让我们看看Seam的代码.所有的Application Framework代码位于org.jboss.seam.framework下面.

最重要的两个类为Home类和Query类.

Home类: 提供了实体类的持久化操作CRUD. Seam提供两种实现:EntityHome和HibernateEntityHome. 一个是给JPA的,一个是给Hibernate的.

Query类: 顾名思义,提供实体类查询的.同样,Seam提供两种实现: EntityQuery, HibernateEntityQuery.一个是给JPA的,一个是给Hibernate的.

Home类和Query类天生就能和会话范围,事件范围和对话范围一起工作.

需要注意的是:Seam Application Framework只能和Seam管理的持久化上下文一起工作,Seam-managed Persistence Context.

默认的情况下,Application Framework的组件查找一个名为entityManager的持久化上下文.

来点仔细的. 我们看一下Home, 其实Home就是给某个实体类干CRUD活的,类似于EJB3的Session Bean, 比如说我们有个实体类Person, 那么PersonHome就是创建,修改,删除Person的工具么.

@Entity
public class Person { // 这个是我们的实体类
    @Id private Long id;
    private String firstName;
    private String lastName;
    private Country nationality;
    
    //getters and setters...
}
1. 可以通过在components.xml配置相对应的Home类:
<framework:entity-home name="personHome" entity-class="eg.Person" />
2. 或者自己写类来继承
@Name("personHome")
public class PersonHome extends EntityHome<Person> {} // 参数是实体类啊

反正我觉的第二种方法最合适,如果你是个程序员的话.

然后你就自动获得PersonHome类了, 内置persist(), remove(), update()和getInstance()方法. 酷吧!当然,你要调用remove()或update()方法前,要先设置setId(Object id)方法.

OK, 那么怎么使用呢, 我们看一个例子, 我们可以直接在页面上使用Home类:

<h1>Create Person</h1>
<h:form>
    <div>First name: <h:inputText value="#{personHome.instance.firstName}"/></div>
    <div>Last name: <h:inputText value="#{personHome.instance.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}"/>
    </div>
</h:form>

用户在页面上的输入直接就能创建一个Person实体对象了,当然,通过PersonHome创建的.不过我们会觉得使用personHome.instance很不爽, 其实我们更加愿意使用person来代替它. 做到这一点也很容易:

1. 在components.xml中配置

<factory name="person" 
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome" 
                       entity-class="eg.Person" />
2.在自己写的类中加一个方法,然后用@Factory注释
@Name("personHome")
public class PersonHome extends EntityHome<Person> {
    // 很简单,这个方法仅仅调用的getInstance()而已
    @Factory("person")
    public Person initPerson() { return getInstance(); }
    
}
OK, 现在我们的页面就可以变成:
<h1>Create Person</h1>
<h:form>
    <div>First name: <h:inputText value="#{person.firstName}"/></div>
    <div>Last name: <h:inputText value="#{person.lastName}"/></div>
    <div>                                              // 这里没变
        <h:commandButton value="Create Person" action="#{personHome.persist}"/>
    </div>
</h:form>
相信你已经很满意了吧.那么,也许你想搞个页面来编辑某个已经持久化的Person实体,那么你需要在pages.xml中配置页面参数:
<pages>
    <page view-id="/editPerson.jsp"> // 编辑页面需要person id参数
        <param name="personId" value="#{personHome.id}"/>
    </page>
</pages>
让我们看看编辑页面:
<h1> // 如果personHome还没有管理某个person实体的话, 页面显示为 创建Person, 否则显示 编辑Person
     // 此时,页面参数personId 为空
    <h:outputText rendered="#{!personHome.managed}" value="Create Person"/>
    <h:outputText rendered="#{personHome.managed}" value="Edit Person"/>
</h1>
<h:form>
    <div>First name: <h:inputText value="#{person.firstName}"/></div>
    <div>Last name: <h:inputText value="#{person.lastName}"/></div>
    <div>
        //如果personHome还没有管理某个person实体的话,按钮显示创建,否则的话显示两个按钮 更新和删除
        // 此时,页面参数personId 为空
        <h:commandButton value="Create Person" action="#{personHome.persist}" rendered="#{!personHome.managed}"/>
        <h:commandButton value="Update Person" action="#{personHome.update}" rendered="#{personHome.managed}"/>
        <h:commandButton value="Delete Person" action="#{personHome.remove}" rendered="#{personHome.managed}"/>
    </div>
</h:form>
关于Home类,我们先讲这么多,再来看看Query类.
Query类通常用来查询一个实体对象的列表.比如获得所有Person:
1.通过components.xml配置
<framework:entity-query name="people" 
                        ejbql="select p from Person p"/>
使用页面:
<h1>List of people</h1>
<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>
要是我们想分页看呢:
<framework:entity-query name="people" 
                        ejbql="select p from Person p" 
                        order="lastName"  // 排序
                        max-results="20"/> // 分页最大结果
在pages.xml配置:
<pages>
    <page view-id="/searchPerson.jsp">
        <param name="firstResult" value="#{people.firstResult}"/>
    </page>
</pages>
更改后的页面:
<h1>Search for people</h1>
<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="First Page">
    <f:param name="firstResult" value="0"/> // <<
</s:link>

<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="Previous Page">
    <f:param name="firstResult" value="#{people.previousFirstResult}"/>
</s:link> 
<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Next Page">
    <f:param name="firstResult" value="#{people.nextFirstResult}"/>
</s:link> 
<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Last Page">
    <f:param name="firstResult" value="#{people.lastFirstResult}"/>
</s:link>
我们甚至可以监听一些事件,例如更新,删除等等然后根据这些事件,刷新查询:
<event type="org.jboss.seam.afterTransactionSuccess">
    <action execute="#{people.refresh}" />
</event>
甚至可以限定是Person实体的事件:
<event type="org.jboss.seam.afterTransactionSuccess.Person">
    <action execute="#{people.refresh}" />
</event>

需要注意的是:上面的Query例子都用的是配置形式,你也可以写自己的类来继承.Seam通常将Query的继承类命名为XXXList, 将Home类命名为XXXHome, 其中XXX是实体类.

有趣的Controller类其实就是一些实体类的帮助类,让你更加容易使用内置组件和内置组件的方法.

        like   session bean                          |                           like session bean

XXXList/Query(负责XXX的查询)  <----  你的实体类XXX  ----> XXXHome (负责XXX的CRUD工作)

                                                            |

                                                   Controller(XXX的帮助类)

December 21

JBPM:事务的划分

JBPM是在客户端线程中运行的流程,因此它自然就是同步的。这意味着token.signal()和taskInstance.end()方法只有在流程执行进入了一个新的等待状态才能返回。

大部分情况下,这是非常直观的方法,因为流程执行能够很容易的和服务器端事务绑定:在同一个事务中流程从一个状态执行到另一个状态。

在流程中需要大量时间计算的场景中,这种方法肯定不是想要的。为了解决这种场景遇到的问题,JBPM引入了一个异步消息系统,允许流程以异步的方式执行。当然,在Java企业环境中,JBPM可以被配置为使用其他的JMS消息代理系统,而不是使用内置的消息系统。

在任何节点中,JPDL支持async属性为true。被配置为异步的节点将不会在客户端线程中执行。相反,一个消息将被异步消息系统发送,然后线程就被返回给客户端(这意味着token.signal()和taskInstance.end()将被返回)。

注意,这时候JBPM客户端代码可以提交事务了。在流程更新的同一个事务中消息将被发送。所以事务的结果就是:token被移到了下一个节点(这时这个节点还没有被执行),并且一个执行节点命令(ExecuteNodeCommand)消息被异步消息系统发送给JBPM的命令执行器Command Executor。

JBPM的命令执行器从消息队列中读取并且执行。在执行节点命令中,流程将被继续执行。每个命令都在一个单独的事务中被执行。

所以为了让异步的流程能够继续,一个JBPM命令执行器必须运行。最简单的方法是在Web应用中配置CommandExecutionServlet。让这个Servlet负责运行JBPM命令执行器。或者你使用其他方法,总之必须保证命令执行器线程必须运行。

作为一个流程的建模者,你不必关心所有的异步消息,关键一点是事务的划分:默认情况下,JBPM在客户端操作事务,计算整个流程直到流程进入一个等待状态。你可以将async属性设为true去在流程中划分事务。

<start-state>
  <transition to="one" />
</start-state>
<node async="true" name="one"> // 异步
  <action class="com...MyAutomaticAction" />
  <transition to="two" />
</node>
<node async="true" name="two"> // 异步
  <action class="com...MyAutomaticAction" />
  <transition to="three" />
</node>
<node async="true" name="three"> // 异步
  <action class="com...MyAutomaticAction" />
  <transition to="end" />
</node>
<end-state name="end" />
客户端代码与流程执行的交互(启动,继续启动)和同步的一样处理:
...start a transaction...
JbpmContext jbpmContext = jbpmConfiguration.createContext();
try {
  ProcessInstance processInstance = jbpmContext.newProcessInstance("my async process");
  processInstance.signal();
  jbpmContext.save(processInstance);
} finally {
  jbpmContext.close();
}
在上述例子中,第一个事务结束后,流程执行的根令牌root token将会指向节点one,然后一个执行节点命令消息ExecutionNodeCommandMessage
被发送给命令执行器。在接下来的事务中,命令执行器将会读取消息队列的消息,然后执行节点one。这个动作能够决定是传递流程执行还是进入等待状态。
如果动作决定传递流程执行,那么在流程执行到达节点two的时候,事务就结束了。然后继续...继续...下去。

JBPM:图的执行Graph Execution

JBPM中,图的执行模型是基于对流程定义的解释和命令模式链的。

对流程定义的解释意味这流程定义必须存储在数据库中(其实不尽然,但必须有某种存储机制)。在流程执行过程中需要流程定义的信息。需要注意的是JBPM使用hibernate的二级缓存来避免在运行时加载流程定义信息。这是因为一般来说流程定义不会轻易改变,所以我们能够将流程定义放入缓存中。

命令模式链意味着图中的每个节点需要负责将流程的执行传递下去。如果某个节点没有传递流程的执行,那么这个节点位于等待状态。

JBPM的设计思想是在流程实例上启动执行,然后这个流程执行将一直继续下去直到进入一个等待状态。

一个令牌token代表一个执行的路径。一个token拥有一个指向流程执行中的一个节点的指针。同样token也可以通过数据库将自己持久化。在等待状态时,token可以被持久化到数据库中。我们看一下JBPM是如何计算token的执行的。当有一个信号singal发送给token的时候,执行开始启动,接着执行通过命令模式链的方式沿着转移transition和节点node传递。在类定义中有相关的方法:

image

当token在节点中的时候,信号可以发送给token.信号的发送指令着执行的启动。一个信号必须指定当前节点中token的离开转移(节点到转移被称为离开转移,转移到节点被称为到达转移) 。

需要注意的是第一个转移就是默认的转移。

给token发送信号的时候,token会调用在其内维护的当前节点的Node.leave(ExecutionContext,Transition)方法。你可以将ExecutionContext想象成是一个token(会引起误导,不过ExecutionContext中维护着一个token).这个方法(Node.leave())能够触发一个离开节点的事件(node-leave类型),并且调用转移transition的take方法Transition.take(ExecutionContext).

这个方法(Transition.take())能够触发一个transition的事件并且调用这个转移Transition的目的节点的enter方法(Node.enter())(每个Transition都维护一个from节点和一个to节点,from节点被称为源节点,to节点被称为目的节点)。

每种类型的节点都在execute方法中有自己的行为。每个节点都通过Node.leave(ExecuteContext,Transition)来负责传递流程的执行。

简单的说:

  • Token.signal(Transition)
  • --> Node.leave(ExecutionContext,Transition)
  • --> Transition.take(ExecutionContext)
  • --> Node.enter(ExecutionContext)
  • --> Node.execute(ExecutionContext)

需要注意的是整个的下一状态的计算(包括动作的调用)都是在客户端线程中完成的。一个常见的错误理解是所有的计算都必须在客户端线程中完成。对于任何的异步调用,你可以使用异步消息JMS。当消息在流程实例更新的同一个事务中被发送时,所有的同步问题都要小心的处理。一些工作流系统在图中的所有节点间使用的是异步消息。但是在高通量环境中,这种算法能够为业务流程提供更高的灵活性和控制性。

 

alartin

Photo 1 of 10
No list items have been added yet.