Spring Web Flow是一个Web框架,它适用于元素按规定流程运行的程序。在本章中,我们将会探索Spring Web Flow并了解它如何应用于Spring Web框架平台。
Spring Web Flow是Spring MVC的扩展,它支持开发基于流程的应用程序。它将流程的定义与实现流程行为的类和视图分离开来。
8.1 在Spring中配置Web Flow
现在,还不支持在Java中配置Spring Web Flow,所以我们别无选择,只能在XML中对其进行配置。有一些bean会使用Spring Web Flow的Spring配置文件命名空间来进行声明。因此,我们需要在上下文定义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"
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.xsd">
</beans>
8.1.1 装配流程执行器
流程执行器(flow executor)驱动流程的执行。当用户进入一个流程时,流程执行器会为用户创建并启动一个流程执行实例。当流程暂停的时候(如为用户展示视图时),流程执行器会在用户执行操作后恢复流程。
在Spring中,<flow:flow-executor>
元素会创建一个流程执行器:
<flow:flow-executor id="flowExecutor"></flow:flow-executor>
尽管流程执行器负责创建和执行流程,但它并不负责加载流程定义。这个责任落在了流程注册表(flow registry)身上,接下来我们会创建它。
8.1.2 配置流程注册表
流程注册表(flow registry)的工作是加载流程定义并让流程执行器能够使用它们。我们可以在Spring中使用<flow:flow-registry>
配置流程注册表,如下所示:
<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:flowlocation-pattern>
元素的值,任何文件名以-flow.xml
结尾的XML文件都将视为流程定义。所有的流程都是通过其ID来进行引用的。这里我们使用了<flow:flow-location-pattern>
元素,流程的ID就是相对于base-path
的路径——或者双星号所代表的路径。
作为另一种方式,我们可以去除base-path
属性,而显式声明流程定义文件的位置:
<flow:flow-registry id="flowRegistry">
<flow:flow-location path="/WEB-INF/flows/springpizza-flow.xml" />
</flow:flow-registry>
如果想指定显式的ID,可以使用如下方式:
<flow:flow-registry id="flowRegistry">
<flow:flow-location id="pizza" path="/WEB-INF/flows/springpizza-flow.xml" />
</flow:flow-registry>
8.1.3 处理流程请求
我们在前一章曾经看到,DispatcherServlet一般将请求分发给控制器。但是对于流程而言,我们需要一个FlowHandlerMapping来帮助DispatcherServlet将流程请求发送给Spring Web Flow。在Spring应用上下文中,FlowHandlerMapping的配置如下:
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
<property name="flowRegistry" ref="flowRegistry"></property>
</bean>
你可以看到,FlowHandlerMapping装配了流程注册表的引用,这样它就能知道如何将请求的URL匹配到流程上。例如,如果我们有一个ID为pizza的流程,FlowHandlerMapping就会知道如果请求的URL模式(相对于应用程序的上下文路径)是/pizza
的话,就要将其匹配到这个流程上。
然而,FlowHandlerMapping的工作仅仅是将流程请求定向到Spring Web Flow上,响应请求的是FlowHandlerAdapter。FlowHandlerAdapter等同于Spring MVC的控制器,它会响应发送的流程请求并对其进行处理。FlowHandlerAdapter可以像下面这样装配成一个Spring bean,如下所示:
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
<property name="flowExecutor" ref="flowExecutor"></property>
</bean>
这个处理适配器是DispatcherServlet和Spring Web Flow之间的桥梁。它会处理流程请求并管理基于这些请求的流程。在这里,它装配了流程执行器的引用,而后者是为所处理的请求执行流程的。
8.2 流程的组件
在Spring Web Flow中,流程是由三个主要元素定义的:状态、转移和流程数据。
- 状态(State)是流程中事件发生的地点。如果你将流程想象成公路旅行,那状态就是路途上的城镇、路边饭店以及风景点。流程中的状态是业务逻辑执行、做出决策或将页面展现给用户的地方,而不是在公路旅行中买Doritos薯片和健怡可乐的所在。
- 如果流程状态就像公路旅行中停下来的地点,那转移(transition)就是连接这些点的公路。在流程中,你通过转移的方式从一个状态到另一个
状态。 - 当你在城镇之间旅行的时候,你可能要买一些纪念品,留下一些记忆并在路上取一些空的零食袋。类似地,在流程处理中,它要收集一些数
据:流程的当前状况。我很想将其称为流程的状态,但是在我们讨论流程的时候状态(state)已经有了另外的含义。
8.2.1 状态
Spring Web Flow定义了五种不同类型的状态,如表8.1所示。通过选择Spring Web Flow的状态几乎可以把任意的安排功能构造成会话式的Web应用。
表8.1 Spring Web Flow可供选择的状态
状态类型 | 它是用来做什么的 |
---|---|
行为(Action) | 行为状态是流程逻辑发生的地方 |
决策(Decision) | 决策状态将流程分成两个方向,它会基于流程数据的评估结果确定流程方向 |
结束(End) | 结束状态是流程的最后一站。一旦进入End状态,流程就会终止 |
子流程(Subflow) | 子流程状态会在当前正在运行的流程上下文中启动一个新的流程 |
视图(View) | 视图状态会暂停流程并邀请用户参与流程 |
视图状态
视图状态用于为用户展现信息并使用户在流程中发挥作用。实际的视图实现可以是Spring支持的任意视图类型,但通常是用JSP来实现的。
在流程定义的XML文件中,<view-state>
用于定义视图状态:
<view-state id="welcome" />
在这个简单的示例中,id属性有两个含义。它在流程内标示这个状态。除此以外,因为在这里没有在其他地方指定视图,所以它也指定了流程到达这个状态时要展现的逻辑视图名为welcome
。
如果你愿意显式指定另外一个视图名,那可以使用view属性做到这一点:
<view-state id="welcome" view="greeting" />
如果流程为用户展现了一个表单,你可能希望指明表单所绑定的对象。为了做到这一点,可以设置model属性:
<view-state id="takePayment" model="flowScope.paymentDetails" />
这里我们指定takePayment
视图中的表单将绑定流程作用域内的paymentDetails
对象。(稍后,我们将会更详细地介绍流程作用域和数据。)
行为状态
视图状态会涉及到流程应用程序的用户,而行为状态则是应用程序自身在执行任务。行为状态一般会触发Spring所管理bean的一些方法并根据方法调用的执行结果转移到另一个状态。
在流程定义XML中,行为状态使用<action-state>
元素来声明。这里是一个例子:
<action-state id="saveOrder">
<evaluate expression="pizzaFlowActions.saveOrder(order)"></evaluate>
<transition to="thankYou"></transition>
</action-state>
尽管不是严格需要的,但是<action-state>
元素一般都会有一个<evaluate>
作为子元素。<evaluate>
元素给出了行为状态要做的事情。expression
属性指定了进入这个状态时要评估的表达式。在本示例中,给出的expression
是SpEL表达式,它表明将会找到ID为pizzaFlowActions
的bean并调用其saveOrder()
方法。
Spring Web Flow与表达式语言
- 在这几年以来,Spring Web Flow在选择的表达式语言方面,经过了一些变化。在1.0版本的时候,Spring Web Flow使用的是对象图导航语言(Object-Graph Navigation Language ,OGNL)。随后的2.0版本又换成了统一表达式语言(Unified Expression Language ,Unified EL)。在2.1版本中,Spring Web Flow使用的是SpEL。
- 尽管可以使用上述的任意表达式语言来配置Spring Web Flow,但SpEL是默认和推荐使用的表达式语言。因此,当定义流程的时候,我们会选择使用SpEL,忽略掉其他的可选方案。
决策状态
有可能流程会完全按照线性执行,从一个状态进入另一个状态,没有其他的替代路线。但是更常见的情况是流程在某一个点根据流程的当前情况进入不同的分支。
决策状态能够在流程执行时产生两个分支。决策状态将评估一个Boolean类型的表达式,然后在两个状态转移中选择一个,这要取决于表达式会计算出true还是false。在XML流程定义中,决策状态通过<decision-state>
元素进行定义。典型的决策状态示例如下所示:
<decision-state id="checkDeliverArea">
<if test="pizzaFlowActions.checkDeliveryArea(customer.zipCode)"
then="addCustomer" else="deliveryWarning" />
</decision-state>
子流程状态
将流程分成独立的部分是个不错的主意。<subflow-state>
允许在一个正在执行的流程中调用另一个流程。这类似于在一个方法中调用另一个方法。
<subflow-state id="order" subflow="pizza/order">
<input name="order" value="order" />
<transition on="orderCreated" to="payment"></transition>
</subflow-state>
在这里,<input>
元素用于传递订单对象作为子流程的输入。如果子流程结束的<end-state>
状态ID为orderCreated
,那么流程将会转移到名为payment
的状态。
结束状态
最后,所有的流程都要结束。这就是当流程转移到结束状态时所做的。<end-state>
元素指定了流程的结束,它一般会是这样声明的:
<end-state id="customerReady"></end-state>
当到达<end-state>
状态,流程会结束。接下来会发生什么取决于几个因素:
- 如果结束的流程是一个子流程,那调用它的流程将会从
<subflow-state>
处继续执行。<end-state>
的ID将会用作事件触发从<subflow-state>
开始的转移。 - 如果
<end-state>
设置了view
属性,指定的视图将会被渲染。视图可以是相对于流程路径的视图模板,如果添加externalRedirect:
前缀的话,将会重定向到流程外部的页面,如果添加flowRedirect:
将重定向到另一个流程中。 - 如果结束的流程不是子流程,也没有指定
view
属性,那这个流程只是会结束而已。浏览器最后将会加载流程的基本URL地址,当前已没有活动的流程,所以会开始一个新的流程实例。
需要意识到流程可能会有不止一个结束状态。子流程的结束状态ID确定了激活的事件,所以你可能会希望通过多种结束状态来结束子流程,从而能够在调用流程中触发不同的事件。即使不是在子流程中,也有可能在结束流程后,根据流程的执行情况有多个显示页面供选择。
8.2.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="pizza.service.CustomerNotFoundException" to="registrationForm" />
全局转移
在创建完流程之后,你可能会发现有一些状态使用了一些通用的转移。
<global-transitions>
<transition on="cancel" to="endState" />
</global-transitions>
8.2.3 流程数据
当流程从一个状态进行到另一个状态时,它会带走一些数据。有时候,这些数据只需要很短的时间(可能只要展现页面给用户)。有时候,这些数据会在整个流程中传递并在流程结束的时候使用。
声明变量
流程数据保存在变量中,而变量可以在流程的各个地方进行引用。它能够以多种方式创建。在流程中创建变量的最简单形式是使用<var>
元素:
<var name="customer" class="pizza.domin.Customer" />
这里,创建了一个新的Customer实例并将其放在名为customer
的变量中。这个变量可以在流程的任意状态进行访问。
作为行为状态的一部分或者作为视图状态的入口,你有可能会使用<evaluate>
元素来创建变量。例如:
<evaluate result="viewScope.toppingsList" expression="T(pizza.domin.Topping).asList()" />
在本例中,<evaluate>
元素计算了一个表达式(SpEL表达式)并将结果放到了名为toppingsList
的变量中,这个变量是视图作用域的(我们将会在稍后介绍关于作用域的更多概念)。
类似地,<set>
元素也可以设置变量的值:
<set name="flowScope.pizza" value="new pizza.domin.Pizza()" />
``
```<set>```元素与```<evaluate>```元素很类似,都是将变量设置为表达式计算的结果。这里,我们设置了一个流程作用域内的```pizza```变量,它的值是Pizza对象的新实例。
**定义流程数据的作用域**
表8.2 Spring Web Flow的作用域
|范围|生命作用域和可见性|
|:----|:----|
|Conversation|最高层级的流程开始时创建,在最高层级的流程结束时销毁。被最高层级的流程和其所有的子流程所共享|
|Flow|当流程开始时创建,在流程结束时销毁。只有在创建它的流程中是可见的|
|Request|当一个请求进入流程时创建,在流程返回时销毁|
|Flash|当流程开始时创建,在流程结束时销毁。在视图状态渲染后,它也会被清除|
|View|当进入视图状态时创建,当这个状态退出时销毁。只在视图状态内是可见的|
当使用```<var>```元素声明变量时,变量始终是流程作用域的,也就是在定义变量的流程内有效。当使用```<set>```或```<evaluate>```的时候,作用域通过```name```或```result```属性的前缀指定。例如,将一个值赋给流程作用域的```theAnswer```变量:
<div class="se-preview-section-delimiter"></div>
```xml
<set name="flow.Scope.theAnswer" value="42" />
8.3 组合起来:披萨流程
8.3.1 定义基本流程
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" />
<subflow-state id="identifyCustomer" subflow="pizza/customer">
<output name="customer" value="order.customer" />
<transition on="customerReady" to="buildOrder"></transition>
</subflow-state>
<subflow-state id="buildOrder" subflow="pizza/order">
<input name="order" value="order" />
<transition on="orderCreated" to="takePayment"></transition>
</subflow-state>
<subflow-state id="takePayment" subflow="pizza/payment">
<input name="order" value="order" />
<transition on="paymentTaken" to="saveOrder"></transition>
</subflow-state>
<action-state id="saveOrder">
<evaluate expression="pizzaFlowActions.saveOrder(order)"></evaluate>
<transition to="thankCustomer"></transition>
</action-state>
<view-state id="thankCustomer">
<transition to="endState"></transition>
</view-state>
<!-- End state -->
<end-state id="endState" />
<global-transitions>
<transition on="cancel" to="endState" />
</global-transitions>
</flow>
在流程定义中,我们看到的第一件事就是order
变量的声明。每次流程开始的时候,都会创建一个Order实例。Order类会带有关于订单的所有信息,包含顾客信息、订购的披萨列表以及支付详情,如下面所示。
package pizza.domin;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class Order implements Serializable {
/**
*
*/
private static final long serialVersionUID = -1536374216267593026L;
private Customer customer;
private List<Pizza> pizzas;
private Payment payment;
public Order() {
pizzas = new ArrayList<Pizza>();
customer = new Customer();
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public List<Pizza> getPizzas() {
return pizzas;
}
public void setPizzas(List<Pizza> pizzas) {
this.pizzas = pizzas;
}
public void addPizza(Pizza pizza) {
pizzas.add(pizza);
}
public float getTotal() {
return 0.0f;
}
public Payment getPayment() {
return payment;
}
public void setPayment(Payment payment) {
this.payment = payment;
}
}
流程定义的主要组成部分是流程的状态。默认情况下,流程定义文件中的第一个状态也会是流程访问中的第一个状态。在本例中,也就是identifyCustomer
状态(一个子流程)。但是如果你愿意的话,你可以通过<flow>
元素的start-state
属性将任意状态指定为开始状态。
<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"
start-state="identifyCustomer">
...
</flow>
识别顾客、构造披萨订单以及支付这样的活动太复杂了,并不适合将其强行塞入一个状态。这是我们为何在后面将其单独定义为流程的原因。
但是为了更好地整体了解披萨流程,这些活动都是以<subflow-state>
元素来进行展现的。
流程变量order
将在前三个状态中进行填充并在第四个状态中进行保存。identifyCustomer
子流程状态使用了<output>
元素来填充order
的customer
属性,将其设置为顾客子流程收到的输出。buildOrder
和takePayment
状态使用了不同的方式,它们使用<input>
将order
流程变量作为输入,这些子流程就能在其内部填充order
对象。
在订单得到顾客、一些披萨以及支付细节后,就可以对其进行保存了。saveOrder
是处理这个任务的行为状态。它使用<evaluate>
来调用ID为pizzaFlowActions
的bean的saveOrder()
方法,并将保存的订单对象传递进来。订单完成保存后,它会转移到thankCustomer
。
thankCustomer
状态是一个简单的视图状态,后台使用了/WEB-INF/flows/pizza/ thankCustomer.jsp
这个JSP文件,如下所示:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html xmlns:jsp="http://java.sun.com/JSP/Page">
<jsp:output omit-xml-declaration="yes"></jsp:output>
<jsp:directive.page contentType="text/html; charset=UTF-8" />
<head>
<title>Spizza</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
状态开始,以准备接受另一个披萨订单。
8.3.2 收集顾客信息
在每个披萨订单开始前的提问和回答阶段可以用下面的流程图来表示。
这个流程比整体的披萨流程更有意思。这个流程不是线性的而是在好几个地方根据不同的条件有了分支。例如,在查找顾客后,流程可能结束
(如果找到了顾客),也有可能转移到注册表单(如果没有找到顾客)。同样,在checkDeliveryArea
状态,顾客有可能会被警告也有可
能不被警告他们的地址在配送范围之外。
<?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"
start-state="identifyCustomer">
<var name="customer" class="com.springinaction.pizza.domain.Customer" />
<view-state id="welcome">
<transition on="phoneEntered" to="lookupCustomer"></transition>
</view-state>
<action-state id="lookupCustomer">
<evaluate result="customer"
expression="pizzaFlowActions.lookupCustomer(requestParameters.phoneNumber)"></evaluate>
<transition to="registerationForm" on-exception="pizza.service.CustomerNotFoundException"></transition>
<transition to="customerReady"></transition>
</action-state>
<view-state id="registerationForm" model="customer">
<on-entry>
<evaluate expression="customer.phoneNumber = requestParameters.phoneNumber"></evaluate>
</on-entry>
<transition on="submit" to="checkDeliveryArea"></transition>
</view-state>
<decision-state id="checkDeliveryArea">
<if test="pizzaFlowActions.checkDeliveryArea(customer.zipCode)"
then="addCustomer" else="deliveryWarning"></if>
</decision-state>
<view-state id="deliveryWarning">
<transition on="accept" to="addCustomer"></transition>
</view-state>
<action-state id="addCustomer">
<evaluate expression="pizzaFlowActions.addCustomer(customer)"></evaluate>
<transition to="customerReady"></transition>
</action-state>
<end-state id="cancel"></end-state>
<end-state id="customerReady">
<output name="customer" />
</end-state>
<global-transitions>
<transition on="cancle" to="cancel"></transition>
</global-transitions>
</flow>
询问电话号码
welcome
状态是一个很简单的视图状态,它欢迎访问Spizza站点的顾客并要求他们输入电话号码。这个状态并没有什么特殊的。它有两个转移:如果从视图触发phoneEntered
事件的话,转移会将流程定向到lookupCustomer
,另外一个就是在全局转移中定义的用来响应cancel
事件的cancel
转移。
welcome
状态的有趣之处在于视图本身。视图welcome
定义在/WEB-INF/flows/pizza/customer/welcome.jsp
中,如下所示:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html xmlns:jsp="http://java.sun.com/JSP/Page">
<jsp:output omit-xml-declaration="yes"></jsp:output>
<jsp:directive.page contentType="text/html; charset=UTF-8" />
<head>
<title>Spizza</title>
</head>
<body>
<h2>Welcome to Spizza!!!</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)
就是一种返回流程的“回程票”(claim ticket)。当用户提交表单时,流程执行key
会在_flowExecutionKey
输入域中返回并在流程暂停的位置进行恢复。
还要注意的是提交按钮的名字。按钮名字的eventId
部分是提供给Spring Web Flow的一个线索,它表明了接下来要触发事件。当点击这个按钮提交表单时,会触发phoneEntered
事件进而转移到lookupCustomer
。
注册新顾客
registrationForm
状态是要求用户填写配送地址的。就像我们之前看到的其他视图状态,它将被渲染成JSP。JSP文件如下所示:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:spring="http://www.springframework.org/tags"
xmlns:form="http://www.springframework.org/tags/form">
<jsp:output omit-xml-declaration="yes"></jsp:output>
<jsp:directive.page contentType="text/html; charset=UTF-8" />
<head>
<title>Spizza</title>
</head>
<body>
<h2>Customer Registration</h2>
<form:form commandName="customer">
<input type="hidden" name="_flowExecutionKey"
value="${flowExecutionKey}" />
<b>Phone number:</b>
<form:input path="phoneNumber" />
<br />
<b>Name:</b>
<form:input path="name" />
<br />
<b>Address:</b>
<form:input path="address" />
<br />
<b>City:</b>
<form:input path="city" />
<br />
<b>State:</b>
<form:input path="state" />
<br />
<b>Zip Code:</b>
<form:input path="zipCode" />
<br />
<input type="submit" name="_eventId_submit" value="Submit" />
<input type="submit" name="_eventId_cancel" value="Cancel" />
</form:form>
</body>
</html>
检查配送区域
在顾客提供其地址后,我们需要确认他的住址在配送范围之内。如果Spizza不能派送给他们,那么我们要让顾客知道并建议他们自己到店面里取走披萨。
为了做出这个判断,我们使用了决策状态。决策状态checkDeliveryArea
有一个<if>
元素,它将顾客的邮政编码传递到pizzaFlowActions bean的checkDeliveryArea()
方法中。这个方法将会返回一个Boolean值:如果顾客在配送区域内则为true,否则为false。
如果顾客在配送区域内的话,那流程转移到addCustomer
状态。否则,顾客被带入到deliveryWarning
视图状态。deliveryWarning
背后的视图就是/WEB-INF/flows/pizza/customer/deliveryWarning.jsp
,如下所示:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html xmlns:jsp="http://java.sun.com/JSP/Page">
<jsp:output omit-xml-declaration="yes"></jsp:output>
<jsp:directive.page contentType="text/html; charset=UTF-8" />
<head>
<title>Spizza</title>
</head>
<body>
<h2>Delivery Unavailable</h2>
<p>The address is outside of our delivery area.You may still place
the order,but you will need to pick it up yourself.</p>
<![CDATA[<a href='${flowExecutionUrl}&_eventId=accept'>Continue,I'll pick up the order</a>|
<a href='${flowExecutionUrl}&_eventId=cancel'>Never mind</a>]]>
</body>
</html>
存储顾客数据
当流程抵达addCustomer
状态时,用户已经输入了他们的地址。为了将来使用,这个地址需要以某种方式存储起来(可能会存储在数据库中)。addCustomer
状态有一个<evaluate>
元素,它会调用pizzaFlowActions bean的addCustomer()
方法,并将customer
流程参数传递进去。
一旦这个过程完成,会执行默认的转移,流程将会转移到ID为customerReady
的结束状态。
结束流程
一般来讲,流程的结束状态并不会那么有意思。但是这个流程中,它不仅仅只有一个结束状态,而是两个。当子流程完成时,它会触发一个与结束状态ID相同的流程事件。如果流程只有一个结束状态的话,那么它始终会触发相同的事件。但是如果有两个或更多的结束状态,流程能够影响到调用状态的执行方向。
当customer
流程走完所有正常的路径后,它最终会到达ID为customerReady
的结束状态。当调用它的披萨流程恢复时,它会接收到一个customerReady
事件,这个事件将使得流程转移到buildOrder
状态。要注意的是customerReady
结束状态包含了一个<output>
元素。在流程中这个元素等同于Java中的return语句。它从子流程中传递一些数据到调用流程。在本示例中,<output>
元素返回customer
流程变量,这样在披萨流程中,就能够将identifyCustomer
子流程的状态指定给订单。另一方面,如果在识别顾客流程的任意地方触发了cancel
事件,将会通过ID为cancel
的结束状态退出流程,这也会在披萨流程中触发cancel
事件并导致转移(通过全局转移)到披萨流程的结束状态。
8.3.3 构建订单
在识别完顾客之后,主流程的下一件事情就是确定他们想要什么类型的披萨。订单子流程就是用于提示用户创建披萨并将其放入订单中的:
<?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">
<input name="order" required="true" />
<!-- Order -->
<view-state id="showOrder">
<transition on="createPizza" to="createPizza" />
<transition on="checkout" to="orderCreated" />
<transition on="cancel" to="cancel" />
</view-state>
<view-state id="createPizza" model="flowScope.pizza">
<on-entry>
<set name="flowScope.pizza" value="new com.springinaction.pizza.domain.Pizza()" />
<evaluate result="viewScope.toppingsList"
expression="T(com.springinaction.pizza.domain.Topping).asList()" />
</on-entry>
<transition on="addPizza" to="showOrder">
<evaluate expression="order.addPizza(flowScope.pizza)" />
</transition>
<transition on="cancel" to="showOrder" />
</view-state>
<!-- End state -->
<end-state id="cancel" />
<end-state id="orderCreated" />
</flow>
createPizza.jsp
:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<div>
<h2>Create Pizza</h2>
<form:form commandName="pizza">
<input type="hidden" name="_flowExecutionKey"
value="${flowExecutionKey}"/>
<b>Size: </b><br/>
<form:radiobutton path="size" label="Small (12-inch)" value="SMALL"/><br/>
<form:radiobutton path="size" label="Medium (14-inch)" value="MEDIUM"/><br/>
<form:radiobutton path="size" label="Large (16-inch)" value="LARGE"/><br/>
<form:radiobutton path="size" label="Ginormous (20-inch)" value="GINORMOUS"/><br/>
<br/>
<b>Toppings: </b><br/>
<form:checkboxes path="toppings" items="${toppingsList}"
delimiter="<br/>"/><br/><br/>
<input type="submit" class="button"
name="_eventId_addPizza" value="Continue"/>
<input type="submit" class="button"
name="_eventId_cancel" value="Cancel"/>
</form:form>
</div>
showOrder.jsp
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ 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>Your order</h2>
<h3>Deliver to:</h3>
<b>${order.customer.name}</b><br/>
<b>${order.customer.address}</b><br/>
<b>${order.customer.city}, ${order.customer.state}
${order.customer.zipCode}</b><br/>
<b>${order.customer.phoneNumber}</b><br/>
<hr/>
<h3>Order total: <fmt:formatNumber type="currency"
value="${order.total}"/></h3>
<hr/>
<h3>Pizzas:</h3>
<c:if test="${fn:length(order.pizzas) eq 0}">
<b>No pizzas in this order.</b>
</c:if>
<br/>
<c:forEach items="${order.pizzas}" var="pizza">
<li>${pizza.size} :
<c:forEach items="${pizza.toppings}" var="topping">
<c:out value="${topping}" />,
</c:forEach>
</li>
</c:forEach>
<form:form>
<input type="hidden" name="_flowExecutionKey"
value="${flowExecutionKey}"/>
<input type="submit" name="_eventId_createPizza"
value="Create Pizza" />
<c:if test="${fn:length(order.pizzas) gt 0}">
<input type="submit" name="_eventId_checkout"
value="Checkout" />
</c:if>
<input type="submit" name="_eventId_cancel"
value="Cancel" />
</form:form>
</body>
</html>
8.3.4 支付
像订单子流程一样,支付子流程也使用<input>
元素接收一个Order对象作为输入。
你可以看到,进入支付子流程的时候,用户会到达takePayment
状态。这是一个视图状态,在这里用户可以选择使用信用卡、支票或现金进行支付。提交支付信息后,将进入verifyPayment
状态。这是一个行为状态,它将校验支付信息是否可以接受。
<?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">
<input name="order" required="true"/>
<view-state id="takePayment" model="flowScope.paymentDetails">
<on-entry>
<set name="flowScope.paymentDetails"
value="new com.springinaction.pizza.domain.PaymentDetails()" />
<evaluate result="viewScope.paymentTypeList"
expression="T(com.springinaction.pizza.domain.PaymentType).asList()" />
</on-entry>
<transition on="paymentSubmitted" to="verifyPayment" />
<transition on="cancel" to="cancel" />
</view-state>
<action-state id="verifyPayment">
<evaluate result="order.payment" expression=
"pizzaFlowActions.verifyPayment(flowScope.paymentDetails)" />
<transition to="paymentTaken" />
</action-state>
<!-- End state -->
<end-state id="cancel" />
<end-state id="paymentTaken" />
</flow>
在流程进入takePayment
视图时,<on-entry>
元素将构建一个支付表单并使用SpEL表达式在流程作用域内创建一个PaymentDetails实例,这是支撑表单的对象。它也会创建视图作用域的paymentTypeList变量,这个变量是一个列表包含了PaymentType枚举的值。在这里,SpEL的T()
操作用于获得PaymentType类,这样就可以调用静态的asList()
方法。
takePayment.jsp
:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<div>
<script>
function showCreditCardField() {
var ccNumberStyle = document.paymentForm.creditCardNumber.style;
ccNumberStyle.visibility = 'visible';
}
function hideCreditCardField() {
var ccNumberStyle = document.paymentForm.creditCardNumber.style;
ccNumberStyle.visibility = 'hidden';
}
</script>
<h2>Take Payment</h2>
<form:form commandName="paymentDetails" name="paymentForm">
<input type="hidden" name="_flowExecutionKey"
value="${flowExecutionKey}"/>
<form:radiobutton path="paymentType"
value="CASH" label="Cash (taken at delivery)"
onclick="hideCreditCardField()"/><br/>
<form:radiobutton path="paymentType"
value="CHECK" label="Check (taken at delivery)"
onclick="hideCreditCardField()"/><br/>
<form:radiobutton path="paymentType"
value="CREDIT_CARD" label="Credit Card:"
onclick="showCreditCardField()"/>
<form:input path="creditCardNumber"
cssStyle="visibility:hidden;"/>
<br/><br/>
<input type="submit" class="button"
name="_eventId_paymentSubmitted" value="Submit"/>
<input type="submit" class="button"
name="_eventId_cancel" value="Cancel"/>
</form:form>
</div>
而PaymentType为:
package com.springinaction.pizza.domain;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.text.WordUtils;
public enum PaymentType {
CASH, CHECK, CREDIT_CARD;
public static List<PaymentType> asList() {
PaymentType[] all = PaymentType.values();
return Arrays.asList(all);
}
@Override
public String toString() {
return WordUtils.capitalizeFully(name().replace('_', ' '));
}
}
在面对支付表单的时候,用户可能提交支付也可能会取消。根据做出的选择,支付子流程将以名为paymentTaken
或cancel
的<end-state>
结束。就像其他的子流程一样,不论哪种<end-state>
都会结束子流程并将控制交给主流程。但是所采用<end-state>
的id将决定主流程中接下来的转移。
8.4 保护Web流程
Spring Web Flow中的状态、转移甚至整个流程都可以借助<secured>
元素实现安全性,该元素会作为这些元素的子元素。例如,为了保护对一个视图状态的访问,你可以这样使用<secured>
:
<view-satae id="restricted">
<secured attributes="ROLE_ADMIN" match="all" />
</view-state>
按照这里的配置,只有授予ROLE_ADMIN
访问权限(借助attributes
属性)的用户才能访问这个视图状态。attributes
属性使用逗号分隔的权限列表来表明用户要访问指定状态、转移或流程所需要的权限。match
属性可以设置为any
或all
。如果设置为any
,那么用户必须至少具有一个attributes
属性所列的权限。如果设置为all
,那么用户必须具有所有的权限。