1. 程式人生 > >《Spring實戰》學習筆記-第八章:使用Spring Web Flow

《Spring實戰》學習筆記-第八章:使用Spring Web Flow

第四版的第八章內容與第三版基本一致。

本章內容:

  • 建立會話式web應用程式
  • 定義流程狀態和行為
  • 保護web流程

網際網路的一個奇特之處就在於它很容易讓人迷失。有如此多的內容可以檢視和閱讀,而超連結是其強大魔力的核心所在。

有時候,web應用程式需要控制web衝浪者的導向,引導他們一步步地訪問應用。比如電子商務網站的付款流程,從購物車開始,應用程式會引導你依次經過配送詳情、賬單資訊以及最終的訂單確認。

Spring Web Flow是一個web框架,它適用於元素規定流程執行的程式。本章中,我們將會探索它是如何用於Spring Web框架平臺的。

其實我們可以使用任何的Web框架編寫流程化的應用程式,比如使用Struts構建特定的流程。但是這樣沒有辦法將流程與實現分開,你會發現流程的定義分散在組成流程的各個元素中,沒有特定的地方能夠完整地描述整個流程。

Spring Web Flow是Spring MVC的擴充套件,它支援開發基於流程的應用程式,可以將流程的定義和實現流程行為的類和檢視分離開來。

在介紹Spring Web Flow的時候,我們會暫且放下Spittr樣例,而使用生產披薩訂單的web程式。

使用的第一步是在專案中進行安裝,那麼就從安裝開始吧。

在Spring中配置Spring Web Flow

Spring Web Flow是基於Spring MVC構建的,這就意味著所有的流程請求都需要經過Spring MVC的DispatcherServlet。我們需要在Spring應用上下文中配置一些Bean來處理流程請求並執行流程。

現在還沒有支援使用Java來配置Spring Web Flow,所以沒得選,只能在XML中進行配置。有一些Bean會使用Spring Web Flow的Spring配置檔案名稱空間來進行宣告,因此我們需要在上下文定義XML檔案中新增相應的名稱空間:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flow="http://www.springframework.org/schema/webflow-config"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/webflow-config http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

聲明瞭名稱空間後,就可以準備裝配Web Flow的Bean了。

編寫流程執行器

顧名思義,流程執行器(flow executor )就是用來驅動流程的執行。當用戶進入到一個流程時,流程執行器會為該使用者建立並啟動一個流程執行器的例項。當流程暫停時(例如為使用者展示檢視時),流程執行器會在使用者執行操作後恢復流程。

在Spring中,<flow:flow-executor>元素可以建立一個流程執行器:
<flow:flow-executor id="flowExecutor" />

儘管流程執行器負責建立和執行流程,但它並不負責載入流程定義。這個要由流程登錄檔(flow registry)負責,下面會建立它。

配置流程登錄檔

流程登錄檔的工作就是載入流程定義,並讓流程執行器可以使用它們。可以在Spring中使用<flow:flow-registry>進行配置:

<flow:flow-registry id="flowRegistry" base-path="/WEB-INF/flows">
    <flow:flow-location-pattern value="/**/*-flow.xml" />
</flow:flow-registry>

正如這裡宣告的,流程登錄檔會在/WEB-INF/flows目錄下尋找流程定義,這個路徑是由base-path屬性指明的。根據<flow:flow-location-pattern>元素,任何以-flow.xml結尾的XML檔案都會被視為流程定義。

所有的流程都是通過其ID來進行引用的。使用<flow:flow-location-pattern>元素,流程的ID就是相對於base-path的路徑,或者是雙星號所代表的路徑,如下圖展示了流程ID是如何計算的:
在使用流程定位模式時,流程定義檔案相對於基本路徑的路徑將用作流程的id

另外,你也可以不使用base-path屬性,直接顯式地宣告流程定義檔案的位置:

<flow:flow-registry id="flowRegistry">
    <flow:flow-location path="/WEB-INF/flows/springpizza.xml" />
</flow:flow-registry>

這裡使用了<flow:flow-location>而不是<flow:flow-location-pattern>path屬性直接指定了/WEB-INF/flows/springpizza.xml為流程定義檔案。當這樣定義時,流程的ID是從流程定義檔案的檔名中獲取的,這就是springpizza

