Chapter 7. 页面流和业务流程

JBoss jBPM 是一个可以运行于任何Java SE或EE环境的业务流程管理引擎。 jBPM使用一组节点组成的图形来描述一个业务流程或用户交互过程,这些节点分别表示等待状态、决策、任务、Web页面等等。 这个图形使用一种简单的、非常易读的,称为jPDL的XML方言来定义,并且可以使用一个Eclipse插件图形化显示和编辑。 jPDL是一种可扩展的语言,适合于解决许多问题,从定义Web应用程序页面流到传统的工作流管理,直到在SOA环境中组织服务。

Seam应用程序使用jBPM解决两个不同的问题:

不要把这两个事情弄混了!它们运行在两个非常不同的层面或粒度中。 页面流Pageflow对话Conversation任务Task 全部来自于一次与单一用户的单一交互。 而一个业务流程则横跨许多任务。进一步说,这两种jBPM应用是完全正交的,可以在一起使用或是分开单独使用,或者都不用。

使用Seam不必了解jDPL。如果你完全乐于使用JSF或是Seam导航规则定义页面流,并且你的应用程序更多的是数据驱动而不是流程驱动,可能就不需要jBPM。 但是我们发现,根据一个定义良好的图形考虑用户交互有助于我们创建更加有生命力的应用程序。

7.1. Seam中的页面流

在Seam中定义页面流有两种方法:

  • 使用JSF或是Seam导航规则 – 无状态的导航模型

  • 使用jPDL – 有状态的导航模型

非常简单的应用程序只需要无状态的导航模型。非常复杂的应用程序则应该在不同的场合结合使用这两种模型。每种模型各有优缺点。

7.1.1. 两种导航模型

无状态的模型定义一种映射,把事件的一组命名的逻辑结果直接映射到视图的结果页面。导航规则除了记录哪个页面是事件来源之外不会保留任何状态。 这意味着Action监听方法有时必须进行一些页面流转的处理,因为只有它们能访问到应用程序的当前状态。

下面是一个使用JSF导航规则定义页面流的例子:

<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>

同一个例子,下面使用Seam导航规则定义:

<page view-id="/numberGuess.jsp">
        
    <navigation>
        <rule if-outcome="guess">
            <redirect view-id="/numberGuess.jsp"/>
        </rule>
        <rule if-outcome="win">
            <redirect view-id="/win.jsp"/>
        </rule>
        <rule if-outcome="lose">
            <redirect view-id="/lose.jsp"/>
        </rule>
    </navigation>

</page>

如果你觉得导航规则过于繁琐,可以在Action监听方法里直接返回View的id。

public String guess() {
    if (guess==randomNumber) return "/win.jsp";
    if (++guessCount==maxGuesses) return "/lose.jsp";
    return null;
}

注意这会导致一个重定向,甚至可以指定要在重定向中使用的一些参数。

public String search() {
    return "/searchResults.jsp?searchPattern=#{searchAction.searchPattern}";
}

有状态模型在一组具名的、合乎逻辑的应用状态之间定义一组重定向。 在这种模型中,可以完全使用jPDL页面流定义来表示任意用户的交互流程,并在编写Action监听方法完全不必知道用户的交互流程。

下面是一个使用jPDL定义页面流的例子:

<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>
   
   <page name="win" view-id="/win.jsp">
      <redirect/>
      <end-conversation />
   </page>
   
   <page name="lose" view-id="/lose.jsp">
      <redirect/>
      <end-conversation />
   </page>
   
</pageflow-definition>

这里我们马上注意到了两件事:

  • JSF/Seam导航规则 更加 简单。(然而,隐藏在后面的Java代码会相当复杂。)

  • jPDL让用户交互的过程立刻变得直接易懂,我们甚至不需要考虑JSP或Java代码。

另外,有状态模型更加 受约束 。对于每一种逻辑状态(页面流中的每一步)而言,都存在一组受限的可能转到其他状态的重定向。 而无状态模型则是一种 实时的 模型,它适用于相对不受约束的、形式自由的导航,在这种情况下,由用户决定他/她下一步想重定向到哪里,而不是应用程序。

有状态和无状态导航之间的差别与传统的模态和非模态交互视图之间的差别很类似。 现在,Seam应用程序通常不是模态的,简单地说 -- 的确,使用对话的一个主要原因就是避免应用程序中的模态行为! 然而,在一个具体对话的级别里Seam应用程序可以是(甚至经常是)模态的。 众所周知,应该尽可能地避免模态行为;预知用户想要做事的顺序是非常困难的!然而,毫无疑问,有状态模型也是需要的。

