第四版的第八章內(nèi)容與第三版基本一致。
本章內(nèi)容:
- 創(chuàng)建會話式web應(yīng)用程序
- 定義流程狀態(tài)和行為
- 保護(hù)web流程
互聯(lián)網(wǎng)的一個奇特之處就在于它很容易讓人迷失。有如此多的內(nèi)容可以查看和閱讀,而超鏈接是其強(qiáng)大魔力的核心所在。
有時候,web應(yīng)用程序需要控制web沖浪者的導(dǎo)向,引導(dǎo)他們一步步地訪問應(yīng)用。比如電子商務(wù)網(wǎng)站的付款流程,從購物車開始,應(yīng)用程序會引導(dǎo)你依次經(jīng)過配送詳情、賬單信息以及最終的訂單確認(rèn)。
Spring Web Flow是一個web框架,它適用于元素規(guī)定流程運(yùn)行的程序。本章中,我們將會探索它是如何用于Spring Web框架平臺的。
其實(shí)我們可以使用任何的Web框架編寫流程化的應(yīng)用程序,比如使用Struts構(gòu)建特定的流程。但是這樣沒有辦法將流程與實(shí)現(xiàn)分開,你會發(fā)現(xiàn)流程的定義分散在組成流程的各個元素中,沒有特定的地方能夠完整地描述整個流程。
Spring Web Flow是Spring MVC的擴(kuò)展,它支持開發(fā)基于流程的應(yīng)用程序,可以將流程的定義和實(shí)現(xiàn)流程行為的類和視圖分離開來。
在介紹Spring Web Flow的時候,我們會暫且放下Spittr樣例,而使用生產(chǎn)披薩訂單的web程序。
使用的第一步是在項(xiàng)目中進(jìn)行安裝,那么就從安裝開始吧。
在Spring中配置Spring Web Flow
Spring Web Flow是基于Spring MVC構(gòu)建的,這就意味著所有的流程請求都需要經(jīng)過Spring MVC的DispatcherServlet
。我們需要在Spring應(yīng)用上下文中配置一些Bean來處理流程請求并執(zhí)行流程。
現(xiàn)在還沒有支持使用Java來配置Spring Web Flow,所以沒得選,只能在XML中進(jìn)行配置。有一些Bean會使用Spring Web Flow的Spring配置文件命名空間來進(jìn)行聲明,因此我們需要在上下文定義XML文件中添加相應(yīng)的命名空間:
<?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">
聲明了命名空間后,就可以準(zhǔn)備裝配Web Flow的Bean了。
編寫流程執(zhí)行器
顧名思義,流程執(zhí)行器(flow executor )就是用來驅(qū)動流程的執(zhí)行。當(dāng)用戶進(jìn)入到一個流程時,流程執(zhí)行器會為該用戶創(chuàng)建并啟動一個流程執(zhí)行器的實(shí)例。當(dāng)流程暫停時(例如為用戶展示視圖時),流程執(zhí)行器會在用戶執(zhí)行操作后恢復(fù)流程。
在Spring中,<flow:flow-executor>
元素可以創(chuàng)建一個流程執(zhí)行器:
<flow:flow-executor id="flowExecutor" />
盡管流程執(zhí)行器負(fù)責(zé)創(chuàng)建和執(zhí)行流程,但它并不負(fù)責(zé)加載流程定義。這個要由流程注冊表(flow registry)負(fù)責(zé),下面會創(chuàng)建它。
配置流程注冊表
流程注冊表的工作就是加載流程定義,并讓流程執(zhí)行器可以使用它們。可以在Spring中使用<flow:flow-registry>
進(jìn)行配置:
<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
屬性指明的。根據(jù)<flow:flow-location-pattern>
元素,任何以-flow.xml
結(jié)尾的XML文件都會被視為流程定義。
所有的流程都是通過其ID來進(jìn)行引用的。使用<flow:flow-location-pattern>
元素,流程的ID就是相對于base-path
的路徑,或者是雙星號所代表的路徑,如下圖展示了流程ID是如何計(jì)算的:

