三、组合起来:披萨流程
这里我们通过订购披萨的过程对流程进行说明。我们首先从构建一个高层次的流程开始,它定义了订购披萨的整体过程。接下来,我们会将这个流程拆分成子流程,这些子流程在较低层次定义了细节。
3.1 定义基本流程
一个新的披萨店决定允许用户在线订购以减轻店面电话的压力。当顾客访问Pizza
站点时,他们需要进行用户识别,选择一个或更多披萨添加到订单中,提供支付信息然后提交订单并等待披萨送过来。如图所示。
下面给出实现披萨订单的整体流程:
<?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">
<output name="customer" value="order.customer"/>
<transition on="customerReady" to="buildOrder" />
</subflow-state>
<!-- Order -->
<subflow-state id="buildOrder" subflow="pizza/order">
<input name="order" value="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">
<transition to="endState" />
</view-state>
<!-- End state -->
<end-state id="endState" />
<global-transitions>
<transition on="cancel" to="endState" />
</global-transitions>
</flow>
说明:在流程定义中,首先第一件事就是order
变量的声明。每次流程开始的时候,都会创建一个Order
实例。
package com.springinaction.pizza.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("order")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
private Customer customer;//顾客
private List<Pizza> pizzas;//pizza
private Payment payment;//支付详情
public Order() {
pizzas = new ArrayList<Pizza>();
customer = new Customer();
}
//getter 和setter方法这里省略
}
说明:默认情况下,流程定义文件中的第一个状态也会是流程访问中的第一个状态。本例中,也就是identifyCustomer
状态(一个子流程)。也可以通过<flow>
元素的start-state
属性显示的指定任意状态为开始状态:
<?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">
......
</flow>
说明:
识别顾客、构造披萨订单以及支付这样的活动太复杂了,并不适合将其强行塞入一个状态。后面将其单独定义为流程。但是为了更好地整体了解披萨流程,这些活动都是以
<subflow-state>
元素来进行展现的。流程变量
order
将在前三个状态中进行填充并在第四个状态中进行保存。identifyCustomer
子流程状态使用<output>
元素来填充order
的customer
属性,将其设置为顾客子流程收到的输出。buildOrder
和takePayment
状态使用了不同的方式,使用<input>
将order
流程变量作为输入,这些子流程就能在其内部填充order
对象。在订单得到顾客、一些披萨和支付细节后,
saveOrder
对其进行保存,是处理这个任务的行为状态。订单完成保存后,会转移到thankCustomer
,这是一个简单的视图状态,后台使用"/WEB-INF/flows/flowspizza/thankCustomer.jsp"
,如下:
<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>
说明:在此页面中,会感谢顾客的订购并为其提供一个完成流程的链接。这个链接展示了用户与流程交互的唯一办法。此处提供了一个flowExecutionUrl
变量,它包含了流程的URL
。结束链接将一个"_eventId"
参数关联到URL
上,以便回到Web
流程时触发finished
事件。这个事件将会让流程到达结束状态。流程将会在结束状态完成。鉴于在流程结束后没有下一步做什么的具体信息,流程将会重新从identifyCustomer
状态开始,以准备接受另一个披萨订单。
3.2 收集顾客信息
顾客在订购披萨的时候,我们需要知道用户的详细信息,特别是地址信息。这里可以根据电话号码进行查询(已注册过的用户),如果查询不到,则需要像用户询问。整个流程如下图所示:
下面对整个流程进行定义:
<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="customer" expression=
"pizzaFlowActions.lookupCustomer(requestParameters.phoneNumber)" /><!--将结果填充到customer中-->
<transition to="registrationForm" on-exception=
"com.springinaction.pizza.service.CustomerNotFoundException" /><!--发生异常后的转移-->
<transition to="customerReady" />
</action-state>
<view-state id="registrationForm" model="customer"><!--注册新顾客-->
<on-entry>
<evaluate expression=
"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">
<ouput name="customer">
</end-state>
<global-transition>
<transition on="cancel" to="cancel" />
</global-transition>
说明:这里书中有些地方可能是有点问题的。此流程应该说是整个订购流程的第一个分支identifyCustomer
,所以,对比来看,这里首先是定义了一个customer
的变量,而最后将customer
进行了输入,这与大流程是相符的。
3.2.1 询问电话号码
下面给出欢迎视图:"/WEB-INF/flows/pizza/customer/welcome.jsp"
:
<%@ 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)
就是一种返回流程的“回程票”(claim ticket)
。当用户提交表单时,流程执行key
会在“_flowExecutionKey”
输入域中返回并在流程暂停的位置进行恢复。同时,按钮的名字“_eventId_”
部分是提供给Spring Web Flow
的一个线索,它标明了接下来要触发的事件。当点击这个按钮提交表单时,会触发phoneEntered
事件进而转移到lookupCustomer
。
3.2.2 查找顾客
目前,lookupCustomer()
方法的实现并不重要,这里只需要知道要么返回customer
对象,要么抛出异常。返回的结果会通过result
属性设置到输入变量customer
中。
3.2.3 注册新顾客
注册表单如下:
<%@ 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>Customer Registration</h2>
<form:form commandName="order">
<input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"/>
<b>Phone number: </b><form:input path="customer.phoneNumber"/><br/>
<b>Name: </b><form:input path="customer.name"/><br/>
<b>Address: </b><form:input path="customer.address"/><br/>
<b>City: </b><form:input path="customer.city"/><br/>
<b>State: </b><form:input path="customer.state"/><br/>
<b>Zip Code: </b><form:input path="customer.zipCode"/><br/>
<input type="submit" name="_eventId_submit" value="Submit" />
<input type="submit" name="_eventId_cancel" value="Cancel" />
</form:form>
</body>
</html>
3.2.4 检查配送区域
在配送表单中需要告知顾客不能将披萨送到它们的地址(就是警告表单deliveryWarning.jsp
):
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head><title>Spring Pizza</title></head>
<body>
<h2>Delivery Unavailable</h2>
<p>The address is outside of our delivery area. The order may still be taken for carry-out.</p>
<a href="${flowExecutionUrl}&_eventId=accept">Accept</a> |
<a href="${flowExecutionUrl}&_eventId=cancel">Cancel</a>
</body>
</html>
说明:这里提供了两个链接,允许用户继续订单或者取消。通过使用与welcome
状态相同的flowExecutionUrl
变量,这些链接分别触发流程中的accept
或cancel
事件。
3.2.5 存储顾客数据
这里使用addCustomer()
方法对用户地址等数据进行保存,并将customer
流程参数传递进去。
3.2.6 结束流程
这里给出了两个流程结束状态,而且使用<output>进行输出,这和整个披萨订购流程是相符合的。
3.3 构建订单
在识别完顾客之后,主流程的下一个事件就是确定它们想要干什么类型的披萨。订单子流程就是用于提示用户创建披萨并将其放入订单中的,如图所示。
整个流程较为简单,下面看如何定义此流程:
<input name="order" required="true" />
<!-- Order -->
<view-state id="showOrder"><!-- 展现order的状态 -->
<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" />
说明:首先是输入一个order
,这是主流程上创建的Order
对象,这里使用<input>
将主流程的Order
对象进行了输入。createPizza
状态的视图是一个表单,这个表单可以添加新的Pizza
对象到订单中。<on-entry>
元素添加了一个新的Pizza
对象到流程作用域内,当表单提交时,表单的内容会填充到该对象中。需要注意的是,这个视图状态引用的model
是流程作用域内的同一个Pizza
对象(都是flowScope.pizza
)。
<%@ 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/>
<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
就不给出了。当通过Continue
按钮提交时,相关数据会绑定到Pizza
对象中且触发addPizza
转移。
3.4 支付
支付流程较为简单,首先是输入相关的支付信息,然后提交。具体流程如图所示。
下面给出流程定义:
<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" />
说明:对于选择支付信息的页面这里就不给出了。这里在进入视图时,<on-entry>
元素构建了一个支付表单并创建了一个PaymentDetails
实例。之后还创建了视图作用域的paymentTypeList
变量,这个变量是一个列表包含了PaymentType
枚举的值。这里需要调用一个adList()
方法。
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('_', ' '));
}
}
说明:至此,整个流程就执行完了。