alartin's profileWindows Live 共享空间PhotosBlogListsMore ![]() | Help |
|
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 ExecutionJBPM中,图的执行模型是基于对流程定义的解释和命令模式链的。 对流程定义的解释意味这流程定义必须存储在数据库中(其实不尽然,但必须有某种存储机制)。在流程执行过程中需要流程定义的信息。需要注意的是JBPM使用hibernate的二级缓存来避免在运行时加载流程定义信息。这是因为一般来说流程定义不会轻易改变,所以我们能够将流程定义放入缓存中。 命令模式链意味着图中的每个节点需要负责将流程的执行传递下去。如果某个节点没有传递流程的执行,那么这个节点位于等待状态。 JBPM的设计思想是在流程实例上启动执行,然后这个流程执行将一直继续下去直到进入一个等待状态。 一个令牌token代表一个执行的路径。一个token拥有一个指向流程执行中的一个节点的指针。同样token也可以通过数据库将自己持久化。在等待状态时,token可以被持久化到数据库中。我们看一下JBPM是如何计算token的执行的。当有一个信号singal发送给token的时候,执行开始启动,接着执行通过命令模式链的方式沿着转移transition和节点node传递。在类定义中有相关的方法: 当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)来负责传递流程的执行。 简单的说:
需要注意的是整个的下一状态的计算(包括动作的调用)都是在客户端线程中完成的。一个常见的错误理解是所有的计算都必须在客户端线程中完成。对于任何的异步调用,你可以使用异步消息JMS。当消息在流程实例更新的同一个事务中被发送时,所有的同步问题都要小心的处理。一些工作流系统在图中的所有节点间使用的是异步消息。但是在高通量环境中,这种算法能够为业务流程提供更高的灵活性和控制性。 20 December JBPM术语:动作Action概述 动作是在流程执行中的事件中的Java代码片段。你可以认为动作是可以在事件上发生的(注意,也可以在节点内发生)。为何要有动作这个概念呢?原因在于图虽然是软件需求间通讯的重要设备,但是图只是软件的一个视图或者投影,它隐藏了太多技术细节(流程图主要给业务分析员看或者编辑的),而动作是给流程图增加技术细节的机制(开发者需要用到它)。我们需要动作来装点图,这样的图才有意义。 因为动作多少和事件有关系,我们看一下事件类型。主要的事件类型包括:进入节点,离开节点,走向转移。 注意:动作可以在事件中发生,也可以在节点中发生!而这两种情况非常不同!
我们看一下事件中动作的例子,假定我们需要在一个指定的转移中更新数据库,更新数据库是技术细节,业务分析员不关心这个。这个例子是如果开除一个员工,我们需要将他的工作证收回,在这个转移中,需要更新人力资源的数据库。 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已经具有多个预定义的节点类型。当然你可以定制自己的节点从而实现特殊需求的节点行为。那么节点负责什么呢? 节点主要负责两件事情:
传递流程的执行 每个节点都有以下几种方式传递流程的执行:
节点的类型 JBPM包含了很多预定义和实现的节点类型,这包括:
JBPM术语:图的元素GraphElement概念 GraphElement是JBPM的一个定义,位于org.jbpm.graph.def包中。GrahpElement是个抽象类。 GraphElement有一个名字name, 一个描述description. GraphElement有多个事件event,使用Map来存储。 GraphElement有多个例外处理器exceptionhandler, 使用List来存储。 GraphElement有一个流程定义processDefinition. 有意思的是:processDefinition本身就是GraphElement的子类。 子类
重要的方法
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并不传递执行,说明这个节点是一个状态。 重要的方法
token.signal()方法能够激活token本身,然后让token离开当前状态,走向默认的转移transition 带参数的signal()方法让token走向指定的转移transition
token.suspend()方法能够将token暂停挂起
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输入的值绑定一样。
关键的思想是我们如何从另一个页面到/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页面时,页面参数的传递就有必要了。你可以通过页面参数的传递来记住先前你在哪个视图上 (例如你在保存页面上,必须记住上个页面你正在编辑的实体)
听起来太复杂了,这有必要么?当你用它的时候你就知道了。这值得下功夫去理解。页面参数是在非Faces请求间传递状态的最佳方式。如果你想让你的搜索结果页面也能做书签的话,我们就能够在同一个代码中使用它来处理POST和GET两种请求了。页面参数能够减少视图定义中请求参数列表的重复性,并且让在代码中处理重定向更加简单。 对话和验证 导向 在Seam中,你可以使用JSF的faces-config.xml文件来设置标准的JSF向导规则。但是JSF的向导规则有很多缺点:
还有一个问题就是如果在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提供了内置的上下文事件,我们可以为每个事件定制自己的监听器: 关于验证的事件
关于变量的事件
关于上下文的事件
关于对话的事件
关于页面流的事件
关于流程和任务的事件
关于组件的事件
关于JSF生命周期的事件
关于认证和安全的事件
关于例外的事件
关于事务的事件
JBoss Seam的事件机制(1):概述JBoss Seam如何做到松耦合的架构呢?光有具备上下文的组件模型是不够的。还必须有:
JBoss Seam的组件模型本身就是为事件驱动的应用来设计的。这些事件都是通过JSF的表达式语言的方法绑定来映射的。在JBoss Seam中,事件可以分为:
<h:commandButton value="Click me!" action="#{helloWorld.sayHello}"/> JSF的按钮绑定动作
<start-page name="hello" view-id="/hello.jsp">
<transition to="hello">
<action expression="#{helloWorld.sayHello}"/>
</transition>
</start-page> jBPM流程或页面流定义
<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><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);
}
}
Seam组件可以监听(观察)这些事件,就像他们观察组件驱动的事件一样。我们将在别的文章中专门讲述。 |
|
|