如果你希望更顯示地指定流程ID,那麼可以通過<flow:flow-location>元素的id屬性來進行設定。例如,要設定pizza作為流程ID,可以這樣進行配置:

<flow:flow-registry id="flowRegistry">
    <flow:flow-location id="pizza"
        path="/WEB-INF/flows/springpizza.xml" />
</flow:flow-registry>

處理流程請求

正如前面的章節中提到的,DispatcherServlet會將請求分發給控制器,但是對於流程而言,你需要FlowHandlerMapping來幫助DispatcherServlet將流程請求傳送給Spring Web Flow。FlowHandlerMapping的配置如下:

<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
    <property name="flowRegistry" ref="flowRegistry" />
</bean>

FlowHandlerMapping裝配了登錄檔的引用,這樣它就知道如何將請求的URL匹配到流程上。例如,如果有一個ID為pizza的流程,FlowHandlerMapping就會知道如果請求的URL是/pizza的話,就會將其匹配到這個流程上。

然而,FlowHandlerMapping的工作僅僅是將流程請求定向到Spring Web Flow,響應請求的是FlowHandlerAdapter,它等同於Spring MVC的控制器,會對流程請求進行響應並處理。FlowHandlerAdapter可以像下面這樣裝配成一個Spring Bean:

<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
    <property name="flowExecutor" ref="flowExecutor" />
</bean>

這個處理介面卡就是DispatcherServlet和Spring Web Flow之間的橋樑。它會處理流程請求並管理基於這些請求的流程。在這裡,它裝配了流程執行器的引用,而後者是為請求執行流程的。

現在已經配置了Spring Web Flow所需的Bean和元件,下面所需的就是真正的定義流程了。首先了解下流程的組成元素。

流程元件

在Spring Web Flow中,流程是由3個主要元素組成的:狀態(state)、轉移(transition)和流程資料(flow data)。狀態是流程中事件發生的地點。如果將流程想象成公路旅行,那麼狀態就是路途上的城鎮、路邊飯店以及風景點等。流程中的狀態是業務邏輯執行、做出決策或將頁面展示給使用者的地方,而不是在公路旅行中買薯片或者可樂這些行為。

如果說流程狀態是公路上停下來的地點,那麼轉移就是連線這些點的公路。在流程上,需要通過轉移從一個狀態到達另一個狀態。

在城鎮間旅行的時候,可能需要購買一些紀念品、留下一下回憶。類似的,在流程處理過程中,它要收集一些資料:流程當前狀況等。也許你很想將其稱為流程的狀態,但是我們定義的狀態已經有了另外的含義。

狀態

Spring Web Flow定義了5種不同的狀態,如下表所示。通過選擇Spring Web Flow的狀態幾乎可以把任意的安排功能構造成會話式的Web應用程式。儘管並不是所有的流程都需要下表中的狀態,但最終你可能會經常使用其中幾個。

狀態型別 作用
行為(Action) 流程邏輯發生的地方
決策(Decision) 決策狀態將流程分為兩個方向,它會基於流程資料的評估結果確定流程方向
結束(End) 結束狀態是流程的最後一站,進入End狀態,流程就會終止
子流程(Subflow) 子流程狀態會在當前正在執行的流程上下文中啟動一個新的流程
檢視(View) 檢視狀態會暫停流程並邀請使用者參與流程

首先了解下這些流程元素在Spring Web Flow定義中是如何表現的。

檢視狀態

檢視狀態用來為使用者展現資訊並使使用者在流程中發揮作用。實際的檢視實現可以是Spring支援的任意檢視型別,但通常是用JSP來實現的。

在流程定義檔案中,<view-state>用來定義檢視狀態:

<view-state id="welcome" />

在這個簡單的示例中,id屬性有兩個含義。其一,它定義了流程中的狀態。其二,因為這裡沒有其他地方指定檢視,那麼它就指定了流程到達這個狀態時要展現的邏輯檢視名稱為welcome。

如果要顯示地指定另外一個檢視名稱,那麼就可以使用view屬性:

<view-state id="welcome" view="greeting" />

