JavaServer Faces (JSF) 是用于 Java™ Web 應(yīng)用程序的第一個標(biāo)準(zhǔn)化的用戶界面框架。而 Seam 是一個擴(kuò)展 JSF 的強(qiáng)大的應(yīng)用程序框架。在這個由三部分組成的新系列中的第一篇文章中,發(fā)現(xiàn)這兩種框架之間的互補性。Dan Allen 介紹了 Seam 對 JSF 生命周期的增強(qiáng),包括上下文狀態(tài)管理、 RESTful URL、Ajax remoting、適當(dāng)?shù)漠惓L幚砗图s定優(yōu)于配置。
JSF 正開始憑借其 Java Web 標(biāo)準(zhǔn)的地位主導(dǎo) Java Web 應(yīng)用程序市場。隨著更多的開發(fā)人員受托使用 JSF 作為基礎(chǔ)來架構(gòu)應(yīng)用程序,他們發(fā)現(xiàn) JSF 的核心規(guī)范中清楚地說明: JSF 不是為成為一個完整的 Web 應(yīng)用程序框架而設(shè)計的。相反,它提供一個健壯的、事件驅(qū)動的 API 和 UI 組件庫,用于構(gòu)建更復(fù)雜的應(yīng)用程序框架。
我在尋找用于彌補 JSF 的組件驅(qū)動架構(gòu)的擴(kuò)展時,發(fā)現(xiàn) Shale 和 Struts 2 都有不足之處。我排除了 Struts 2,因為它將 JSF 看作是面向更大范圍的設(shè)計。而 Shale 似乎更靠近一些,它基本上是基于 JSF,但是 對此我持保留意見。相反,JBoss Seam 是一個全面的應(yīng)用程序框架,它構(gòu)建在 JSF 的基礎(chǔ)上,但是并沒有損害它的核心目標(biāo)。
這個由三部分組成的系列將介紹 Seam 應(yīng)用程序框架,演示它的優(yōu)點,并希望使您相信它與 JSF 是開發(fā) Java 企業(yè)應(yīng)用程序的極好的組合。在閱讀本系列之前,如果您想下載 Seam,那么請閱讀 參考資料 一節(jié)。
![]() |
|
剛剛閱讀到關(guān)于 JBoss Seam 的文章(見 參考資料)的第一頁,我就知道 Seam 正是我要找的項目。Seam 的開發(fā)人員,尤其是 Gavin King,在經(jīng)過足夠多的、實際的開發(fā)之后,知道一個 Web 應(yīng)用程序框架必須從一開始就攻破難題,包括上下文狀態(tài)管理、RESTful 和用戶友好的 URL、Ajax remoting、適當(dāng)?shù)漠惓L幚砗图s定優(yōu)于配置。令 Java 開發(fā)人員欣喜的是,Seam 可以滿足所有這些需求,甚至可以滿足更多需求。如果您正使用 JSF,并且還沒聽說過 Seam,那么我強(qiáng)烈建議您看看 Seam 的參考文檔(見 參考資料)。Seam 附帶的手冊就是最好的資料!
盡管 Seam 顯然非常適合作為 JSF 的補充,但是在激烈的競爭環(huán)境中,它遭到了一定程度的輕視。當(dāng)今市場中充斥著各種各樣的 Web 應(yīng)用程序框架 —— 包括 Shale 和 Struts 2,新來者往往不受重視,Seam 還沒有在主流行列站穩(wěn)腳跟。 Seam 沒有很快流行的另一個原因是關(guān)于這種框架的某些流言使 Java 開發(fā)人員沒能認(rèn)識到它的直接優(yōu)點。
我要粉碎的一個流言是:Seam 只有和 EJB 3 一起使用時才有用,或者說在使用 Seam 開發(fā)應(yīng)用程序時需要一個 EJB3 容器。實際上,Seam 的文檔清楚地駁斥了這種誤解:"Seam 并不要求組件是 EJB,甚至在沒有兼容 EJB 3.0 的容器時也能使用。" 如果說只有在使用 EJB 3 的同時才能使用 Seam,那么無異于說只有在使用 Hibernate 的同時才能使用 Spring。雖然這兩對都有很強(qiáng)的互補性,但是每一對的兩者之間都不是相互依賴的。
![]() |
|
正如我將要解釋的那樣,Seam 通過一些有價值的 hook 和組件管理進(jìn)程 擴(kuò)展默認(rèn) JSF 生命周期。還可以完全獨立于 EJB3 使用 Seam。但是要記住,和 EJB3 一樣,Seam 依賴于 JDK 5 注釋元數(shù)據(jù)進(jìn)行組件聲明,因此使用 Seam 時,還需要同時使用兼容 Java 5 的 JVM。圖 1 顯示了一個 Seam POJO 實現(xiàn)的應(yīng)用程序堆棧:
圖 1. 一個 Seam POJO 應(yīng)用程序堆棧

