Spring实战第八章
Spring Web Flow
Spring Web Flow是一个Web框架,它适用于元素按规定流程运行的程序。
Spring Web Flow是Spring MVC的扩展,它支持开发基于流程的应用程序。它将流程的定义与实现流程行为的类和视图分离开来。
Spring Web Flow是构建于Spring MVC基础之上的。这意味着所有的流程请求都需要首先经过Spring MVC的DispatcherServlet。需要在Spring应用上下文中配置一些bean来处理流程请求并执行流程。
只能在XML中对其进行配置。XML中配置的上下文定义XML文件中添加这个命名空间声明(位于与flows之中,位于spring应用上下文中):
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flow="http://www.springframework.org/schema/webflow-config" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/webflow-config http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
一、在Spring中配置Web Flow
1、装配流程执行器(位于flows中)
流程执行器(flow executor)驱动流程的执行。当用户进入一个流程时,流程执行器会为用户创建并启动一个流程执行实例。当流程暂停的时候(如为用户展示视图时),流程执行器会在用户执行操作后恢复流程。
在Spring中,<flow:flow-executor>元素会创建一个流程执行器:
<!-- Executes flows: the entry point into the Spring Web Flow system --> <flow:flow-executor id="flowExecutor" />
尽管流程执行器负责创建和执行流程,但它并不负责加载流程定义。
2、配置流程注册表
流程注册表(flow registry)的工作是加载流程定义并让流程执行器能够使用它们。我们可以在Spring中使用<flow:flow-registry>配置流程注册表,如下所示:
<!-- The registry of executable flow definitions --> <flow:flow-registry id="flowRegistry" base-path="/WEB-INF/flows"> <flow:flow-location-pattern value="/**/*-flow.xml" /> </flow:flow-registry>
在这里的声明中,流程注册表会在“/WEB-INF/flows”目录下查找流程定义,这是通过base-path属性指明的。依据<flow:flow-location-pattern>元素的值,任何文件名以“-flow.xml”结尾的XML文件都将视为流程定义。所有的流程都是通过其ID来进行引用的。这里我们使用了<flow:flow-location-pattern>元素,流程的ID就是相对于base-path的路径——或者双星号所代表的路径。图8.1展示了示例中的流程ID是如何计算的。????
<flow:flow-location-pattern value="/*-flow.xml" />
自动映射为下面的路径:
或者:作为另一种方式,我们可以去除base-path属性,而显式声明流程定义文件的位置:
<flow:flow-registry id="flowRegistry"> <flow:flow-location path="/WEB-INF/flows/springpizza.xml"/> </flow:flow-registry>
使用了<flow:flow-location>,path属性直接指明了“/WEB-INF/flows/springpizza.xml”作为流程定义。当我们这样配置的话,流程的ID是从流程定义文件的文件名中获得的,在这里就是springpizza。
再或者:
更显式地指定流程ID,那你可以通过<flow:flowlocation>元素的id属性来进行设置。
<flow:flow-registry id="flowRegistry"> <flow:flow-location id="pizza" path="/WEB-INF/flows/springpizza.xml"></flow:flow-location> </flow:flow-registry>
3、处理流程请求
对于流程而言,我们需要一个FlowHandlerMapping来帮助DispatcherServlet将流程请求发送给Spring Web Flow。
<!--Maps request paths to flows in the flowRegistry--> <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping"> <property name="flowRegistry" ref="flowRegistry" /> </bean>
FlowHandlerMapping装配了流程注册表的引用,这样它就能知道如何将请求的URL匹配到流程上。例如,如果我们有一个ID为pizza的流程,FlowHandlerMapping就会知道如果请求的URL模式(相对于应用程序的上下文路径)是“/pizza”的话,就要将其匹配到这个流程上。
然而,FlowHandlerMapping的工作仅仅是将流程请求定向到Spring Web Flow上,响应请求的是FlowHandlerAdapter。FlowHandlerAdapter等同于SpringMVC的控制器,它会响应发送的流程请求并对其进行处理。配置如下:
<!-- Dispatches requests mapped to flows to FlowHandler implementations --> <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter"> <property name="flowExecutor" ref="flowExecutor" /> </bean>
这个处理适配器是DispatcherServlet和Spring Web Flow之间的桥梁。它会处理流程请求并管理基于这些请求的流程。在这里,它装配了流程执行器的引用,而后者是为所处理的请求执行流程的。
总的flows.xml的配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flow="http://www.springframework.org/schema/webflow-config" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/webflow-config http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:component-scan base-package="com.springinaction.pizza.flow" /> <!-- Executes flows: the entry point into the Spring Web Flow system --> <flow:flow-executor id="flowExecutor" /> <!-- The registry of executable flow definitions --> <flow:flow-registry id="flowRegistry" base-path="/WEB-INF/flows"> <flow:flow-location-pattern value="/**/*-flow.xml" /> </flow:flow-registry> <!--Maps request paths to flows in the flowRegistry--> <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping"> <property name="flowRegistry" ref="flowRegistry" /> </bean> <!-- Dispatches requests mapped to flows to FlowHandler implementations --> <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter"> <property name="flowExecutor" ref="flowExecutor" /> </bean> </beans>
二、流程的组件
在Spring Web Flow中,流程是由三个主要元素定义的:状态、转移和流程数据。状态(State)是流程中事件发生的地点。流程中的状态是业务逻辑执行、做出决策或将页面展现给用户的地方。如果流程状态就像公路旅行中停下来的地点,那转移(transition)就是连接这些点的公路。在流程中,你通过转移的方式从一个状态到另一个状态。在流程处理中,它要收集一些数据:流程的当前状况。
1、状态
Spring Web Flow定义了五种不同类型的状态
状 态 类 型 | 它是用来做什么的 |
行为(Action) | 行为状态是流程逻辑发生的地方 |
决策(Decision) |
决策状态将流程分成两个方向,它会基于流程数据的评估结果 |
结束(End) | 结束状态是流程的最后一站。一旦进入End状态,流程就会终止 |
子流程(Subflow) |
子流程状态会在当前正在运行的流程上下文中启动一个新的流 |
视图(View) | 视图状态会暂停流程并邀请用户参与流程 |
1.1视图状态
视图状态用于为用户展现信息并使用户在流程中发挥作用。实际的视图实现可以是Spring支持的任意视图类型,但通常是用JSP来实现的。
在流程定义的XML文件中,<view-state>用于定义视图状态:
<view-state id="welcome"/>
此时:id属性有两个含义。它在流程内标示这个状态。除此以外,因为在这里没有在其他地方指定视图,所以它也指定了流程到达这个状态时要展现的逻辑视图名为welcome。
如果你愿意显式指定另外一个视图名,那可以使用view属性做到这一点:
<view-state id="welcome" view="greeds"/>
如果流程为用户展现了一个表单,你可能希望指明表单所绑定的对象。为了做到这一点,可以设置model属性:
<view-state id="takePlayment" model="flowScope.paymentsDetails"/>
指定takePayment视图中的表单将绑定流程作用域内的paymentDetails对象。
1.2、行为状态
行为状态则是应用程序自身在执行任务。行为状态一般会触发Spring所管理bean的一些方法并根据方法调用的执行结果转移到另一个状态。在流程定义XML中,行为状态使用<action-state>元素来声明。
<action-state id="saveOrder"> <evaluate expression="pizzaFlowActions.saveOrder(order)" /> <transition to="thankYou" /> </action-state>
<action-state>元素一般都会有一个<evaluate>作为子元素。<evaluate>元素给出了行为状态要做的事情。expression属性指定了进入这个状态时要评估的表达式。在本示例中,给出的expression是SpEL表达式,它表明将会找到ID为pizzaFlowActions的bean并调用其saveOrder()方法。
Spring Web Flow与表达式语言:SpEL是默认和推荐使用的表达式语言。
1.3决策状态
决策状态能够在流程执行时产生两个分支。决策状态将评估一个Boolean类型的表达式,然后在两个状态转移中选择一个,这要取决于表达式会计算出true还是false。在XML流程定义中,决策状态通过<decision-state>元素进行定义。
<decision-state id="checkDeliverArea"> <if test="pizzaFlowActions.checkDeliverArea(customer.zipCode)" then="addCustomer" else ="deliveryWarning"/> </decision-state>
<decision-state>并不是独立完成工作的。<if>元素是决策状态的核心。这是表达式进行评估的地方,如果表达式结果为true,流程将转移到then属性指定的状态中,如果结果为false,流程将会转移到else属性指定的状态中。
1.4子流程状态
将流程分成独立的部分是个不错的主意。<subflow-state>允许在一个正在执行的流程中调用另一个流程。
<!-- Order --> <subflow-state id="order" subflow="pizza/order"> <input name="order" value="order"/> <transition on="orderCreated" to="payment" /> </subflow-state>
<input>元素用于传递订单对象作为子流程的输入。如果子流程结束的<end-state>状态ID为orderCreated,那么流程将会转移到名为payment的状态。
sub
1.5结束状态
所有的流程都要结束。这就是当流程转移到结束状态时所做的。<end-state>元素指定了流程的结束,它一般会是这样声明的:
<end-state id="customerReady" />
当到达<end-state>状态,流程会结束。接下来会发生什么取决于几个因素:
如果结束的流程是一个子流程,那调用它的流程将会从<subflow-state>处继续执行。<end-state>的ID将会用作事件触发从<subflow-state>开始的转移。
如果<end-state>设置了view属性,指定的视图将会被渲染。视图可以是相对于流程路径的视图模板,如果添加“externalRedirect:”前缀的话,将会重定向到流程外部的页面,如果添加“flowRedirect:”将重定向到另一个流程中。
如果结束的流程不是子流程,也没有指定view属性,那这个流程只是会结束而已。浏览器最后将会加载流程的基本URL地址,当前已没有活动的流程,所以会开始一个新的流程实例
需要意识到流程可能会有不止一个结束状态。子流程的结束状态ID确定了激活的事件,所以你可能会希望通过多种结束状态来结束子流程,从而能够在调用流程中触发不同的事件。即使不是在子流程中,也有可能在结束流程后,根据流程的执行情况有多个显示页面供选择。
2转移
转移连接了流程中的状态。流程中除结束状态之外的每个状态,至少都需要一个转移,这样就能够知道一旦这个状态完成时流程要去向哪里。状态可以有多个转移,分别对应于当前状态结束时可以执行的不同的路径。
转移使用<transition>元素来进行定义,它会作为各种状态元素(<action-state>、<view-state>、<subflow-state>)的子元素。最简单的形式就是<transition>元素在流程中指定下一个状态:
<transition to="customerReady" />
属性to用于指定流程的下一个状态。如果<transition>只使用了to属性,那这个转移就会是当前状态的默认转移选项,如果没有其他可用转移的话,就会使用它。
更常见的转移定义是基于事件的触发来进行的。在视图状态,事件通常会是用户采取的动作。在行为状态,事件是评估表达式得到的结果。而在子流程状态,事件取决于子流程结束状态的ID。在任意的事件中(这里没有任何歧义),你可以使用on属性来指定触发转移的事件:
<transition on="phoneEntered" to="lookupCustomer"/>
如果触发了phoneEntered事件,流程将会进入lookupCustomer状态。
在抛出异常时,流程也可以进入另一个状态。例如,如果顾客的记录没有找到,你可能希望流程转移到一个展现注册表单的视图状态。以下的代码片段显示了这种类型的转移:
<transition on-exception="com.springinacyion,pizza.service.CustomerNoFoundExcetion" to="registrationForm"/>
属性on-exception类似于on属性,只不过它指定了要发生转移的异常而不是一个事件。
2.1全局转移
可以将<transition>元素作为<global-transitions>的子元素,把它们定义为全局转移。
<global-transitions> <transition on="cancel" to="endState"/> </global-transitions>
定义完这个全局转移后,流程中的所有状态都会默认拥有这个cancel转移。
3、流程数据
当流程从一个状态进行到另一个状态时,它会带走一些数据。有时候,这些数据只需要很短的时间(可能只要展现页面给用户)。有时候,这些数据会在整个流程中传递并在流程结束的时候使用。
3.1声明变量
流程数据保存在变量中,而变量可以在流程的各个地方进行引用。它能够以多种方式创建。在流程中创建变量的最简单形式是使用<var>元素:
<var name="customer" class="com.springinaction.pizza.domain.Customer"/>
创建了一个新的Customer实例并将其放在名为customer的变量中。这个变量可以在流程的任意状态进行访问。
作为行为状态的一部分或者作为视图状态的入口,可能会使用<evaluate>元素来创建变量。
<evaluate result="viewScope.paymentTypeList" expression="T(com.springinaction.pizza.domain.PaymentType).asList()" />
在本例中,<evaluate>元素计算了一个表达式(SpEL表达式)并将结果放到了名为paymentTypeList的变量中,这个变量是视图作用域的。
<set>元素也可以设置变量的值:
<set name="flowScope.paymentDetails" value="new com.springinaction.pizza.domain.PaymentDetails()" />
<set>元素与<evaluate>元素很类似,都是将变量设置为表达式计算的结果。
3.2定义流程数据的作用域
范 围 | 生命作用域和可见性 |
Conversation |
最高层级的流程开始时创建,在最高层级的流程结束时销毁。被最 |
Flow |
当流程开始时创建,在流程结束时销毁。只有在创建它的流程中是 |
Request | 当一个请求进入流程时创建,在流程返回时销毁 |
Flash |
当流程开始时创建,在流程结束时销毁。在视图状态渲染后,它也 |
View |
当进入视图状态时创建,当这个状态退出时销毁。只在视图状态内 |
当使用<var>元素声明变量时,变量始终是流程作用域的,也就是在定义变量的流程内有效。当使用<set>或<evaluate>的时候,作用域通过name或result属性的前缀指定。
二、实例分析:
注意:首先对于订购披萨是一个整个流程,流程的ID是pizza,在注册流程的时候,<flow:flow-location-pattern value="/**/*-flow.xml" />,其中的**即为pizza(输入url:项目名/pizza)
其余的customer、order和payment是其子流程。
创建程序的目录:
pizza.xml的配置如下:
<?xml version="1.0" encoding="UTF-8"?> <flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"> <var name="order" class="com.springinaction.pizza.domain.Order"/> 每次的流程的开始都是有一个新的订单 <!-- Customer --> <subflow-state id="identifyCustomer" subflow="pizza/customer"> 子流程状态。 调用customer子流程,完成对顾客订单的 <output name="customer" value="order.customer"/> 使用customer子流程中customer类型填充order中的customer属性 <transition on="customerReady" to="bulidOrder" /> 当customer子流程的结束状态是customerReady时,转移到bulidOrder子流程 </subflow-state> <!-- Order --> <subflow-state id="buildOrder" subflow="pizza/order"> <input name="order" value="order"/> 通过input使得主流程中的order来填充子流程中的order <transition on="orderCreated" to="takePayment" /> </subflow-state> <!-- Payment --> <subflow-state id="takePayment" subflow="pizza/payment"> <input name="order" value="order"/> <transition on="paymentTaken" to="saveOrder"/> </subflow-state> <action-state id="saveOrder"> <evaluate expression="pizzaFlowActions.saveOrder(order)" /> <transition to="thankCustomer" /> </action-state> <view-state id="thankCustomer"> 简单视图,“/WEB-INF/flows/pizza/thankCustomer.jsp” <transition to="endState" /> </view-state> <!-- End state --> <end-state id="endState" /> <global-transitions> <transition on="cancel" to="endState" /> </global-transitions> </flow>
其中对于:感谢界面
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <html> <head><title>Spring Pizza</title></head> <body> <h2>Thank you for your order!</h2> <![CDATA[ <a href='${flowExecutionUrl}&_eventId=finished'>Finish</a>
]]>
</body> </html>
Spring Web Flow为视图的用户提供了一个flowExecutionUrl变量,它包含了流程的URL。结束链接将一个“_eventId”参数关联到URL上,以便回到Web流程时触发finished事件。这个事件将会让流程到达结束状态。
流程将会在结束状态完成。鉴于在流程结束后没有下一步做什么的具体信息,流程将会重新从identifyCustomer状态开始,以准备接受另一个披萨订单。
其他流程:
1、收集顾客信息
<?xml version="1.0" encoding="UTF-8"?> <flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"> <var name="customer" class="com.springinaction.pizza.domain.Customer"/> <!-- Customer --> <view-state id="welcome"> <transition on="phoneEntered" to="lookupCustomer"/> <transition on="cancel" to="cancel"/> </view-state> <action-state id="lookupCustomer"> <evaluate result="order.customer" expression= "pizzaFlowActions.lookupCustomer(requestParameters.phoneNumber)" /> <transition to="registrationForm" on-exception= "com.springinaction.pizza.service.CustomerNotFoundException" /> <transition to="customerReady" /> </action-state> <view-state id="registrationForm" model="order" popup="true" > <on-entry> <evaluate expression= "order.customer.phoneNumber = requestParameters.phoneNumber" /> </on-entry> <transition on="submit" to="checkDeliveryArea" /> <transition on="cancel" to="cancel" /> </view-state> <decision-state id="checkDeliveryArea"> <if test="pizzaFlowActions.checkDeliveryArea(order.customer.zipCode)" then="addCustomer" else="deliveryWarning"/> </decision-state> <view-state id="deliveryWarning"> <transition on="accept" to="addCustomer" /> <transition on="cancel" to="cancel" /> </view-state> <action-state id="addCustomer"> <evaluate expression="pizzaFlowActions.addCustomer(order.customer)" /> <transition to="customerReady" /> </action-state> <!-- End state --> <end-state id="cancel" /> <end-state id="customerReady" /> </flow>
welcome状态是一个很简单的视图状态,它欢迎访问Spizza站点的顾客并要求他们输入电话号码。这个状态并没有什么特殊的。它有两个转移:如果从视图触发phoneEntered事件的话,转移会将流程定向到lookupCustomer,另外一个就是在全局转移中定义的用来响应cancel事件的cancel转移。
welcome视图:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <html> <head><title>Spring Pizza</title></head> <body> <h2>Welcome to Spring Pizza!!!</h2> <form:form> <input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"/> <input type="text" name="phoneNumber"/><br/> <input type="submit" name="_eventId_phoneEntered" value="Lookup Customer" /> </form:form> </body> </html>
这个简单的表单提示用户输入其电话号码。但是表单中有两个特殊的部分来驱动流程继续。
首先要注意的是隐藏的“_flowExecutionKey”输入域。当进入视图状态时,流程暂停并等待用户采取一些行为。赋予视图的流程执行key(flow execution key)就是一种返回流程的“回程票”(claimticket)。当用户提交表单时,流程执行key会在“_flowExecutionKey”输入域中返回并在流程暂停的位置进行恢复。
还要注意的是提交按钮的名字。按钮名字的“_eventId_”部分是提供给Spring Web Flow的一个线索,它表明了接下来要触发事件。当点击这个按钮提交表单时,会触发phoneEntered事件进而转移lookupCustomer。
疑惑:1、Spring web flow的视图解析器在什么地方;2、为什么不能正常拦截