如果流程為使用者展現了一個表單,你希望指定表單所繫結的物件,可以使用model屬性:

<view-state id="takePayment" model="flowScope.paymentDetails"/>

這裡指定了takePayment檢視將繫結流程範圍內的paymentDetails物件。

行為狀態

檢視狀態包括流程應用的使用者,而行為狀態則是應用程式自身在執行任務。行為狀態一般會觸發Spring所管理Bean的一些方法,並跟你講方法呼叫的執行結果轉移到另一個狀態。

在流程定義檔案中,行為狀態使用<action-state>元素來宣告:

<action-state id="saveOrder">
    <evaluate expression="pizzaFlowActions.saveOrder(order)" />
    <transition to="thankYou" />
</action-state>

儘管沒有嚴格要求,但是<action-state>元素一般都有一個<evaluate>子元素,該元素給出了行為狀態要做的事情,expression屬性指定了進入這個狀態時要評估的表示式。本例中,給出的是SpEL表示式,這表明它將會找到ID為pizzaFlowActions的Bean,並呼叫其saveOrder()方法。

決策狀態

流程有可能會按照線性執行下去,從一個狀態到另一個狀態,沒有其他的替代路線。但是更常見的是流程在某一個點根據流程當前情況進入不同的分支。

決策狀態能夠使得在流程執行時產生兩個分支,它會評估一個Boolean表示式,根據結果是true還是false在兩個狀態轉移中選擇一個。在流程定義檔案中,使用<decision-state>元素來定義決策狀態:

<decision-state id="checkDeliveryArea">
    <if test="pizzaFlowActions.checkDeliveryArea(customer.zipCode)"
        then="addCustomer"
        else="deliveryWarning" />
</decision-state>

<decision-state>並不是單獨工作的,<if>元素是其核心,它是進行表示式評估的地方,如果表示式結果為true,流程會轉向then屬性指定的狀態,為false會轉向else指定的狀態中。

子流程狀態

也許你不會將應用程式的所有邏輯都寫在一個方法裡,而是將其分散到多個類、方法一起其他結構中。

同樣的,將流程分成獨立的部分也是個不錯的主意。<subflow-state>元素允許在一個正在執行的流程中呼叫另一個流程:

<subflow-state id="order" subflow="pizza/order">
    <input name="order" value="order"/>
    <transition on="orderCreated" to="payment" />
</subflow-state>

這裡,<input>元素作為子流程的輸入被用於傳遞訂單物件。如果子流程結束的<end-state>狀態ID為orderCreated,那麼本流程就會轉移到ID為payment的狀態。

結束狀態

最後,所有的流程都要結束。這就是流程轉移到結束狀態時所做的。<end-state>元素指定了流程的結束:

<end-state id="customerReady" />

當流程到達<end-state>時,流程就會結束。接下來發生什麼要取決於以下幾個因素:
- 如果結束的流程是個子流程,那麼呼叫它的流程將會從<subflow-state>處繼續執行。<end-state>的ID將會用作時間觸發從<subflow-state>開始的轉移。
- 如果<end-state>設定了view屬性,那麼就會渲染指定的檢視。檢視可以是相對於流程的路徑,也可以是流程模板,使用externalRedirect:字首的會重定向到流程外部的頁面,而使用flowRedirect:字首的則會重定向到另外一個流程。
- 如果結束的流程不是子流程也沒有配置view屬性,那麼這個流程就會結束。瀏覽器最後將會載入流程的基本URL地址,同時,因為沒有活動的流程,所以會開始一個新的流程例項。

需要注意的是一個流程可能有多個結束狀態。因為子流程的結束狀態ID確定了啟用的事件,所以也許你會希望以多種結束狀態來結束子流程,從而能夠在呼叫流程中觸發不同的事件,即使不是在子流程中,也有可能在結束流程後,根據流程的執行情況有多個顯示頁面供選擇。

下面看一下流程是如何在狀態間遷移的,如何在流程中通過定義轉移來完成道路鋪設。

轉移

如前文所述,轉移連線了流程中的狀態。流程中除結束狀態外的每個狀態,至少需要一個轉移,這樣就知道在狀態完成時的走向。一個狀態也許有多個轉移,分別表示當前狀態結束時可以執行的不同路徑。

