20、使用Spring Web Flow(2)(Spring笔记)

三、组合起来:披萨流程

这里我们通过订购披萨的过程对流程进行说明。我们首先从构建一个高层次的流程开始,它定义了订购披萨的整体过程。接下来,我们会将这个流程拆分成子流程,这些子流程在较低层次定义了细节。

3.1 定义基本流程

一个新的披萨店决定允许用户在线订购以减轻店面电话的压力。当顾客访问Pizza站点时,他们需要进行用户识别,选择一个或更多披萨添加到订单中,提供支付信息然后提交订单并等待披萨送过来。如图所示。

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="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>元素来填充ordercustomer属性,将其设置为顾客子流程收到的输出。buildOrdertakePayment状态使用了不同的方式,使用<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 收集顾客信息

顾客在订购披萨的时候,我们需要知道用户的详细信息,特别是地址信息。这里可以根据电话号码进行查询(已注册过的用户),如果查询不到,则需要像用户询问。整个流程如下图所示:


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变量,这些链接分别触发流程中的acceptcancel事件。

3.2.5 存储顾客数据

这里使用addCustomer()方法对用户地址等数据进行保存,并将customer流程参数传递进去。

3.2.6 结束流程

这里给出了两个流程结束状态,而且使用<output>进行输出,这和整个披萨订购流程是相符合的。

3.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 支付

支付流程较为简单,首先是输入相关的支付信息,然后提交。具体流程如图所示。


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('_', ' '));
  }
}

说明:至此,整个流程就执行完了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容