这两种模型之间的最大不同是后退按钮行为。

7.1.2. Seam和后退按钮

当使用JSF或Seam导航规则时,Seam允许用户通过后退、前进和刷新按钮自由地导航。当用户有以上操作时,应用程序的职责是确保发生对话状态的内部保持一致。 许多开发者都将Web应用框架例如Struts或WebWork -- 它们不支持对话模型 -- 和无状态组件模型例如EJB无状态Session Beans或Spring框架相结合过,这使得他们认为这几乎是不可能的一件事。 然而,就我们的经验,在Seam的上下文中,存在一个定义良好的对话模型,基于有状态的Session Bean,实现会话状态的内部一致实际上非常容易。 通常,在Action监听方法的开始把 no-conversation-view-id 的使用和null检查结合起来是很简单的。 我们认为支持自由导航将几乎总是可行的。

既然如此,pages.xml 中就包含一个 no-conversation-view-id 声明。 它告诉Seam,如果在一个对话当中有一个页面渲染的请求,而这个对话不再存在,就重定向到一个不同的页面。

<page view-id="/checkout.xhtml" 
        no-conversation-view-id="/main.xhtml"/>

另一方面,在有状态的模型中,后退按钮被认为是一个返回前一状态的未定义跳转。 因为有状态模型强迫当前状态必须有一组明确定义的跳转,所以后退按钮在有状态模型中是被默认禁止的! Seam明确地监测到后退按钮的使用,并且阻止执行前一个Action和访问过期的页面,而后简单地将用户重定向回当前页面(并且显示一个消息)。 这到底是有状态模型的一个特性还是一个限制,则取决于你在其中的角色:作为一个开发者,这是一个特性; 作为用户,这可能是一个限制!可以为一些特殊的Page节点设置 back="enabled" 属性,从而允许后退按钮导航。

<page name="checkout" 
        view-id="/checkout.xhtml" 
        back="enabled">
    <redirect/>
    <transition to="checkout"/>
    <transition name="complete" to="complete"/>
</page>

这个配置允许使用后退按钮 checkout 状态转到 任意前一个状态

当然,如果在一个页面流中有一个页面渲染的请求,并且这个页面流的对话不再存在,我们依然需要决定如何处理这种情况。 既然这样,需要在页面流定义中包括一个 no-conversation-view-id 声明:

<page name="checkout" 
        view-id="/checkout.xhtml" 
        back="enabled" 
        no-conversation-view-id="/main.xhtml">
    <redirect/>
    <transition to="checkout"/>
    <transition name="complete" to="complete"/>
</page>

在实践中,这两种导航模型都有它们自己的用处,你将很快学会如何在这两种模型之间做出选择。

7.2. 使用jPDL页面流

7.2.1. 安装页面流

我们需要安装Seam jBPM相关的组件,并且告诉它们在哪里找到页面流定义。我们可以在 components.xml 配置文件中指定这个配置。

<bpm:jbpm>
    <bpm:pageflow-definitions>
        <value>pageflow.jpdl.xml</value>
    </bpm:pageflow-definitions>
</bpm:jbpm>

第一行安装jBPM,第二行指向一个基于jPDL的页面流定义。

7.2.2. 开始页面流

我们可以通过在 @Begin@BeginTask@StartTask 的注解中指定流程定义的名字来“启动”一个基于jPDL的页面流。

@Begin(pageflow="numberguess")
public void begin() { ... }

作为选择,我们也可以使用pages.xml来启动一个页面流:

<page>
        <begin-conversation pageflow="numberguess"/>
    </page>

在上面这个例子中,如果我们在渲染阶段 RENDER_RESPONSE 阶段— 例如,在一个 @Factory@Create 方法中 -- 启动一个页面流,那就意味着我们已经处在已产生的页面中,就要使用页面流中的 <start-page> 节点作为页面流的第一个节点,如上例示所示。

但是如果这个页面流作为一个Action监听器的执行结果而启动的话,这个action监听器的结果则将决定哪个页面作为第一个被渲染的页面。 既然这样,我们在页面流中使用一个 <start-state> 作为第一个节点,并且为每一个可能的结果声明一个跳转。

<pageflow-definition name="viewEditDocument">

    <start-state name="start">
        <transition name="documentFound" to="displayDocument"/>
        <transition name="documentNotFound" to="notFound"/>
    </start-state>
    
    <page name="displayDocument" view-id="/document.jsp">
        <transition name="edit" to="editDocument"/>
        <transition name="done" to="main"/>
    </page>
    
    ...
    
    <page name="notFound" view-id="/404.jsp">
        <end-conversation/>
    </page>
    
</pageflow-definition>

7.2.3. 页面节点和跳转

每一个 <page> 节点描绘一种状态,在该状态下系统等待用户的输入:

<page name="displayGuess" view-id="/numberGuess.jsp">
    <redirect/>
    <transition name="guess" to="evaluateGuess">
        <action expression="#{numberGuess.guess}" />
    </transition>
</page>

其中的 view-id 是一个JSF 视图id。 <redirect/> 元素跟JSF导航规则中的 <redirect/> 有相同的作用: 即,一个post-then-redirect(提交后跳转)行为,这样做是为了防止用户点击了浏览器的刷新按钮进行重复刷新。 (需要注意的是,Seam会在这些浏览器重定向中保存对话的上下文,所以在Seam中不需要类似ROR(Ruby On Rails)风格的”flash”构造!)

跳转的名字是一个JSF输出的名字,在 numberGuess.jsp 中通过点击一个Command按钮或是Command链接触发这个跳转。

<h:commandButton type="submit" value="Guess" action="guess"/>

当点击这个按钮触发这个跳转之后,jBPM将会通过调用 numberGuess 组件的 guess() 方法激活跳转Action。 注意到jPDL中用于指定Action的语法类似于JSF的EL表达式,并且这个跳转的Action Handler不过是Seam当前上下文中的一个组件里的一个方法。 同JSF事件一样,我们可以在jBPM中拥有完全相同的事件模型!(同一类型准则)

在没有指定输出的情况下(例如,一个Command按钮没有定义 action 属性), 如果存在没有指定name的跳转,Seam将激活该跳转,或者如果所有的跳转都有name的话,Seam简单地重新显示该页。 所以我们可以稍微地简化示例页面流和这个按钮:

<h:commandButton type="submit" value="Guess"/>

将激活一个未命名的跳转:

<page name="displayGuess" view-id="/numberGuess.jsp">
    <redirect/>
    <transition to="evaluateGuess">
        <action expression="#{numberGuess.guess}" />
    </transition>
</page>

甚至可以通过点击一个按钮来执行一个Action方法,在这种情况下,Action执行的结果将决定采用哪个跳转:

<h:commandButton type="submit" value="Guess" action="#{numberGuess.guess}"/>
<page name="displayGuess" view-id="/numberGuess.jsp">
    <transition name="correctGuess" to="win"/>
    <transition name="incorrectGuess" to="evaluateGuess"/>
</page>

然而,这是一种不可取的方式,因为它把流程控制的职责从页面流定义转到了其他组件。最好还是把这些相关的职责集中到页面流本身。

7.2.4. 流程控制

通常,我们定义页面流的时候不需要更多强大的jPDL特性,然而我们还是需要 <decision> 节点。

<decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
    <transition name="true" to="win"/>
    <transition name="false" to="evaluateRemainingGuesses"/>
</decision>

通过在Seam上下文中执行一个JSF EL表达式来决定如何跳转。

7.2.5. 流程的结束

使用 <end-conversation> 或是 @End 结束对话。 (实际上,为了程序的易读性,鼓励 两者共同 使用。)

<page name="win" view-id="/win.jsp">
    <redirect/>
    <end-conversation/>
</page>

可选择的,可以指定一个jBPM的 状态转移(transition) 名字结束一个任务。 在这种情况下,Seam将给主控的业务流程发送一个结束当前任务的信号。

<page name="win" view-id="/win.jsp">
    <redirect/>
    <end-task transition="success"/>
</page>

7.2.6. 页面流组合

多个页面流可以进行组合,并当另一个页面流执行时暂停一个页面流。 这个 <process-state> 节点的作用就是暂停外部的页面流,同时开始执行一个命名的页面流。

<process-state name="cheat">
    <sub-process name="cheat"/>
    <transition to="displayGuess"/>
</process-state>

这个内部页面流从 <start-state> 节点开始执行。 当执行到 <end-state> 节点时,内部页面流执行完毕,同时外部的页面流会以 <process-state> 定义的跳转恢复执行。

7.3. Seam中的业务流程管理

一个业务流程由一系列定义良好的任务组成,这些任务必须由用户或是软件系统遵照一系列定义良好的规则来完成,这些规则规定了 可以执行任务,什么时候 应该执行。 Seam对jBPM的整合使得给用户显示任务列表以及使用户管理他们的任务变得简单。 Seam还使应用程序在 BUSINESS_PROCESS 的上下文中存储与业务流程相关的状态,通过jBPM变量来持久化这个状态。

