使用SpringWebFlow
· 創建會話式的Web應用程序
· 定義流程狀態和行為
Spring Web Flow 是Spring MVC 的擴展,它支持開發基於流程的應用程序。它將流程的定義與實現流程行為的類和視圖分離開來。
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:folw="http://www.springframework.org/schema/webflow-config"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/webflow-config ">
</beans>
在聲明了命名空間之后,就為裝配Web Flow的bean做好了准備。首先從流程執行器(flow executor)開始。
流程執行器驅動流程的執行。當用戶進入一個流程時,流程執行器會為用戶創建並啟動一個流程執行實例。當流程暫停的時候(如為用戶展示視圖時),流程執行器會在用戶執行操作后恢復流程。
在Spring中,<flow:flow-executor>元素會創建一個流程執行器:
<folw:flow-executor id="flowExecutor"/>
盡管流程執行器負責創建和執行流程,但它並不負責加載流程定義。這個責任落在流程注冊表(flow registry)身上。
流程注冊表(flow registry)的工作是加載流程定義並讓流程執行器能夠使用它們、我們可以在Spring中使用<flow:flow-registry>配置流程注冊表,如下所示:
<folw:flow-registry id="flowRegistry" base-path="/WEB-INF/flows">
<folw:flow-location-pattern value="*-flow.xml"/>
</folw:flow-registry>
在這個聲明中,流程注冊表會在”/WEB-INF/flows”目錄下查找流程定義,這是通過base-path屬性指明的。根據<folw:flow-location-pattern>元素的值,任何文件名以”-flow.xml”結尾的XML文件都將視為流程定義。
所有的流程都是通過其ID來進行引用的。這里我們使用了<flow:flow-location-pattern>元素,元素的ID就是相對於base-path的路徑–或者雙星號所代表的的路徑。在使用流程定位模式的時候,流程定義文件相對於基本路徑的路徑將被用作流程的ID。如下:
/WEB-INF/flows/order/order-flow.xml
在這個配置中,”/WEB-INF/flows”代表流程注冊表基本路徑,”order”表示流程ID,”order-flow.xml”流程定義文件。
作為另外一種方式,我們可以去除base-path屬性,而顯示聲明流程定義文件的位置:
<folw:flow-registry id="flowRegistry">
<folw:flow-location path="/WEB-INF/flows/springpizza.xml"/>
</folw:flow-registry>
在這里,使用了<flow:flow-location>而不是<flow:flow-location-pattern>,path屬性直接指明了”/WEB-INF/flows/springpizza.xml”作為流程定義。當這樣配置的時候,流程ID是從流程文件的文件名中獲取的,這里就是springpizza。如果希望更顯示地指定流程ID,則可以通過<flow:flow-location>元素的id屬性進行設置。如下:
<folw:flow-registry id="flowRegistry">
<folw:flow-location id="pizza" path="/WEB-INF/flows/springpizza.xml"/>
</folw:flow-registry>
DispatcherServlet用於將請求分發給控制器。但是對於流程而言,我們需要一個FlowHandlerMapping來幫助DispatcherServlet將流程請求發送給Spring Web Flow。在Spring應用上下文中,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。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中,流程是由三個主要元素定義的:狀態、轉移和流程數據。
Spring Web Flow定義了五種不同類型的狀態。如下表所示,通過選擇Spring Web Flow的狀態幾乎樂意把任意的安排功能構造成會話式的Web應用。
| 狀態類型 |
用途 |
| 行為(Action) |
行為狀態是流程邏輯發生的地方 |
| 決策(Decision) |
決策狀態將流程分成兩個方向,它會基於流程數據的評估結果確定流程方向 |
| 結束(End) |
結束狀態是流程的最后一站。一旦進入End狀態,流程就會終止 |
| 子流程(Subflow) |
子流程狀態會在當前正在運行的流程上下文中啟動一個新的流程 |
| 視圖(View) |
視圖狀態會暫停流程並邀請用戶參與流程 |
視圖狀態
視圖狀態用於為用戶展現信息並使使用在流程中發揮作用。實際的視圖可以是Spring支持的任意視圖類型,但通常是用JSP來實現的。在流程定義的XML文件中,<view-state>用於定義視圖狀態:
<view-state id="welcome"/>
在這個簡單的示例中,id屬性有兩個含義。它在流程內標識這個狀態,除此之外,因為在這里沒有其他地方指定視圖,所以它也指定了流程到達這個狀態時要展現的邏輯視圖名為welcome。如果希望顯示的指定另一個視圖名,則可以使用view屬性:
<view-state id="welcome" view="greeting"/>
如果流程為用戶展現了一個表單,寫昂指定表單所綁定的對象,可以通過model屬性進行設置:
<view-state id="takePayment" model="flowScope.paymentDetails"/>
· 1
這里我們指定takePayment視圖中的表單將綁定流程作用域內的paymentDetails對象。
行為狀態
視圖狀態會涉及到流程應用程序的用戶,而行為狀態則是應用程序自身在執行任務。行為狀態一般會觸發Spring所管理bean的一些方法並根據方法調用的執行結果轉移到另一個狀態。在流程定義XML中,行為狀態使用<action-state>元素來聲明。
<action-state id="saveOrder">
<evaluate expression="pizzaFlowActions.saveOrder(order)"/>
<transition to="thankYou"/>
</action-state>
盡管不是嚴格要求,但是<action-state>元素一般都會有一個<evaluate>作為子元素。<evaluate>元素給出了行為狀態要做的事情。expression屬性指定了進入這個狀態時要評估的表達式。在本示例中,給出的expression是SpEL表達式,它表明將會找到ID為pizzaFlowActions的bean並調用其saveOrder()方法。
決策狀態
有可能流程會完全按照線性執行,從一個狀態進入另一個狀態,沒有其他的替代路線。但是更常見的情況是流程在某一個點根據流程的當前情況進入不同的分支。決策狀態能夠在流程執行中產生兩個分支。決策狀態將評估一個Boolean類型的表達式,然后在兩個狀態中選擇一個。這要取決於表達式會計算出true還是false。在XML流程定義中,決策狀態通過<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>
· 1
· 2
· 3
· 4
在這里<input>元素用於傳遞訂單對象作為子流程的輸入。如果子流程結束的<end-state>狀態ID為orderCreated,那么流程將會轉移到名為payment的狀態。
結束狀態
最后,所有的流程都結束了。這就是當流程轉移到結束狀態時所做的。<end-state>元素指定了流程的結束,如下:
<end-state id="customerReady"/>
· 1
當到達<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="thankYou"/>
· 1
屬性to用於指定流程的下一個狀態。如果<transition>只是用了to屬性,那這個轉移就會是當前狀態的默認轉移選項,如果沒有其他可用轉移的話,就會使用它。更常見的轉移定義是基於事件的觸發來進行的。在視圖狀態,事件通常會是用戶采取的動作。在行為狀態,事件是評估表達式得到的結果。而在子流程中,事件取決於子流程結束狀態的ID。在任意的事件中,你可以使用on屬性來指定觸發轉移的事件:
<transition on="phoneEntered" to="lookupCustomer"/>
· 1
在本例中,如果觸發了phoneEntered事件,流程將會進入lookupCustomer狀態。
在拋出異常時,流程也可以進入另一個狀態。例如,如果顧客的記錄沒有找到,你可能希望流程轉移到一個展現注冊表單的視圖狀態。如下所示:
<transition on-exception="com.springpizza.exception.CustomerNotFoundException" to="registrationForm"/>
· 1
屬性on-exception類似於to屬性,只不過它指定了要發生轉移的異常而不是一個事件。
全局轉移
在流程創建完成后可能會發現有一些狀態使用了一些通用的轉移。與其在多個狀態中都重復通用的轉移,我們可以將<transition>元素作為<global-transitions>的子元素,把它們定義為全局轉移。
<global-transitions>
<transition on="cancel" to="endState"/>
</global-transitions>
· 1
· 2
· 3
定義完這個全局轉以后,流程中的所有狀態都會默認擁有這個cancel轉移。
當流程從一個狀態進入到另一個狀態時,它會帶走一個數據。有時候,這些數據只需要很短的時間(可能只要展現頁面給用戶)。有時候,這些數據會在整個流程中傳遞並在流程結束的時候使用。
聲明變量
流程數據保存在變量中,而變量可以在流程的各個地方進行引用。它能夠以多種方式創建。在流程中創建變量的最簡單形式是使用<var>元素:
<var name="customer" class="com.springpizza.domain.Customer"/>
· 1
這里,創建了一個新的Customer實例並將其放在名為customer的變量中。這個變量可以在流程的任意狀態進行訪問。作為行為狀態的一部分或者作為視圖狀態的入口,有可能會使用<evaluate>元素來創建變量。例如:
<evaluate result="viewScope.toppingsList" expression="T(com.springpizza.pizza.domain.Topping).asList()" />
· 1
在本例中,<evaluate>元素計算了一個表達式(SpEL表達式)並將結果放到了名為ttoppingsList的變量中,這個變量是視圖作用域的。
類似地,<set>元素也可以設置變量的值:
<set name="flowScope.pizza" value="new com.springpizza.pizza.domain.Pizza()"/>
· 1
<set>元素與<evaluate>元素很類似,都是將變量設置為表達式計算的結果。這里,設置了一個流程作用域內的pizza變量,它的值是Pizza對象的新實例。
定義流程數據的作用域
流程中攜帶的數據會擁有不同的生命作用域和可見性,這取決於保存數據的變量本身的作用域。Spring Web Flow定義了五種不同作用域:
| 范圍 |
生命作用域和可見性 |
| Conversation |
最高層級的流程開始時創建,在最高層級的流程結束時銷毀。被最高層級的流程和其所有的子流程所共享 |
| Flow |
當流程開始時創建,在流程結束時銷毀。只有在創建它的流程中是可見的 |
| Request |
當一個請求進入流程時創建,在流程返回時銷毀 |
| Flash |
當流程開始時創建,在流程結束時銷毀。在視圖狀態渲染后,它也會被清除 |
| View |
當進入視圖狀態時創建,當這個狀態退出時銷毀。只在視圖狀態內是可見的 |
當使用<var>元素聲明變量時,變量始終是流程作用域的,也就是在定義變量的流程內有效。當使用<set>或<evaluate>的時候,作用域通過name或result屬性的前綴指定。
