如果你曾经订购过披萨,你可能会知道流程。他们首先会询问你的电话号码。电话号码除了能够让送货司机在找不到你家的时候打电话给你,还可以作为你在这个披萨店的标识。如果你是回头客,他们可以使用这个电话号码来查找你的地址,这样他们就知道将你的订单派送到什么地方了。
对于一个新的顾客来讲,查询电话号码不会有什么结果。所以接下来,他们将询问你的地址。这样,披萨店的人就会知道你是谁以及将披萨送到哪里。但是在问你要哪种披萨之前,他们要确认你的地址在他们的配送范围之内。如果不在的话,你需要自己到店里并取走披 萨。
在每个披萨订单开始前的提问和回答阶段可以用图 8.3 的流程图来表示。
这个流程比整体的披萨流程更有意思。这个流程不是线性的而是在好几个地方根据不同的条件有了分支。例如,在查找顾客后,流程可能结束(如果找到了顾客),也有可能转移到注册表单(如果没有找到顾客)。同样,在 check-DeliveryArea 状态,顾客有可能会被警告也有可能不被警告他们的地址在配送范围之外。
以下的程序清单展示了识别顾客的流程定义。
程序清单 8.4 使用 Web 流程来识别饥饿的披萨顾客
Copy <?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"/>
<!-- 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">
<output name="customer" />
</end-state>
<global-transitions>
<transition on="cancel" to="cancel" />
</global-transitions>
</flow>
这个流程包含了几个新的技巧,包括我们首次使用的 <decision-state>
元素。因为它是 pizza 流程的子流程,所以它也可以接受 Order 对象作为输入。
与前面一样,我们还是将这个流程的定义分解成一个个的状态,让我们从 welcome 状态开始。
询问电话号码
welcome 状态是一个很简单的视图状态,它欢迎访问 Spizza 站点的顾客并要求他们输入电话号码。这个状态并没有什么特殊的。它有两个转移:如果从视图触发 phoneEntered 事件的话,转移会将流程定向到 lookupCustomer,另外一个就是在全局转移中定义的用来响应 cancel 事件的 cancel 转移。
welcome 状态的有趣之处在于视图本身。视图 welcome 定义在 /WEBINF/flows/pizza/customer/welcome.jspx
中,如下所示。
Copy <%@ 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。
查找顾客
当欢迎表单提交后,顾客的电话号码将包含在请求参数中并准备用于查询顾客。lookupCustomer 状态的 <evaluate>
元素是查找发生的地方。它将电话号码从请求参数中抽取出来并传递到 pizzaFlowActions bean 的 lookup-Customer() 方法中。
目前,lookupCustomer() 的实现并不重要。只需知道它要么返回 Customer 对象,要么抛出 CustomerNotFoundException 异常。
在前一种情况下,Customer 对象将会设置到 customer 变量中(通过 result 属性)并且默认的转移将把流程带到 customerReady 状态。但是如果不能找到顾客的话,将抛出 CustomerNotFoundException 并且流程被转移到 registrationForm 状态。
注册新顾客
registrationForm 状态是要求用户填写配送地址的。就像我们之前看到的其他视图状态,它将被渲染成 JSP。JSP 文件如下所示。
Copy <%@ 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>
这并非我们在流程中看到的第一个表单。welcome 视图状态也为顾客展现了一个表单,那个表单很简单,并且只有一个输入域,从请求参数中获得输入域的值也很简单。但是注册表单就比较复杂了。
在这里不是通过请求参数一个个地处理输入域,而是以更好的方式将表单绑定到 Customer 对象上 —— 让框架来做所有繁杂的工作。
检查配送区域
在顾客提供其地址后,我们需要确认他的住址在配送范围之内。如果 Spizza 不能派送给他们,那么我们要让顾客知道并建议他们自己到店面里取走披萨。
为了做出这个判断,我们使用了决策状态。决策状态 checkDeliveryArea 有一个元素,它将顾客的邮政编码传递到 pizzaFlowActions bean 的check-DeliveryArea() 方法中。这个方法将会返回一个 Boolean 值:如果顾客在配送区域内则为 true,否则为 false。
如果顾客在配送区域内的话,那流程转移到 addCustomer 状态。否则,顾客被带入到 deliveryWarning 视图状态。deliveryWarning 背后的视图就是/WEB-INF/flows/pizza/customer/deliveryWarning.jspx
,如下所示:
程序清单 8.7 告知顾客不能将披萨配送到他们的地址
Copy <%@ 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>
在 deliveryWarning.jspx 中与流程相关的两个关键点就是那两个链接,它们允许用户继续订单或者将其取消。通过使用与 welcome 状态相同的 flow-ExecurtionUrl 变量,这些链接分别触发流程中的 accept 或 cancel 事件。如果发送的是 accept 事件,那么流程会转移到 addCustomer 状态。否则,接下来会是全局的取消转移,子流程将会转移到 cancel 结束状态。
稍后我们将介绍结束状态。让我们先来看看 addCustomer 状态。
存储顾客数据
当流程抵达 addCustomer 状态时,用户已经输入了他们的地址。为了将来使用,这个地址需要以某种方式存储起来(可能会存储在数据库中)。add-Customer 状态有一个 <evaluate>
元素,它会调用 pizzaFlowActions bean 的 addCustomer() 方法,并将 customer 流程参数传递进去。
一旦这个过程完成,会执行默认的转移,流程将会转移到 ID 为 customer-Ready 的结束状态。
结束流程
一般来讲,流程的结束状态并不会那么有意思。但是这个流程中,它不仅仅只有一个结束状态,而是两个。当子流程完成时,它会触发一个与结束状态 ID 相同的流程事件。如果流程只有一个结束状态的话,那么它始终会触发相同的事件。但是如果有两个或更多的结束状态,流程能够影响到调用状态的执行方向。
当 customer 流程走完所有正常的路径后,它最终会到达 ID 为 customer-Ready 的结束状态。当调用它的披萨流程恢复时,它会接收到一个customer-Ready 事件,这个事件将使得流程转移到 buildOrder 状态。
要注意的是 customerReady 结束状态包含了一个 <output>
元素。在流程中这个元素等同于 Java 中的 return 语句。它从子流程中传递一些数据到调用流程。在本示例中,<output>
元素返回 customer 流程变量,这样在披萨流程中,就能够将 identifyCustomer 子流程的状态指定给订单。另一方面,如果在识别顾客流程的任意地方触发了 cancel 事件,将会通过 ID 为 cancel 的结束状态退出流程,这也会在披萨流程中触发 cancel 事件并导致转移(通过全局转移)到披萨流程的结束状态。