Seam無縫整合 JSF: 藉助 Seam 進行對話

梧桐雨—168發表於2008-04-19
藉助 Seam 開發有狀態的 CRUD 應用程式是件輕而易舉的事情。在 無縫 JSF 系列文章的第二篇中,Dan Allen 向您展示如何使用 Java™Server Faces (JSF) 和 Seam 為基於 Web 的高爾夫課程目錄開發建立、讀取、更新和刪除用例。在此過程中,他突出強調了 Seam 對 JSF 生命週期的兩項增強功能 —— 也就是 conversation 作用域和通過自定義 Java 5 註釋進行配置 —— 並解釋了其能夠降低伺服器負載和縮減開發時間的原因。

在這個分為三部分的系列文章的第一篇中介紹了 Seam,它是既能顯著增強 JSF 功能又能實現基於元件的架構的應用程式框架。在這篇文章中,我解釋了 Seam 和其他經常與 JSF 結合使用的 Web 框架的不同之處,展示了向現有 JSF 應用程式新增 Seam 是多麼輕鬆,最後概述了 Seam 對 JSF 應用程式生命週期的增強,同時還涉及到有狀態的對話、工廠元件以及使用註釋進行隱祕配置。

儘管這篇文章可能引發了您對 Seam 的興趣,但是您可能無法確信它能夠改善 JSF 開發體驗。整合一組新工具通常比閱讀它複雜得多,並且有時候並不值得。在無縫 JSF 系列文章的第二篇文章中,您將親自發現 Seam 是否能夠實現其簡化 JSF 開發的承諾。在使用 Seam 構建執行標準 CRUD 操作的簡單應用程式之後,我敢肯定您會認為 Seam 是對 JSF 框架的必要擴充套件。結果,Seam 還能幫助降低資料庫層有限資源的負載。

關於本系列
無縫 JSF 講述了 Seam 是真正適合 JSF 的第一個應用程式框架,能夠修正其他擴充套件框架無法修正的主要弱點。閱讀該系列的文章,然後自己判斷 Seam 是不是對 JSF 的適當補充。

Open 18 應用程式

Open 18 是基於 Web 的應用程式,允許使用者管理一列曾經體驗過的高爾夫課程,並記錄每個場次的分數。為了體現本討論的目的,該應用程式的範圍僅限於管理高爾夫課程目錄。第一個螢幕展現了已經輸入的課程列表,並列出各個課程的一些相關欄位,如課程名稱、地點和專賣店的電話號碼。使用者可以從該處檢視完整的課程詳細內容、新增新課程、編輯現有課程,最終還可以刪除課程。

在講述如何使用 Seam 為 Open 18 應用程式開發用例時,我重點講述它如何簡化程式碼,自動管理一系列請求期間的狀態,並對輸入資料執行資料模型驗證。

該系列文章的目標之一是證明 Seam 可以整合到現有的任何 JSF 應用程式,並且不需要轉換到 Enterprise JavaBeans (EJB) 3。因此,Open 18 應用程式並不依靠 Seam 的 JPA EntityManager 整合進行事務型資料庫訪問,也不依靠 EBJ3 有狀態會話 bean 進行狀態管理。(Seam 附帶的示例 大多都使用了這兩項技術。)Open 18 設計為使用無狀態的分層架構。服務層和資料訪問 (DAO) 層使用 Spring 框架繫結到一起。我相信由於 Spring 在 Web 應用程式領域的普遍性,該設計是切實可行的選擇。該應用程式展示瞭如何通過使用 conversation 作用域將有狀態的行為引入到 JSF 託管的 bean。記住這些 bean 是簡單的 POJO。

您可以 下載 Open 18 原始檔 以及 Maven 2,以編譯並執行樣例程式碼。為了使您快速入門,我已經將該應用程式配置為使用 Seam 和 Spring-JSF 整合。如果想要在自己的專案中設定 Seam,可以在 本系列第一篇文章 中找到完整的操作指導。請參見 參考資料 瞭解關於整合 JSF 和 Spring 的更多資訊。

兩個容器的故事