轉移是通過<transition>元素來定義的,作為其他狀態元素(<action-state><view-state><subflow-state>)的子元素。最簡單的形式就是<transition>元素在流程中指定下一個狀態:

<transition to="customerReady" />

屬性to用於指定流程中的下一個狀態。如果<transition>元素只使用了to屬性,那麼這個轉移就會是當前狀態的預設轉移選項,如果沒有其他可用轉移的話,就會使用它。

更為常見的轉移定義是基於事件的觸發來進行的。在檢視狀態,事件通常會是使用者採取的動作。在行為狀態,事件是評估表示式得到的結果。而在子流程狀態,事件取決於子流程結束狀態的ID。在任意事件中,你可以使用on屬性來指定觸發轉移的事件:

<transition on="phoneEntered" to="lookupCustomer"/>

在示例中,如果觸發了phoneEntered事件流程,就會進入lookupCustomer狀態。

在丟擲異常時,流程也可能進入另一種狀態。例如,如果沒有找到顧客的記錄,你可能希望流程轉移到一個顯示登錄檔單的檢視狀態,如下面:

<transition on-exception="com.springinaction.pizza.service.CustomerNotFoundException"
    to="registrationForm" />

屬性on-exception和屬性on十分類似,它是指定了要發生轉移的異常而不是一個事件。

全域性轉移

在建立完流程後,也許你會發現有些狀態使用了一些通用的轉移。例如在整個流程中到處都有如下轉移:

<transition on="cancel" to="endState" />

與其在多個流程狀態中重複通用的轉移,不如將其作為<globaltransitions>的子元素,從而作為全域性轉移

<global-transitions>
    <transition on="cancel" to="endState" />
</global-transitions>

定義完全域性轉移,流程中所有的狀態都會預設擁有這個cancel轉移。

流程資料

當流程從一個狀態到達另一個狀態時,它會帶走一些資料。有時這些資料很快就會被使用,比如直接展示給使用者,有時這些資料需要在整個流程中傳遞並在流程結束時使用。

宣告變數

流程資料是儲存在變數中的,而變數可以在流程的任意位置進行引用,並且可以以多種方式進行建立。其中最簡單的方式就是使用<var>元素:

<var name="customer" class="com.springinaction.pizza.domain.Customer"/>

這裡建立了一個新的Customer例項並將其放在customer變數中,這個變數可以在流程的任意狀態下進行訪問使用。

作為行為狀態的一部分或者說作為檢視狀態的入口,也可以使用<evaluate>元素來建立變數:

<evaluate result="viewScope.toppingsList"
    expression="T(com.springinaction.pizza.domain.Topping).asList()" />

這裡<evaluate>元素計算了一個SpEL表示式,並將結果放到toppingsList變數中,這個變數是檢視作用域的。

類似的,<set>元素也可以設定變數的值:

<set name="flowScope.pizza"
    value="new com.springinaction.pizza.domain.Pizza()" />

<set>元素與<evaluate>元素類似,都是講變數設定為表示式計算的結果。這裡我們設定了一個流程範圍的pizza變數,它的值為Pizza物件的新例項。

流程資料的作用域

流程中所攜帶的資料都有其各自的生命週期,這取決於儲存資料的變數本身的作用域,如下表:

範圍 生命週期
Conversation 最高層級的流程開始時建立,在最高層級的流程結束時銷燬。由最高層級的流程和其所有的子流程所共享
Flow 當流程開始時建立,在流程結束時銷燬。只在建立它的流程中是可見的
Request 當一個請求進入流程時建立,流程返回時銷燬
Flash 流程開始時建立,流程結束時銷燬。在檢視狀態解析後,才會被清除
View 進入檢視狀態時建立,退出這個狀態時銷燬,只在檢視狀態內可見

當使用<var>元素宣告變數時,變數始終是流程作用域的,也就是在流程作用域內定義變數。當使用<set><evaluate>時,作用域通過name或result屬性的字首指定。例如,將一個值賦給流程作用域的theAnswer變數:

<set name="flowScope.theAnswer" value="42"/>

