第4版的第8章內容與第3版基本1致。
本章內容:
互聯(lián)網的1個奇特的地方就在于它很容易讓人迷失。有如此多的內容可以查看和瀏覽,而超鏈接是其強大魔力的核心所在。
有時候,web利用程序需要控制web沖浪者的導向,引導他們1步步地訪問利用。比如電子商務網站的付款流程,從購物車開始,利用程序會引導你順次經過配送詳情、賬單信息和終究的定單確認。
Spring Web Flow是1個web框架,它適用于元素規(guī)定流程運行的程序。本章中,我們將會探索它是如何用于Spring Web框架平臺的。
其實我們可使用任何的Web框架編寫流程化的利用程序,比如使用Struts構建特定的流程。但是這樣沒有辦法將流程與實現(xiàn)分開,你會發(fā)現(xiàn)流程的定義分散在組成流程的各個元素中,沒有特定的地方能夠完全地描寫全部流程。
Spring Web Flow是Spring MVC的擴大,它支持開發(fā)基于流程的利用程序,可以將流程的定義和實現(xiàn)流程行動的類和視圖分離開來。
在介紹Spring Web Flow的時候,我們會暫且放下Spittr樣例,而使用生產披薩定單的web程序。
使用的第1步是在項目中進行安裝,那末就從安裝開始吧。
Spring Web Flow是基于Spring MVC構建的,這就意味著所有的流程要求都需要經過Spring MVC的DispatcherServlet。我們需要在Spring利用上下文中配置1些Bean來處理流程要求并履行流程。
現(xiàn)在還沒有支持使用Java來配置Spring Web Flow,所以沒得選,只能在XML中進行配置。有1些Bean會使用Spring Web Flow的Spring配置文件命名空間來進行聲明,因此我們需要在上下文定義XML文件中添加相應的命名空間:
聲明了命名空間后,就能夠準備裝配Web Flow的Bean了。
顧名思義,流程履行器(flow executor )就是用來驅動流程的履行。當用戶進入到1個流程時,流程履行器會為該用戶創(chuàng)建并啟動1個流程履行器的實例。當流程暫停時(例如為用戶展現(xiàn)視圖時),流程履行器會在用戶履行操作后恢復流程。
在Spring中,元素可以創(chuàng)建1個流程履行器:
雖然流程履行器負責創(chuàng)建和履行流程,但它其實不負責加載流程定義。這個要由流程注冊表(flow registry)負責,下面會創(chuàng)建它。
流程注冊表的工作就是加載流程定義,并讓流程履行器可使用它們。可以在Spring中使用進行配置:
正如這里聲明的,流程注冊表會在/WEB-INF/flows目錄下尋覓流程定義,這個路徑是由base-path屬性指明的。根據(jù)元素,任何以-flow.xml結尾的XML文件都會被視為流程定義。
所有的流程都是通過其ID來進行援用的。使用元素,流程的ID就是相對base-path的路徑,或是雙星號所代表的路徑,以下圖展現(xiàn)了流程ID是如何計算的:
另外,你也能夠不使用base-path屬性,直接顯式地聲明流程定義文件的位置:
這里使用了而不是,path屬性直接指定了/WEB-INF/flows/springpizza.xml為流程定義文件。當這樣定義時,流程的ID是從流程定義文件的文件名中獲得的,這就是springpizza。
如果你希望更顯示地指定流程ID,那末可以通過元素的id屬性來進行設置。例如,要設定pizza作為流程ID,可以這樣進行配置:
正如前面的章節(jié)中提到的,DispatcherServlet會將要求分發(fā)給控制器,但是對流程而言,你需要FlowHandlerMapping來幫助DispatcherServlet將流程要求發(fā)送給Spring Web Flow。FlowHandlerMapping的配置以下:
FlowHandlerMapping裝配了注冊表的援用,這樣它就知道如何將要求的URL匹配到流程上。例如,如果有1個ID為pizza的流程,F(xiàn)lowHandlerMapping就會知道如果要求的URL是/pizza的話,就會將其匹配到這個流程上。
但是,F(xiàn)lowHandlerMapping的工作僅僅是將流程要求定向到Spring Web Flow,響應要求的是FlowHandlerAdapter,它同等于Spring MVC的控制器,會對流程要求進行響應并處理。FlowHandlerAdapter可以像下面這樣裝配成1個Spring Bean:
這個處理適配器就是DispatcherServlet和Spring Web Flow之間的橋梁。它會處理流程要求并管理基于這些要求的流程。在這里,它裝配了流程履行器的援用,而后者是為要求履行流程的。
現(xiàn)在已配置了Spring Web Flow所需的Bean和組件,下面所需的就是真實的定義流程了。首先了解下流程的組成元素。
在Spring Web Flow中,流程是由3個主要元素組成的:狀態(tài)(state)、轉移(transition)和流程數(shù)據(jù)(flow data)。狀態(tài)是流程中事件產生的地點。如果將流程想象成公路旅行,那末狀態(tài)就是路途上的城鎮(zhèn)、路邊飯店和風景點等。流程中的狀態(tài)是業(yè)務邏輯履行、做出決策或將頁面展現(xiàn)給用戶的地方,而不是在公路旅行中買薯片或可樂這些行動。
如果說流程狀態(tài)是公路上停下來的地點,那末轉移就是連接這些點的公路。在流程上,需要通過轉移從1個狀態(tài)到達另外一個狀態(tài)。
在城鎮(zhèn)間旅行的時候,可能需要購買1些記念品、留下1下回想。類似的,在流程處理進程中,它要搜集1些數(shù)據(jù):流程當前狀態(tài)等。或許你很想將其稱為流程的狀態(tài),但是我們定義的狀態(tài)已有了另外的含義。
Spring Web Flow定義了5種不同的狀態(tài),以下表所示。通過選擇Spring Web Flow的狀態(tài)幾近可以把任意的安排功能構造成會話式的Web利用程序。雖然其實不是所有的流程都需要下表中的狀態(tài),但終究你可能會常常使用其中幾個。
狀態(tài)類型 | 作用 |
---|---|
行動(Action) | 流程邏輯產生的地方 |
決策(Decision) | 決策狀態(tài)將流程分為兩個方向,它會基于流程數(shù)據(jù)的評估結果肯定流程方向 |
結束(End) | 結束狀態(tài)是流程的最后1站,進入End狀態(tài),流程就會終止 |
子流程(Subflow) | 子流程狀態(tài)會在當前正在運行的流程上下文中啟動1個新的流程 |
視圖(View) | 視圖狀態(tài)會暫停流程并約請用戶參與流程 |
首先了解下這些流程元素在Spring Web Flow定義中是如何表現(xiàn)的。
視圖狀態(tài)用來為用戶展現(xiàn)信息并使用戶在流程中發(fā)揮作用。實際的視圖實現(xiàn)可以是Spring支持的任意視圖類型,但通常是用JSP來實現(xiàn)的。
在流程定義文件中,用來定義視圖狀態(tài):
在這個簡單的示例中,id屬性有兩個含義。其1,它定義了流程中的狀態(tài)。其2,由于這里沒有其他地方指定視圖,那末它就指定了流程到達這個狀態(tài)時要展現(xiàn)的邏輯視圖名稱為welcome。
如果要顯示地指定另外1個視圖名稱,那末就能夠使用view屬性:
如果流程為用戶展現(xiàn)了1個表單,你希望指定表單所綁定的對象,可使用model屬性:
這里指定了takePayment視圖將綁定流程范圍內的paymentDetails對象。
視圖狀態(tài)包括流程利用的用戶,而行動狀態(tài)則是利用程序本身在履行任務。行動狀態(tài)1般會觸發(fā)Spring所管理Bean的1些方法,并跟你講方法調用的履行結果轉移到另外一個狀態(tài)。
在流程定義文件中,行動狀態(tài)使用元夙來聲明:
雖然沒有嚴格要求,但是元素1般都有1個子元素,該元素給出了行動狀態(tài)要做的事情,expression屬性指定了進入這個狀態(tài)時要評估的表達式。本例中,給出的是SpEL表達式,這表明它將會找到ID為pizzaFlowActions的Bean,并調用其saveOrder()方法。
流程有可能會依照線性履行下去,從1個狀態(tài)到另外一個狀態(tài),沒有其他的替換線路。但是更常見的是流程在某1個點根據(jù)流程當前情況進入不同的分支。
決策狀態(tài)能夠使得在流程履行時產生兩個分支,它會評估1個Boolean表達式,根據(jù)結果是true還是false在兩個狀態(tài)轉移當選擇1個。在流程定義文件中,使用元夙來定義決策狀態(tài):
其實不是單獨工作的,元素是其核心,它是進行表達式評估的地方,如果表達式結果為true,流程會轉向then屬性指定的狀態(tài),為false會轉向else指定的狀態(tài)中。
或許你不會將利用程序的所有邏輯都寫在1個方法里,而是將其分散到多個類、方法1起其他結構中。
一樣的,將流程分成獨立的部份也是個不錯的主張。元素允許在1個正在履行的流程中調用另外一個流程:
這里,元素作為子流程的輸入被用于傳遞定單對象。如果子流程結束的狀態(tài)ID為orderCreated,那末本流程就會轉移到ID為payment的狀態(tài)。
最后,所有的流程都要結束。這就是流程轉移到結束狀態(tài)時所做的。元素指定了流程的結束:
當流程到達時,流程就會結束。接下來產生甚么要取決于以下幾個因素:
- 如果結束的流程是個子流程,那末調用它的流程將會從處繼續(xù)履行。的ID將會用作時間觸發(fā)從開始的轉移。
- 如果設置了view屬性,那末就會渲染指定的視圖。視圖可以是相對流程的路徑,也能夠是流程模板,使用externalRedirect:前綴的會重定向到流程外部的頁面,而使用flowRedirect:前綴的則會重定向到另外1個流程。
- 如果結束的流程不是子流程也沒有配置view屬性,那末這個流程就會結束。閱讀器最后將會加載流程的基本URL地址,同時,由于沒有活動的流程,所以會開始1個新的流程實例。
需要注意的是1個流程可能有多個結束狀態(tài)。由于子流程的結束狀態(tài)ID肯定了激活的事件,所以或許你會希望以多種結束狀態(tài)來結束子流程,從而能夠在調用流程中觸發(fā)不同的事件,即便不是在子流程中,也有可能在結束流程后,根據(jù)流程的履行情況有多個顯示頁面供選擇。
下面看1下流程是如何在狀態(tài)間遷移的,如何在流程中通過定義轉移來完成道路鋪設。
如前文所述,轉移連接了流程中的狀態(tài)。流程中除結束狀態(tài)外的每一個狀態(tài),最少需要1個轉移,這樣就知道在狀態(tài)完成時的走向。1個狀態(tài)或許有多個轉移,分別表示當前狀態(tài)結束時可以履行的不同路徑。
轉移是通過元夙來定義的,作為其他狀態(tài)元素(、和)的子元素。最簡單的情勢就是元素在流程中指定下1個狀態(tài):
屬性to用于指定流程中的下1個狀態(tài)。如果元素只使用了to屬性,那末這個轉移就會是當前狀態(tài)的默許轉移選項,如果沒有其他可用轉移的話,就會使用它。
更加常見的轉移定義是基于事件的觸發(fā)來進行的。在視圖狀態(tài),事件通常會是用戶采取的動作。在行動狀態(tài),事件是評估表達式得到的結果。而在子流程狀態(tài),事件取決于子流程結束狀態(tài)的ID。在任意事件中,你可使用on屬性來指定觸發(fā)轉移的事件:
在示例中,如果觸發(fā)了phoneEntered事件流程,就會進入lookupCustomer狀態(tài)。
在拋出異常時,流程也可能進入另外一種狀態(tài)。例如,如果沒有找到顧客的記錄,你可能希望流程轉移到1個顯示注冊表單的視圖狀態(tài),以下面:
屬性on-exception和屬性on10分類似,它是指定了要產生轉移的異常而不是1個事件。
在創(chuàng)建完流程后,或許你會發(fā)現(xiàn)有些狀態(tài)使用了1些通用的轉移。例如在全部流程中到處都有以下轉移:
與其在多個流程狀態(tài)中重復通用的轉移,不如將其作為的子元素,從而作為全局轉移。
定義完全局轉移,流程中所有的狀態(tài)都會默許具有這個cancel轉移。
當流程從1個狀態(tài)到達另外一個狀態(tài)時,它會帶走1些數(shù)據(jù)。有時這些數(shù)據(jù)很快就會被使用,比如直接展現(xiàn)給用戶,有時這些數(shù)據(jù)需要在全部流程中傳遞并在流程結束時使用。
流程數(shù)據(jù)是保存在變量中的,而變量可以在流程的任意位置進行援用,并且可以以多種方式進行創(chuàng)建。其中最簡單的方式就是使用元素:
這里創(chuàng)建了1個新的Customer實例并將其放在customer變量中,這個變量可以在流程的任意狀態(tài)下進行訪問使用。
作為行動狀態(tài)的1部份或說作為視圖狀態(tài)的入口,也能夠使用元夙來創(chuàng)建變量:
這里元素計算了1個SpEL表達式,并將結果放到toppingsList變量中,這個變量是視圖作用域的。
類似的,元素也能夠設置變量的值:
元素與元素類似,都是講變量設置為表達式計算的結果。這里我們設置了1個流程范圍的pizza變量,它的值為Pizza對象的新實例。
流程中所攜帶的數(shù)據(jù)都有其各自的生命周期,這取決于保存數(shù)據(jù)的變量本身的作用域,以下表:
范圍 | 生命周期 |
---|---|
Conversation | 最高層級的流程開始時創(chuàng)建,在最高層級的流程結束時燒毀。由最高層級的流程和其所有的子流程所同享 |
Flow | 當流程開始時創(chuàng)建,在流程結束時燒毀。只在創(chuàng)建它的流程中是可見的 |
Request | 當1個要求進入流程時創(chuàng)建,流程返回時燒毀 |
Flash | 流程開始時創(chuàng)建,流程結束時燒毀。在視圖狀態(tài)解析后,才會被清除 |
View | 進入視圖狀態(tài)時創(chuàng)建,退出這個狀態(tài)時燒毀,只在視圖狀態(tài)內可見 |
當使用元素聲明變量時,變量始終是流程作用域的,也就是在流程作用域內定義變量。當使用或時,作用域通過name或result屬性的前綴指定。例如,將1個值賦給流程作用域的theAnswer變量:
到目前為止,我們已看到了Web流程的所有原材料,下面要將其進行整合了,完成1個完全的流程。
首先從構建1個高層次的流程開始,它定義了訂購披薩的整體流程,然后將其拆分為多個子流程。
當顧客訪問Spizza網站時,他們需要進行用戶辨認、選擇1個或多個披薩添加到定單、提供支付信息,然后提交定單,等待披薩上來,以下圖:
下面展現(xiàn)Spring Web Flow的XML流程定義來實現(xiàn)披薩定單的整體流程:
流程定義中的第1件事就是聲明order變量。每次流程開始的時候都會創(chuàng)建1個Order實例。Order類會包括關于定單的所有信息、顧客信息、訂購的披薩和支付信息等。
流程定義的主要組成部份是流程的狀態(tài),默許情況下,流程定義文件中的第1個狀態(tài)會是流程訪問的第1個狀態(tài)。本例中就是identifyCustomer狀態(tài)(1個子流程)。也能夠通過元素的start-state屬性來指定任意狀態(tài)為開始狀態(tài):
辨認顧客、構建披薩定單和支付這樣的活動比較復雜,其實不合適將其直接放在1個狀態(tài),而是以元素展現(xiàn)的。
流程變量order將在前3個狀態(tài)中進行填充并在第4個狀態(tài)中進行保存。identifyCustomer子流程使用了元夙來填充order的customer屬性,將其設置為調用顧客子流程收到的輸出。buildOrder和takePayment狀態(tài)使用了不同的方式,它們使用將order流程變量作為輸入,這些子流程就可以在其內部填充order對象。
在定單得到顧客、披薩和支付信息后,就能夠對其進行保存。saveOrder是處理這個任務的行動狀態(tài)。它使用來調用ID為pizzaFlowActions的Bean的saveOrder()方法,并將保存的定單對象傳遞進來。定單完成保存后會轉移到thankCustomer。
thankCustomer狀態(tài)是1個簡單的視圖狀態(tài),后臺使用了/WEB-INF/flows/pizza/thankCustomer.jsp文件進行展現(xiàn):
該頁面提供了1個完成流程的鏈接,它展現(xiàn)了用戶與流程交互的唯1辦法。
Spring Web Flow為視圖的用戶提供了1個flowExecutionUrl變量,它包括了流程的URL。結束鏈接將1個_eventId參數(shù)關聯(lián)到URL上,以便返回到Web流程時觸發(fā)finished事件。這個事件將會使流程到達結束狀態(tài)。
流程將會在結束狀態(tài)完成。由于在流程結束后沒有下1步做甚么具體信息,流程將會重新從identifyCustomer狀態(tài)開始,以準備接受下1個定單。
下面還要定義identifyCustomer、buildOrder、takePayment這些子流程。
對1個顧客,需要搜集其電話、住址等信息,以下面的流程圖:
這個流程不再是線性的,而是有了分支。例如在查找顧客后,流程可能結束,也可能轉到注冊表單。一樣的,在checkDeliveryArea狀態(tài),顧客可能會被告警,也多是不被告警。
程序清單:
下面將這個流程定義分解成1個個的狀態(tài)。
welcome狀態(tài)是1個很簡單的視圖狀態(tài),它歡迎訪問Spizza網站的顧客并要求輸入電話。它有兩個轉移:如果從視圖觸發(fā)phoneEntered事件,就會定向到lookupCustomer,另外1個就是在全局轉移中定義用來響應cancel事件的cancel轉移。
頁面代碼:
這個簡單的表單用來讓用戶輸入電話號碼,有兩個特殊的部份,首先是隱藏的_flowExecutionKey輸入。當進入視圖狀態(tài)時,流程暫停并等待用戶采取1些行動。當用戶提交表單時,流程履行鍵會在_flowExecutionKey輸入域中返回,并在流程暫停的位置進行恢復。
還需要注意提交按鈕的名稱_eventId_部份是Spring Web Flow的1個線索,它表明了接下來要觸發(fā)事件。當點擊這個按鈕提交表單時,就會觸發(fā)phoneEntered事件,進而轉移到lookupCustomer。
當歡迎顧客的表單提交后,顧客的電話號碼將包括在要求參數(shù)中,并用于查詢顧客。lookupCustomer狀態(tài)的元素是查找產生的位置。它將電話號碼從要求參數(shù)中抽取出來,并傳遞到pizzaFlowActions Bean的lookupCustomer()方法中。該方法要末返回Customer對象,要末拋出CustomerNotFoundException異常。
在前1種情況下,Customer對象會被設置到customer變量中(通過result屬性)并默許的轉移將流程帶到customerReady狀態(tài)。如果沒有查到顧客,那末會拋出異常,流程會轉移到registrationForm狀態(tài)。
registrationForm要求用戶填寫配送地址:
該表單綁定到了Order.customer對象上。
顧客提供了地址后,需要確認住址是不是在配送范圍內,因此使用了決策狀態(tài)。
決策狀態(tài)checkDeliveryArea有1個元素,它將顧客的郵編傳遞到pizzaFlowActions Bean的checkDeliveryArea()方法中,該方法會返回1個Boolean值。
如果顧客在配送范圍內,那末流程將轉移到addCustomer狀態(tài),否則進入deliveryWarning視圖狀態(tài)。deliveryWarnin視圖:
其中有兩個鏈接,允許用戶繼續(xù)定單或取消定單。通過使用與welcome狀態(tài)相同的flowExecutionUrl變量,這些鏈接分別觸發(fā)流程中的accept和cancel事件。如果發(fā)送的是accept事件,那末流程會轉移到addCustomer狀態(tài)。否則,子流程會轉移到cancel狀態(tài)。
addCustomer有1個元素,它會調用pizzaFlowActions.addCustomer()方法,將order.customer流程參數(shù)傳遞進去。
1旦這個流程完成,就會履行默許轉移,流程會轉移到ID為customerReady的結束狀態(tài)。
當customer流程完成所有的路徑后,會到達customerReady的結束狀態(tài)。當調用它的披薩流程恢復時,它會接收到1個customerReady事件,這個事件將使得流程轉移到buildOrder狀態(tài)。
注意,customerReady結束狀態(tài)包括了1個元素。在流程中,它同等于Java的return語句。它會從子流程中傳遞1些數(shù)據(jù)到調用流程。例如,元素返回customer變量,這樣披薩流程中的identifyCustomer子流程狀態(tài)就能夠將其指定給定單。
另外,如果用戶在任意地方觸發(fā)了cancel事件,將會通過cancel狀態(tài)結束流程,這也會在披薩流程中觸發(fā)cancel事件并致使轉移到披薩流程的結束狀態(tài)。
下面就是肯定顧客想要甚么樣的披薩,提示用戶創(chuàng)建披薩并將其放入定單,如圖:
可以看到,showOrder狀態(tài)位于定單子流程的中心位置。這是用戶進入這個流程時的狀態(tài),也是用戶添加披薩定單后轉移的目標狀態(tài)。它展現(xiàn)了定單確當前狀態(tài),并允許用戶添加其他的披薩到定單中。
添加披薩定單時,會轉移到createPizza狀態(tài)。這是1個視圖狀態(tài),允許用戶對披薩進行選擇。
在showOrder狀態(tài),用戶可以提交定單,也能夠取消。