Spring Web Flow
http://blog.csdn.net/zmxj/archive/2005/05/31/384942.aspx
2005年3月
轉自
:http://www.nirvanastudio.org/nicholas/SpringWebFlow.htm
你是否覺得當你的Web應用越來越復雜,理解和管理頁面流程—驅動你應用程序用例的樂譜—也越來越困難了呢?而被迫使用特定的方式做事情并且無法重用是不是讓你感覺很累?你是否覺得使用了太多時間開發你自己特定的方法去解決普遍問題就像會話狀態管理?
進入Spring Web Flow。
Spring Web Flow (SWF) 是Spring Framework的一個脫離模塊。這個模塊是Spring Web應用開發模塊棧的一部分,Spring Web包含Spring MVC。
Spring Web Flow 的目標是成為管理Web應用頁面流程的最佳方案。當你的應用需要復雜的導航控制,例如向導,在一個比較大的事務過程中去指導用戶經過一連串的步驟的時候,SWF將會是一個功能強大的控制器。
以下是一個受控制的導航的例子,使用UML狀態圖描述:

圖 1 - 一個航空訂票服務流程
聰明的讀者會認出這個是一個典型的航空訂票流程,就是那種每次你通過在線方式參與訂票的過程。
在傳統的Web應用中,頁面流程就像上面所展示的,不是很明確 — 他們不是一等公民。就拿一個使用Struts的Web應用舉個例子,為了在Struts里面實現頁面流,大多數開發人員利用了框架提供的Action和視圖。在這種情況下,一個單獨的Action就和一個指定的請求URL產生了聯系。只有當請求從那個URL過來的時候,Action才會被執行。在執行過程中,Action運行一些處理并且選擇一個合適的視圖顯示結果。這非常簡單。
所以要在Struts中實現多步控制的頁面流,你需要通過不同的視圖將獨立的Action形成鏈。用來處理不同事件,例如“后退”或“提交”的Action URL都是硬編碼在視圖中的。一些ad-hoc形式的會話存儲被用來管理流程狀態。提交后重定向被用來阻止重復提交,等等。
雖然這是一個簡單并且有效的方法,但是它具有一個很大的缺陷:從struts-config.xml文件的Action定義中不能清晰的看到頁面流程。你無法從幾棵樹看到一片森林,就像你無法從Action和視圖的定義看到頁面流程一樣。靈活性也因為Action和視圖不能被重用而大打折扣。最后,你仍然需要做很多工作,這并不見得容易。
Spring MVC提供了一個輕便的高層次的功能:表單控制器實現了一個與定義的頁面流程。它提供了兩個這樣的控制器:SimpleFormController和AbstractWizardController。盡管如此,這些仍然是大多數頁面流程控制概念的例子。
Tapestry和JSP在頁面的基礎上而不是請求的基礎上使用事件驅動的方法,使得每個頁面和它的后退控制器邏輯保持一致。然而,仍然需要提供一個優秀的類根據一個定義良好的能跨越多個頁面和不同路徑的生存周期去支持一個邏輯頁面流程。就如你所看到的,這個頁面流程的生存周期要比單一的請求長,但是卻比一個會話要短。
這就是Spring Web Flow的切入點,允許你使用一個簡單清晰的方法體現你的頁面流程,并且隨時重用,包括像Struts、Spring MVC、Tapestry、JSP甚至Portlets這些環境下。
正如你所看到的,Spring Web Flow提供以下優點:
- Web應用中的頁面流程可以通過Web流程的定義(XML文件或者Java類)清晰的展現出來。
- Web流程被設計成自包含的。這就允許你把你的應用中的一部分看作是一個模塊,這樣就你可以在多種場合重用它。
- Web流程捕獲任何合理的頁面流程總是使用同種技術。你不必被迫在特定的場合使用特定的控制器。
- 最后,Web流程是一等公民并且可以通過一個良好定義的契約使用。它具有一個清晰的,可觀察的生存周期為你自動管理。通過簡單配置,系統便會為你管理復雜的邏輯,總而言之,這非常容易使用。
現在已經有能力說Web流程是一組狀態(states)的集合。一個狀態是流程中發生某事的一個點。舉個例子,譬如顯示一個視圖或者執行一個Action。每個狀態都有一個或更多的轉變(transitions)用來移動到下一個狀態。
一個轉變是由一個事件(event)觸發的。
為了展示一個Web流程的定義,下面的XML片段展示了上面UML狀態圖定義的航空訂票處理:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE webflow PUBLIC "-//SPRING//DTD WEBFLOW//EN"
"http://www.springframework.org/dtd/spring-webflow.dtd">
<webflow id="bookflight" start-state="obtainTripInfo">
<action-state id="obtainTripInfo">
<action bean="bookingActions" method="bindAndValidate"/>
<transition on="success" to="suggestItineraries"/>
<transition on="error" to="tryAgain"/>
</action-state>
<action-state id="suggestItineraries">
<action bean="bookingActions"/>
<transition on="success" to="displaySuggestedItineraries"/>
</action-state>
<view-state id="displaySuggestedItineraries" view="suggestedItenaries">
<transition on="startOver" to="cancel"/>
<transition on="select" to="selectItinerary"/>
</view-state>
<action-state id="selectItinerary">
<action bean="bookingActions"/>
<transition on="success" to="isPassengerInfoRequired"/>
</action-state>
<decision-state id="isPassengerInfoRequired">
<if test="${requestScope.passenger == null}" then="enterPassengerInformation"/>
<if test="${requestScope.passenger.preferences.alwaysConfirmPassengerInfo}"
then="enterPassengerInformation" else="displayReservationVerification"/>
</decision-state>
<subflow-state id="enterPassengerInformation" flow="passenger">
<attribute-mapper>
<input value="${requestScope.passenger.id}" as="passengerId"/>
</attribute-mapper>
<transition on="finish" to="displayReservationVerification"/>
</subflow-state>
<view-state id="displayReservationVerification" view="reservationVerification">
<transition on="startOver" to="cancel"/>
<transition on="assignSeats" to="chooseSeatAssignments"/>
<transition on="book" to="book"/>
</view-state>
<subflow-state id="chooseSeatAssignments" flow="seatAssignments">
<attribute-mapper>
<input value="${requestScope.passenger.id}" as="passengerId"/>
<input name="itinerary"/>
</attribute-mapper>
<transition on="finish" to="displayReservationVerification"/>
</subflow-state>
<action-state id="book">
<action bean="bookingActions"/>
<transition on="success" to="displayConfirmation"/>
</action-state>
<end-state id="displayConfirmation" view="reservationConfirmation"/>
<end-state id="tryAgain" view="tryAgain"/>
<end-state id="cancel" view="home"/>
</webflow>
圖 2 - 基于XML的航空訂票流程定義
就像你所看到的,僅僅是掃過XML定義,邏輯流程驅動的訂票流程處理就已經可以清晰地辨認出來了,即使你都不了解Spring Web Flow實現細節。
如果你看得仔細點,你將會發現兩個子流程產生了訂票流程的子過程。第一個子流程指導用戶輸入乘客信息。第二個讓用戶分配他的座位。這個內嵌的流程扮演了“迷你應用程序模塊”的角色,這是Spring Web Flow強大的功能之一。
你可以將這份定義上交給一位業務分析人員,并且她估計能看懂。更好的是你可以根據這個定義繪制一個可視化圖表將其提交給業務分析人員。做這個的工具已經誕生了。
這篇文章的下一部分將逐塊分解上面的航空訂票流程定義,并且提供對話框演示Spring Web Flow是如何工作的。
流程定義
這是第一行的XML流程定義:
<webflow id="bookflight" start-state="obtainTripInfo">
...
</webflow>
webflow 元素定義了流程,指定它的id和start-date。id是一個簡單的唯一的標識符,start-state是一個轉變的初始狀態,這發生在當一個新的流程會話在運行時被激活的時候。
所以,在業務案例上,當訂票會話被激活的時候,它將轉變為obtainTripInfo狀態。
獲得行程信息行為狀態(Action State)
下面是obtainTripInfo狀態定義。
<action-state id="obtainTripInfo">
<action bean="bookingActions" method="bindAndValidate"/>
<transition on="success" to="suggestItineraries"/>
<transition on="error" to="tryAgain"/>
</action-state>
記得當狀態被進入,針對該狀態的行為就發生了。正如你將看到的,不同的狀態類型有不同的執行動作。action state,正如obtainTripInfo,在進入的時候執行一個Action。該Action返回執行的邏輯結果,并且這個結果被映射到狀態轉變上。一切就是這么簡單。
所以,在這個業務案例上,obtainTripInfo,當進入的時候執行bookingActions這個Action的bindAndValidate方法。這個方法從瀏覽器綁定表單輸入到一個Trip領域對象并且檢驗它。如果處理成功,就進入suggestItineraries狀態。如果錯誤發生,進入tryAgain狀態。
訂票Action
當在Spring IoC中使用Spring Web Flow的時候,action元素的bean屬性涉及到Spring Application Context中定義的一個相同名稱的Action實現。下面是bookingActions的定義:
web-context.xml
<bean id="bookingActions"
class="org.springframework.samples.bookflight.BookingActions">
<property name="bookingAgent" ref="myBookingAgent"/>
</bean>
這就允許我們的Action實現被Spring管理并且通過依賴注入進行配置。
建議路線行為狀態
現在我們看一下下一個Action State,給定一個綁定的并且通過檢驗的Trip對象作為輸入,返回一個建議的路線集合:
<action-state id="suggestItineraries">
<action bean="bookingActions"/>
<transition on="success" to="displaySuggestedItineraries"/>
</action-state>
下面是Action實現代碼:
public class BookingActions extends FormAction {
...
public Event suggestItineraries(RequestContext context) {
Trip trip = (Trip)context.getRequestScope().getAttribute("trip");
Collection<Itinerary> itineraries = bookingAgent.suggestItineraries(trip);
context.getRequestScope().setAttribute("itineraries", itineraries);
return success();
}
}
當進入suggestItineraries狀態的時候,suggestItineraries就被調用了。其他的Action State也是同樣的工作方式:進入狀態并調用指定的方法。
顯示建議路線視圖狀態(View State)
一旦返回了一個建議的路線集合,下一步就是讓用戶看到它們并且讓用戶選擇他最喜歡的。這可以通過以下的狀態定義完成:
<view-state id="displaySuggestedItineraries" view="suggestedItenaries">
<transition on="startOver" to="cancel"/>
<transition on="select" to="selectItinerary"/>
</view-state>
就如你所看到的,displaySuggestedItineraries是一個view state - 一個我們還未討論過的狀態類型。一個視圖狀態,當進入的時候,導致執行流程暫停,并將控制返回給客戶端同時根據配置的視圖同時返回。隨后,在用戶思考過后,客戶端發出一個事件描述用戶執行的Action。繼續流程,事件的發生已經映射到了一個狀態的轉變,這個轉變把用戶帶到了流程的下一步。
所以,在這個業務案例上,當進入displaySuggestedItineraries的時候suggestedIteneraries視圖被渲染并且將控制返回給瀏覽器。然后用戶選擇路線之后點擊“選擇”按鈕。這就出發了select事件,傳遞選擇的路線id作為事件參數。
用戶也可能選擇startOver,這時候流程轉變到了取消狀態。
對于view屬性,Spring MVC中,FlowControoler使用熟悉的 ModelAndView和ViewResolver構造,在Struts中,FlowAction用ActionForward。
客戶端狀態
在這個問題上你可能會問:
“...自從進入ViewState之后,執行流程暫停了,控制返回給了瀏覽器,那么流程如何重新拾起并且繼續運行呢?”
答案就是客戶端跟蹤一個唯一的id用戶標示流程執行點,并且將這個id放在input標簽內,以便引起下一個事件。典型的做法是放在一個隱藏域內。
舉個例子,在一個JSP文件里:
<input type="hidden" value="<c:out value="${flowExecution.id}"/>">
“是否需要乘客信息?” 決策狀態(Decision State)
用戶選擇了她需要的路線之后,流程需要做一個上下文關系(contextual)的決策關于下一步執行什么。
需要特別指出的是,如果用戶沒有登錄,或者她已經登錄但是希望確認她的信息 - 例如她所使用的信用卡 - 流程控制需要允許她確定這些信息。另一方面,如果她已經登錄并且希望直接進入預定頁面,流程控制應該跳個這個可選步驟。
基本上需要做一個動態的決策重新考慮她的信息和偏好的。
決策狀態最適合這個,看下面的定義:
<decision-state id="isPassengerInfoRequired">
<if test="${requestScope.passenger == null}" then="enterPassengerInformation"/>
<if test="${requestScope.passenger.preferences.alwaysConfirmPassengerInfo}"
then="enterPassengerInformation" else="displayReservationVerification"/>
</decision-state>
輸入乘客信息子流程狀態(SubFlow State)
處理乘客信息的過程邏輯上獨立的。這是處理的一部分,但是在機票預定這個上下文環境之外它也可以獨立存在。
子流程(Subflow)狀態機制就是針對這個實現的。當進入一個子流程狀態,這個子流程就被產生了。父流程掛起知道子流程結束。這讓你可以把你的應用作為一系列自包含的模塊看待,至于流程,你可以很容易的把多種情況統一處理。
看一下enterPassengerInformation子流程狀態:
<subflow-state id="enterPassengerInformation" flow="passenger">
<attribute-mapper>
<input value="${requestScope.passenger.id}" as="passengerId"/>
</attribute-mapper>
<transition on="finish" to="displayReservationVerification"/>
</subflow-state>
flow 屬性是這個進入這個流程的id,attribute-mapper 元素從子流程映射屬性。輸入映射將屬性向下映射到子流程。輸出映射將屬性倒退會父流程當子流程結束的時候。你可以從這里看到表達式也是支持的。
所以,在這個業務用例上,當進入enterPassengerInformation狀態,乘客流程就產生了。passengerId屬性傳遞給這個流程作為輸入。從這里,自流程作它需要做的。對于父流程來說這是一個黑箱。當子流程結束,父流程繼續,應答最后結果并決定去哪執行下一步 — 在這里,去確認預定。
顯示確認結束狀態(End State)
最有一個狀態類型在這里討論:結束狀態。當進入結束狀態,活動的流程會話就結束了。在結束上面,所有與之相關的資源都被自動清理。
displayConfirmation結束狀態在一條路線被被成功預定后顯示確認信息:
<end-state id="displayConfirmation" view="reservationConfirmation"/>
當進入這個狀態的時候,訂票流程結束了并且顯示reservationConfirmation視圖。因為訂票流程是根流程,并非子流程,所以任何分配的資源都會被自動清理。
注意:結束流程如果是一個子流程,進入這個狀態就會被認為是一個子流程結果并繼續父流程。更特別的是,這個狀態的ID在繼續父流程的子流程的狀態上被用作一個狀態的轉變。你可以從enterPassengerInformation子流程狀態定義上看出來。注意它如何響應子流程的“完成”結果,是通過一個“完成”結束狀態。
到這里,你了解了Spring Web Flow是關于什么的,并且你也看到了一個現實的例子。現在你要看到的就是如何部署這個流程定義到特定的環境中去執行,就行Spring MVC在一個Servlet環境下一樣:
做這事是很容易的,這里你需要和Spring MVC一起使用:
<bean name="/booking.htm" class="org.springframework.web.flow.mvc.FlowController">
<property name="flow">
<ref bean="bookingFlow"/>
</property>
</bean>
<bean id="bookingFlow" class="org.springframework.web.flow.config.XmlFlowFactoryBean">
<property name="location" value="classpath:bookflight-flow.xml"/>
</bean>
這就自動將bookingFlow導出至/booking.htm這個URL在一個Servlet環境里。
下面的部分介紹了一些SWF更高級的特性。
FlowExecutionListener 構造了一個觀察者允許你監聽并且對一個執行著的流程的生存周期作出反應。你可以使用這個特性作任何事,從一個狀態的預處理到后期條件的檢測,或則審計、安全處理。
一個執行著的流程的狀態的存儲機制是完全可插拔的。基于HttpSession的存儲是默認的,但是SWF提供兩種其他的存儲方式:一個是使用服務器端連續的會話儲存,另一種是使用完全的客戶端序列化。定義你自己的存儲方式,舉個例子,譬如使用數據庫存儲,是不推薦的。
你應該注意到Spring Web Flow并不是一攬子全包的解決方案。正如你所看到的,這是一個有狀態的系統能夠自動管理這些由業務處理過程驅動的頁面流程。它不能被當作簡單的、無狀態的解決方案。舉個例子,它不能被用在一些需要自由導航的站點,一些可以讓用戶自由“點擊周圍任意鏈接”的站點。Spring Web Flow被設計為強大的受控制導航,可以指導用戶按照一個清晰的業務目的和生存周期進行處理。
為了使得用例更具體,這里有一些“不錯的流程”的例子,這些流程就適合使用SWF系統:
下面的一些例子是不適合使用SWF的:
Spring Web Flow打算要作為一個優秀的傳統的控制器在任何Web環境下,就像Spring MVC、Struts、Tapestry、Web Work、JSP或者Portlets一樣。一個單一的站點可以適當的組合使用簡單的控制器管理Web流程。
Spring Web Flow 1.0 final 版本將隨著Spring 1.3正式版發布,時間定在JavaOne大會前大概六月份的時候。就現在而言,只能期待正式、穩定的預覽版。這個產品目前已經在特性集合和示例程序方面相當成熟了。
當開發小組給最終發布版砌上最后一塊磚時,下面是一些最重要的特性我們長在著手完成:
作為獨立的庫,Spring Web Flow很好與其他框架整合了。除了Spring MVC以外,已經提供了和Struts、Portlet MVC的整合,JSP和Tapestry的整合在最終版中也會見到。
在Spring 1.2中,在MBeanServer中輸出用于管理和監視的bean是很容易的。一個FlowExecutionMBean管理接口已經存在了,我們計劃擴展以便可以從JMX控制臺集中監控在服務器執行的所有流程的全局統計數據。
系統中的每個結構都可以做成可插拔的以獲得簡單的擴展或定制,甚至是從xml定義中。這包括狀態和轉變,以及其中的其他概念。
提供的特性和例子程序展示了在執行過程中使用事務補償來回滾先前提交的事務,我們對這點很感興趣。
Spring Web Flow 是控制業務處理流程的有效解決方案。并且用起來也很有意思,如果你還沒試過,那么你還等什么呢?
Spring Web Flow is covered in the Core Spring training course offered by Interface21 - http://www.springframework.com/training
The Spring Framework, http://www.springframework.org/
The Spring Web Flow Wiki, http://opensource.atlassian.com/confluence/spring/display/WEBFLOW/Home
The kdonald blog, http://www.jroller.com/page/kdonald
Struts, http://struts.apache.org/
Java Server Faces, http://java.sun.com/j2ee/javaserverfaces/
Tapestry, http://jakarta.apache.org/tapestry
WebWork, http://www.opensymphony.com/webwork/
JMX, http://java.sun.com/jmx
JavaOne, http://java.sun.com/javaone/
譯者
Nicholas@NirvanaStudio
2005-5-19
原文地址:http://www.theserverside.com/articles/content/SpringWebFlow/article.html