實際上,即使完全不引用 EJB 3 jar 或描述符文件,也可以使用 Seam 的很多功能。當(dāng)和 POJO 一起使用 Seam 時,該框架保留對組件實例化的完全控制,并且不要求任何專門的配置。Seam 負(fù)責(zé)大多數(shù) Java 5 注釋處理,而不需要依賴于 EJB 3 中的任何機(jī)制。的確 依賴于 EJB3 容器的一組有限的注釋則是專用于那個環(huán)境的。在某些情況下,將 Seam 集成到一個沒有 EJB 3 耦合的 IT 投資中可以獲得更好的成本效益。如何使用 Seam 視個人偏好而定。
![]() ![]() |
![]()
|
如今有那么多種 Java 框架,每天只有有限的那么多小時,顯然,如果 Seam 難于集成的話,它就無立足之地。幸運的是,將 Seam 添加到項目中很簡單。因為 JSF 生命周期仍然是 Seam 應(yīng)用程序的中心部分,所以不需要經(jīng)歷一個再訓(xùn)練時期。只需添加 4 個 jar 文件,注冊一個 servlet 監(jiān)聽器和一個 JSF phase 監(jiān)聽器,最后再加上一個空白的 Java 屬性文件。完成這些設(shè)置后,就可以一次性地將本地 JSF 應(yīng)用程序轉(zhuǎn)移到 Seam 管理的 bean 上。
要開始使用 Seam,首先需要將所需的 jar 文件添加到項目中。如果您當(dāng)前不是使用 Hibernate,或者還沒有升級到最新的版本,那么在設(shè)置時需要執(zhí)行一個額外的步驟。這里需要包含來自 Hibernate 3.2 distribution 的 jar,以及它的眾多的依賴項。Seam 還使用 Hibernate 注釋用于數(shù)據(jù)驗證,所以除了主 Hibernate jar 之外,還必須包括那個擴(kuò)展 jar。需要的 Seam 發(fā)行版中的庫有 jboss-seam.jar 和 jboss-seam-ui.jar,以及兩個支持庫:Javassist(用于 Java 的加載時反射系統(tǒng))和 Java Persistence API。圖 2 中的項目樹說明了一個 Seam 項目中的 jar 集合。該圖中顯示的大多數(shù)附加庫支持 JSF 的 MyFaces 實現(xiàn)。
圖 2. Seam 項目中的 jar 庫

