20、使用Spring Web Flow(2)(Spring筆記)

三、組合起來:披薩流程

這里我們通過訂購披薩的過程對流程進行說明。我們首先從構(gòu)建一個高層次的流程開始,它定義了訂購披薩的整體過程。接下來,我們會將這個流程拆分成子流程,這些子流程在較低層次定義了細節(jié)。

3.1 定義基本流程

一個新的披薩店決定允許用戶在線訂購以減輕店面電話的壓力。當顧客訪問Pizza站點時,他們需要進行用戶識別,選擇一個或更多披薩添加到訂單中,提供支付信息然后提交訂單并等待披薩送過來。如圖所示。

1

下面給出實現(xiàn)披薩訂單的整體流程:

<?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變量的聲明。每次流程開始的時候,都會創(chuàng)建一個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方法這里省略
}

說明:默認情況下,流程定義文件中的第一個狀態(tài)也會是流程訪問中的第一個狀態(tài)。本例中,也就是identifyCustomer狀態(tài)(一個子流程)。也可以通過<flow>元素的start-state屬性顯示的指定任意狀態(tài)為開始狀態(tài):

<?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>

說明:

  • 識別顧客、構(gòu)造披薩訂單以及支付這樣的活動太復(fù)雜了,并不適合將其強行塞入一個狀態(tài)。后面將其單獨定義為流程。但是為了更好地整體了解披薩流程,這些活動都是以<subflow-state>元素來進行展現(xiàn)的。

  • 流程變量order將在前三個狀態(tài)中進行填充并在第四個狀態(tài)中進行保存。identifyCustomer子流程狀態(tài)使用<output>元素來填充ordercustomer屬性,將其設(shè)置為顧客子流程收到的輸出。buildOrdertakePayment狀態(tài)使用了不同的方式,使用<input>order流程變量作為輸入,這些子流程就能在其內(nèi)部填充order對象。

  • 在訂單得到顧客、一些披薩和支付細節(jié)后,saveOrder對其進行保存,是處理這個任務(wù)的行為狀態(tài)。訂單完成保存后,會轉(zhuǎn)移到thankCustomer,這是一個簡單的視圖狀態(tài),后臺使用"/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。結(jié)束鏈接將一個"_eventId"參數(shù)關(guān)聯(lián)到URL上,以便回到Web流程時觸發(fā)finished事件。這個事件將會讓流程到達結(jié)束狀態(tài)。流程將會在結(jié)束狀態(tài)完成。鑒于在流程結(jié)束后沒有下一步做什么的具體信息,流程將會重新從identifyCustomer狀態(tài)開始,以準備接受另一個披薩訂單。

3.2 收集顧客信息

顧客在訂購披薩的時候,我們需要知道用戶的詳細信息,特別是地址信息。這里可以根據(jù)電話號碼進行查詢(已注冊過的用戶),如果查詢不到,則需要像用戶詢問。整個流程如下圖所示:


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)" /><!--將結(jié)果填充到customer中-->
    <transition to="registrationForm" on-exception=
        "com.springinaction.pizza.service.CustomerNotFoundException" /><!--發(fā)生異常后的轉(zhuǎn)移-->
    <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>

說明:這里書中有些地方可能是有點問題的。此流程應(yīng)該說是整個訂購流程的第一個分支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"輸入域。當進入視圖狀態(tài)時,流程暫停并等待用戶采取一些行為。賦予視圖的流程執(zhí)行key(flow execution key)就是一種返回流程的“回程票”(claim ticket)。當用戶提交表單時,流程執(zhí)行key會在“_flowExecutionKey”輸入域中返回并在流程暫停的位置進行恢復(fù)。同時,按鈕的名字“_eventId_”部分是提供給Spring Web Flow的一個線索,它標明了接下來要觸發(fā)的事件。當點擊這個按鈕提交表單時,會觸發(fā)phoneEntered事件進而轉(zhuǎn)移到lookupCustomer

3.2.2 查找顧客

目前,lookupCustomer()方法的實現(xiàn)并不重要,這里只需要知道要么返回customer對象,要么拋出異常。返回的結(jié)果會通過result屬性設(shè)置到輸入變量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 檢查配送區(qū)域

在配送表單中需要告知顧客不能將披薩送到它們的地址(就是警告表單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>

說明:這里提供了兩個鏈接,允許用戶繼續(xù)訂單或者取消。通過使用與welcome狀態(tài)相同的flowExecutionUrl變量,這些鏈接分別觸發(fā)流程中的acceptcancel事件。

3.2.5 存儲顧客數(shù)據(jù)

這里使用addCustomer()方法對用戶地址等數(shù)據(jù)進行保存,并將customer流程參數(shù)傳遞進去。

3.2.6 結(jié)束流程

這里給出了兩個流程結(jié)束狀態(tài),而且使用<output>進行輸出,這和整個披薩訂購流程是相符合的。

3.3 構(gòu)建訂單

在識別完顧客之后,主流程的下一個事件就是確定它們想要干什么類型的披薩。訂單子流程就是用于提示用戶創(chuàng)建披薩并將其放入訂單中的,如圖所示。


3

整個流程較為簡單,下面看如何定義此流程:

<input name="order" required="true" />

<!-- Order -->
<view-state id="showOrder"><!-- 展現(xiàn)order的狀態(tài) -->
    <transition on="createPizza" to="createPizza" />
    <transition on="checkout" to="orderCreated" />
    <transition on="cancel" to="cancel" />
</view-state>

<view-state id="createPizza" model="flowScope.pizza"><!-- 創(chuàng)建披薩的狀態(tài)-->
    <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,這是主流程上創(chuàng)建的Order對象,這里使用<input>將主流程的Order對象進行了輸入。createPizza狀態(tài)的視圖是一個表單,這個表單可以添加新的Pizza對象到訂單中。<on-entry>元素添加了一個新的Pizza對象到流程作用域內(nèi),當表單提交時,表單的內(nèi)容會填充到該對象中。需要注意的是,這個視圖狀態(tài)引用的model是流程作用域內(nèi)的同一個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按鈕提交時,相關(guān)數(shù)據(jù)會綁定到Pizza對象中且觸發(fā)addPizza轉(zhuǎn)移。

3.4 支付

支付流程較為簡單,首先是輸入相關(guān)的支付信息,然后提交。具體流程如圖所示。


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>元素構(gòu)建了一個支付表單并創(chuàng)建了一個PaymentDetails實例。之后還創(chuàng)建了視圖作用域的paymentTypeList變量,這個變量是一個列表包含了PaymentType枚舉的值。這里需要調(diào)用一個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('_', ' '));
  }
}

說明:至此,整個流程就執(zhí)行完了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容