一个简单的业务流程定义看起来跟页面流定义非常相似(是同一种类型的东西),不同的是用 <task-node> 节点替换了 <page> 节点。在一个长运行期的业务流程中,等待状态表示系统正在等待用户登录并执行任务。

<process-definition name="todo">
   
   <start-state name="start">
      <transition to="todo"/>
   </start-state>
   
   <task-node name="todo">
      <task name="todo" description="#{todoList.description}">
         <assignment actor-id="#{actor.id}"/>
      </task>
      <transition to="done"/>
   </task-node>
   
   <end-state name="done"/>
   
</process-definition>

我们很有可能在一个项目中同时使用jPDL业务流程定义和jPDL页面流程定义。 如果这样,他们二者的关系是:一个业务流程中的 <task> 对应一个完整的页面流 <pageflow-definition>

7.4. 使用jPDL业务流程定义

7.4.1. 安装流程定义

我们需要安装jBPM,并且告诉它到哪里可以找到业务流程定义文件:

<bpm:jbpm>
    <bpm:process-definitions>
        <value>todo.jpdl.xml</value>
    </bpm:process-definitions>
</bpm:jbpm>

7.4.2. 初始化Actor id

我们总是需要知道当前的登录用户。jBPM使用 actor idgroup actor id “识别”用户。 我们使用Seam内置的 actor 组件指定当前用户的id。

@In Actor actor;

public String login() {
    ...
    actor.setId( user.getUserName() );
    actor.getGroupActorIds().addAll( user.getGroupNames() );
    ...
}

7.4.3. 启动一个业务流程

使用 @CreateProcess 注解来启动一个业务流程实例。

@CreateProcess(definition="todo")
public void createTodo() { ... }

也可用使用pages.xml来启动一个业务流程。

<page>
    <create-process definition="todo" />
</page>

7.4.4. 任务分配

当一个流程执行到一个任务节点时,会创建任务实例。这些任务实例必须分配给用户或是用户组。我们可以手动编码指定actor id或是委托给一个Seam组件。

<task name="todo" description="#{todoList.description}">
    <assignment actor-id="#{actor.id}"/>
</task>

在这里例子中,我们简单的将任务分配给当前用户。也可以将任务分配给一批用户(pool actor):

<task name="todo" description="#{todoList.description}">
    <assignment pooled-actors="employees"/>
</task>

7.4.5. 任务列表

几个内置的Seam组件使得显示任务列表变得简单。pooledTaskInstanceList 是一个汇集任务集合,用户可以把这些任务分配给他们自己。

<h:dataTable value="#{pooledTaskInstanceList}" var="task">
    <h:column>
        <f:facet name="header">Description</f:facet>
        <h:outputText value="#{task.description}"/>
    </h:column>
    <h:column>
        <s:link action="#{pooledTask.assignToCurrentActor}" value="Assign" taskInstance="#{task}"/>
    </h:column>            	
</h:dataTable>

请注意,我们可以使用普通的JSF标签 <h:commandLink> 来替代 <s:link> 标签:

<h:commandLink action="#{pooledTask.assignToCurrentActor}"> 
    <f:param name="taskId" value="#{task.id}"/>
</h:commandLink>

pooledTask 组件是一个内置组件,它简单把任务分配给当前用户。

taskInstanceListForType组件包含一种特殊类型的任务,这些任务分配给当前用户:

<h:dataTable value="#{taskInstanceListForType['todo']}" var="task">
    <h:column>
        <f:facet name="header">Description</f:facet>
        <h:outputText value="#{task.description}"/>
    </h:column>
    <h:column>
        <s:link action="#{todoList.start}" value="Start Work" taskInstance="#{task}"/>
    </h:column>            	
</h:dataTable>

7.4.6. 执行一个任务

我们在监听方法上使用 @StartTask 或者 @BeginTask 注解来执行一个任务。

@StartTask
public String start() { ... }

我们也可以使用pages.xml来执行一个任务。

<page>
    <start-task />
</page>

这些注解启动一种特殊类型的对话,这个对话在整个业务流程中具有意义。通过该对话可以访问保存在业务流程上下文中的状态。

如果我们使用 @EndTask 注解来结束该对话,Seam会发出信号完成该任务。

@EndTask(transition="completed")
public String completed() { ... }

作为选择,我们还可以使用pages.xml

<page>
    <end-task transition="completed" />
</page>

也可以使用EL在pages.xml指定transition 。

此时,jBPM接受指令并且继续执行业务流程定义。(在更加复杂的流程中,在流程向下执行之前,可能还需要完成其他一些任务。)

若想对jBPM处理复杂业务流程的高级特性有一个更加彻底的认识,请参阅jBPM的参考文档。