您是否想將 JavaServer Faces (JSF)的強大前端功能、Tiles 的內容格式編排優勢和 Struts controller 層的靈活性都加入到您的J2EE Web 應用程序中?企業級 Java 專家 Srikanth Shenoy 和 Nithin Mallya 為您展示了如何將這三者的功能集成到一起。本文演示了如何在 Struts-Faces集成庫中定制類以使得它們可以與 Tiles 和 JSF 一同使用,并用一個實際的例子解釋了這個過程背后的基本原理以及如何使用新的一組類的細節。
將 Struts、Tiles 和 JavaServer Faces (JSF) 一起使用,開發人員可以實現易于管理和重用的、健壯的、界面清晰的 Web 應用程序。
Struts 框架推出已經有一段時間了,它已經成為在開發 J2EE Web 應用程序時開發人員所采用的事實上的標準。Tiles 框架是在 Struts 之后不久出現的,它通過為開發人員提供用組件組裝展示頁面的能力開拓了自己的生存環境。JSF 是 Web 應用程序框架中最新的成員,它提供了驗證用戶輸入和處理用戶事件的機制,最重要的是,這是一種以協議無關的方式呈現用戶界面組件的方法(有關這些 技術的概況,參見本文相關頁面“ The major players”)。
盡管 Struts 和 JSF 中有一些功能是重疊的,但是它們在其他方面起到了互為補充的作用。這三種技術的結合可以為開發 Web 應用程序、組織其展示和以協議無關的方式呈現定制的用戶界面(UI)組件提供一種高效的途徑。
為了運行本文中的示例代碼,需要 Struts 1.1、Tiles、JavaServer Faces Reference Implementation (JSF-RI) Early Access Release 4.0 以及 Struts-Faces 0.4。Jakarta 項目提供的 Struts 1.1 發行版本將 Struts 和 Tiles 捆綁發布。還可以從 Jakarta 項目上下載 Struts-Faces 集成庫。JSF-RI 是 Sun 的 Web 開發工具包(Web Services Developer Pack)的一部分(在 參考資料中有這些下載和示例代碼的鏈接)。
現在回到集成三種技術的細節上。首先有個壞消息:在本文發表的時候,這三種技術是不能直接互操作的。好消息是:在本文中,我們展示了集成 Struts、Tiles 和 JSF 的方法。我們假設您已經了解 Struts 和 Tiles。對 JSF 有一些了解會有幫助(參閱 參考資料中提供的 developerWorks 上的 JSF 教程的鏈接),但是不了解也不妨礙對本文的理解。
JSF 簡介
JSF 應用程序是使用 JSF 框架的普通 J2EE Web 應用程序,JSF 框架提供了豐富的 GUI 組件模型,這些模型體現了真正的 GUI 框架內涵。您可能聽人們說過,盡管某種技術不錯,但是它的外觀仍然需要改進。是的,用 HTML 組件構建平淡無奇的頁面的日子已經過去了,如果使用 JSF 的話,具有更高級 GUI 外觀的日子就在眼前。您會問,怎么做呢?樹形組件、菜單組件和圖形是已經存在的 UI 組件,這些 JSF 一定要提供。更進一步,JSF 通過提供容易使用的 API 鼓勵創建自定義組件。
注: 這里所提到的 UI 組件是 Sun 提供的示例的一部分。像所有規范一樣,實際的實現由不同的提供商完成。
在傳統的使用模型-視圖-控制器(MVC)的 Web 應用程序中,GUI 組件是由處理展示和業務邏輯的自定義標記所表示的。這樣就出現了必須“編寫與客戶機設備打交道的代碼”的問題,這會產生重復的代碼。使用 JSF 就不會有這個問題。
JSF 結構將 展示邏輯 (“什么”)與 UI 組件的 業務邏輯 (“為什么”和“如何”)分離。通過在 JSP 頁面中使用 JSF 標記,就可以將 renderer 與 UI 組件關聯在一起。一個 UI 組件可以用不同的 renderer 從而以不同的方式呈現。特定于 UI 組件的代碼在服務器上運行,并且響應用戶操作所產生的事件。
JSF-RI 提供了一個 render kit,它帶有一個自定義標記庫,用以從 UI 組件呈現 HTML。它還提供了根據需要定制這些組件外觀的能力。如果需要特殊的組件,那么可以為特定的客戶機設備構造定制的標記并讓它與一個子 UI 組件和定制的 renderer 相關聯。對于不同的設備,您所需要做的就是指定不同的 renderer。
JSF 和 UI 組件
您可能已經用 Java AWT 或者 Swing API 創建過 Java GUI 應用程序,所以您應該熟悉 JSF 的 UIComponent
(它與 AWT 或者 Swing 組件很相像)。它儲存其子組件的樹(如果有的話)并為客戶端發生的動作生成標準事件,例如單擊一個按鈕以提交表單。這些事件緩存在 FacesContext
中。您可以用自定義標記關聯每一個這種事件的處理程序。例如,用一個自定義的 ActionListener
處理用戶單擊或者表單提交。
JSF UIComponent
、 Renderer
和標記總是共同工作的。所有 JSP 自定義標記都是通過繼承 UIComponentTag
創建的。 doStart
和 doEnd
方法總是在 UIComponentTag
類中實現。您只需在這些標記類中提供其他的功能。
圖 1展示了自定義標記、UI 組件和 renderer 之間的關系。客戶機瀏覽器訪問用 JSF 標記( jsf:myTag
)表示 UI 組件( MyComponent
)的 JSP 頁面。這個 UI 組件運行在服務器上,并用適當的 renderer ( MyRenderer
)以 HTML 的形式呈現給客戶。這個 JSP 頁面表現了在 JSF-RI 中使用帶自定義標記的用戶界面組件而不是在 HTML 中對它們進行編碼。
例如,圖 1 展示了 h:panel:group
標記的使用。這個標記用于將一個父組件下面的各個組件組織到一起。如果與像 panel_grid
和 panel_data
這樣的其他面板標記共同使用,那么它會在運行時生成 HTML 表中的列的標記。JSF-RI-提供的 html_basic 標記庫用于表示像文本字段、按鈕這樣的 HTML 組件。
圖1. 呈現一個 JSF 頁面
JSF 生命周期
JSF 生命周期包括六個階段:一個傳入的請求可能會經歷全部階段,也可能不經歷任何階段,這取決于請求的類型、在生命周期中發生的驗證和轉換錯誤以及響應的類型。JSF 框架處理由 JSP 頁生成的 Faces 請求,并返回 faces或者 non-faces 響應。
在提交一個 JSF 表單,或者當用戶單擊指向在 URL 中具有 /faces 前綴的 URL 的鏈接時,就會出現 faces 響應。所有 faces 請求都由一個 FacesServlet
處理 -- 這是 JSF 中的控制器。
發送給一個 servlet 或者一個沒有 JSF 組件的 JSP 頁面的請求稱為 non-faces 請求。如果結果頁中有 JSF 標記,那么它就稱為 faces 響應,如果沒有 JSF 標記,就是 non-faces 響應。
JSF 生命周期有六個階段:
- 重建請求樹
- 應用請求值
- 進行驗證
- 更新模型值
- 調用應用程序
- 呈現響應
根據 JSF 規范,每一階段表示請求處理生命周期的一個邏輯概念。不過在 JSF-RI 中,這些階段是由具有對應名字的實際類表示的。下面一節描述了每一階段是如何對請求進行處理并生成響應的。您將首先看到的是處理一個 faces 請求所涉及的階段,然后是處理 faces 響應所涉及的階段。
處理 faces 請求
為了理解 JSF 請求處理,請看 FlightSearch.jsp,這是 清單 1中的一個簡單的 JSF 表單。一個 JSF 頁面基本上就是這個樣子的。這個 JSF 表單有輸入文本字段 from和 to cities、 departure 和 return dates,還有提交和重設表單的按鈕(我們會在稍后分析清單1中每一個標記的意義)。現在,假設提交這個表單產生了一個 faces 請求。
這個請求被 FacesServlet
所接收、并在向客戶發回響應之前通過不同的階段。 圖 2展示了如何對 JSF 請求進行處理。讓我們看一看這是如何進行的。
1. 接收請求
FacesServlet
接收請求并從 FacesContextFactory
得到 FacesContext
的一個實例。
2. 委托生命周期處理
FacesServlet
通過對在 faces 上下文中傳遞的 Lifecycle
實現調用 execute
方法將生命周期處理委托給 Lifecycle
接口。
3. Lifecycle 執行每一階段
Lifecycle
實現執行從重建組件樹階段開始的每一階段。
4. 創建的組件樹
在重建組件樹階段,用 travelForm
中的組件創建一個組件樹。這個樹以 UIForm
作為根,用不同的文本字段和按鈕作為其子組件。
fromCity
字段有一個驗證規則,它規定其不能為空,如 validate_required
標記所示。這個標記將 fromCity
文本字段與一個 JSF Validator
鏈接起來。
JSF 有幾個內建的驗證器。相應的 Validator
是在這個階段初始化的。這個組件樹緩存在 FacesContext
中、并且這個上下文會在后面用于訪問樹及調用任何一個事件處理程序。同時 UIForm
狀態會自動保存。所以,當刷新這一頁時,就會顯示表單的原始內容。
5. 從樹中提取值
在應用請求值階段,JSF 實現遍歷組件樹并用 decode
方法從請求中提取值,并在本地設置每一個組件。如果在這個過程中出現了任何錯誤,那么它們就在 FacesContext
中排隊并在呈現響應階段顯示給用戶。
同時,在這個階段排隊的所有由像單擊按鈕這樣的用戶操作產生的事件,都廣播給注冊的偵聽器。單擊 reset 按鈕會將文本字段中的值重新設置為它們原來的值。
6. 處理驗證
在處理驗證階段,對在應用請求值階段設置的本地值進行所有與各組件相關的驗證。當 JSF 實現對每一個注冊的驗證器調用 validate
方法時就會進入此階段。
如果任何一項驗證失敗,那么生命周期就會進入呈現響應階段,在那里呈現帶有錯誤信息的同一頁面。在這里,所有在這一階段排隊的事件同樣都會廣播給注冊的偵聽器。
JSF 實現處理源字段上的驗證器。如果數據是無效的,那么控制就交給呈現響應階段,在這個階段重新顯示 FlightSearch.jsp 并帶有相關組件的驗證錯誤。通過在 JSP 頁面中聲明 output_errors,
,頁面中的所有錯誤都會顯示在頁面的底部。
7. 設置模型對象值
在更新模型值階段,成功處理了所有驗證后,JSF 實現就通過對每一組件調用 updateModel
方法用有效值設置模型對象值。如果在將本地數據轉換為由模型對象屬性所指定的類型時出現任何錯誤,那么生命周期就進入呈現響應階段,并將錯誤顯示出來。來自表單字段屬性的值會填充為模型對象的屬性值。
8. 可以調用 ActionListener
可以將一個 ActionListener
與一個用戶操作,如單擊提交按鈕相關聯,如 清單 1所示。在調用應用程序階段,對 FlightSearchActionListener
調用了 processAction
方法。在實際應用中, processAction
方法在調用后會搜索數據以找出滿足條件的航班,并從組件的 action 屬性中提取輸出。
在本文提供的這個示例 Web 應用程序中,我們使用了靜態數據表示航班表。這個方法還將提取的 action 屬性發送給 NavigationHandler
實現。 NavigationHandler
查詢 faces-config.xml 文件 -- 這是 JSF 的默認應用程序配置文件 -- 以根據這一輸出確定下一頁是什么。
9. 呈現響應 在呈現響應階段,如果在 faces 上下文中沒有錯誤,就顯示由查詢配置文件得到的這一頁 FlightList.jsp。如果是因為前面任一階段的錯誤而到達這一階段的,那么就會重新顯示帶有錯誤信息的 FlightSearch.jsp。
圖 2. 處理一個 JSF 請求
單擊這里以觀看該圖。
清單 1. FlightSearch.jsp,一個簡單的 JSF 表單
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<f:use_faces>
<h:form id="flightForm" formName="flightForm" >
<h:input_text id="fromCity" valueRef="FlightSearchBean.fromCity">
<f:validate_required/>
<h:input_text/>
<h:input_text id="toCity" valueRef="FlightSearchBean.toCity">
<h:input_text id="departureDate"
valueRef="FlightSearchBean.departureDate">
<h:input_text id="arrivalDate"
valueRef="FlightSearchBean.arrivalDate">
<h:command_button id="submit" action="success"
label="Submit" commandName="submit" >
<f:action_listener
type="foo.bar.FlightSearchActionListener"/>
</h:command_button>
<h:command_button id="reset" action="reset" label="Reset"
commandName="reset" />
<h:output_errors/>
</h:form>
</f:use_faces>
|
在這段代碼中使用了兩個 JSF-RI 的標記庫。 html_basic 標記庫定義了 HTML 組件常用的標記,而 jsf-core 標記庫包含用于注冊偵聽器和驗證器的標記。其他標記有:
f:use_faces
標記向 JSF 實現表明后面的標記是 faces 標記。
f:validate_required
標記表明它所在的字段(在 FlightSearchBean
中是 fromCity
字段)在提交表單時應該有值。
h:form
和 h:input_text
標記分別表示一個名為 flightSearchForm
的 HTML 表單和各種文本字段。
h:command_button
標記用于表示提交和重設按鈕。
- 最后,
h:output_errors
標記類似于 Struts html:errors
標記,用于顯示在表單字段驗證中出現的任何錯誤。
一個名為 FlightSearchBean 的 JavaBean 表示在更新模型值階段用 UIComponent
數據更新的模型。通常在 JSP 頁中 JavaBean 是用 jsp:useBean
標記聲明的。您可能注意到了在 FlightSearch.jsp 中沒有這樣做。這是因為可以使用 JSF 的一個名為 Managed Beans 的功能,在 faces 配置文件中聲明所有 JSP 頁面使用的 JavaBeans 組件。在開始時,servlet 容器會初始化這些 JavaBeans 組件。faces-config.xml 文件中的 FlightSearchBean 入口如清單 2所示:
清單 2. faces-config.xml 的 TravelInfoBean入口
<managed-bean>
<managed-bean-name>FlightSearchBean</managed-bean-name>
<managed-bean-class>
foo.bar.FlightSearchBean
</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
|
現在讓我們看一看這些階段是如何處理響應的。
呈現 faces 響應
一個 faces 響應是由 Faces 應用程序在生成包含 JSF 標記的 JSP 頁時生成的。這個響應可以是 JSF 應用程序的 faces 或者 non-faces 響應。
在我們的例子中,清單 1 中頁面的呈現是一個 faces 響應。您可能熟悉 Tag
接口的 doStartTag()
和 doEndTag()
方法。在 JSF 和 Struts-Faces 中,每一個標記都是從 UIComponentTag
擴展的。 UIComponentTag
實現了 doStartTag()
和 doEndTag()
方法。
它還有兩個抽象方法 getComponentType()
和 getRendererType()。
通過在具體的標記類中實現這兩個方法,就可以分別指定組件和 renderer 的類型。
考慮一個帶有文本字段的簡單 JSF 表單。在呈現 JSF 表單時執行以下一系列步驟。
1. 調用 doStartTag() 方法
Servlet 窗口對 FormTag
調用 doStartTag()
方法。
2. 得到 UIComponent
FormTag
從 getComponentType()
方法得到其 UIComponent。
UIComponentTag
( FormTag
的父組件)使用 getComponentType()
以從 faces-config.xml 文件中查詢這個組件的類名,并創建 UIComponent(FormComponent
)的一個實例。
3. 得到 renderer
下一步, FormTag
從 getRendererType
方法中得到其 renderer 。與組件類型一樣,renderer 名是在 faces-config.xml 文件中查詢的。
4. 調用編碼方法
在創建了 FormComponent
和 FormRenderer
后,對 FormComponent
調用 encodeBegin()
方法。每一個標記的呈現都由 encodeBegin()
開始、由 encodeEnd()
結束。 encodeBegin()
方法是按嵌套的順序調用的。
5. 結束標記和呈現 HTML
servlet 容器對標記調用 doEndTag()
方法。以嵌套的反順序對每一個組件調用 encodeEnd()
方法。在最后,表單和所有嵌套的組件都呈現為 HTML。這時,HTML 就生成完畢,并呈現出對應于 JSP 的 HTML。
圖 3 顯示構成生成 faces 響應的事件序列。
圖 3. 呈現一個 faces 響應
單擊這里以查看該圖。
為什么將這三者集成為一體?
隨著 JSP 和相關規范的不斷發展,像 JSF 和 JSP 標記庫(或者 JSTL,它使用簡單的標記封裝許多 JSP 應用程序常用的核心功能)這樣的新標準正在不斷出現。下面是使用集成為一個整體的新技術一些好處:
- 更清晰地分離行為和展示。 將標記、 renderer 和組件分離,就可以更好地定義開發周期中的頁面作者和應用程序開發人員的作用。
- 改變一個組件的展示不會有雪崩效應。現在您可以容易地只對 renderer 作出改變。在傳統的 MVC 模型中,由于沒有這種分離,對于標記的任何改變都需要改變業務邏輯。現在再不需要這樣了。
- renderer 無關性。 也可以說是協議無關性,通過對帶有多個 renderer 的多種展示設備重復使用組件邏輯實現。使用不同 renderer 的能力使得不再需要對特定的設備編寫整個表示層代碼。
- 組裝和重用自定義組件的標準。JSF 的考慮范圍超出了“表單和字段”,它提供了豐富的組件模型用以呈現自定義 GUI 組件。用 JSF 可以定制每一個組件在頁面中的外觀和行為。開發人員還擁有創建他們自己的 GUI 組件(如菜單和樹)的能力,這些組件可以用簡單的自定義標記容易地加入到任何 JSP 頁面中。就像 AWT 和 Swing 所提供的 Java 前端 GUI 組件一樣,我們可以在我們的 Web 頁而中有自定義的組件,它們使用自己的事件處理程序并有定制的外觀。這是 Web 層的 GUI 天堂!
Struts 是一種已經擁有大量客戶基礎的框架。許多 IT 部門認識到這種 MVC 框架的價值并使用它有一段時間了。JSF 沒有像 Structs 這樣強大的控制器結構,也沒有像它那樣標準化的 ActionForm
和 Actions
(及它們聲明的能力)。將 Tiles 集成到集合體中,就給了自己重復使用和以無縫的方式改變公司布局的能力。
移植支持 JSF 的 Struts 應用程序的挑戰是雙重的。首先,Struts 標記不是 JSF 兼容的。換句話說,它們沒有像 JSF 規范所規定的那樣擴展 UIComponentTag
,所以,JSF 不能解釋它們并關聯到 UIComponent
和 Renderers
。
其次,在 FacesServlet
與 Struts RequestProcessor
之間沒有鏈接。在 Struts 應用程序中, RequestProcessor
負責用 ActionForm
和 Actions
類中的回調方法顯示。 ActionForm
屬性和 validate()
的 getter 和 setter 是 ActionForm
中的回調方法。對于 Action
, execute()
是回調方法。除非調用了 RequestProcessor
,否則 Struts ActionForm
和 Actions
類中的回調方法沒有機會調用業務邏輯。
將 Struts 和 JSF 與 Struts-Faces 集成
這里,您可能會問是否有軟件可以幫助將 Struts 與 JSF 集成,或者是否必須自己編寫集成軟件。
好消息是已經有這樣的軟件了。 Struts-Faces 是一個早期發布的 Struts JSF 集成庫。這個庫是由 Craig McClanahan 創建的,它使得將現有 Struts 應用程序移植到 JSF 變得容易了(保留了對現有 Struts 投資的價值)。Struts-Faces 還力圖與 JSF 進行簡潔的集成,這樣就可以在前端使用 JSF,同時后端仍然有熟悉的 Struts 組件。
圖 4 展示了 Struts-Faces 與 JSF 類之間的關系。藍色的類屬于 Struts-Faces。
圖 4. Struts-Faces 類圖
單擊這里以查看該圖。
下面是 Struts-Faces 的主要組件:
FacesRequestProcessor
類,它處理所有 faces 請求。這個類繼承了常規 Struts RequestProcessor
,并處理 faces 請求。Non-faces 請求發送給出其父類 -- RequestProcessor
。
ActionListenerImpl
類,它處理像提交表單或者單擊鏈接這樣的 ActionEvent
。這個類用于代替由 JSF-RI 提供的默認 ActionListener
實現。只要在一個 faces 請求中生成 ActionEvent
,就會對 ActionListenerImpl
調用 processAction()
方法、并將 ActionEvents
轉送給 FacesRequestProcessor
。這很有意思,因為 RequestProcessor
通常只由 ActionServlet
調用以處理 HTTP 請求。
FormComponent
類,它擴展了 JSF Form 組件,但是是在 Struts 生命周期內調用的。
FormComponent
的 renderer 和標記。
- 只用于輸出的數據標記和 renderer ,這里不需要分離組件。例如,
ErrorsTag
和 ErrorsRenderer
用于在 HTML 中顯示表單錯誤。
ServletContextListener
的名為 LifeCycleListener
的實現。它用于在初始化時注冊相應的 RequestProcessor
。
- faces-config.xml 文件。這個文件已經捆綁在 struts-faces.jar 文件中。
清單 3 展示了使用 Struts-Faces 標記的 FlightSearch.jsp。它類似于在 清單 1中展示的 JSF 例子。這里用粗體突出了區別之處。在這里,您會發現增加了一個新標記庫 tags-faces。這個標記庫定義聲明這些標記由 Struts-Faces API 所使用。
清單 3. FlightSearch.jsp 使用 Struts-Faces 標記
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-faces"
prefix="s" %>
<f:use_faces>
<s:form action="/listFlights">
<h:input_text id="fromCity" valueRef="FlightSearchForm.fromCity"/>
<h:input_text id="toCity" valueRef="FlightSearchForm.toCity"/>
<h:input_text id="departureDate"
valueRef="FlightSearchForm.departureDate">
<h:input_text id="arrivalDate"
valueRef="FlightSearchForm.arrivalDate">
<h:command_button id="submit" action="success" label="Submit"
commandName="submit" />
<h:command_button id="reset" action="reset" label="Reset"
commandName="reset" />
<s:errors/>
</s:form>
</f:use_faces>
|
s:form
標記用于創建這個 HTML 表單。表單的 action 屬性是 /listFlights而不是像 清單 1那樣指定為表單名 flightForm。在 JSF 中,表單名只是指定給 UIForm 的名字而沒有更多的意義。
FlightSearchBean
是 JSF 表單的模型,并在更新模型值階段得到其值。不過在 Struts 中,表單 action 指向 Struts 配置文件 struts-config.xml 中的 ActionMapping
。為了理解它是如何起作用的,還必須看一下清單 4 中顯示的 struts-config.xml 文件。
您會看到 /listFlights 的 ActionMapping
表明這個 URI 路徑的 ActionForm
是 foo.bar.FlightSearchForm
,而 Action
類是 foo.bar.FlightSearchAction
。換句話說, ActionForm
( FlightSearchForm
)本身就是 Struts-Faces 中的 HTML 表單的模型,它的 action 間接地指向這個模型(您可以在清單 3 中看到這一點,那里文本字段標記指向 FlightSearchForm
。在普通 Struts 應用程序中這會是 <html:text property="fromCity"/>
)。
清單 4. 在 struts-config.xml 中聲明 Action
<form-bean name="FlightSearchForm"
type="foo.bar.FlightSearchForm"/>
<!-- ========== Action Mapping Definition ========================= -->
<action-mappings>
<!-- List Flights action -->
<action path="/listFlights"
type="foo.bar.FlightSearchAction"
name="FlightSearchForm"
scope="request"
input="/faces/FlightSearch.jsp">
<forward name="success" path="/faces/FlightList.jsp"/>
</action>
</action-mappings>
|
您會注意到在 action 屬性中缺少熟悉的 .do。這是因為 Struts-Faces 使用表單 action 本身作為表單名(它還應該與 Struts 配置文件中的 ActionForm
名相匹配)。
 |
