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

Blog


    21 December

    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。当消息在流程实例更新的同一个事务中被发送时,所有的同步问题都要小心的处理。一些工作流系统在图中的所有节点间使用的是异步消息。但是在高通量环境中,这种算法能够为业务流程提供更高的灵活性和控制性。

    20 December

    JBPM术语:动作Action

    概述

    动作是在流程执行中的事件中的Java代码片段。你可以认为动作是可以在事件上发生的(注意,也可以在节点内发生)。为何要有动作这个概念呢?原因在于图虽然是软件需求间通讯的重要设备,但是图只是软件的一个视图或者投影,它隐藏了太多技术细节(流程图主要给业务分析员看或者编辑的),而动作是给流程图增加技术细节的机制(开发者需要用到它)。我们需要动作来装点图,这样的图才有意义。

    因为动作多少和事件有关系,我们看一下事件类型。主要的事件类型包括:进入节点,离开节点,走向转移

    注意:动作可以在事件中发生,也可以在节点中发生!而这两种情况非常不同!

    • 在事件中的动作当事件触发时执行,事件中的动作无法影响流程的控制,有点象观察者模式,动作是事件的监听器
    • 在节点中的动作必须负责传递流程的执行,因此能够影响流程的控制

    image

    我们看一下事件中动作的例子,假定我们需要在一个指定的转移中更新数据库,更新数据库是技术细节,业务分析员不关心这个。这个例子是如果开除一个员工,我们需要将他的工作证收回,在这个转移中,需要更新人力资源的数据库。

    public class RemoveEmployeeUpdate implements ActionHandler { // 实现ActionHandler
      public void execute(ExecutionContext ctx) throws Exception {
        // 从流程变量中获得开除的员工
        String firedEmployee = (String) ctx.getContextInstance().getVariable("fired employee");
        
        // by taking the same database connection as used for the jbpm updates, we 
        // 重用JBPM的事务来更新数据库
        Connection connection = ctx.getProcessInstance().getJbpmSession().getSession().getConnection();
        Statement statement = connection.createStatement();
        statement.execute("DELETE FROM EMPLOYEE WHERE ...");
        statement.execute(); 
        statement.close();
      }
    }
    <process-definition name="yearly evaluation">
    
      ...
      <state name="fire employee">
        <transition to="collect badge">
          <action class="com.nomercy.hr.RemoveEmployeeUpdate" />
        </transition>
      </state>
      
      <state name="collect badge">
      ...
      
    </process-definition>
    在这个例子中,我们实现了ActionHandler,同时将这个类作为动作放在名为收回工作证的转移中。

    JBPM术语:节点Node和节点类型NodeType

    概述

    流程由节点和转移组成。每个节点都有特定的类型。节点的类型决定了当一个流程执行在运行时到达这个节点后将发生什么。JBPM已经具有多个预定义的节点类型。当然你可以定制自己的节点从而实现特殊需求的节点行为。那么节点负责什么呢?

    节点主要负责两件事情:

    1. 执行普通的Java代码。通常这些Java代码和节点的功能相关。例如,创建一些任务实例,发送一些通知,更新一下数据库,等等
    2. 传递流程的执行process execution。

    传递流程的执行

    每个节点都有以下几种方式传递流程的执行:

    • 不传递流程的执行。如果该节点不传递流程的执行,那么该节点就是一个等待状态。
    • 将流程的执行传递给节点的其中一个转移。这意味着到达这个节点的token将被传递给这个节点的其中一个转移。这个过程是通过调用executionContext.leaveNode(String)来实现的。这意味这个节点能够执行一些定制的程序逻辑,然后自动的继续流程的执行,而无需等待,可以称之为自动化节点。
    • 创建流程执行的新路径。一个节点可以创建新的token.每个新的token都代表流程执行的一个新路径,每个token都可以被送到节点的转移中。这种节点的例子是分叉fork节点。
    • 终结流程执行的路径。一个节点可以决定终结流程执行的路径。这意味这token终止了,这个流程路径结束了。
    • 最常见的是,一个节点可以修改流程实例的整个运行结构。运行结构是包含一个token树的流程实例。每个token都代表一个流程执行的路径.一个节点能够创建和终止token,能够将每个token放入图的节点中,或者将token发送给转移。

    节点的类型

    JBPM包含了很多预定义和实现的节点类型,这包括:

    1. 任务节点 task-node, 任务节点类型的节点代表一个或多个人工操作的任务,当一个执行到达一个任务节点的时候,工作流参与者的任务列表中就会有任务实例被创建,然后这个节点将会作为一个等待状态,等待参与者操作任务,任务结束后将会触发流程执行的重新启动,换句话说,任务结束将会给token一个信号,让其激活,继续执行。
    2. 状态 state, 状态类型的节点本质上说就是等待状态。状态类型的节点和任务节点类型的区别在于流程执行到达状态类型的节点的时候,没有任何任务实例被创建。这在流程等待外部系统交互的时候非常有用。流程执行进入状态类型的节点后,将会一直等待,直到外部系统发送回一个响应信息,接着token.signal()方法会被调用,然后触发流程执行继续下去。
    3. 决策 decision, 决策
    4. 分叉 fork
    5. 合并 join
    6. 节点 Node(很奇怪不是么,有一个节点类型为节点的节点), 这种类型的节点主要是当你想要在节点中编写自己的代码的时候才用到的。这个类型为节点的节点有一个Action子元素,当流程执行到达这个节点后,这个Action将被执行。你在动作控制器ActionHandler中编写的代码能够做任何事情,但是它仍然需要负责流程执行的传递。当你希望使用Java API去实现一些对于业务分析员非常重要的业务逻辑的时候这种类型的节点非常有用。这种节点是能够在流程的图表示中出现的。那动作Action作比较,如果你使用动作action那么你添加的代码在流程的图表示中是不可见的。当你觉得逻辑对于业务分析员不重要的时候你可以将代码编写到动作action中。

    JBPM术语:图的元素GraphElement

    概念

    GraphElement是JBPM的一个定义,位于org.jbpm.graph.def包中。GrahpElement是个抽象类。

    GraphElement有一个名字name, 一个描述description.

    GraphElement有多个事件event,使用Map来存储。

    GraphElement有多个例外处理器exceptionhandler, 使用List来存储。

    GraphElement有一个流程定义processDefinition. 有意思的是:processDefinition本身就是GraphElement的子类。

    子类

    • Node 节点
    • Transition 转移
    • ProcessDefinition 流程定义
    • Task 任务

    重要的方法

    • executeAction(Action ation, ExecutionContext excContext)
    • fireEvent(String eventType, ExcecutionContext, exeContext)
    • fireAndPropagateEvent(String eventType, ExcecutionContext exeContext)

    JBPM术语定义:令牌token

    概念

    token是执行execution的一条路径, 它是运行时的概念,维护一个指向流程定义中节点的指针。

    我们通常通过ProcessInstance.getRootToken()或ProcessInstance.findToken(String token)来获得token。

    每个token可以有一个父token, 这意味着这个token可以由父token产生出来,这主要考虑的是流程可以分叉fork,因此每个分支都要有新的token来代表各自的执行路径。当然每个token都可以有多个子token,使用Map来存储它们。

    每个token都可以有多个注释comment,使用List来存储它们。

    每个token都有一个流程实例ProcessInstance, 也有一个子流程实例Sub ProcessInstance

    每个token都有一个节点Node, 表示当前节点

    原理

    信号signal用来指引一个令牌token去继续一个图的执行。当token接受到一个无名的信号时,它将离开当前的节点(注意包括节点的子类),走向默认的转移transition, 要是信号有参数指定转移的话,走向该指定的转移。注意在流程实例ProcessInstance中信号signal是委派给根令牌root token的!因此对流程实例的信号实际就是根令牌的信号。

    当token进入一个节点后(token维护一个Node),这个节点将被执行。注意节点node自身负责图的继续执行,当这个token离开这个节点就意味着这个节点中的图的继续执行已经完成了。每种节点类型node type都可以实现自己特定的图的继续执行。

    如果一个节点node并不传递执行,说明这个节点是一个状态

    重要的方法

    • get/setNode(Node node)
    • signal(), signal(String transitionName), signal(Transition transition)

    token.signal()方法能够激活token本身,然后让token离开当前状态,走向默认的转移transition

    带参数的signal()方法让token走向指定的转移transition

    • suspend()

    token.suspend()方法能够将token暂停挂起

    • resume()

    token.resume()方法能够将token重新启动

    18 December

    JBoss Seam事件机制(3):页面动作

    在JBoss Seam事件机制(1)概述中讲到,Seam中的页面动作发生在页面渲染之前,我们在WEB-INF/pages.xml文件中配置页面动作。我们还提到了page元素中的view-id不一定非要是JSP或者Facelet页面,这给整合其他WEB框架留了空间,并且能够让我们处理非JSF的请求。另外页面动作可以返回一个JSF输出,通过JSF输出来定制导向。除此之外,在page元素中,我们可以使用多个action元素来完成有条件的页面动作:

    <pages>
        <page view-id="/hello.jsp">
            <action execute="#{helloWorld.sayHello}" if="#{not validation.failed}"/>
            <action execute="#{hitCount.increment}"/>
        </page>
    </pages>
    页面参数
    我们知道一个JSF请求(表单提交)包括(封装)了动作和参数,所以一个页面动作也许需要参数。使用GET的请求可以做书签,因为
    页面参数都作为可读的请求参数处理了。但是JSF表单使用的是POST。Seam能够让我们将请求参数和模型属性相关联和映射:
    <pages>
          <page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
              <param name="firstName" value="#{person.firstName}"/>
              <param name="lastName" value="#{person.lastName}"/>
          </page>
      </pages>
    page元素中的param参数是双向的,就像JSF输入的值绑定一样。
    • 当一个非Faces请求(GET)请求某个视图view-id的时候,Seam经过正确的类型转换后将请求的参数值赋给模型对象.

    • 任何<s:link> 或者 <s:button> 都显式得包含请求参数。参数值由渲染阶段(<s:link>渲染)的值绑定计算结果来决定.

    • 请求某个视图view-id的任何带有<redirect/>的导向规则都显式得包含请求参数 . 参数值由调用应用阶段结束时的值绑定计算来决定.

    • 值通过任何对给定视图view-id的页面的JSF表单提交来传递. 这意味着视图参数有些类似于Faces请求中页面范围的上下文变量。

    关键的思想是我们如何从另一个页面到/hello.jsp(或者从/hello.jsp又回到/hello.jsp), 在值绑定中的模型的值仍然被记住,而不需要任何的对话或者其他的服务器端的状态。

    请求值的传递

    如果只是name属性被指定的话,请求参数使用PAGE页面上下文(没有和模型属性映射)来传递。

    <pages>
          <page view-id="/hello.jsp" action="#{helloWorld.sayHello}">
              <param name="firstName" /> // 没有value
              <param name="lastName" />
          </page>
    </pages>
    如果你想创建多层次主从模式的CRUD页面时,页面参数的传递就有必要了。你可以通过页面参数的传递来记住先前你在哪个视图上
    (例如你在保存页面上,必须记住上个页面你正在编辑的实体)
    • 如果参数以视图的页面参数列出的情况下,任何<s:link> 或者 <s:button> 显式的传递请求参数.

    • 值通过任何对给定视图view-id的页面的JSF表单提交来传递. 这意味着视图参数有些类似于Faces请求中页面范围的上下文变量。

    听起来太复杂了,这有必要么?当你用它的时候你就知道了。这值得下功夫去理解。页面参数是在非Faces请求间传递状态的最佳方式。如果你想让你的搜索结果页面也能做书签的话,我们就能够在同一个代码中使用它来处理POST和GET两种请求了。页面参数能够减少视图定义中请求参数列表的重复性,并且让在代码中处理重定向更加简单。

    对话和验证

    导向

    在Seam中,你可以使用JSF的faces-config.xml文件来设置标准的JSF向导规则。但是JSF的向导规则有很多缺点:

    • 在重定向的时候无法指定使用的请求参数.

    • 无法根据规则来启动或者结束对话.

    • 规则是根据动作方法返回的值来工作的,无法通过EL表达式来计算.

    还有一个问题就是如果在Seam中使用标准的JSF导向,业务逻辑将散落在pages.xml和faces-config.xml两个配置文件中,很散乱。因此建议只是用pages.xml来配置页面导向

    我们看一下如何转换:

    <navigation-rule> // JSF标准导向,在faces-config.xml中配置
        <from-view-id>/editDocument.xhtml</from-view-id>
        
        <navigation-case>
            <from-action>#{documentEditor.update}</from-action>
            <from-outcome>success</from-outcome>
            <to-view-id>/viewDocument.xhtml</to-view-id>
            <redirect/>
        </navigation-case>
        
    </navigation-rule>
    <page view-id="/editDocument.xhtml"> // 等价的Seam导向,在pages.xml中配置
        
        <navigation from-action="#{documentEditor.update}">
            <rule if-outcome="success">
                <redirect view-id="/viewDocument.xhtml"/>
            </rule>
        </navigation>
        
    </page>
    <page view-id="/editDocument.xhtml"> // 改良版1, 甚至不使用JSF返回的string
        
        <navigation from-action="#{documentEditor.update}" 
                       evaluate="#{documentEditor.errors.size}">
            <rule if-outcome="0"> // 如果错误数量为0,就代表OK
                <redirect view-id="/viewDocument.xhtml"/>
            </rule>
        </navigation>
        
    </page>
    <page view-id="/editDocument.xhtml"> // 改良版2,判断错误是否为空
        
        <navigation from-action="#{documentEditor.update}">
            <rule if="#{documentEditor.errors.empty}"> 
                <redirect view-id="/viewDocument.xhtml"/>
            </rule>
        </navigation>
    </page>
    <page view-id="/editDocument.xhtml"> // 如果你希望编辑后,结束对话
        
        <navigation from-action="#{documentEditor.update}">
            <rule if="#{documentEditor.errors.empty}">
                <end-conversation/>
                <redirect view-id="/viewDocument.xhtml"/>
            </rule>
        </navigation>
        
    </page>
    <page view-id="/editDocument.xhtml"> // 改良版3,或者你需要将参数传递下去
        // 如果我们结束了对话,后续的请求无法知道哪个document我们感兴趣,我们需要将document id作为参数传递
        <navigation from-action="#{documentEditor.update}">
            <rule if="#{documentEditor.errors.empty}">
                <end-conversation/>
                <redirect view-id="/viewDocument.xhtml">
                    <param name="documentId" value="#{documentEditor.documentId}"/>
                </redirect>
            </rule>
        </navigation>
        
    </page>
    值得注意的是:JSF对于输出返回为Null的采用特殊处理,如果返回为Null的话指向原页面(重新显示页面)。但在Seam中不太一样。
    <page view-id="/editDocument.xhtml">
        
        <navigation from-action="#{documentEditor.update}">
            <rule> // 这里的规则对非Null输出是有效的,但是对Null输出无效
                <render view-id="/viewDocument.xhtml"/>
            </rule>
        </navigation>
        
    </page>
    需要变成:
    <page view-id="/editDocument.xhtml">
        // 去掉rule元素
        <navigation from-action="#{documentEditor.update}">
            <render view-id="/viewDocument.xhtml"/>
        </navigation>
        
    </page>
    <page view-id="/editDocument.xhtml">
        // view-id也可以使用EL表达式指定
        <navigation if-outcome="success">
            <redirect view-id="/#{userAgent}/displayDocument.xhtml"/>
        </navigation>
        
    </page>
    细粒度的定义导向,页面动作和参数
    如果你有很多页面动作和参数,或者有很多导向规则,你可能想使用多个文件来配置。这时候你可以使用view-id来创建响应的page.xml
    文件。例如你有个view-id为/calc/calculator.jsp的页面,你可以创建calc/calculator.page.xml文件来配置,内容如下:
     
    <page action="#{calculator.calculate}"> // 使用page元素作为根元素
        <param name="x" value="#{calculator.lhs}"/> // 指定参数
        <param name="y" value="#{calculator.rhs}"/>
        <param name="op" converter="#{operatorConverter}" value="#{calculator.op}"/>
    </page>

    JBoss Seam的事件机制(2):内置的上下文事件

    在JBoss Seam的事件机制(1)概述中我们提到Seam提供了内置的上下文事件,我们可以为每个事件定制自己的监听器:

    关于验证的事件

    • org.jboss.seam.validationFailed, 当JSF验证失败的时候被调用

    关于变量的事件

    • org.jboss.seam.preSetVariable.<name>
    • org.jboss.seam.postSetVariable.<name>
    • org.jboss.seam.preRemoveVariable.<name>
    • org.jboss.seam.postRemoveVariable.<name>

    关于上下文的事件

    • org.jboss.seam.preDestroyContext.<SCOPE>
    • org.jboss.seam.postDestroyContext.<SCOPE>

    关于对话的事件

    • org.jboss.seam.beginConversation
    • org.jboss.seam.noConversation,当需要长时间运行的对话,缺没有的时候被调用
    • org.jboss.seam.endConversation

    关于页面流的事件

    • org.jboss.seam.beginPageflow.<name>
    • org.jboss.seam.endPageflow.<name>

    关于流程和任务的事件

    • org.jboss.seam.createProcess.<name>
    • org.jboss.seam.endProcess.<name>
    • org.jboss.seam.initProcess.<name>
    • org.jboss.seam.startTask.<name>
    • org.jboss.seam.endTask.<name>
    • org.jboss.seam.initTask.<name>

    关于组件的事件

    • org.jboss.seam.postCreate.<name>
    • org.jboss.seam.preDestroy.<name>

    关于JSF生命周期的事件

    • org.jboss.seam.beforePhase
    • org.jboss.seam.afterPhase
    • org.jboss.seam.postInitialization

    关于认证和安全的事件

    • org.jboss.seam.preAuthenticate.<name>
    • org.jboss.seam.postAuthenticate.<name>
    • org.jboss.seam.notLoggedIn
    • org.jboss.seam.remeberMe 如果Seam在客户端Cookie中发现username时发生

    关于例外的事件

    • org.jboss.seam.exceptionHandled.<type>
    • org.jboss.seam.exceptionHandled
    • org.jboss.seam.exceptionNotHandled

    关于事务的事件

    • org.jboss.seam.afterTransactionSucess.<name>
    • org.jboss.seam.afterTransactionSucess

    JBoss Seam的事件机制(1):概述

    JBoss Seam如何做到松耦合的架构呢?光有具备上下文的组件模型是不够的。还必须有:

    1. 事件模型。事件模型通过事件<-->监听器模式来构建。采用的方式是象JSF方法绑定一样的方式实现。
    2. 使用注释和拦截器来纵向切入实现业务逻辑的组件,从而达到松耦合的事件触发和响应

    JBoss Seam的组件模型本身就是为事件驱动的应用来设计的。这些事件都是通过JSF的表达式语言的方法绑定来映射的。在JBoss Seam中,事件可以分为:

    1. JSF事件,例如:
    2. <h:commandButton value="Click me!" action="#{helloWorld.sayHello}"/> JSF的按钮绑定动作
    3. jBPM状态转移事件,例如:
    4. <start-page name="hello" view-id="/hello.jsp">
          <transition to="hello">
              <action expression="#{helloWorld.sayHello}"/>
          </transition>
      </start-page> jBPM流程或页面流定义
    5. Seam页面动作,页面事件发生在我们渲染一个页面之前,我们通过在WEB-INF/pages.xml文件中配置页面动作。我们可以为一个特定的JSF视图ID (View ID)设定一个页面动作,例如:
    6. <pages>
          <page view-id="/hello/hello.jsp" action="#{helloWorld.sayHello1}"/>
      </pages>
      我们也可以使用通配符为一个页面模式设定页面动作,例如:
      <pages>
          <page view-id="/hello/*" action="#{helloWorld.sayHello2}"/>
      </pages>
      如果多个页面动作匹配当前的页面视图的话,那么所有动作按照最窄到最宽泛的顺序依次调用
      例如上述sayHello1,sayHello2动作都和hello.jsp匹配,那么在hello.jsp页面上先调用
      sayHello1然后调用sayHello2。页面动作可以返回一个JSF的输出outcome,如果outcome
      不为null的话,Seam将使用定义好的规则去导向到一个视图。更棒的是page元素中的视图
      view-id可以不是JSP页面或者Facelet页面,允许我们通过view-id来整合基于动作的Struts
      或者Webwork框架。你可以使用action元素指定多个条件页面动作(在某种条件下才执行的动作)
      例如:
      <pages>
          <page view-id="/hello.jsp">
              <action execute="#{helloWorld.sayHello}" if="#{not validation.failed}"/>
              <action execute="#{hitCount.increment}"/>
          </page>
      </pages>
       
    7. Seam组件驱动的事件,Seam组件之间可以直接通过方法调用来相互沟通。有状态的组件可以实现观察者模式。虽然如此,Seam提供的组件驱动的事件比直接通过方法调用更加松耦合。我们来看一下如何设置组件驱动的事件。我们可以在components.xml文件中设置事件监听器(观察者):
    8. <components>
          <event type="hello"> // 设置事件, Observable
              <action execute="#{helloListener.sayHelloBack}"/> // 设置监听器,Observer
              <action execute="#{logger.logHello}"/> // 设置监听器,Observer
          </event>
      </components> 你可能想问事件类型type="hello"是什么,它只是一个任意的字串。事件类型将在组件驱动事件的时候使用
      (raiseEvent)。当事件发生时,在事件中注册的动作将根据在components.xml文件中的次序依次调用。你可能要问:我如何
      在组件中触发一个事件呢?基本上,你有两种选择:使用内置的组件和通过注释。下列代码在运行HelloWord的sayHello方法时
      触发事件类型为"hello"的事件(在components.xml文件中配置):
      @Name("helloWorld")
      public class HelloWorld {
          public void sayHello() {
              FacesMessages.instance().add("Hello World!");
              Events.instance().raiseEvent("hello"); // Events 内置组件 触发hello事件
          }
      }
      @Name("helloWorld")
      public class HelloWorld {
          @RaiseEvent("hello") // 注释 触发hello事件
          public void sayHello() {
              FacesMessages.instance().add("Hello World!");
          }
      }
      我们看到HelloWorld的sayHello方法其实是事件的产生者。值得注意的是事件的产生者和事件的消费者
      (监听者)没有任何依赖性,例如下面的hello监听器和上述的HelloWord没有依赖:
      @Name("helloListener") // 在components.xml文件的hello事件中注册的监听者
      public class HelloListener {
          public void sayHelloBack() { // 当hello事件触发后,监听者调用该监听方法
              FacesMessages.instance().add("Hello to you too!");
          }
      }
      注意:如果你讨厌在components.xml文件配置太多事件和监听器,那么你也可以使用注释来配置:
      @Name("helloListener")
      public class HelloListener {
          @Observer("hello") // 指明使用sayHelloBack方法来监听hello事件
          public void sayHelloBack() {
              FacesMessages.instance().add("Hello to you too!");
          }
      }
      到这里你会发现我们根本没有用事件对象!实际上,事件产生者和事件消费者之间根本不必有事件对象来
      处理状态的问题。状态问题由Seam的上下文处理了,并且在Seam组件之间共享!
      不过,你要真的想要一个事件对象,可以象这样:
      @Name("helloWorld") // 事件产生者
      public class HelloWorld {
          private String name;
          public void sayHello() {
              FacesMessages.instance().add("Hello World, my name is #0.", name);
              Events.instance().raiseEvent("hello", name); // name是String参数,传递多个Object参数
          }
      }
      @Name("helloListener") // 事件监听者
      public class HelloListener {
          @Observer("hello")
          public void sayHelloBack(String name) {
              FacesMessages.instance().add("Hello #0!", name);
          }
      }
    9. Seam具有上下文的事件,Seam本身定义了一系列内置的事件。注意这些事件的名称都是字符串定义的:

              Seam组件可以监听(观察)这些事件,就像他们观察组件驱动的事件一样。我们将在别的文章中专门讲述。