構建利用 Spring 框架的 JSF 應用程式的第一個步驟是配置 JSF,使其可以訪問 Spring 容器中的 bean。spring-web 包是 Spring 釋出的一部分,附帶有自定義 JSF 變數解析器,可構建此橋樑。首先,Spring 解析器委託給 JSF 實現附帶的本地解析器。本地解析器嘗試將值繫結引用(如 #{courseManager})與 JSF 容器中的託管 bean 相匹配。該 bean 名稱是由 #{} 表示式分隔符之間的字元組成的,在這個例子中為 courseManager。如果該查詢未能發現匹配,自定義解析器就會檢查 Spring 的 WebApplicationContext,以查詢帶有匹配 id 屬性的 Spring bean。請記住 Seam 是 JSF 框架的擴充套件,因此 JSF 可以訪問的任何變數也可以被 Seam 訪問。

Spring 變數解析器是使用變數解析器節點在 faces-config.xml 檔案中配置的,如清單1所示:


清單 1. 配置 spring 變數解析器
                

  org.springframework.web.jsf.DelegatingVariableResolver






回頁首


Seam 的上下文元件

為了體現本文的目的,我假設基於 Spring 的服務層是不證自明的。除了 JSF-Spring 整合層之外 —— 該層負責向 JSF 公開 Spring bean (因此也向 Seam 公開該 bean),並沒有深入地使用 Spring。服務層物件將作為無狀態的介面對待,CRUD 操作可以委託給該介面。解決這些應用程式細節之後,就可以自由地重點研究 Seam 如何將託管 bean 轉換成有狀態的元件,這些元件明確其在促進使用者與應用程式互動方面的角色。

通過建立名為 courseAction 的支援 bean 來支援管理高爾夫課程目錄的檢視,就開始開發 Open 18 應用程式。該託管 bean 公開一個高爾夫課程物件集合,然後對管理這些例項的操作做出響應。這些資料的持久化委託給基於 Spring 的服務層。

在典型的 JSF 應用程式中,使用託管 bean 工具來註冊 CourseAction bean,然後藉助其委託物件(或 “依賴項”)注入該 bean。為此,必須開啟 faces-config.xml 檔案,然後使用該 bean 的名稱和類新增新的 managed-bean 節點,如清單 2 所示。通過使用值繫結表示式新增引用其他託管 bean 的子 managed-property 節點,指定要向該類的屬性中注入的依賴項。在這個例子中,惟一的依賴項是無狀態的服務物件 courseManager,它是使用來自 Appfuse 專案的 GenericManager 類實現的(請參見 參考資料)。


清單 2. 作為 JDF 託管 bean 定義的 CourseAction
                

  courseAction
  com.ibm.dw.open18.CourseAction
  
    courseManager
    #{courseManager}
  


註釋簡化了 XML!

現在您想起了使用本地 JSF 方法定義託管 bean 有多麻煩,請忘記曾經看到 managed-bean XML 宣告 —— 因為您不再需要它了!在 Seam 構建的應用程式中,bean 僅僅是使用 Java 5 註釋宣告的。Seam 將這些 bean 稱為上下文元件。儘管您可能覺得該術語很深奧,但是它只是描述一個元件(或命名例項)與給定的作用域(或稱為上下文)有關。

Seam 在為上下文元件分配的作用域的生命期內對該元件進行管理。 Seam 元件更像 Spring bean,而不是 JSF 託管 bean,這是因為它們插入到複雜的、面向方面的框架。在功能方面,Seam 框架遠勝於 JSF 的基本控制反轉 (IOC) 容器。觀察清單 3 中 courseAction 的宣告。CourseAction 類被重構為利用 Seam 的註釋。


清單 3. 作為 Seam 元件定義的 CourseAction
                
@Name("courseAction")
public class CourseAction {
    @In("#{courseManager}")
    private GenericManager courseManager;
}

與 Spring 的深入整合
自版本 1.2 起,Seam 開始包括 Spring 的自定義 namespace handler,它允許將 Spring bean 公開為 Seam 元件。向 Spring bean 定義新增 標記,允許您以基本格式使用 @In 註釋(來自 清單 3),而不必顯式指定值繫結表示式。在這個例子中,Seam 會將該屬性的名稱與 Seam 元件相匹配,現在搜尋中包括公開為 Seam 元件的 Spring bean。請參見 參考資料 瞭解有關 Seam 1.2 新特性的更多資訊。

注意所有 XML 語句都被去掉了!總之,這就是 Seam 註釋的美妙之處。類的 @Name 註釋指導 Seam 的變數解析器處理名稱與註釋中的值相匹配的變數請求。然後 Seam 例項化這個類的例項,注入 @In 註釋指派的任何依賴項,然後假借該變數名公開該例項。使用清單 3 作為示例,Seam 建立了 CourseAction 類例項,將 courseManager Spring bean 注入courseManager 屬性,然後在收到對變數 courseAction 的請求時返回該例項。額外的好處是,該 bean 的配置接近於程式碼,因此對繼承程式碼庫的新開發人員來說更加透明(甚至對於您這樣只學了 6 個月的人來說也是如此)。

@In 註釋告知 Seam 將繫結表示式 #{courseManager} 的值注入到定義它的屬性。安裝 JSF-Spring 整合之後,該表示式解析成 Spring bean 配置中定義的名為 courseManager 的 bean。





回頁首


準備課程列表

既然已經準備就緒,就可以繼續研究第一個用例。在 Open 18 應用程式的開始螢幕中,向使用者提供了當前儲存在資料庫中的所有課程列表。藉助 h:dataTable 元件標記,清單 4 中的頁面定義相當直觀,並且不允許任何 Seam 特有的元素:


清單 4. 初始課程列表檢視
                

Courses

No courses found. <!-- column definitions go here --&gt

Java 程式碼可能有點難懂。清單 5 展示瞭如何使用本地 JSF 在作用域為該請求的支援 bean 中準備一個課程集合。為了簡便起見,去掉了注入的 Spring bean。


清單 5. 作為 DataModel 公開課程
                
public class CourseAction {
    // ...

    private DataModel coursesModel = new ListDataModel();

    public DataModel getCourses() {
        System.out.println("Retrieving courses...");
        coursesModel.setWrappedData(courseManager.getAll());
        return coursesModel;
    }
    
    public void setCourses(DataModel coursesModel) {
        this.coursesModel = coursesModel;
    }
}

清單 5 中的 Java 程式碼看起來相當直觀,不是嗎?下面研究 JSF 使用支援 bean 時帶來的效能問題。提供實體列表時,您可能使用兩種方法之一。第一種是應用條件邏輯呈現至少包含一項的集合所支援的 h:dataTable ,第二種是顯示一條資訊型訊息,宣告找不到任何實體。要做出決定,可能需要諮詢 #{courseAction.courses},然後再對支援 bean 呼叫相關的 getter 方法。

如果載入截至目前所開發的頁面,然後檢視最終的伺服器日誌輸出,就會看到:

Retrieving courses...
Retrieving courses...
Retrieving courses...

那麼兄弟!如果您將這些程式碼投入生產,最好能找到一個 DBA 找不到的安全隱藏點!這類程式碼執行對於資料庫來說是個負累。更糟的是,回發時情況會惡化,此時可能發生額外的冗餘資料庫呼叫。

讓資料庫休息下!

如果曾經使用 JSF 開發過應用程式,就會了解盲目地在 getter 方法中獲取資料非常不妥。為什麼?因為在典型的 JSF 執行生命週期中,會多次呼叫 getter 方法。工作區嘗試通過委託物件使資料檢索過程與後續的資料訪問過程相隔離。其目的是避免每次諮詢支援 bean 的訪問函式時帶來執行查詢的計算成本。解決方案包括在建構函式中初始化 DataModel(靜態塊),或 “init” 託管屬性;在該 bean 的私有屬性中快取結果;使用 HttpSession 或作用域為會話的支援 bean;並依賴另一層 O/R 快取機制。

清單 6 顯示了另一種選擇:使用作用域為該請求的 bean 的私有屬性臨時快取查詢結果。您會發現,這至少能夠在頁面呈現階段消除冗餘獲取,但是當該 bean 在後續頁面超出作用域時,仍然會丟棄該快取。


清單 6. 作為 DataModel 公開課程,僅獲取一次
                
public class CourseAction {
    // ...

    private DataModel coursesModel = null;

    public DataModel getCourses() {
        if (coursesModel == null) {
            System.out.println("Retrieving courses...");
            coursesModel = new ListDataModel(courseManager.getAll());
        }
        return coursesModel;
    }
    
    public void setCourses(DataModel coursesModel) {
        this.coursesModel = coursesModel;
    }
}

清單 6 中的方法只是切斷資料檢索和資料訪問的嘗試之一。無論您制定什麼樣的解決方案,保持資料的可用性直到不再需要是避免冗餘資料獲取的關鍵。幸運的是,這類上下文狀態管理正是 Seam 所擅長的!





回頁首


上下文狀態管理

Seam 使用工廠模式初始化非元件物件和集合。一旦初始化資料之後,Seam 就可以將生成的物件放到一個可用的作用域中,然後就可以在其中反覆讀取,而不再需要藉助工廠方法。這個特殊的上下文就是 conversation 作用域。conversation 作用域提供了在一組明確定義的請求期間臨時維護狀態的方法。

直到最近,也很少有 Web 應用程式架構提供任何型別的能夠表現對話的構造。現有的任何上下文都沒有提供合適的粒度水平,用於處理多請求操作。您會發現,對話提供了一種方式,可以防止短期儲存丟失,而短期儲存丟失在 Web 應用程式中很常見,並且還是濫用資料庫的根本原因。結合工廠元件模式使用對話使得在合適時諮詢資料庫成為可能,而不是為了重新獲取應用程式未能跟蹤的資料。

雙射
雙射是 Seam 對依賴項注入概念的擴充套件。除了接收上下文變數來設定元件屬性值之外,雙射允許將元件屬性值推出目標上下文,該操作稱為 outjecting。雙射與依賴項注入的不同之處在於它是動態的、上下文相關的並且是雙向的。也就是說,雙射是動態上一致的,即呼叫元件時進行匯入和匯出值操作。因此,雙射更適合有狀態的元件,如 Web 應用程式中使用的那些元件。

使用對話防止儲存丟失

要完成一項任務,應用程式常常必須指導使用者瀏覽一系列螢幕。該過程通常需要多次向伺服器發出 post,或者是由使用者直接提交表單,或者通過 Ajax 請求。在任何一種情況下,都應該能夠在用例期間通過維護伺服器端物件的狀態跟蹤該應用程式。對話相當於邏輯工作單元。它允許您藉助確定的起始點和結束點在單個瀏覽器視窗中為單個使用者建立單獨的上下文。使用者與該應用程式的互動狀態是針對整個對話維護的。

Seam 提供了兩類對話:臨時對話和長時間執行的對話。臨時對話 存在於整個請求過程,包括重定向。這項功能解決了 JSF 開發過程中的一項難題,即重定向將無意中丟棄儲存在 FacesContext(如 FacesMessage 例項)中的資訊。臨時對話是 Seam 中的標準操作模式:您可以免費獲得這些模式。這意味著在經過重定向之後取出的任何值仍然能夠存在,而不需要您執行額外的工作。這項功能是安全網,允許 Seam 自由地在任意適當的時候使用重定向。

相比之下,長期執行的對話 能夠在一系列明確定義的請求期間保持作用域中的變數。您可以在配置檔案中定義對話邊界,藉助註釋進行宣告,也可以藉助 Seam API,通過程式設計對其進行控制。長期執行的對話有點像小會話,隔離在自己的瀏覽器選項卡中(或視窗),能夠在對話結束或超時時自動清除。與對應的會話相比,conversation 作用域的要點之一是:conversation 作用域將發生在同一應用程式螢幕上位於多個瀏覽器選項卡中的活動分離開。簡單地講,使用對話消除了併發衝突的危險。(請參見 參考資料 閱讀關於 Seam 如何隔離併發對話的詳細討論。)

Seam 對話是對 ad-hoc 會話管理方法的重大改進,後者是現場臨時準備的,或者是其他框架鼓勵使用的。conversation 作用域的引入還解決了很多開發人員指出的問題,即 JSF 使用物件打亂了 HttpSession,沒有提供任何自動垃圾回收 (GC) 機制。對話允許您建立有狀態的元件,而不必使用 HttpSession。藉助 conversation 作用域,幾乎不再需要使用會話作用域,並且您可以更為隨意地使用。





回頁首


藉助 Seam 建立物件

回到課程列表示例,這時該重構程式碼,以利用工廠模式。目的是允許 Seam 管理課程集合,以便其在請求(包括重定向)期間保持可用。如果希望 Seam 管理該集合,則必須使用合適的註釋將建立過程交給 Seam。

Seam 使用構建函式例項化和裝配元件。這些構建函式是在 bean 類中通過註釋宣告的。實際上,您已經見到過其中一個例子: @Name 註釋。@Name 註釋告知 Seam 使用預設的類建構函式建立新例項。要構建自己的課程列表,您不希望使用元件例項,而是使用物件集合。為此,您希望使用 @Factory 註釋。@Factory 註釋向已提取變數的建立過程附加了一個方法,這是在註釋的值中指定的,當該變數沒有繫結任何值時就會使用該方法。

在清單 7 中,工廠方法 findCourses()(位於 CourseAction 類)用於初始化 courses 屬性的值,該值是作為 DataModel 提取到檢視中的。該工廠方法通過將這項工作委託給服務層來例項化課程物件集合。


清單 7. 使用 DataModel 註釋公開課程
                
@Name("courseAction")
public class CourseAction {
    // ...

    @DataModel
    private List courses;
    
    @Factory("courses")
    public void findCourses() {
        System.out.println("Retrieving courses...");
        courses = courseManager.getAll();
    }
}

請注意,這裡不存在 getCourses()setCourses()方法!藉助 Seam,使用標記著 @DataModel 註釋的私有屬性的名稱和值將資料提取到檢視中。因此不需要屬性訪問函式。在這個方案中,@DataModel 註釋執行兩項功能。首先,它提取或公開 該屬性,以便 JSF 變數解析器可以通過值繫結表示式 #{courses} 對它進行訪問。其次,它提供了手動在 DataModel 型別中包裝課程列表的備選方式(如 清單 4 中所示)。作為替代,Seam 自動在 DataModel 例項中嵌入課程列表,以便其可以方便地與 UIData 元件(如 h:dataTable)一起使用。因此,支援 bean(CourseAction)成為簡單的 POJO。然後由該框架處理 JSF 特有的細節。

清單 8 顯示了該檢視中發生的相應重構。與 清單 5 惟一的不同之處在於值繫結表示式。利用 Seam 的提取機制時,使用縮寫的值繫結表示式 #{courses} ,而不是通過 #{courseAction.courses} 諮詢支援 bean 的訪問方法。提取的變數直接放到該變數上下文中,不受其支援 bean 的約束。


清單 8. 使用提取的 DataModel 的課程列表檢視
                

Courses

No courses found. <!-- column definitions goes here --&gt

現在再次訪問該頁面時,以下訊息在控制檯中只出現一次:

 Retrieving courses...

使用工廠構建函式以及臨時 conversation 作用域能夠在請求期間保持這些資料,並確保變數 courses 僅例項化一次,而不管在檢視中它被訪問了多少次。

逐步分析建立方案

您可能想知道 @Factory 註釋什麼時候起作用。為了防止註釋變得太神祕,我們將逐步分析剛剛描述的建立方案。可以按照圖 1 中的序列圖進行研究:


圖 1. Seam 提取使用工廠方法初始化的 DataModel
Seam 工廠序列圖

檢視元件(如 h:dataTable)依靠值繫結表示式 #{courses} 提供課程集合。本地 JSF 變數解析器首先查詢與名稱 courses 相匹配的 JSF 託管 bean。如果找不到任何匹配,Seam 就會收到解析該變數的請求。Seam 搜尋其元件,然後發現在 CourseAction 類中,@DataModel 註釋被指派給具有等價名稱(courses)的屬性。然後如果不存在 CourseAction 類例項,則建立之。

如果 courses 屬性的值為 null,Seam 就會再次使用該屬性的名稱作為鍵查詢 @Factory 註釋。藉助 findCourses() 方法找到匹配之後,Seam 呼叫它來初始化該變數。最後作為 courses 提取該屬性的值,將其包裝到 DataModel 例項。現在 JSF 變數解析器和檢視就可以使用包裝的值。任何針對此上下文變數的後續請求都會返回已經準備好的課程集合。

既然已經清楚檢索課程列表以及在 Seam 託管的上下文變數中維護該值的方法,下面研究課程列表以外的內容。您已經準備好與課程目錄進行互動。在以下幾節中,將使用顯示單門課程詳細內容(以及新增、編輯和刪除課程)的功能,擴充套件 Open 18 應用程式。





回頁首


實現 CRUD 的巧妙方式

遇到的第一項 CRUD 操作是顯示從課程列表中選出的單門課程的詳細內容。 JSF 規範實際上為您處理了一些資料選擇收集工作。當從 UIData 元件(如h:dataTable)的某行觸發 h:commandLink 之類的操作時,在呼叫事件監聽程式之前,元件的當前行設定為與該事件相關的行。可以將當前行想象成一個指標,在這個例子中,該指標固定在接受該操作的行。實際上,JSF 瞭解行操作與該行的底層資料有關。處理該操作時,JSF 幫助將這些資料放到上下文中。

JSF 本身允許您以兩種方式訪問支援被啟用行的資料。一種方式是使用 DataModel#getRowData() 方法檢索該資料。另一種方法是從對應於臨時迴圈變數的值繫結中讀取該資料,該變數定義在元件標記的 var 屬性中。在第二種情況下,在事件處理期間將再次向變數解析器公開臨時迴圈變數(_course)。這兩種訪問形式最終都需要與 JSF API 進行互動。

如果選擇 DataModel API 作為行資料入口點,那麼必須將 DataModel 包裝器物件公開為支援 bean 的屬性,如 清單 4 所示。另一方面,如果選擇通過值繫結訪問行資料,則必須諮詢 JSF 變數解析器。後一種方法還會將您與檢視中使用的臨時迴圈變數名稱 _course 聯絡起來。

現在考慮 Seam 更抽象的獲得所選資料的方法。Seam 允許您將針對 Seam 元件定義的 @DataModel 註釋與 @DataModelSelection 補充註釋配對。在回發期間,Seam 自動檢測該配對。然後將 UIData 元件的當前行資料注入指派了 @DataModelSelection 註釋的屬性。該方法使支援 bean 與 JSF API 分離,因此使其返回 POJO 狀態。

元件ID
為 JSF 元件標記的 id 屬性指定值,總是不錯的主意,命名容器的屬性更是如此。如果沒有為元件指派 ID,JSF 實現就會生成隱祕的 ID。擁有有意義的 ID 有助於除錯或編寫訪問該 DOM 的 JavaScript。

長期執行的對話

要確保回發時該課程列表仍然可用,並且不必重新從資料庫中獲取該列表,就能呈現下一個響應,則必須將當前的臨時對話轉變成長期執行的對話。

說服 Seam 將臨時對話提升到長期執行對話的一種方式是設定一個方法,使其在執行過程中駐留 @Begin 註釋。還必須將元件本身放到該 conversation 作用域中。通過在 CourseAction 類定義頂部新增 @Scope(ScopeType.CONVERSATION) 註釋,就可以實現。使用長期執行的對話,允許變數保持作用域直至對話結束,而不僅僅是單個請求。對於 UIData 元件來說,這種跨多個請求的穩定性尤其重要。(請參閱 本系列第一篇文章 中關於有狀態元件的討論,瞭解資料不穩定可能對 UIData 元件的列隊執行事件所造成的問題。)

您希望允許使用者從課程目錄中選擇單個課程。要實現這項功能,在 h:commandLink 中包裝各個課程的名稱,h:commandLink 將方法繫結 #{courseAction.selectCourse} 指派成操作,如清單 9 所示。當使用者單擊其中一個連結時,就會觸發對支援 bean 的 selectCourse() 方法的呼叫過程。由於 Seam 控制著注入過程,所以與該行有關的課程資料將自動分配給帶有 @DataModelSelection 註釋的屬性。因此,不必執行任何查詢,就能使用該屬性,詳細資訊如清單 10 所示。


清單 9. 新增命令連結以選擇課程
                

Courses

No courses found. Course Name <!-- additional properties --&gt

向提供資料選擇的支援 bean 新增的內容主要是註釋;放到 conversation 作用域時,必須將該類序列化。


清單 10. 用於捕獲所選課程的 DataModelSelection 註釋
                
@Name("courseAction")
@Scope(ScopeType.CONVERSATION)
public class CourseAction implements Serializable {
    // ...

    @DataModel
    private List courses;
  
    @DataModelSelection
    private Course selectedCourse;
    
    @Begin(join=true)
    @Factory("courses")
    public void findCourses() {
        System.out.println("Retrieving courses...");
        courses = courseManager.getAll();
    }
  
    public String selectCourse() {
        System.out.println("Selected course: " + selectedCourse.getName());
        System.out.println("Redirecting to /courses.jspx");
        return "/courses.jspx";
    }
}

持久化上下文
持久化上下文 是 Seam 可以管理的上下文之一。持久化上下文是藉助 Hibernate 或 JPA 從資料庫載入的所有物件的標識作用域和記憶體快取。與 Spring 提倡的無狀態架構相比,Seam 的建立者推薦使用作用域為對話的元件,並使該元件跨多個請求保留持久化上下文。無狀態模型引入的問題是:關閉持久化上下文時,所有載入的物件進入 “隔離” 狀態,無法再擔保這些物件的標識是否正確。結果導致資料庫和開發人員不得不解決跨持久化上下文會話的物件是否相等。本文中沒有利用託管的持久化上下文。請參閱 參考資料 瞭解更多內容。

對話的優點

清單 10 中可以看出,所有變數作用域是由 Seam 處理的。當執行工廠方法來初始化課程集合時,Seam 遇到 @Begin 註釋,因此將該臨時對話提升為長期執行的對話。@DataModel 註釋提取的變數採用其所有者元件的作用域。因此,在對話期間,該課程集合保持可用。當遇到標記著 @End 註釋的方法時,對話結束。

單擊某一行的課程名稱時,Seam 使用支援該行的課程資料值填充帶有 @DataModelSelection 註釋的屬性。然後觸發操作方法 selectCourse(),導致在控制檯上顯示所選課程的名稱。最後,重新顯示課程列表。隨後就會在控制檯中看到:

Retrieving courses...
Selected course: Sample Course
Redirecting to /courses.jspx

藉助 Seam,就不必在 faces-config.xml 中定義導航規則,即對映每個操作的返回值。取而代之,Seam 檢查操作的返回值是不是有效的檢視模板(技術上稱之為檢視 id),並對其執行動態導航。這項功能能夠使簡單的應用程式保持簡單,還允許對更高階的用例使用宣告式導航。請記住,在這個例子中,Seam 在執行導航時發出了重定向命令。

如果需要通過宣告結束對話,則可以使用 @End(beforeRedirect=true) 註釋操作方法 selectCourse(),在這種情況下,對話會在每次呼叫該方法後結束。beforeRedirect 屬性確保在呈現下一個頁面之前清除對話上下文中的變數,這樣能使臨時對話的工作短路,而在重定向時臨時對話通常會填充這些值。在這個方案中,在每次選中課程時開始資料準備過程。執行完以上描述的同一事件序列之後,現在控制檯將顯示:

Retrieving courses...
Selected course: Sample Course
Redirecting to /courses.jspx
Retrieving courses...





回頁首


提取課程的詳細內容

您尚未詳細瞭解顯示課程的用例。@DataModelSelection 註釋負責將當前行資料注入支援 bean 的例項變數,但是它不是在執行該操作方法之後填充資料,使其可用於隨後的檢視。為此,必須提取所選的值。

您已經看到一種注入形式,即 @DataModel 註釋向要呈現的檢視公開一個物件集合。@DataModel 註釋對單個物件例項的補充是 @Out 註釋。@Out 註釋僅僅獲取該屬性,並使用該屬性自己的名稱向變數解析器公開其值。預設情況下,每次啟用時,@Out 註釋都需要非 null 值。因為並非總是存在課程選擇,如第一次顯示課程列表時,所以必須將所需的註釋標記設定為 false,以表明該提取是有條件的。

命名迴圈變數
h:dataTable 選擇臨時迴圈變數名時,必須謹慎,不能使其與其他提取變數發生衝突。在所有示例中,我指定迴圈變數名稱時使用了下劃線字首。該字首不僅可以防止與提取的課程發生衝突,還有助於說明該變數限定了作用域。上下文變數儲存在給定作用域的共享對映中,因此為其選擇名稱時一定要謹慎!

預設情況下,@Out 註釋反映了用於確定上下文變數名稱的屬性名稱。如果您認為更合適的話,可以選擇為提取的變數使用不同名稱。因為課程資料將被提取到 conversation 作用域,並且可能在後續的一些請求中使用,所以該名稱的 “所選” 特徵失去了原來的意義。在這種情況下,最好使用實體本身的名稱。因此,selectedCourse 屬性的推薦註釋為 @Out(value="course", required=false)

可以在新頁面上顯示課程詳細內容,也可以顯示在同一頁面的表格下面。為了演示的目的,在同一頁面顯示詳細內容,同時限制要構造的檢視數目。要在另一個頁面中訪問提取的變數,不需要額外的工作或特殊技巧。

修訂過的支援 bean

該支援 bean 的上一個版本 的差別不大,因此,清單 11 僅突出顯示了兩者的不同之處。selectedCourse 屬性現在有兩個註釋。selectCourse() 方法也被稍加整理。現在它在繼續呈現檢視之前重新提取該課程物件。在無狀態的設計中,必須確保完全由資料層填充物件,並且正確地初始化任何與顯示其詳細內容有關的延遲載入。


清單 11. 將所選課程提取到檢視
                
    // ...

    @DataModelSelection
    @Out(value="course", required=false)
    private Course selectedCourse;
    
    public String selectCourse() {
        System.out.println("Selected course: " + selectedCourse.getName());
        // refetch the course, loading all lazy associations
        selectedCourse = courseManager.get(selectedCourse.getId());
        System.out.println("Redirecting to /courses.jspx");
        return "/courses.jspx";
    }

    // ...

其中大多數有趣的變化都發生在檢視中,但是這些變化並不新奇。清單 12 顯示了在選中某個課程時,呈現在 h:dataTable 下面的詳細內容皮膚:


清單 12. 有條件地為所選課程顯示課程詳細內容
                

  

Course Detail

<!-- additional properties --&gt
Course Name #{course.name}





回頁首


重新注入課程

Open 18 應用程式最複雜的用例是建立和更新操作。但是藉助 Seam,實現起來並不困難。要完成這兩項需求,必須使用一個額外的註釋:@In。將課程提取到呈現課程編輯器表單的檢視之後,必須在回發時捕獲已更新的物件。就像使用 @Out 將變數推送到檢視中一樣,可以使用 @In 在回發時重新捕獲它們。

當使用者處理載入到表單中的課程資訊時,該課程實體耐心地在 conversation 作用域中等待。因為應用程式使用無狀態的服務介面,所以此時的課程例項看作已經與持久化上下文 “分離”。提交該表單時,最終到達 JSF 的更新模型值(Update Model Value)階段。此時,與表單中欄位有關的課程物件將收到使用者的更新。當呼叫該操作方法時,必須重新使已更新的物件與持久化上下文建立聯絡。通過使用 save() 方法將該物件傳遞迴服務層,就可以實現。

但是等等 —— 驗證在哪裡?您肯定不希望無效資料損壞您的資料庫!另一方面,您可能不希望驗證標記打亂您的檢視模板。您甚至可能同意驗證程式碼不屬於檢視層的說法。幸運的是,Seam 負責完成 JSF 驗證的瑣碎工作!

藉助 Seam 和 Hibernate 進行驗證

如果您將整個表單包裝到一個 s:validateAll 元件標記中, Seam 允許您在 JSF 的流程驗證(Process Validation)階段執行對資料模型定義的驗證。這種驗證方法比以下方法更有吸引力:在檢視中到處設定 JSF 驗證器標記,或者維護一個配置檔案,寫滿針對第三方驗證框架的驗證定義。取而代之,可以使用 Hibernate Validator 註釋向實體類屬性指派驗證標準,如清單 13 所示。然後 Hibernate 在持久化物件時,對驗證進行兩次檢查,為您提供雙重保護。這個雙重保障方法意味著檢視中不小心出現的 bug 沒有任何機會危害您的資料質量。(請參閱 參考資料 瞭解關於 Hibernate Validator 的更多內容。)


清單 13. 帶有 Hibernate 驗證註釋的課程實體
                
@Entity
@Table(name = "course")
public class Course implements Serializable {

    private long id;
    private String name;
    private CourseType type = CourseType.PUBLIC;
    private Address address;
    private String uri;
    private String phoneNumber;
    private String description;

    public Course() {}

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    @NotNull
    public long getId() {
        return this.id;
    }

    public void setId(long id) {
        this.id = id;
    }

    @Column(name = "name")
    @NotNull
    @Length(min = 1, max = 50)
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Column(name = "type")
    @Enumerated(EnumType.STRING)
    @NotNull
    public CourseType getType() {
        return type;
    }

    public void setType(CourseType type) {
        this.type = type;
    }

    @Embedded
    public Address getAddress() {
        return address;
    }
    
    public void setAddress(Address address) {
        this.address = address;
    }
    
    @Column(name = "uri")
    @Length(max = 255)
    @Pattern(regex = "^https?://.+$", message = "validator.custom.url")
    public String getUri() {
        return this.uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    @Column(name = "phone")
    @Length(min = 10, max = 10)
    @Pattern(regex = "^\\d*$", message = "validator.custom.digits")
    public String getPhoneNumber() {
        return this.phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Column(name = "description")
    public String getDescription() {
        return this.description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    // equals and hashCode not shown
}


只需少量步驟 ...

課程物件僅在回發時注入,而回發是使用者提交課程編輯器表單觸發的,不是由每個涉及 courseAction 元件的請求觸發的。要想有條件地使用 @In 註釋,必須在定義它時將其 required 標誌設定為 false。這樣做可以確保 Seam 在找不到要注入的課程物件時不會發出警報。

當提交課程編輯器表單時,就可以注入以前提取的課程物件。要確保將該例項重新注入回同一屬性,則向 @In 註釋提供的名稱必須等價於 @Out 註釋所使用的名稱。作為新增這些內容的結果,selectedCourse 屬性現在擁有三個註釋。(情況變得複雜起來!)

還必須向支援 bean 提供三個額外的操作方法,以處理講述到的新 CRUD 操作。新註釋以及 addCourse()editCourse()saveCourse() 操作方法如清單 14 所示:


清單 14. 用於建立、編輯和儲存課程的其他操作
                
    // ...

    @DataModelSelection
    @In(value="course", required=false)
    @Out(value="course", required=false)
    private Course selectedCourse;
    
    public String addCourse() {
        selectedCourse = new Course();
        selectedCourse.setAddress(new Address());
        return "/courseEditor.jspx";
    }
    
    public String editCourse() {
        selectedCourse = courseManager.get(selectedCourse.getId());
        return "/courseEditor.jspx";
    }
    
    public String saveCourse() {
        // remove course from cached collection
        // optionally, the collection could be nullified, forcing a refetch
        if (selectedCourse.getId() > 0) {
            courses.remove(selectedCourse);
        }
        courseManager.save(selectedCourse);
        // add course to the cached collection
        // optionally, the collection could be nullified, forcing a refetch
        courses.add(selectedCourse);
        FacesMessages.instance().add("#{course.name} has been saved.");
        return "/courses.jspx";
    }

    // ...

課程編輯器頁面負責建立和更新。Seam 之所以這麼酷,是因為它能夠暗中指揮通訊,在這個例子中,是通過在您瀏覽頁面時將所選課程儲存在上下文中實現的。不需要使用 HttpSession 請求引數,也不需要想方設法儲存所選課程。而僅僅是提取想要公開的內容,並注入期望接收的內容。





回頁首


編輯器模板

從編輯器頁面(如清單 15 所示)觀察表單元件。該頁使用了以下兩個 Seam 元件標記,使得開發檢視的工作變得更加簡單:

  • s:decorate 結合 afterInvalidField facet 在每個輸入元件之後插入 s:message 元件,輸入元件使您不必在頁面中重複標記。
  • s:validateAll 指導 Seam 將 Hibernate Validator 註釋結合到 JSF 驗證過程,以便在回發時驗證表單中的每個欄位。

您不會在課程編輯器檢視頁面上發現任何本地 JSF 驗證器,因為 Seam 在利用 Hibernate Validator 時,完全不需使用本地驗證器。該頁面還顯示了 Seam 附帶的列舉轉換器 元件,以防您碰巧使用 Java 5 列舉型別。


清單 15. 課程編輯器檢視
                






回頁首


新增刪除功能

回顧程式碼片段,可以發現到目前為止重點內容大多涉及消除程式碼、選擇,而不是通過註釋描述功能,並由框架負責處理細節。這種簡單性允許您集中精力處理更復雜的問題,並新增深受大家喜歡的奇特 Ajaxian 效果。您可能尚未認識到只需再做少量工作,就可以實現所有 CRUD 操作 —— 實際上即將到達最後階段!

在應用程式中實現刪除功能是一項簡單的事情。只需向每行新增另一個 h:commandLink,該命令連結能啟用支援 bean 的刪除方法(deleteCourse())。我們已經實現了公開所選課程的工作,僅僅需要將繫結到課程屬性的課程物件傳遞給 CourseManager 以終止該課程,如清單 16 中所示:


清單 16. 向 deleteCourse 新增命令連結
                

  
    Course Name
    
  
  
    Actions
    
  
  <!-- additional properties --&gt


deleteCourse() 方法中,如清單 17中所示,利用 Seam 的 FacesMessages 元件警告使用者正在發生的操作。該訊息是以典型的途徑在檢視中使用 h:messages JSF 元件顯示的。但是首先請注意,建立訊息是多麼簡單!您可以徹底拋棄以前令人頭疼的 JSF 工具類;Seam 可靠地消除了 JSF 以前的陰影。


清單 17. 向 deleteCourse 新增操作方法
                
    // ...

    public String deleteCourse() {
        courseManager.remove(selectedCourse.getId());
        courses.remove(selectedCourse);
        FacesMessages.instance().add(selectedCourse.getName() + " has been removed.");
        // clear selection so that it won't be shown in the detail pane
        selectedCourse = null;
        return "/courses.jspx";
    }

    // ...





回頁首


完整的課程列表

處理完所有 CRUD 操作,就即將完工了!剩下的惟一的一個步驟是將整個課程列表組裝到一起,如清單 18 所示:


清單 18. 完整的課程列表檢視
                

Courses

No courses found. Course Name Location Phone Number Actions

Course Detail

Course Name #{course.name} (#{course.type})
Website #{course.uri}
Phone #{course.phoneNumber}
State #{course.address.state}
City #{course.address.city}
ZIP Code #{course.address.postalCode}

...#{course.description}


恭喜!您完成了第一個基於 Seam 的 CRUD 應用程式。





回頁首


結束語

無縫 JSF 系列第二篇文章中,您親自發現了 Seam 的 Java 5 註釋如何簡化程式碼,conversation 作用域如何自動在一系列請求期間管理狀態,以及如何同時使用 Seam 和 Hibernate Validator 對輸入資料執行資料模型驗證。

實際上可以使用 seam-gen 自動完成大多數 CRUD 工作(請參見 參考資料), seam-gen 是 Ruby-on-Rails 樣式的 Seam 應用程式生成器。但是我希望您從本文的練習中瞭解到 Seam 不僅僅是另一個 Web 框架。採用 Seam 並不強制您拋棄 JSF 經驗。相反,Seam 是對 JSF 非常強大的擴充套件,實際上它增強了 JSF 的生命週期。Seam 和 JSF 結合起來可以順利地和任何無狀態的服務層或 EJB3 模型進行整合。

既然已經瞭解 Seam 減輕 JSF 開發的一些方式,您可能想知道它對 第 1 部分 中討論的更高階 Web 2.0 技術的支援程度。在本系列的最後一個部分中,將講述如何使用 Ajax remoting 通過在課程目錄和 Google Maps 之間建立 mashup,進一步開發 Open 18 應用程式,在這個過程中,您將瞭解 Seam 的 Java

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/13270562/viewspace-244087/,如需轉載,請註明出處,否則將追究法律責任。

Seam無縫整合 JSF: 藉助 Seam 進行對話
請登入後發表評論 登入
全部評論

相關文章