集成 Struts 和 Tiles 的五個步驟
以下五步可以讓 Struts 1.1 和 Tiles 共同工作: 1. 創建一個 JSP 以表示站點的布局。這是主 JSP,并帶有頁頭、頁體和頁腳的占位符。分別用 Tiles 標記添加到主 JSP 頁面中。 2. 創建一個 Tiles 定義文件并定義每個集成頁面的每個占位符中必須包括哪個 JSP 頁面。用惟一的名稱標識出每一個合成頁面定義。 3. 在 struts-config.xml 文件中改變全局和本地轉發以使用上一步驟中給出的惟一名稱而不是別名。 4. 在啟動時用 TilesPlugIn 裝載 Tiles 定義文件。將 TilesPlugIn 項加入到 struts-config.xml 文件中。 5. 將 TilesRequestProcessor 項添加到 struts-config.xml 文件中。這是支持 Tiles 的 Struts 應用程序的默認請求處理程序。 | |
還要注意我們在這里沒有使用 JSF validation
標記。這是因為在 Struts 中,驗證是在 ActionForm
類中的 validate()
方法中進行的,有可能是通過使用 Commons-Validator。 s:errors
標記類似于 Struts 錯誤標記并用于顯示在驗證時出現的錯誤消息。
另一件要注意的事情是沒有 ActionListener
顯式地與提交按鈕相關聯。這是因為在 Struts-Faces 中已經提供了 ActionListener
并且總是將 faces 請求與 ActionEvent
s 一同轉交給 FacesRequestProcessor
,在那里根據 struts-config.xml 文件將請求分派給相應的 Action
類。
將Struts 應用程序移植到 JSF
為了將 Struts Web 應用程序與 JSF 集成,遵循以下步驟:
- 將 struts-faces.jar 文件與特定于 JSF 的 JAR(jsf-api.jar、jsf-ri.jar) 添加到 Web 應用程序的 WEB-INF/lib目錄中。
- 如果準備使用 JSF 和 JSTL,則將特定于 JSTL 的 JAR(jstl.jar、standard.jar)添加到 WEB-INF/lib 文件夾中。這一步只有在部署到常規 Tomcat 時才會需要。JWSDP 已經提供了這些 JAR。
- 修改 Web 應用程序部署描述符 ( /WEB-INF/web.xml)以便有一個 Faces Servlet 項, 如清單 5 所示。
- 修改 JSP 頁面以使用 JSF 和 Struts-Faces 標記而不是 Struts 標記。特別是用 Struts-Faces 相應標記替換
html、b
ase、
form
和 errors
標記。用 JSF 相應標記替換 text
、 textarea
和 radio
標記。Struts-Faces 沒有單獨針對這些的標記。盡管沒有要求,但是您可能還會考慮用 JSTL 標記替換 Struts Logic 標記。
- 對于每一個使用 JSF 標記的 JSP,修改 struts-config.xml 文件以在指向該 JSP 的 Action Mapping 中的 global-forwards和 local-forwards中加入前綴 /faces。
- 如果 Web 應用程序使用了任何您創建的自定義組件,那么您就需要用 JSF 實現的默認 RenderKit 注冊它們。可以通過在 WEB-INF 文件中創建一個 faces-config.xml 文件、并增加每一個組件和 renderer 的項做到這一點。不過,要記住 faces-config.xml 文件已經綁定在 struts-faces.jar 文件中了。您必須從 struts-faces.jar 文件中提出它、加入自己的內容并將它放到 WEB-INF文件夾中。
清單 5. 在 web.xml 中聲明 FacesServlet
<!-- JavaServer Faces Servlet Configuration -->
<servlet>
<servlet-name>faces</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- JavaServer Faces Servlet Mapping -->
<servlet-mapping>
<servlet-name>faces</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
|
集成 Struts-Faces 和 Tiles 的挑戰
Struts-Faces 庫提供了 Struts 與 JSF 之間的一個高效的橋梁,使得在 J2EE Web 應用程序中擁有豐富的表示層成為現實。您可以通過在組合體中添加 Titles 使表示層更豐富,這樣不僅得到了 Struts 和 JSF 組合的好處,而且還可以高效地重復使用不同的 JSP 頁面,因為它們將由可以根據需要添加或者刪除的組件部分或者 tiles 所構成。
本文已經展示了 Struts 和 JSP 的集成,您會想將 Tiles 加入到組合中只是小事一樁,是不是?
不幸的是,JSF 仍然處于早期階段,還沒有給出最后的發布。基于這一考慮,Struts-Faces 集成軟件開發仍然在不斷地發展以包括 JSF 的不同的功能,并且還沒有支持 Tiles。
Struts 和 Tiles 可以無縫地共同工作,但是在集成之路上您會遇到路障。在下面幾小節中,您會看到在與 Tiles 共同使用 Struts-Faces 集成庫時經常遇到的問題的匯總。對于每一個問題,我們詳細說明了一個修改 Struts-Faces 類的解決方案。我們將用一個航班搜索示例解釋這個解決方案。
清單 6 展示了航班搜索頁面的布局。注意我們稱它為航班搜索頁面而不是 FlightSearch.jsp。這是因為 FlightSearch JSP 是用戶在 foobar 旅行 Web 站點看到的合成頁面的主體。
現在,我們保持實際的 FlightSearch.jsp 不變。我們將隨著進展改變它。在您這邊,也需要用航班搜索頁的定義創建一個 Tiles 定義文件。清單 7(緊接著清單 6)展示了 Tiles 定義文件中航班搜索頁的一項。注意對帶有 extends
屬性的主布局模板的重復使用。
在清單 6 和 7 后是每一個可能的挑戰。
清單 6. 航班搜索例子的 Tiles 布局
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-faces"prefix="s" %>
<!-- Layout component parameters: header, menu, body, footer -->
<s:html>
<head>
<title> <tiles:getAsString name="title"/></title>
<s:base/>
</head>
<body>
<TABLE border="0" width="100%" cellspacing="5">
<tr>
<td><tiles:insert attribute="header"/></td>
</tr>
<tr>
<td><tiles:insert attribute="body"/></td>
</tr>
<tr><td><hr></td></tr>
<tr>
<td><tiles:insert attribute="footer" /></td>
</tr>
</TABLE>
</body>
</s:html>
|
清單 7. 航班搜索頁的 Tiles 定義
<!-- Master Layout definition -->
<definition name="foobar.master-layout"
path="/faces/layout/MasterLayout.jsp">
<put name="title" value="Welcome to Foo Bar Travels" />
<put name="header" value="/faces/common/header.jsp" />
<put name="footer" value="/faces/common/footer.jsp" />
<put name="body" value="" />
</definition>
<!-- Definition for Flight Search Page -->
<definition name="/foobar.flight-search"
extends="foobar.master-layout">
<put name="body" value="/faces/FlightSearch.jsp" />
</definition>
|
響應已經提交
這是您在試圖訪問航班搜索表單時馬上會看到的第一個問題。小心查看堆棧跟蹤。您會看到問題出在類 com.sun.faces.lifecycle.ViewHandlerImpl
上。這是一個實現了 ViewHandler
接口的 JSF-RI 類。
圖 2展示了 ViewHandler
所扮演的角色。這是一個將請求轉發給下一頁的類。在轉發請求時,它不在轉發前檢查響應的狀態 -- 這只有在使用 Tiles 時才會發生,因為 Tiles 內部將 JSP 頁面包括在響應內,而 JSF-RI 在第一次轉發后提交響應、然后試圖再次轉發給下面的包括 JSP 的 Tiles。
要解決這個問題,必須創建一個自定義的 ViewHandler
實現,它將檢查響應的狀態以確定它是否提交過。如果響應沒有提交過,那么請求就轉發給下一頁,否則,就加入請求并顯示相應的 JSP。我們將創建一個名為 STFViewHandlerImpl
的類,它實現了 ViewHandler
接口并實現了所需要的方法 renderView()。
清單 8 展示了 STFViewHandlerImpl
中的 renderView()
方法:
清單 8. STFViewHandlerImpl 中的 renderView()方法
RequestDispatcher rd = null;
Tree tree = context.getTree();
String requestURI = context.getTree().getTreeId();
rd = request.getRequestDispatcher(requestURI);
/** If the response is committed, include the resource **/
if( !response.isCommitted() ) {
rd.forward(request, context.getServletResponse());
}
else {
rd.include(request, context.getServletResponse());
}
|
現在您實現了自己的 ViewHandler
,如何通知 JSF-RI 使用您的 ViewHandler
而不是默認的實現呢?要回答這個問題,就必須理解 FacesServlet
的工作過程。
在 Faces 初始化過程中, FacesServlet
會讓 LifecycleFactory
實現返回 Lifecycle
類的一個實現,如清單 9 所示:
清單 9. FacesServlet 中 Faces 的初始化
//Get the LifecycleFactory from the Factory Finder
LifecycleFactory factory = (LifecycleFactory)
FactoryFinder.getFactory("javax.faces.lifecycle.LifecycleFactory");
//Get the context param from web.xml
String lifecycleID =
getServletContext().getInitParameter("javax.faces.lifecycle.LIFECYCLE_ID");
//Get the Lifecycle Implementation
Lifecycle lifecycle = factory.getLifecycle(lifeCycleID);
|
Lifecycle
實現對象擁有在呈現響應階段要使用的 ViewHandler
。您可以通過對 Lifecycle
實現調用 setViewHandler
方法讓自己的 ViewHandler
實現成為默認的。
現在問題變為如何得到默認 Lifecycle
實現?回答是不需要這樣做。只要創建一個新的實現并用一個惟一 ID 注冊它,如清單 10 所示:
清單 10. 注冊自定義 ViewHandler 和 Lifecycle
//Get the LifecycleFactory from the Factory Finder
LifecycleFactory factory = (LifecycleFactory)
FactoryFinder.getFactory("javax.faces.lifecycle.LifecycleFactory");
//Create a new instance of Lifecycle implementation -
//com.sun.faces.lifecycle.LifecycleImpl
//According to the documentation, factory.getLifecycle("STFLifecycle")
//should work, but JSF-RI has a defect.
//Hence this workaround of creating a RI class explicitly.
LifecycleImpl stfLifecycleImpl = new LifecycleImpl();
//Create a new instance of our STFViewHandler and set it on the Lifecycle
stfLifecycleImpl.setViewHandler(new STFViewHandlerImpl());
//Register the new lifecycle with the factory with a unique
//name "STFLifecycle"
factory.addLifecycle("STFLifecycle", stfLifecycleImpl);
|
您可以看到 lifecycleId
硬編碼為 STFLifecycle
。實際上不是這樣。當您回過頭分析 清單 9時就會清楚。 FacesServlet
從在 web.xml 文件中聲明的上下文參數中得到名為 javax.faces.lifecycle.LIFECYCLE_ID
的 lifecycle ID,如下所示:
<context-param>
<param-name>javax.faces.lifecycle.LIFECYCLE_ID</param-name>
<param-value>STFLifecycle</param-value>
</context-param>
|
因為 FacesServlet
取決于其初始化時的 Lifecycle
實現,在 清單 10中展示的代碼應該在 FacesServlet
初始化之前執行。通過創建另一個 servlet 并在 FacesServlet
之前初始化它而做到這一點。
但是一種更聰明的辦法是實現一個 ServletContextListener
接口。這個類聲明兩個方法: contextInitialized()
和 contextDestroyed()
,在 Web 應用程序被創建及 Web 應用程序被銷毀之前會分別調用它們。因而 清單 10中的代碼在 contextInitialized()
方法中執行,而自定義 ViewHandler
已經用標識名 STFLifecycle
注冊到 Lifecycle
,并且可被 FacesServlet
使用。 ServletContextListener
類本身是在 web.xml 文件中聲明的,如下所示:
<listener>
<listener-class>foo.bar.stf.application.STFContextListener
</listener-class>
</listener>
|
這不是注冊一個帶有自定義 ViewHandler
的 Lifecycle
惟一方法。事實上 FactoryFinder
實現了自己的發現算法以發現 Factory
對象,包括 LifecycleFactory
。這些機制按照順序包括在系統屬性中查看工廠實現類名的機制、faces.properties file、或者 1.3 Services 發現機制( META-INF/services/{factory-class-name}
)。不過,我們討論的這種機制是最容易的,也是最不具有破壞性的一種。
404 Resource Not Found
在解決了提交響應的問題后,單擊任何一個 Tiles 特定的鏈接或者輸入一個會呈現 Faces 響應的 URL。在這里,可以輸入顯示 FlightSearchForm
的 URL。
在這樣做了以后,您會得到一個 foobar.flight-search - 404 Resource Not Found 錯誤。 foobar.flight-search
是航班搜索頁面的 Tiles 定義的名字。 FacesRequestProcessor
不能處理 Tiles 請求(因為它擴展的是 RequestProcessor
而不是 TilesRequestProcessor
),所以會得到錯誤。
為解決這個問題,我們將創建一個名為 STFRequestProcessor
(表示 Struts-Tiles-Faces Request Processor)的新的請求處理程序。現在我們將拷貝 FacesRequestProcessor
的所有代碼到這個新類中。惟一的區別是 STFRequestProcessor
繼承的是 TilesRequestProcessor
而不是繼承常規的 RequestProcessor
。這個新的 RequestProcessor
可以處理 Tiles 請求。清單 11 詳細列出了這個 STFRequestProcessor
:
清單 11. STFRequestProcessor.java
正如您所知道的, Struts
框架的 RequestProcessor
是在 struts-config.xml 文件中指定的。將下面的項添加到 struts-cinfig.xml
文件中后, STFRequestProcessor
就成為處理程序:
<controller processorClass="foobar.stf.application.STFRequestProcessor" />
|
表單提交顯示返回同一個表單
由于 STFRequestProcessor
的作用,這時您就可以瀏覽并查看航班頁面了。不過,在提交航班搜索表單時,您會得到返回來的同一個表單,而且沒有頁頭和頁腳!并且沒有驗證錯誤。事實上,根本就沒有進行驗證!
為了了解到底發生了什么事情,我們用瀏覽器回到航班頁面并檢查 HTML 源代碼。您會看到像下面這樣的一項:
<form name="FlightSearchForm" method="post"
action="/flightapp/faces/FlightSearch.jsp">
|
注意表單 action 是指向 JSP 頁而不是一個 .do 的。啊哈!這就是問題!這不是由于同時使用 Tiles 和 Struts-Faces 而帶來的新問題,Struts-Faces 的默認行為是讓 JSP 與表單 action 有同樣的名字。這種行為在有單一的 JSP 頁(如在前面的 Struts-Faces 例子中)時沒有問題。 清單 3展示了原來的 FlightSearch.jsp,讓我們繼續并像下面這樣修改 action:
<s:form action="/listFlights.do>
|
當然,光有這種修改并不能解決問題。作了這種改變后,您就會發現 STFRequestProcessor
不能找到 ActionForm
。顯然還需要其他的改變。
不過,在繼續往下之前,看一下圖  5。它顯示了在呈現負責 Struts-Faces 表單的 faces 時相關的一系列事件。這與 圖 3相同,除了在 FormComponent
中突出顯示的方法 createActionForm()。
由 Struts-Faces API 提供的 FormComponent
類是 javax.faces.component.UIForm
的特殊子類,它支持請求或者會話范圍的表單 Bean。
圖 5. 呈現 Struts-Faces 響應
單擊這里以查看該圖。
正如您所看到的, createActionForm()
方法使用 action 名以從 Struts 配置文件中得到 ActionMapping
。因為沒有對于 /listFlights.do 的 ActionMapping
,所以 Struts 不能找到 ActionForm。
這個問題的解決方法是使用 org.apache.struts.util.RequestUtils
。 RequestUtils
中的 static 方法 getActionMappingName()
具有足夠的智能解析映射到正確 ActionMapping
的路徑( /x/y/z)或者后綴( .do)。
清單 12 以粗體顯示對 createActionForm
方法的改變。我們沒有對 Struts-Faces 中的 FormComponent
作這些改變,而是通過繼承 FormComponent
并覆蓋 createActionForm()
方法創建了一個新的 STFFormComponent。
清單 12. FormComponent 中修改過的 createActionForm() 方法
// Look up the application module configuration information we need
ModuleConfig moduleConfig = lookupModuleConfig(context);
// Look up the ActionConfig we are processing
String action = getAction();
String mappingName = RequestUtils.getActionMappingName(action);
ActionConfig actionConfig = moduleConfig.findActionConfig(mappingName);
....
....
|
對新的 STFFormComponent
還要作一項改變。Struts-Faces 將 action 名本身作為表單名。這需要改變,因為 action 帶有后綴 .do,而表單名沒有后綴 .do。所以我們在 STFFormComponent
上增加一個名為 action
的新屬性,并覆蓋 getAction()
和 setAction()
方法。
FormRenderer 的改變
必須對 FormRenderer
(以 HTML 格式呈現 Struts-Faces 表單的類)的 encodeBegin
方法進行類似于 清單 10所示的修改。
同樣,通過繼承 FormRenderer
做到這一點。此外,還必須改變寫出到 HTML 的表單 action。清單 13以粗體詳細列出了這些改變:
清單 13. FormRenderer 的改變
protected String action(FacesContext context, UIComponent component) {
String treeId = context.getTree().getTreeId();
StringBuffer sb = new StringBuffer
(context.getExternalContext().getRequestContextPath());
sb.append("/faces");
// sb.append(treeId); -- This is old code, replaced with
// the two lines below.
STFFormComponent fComponent = (STFFormComponent) component;
sb.append(fComponent.getAction());
return (context.getExternalContext().encodeURL(sb.toString()));
}
|
FormTag的改變
正如您已經知道的,當組件和 renderer 改變時,標記也必須改變。在這里,通過繼承 Struts-Faces 中的 FormTag
創建一個新的標記: STFFormTag
。不必改變任何功能,只要覆蓋 getComponentType()
和 getRendererType()
方法。清單 14 展示了從 STFFormComponent
覆蓋的方法:
清單 14. FormTag 的改變
public String getComponentType()
{
return ("STFFormComponent");
}
public String getRendererType()
{
return ("STFFormRenderer");
}
|
修改 faces-config.xml 文件
自定義組件和 renderer 必須在 faces-config.xml 文件中聲明,這樣 JSF 框架才可以初始化并使用它們。現在我們已經創建了一個新組件 STFFormComponent
和一個新 renderer STFFormRenderer
。
現在我們將在 faces-config.xml 文件中增加一個聲明,如清單 15 所示。 component-class 是組件的完全限定類名。 component-type 指的是在 STFFormTag
( 清單 12)中用于標識組件的名字。以類似的方式發現和解釋 renderer。注意 faces-config.xml 文件是在 struts-faces.jar 文件中的。從 struts-faces.jar 文件中取出這個文件并將它放到 Web 應用程序的 WEB-INF文件夾中并修改它。
清單 15. 在 faces-config.xml 中聲明自定義組件和 renderer
<faces-config>
<!-- Custom Components -->
<component>
<component-type>STFFormComponent</component-type>
<component-class>
foobar.stf.component.STFFormComponent
</component-class>
</component>
..
..
..
<!-- Custom Renderers -->
<render-kit>
<renderer>
<renderer-type>STFFormRenderer</renderer-type>
<renderer-class>
foobar.stf.renderer.STFFormRenderer
</renderer-class>
</renderer>
..
..
..
</render-kit>
</faces-config>
|
修改 struts-faces.tld 文件
您不會在這個示例 Struts-Faces 應用程序中看到 struts-faces.tld 文件,它打包到了 struts-faces.jar 文件中。打開并分析這個文件。它聲明了一個名為 org.apache.struts.faces.taglib.LifecycleListener
的類,這個類實現了 ServletContextListener
并初始化 FacesRequestProcessor
。
因為希望使用新的 STFRequestProccessor
,所以必須將這個文件從 struts-faces.jar 文件中刪除,將它放到 Web 應用程序的 WEB-INF 文件夾中,并刪除偵聽器聲明。如果讓這個 tld 文件保持原樣,那么在初始化這個 Web 應用程序時,除了 STFRequestProcessor
,還會實例化一個 FacesRequestProcessor。
修改 base href 標記
現在,您已經完成了 Struts、Tiles、JSF 集成的最困難的部分了。您甚至可以瀏覽航班搜索頁面,并輸入搜索標準查看航班列表。現在試著從航班列表頁面返回航班搜索表單。您會得到一個 HTTP 400 錯誤。這個錯誤的原因是 HTML base href
標記。它被設置為 Master Layout 頁面。
<base href=
"http://localhost:8080/stf-example/faces/layout/MasterLayout.jsp" />
|_________| |_____________________|
Context Servlet Path
|
程序所有頁面瀏覽都是相對于布局頁面計算的。如果加入的 base href
標記只達到 Web 應用程序上下文則會很方便,像這樣:
<base href="http://localhost:8080/stf-example/" />
|
我們可以通過定制 Struts-Faces BaseTag
做到這一點。這個類中的改變相當微不足道。只須在 base href 中去掉 HttpServletRequest.getServletPath()
。
因為這些改變是與顯示相關的,所以為它創建了一個名為 STFBaseRenderer
的新 renderer。這個新標記稱為 STFBaseTag
,它聲明 STFBaseRenderer
作為其關聯的 renderer。不需要新的組件。
有了這些信息,通過繼承 BaseTag
并覆蓋 getRendererType
方法創建新的 STFBaseTag
,如下所示:
public String getRendererType()
{
return ("STFBaseRenderer");
}
|
到目前為止所作的改變
恭喜!經過這些相對較小的修改,您已經成功地集成了 Struts、Tiles 和 JSF,并保留了您以前在這些技術上所做的所有投資。本文演示了如何將 JSF 強大的前端能力、 Tiles 的內容格式編排優勢以及 Struts 控制器層的靈活性結合在一個包中,使得創建一個 J2EE Web 應用程序成為一項更容易的任務。
我們討論了定制 Struts 類以便與 JavaServer Faces 和 Tiles 框架形成緊密集成的工作關系,包括下面這些修改和增加:
- 新的
ViewHandler
,用于檢查提交的響應。
- 新的
ServletContextListener
,用于創建新的 Lifecycle
實現并注冊這個定制的 ViewHandler。
- 新的
RequestProcessor
,用于處理 Tiles 請求。
- 修改過的 web.xml 文件,聲明新的
ServletContextListener
和 JSF Lifecycle ID。
- 新的
FormTag、
FormComponent
和 FormRenderer
類。
- 新的
BaseTag
和 BaseRenderer
類。
- 修改過的 faces-config.xml 文件,它聲明了新的組件和 renderer。
- 修改過的 struts-faces.tld 文件,不聲明偵聽器。
希望它可以概括本文中使用的復合技術,最重要的是,我們為您提供了將 Struts、Tiles 和 JavaServer Faces 結合到用于構建 Web 應用程序的一個強大而靈活的機制中的一個令人信服的路線圖。
代碼下載