另外,你也可以不使用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
為流程定義文件。當(dāng)這樣定義時,流程的ID是從流程定義文件的文件名中獲取的,這就是springpizza
。
如果你希望更顯示地指定流程ID,那么可以通過<flow:flow-location>
元素的id屬性來進(jìn)行設(shè)置。例如,要設(shè)定pizza作為流程ID,可以這樣進(jìn)行配置:
<flow:flow-registry id="flowRegistry">
<flow:flow-location id="pizza"
path="/WEB-INF/flows/springpizza.xml" />
</flow:flow-registry>
處理流程請求
正如前面的章節(jié)中提到的,DispatcherServlet
會將請求分發(fā)給控制器,但是對于流程而言,你需要FlowHandlerMapping
來幫助DispatcherServlet
將流程請求發(fā)送給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,響應(yīng)請求的是FlowHandlerAdapter
,它等同于Spring MVC的控制器,會對流程請求進(jìn)行響應(yīng)并處理。FlowHandlerAdapter
可以像下面這樣裝配成一個Spring Bean:
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
<property name="flowExecutor" ref="flowExecutor" />
</bean>
這個處理適配器就是DispatcherServlet
和Spring Web Flow之間的橋梁。它會處理流程請求并管理基于這些請求的流程。在這里,它裝配了流程執(zhí)行器的引用,而后者是為請求執(zhí)行流程的。
現(xiàn)在已經(jīng)配置了Spring Web Flow所需的Bean和組件,下面所需的就是真正的定義流程了。首先了解下流程的組成元素。
流程組件
在Spring Web Flow中,流程是由3個主要元素組成的:狀態(tài)(state)、轉(zhuǎn)移(transition)和流程數(shù)據(jù)(flow data)。狀態(tài)
是流程中事件發(fā)生的地點(diǎn)。如果將流程想象成公路旅行,那么狀態(tài)就是路途上的城鎮(zhèn)、路邊飯店以及風(fēng)景點(diǎn)等。流程中的狀態(tài)是業(yè)務(wù)邏輯執(zhí)行、做出決策或?qū)㈨撁嬲故窘o用戶的地方,而不是在公路旅行中買薯片或者可樂這些行為。
如果說流程狀態(tài)是公路上停下來的地點(diǎn),那么轉(zhuǎn)移就是連接這些點(diǎn)的公路。在流程上,需要通過轉(zhuǎn)移從一個狀態(tài)到達(dá)另一個狀態(tài)。
在城鎮(zhèn)間旅行的時候,可能需要購買一些紀(jì)念品、留下一下回憶。類似的,在流程處理過程中,它要收集一些數(shù)據(jù):流程當(dāng)前狀況等。也許你很想將其稱為流程的狀態(tài),但是我們定義的狀態(tài)已經(jīng)有了另外的含義。
狀態(tài)
Spring Web Flow定義了5種不同的狀態(tài),如下表所示。通過選擇Spring Web Flow的狀態(tài)幾乎可以把任意的安排功能構(gòu)造成會話式的Web應(yīng)用程序。盡管并不是所有的流程都需要下表中的狀態(tài),但最終你可能會經(jīng)常使用其中幾個。
狀態(tài)類型 | 作用 |
---|---|
行為(Action) | 流程邏輯發(fā)生的地方 |
決策(Decision) | 決策狀態(tài)將流程分為兩個方向,它會基于流程數(shù)據(jù)的評估結(jié)果確定流程方向 |
結(jié)束(End) | 結(jié)束狀態(tài)是流程的最后一站,進(jìn)入End狀態(tài),流程就會終止 |
子流程(Subflow) | 子流程狀態(tài)會在當(dāng)前正在運(yùn)行的流程上下文中啟動一個新的流程 |
視圖(View) | 視圖狀態(tài)會暫停流程并邀請用戶參與流程 |
首先了解下這些流程元素在Spring Web Flow定義中是如何表現(xiàn)的。
視圖狀態(tài)
視圖狀態(tài)用來為用戶展現(xiàn)信息并使用戶在流程中發(fā)揮作用。實(shí)際的視圖實(shí)現(xiàn)可以是Spring支持的任意視圖類型,但通常是用JSP來實(shí)現(xiàn)的。
在流程定義文件中,<view-state>
用來定義視圖狀態(tài):
<view-state id="welcome" />
在這個簡單的示例中,id屬性有兩個含義。其一,它定義了流程中的狀態(tài)。其二,因?yàn)檫@里沒有其他地方指定視圖,那么它就指定了流程到達(dá)這個狀態(tài)時要展現(xiàn)的邏輯視圖名稱為welcome。
如果要顯示地指定另外一個視圖名稱,那么就可以使用view
屬性:
<view-state id="welcome" view="greeting" />
如果流程為用戶展現(xiàn)了一個表單,你希望指定表單所綁定的對象,可以使用model
屬性:
<view-state id="takePayment" model="flowScope.paymentDetails"/>
這里指定了takePayment視圖將綁定流程范圍內(nèi)的paymentDetails對象。
行為狀態(tài)
視圖狀態(tài)包括流程應(yīng)用的用戶,而行為狀態(tài)則是應(yīng)用程序自身在執(zhí)行任務(wù)。行為狀態(tài)一般會觸發(fā)Spring所管理Bean的一些方法,并跟你講方法調(diào)用的執(zhí)行結(jié)果轉(zhuǎn)移到另一個狀態(tài)。
在流程定義文件中,行為狀態(tài)使用<action-state>
元素來聲明:
<action-state id="saveOrder">
<evaluate expression="pizzaFlowActions.saveOrder(order)" />
<transition to="thankYou" />
</action-state>
盡管沒有嚴(yán)格要求,但是<action-state>
元素一般都有一個<evaluate>
子元素,該元素給出了行為狀態(tài)要做的事情,expression
屬性指定了進(jìn)入這個狀態(tài)時要評估的表達(dá)式。本例中,給出的是SpEL表達(dá)式,這表明它將會找到ID為pizzaFlowActions的Bean,并調(diào)用其saveOrder()方法。
決策狀態(tài)
流程有可能會按照線性執(zhí)行下去,從一個狀態(tài)到另一個狀態(tài),沒有其他的替代路線。但是更常見的是流程在某一個點(diǎn)根據(jù)流程當(dāng)前情況進(jìn)入不同的分支。
決策狀態(tài)能夠使得在流程執(zhí)行時產(chǎn)生兩個分支,它會評估一個Boolean表達(dá)式,根據(jù)結(jié)果是true還是false在兩個狀態(tài)轉(zhuǎn)移中選擇一個。在流程定義文件中,使用<decision-state>
元素來定義決策狀態(tài):
<decision-state id="checkDeliveryArea">
<if test="pizzaFlowActions.checkDeliveryArea(customer.zipCode)"
then="addCustomer"
else="deliveryWarning" />
</decision-state>
<decision-state>
并不是單獨(dú)工作的,<if>
元素是其核心,它是進(jìn)行表達(dá)式評估的地方,如果表達(dá)式結(jié)果為true,流程會轉(zhuǎn)向then
屬性指定的狀態(tài),為false會轉(zhuǎn)向else
指定的狀態(tài)中。
子流程狀態(tài)
也許你不會將應(yīng)用程序的所有邏輯都寫在一個方法里,而是將其分散到多個類、方法一起其他結(jié)構(gòu)中。
同樣的,將流程分成獨(dú)立的部分也是個不錯的主意。<subflow-state>
元素允許在一個正在執(zhí)行的流程中調(diào)用另一個流程:
<subflow-state id="order" subflow="pizza/order">
<input name="order" value="order"/>
<transition on="orderCreated" to="payment" />
</subflow-state>
這里,<input>
元素作為子流程的輸入被用于傳遞訂單對象。如果子流程結(jié)束的<end-state>
狀態(tài)ID為orderCreated,那么本流程就會轉(zhuǎn)移到ID為payment的狀態(tài)。
結(jié)束狀態(tài)
最后,所有的流程都要結(jié)束。這就是流程轉(zhuǎn)移到結(jié)束狀態(tài)時所做的。<end-state>
元素指定了流程的結(jié)束:
<end-state id="customerReady" />
當(dāng)流程到達(dá)<end-state>
時,流程就會結(jié)束。接下來發(fā)生什么要取決于以下幾個因素:
- 如果結(jié)束的流程是個子流程,那么調(diào)用它的流程將會從
<subflow-state>
處繼續(xù)執(zhí)行。<end-state>
的ID將會用作時間觸發(fā)從<subflow-state>
開始的轉(zhuǎn)移。 - 如果
<end-state>
設(shè)置了view屬性,那么就會渲染指定的視圖。視圖可以是相對于流程的路徑,也可以是流程模板,使用externalRedirect:
前綴的會重定向到流程外部的頁面,而使用flowRedirect:
前綴的則會重定向到另外一個流程。 - 如果結(jié)束的流程不是子流程也沒有配置view屬性,那么這個流程就會結(jié)束。瀏覽器最后將會加載流程的基本URL地址,同時,因?yàn)闆]有活動的流程,所以會開始一個新的流程實(shí)例。
需要注意的是一個流程可能有多個結(jié)束狀態(tài)。因?yàn)樽恿鞒痰慕Y(jié)束狀態(tài)ID確定了激活的事件,所以也許你會希望以多種結(jié)束狀態(tài)來結(jié)束子流程,從而能夠在調(diào)用流程中觸發(fā)不同的事件,即使不是在子流程中,也有可能在結(jié)束流程后,根據(jù)流程的執(zhí)行情況有多個顯示頁面供選擇。
下面看一下流程是如何在狀態(tài)間遷移的,如何在流程中通過定義轉(zhuǎn)移來完成道路鋪設(shè)。
轉(zhuǎn)移
如前文所述,轉(zhuǎn)移連接了流程中的狀態(tài)。流程中除結(jié)束狀態(tài)外的每個狀態(tài),至少需要一個轉(zhuǎn)移,這樣就知道在狀態(tài)完成時的走向。一個狀態(tài)也許有多個轉(zhuǎn)移,分別表示當(dāng)前狀態(tài)結(jié)束時可以執(zhí)行的不同路徑。
轉(zhuǎn)移是通過<transition>
元素來定義的,作為其他狀態(tài)元素(<action-state>
、<view-state>
和<subflow-state>
)的子元素。最簡單的形式就是<transition>
元素在流程中指定下一個狀態(tài):
<transition to="customerReady" />
屬性to
用于指定流程中的下一個狀態(tài)。如果<transition>
元素只使用了to
屬性,那么這個轉(zhuǎn)移就會是當(dāng)前狀態(tài)的默認(rèn)轉(zhuǎn)移選項(xiàng),如果沒有其他可用轉(zhuǎn)移的話,就會使用它。
更為常見的轉(zhuǎn)移定義是基于事件的觸發(fā)來進(jìn)行的。在視圖狀態(tài),事件通常會是用戶采取的動作。在行為狀態(tài),事件是評估表達(dá)式得到的結(jié)果。而在子流程狀態(tài),事件取決于子流程結(jié)束狀態(tài)的ID。在任意事件中,你可以使用on
屬性來指定觸發(fā)轉(zhuǎn)移的事件:
<transition on="phoneEntered" to="lookupCustomer"/>
在示例中,如果觸發(fā)了phoneEntered事件流程,就會進(jìn)入lookupCustomer狀態(tài)。
在拋出異常時,流程也可能進(jìn)入另一種狀態(tài)。例如,如果沒有找到顧客的記錄,你可能希望流程轉(zhuǎn)移到一個顯示注冊表單的視圖狀態(tài),如下面:
<transition on-exception="com.springinaction.pizza.service.CustomerNotFoundException"
to="registrationForm" />
屬性on-exception
和屬性on
十分類似,它是指定了要發(fā)生轉(zhuǎn)移的異常而不是一個事件。
全局轉(zhuǎn)移
在創(chuàng)建完流程后,也許你會發(fā)現(xiàn)有些狀態(tài)使用了一些通用的轉(zhuǎn)移。例如在整個流程中到處都有如下轉(zhuǎn)移:
<transition on="cancel" to="endState" />
與其在多個流程狀態(tài)中重復(fù)通用的轉(zhuǎn)移,不如將其作為<globaltransitions>
的子元素,從而作為全局轉(zhuǎn)移。
<global-transitions>
<transition on="cancel" to="endState" />
</global-transitions>
定義完全局轉(zhuǎn)移,流程中所有的狀態(tài)都會默認(rèn)擁有這個cancel轉(zhuǎn)移。
流程數(shù)據(jù)
當(dāng)流程從一個狀態(tài)到達(dá)另一個狀態(tài)時,它會帶走一些數(shù)據(jù)。有時這些數(shù)據(jù)很快就會被使用,比如直接展示給用戶,有時這些數(shù)據(jù)需要在整個流程中傳遞并在流程結(jié)束時使用。
聲明變量
流程數(shù)據(jù)是保存在變量中的,而變量可以在流程的任意位置進(jìn)行引用,并且可以以多種方式進(jìn)行創(chuàng)建。其中最簡單的方式就是使用<var>
元素:
<var name="customer" class="com.springinaction.pizza.domain.Customer"/>
這里創(chuàng)建了一個新的Customer實(shí)例并將其放在customer變量中,這個變量可以在流程的任意狀態(tài)下進(jìn)行訪問使用。
作為行為狀態(tài)的一部分或者說作為視圖狀態(tài)的入口,也可以使用<evaluate>
元素來創(chuàng)建變量:
<evaluate result="viewScope.toppingsList"
expression="T(com.springinaction.pizza.domain.Topping).asList()" />
這里<evaluate>
元素計(jì)算了一個SpEL表達(dá)式,并將結(jié)果放到toppingsList變量中,這個變量是視圖作用域的。
類似的,<set>
元素也可以設(shè)置變量的值:
<set name="flowScope.pizza"
value="new com.springinaction.pizza.domain.Pizza()" />
<set>
元素與<evaluate>
元素類似,都是講變量設(shè)置為表達(dá)式計(jì)算的結(jié)果。這里我們設(shè)置了一個流程范圍的pizza變量,它的值為Pizza對象的新實(shí)例。
流程數(shù)據(jù)的作用域
流程中所攜帶的數(shù)據(jù)都有其各自的生命周期,這取決于保存數(shù)據(jù)的變量本身的作用域,如下表:
范圍 | 生命周期 |
---|---|
Conversation | 最高層級的流程開始時創(chuàng)建,在最高層級的流程結(jié)束時銷毀。由最高層級的流程和其所有的子流程所共享 |
Flow | 當(dāng)流程開始時創(chuàng)建,在流程結(jié)束時銷毀。只在創(chuàng)建它的流程中是可見的 |
Request | 當(dāng)一個請求進(jìn)入流程時創(chuàng)建,流程返回時銷毀 |
Flash | 流程開始時創(chuàng)建,流程結(jié)束時銷毀。在視圖狀態(tài)解析后,才會被清除 |
View | 進(jìn)入視圖狀態(tài)時創(chuàng)建,退出這個狀態(tài)時銷毀,只在視圖狀態(tài)內(nèi)可見 |
當(dāng)使用<var>
元素聲明變量時,變量始終是流程作用域的,也就是在流程作用域內(nèi)定義變量。當(dāng)使用<set>
或<evaluate>
時,作用域通過name或result屬性的前綴指定。例如,將一個值賦給流程作用域的theAnswer變量:
<set name="flowScope.theAnswer" value="42"/>
到目前為止,我們已經(jīng)看到了Web流程的所有原材料,下面要將其進(jìn)行整合了,完成一個完整的流程。
組合起來:披薩流程
首先從構(gòu)建一個高層次的流程開始,它定義了訂購披薩的整體流程,然后將其拆分為多個子流程。
定義基本流程
當(dāng)顧客訪問Spizza網(wǎng)站時,他們需要進(jìn)行用戶識別、選擇一個或多個披薩添加到訂單、提供支付信息,然后提交訂單,等待披薩上來,如下圖:

下面展示Spring Web Flow的XML流程定義來實(shí)現(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.3.xsd">
<var name="order" class="com.springinaction.pizza.domain.Order" />
<!-- 調(diào)用顧客子流程 -->
<subflow-state id="identifyCustomer" subflow="pizza/customer">
<output name="customer" value="order.customer" />
<transition on="customerReady" to="buildOrder" />
</subflow-state>
<!-- 調(diào)用訂單子流程 -->
<subflow-state id="buildOrder" subflow="pizza/order">
<input name="order" value="order" />
<transition on="orderCreated" to="takePayment" />
</subflow-state>
<!-- 調(diào)用支付子流程 -->
<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" />
<!-- 全局取消轉(zhuǎn)移 -->
<global-transitions>
<transition on="cancel" to="endState" />
</global-transitions>
</flow>
流程定義中的第一件事就是聲明order變量。每次流程開始的時候都會創(chuàng)建一個Order實(shí)例。Order類會包含關(guān)于訂單的所有信息、顧客信息、訂購的披薩以及支付信息等。
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
}
流程定義的主要組成部分是流程的狀態(tài),默認(rèn)情況下,流程定義文件中的第一個狀態(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.3.xsd"
start-state="identifyCustomer">
...
</flow>
識別顧客、構(gòu)建披薩訂單和支付這樣的活動比較復(fù)雜,并不適合將其直接放在一個狀態(tài),而是以<subflow-state>
元素展現(xiàn)的。
流程變量order將在前3個狀態(tài)中進(jìn)行填充并在第4個狀態(tài)中進(jìn)行保存。identifyCustomer子流程使用了<output>
元素來填充order的customer屬性,將其設(shè)置為調(diào)用顧客子流程收到的輸出。buildOrder和takePayment狀態(tài)使用了不同的方式,它們使用<input>
將order流程變量作為輸入,這些子流程就能在其內(nèi)部填充order對象。
在訂單得到顧客、披薩以及支付信息后,就可以對其進(jìn)行保存。saveOrder是處理這個任務(wù)的行為狀態(tài)。它使用<evaluate>
來調(diào)用ID為pizzaFlowActions的Bean的saveOrder()方法,并將保存的訂單對象傳遞進(jìn)來。訂單完成保存后會轉(zhuǎn)移到thankCustomer。
thankCustomer狀態(tài)是一個簡單的視圖狀態(tài),后臺使用了/WEB-INF/flows/pizza/thankCustomer.jsp
文件進(jìn)行展示:
<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。結(jié)束鏈接將一個_eventId參數(shù)關(guān)聯(lián)到URL上,以便返回到Web流程時觸發(fā)finished事件。這個事件將會使流程到達(dá)結(jié)束狀態(tài)。
流程將會在結(jié)束狀態(tài)完成。由于在流程結(jié)束后沒有下一步做什么具體信息,流程將會重新從identifyCustomer狀態(tài)開始,以準(zhǔn)備接受下一個訂單。
下面還要定義identifyCustomer、buildOrder、takePayment這些子流程。
收集顧客信息
對于一個顧客,需要收集其電話、住址等信息,如下面的流程圖:

這個流程不再是線性的,而是有了分支。例如在查找顧客后,流程可能結(jié)束,也可能轉(zhuǎn)到注冊表單。同樣的,在checkDeliveryArea狀態(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">
<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>
下面將這個流程定義分解成一個個的狀態(tài)。
詢問電話號碼
welcome狀態(tài)是一個很簡單的視圖狀態(tài),它歡迎訪問Spizza網(wǎng)站的顧客并要求輸入電話。它有兩個轉(zhuǎn)移:如果從視圖觸發(fā)phoneEntered事件,就會定向到lookupCustomer,另外一個就是在全局轉(zhuǎn)移中定義用來響應(yīng)cancel事件的cancel轉(zhuǎn)移。
頁面代碼:
<%@ 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
輸入。當(dāng)進(jìn)入視圖狀態(tài)時,流程暫停并等待用戶采取一些行為。當(dāng)用戶提交表單時,流程執(zhí)行鍵會在_flowExecutionKey輸入域中返回,并在流程暫停的位置進(jìn)行恢復(fù)。
還需要注意提交按鈕的名稱_eventId_
部分是Spring Web Flow的一個線索,它表明了接下來要觸發(fā)事件。當(dāng)點(diǎn)擊這個按鈕提交表單時,就會觸發(fā)phoneEntered事件,進(jìn)而轉(zhuǎn)移到lookupCustomer。
查找顧客
當(dāng)歡迎顧客的表單提交后,顧客的電話號碼將包含在請求參數(shù)中,并用于查詢顧客。lookupCustomer狀態(tài)的<evaluate>
元素是查找發(fā)生的位置。它將電話號碼從請求參數(shù)中抽取出來,并傳遞到pizzaFlowActions Bean的lookupCustomer()方法中。該方法要么返回Customer對象,要么拋出CustomerNotFoundException異常。
在前一種情況下,Customer對象會被設(shè)置到customer變量中(通過result
屬性)并默認(rèn)的轉(zhuǎn)移將流程帶到customerReady狀態(tài)。如果沒有查到顧客,那么會拋出異常,流程會轉(zhuǎn)移到registrationForm狀態(tài)。
注冊新顧客
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對象上。
檢查配送區(qū)域
顧客提供了地址后,需要確認(rèn)住址是否在配送范圍內(nèi),因此使用了決策狀態(tài)。
決策狀態(tài)checkDeliveryArea有一個<if>
元素,它將顧客的郵編傳遞到pizzaFlowActions Bean的checkDeliveryArea()方法中,該方法會返回一個Boolean值。
如果顧客在配送范圍內(nèi),那么流程將轉(zhuǎn)移到addCustomer狀態(tài),否則進(jìn)入deliveryWarning視圖狀態(tài)。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>
其中有兩個鏈接,允許用戶繼續(xù)訂單或者取消訂單。通過使用與welcome狀態(tài)相同的flowExecutionUrl變量,這些鏈接分別觸發(fā)流程中的accept和cancel事件。如果發(fā)送的是accept事件,那么流程會轉(zhuǎn)移到addCustomer狀態(tài)。否則,子流程會轉(zhuǎn)移到cancel狀態(tài)。
存儲顧客數(shù)據(jù)
addCustomer有一個<evaluate>
元素,它會調(diào)用pizzaFlowActions.addCustomer()方法,將order.customer流程參數(shù)傳遞進(jìn)去。
一旦這個流程完成,就會執(zhí)行默認(rèn)轉(zhuǎn)移,流程會轉(zhuǎn)移到ID為customerReady的結(jié)束狀態(tài)。
結(jié)束流程
當(dāng)customer流程完成所有的路徑后,會到達(dá)customerReady的結(jié)束狀態(tài)。當(dāng)調(diào)用它的披薩流程恢復(fù)時,它會接收到一個customerReady事件,這個事件將使得流程轉(zhuǎn)移到buildOrder狀態(tài)。
注意,customerReady結(jié)束狀態(tài)包含了一個<output>
元素。在流程中,它等同于Java的return
語句。它會從子流程中傳遞一些數(shù)據(jù)到調(diào)用流程。例如,<output>
元素返回customer變量,這樣披薩流程中的identifyCustomer子流程狀態(tài)就可以將其指定給訂單。
另外,如果用戶在任意地方觸發(fā)了cancel事件,將會通過cancel狀態(tài)結(jié)束流程,這也會在披薩流程中觸發(fā)cancel事件并導(dǎo)致轉(zhuǎn)移到披薩流程的結(jié)束狀態(tài)。
構(gòu)建訂單
下面就是確定顧客想要什么樣的披薩,提示用戶創(chuàng)建披薩并將其放入訂單,如圖:

可以看到,showOrder狀態(tài)位于訂單子流程的中心位置。這是用戶進(jìn)入這個流程時的狀態(tài),也是用戶添加披薩訂單后轉(zhuǎn)移的目標(biāo)狀態(tài)。它展現(xiàn)了訂單的當(dāng)前狀態(tài),并允許用戶添加其他的披薩到訂單中。
添加披薩訂單時,會轉(zhuǎn)移到createPizza狀態(tài)。這是一個視圖狀態(tài),允許用戶對披薩進(jìn)行選擇。
在showOrder狀態(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">
<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">
<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" />
</flow>
這個子流程實(shí)際上回操作主流程創(chuàng)建的Order對象,在這里我們使用<input>
元素來將Order對象傳遞進(jìn)流程。
接下來會看到showOrder狀態(tài),它是一個基本的視圖狀態(tài),具有3個不同的轉(zhuǎn)移,分別用于創(chuàng)建披薩、提交訂單和取消訂單。
createPizza的視圖是一個表單,這個表單可以添加新的Pizza對象到訂單。<on-entry>
元素添加了一個新的Pizza對象到流程作用域內(nèi),當(dāng)表單提交時它將填充進(jìn)訂單。值得注意的是,這個視圖狀態(tài)引用的model是流程作用域同一個Pizza對象。Pizza對象將綁定到創(chuàng)建披薩的表單中:
<%@ 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/>
<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>
當(dāng)通過Continue按鈕提交訂單時,尺寸和配料選擇會綁定到Pizza對象中,并且觸發(fā)addPizza
轉(zhuǎn)移。與這個轉(zhuǎn)移關(guān)聯(lián)的<evaluate>
元素表明在轉(zhuǎn)移到showOrder狀態(tài)之前,流程作用域內(nèi)的Pizza對象會傳遞給訂單的addPizza()方法中。
有兩種方法可以結(jié)束流程,用戶可以點(diǎn)擊showOrder視圖中的Cancel按鈕或者Checkout按鈕。這兩種操作都會使流程轉(zhuǎn)移到一個<end-state>
。但是選擇的結(jié)束狀態(tài)ID決定了退出這個流程時觸發(fā)事件,進(jìn)而最終確定主流程的下一個行為。主流程要么基于cancel要么基于orderCreated事件進(jìn)行狀態(tài)轉(zhuǎn)移。在前者情況下,外邊的流程會結(jié)束;后者,會轉(zhuǎn)移到takePayment子流程。
支付
在披薩流程要結(jié)束的時候,最后的子流程提示用戶輸入他們的支付信息,如下圖:

支付子流程也是使用<input>
元素接收一個Order對象作為輸入。
可以看到,進(jìn)入支付子流程的時候,用戶會到達(dá)takePayment狀態(tài)。這是一個視圖狀態(tài),在這里用戶可以選擇信用卡、支票或者現(xiàn)金進(jìn)行支付。提示支付信息后,進(jìn)入verifyPayment狀態(tài),這是一個行為狀態(tài),會校驗(yà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">
<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" />
</flow>
在流程進(jìn)入takePayment視圖時,<on-entry>
元素將構(gòu)建一個支付表單并使用SpEL表達(dá)式在流程范圍內(nèi)創(chuàng)建PaymentDetails實(shí)例,該實(shí)例實(shí)際上是表單背后的對象。它也會創(chuàng)建視圖作用域的paymentDetails變量,這個變量是一個包含了PaymentType
enum的值的列表。在這里,SpEL的T()
作用于PaymentType類,這樣就可以調(diào)用靜態(tài)的asList()方法。
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('_', ' '));
}
}
在面對支付表單的時候,用戶可能提交支付,也可能會取消。根據(jù)做出的選擇,支付子流程將名為paymentTaken或cancel的<end-state>
結(jié)束。就像其他的子流程一樣,不論哪種<end-state>
都會結(jié)束子流程并將控制交給主流程。但是所采用的id將決定主流程接下來的轉(zhuǎn)移。
目前我們已經(jīng)依次介紹了披薩流程及其子流程,下面快速了解下如何對流程及其狀態(tài)的訪問增加安全保護(hù)。
保護(hù)Web流程
Spring Web Flow中的狀態(tài)、轉(zhuǎn)移甚至整個流程都可以借助<secured>
元素實(shí)現(xiàn)安全性,該元素會作為這些元素的子元素。例如,為了保護(hù)對一個視圖狀態(tài)的訪問:
<view-state id="restricted">
<secured attributes="ROLE_ADMIN" match="all"/>
</view-state>
按照這里的配置,只有授權(quán)ROLE_ADMIN
訪問權(quán)限(借助attributes屬性)的用戶才能訪問這個視圖狀態(tài)。attributes屬性使用逗號分隔的權(quán)限列表來表明用戶要訪問指定狀態(tài)、轉(zhuǎn)移或流程所需要的權(quán)限。match屬性可以設(shè)置為any或all。如果是any,那么用戶至上具備一個attributes屬性所列的權(quán)限。如果的all,那么用戶必須具有所有權(quán)限。具體見下一章。
如果覺得有用,歡迎關(guān)注我的微信,有問題可以直接交流:
