《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是如何計算的:
另外,你也可以不使用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"