接下來的步驟是在 web.xml 文件中安裝 servlet 監(jiān)聽器類。該監(jiān)聽器在部署應(yīng)用程序時初始化 Seam。
清單 1. Seam servlet 監(jiān)聽器配置
<listener> <listener-class>org.jboss.seam.servlet.SeamListener</listener-class> </listener> |
接下來,將 JSF phase 監(jiān)聽器添加到 faces-config.xml 文件中,如清單 2 所示。該監(jiān)聽器將 Seam 集成到標(biāo)準(zhǔn) JSF 生命周期中。(圖 3 大致描繪了集成到這個生命周期中的 Seam 增強(qiáng)。)
清單 2. Seam phase 監(jiān)聽器配置
<lifecycle> <phase-listener>org.jboss.seam.jsf.SeamPhaseListener</phase-listener> </lifecycle> |
最后,將一個空的 seam.properties 文件放在類路徑的根下,以便指示 Seam 進(jìn)行加載,如清單 3 所示。這個空白文件被用作一個 JVM 類加載器優(yōu)化,使 Seam 在類路徑下更小的區(qū)域內(nèi)搜索組件,從而大大減少加載時間。
清單 3. Seam 屬性文件
# The mere presence of this file triggers Seam to load # It can also be used to tune parameters on configurable Seam components |
當(dāng)然,在這種最小設(shè)置中,Seam 的很多特性是不可用的。以上說明只是為了演示 Seam 很少涉足入門級使用。例如,Seam 包括一個 servlet 過濾器,該過濾器擴(kuò)展 JSF 生命周期以外的 Seam 特性。 servlet 過濾器的用法包括與非 JSF 請求集成,通過重定向傳播 conversation,以及管理文件上傳。請參閱 參考資料,看看 Seam 參考文檔,其中討論了用于控制附加功能的配置文件 —— 特別是 EJB3 集成。
![]() ![]() |
![]()
|
與典型的 JSF 配置過程相比,使用 Seam 開發(fā)受管 bean 非常容易。為了將 bean 暴露到 JSF 生命周期中,只需在類定義的上面添加一個簡單的注釋 @Name
。然后,Seam 會負(fù)責(zé)控制組件的可見性和生命周期。最妙的是,不需要在 faces-config.xml 文件中定義這個 bean。
清單 4 顯示了 @Name
注釋以及 @DataModel
、@DataModelSelection
、@In
、@Out
和 @Factory
。這些注釋使變量能夠在視圖模板和 Seam 組件之間雙向流動。
在 Seam 用語中,這個動作被稱作雙射(bijection,即 bidirectional injection 的簡稱)。當(dāng)注出(outject)屬性數(shù)據(jù)時,視圖可以通過名稱找到它。在 postback 或者組件初始化時,數(shù)據(jù)被注入(inject)到一個組件中。后者是著名的控制反轉(zhuǎn)(inversion of control,IOC)模式的一種實現(xiàn),可用于連接委托對象。傳統(tǒng) IOC 與 Seam 的雙射之間的主要不同點在于,雙射使長期作用域中的組件可以引用短期作用域中的組件。可以進(jìn)行這種連接是因為 Seam 在調(diào)用組件時(而不是啟動容器時)解析依賴項。雙射是有狀態(tài)組件開發(fā)的基礎(chǔ)。
顯然,清單 4 中的 POJO bean 只是簡單地演示了 Seam 的用法。隨著本系列討論的繼續(xù),我將探索另外的方法來實現(xiàn) Seam。
清單 4. 一個典型的 Seam POJO bean
@Name("addressManager") public class AddressManagerBean { @DataModel private List<Address> addresses; @DataModelSelection @Out( required = false ) private Address selectedAddress; @Factory( value = "addresses" ) public void loadAddress() { // logic to load addresses into this.addresses } public String showDetail() { // no work needs to be done to prepare the selected address return "/address.jspx"; } public String list() { return "/addresses.jspx"; } } |
為了使用由一個已有的 Spring 容器管理的服務(wù)層對象中的投資,需要將所有處理相關(guān)業(yè)務(wù)邏輯的 Spring bean 注入到 Seam 組件中。首先需要確保已經(jīng)配置了 Spring-JSF 集成,它由 Spring 框架附帶的一個定制變量解析器進(jìn)行處理(見 參考資料)。有了這座橋梁,Spring 與 Seam 的集成就很簡單,只需使用 @In
Java 5 注釋和一個值綁定表達(dá)式,以表明 Seam 組件的哪些屬性應(yīng)該接收一個 Spring bean 的注入,如清單 5 所示。(將來版本的 Seam 將包括用于 Spring 的一個定制的名稱空間,以滿足值綁定表達(dá)式的需要。)
清單 5. 注入一個 Spring bean
@Name("addressManager") public class AddressManagerBean { @In("#{addressService}") private AddressService addressService; } |
這個例子設(shè)置支持使用以輕量級容器(這里就是 Spring)配置的無狀態(tài)服務(wù)和數(shù)據(jù)訪問(DAO)層。因為不需要 EJB3,所以部署的目標(biāo)可以是任何基本的 servlet 容器。
現(xiàn)在,您對 Seam-JSF 實現(xiàn)有了一個初步的印象,接下來我將更深入地探討我在使用 JSF 時遇到的挑戰(zhàn),以及 Seam 如何緩解這些挑戰(zhàn)。
為了充分理解 Seam 為 JSF 帶來了什么,就需要理解 JSF 與其他流行的基于 Web 的編程方法有何不同。JSF 是實現(xiàn)傳統(tǒng)的 Model-View-Controller (MVC) 架構(gòu)的一種 Web 框架。不同之處在于,它采用該模式的一種特別豐富的實現(xiàn)。與 Model 2 或者 Struts、WebWork 和 Spring MVC 之類的框架中使用的 “push-MVC” 方法相比,JSF 中的 MVC 實現(xiàn)更接近于傳統(tǒng)的 GUI 應(yīng)用程序。前面那些框架被歸類為基于動作的(action-based),而 JSF 則屬于基于組件模型 的新的框架家族中的一員。
如果將基于動作的框架想象為使用 “push” 模型,而將組件框架想象為使用 “pull” 模型,那么這種區(qū)別就很容易理解了。組件框架中的控制器不是預(yù)先處理頁面請求(在基于動作的框架中控制器就是這么做的),而是在請求生命周期中作出讓步,在視圖中調(diào)用數(shù)據(jù)提供方法。此外,頁面上的元素,即組件被綁定到事件,這些事件可以觸發(fā)服務(wù)器端對象(激活后)的方法調(diào)用,從而導(dǎo)致重新顯示相同的視圖,或者轉(zhuǎn)換到另一個頁面。因此,組件框架也被歸類為事件驅(qū)動的。組件框架抽象出用于事件通信的底層請求-響應(yīng)協(xié)議。
事件驅(qū)動方法的優(yōu)點是可以減少單個方法在呈現(xiàn)視圖時需要預(yù)先做的工作。在組件框架中,UI 事件或解析的值綁定表達(dá)式直接導(dǎo)致方法調(diào)用。
一個應(yīng)用程序即使只達(dá)到中度成熟,它通常也需要在任何給定頁面上使用很多不相關(guān)的活動。如果將對所有這些信息的管理全部放入一個動作或者一個動作鏈中,那么勢必給維護(hù)帶來極大的困擾。因此,開發(fā)人員常常發(fā)現(xiàn)他們的代碼偏離了面向?qū)ο竽P偷能壍溃炊萑肓诉^程編程模型的泥潭。相反,組件框架將這種工作隔離出來,更自然地加強(qiáng)了對象的角色和責(zé)任。
![]() ![]() |
![]()
|
對于 JSF 和組件框架的基礎(chǔ)已經(jīng)介紹得差不多了。實際上 —— 很多 Java 開發(fā)人員最近發(fā)現(xiàn) —— 轉(zhuǎn)移到 JSF 并非總是一帆風(fēng)順。采用組件模型會帶來一些全新的問題,首要的一個問題是您通常需要試著使應(yīng)用程序符合基于動作的 Web。很多時候,JSF 需要具有像基于動作的框架那樣的行為,但是在標(biāo)準(zhǔn) JSF 中這是不可行的,至少不為每個請求使用 phase 監(jiān)聽器就不行。
JSF 的其他主要缺點還包括對 HTTP 會話的依賴過重(尤其是在一序列的頁面之間傳播數(shù)據(jù)時),簡陋的異常處理,缺少書簽支持,以及太多的 XML 配置。通過與 JSF 自然地集成,同時加入 JSF 規(guī)范委員會放棄的或者忽略掉的新功能,Seam 解決了很多這樣的問題。Seam 的框架鼓勵使用緊湊的、易讀的、可重用的代碼,并且避免了所有為解決上述問題而常常加入的 “粘連(glue)” 邏輯。圖 3 涵蓋了 JSF 生命周期中用于簡化應(yīng)用程序代碼的大多數(shù) Seam 擴(kuò)展點:
圖 3. Seam 生命周期增強(qiáng)

讓我們來考慮其中一些增強(qiáng),因為它們適用于 JSF 開發(fā)中一些常見的挑戰(zhàn)。
Seam 演示了 Java 5 注釋的一個非常實用的用法。Seam 的部署掃描程序檢查所有包含 seam.properties 文件的歸檔文件,并為所有標(biāo)有 @Name
注釋的類創(chuàng)建一個 Seam 組件。由于 Java 語言缺乏用于在代碼級添加元數(shù)據(jù)的一種公共語法,因此需要設(shè)計很多 XML 配置。當(dāng) Java 5 規(guī)范中加入注釋后,就獲得了一個更好的解決方案。由于大多數(shù) backing bean 是為了在特定應(yīng)用程序中使用而開發(fā)的,因此沒有理由將這些 bean 的配置 “抽象” 到類本身以外的任何文件中。附帶的好處是,您可以少處理一個文件。Seam 提供了一組完整的注釋來幫助將 bean 集成到 JSF 生命周期中。清單 4 顯示了其中一些。
在不使用組件框架的情況下,另一個必須解決的熟悉的問題是預(yù)先處理每個請求,就像在基于動作的框架中那樣。受此影響的用例是 RESTful URL、書簽支持、通過 URL 模式獲得的安全性以及頁面流驗證等。這也是學(xué)習(xí)使用 JSF 的開發(fā)人員容易感到困惑的主要原因之一。有些 JSF 供應(yīng)商通過用開發(fā)人員工具提供 onPageLoad 功能來繞過這個問題(見 參考資料),但這不是核心規(guī)范的一部分。
當(dāng)用戶直接從書簽(比如)請求一個商品詳細(xì)信息屏幕時,通常會發(fā)生什么事情呢?由于 JSF 控制器采取被動方式,當(dāng)頁面開始呈現(xiàn)時,即使明顯沒有目標(biāo)數(shù)據(jù),也不能將用戶重新帶到邏輯流的開始處。相反,這種情況下只能顯示一個空頁面,其中只有一些空值或其他可能存在的假信號。
首先,您可能會本能地想要在頁面的主 backing bean 上實現(xiàn)一個 “prerender” 方法。然而,在組件框架中,backing bean 與頁面之間的關(guān)系并不一定都是一對一的。每個頁面可能依賴于多個 backing bean,每個那樣的 bean 也可能在多個不同的頁面上使用。必須用某種方式將一個視圖 ID(例如 /user/detail.jspx)與一個或多個方法關(guān)聯(lián)起來,當(dāng)選擇呈現(xiàn)相應(yīng)的視圖模板時就調(diào)用這個(些)方法。您可以使用 phase-listener 方法,但是這仍然需要定制的邏輯來確定對于當(dāng)前視圖和階段是否應(yīng)該執(zhí)行該功能。這種解決方案不但會導(dǎo)致很多冗余邏輯,而且會將視圖 ID(很可能是應(yīng)用程序中最不確定的部分)硬編碼到編譯后的 Java 代碼中。
Seam 的頁面動作可以幫助您預(yù)先攔截呈現(xiàn)的假信號。頁面動作是使用方法綁定指定的,方法綁定在進(jìn)入頁面時、Render Response 階段之前執(zhí)行。對于 /WEB-INF/pages.xml 配置文件中一個給定的視圖 ID,可以配置任意數(shù)量的方法綁定。(或者,可以通過將它們放在視圖模板鄰近的一個文件中,復(fù)制它的名稱,但是將文件擴(kuò)展名換為 *.page.xml,從而分解每個頁面的定義)。對于頁面動作,XML 是有必要的,因為視圖 ID 非常容易變化。就像 JSF 通過 Apply Request Values 階段的值綁定將 post 數(shù)據(jù)映射到模型對象一樣, Seam 可以通過執(zhí)行頁面動作之前的值綁定將任意請求參數(shù)映射到模型對象。這些請求參數(shù)注入的配置嵌套在頁面動作 XML 聲明中。如果頁面動作方法調(diào)用返回一個非空字符串值,則 Seam 將其當(dāng)作一個導(dǎo)航事件。因此,不必遷移到一個完整的基于動作的框架中,仍然可以比得上最特別的特性。Seam 包括很多內(nèi)置的頁面動作,它們通常跨應(yīng)用程序使用。其中包括用于驗證 conversation 是否建立的一個動作;可以啟動、嵌套和結(jié)束 conversation 的動作;處理預(yù)期異常的動作;以及確保適當(dāng)?shù)膽{證的動作。
頁面動作是啟用對 JSF 的書簽支持的關(guān)鍵。Seam 的創(chuàng)立者允許在進(jìn)入頁面時請求參數(shù) actionMethod
觸發(fā)一個方法調(diào)用,從而利用了這一特性。更妙的是,您不需要做任何額外的工作就能為書簽創(chuàng)建鏈接。 Seam 提供了兩個組件標(biāo)記:s:link
和 s:button
,用以處理細(xì)節(jié)。這兩個標(biāo)記分別對應(yīng)于 JSF 中的 h:commandLink
和 h:commandButton
。不同之處在于,Seam 組件標(biāo)記組裝的鏈接使用一個 HTTP GET
操作發(fā)出請求,而不是使用 JSF 的 HTTP POST
表單提交模型表示。因此,Seam 創(chuàng)建的鏈接對書簽更 “友好”,對于開發(fā)人員來說更方便。
您可能還注意到,當(dāng)使用頁面動作時,地址欄中的 URL 對應(yīng)于正在顯示的頁面,而不總是背后的一個頁面。(后一種情況之所以會發(fā)生,是因為 JSF 將表單配置為 post 回生成它們的 URL。地址欄沒有更新,以反映執(zhí)行動作后的新視圖,因為 JSF 通過一個服務(wù)器端重定向使之前進(jìn)。)如果您想演示頁面動作的靈活性,那么可以使用它們來創(chuàng)建 RESTful URL(例如 /faces/product/show/10)。為此,將頁面動作方法映射到視圖 ID“/product/show/*”,其中 /faces 前綴是 JSF servlet 映射部分。然后,該頁面動作方法利用請求 URL,以判斷數(shù)據(jù)類型和數(shù)據(jù)標(biāo)識符,加載數(shù)據(jù),然后導(dǎo)航到適當(dāng)?shù)哪0濉_@個例子很明顯地演示了 JSF 請求 URL 與視圖模板之間并不一定存在一對一的關(guān)系。
JSF 最大的一個失敗是沒有在用戶觸發(fā)的動作或動作監(jiān)聽器方法以外的其他地方提供可靠的機(jī)會來為視圖準(zhǔn)備數(shù)據(jù)。將邏輯放在一個動作方法中并不能保證該邏輯在視圖呈現(xiàn)之前得到執(zhí)行,因為頁面視圖并不總是在用戶觸發(fā)的事件之后。
例如,當(dāng)一個 JSF 應(yīng)用程序的 URL 第一次被請求時,會發(fā)生什么情況?如果需要在該頁面上顯示從服務(wù)層獲得的一組數(shù)據(jù),那么在 JSF 生命周期中始終沒有好的機(jī)會來取數(shù)據(jù)。您可能會認(rèn)為,可以將邏輯放在映射到視圖中值綁定表達(dá)式的 backing bean 的 getter 方法中。但是,每當(dāng) JSF 解析那個表達(dá)式時,就會觸發(fā)另一個調(diào)用,新的調(diào)用又會訪問服務(wù)層。即使頁面上只有少數(shù)幾個組件,getter 方法也可能被推后數(shù)次執(zhí)行。顯然,就性能而言這不是最優(yōu)的。即使通過使用受管 bean 上的私有屬性維護(hù)狀態(tài),每當(dāng)面對那樣的情況時,仍然必須增加額外的管道。一種解決方案是使用 Seam 的頁面動作。但是由于這種任務(wù)是如此常見,Seam 提供了一個更加容易的解決方案。
Seam 引入了工廠數(shù)據(jù)提供者(factory data provider)的概念,工廠數(shù)據(jù)提供者由 @Factory
Java 5 注釋指定。雖然有兩種方法配置工廠,但是最終結(jié)果是同樣的數(shù)據(jù)只需在第一次被請求時準(zhǔn)備一次。 Seam 確保隨后對相同數(shù)據(jù)的請求將直接返回之前創(chuàng)建的結(jié)果集,而不必再一次觸發(fā)對查找方法的調(diào)用。通過與 conversation 相結(jié)合,工廠數(shù)據(jù)提供者成為實現(xiàn)數(shù)據(jù)短期緩存的非常強(qiáng)大的特性,否則,取這些數(shù)據(jù)的代價可能較高。在 JSF 不負(fù)責(zé)減少它解析一個值綁定表達(dá)式的次數(shù)的情況下,Seam 的工廠特性常常變得非常方便。
關(guān)于 JSF 很容易引起困惑的一個地方是它的狀態(tài)管理功能。JSF 規(guī)范解釋了在接收一個動作之后頁面是如何 “恢復(fù)(restored)” 的,在此期間時間事件要進(jìn)行排隊,選擇要注冊。仔細(xì)研究規(guī)范中的用詞可以發(fā)現(xiàn),雖然在 postback 上恢復(fù)了組件樹,但是那些組件使用的 backing bean 數(shù)據(jù)并沒有被恢復(fù)。組件只是以字符串文字的形式存儲值綁定(使用 #{value}
語法的 EL 表達(dá)式),只在運行時解析底層數(shù)據(jù)。這意味著如果一個值是短期作用域存儲的,例如頁面或請求作用域,那么當(dāng) JSF 生命周期到達(dá) Restore View 階段時,這個值將消失。
不將值綁定數(shù)據(jù)存儲在組件樹中的一個最值得注意的不利方面是虛幻事件效果(見 參考資料),這是由 UIData
家族中的臨時父組件導(dǎo)致的。如果一個值綁定表達(dá)式引用的模型數(shù)據(jù)不再可用,或者在組件樹被恢復(fù)之前發(fā)生了更改,那么組件樹的一些部分將被撤銷。如果在這些被撤銷的分支中,有一個組件中觸發(fā)了一個事件,那么它將不能被發(fā)現(xiàn),而且這種事件丟失情況是難于覺察的。(只是隊列開發(fā)人員可能會驚呼 “為什么我的動作沒有被調(diào)用?”)
雖然丟失的事件看上去像是異常狀況,但并不會導(dǎo)致 JSF 生命周期中出現(xiàn)紅色標(biāo)志。因為這些組件依賴底層數(shù)據(jù),以保持穩(wěn)定和適當(dāng)?shù)乇换謴?fù),所以 JSF 難于知道丟失了什么。
不幸的是,JSF 規(guī)范天真地引導(dǎo)開發(fā)人員將大多數(shù) backing bean 放入 conversation 作用域中 —— 甚至可以在 “方便的” 作用域內(nèi)調(diào)用它。然后,服務(wù)器管理員則必須處理由此導(dǎo)致的 “內(nèi)存溢出” 錯誤,集群環(huán)境中的服務(wù)器相似性,以及服務(wù)器重啟時的串行化異常。MyFaces Tomahawk 項目通過 t:saveState
標(biāo)記的形式提供了對虛幻事件的一個解決方案。MyFaces 標(biāo)記允許將數(shù)據(jù)(包括整個 backing bean)存儲為組件樹的一部分,而僅僅是值綁定。然而,這種解決方案有些簡陋,很像使用隱藏的表單字段在請求之間傳遞值。它還造成視圖與控制器之間緊密耦合。Seam 的創(chuàng)立者意識到,Java Servlet 規(guī)范中三個內(nèi)置的上下文(請求、會話和應(yīng)用程序)不足以構(gòu)成數(shù)據(jù)驅(qū)動的 Web 應(yīng)用程序的全部作用域。在 Seam 中,他們引入了 conversation 作用域,這是一個有狀態(tài)作用域,由頁面流的起止點界定。
“Seam 強(qiáng)調(diào)使用有狀態(tài)組件。” Seam 參考文檔中的這句話體現(xiàn)了 Seam 的核心思想。很長一段時間內(nèi),關(guān)于 Web 應(yīng)用程序的看法是,它們是無狀態(tài)的 —— 這種思想一定程度上要歸因于 HTTP 協(xié)議的無狀態(tài)性質(zhì)。大多數(shù)框架為了迎合這一點,在結(jié)束頁面呈現(xiàn)之前提供 one-shot-processing。這種方法導(dǎo)致很大的阻力,因為任何大的應(yīng)用程序都需要長時間運行的 conversation 來滿足某些用例。需要有狀態(tài)上下文的應(yīng)用程序的例子有很多,例如存儲檢查過程、產(chǎn)品定制、多頁表單向?qū)Ш秃芏嗥渌诰€形交互的應(yīng)用程序。雖然其中有些例子可以通過使用 URL 參數(shù)(aka RESTful URL)和隱藏字段在頁面之間遷移數(shù)據(jù),但是這樣做對于開發(fā)人員來說有些繁雜。而且,如今這種做法已經(jīng)過時了。因為大多數(shù) Web 框架仍然在無狀態(tài)模型下操作,所以您常常發(fā)現(xiàn)自己走出了這種框架,而 “開辟” 出定制解決方案。
JSF 大量依賴于 HTTP 會話,試圖引入有狀態(tài)上下文。實際上,當(dāng)和會話作用域的 backing bean 一起使用時,JSF 組件的行為要好得多。如果不小心設(shè)計,過度使用 HTTP 會話會導(dǎo)致嚴(yán)重的內(nèi)存泄漏、性能瓶頸和安全問題。此外,在多標(biāo)簽瀏覽器環(huán)境中,使用 HTTP 會話可能導(dǎo)致非常奇怪的行為,破壞用戶神圣的 Back 按鈕。值得注意的是,JSF 只是與您互作讓步:它是一個有狀態(tài) UI,處理保存和恢復(fù)組件樹的所有細(xì)節(jié),但是它在保存和恢復(fù)數(shù)據(jù)方面沒有提供幫助。因此,JSF 帶來有狀態(tài) UI,而您則帶來有狀態(tài)數(shù)據(jù)。不幸的是,需要由您來負(fù)責(zé)確保它們是相符的。
在 Seam 之前,使用有狀態(tài)數(shù)據(jù)的惟一方便的方式是依賴于 HTTP 會話。Seam 糾正了這個問題,它通過建立一個全新的 conversation 作用域,完善了 JSF 的狀態(tài)管理。隨著將 Seam 添加到 JSF 生命周期中,conversation 上下文與一個瀏覽器窗口(或標(biāo)簽頁)聯(lián)系在一起,這個瀏覽器窗口(或標(biāo)簽頁)由隨每個請求提交的一個標(biāo)志來標(biāo)識。conversation 作用域使用 HTTP 會話的一個單獨的區(qū)段在頁面之間遷移數(shù)據(jù)。記住,Seam 使用 HTTP 會話用于 conversation 持久性這一點是完全透明的。 Seam 并不是不負(fù)責(zé)任地將組件推卸到 HTTP 會話中,使其茫然地呆在那里。相反,Seam 小心地管理那個區(qū)段的會話數(shù)據(jù)的生命周期,并且當(dāng) conversation 終止時,自動清除它們。Seam 聰明地使用雙射來允許以一種新的說明性方式使數(shù)據(jù)流入和流出一個 “Web conversation” 的每個頁面。 Seam 的 conversation 作用域同時還克服了 HTTP 會話的限制,幫助開發(fā)人員放棄使用 HTTP 會話。
Seam 的創(chuàng)立者曾說過:“在異常處理方面,JSF 非常有限”。這一點顯然毫無爭議。 JSF 規(guī)范完全忽視異常管理,將責(zé)任完全放在 servlet 容器上。允許 servlet 容器處理異常的問題在于,這嚴(yán)重限制了錯誤頁面上顯示的內(nèi)容,并且禁止了事務(wù)回滾。由于錯誤頁面是在請求分發(fā)器轉(zhuǎn)發(fā)之后顯示的,FacesContext
不再在作用域中,因此這時執(zhí)行業(yè)務(wù)邏輯就太遲了。您的最后一線希望是使用 Servlet API,并抓住 javax.servlet.error.*
請求屬性,以搜索能表明出錯處的信息。
這一點上,Seam 再次扮演救世主,它提供了優(yōu)雅的、說明性方式的異常處理。異常管理可以通過注釋指定,或者在配置文件中指定。可以將注釋 @HttpError
、@Redirect
和 @ApplicationException
放在異常類的上面,表明當(dāng)異常被拋出時應(yīng)該采取的動作。對于那些不能修改的異常類,可以使用 XML 配置選項。Seam 中的異常管理器負(fù)責(zé)發(fā)送 HTTP 狀態(tài)碼、執(zhí)行重定向、促使頁面呈現(xiàn)、終止 conversation 和定制出現(xiàn)異常時的用戶消息。由于在開始呈現(xiàn)響應(yīng)之后,JSF 不能改變動作的過程,一些固有的限制規(guī)定了何時才能處理這些異常。通過適當(dāng)使用其他 Seam 特性,例如頁面動作,可以確保大多數(shù)異常情況在呈現(xiàn)響應(yīng)之前得到解決。
由于最后發(fā)行的 JSF 規(guī)范幾乎與 Ajax 重合,JSF 框架在異步 JavaScript 和局部頁面呈現(xiàn)(partial page rendering)方面幫助不大。在某些時候,甚至這兩種類型的編程甚至不一致。最終的解決辦法是建議使用 JSF PhaseListener
或組件 Renderer
來處理局部頁面更新。即使如此,這一點仍然很明顯:JSF 使得 Ajax 比過去更難于采用。有些項目,例如 ICEfaces,甚至用一個更好的、專為頁面-服務(wù)器通信設(shè)計的技術(shù)(即 direct-to-DOM rendering)來替代 JSF 生命周期。
Seam 為 JavaScript remoting(常常記在 Ajax 名下的一種技術(shù))提供了一種獨特的方式,該方式與 Direct Web Remoting (DWR) 庫的方式大致相似。Seam 通過允許 JavaScript 直接調(diào)用服務(wù)器組件上的方法,將客戶機(jī)與服務(wù)器連在一起。Seam remoting 比 DWR 更強(qiáng)大,因為它可以訪問豐富的上下文組件模型,而不僅僅是一個孤立的端點。這種交互構(gòu)建在 JSF 的事件驅(qū)動設(shè)計的基礎(chǔ)上,所以它可以更嚴(yán)格地遵從 Swing 范例。最妙的是,提供這個功能的同時并沒有增加開發(fā)方面的負(fù)擔(dān)。只需在組件的方法上加一個簡單的注釋 @WebRemote
,JavaScript 就可以訪問該方法。當(dāng)服務(wù)器組件的狀態(tài)被修改之后,Ajax4JSF 組件庫就可以處理局部呈現(xiàn)。簡言之:Seam remoting 庫使 JSF 可以實現(xiàn)它的創(chuàng)立者一向期望的交互設(shè)計。