到目前為止,我們已經看到了Web流程的所有原材料,下面要將其進行整合了,完成一個完整的流程。

組合起來:披薩流程

首先從構建一個高層次的流程開始,它定義了訂購披薩的整體流程,然後將其拆分為多個子流程。

定義基本流程

當顧客訪問Spizza網站時,他們需要進行使用者識別、選擇一個或多個披薩新增到訂單、提供支付資訊,然後提交訂單,等待披薩上來,如下圖:
網上購買披薩的流程

下面展示Spring Web 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.3.xsd">
    <var name="order" class="com.springinaction.pizza.domain.Order" />
    <!-- 呼叫顧客子流程 -->
    <subflow-state id="identifyCustomer" subflow="pizza/customer">
        <output name="customer" value="order.customer" />
        <transition on="customerReady" to="buildOrder" />
    </subflow-state>
    <!-- 呼叫訂單子流程 -->
    <subflow-state id="buildOrder" subflow="pizza/order">
        <input name="order" value="order" />
        <transition on="orderCreated" to="takePayment" />
    </subflow-state>
    <!-- 呼叫支付子流程 -->
    <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 id="endState" />
    <!-- 全域性取消轉移 -->
    <global-transitions>
        <transition on="cancel" to="endState" />
    </global-transitions>
</flow>

流程定義中的第一件事就是宣告order變數。每次流程開始的時候都會建立一個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;
   private Payment payment;

   public Order() {
      pizzas = new ArrayList<Pizza>();
      customer = new Customer();
   }

   //getters and setters
}   

流程定義的主要組成部分是流程的狀態,預設情況下,流程定義檔案中的第一個狀態會是流程訪問的第一個狀態。本例中就是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.3.xsd"
    start-state="identifyCustomer">
    ...
</flow>

識別顧客、構建披薩訂單和支付這樣的活動比較複雜,並不適合將其直接放在一個狀態,而是以<subflow-state>元素展現的。

流程變數order將在前3個狀態中進行填充並在第4個狀態中進行儲存。identifyCustomer子流程使用了<output>元素來填充order的customer屬性,將其設定為呼叫顧客子流程收到的輸出。buildOrder和takePayment狀態使用了不同的方式,它們使用<input>將order流程變數作為輸入,這些子流程就能在其內部填充order物件。

在訂單得到顧客、披薩以及支付資訊後,就可以對其進行儲存。saveOrder是處理這個任務的行為狀態。它使用<evaluate>來呼叫ID為pizzaFlowActions的Bean的saveOrder()方法,並將儲存的訂單物件傳遞進來。訂單完成儲存後會轉移到thankCustomer。

thankCustomer狀態是一個簡單的檢視狀態,後臺使用了/WEB-INF/flows/pizza/thankCustomer.jsp檔案進行展示:

<html xmlns:jsp="http://java.sun.com/JSP/Page">
    <jsp:output omit-xml-declaration="yes" />
    <jsp:directive.page contentType="text/html;charset=UTF-8" />
    <head><title>Spizza</title></head>
    <body>
        <h2>Thank you for your order!</h2>
        <![CDATA[
        <a href='${flowExecutionUrl}&_eventId=finished'>Finish</a>
        ]]>
    </body>
</html>

該頁面提供了一個完成流程的連結,它展示了使用者與流程互動的唯一辦法。

Spring Web Flow為檢視的使用者提供了一個flowExecutionUrl變數,它包含了流程的URL。結束連結將一個_eventId引數關聯到URL上,以便返回到Web流程時觸發finished事件。這個事件將會使流程到達結束狀態。

流程將會在結束狀態完成。由於在流程結束後沒有下一步做什麼具體資訊,流程將會重新從identifyCustomer狀態開始,以準備接受下一個訂單。

下面還要定義identifyCustomer、buildOrder、takePayment這些子流程。

收集顧客資訊

對於一個顧客,需要收集其電話、住址等資訊,如下面的流程圖:
識別顧客流程

這個流程不再是線性的,而是有了分支。例如在查詢顧客後,流程可能結束,也可能轉到登錄檔單。同樣的,在checkDeliveryArea狀態,顧客可能會被告警,也可能是不被告警。

程式清單:

<?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" />
</flow>

下面將這個流程定義分解成一個個的狀態。

詢問電話號碼

welcome狀態是一個很簡單的檢視狀態,它歡迎訪問Spizza網站的顧客並要求輸入電話。它有兩個轉移:如果從檢視觸發phoneEntered事件,就會定向到lookupCustomer,另外一個就是在全域性轉移中定義用來響應cancel事件的cancel轉移。

頁面程式碼:

<%@ 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輸入。當進入檢視狀態時,流程暫停並等待使用者採取一些行為。當用戶提交表單時,流程執行鍵會在_flowExecutionKey輸入域中返回,並在流程暫停的位置進行恢復。

還需要注意提交按鈕的名稱_eventId_部分是Spring Web Flow的一個線索,它表明了接下來要觸發事件。當點選這個按鈕提交表單時,就會觸發phoneEntered事件,進而轉移到lookupCustomer。

查詢顧客

當歡迎顧客的表單提交後,顧客的電話號碼將包含在請求引數中,並用於查詢顧客。lookupCustomer狀態的<evaluate>元素是查詢發生的位置。它將電話號碼從請求引數中抽取出來,並傳遞到pizzaFlowActions Bean的lookupCustomer()方法中。該方法要麼返回Customer物件,要麼丟擲CustomerNotFoundException異常。

在前一種情況下,Customer物件會被設定到customer變數中(通過result屬性)並預設的轉移將流程帶到customerReady狀態。如果沒有查到顧客,那麼會丟擲異常,流程會轉移到registrationForm狀態。

註冊新顧客

registrationForm要求使用者填寫配送地址:

<%@ 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>

該表單繫結到了Order.customer物件上。

檢查配送區域

顧客提供了地址後,需要確認住址是否在配送範圍內,因此使用了決策狀態。

決策狀態checkDeliveryArea有一個<if>元素,它將顧客的郵編傳遞到pizzaFlowActions Bean的checkDeliveryArea()方法中,該方法會返回一個Boolean值。

如果顧客在配送範圍內,那麼流程將轉移到addCustomer狀態,否則進入deliveryWarning檢視狀態。deliveryWarnin檢視:

<%@ 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事件。如果傳送的是accept事件,那麼流程會轉移到addCustomer狀態。否則,子流程會轉移到cancel狀態。

儲存顧客資料

addCustomer有一個<evaluate>元素,它會呼叫pizzaFlowActions.addCustomer()方法,將order.customer流程引數傳遞進去。

一旦這個流程完成,就會執行預設轉移,流程會轉移到ID為customerReady的結束狀態。

結束流程

當customer流程完成所有的路徑後,會到達customerReady的結束狀態。當呼叫它的披薩流程恢復時,它會接收到一個customerReady事件,這個事件將使得流程轉移到buildOrder狀態。

注意,customerReady結束狀態包含了一個<output>元素。在流程中,它等同於Java的return語句。它會從子流程中傳遞一些資料到呼叫流程。例如,<output>元素返回customer變數,這樣披薩流程中的identifyCustomer子流程狀態就可以將其指定給訂單。

另外,如果使用者在任意地方觸發了cancel事件,將會通過cancel狀態結束流程,這也會在披薩流程中觸發cancel事件並導致轉移到披薩流程的結束狀態。

構建訂單

下面就是確定顧客想要什麼樣的披薩,提示使用者建立披薩並將其放入訂單,如圖:
通過訂單子流程新增披薩

可以看到,showOrder狀態位於訂單子流程的中心位置。這是使用者進入這個流程時的狀態,也是使用者新增披薩訂單後轉移的目標狀態。它展現了訂單的當前狀態,並允許使用者新增其他的披薩到訂單中。

新增披薩訂單時,會轉移到createPizza狀態。這是一個檢視狀態,允許使用者對披薩進行選擇。

在showOrder狀態,使用者可以提交訂單,也可以取消。

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

    <!-- Order -->
    <view-state id="showOrder">
        <transition on="createPizza" to="createPizza" />
        <transition on="checkout" to="orderCreated" />
        <transition on="cancel" to="cancel" />
    </view-state>

    <view-state id="createPizza" model="flowScope.pizza"