三、组合起来,披萨订购流程
订购披萨的过程可以很好地定义在一个流程中。首先从构建一个高层次的流程开始,它定义了订购披萨的整体过程。
3.1 定义基本流程
一个新的披萨连锁店Spizza决定允许用户在线订购以减轻店面电话销售的压力。当顾客访问Spizza网站时,它们需要进行用户识别、选择一个或更多披萨添加到订单中、提供支付信息,然后提交订单并等待热呼又新鲜的披萨送过来。
图中方框代表了状态而箭头代表了转移。可以看到,订购披萨的整个流程非常简单且是线性的。在Spring Web Flow中,表示这个流程时很容易的。配置命名为order-flow.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.4.xsd"
start-state="identifyCustomer">
<var name="order" class="com.paner.pizza.domain.Order" />
<var name="flowActions" class="com.paner.pizza.controller.PizzaFlowActions"/>
<on-start>
<set name="conversationScope.pizzaFlowActions" value="flowActions"></set>
</on-start>
<!-- 顾客子流程 -->
<subflow-state id="identifyCustomer" subflow="lookupcustomer-flow">
<output name="customers" value="order.customer"/>
<transition on="customerReady" to="buildOrder" />
</subflow-state>
<!-- 订单子流程 -->
<subflow-state id="buildOrder" subflow="showorder-flow">
<input name="order" value="order"/>
<transition on="orderCreated" to="takePayment" />
</subflow-state>
<!-- 支付流程 -->
<subflow-state id="takePayment" subflow="takePayment-flow">
<input name="order" value="order"/>
<transition on="paymentTaken" to="saveOrder" />
</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 id="endState"></end-state>
<!-- 全局取消转移 -->
<global-transitions>
<transition on="cancle" to="endState"></transition>
</global-transitions>
</flow>
在流程定义中,你看到的第一件事就是order变量的声明。每次流程开始的时候,都会创建一个Order实例。Order类会包含关于订单的所有信息,包含顾客信息、订购的披萨以及支付详情。如程序清单:
package com.paner.pizza.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class Order implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private Payment payment;
private Customer customer;
private List<Pizza> pizzas;
public Order(){
pizzas = new ArrayList<Pizza>();
customer = new Customer();
}
public Payment getPayment() {
return payment;
}
public void setPayment(Payment payment) {
this.payment = payment;
}
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);
}
}
接下来申请的变量就是流程的行为逻辑:
package com.paner.pizza.controller;
import java.io.Serializable;
import com.paner.pizza.domain.Customer;
import com.paner.pizza.domain.Order;
import com.paner.pizza.domain.Payment;
import com.paner.pizza.domain.PaymentDetails;
import com.paner.pizza.service.CustomerNotFoundException;
public class PizzaFlowActions implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
public Customer lookupCustomer(String phoneNumber) throws CustomerNotFoundException{
if (phoneNumber.equalsIgnoreCase("15856915194")) {
Customer customer = new Customer();
customer.setName("paner");
customer.setPhoneNumber("15856915133");
customer.setAddress("china");
customer.setCity("南京");
customer.setState("effective");
customer.setZipCode("211202");
return customer;
}
else {
throw new CustomerNotFoundException("没找到该客户信息");
}
}
public Payment verifyPayment(PaymentDetails paymentDetails){
Payment payment = new Payment();
String temp = (paymentDetails.getPaymentType()).toString();
payment.setFinished(temp);
return payment;
}
public void saveOrder(Order order)
{
System.out.println("保存订单");
}
public boolean checkDeliveryArea(String zipCode)
{
if (zipCode.compareToIgnoreCase("999999")>0) {
return false;
}
return true;
}
public void addCustomer(Customer customer)
{
System.out.println("将顾客信息保存到数据库中:"+customer.getName());
}
}
流程定义的主要组成部分是流程的状态。默认情况下,流程定义文件中的第一个状态也会是流程访问中的第一个状态。在本例中,也就是identfyCustomer状态。
识别顾客、构建披萨订单以及支付这样的活动太复杂了,并不适合将其强行塞入一个状态。这是我们为何在后面将其单独定义为流程的原因。但是为了更好地整体了解披萨流程,这些活动都是以元素来进行展现的。
3.2 收集顾客信息
进入第一个子流程, 在每个披萨订单开始前的提问和回答阶段可以用下图的流程图表示:
这个流程比整体的披萨流程更有意思。这个流程不是线性的,而是在好几个位置根据不同的条件有了分支。
下面的程序展示了识别顾客的流程定义。
<?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.4.xsd">
<var name="customers" class="com.paner.pizza.domain.Customer" />
<!-- 欢迎顾客 -->
<view-state id="welcome" >
<transition on="phoneEntered" to="lookupCustomer"></transition>
</view-state>
<!-- 查找顾客 -->
<action-state id="lookupCustomer">
<evaluate result="customers" expression=
"pizzaFlowActions.lookupCustomer(requestParameters.phoneNumber)"></evaluate>
<transition to="registrationForm" on-exception=
"com.paner.pizza.service.CustomerNotFoundException"></transition>
<transition to="customerReady"></transition>
</action-state>
<!-- 注册新顾客 -->
<view-state id="registrationForm" model="customers">
<on-entry>
<evaluate expression="customers.phoneNumber = requestParameters.phoneNumber"></evaluate>
</on-entry>
<transition on="submit" to="checkDeliveryArea"></transition>
</view-state>
<!-- 检查配送区域 -->
<decision-state id="checkDeliveryArea">
<if then="addCustomer" test="pizzaFlowActions.checkDeliveryArea(customers.zipCode)"
else="deliveryWarning"
/>
</decision-state>
<!-- 显示配送警告 -->
<view-state id="deliveryWarning">
<transition on="accept" to="addCustomer"></transition>
</view-state>
<!-- 添加新顾客 -->
<action-state id="addCustomer">
<evaluate expression="pizzaFlowActions.addCustomer(customers)"></evaluate>
<transition to="customerReady"> </transition>
</action-state>
<end-state id="cancle"></end-state>
<end-state id="customerReady">
<output name="customers"/>
</end-state>
<!-- 全局取消转移 -->
<global-transitions>
<transition on="cancle" to="endState"></transition>
</global-transitions>
</flow>
这个流程包含了几个新技巧,包括首次使用的<decision-state>
元素。因为它是pizza流程的子流程,所以它也可以接受order对象作为输入。
询问电话号码
Welcome状态是一个很简单的视图状态。它欢迎访问Spizza网站的顾客并要求他们输入电话号码。这个状态并没有什么特殊的。它有两个转移:如果从视图触发phoneEntered事件,转移会将流程定向到lookupCustomer,另外一个就是在全局转移中定义用来响应cancel事件的cancel转移。
welcome状态的有趣之处在于视图本身。视图welcome定义在/WEB-INF/jsp/welcome.jsp中,如下所示:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Spizza</title>
</head>
<body>
<h2>欢迎来到Spizza!</h2>
<form:form>
<input type="hidden" name="_flowExecutorKey"
value="${flowExecutionKey}"/>
<label>电话号码:</label>
<input type="text" name="phoneNumber"/><br/>
<br>
<input type="submit" name="_eventId_phoneEntered"
value="查找"/>
</form:form>
</body>
</html>
这个简单的表提示用于输入其电话号码。但是有两个特殊的部分来驱动流程继续。
首先要注意的是隐藏的_flowExecutionKey输入域。当进入视图状态时,流程暂停并等待用户采取一些行为。赋予视图的流程执行键(flow execution key)就是一种返回流程的“回程票(claim ticker)”。当用户提交表单,流程执行键将会在_flowExecutionKey输入域中返回并在流程暂停的位置进行恢复。
还要注意提交按钮的名字。按钮名字的eventId部分是Spring Web Flow的一个线索,它表明了接下来要触发的时间。当点击这个按钮提交表单时,会触发phoneEntered事件进而转移到lookupCustomer。
下面是运行效果:(要运行还要把上面所缺少的类给补全,可以在这儿找到项目位置,然后根据需要把类补全)
开启订单
查找到会调到,订单区域:
查找不到顾客
回调到注册界面:
检查配送区域
在顾客提供其地址后,我们需要确认他的地址是否在配送范围之内。如果Spizza不能派送给他们,那么我们要让顾客知道并建议他们自己到店里自提披萨。
为了作出这个判断,我们使用了决策状态。决策状态checkDeliveryArea有一个if元素,它将顾客的邮政编码传递到pizzaFlowActions Bean的checkDeliveryArea()方法中。这个方法将返回一个Boolean值:如果顾客在配送区域内则是true,否则为false。、
如果顾客在配送区域内的话,那么流程转移到addCustomer状态。否则,顾客带入到deiveryWarning视图状态。deliveryWarning背后的而试图就是WEB-INF/jsp/deliveryWarning.jsp,如下所示:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Spizza</title>
</head>
<body>
<h2>地址不可达</h2>
<p>不好意思,你的地址不在我们的配送范围内;若继续订购,请到店里来自取。</p>
<a href="${flowExecutorUrl}&_eventId=accpet">继续,我将自己取。</a>
<a href="${flowExecutorUrl}&_eventId=cancel">取消订单</a>
</body>
</html>
运行效果:
存储顾客数据(这里可以用数据库存储用户信息)
当流程抵达addCustomer状态时,用户已经输入了他们的地址。为了将来使用,这个地址需要一某种方式存储起来。addCustomer状态有一个元素,它会调用pizzaFlowActions Bean的addCustomner()方法,并将customer流程参数传递进去。
一旦这个过程完成,他会执行默认的转移,流程将会转移到ID为customerReady的结束状态。
结束流程
通常,流程的结束状态并不会那么有意思。但是这个流程中,它不仅仅只有一个结束状态,而是两个。当子流程完成时,它会触发一个与结束状态ID相同的流程事件。如果流程只有一个结束状态的话,那么它始终会触发相同的事件。但是如果有两个或更多的结束状态,流程则会影响到调用状态的执行方向。
当customer流程完成所有的路径后,它最终会到达ID为customerReady的结束状态。当调用它的披萨流程恢复时,它会接受到一个customerReady事件,这个事件将使得流程转移到buildOrder状态。
要注意的是customerReady结束状态包含了一个元素。在流程中这个元素等同于java中的return语句。它从子流程中传递一些数据到调用流程。在本实例中,元素返回customer流程变量,这样披萨流程中的identifyCustomer子流程状态可以将其指定给订单。
另一方面,如果在识别顾客流程的任意地方触发了cancel事件,将会通过ID为cancel的结束状态退出流程,这也会在披萨流程中触发cancel事件并导致转移到披萨流程的结束状态。
3.3 构建订单
在识别完顾客之后,主流程的下一件事情就是确定他们想要什么类型的披萨。订单子流程就是用于提示用户创建披萨并将其放入订单中的,如下图:
showOrder状态位于订单子流程的中心位置。这是用户进入这个流程时看到的第一个状态,它也是用户在添加披萨到订单后要转移到的状态。它展现了订单的当前状态并允许用户添加其他的披萨到订单中。
要添加披萨到订单时,流程会转移到createPizza状态。这是另外一个视图状态,允许用户选择披萨的尺寸和面饼上面的配料。在这里,用户可以添加或取消披萨,两个事件都会是流程转移会showOrder状态。
从showOrder状态,用户可能提交订单也可能取消订单。两种选择都会结束订单子流程,但是主流程会根据选择不同进入不同的执行路径。
程序清单显示了如何将图8.4中所述的内容转变成Spring Web Flow定义。
<?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.4.xsd"
>
<input name="order" required="true"></input>
<view-state id="showOrder">
<transition on="createPizza" to="createPizza"></transition>
<transition on="checkout" to="orderCreated"></transition>
<transition on="cancel" to="endState"></transition>
</view-state>
<view-state id="createPizza" model="flowScope.pizza" view="createPizza">
<on-entry>
<set name="flowScope.pizza"
value="new com.paner.pizza.domain.Pizza()"></set>
<evaluate result="viewScope.toppingList"
expression="T(com.paner.pizza.domain.Topping).asList()"></evaluate>
</on-entry>
<transition on="addPizza" to="showOrder">
<evaluate expression="order.addPizza(flowScope.pizza)"></evaluate>
</transition>
<transition on="cancel" to="showOrder"></transition>
</view-state>
<end-state id="endState"></end-state>
<end-state id="orderCreated"></end-state>
</flow>
这个子流程实际上会操作主流程创建的Order对象。因此,我们需要以某种方式将Order从主流程传递到子流程。你可能还记得在上一节我们使用了元素来讲Order传递进流程。在这里,使用它接受Order对象。如果你觉得这个流程与Java中的方法有些类似,那么这里使用的元素就有效定义了这个子流程的签名。这个流程需要一个名为order的参数。
接下来我们看到showOrder状态,它是一个基本的视图状态并具有3个不同的转移,分别用于创建披萨、提交订单以及取消订单。
createPizza状态更有意思一些。它的视图是一个表单,这个表单可以添加新的Pizza对象订单中。元素添加了一个新的Pizza对象到流程作用域内,当表单提交时它将填充进订单。需要注意的是,这个视图状态引用的model是流程作用域内同一个Pizza对象。Pizza对象将绑定到创建披萨的表单中,如程序清单8.9所示:
<%@page import="com.paner.pizza.domain.Pizza"%>
<%@page import="java.util.List"%>
<%@page import="com.paner.pizza.domain.Order"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Spizza</title>
</head>
<body>
<h2>${order.getCustomer().getName()},请开启您的披萨订单</h2>
<%
Order order = (Order)request.getAttribute("order");
if(order.getPizzas().size()>0)
{
%>
<p> 你已经选择的披萨种类:</p>
<%
}
for(Pizza pizza : order.getPizzas())
{
%>
<p>尺寸:<%=pizza.getSize() %></p>
<p>馅料:<%=pizza.getTopping().toString() %></p>
<%
}
%>
<form:form>
<input type="hidden" name="_flowExecutorKey"
value="${flowExecutionKey}"/>
<input type="submit" name="_eventId_createPizza"
value="请选择订购的披萨"/>
<input type="submit" name="_eventId_checkout"
value="付钱"/>
<input type="submit" name="_eventId_cancel"
value="取消订单"/>
</form:form>
</body>
</html>
效果图:
选择过后:
有两种方法来结束这个流程。用户可以点击showOreder视图中的Cancel按钮或者Checkout按钮。这两种操作都会式流程转移到一个。但是选择的结束状态ID决定了退出这个流程时触发事件,进而最终确定了主流程的下一步行为。主流程要么基于cancel要么基于orderCreated事件进行状态转移。在前者情况下,外边的流程会结束;在后者的情况下,它将转移到takePayment子流程,这也是接下来我们要介绍的流程。
3.4 支付
在披萨流程要结束的时候,最后的子流程提示用户输入他们的支付信息。这个简单的流程如图所示:
像其他子流程一样,支付自流程也使用元素接受一个Order对象作为输入。
可以看到,进入支付子流程的时候,用户会到达takePayment状态。这是一个视图状态,在这里用户可以选择使用信用卡,支票或现金进行支付。提交支付信息后,将进入verifyPayment状态。这是一个行为状态,它将校验支付信息是否可以接受。
使用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.4.xsd">
<input name="order" required="true"/>
<view-state id="takePayment" model="flowScope.paymentDetails">
<on-entry>
<set name="flowScope.paymentDetails"
value="new com.paner.pizza.domain.PaymentDetails()"></set>
<evaluate result="viewScope.paymentTypeList"
expression="T(com.paner.pizza.domain.PaymentType).asList()"></evaluate>
</on-entry>
<transition on="paymentSubmitted" to="verifyPayment"></transition>
<transition on="cancel" to="cancel"></transition>
</view-state>
<action-state id="verifyPayment">
<evaluate result="order.payment"
expression="pizzaFlowActions.verifyPayment(flowScope.paymentDetails)"></evaluate>
<transition to="paymentTaken"></transition>
</action-state>
<end-state id="endState"></end-state>
<end-state id="paymentTaken"></end-state>
</flow>
付账的jsp内容:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Spizza</title>
</head>
<body>
<h2>请为您的披萨付款</h2>
<form:form commandName="paymentDetails">
<input type="hidden" name="_flowExecutionKey"
value="${flowExecutionKey} }" />
<label>支付方式:</label><br>
<form:radiobuttons path="paymentType" items="${paymentTypeList}"
/><br><br>
<input type="submit" class="button"
name="_eventId_paymentSubmitted" value="付款"/>
<input type="submit" class="button"
name="_eventId_cancek" value="取消"/>
</form:form>
</body>
</html>
运行效果图:
更多详细请看:<spring 实战>
的第八章 ,参考代码订单项目示例