在這個分為三部分的系列文章的第一篇(http://www.bianceng.cn/Programming/Java/201103/24860.htm)中介紹了 Seam,它是既能顯著增強 JSF 功能又能實現基於組件的架構的應用程序框架。在這篇文章中,我解釋了 Seam 和其他經常與 JSF 結合使用的 Web 框架的不同之處,展示了向現有 JSF 應用程序添加 Seam 是多麼輕松,最後概述了 Seam 對 JSF 應用程序生命周期的增強,同時還涉及到有狀態的對話、工廠組件以及使用注釋進行隱秘配置。
盡管這篇文章可能引發了您對 Seam 的興趣,但是您可能無法確信它能夠改善 JSF 開發體驗。集成一組新工具通常比閱讀它復雜得多,並且有時候並不值得。在無縫 JSF 系列文章的第二篇文章中,您將親自發現 Seam 是否能夠實現其簡化 JSF 開發的承諾。在使用 Seam 構建執行標准 CRUD 操作的簡單應用程序之後,我敢肯定您會認為 Seam 是對 JSF 框架的必要擴展。結果,Seam 還能幫助降低數據庫層有限資源的負載。
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 變量解析器
<variable-resolver>
org.springframework.web.jsf.DelegatingVariableResolver
</variable-resolver>
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
<managed-bean>
<managed-bean-name>courseAction</managed-bean-name>
<managed-bean-class>com.ibm.dw.open18.CourseAction</managed-bean-class>
<managed-property>
<property-name>courseManager</property-name>
<value>#{courseManager}</value>
</managed-property>
</managed-bean>
注釋簡化了 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<Course, Long> courseManager;
}
注意所有 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. 初始課程列表視圖
<h2>Courses</h2>
<h:panelGroup rendered="#{courseAction.courses.rowCount eq 0}">
No courses found.
</h:panelGroup>
<h:dataTable id="courses" var="_course" value="#{courseAction.courses}"
rendered="#{courseAction.courses.rowCount gt 0}">
<!-- column definitions go here -->
</h:dataTable>
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 應用程序中很常見,並且還是濫用數據庫的根本原因。結合工廠組件模式使用對話使得在合適時咨詢數據庫成為可能,而不是為了重新獲取應用程序未能跟蹤的數據。
使用對話防止存儲丟失
要完成一項任務,應用程序常常必須指導用戶浏覽一系列屏幕。該過程通常需要多次向服務器發出 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<Course> 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 的課程列表視圖
<h2>Courses</h2>
<h:panelGroup rendered="#{courses.rowCount eq 0}">
No courses found.
</h:panelGroup>
<h:dataTable id="courses" var="_course" value="#{courses}"
rendered="#{courses.rowCount gt 0}">
<!-- column definitions goes here -->
</h:dataTable>
現在再次訪問該頁面時,以下消息在控制台中只出現一次:
Retrieving courses...
使用工廠構建函數以及臨時 conversation 作用域能夠在請求期間保持這些數據,並確保變量 courses 僅實例化一次,而不管在視圖中它被訪問了多少次。
逐步分析創建方案
您可能想知道 @Factory 注釋什麼時候起作用。為了防止注釋變得太神秘,我們將逐步分析剛剛描述的創建方案。可以按照圖 1 中的序列圖進行研究:
圖 1. Seam 提取使用工廠方法初始化的 DataModel
視圖組件(如 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 狀態。
長期運行的對話
要確保回發時該課程列表仍然可用,並且不必重新從數據庫中獲取該列表,就能呈現下一個響應,則必須將當前的臨時對話轉變成長期運行的對話。
說服 Seam 將臨時對話提升到長期運行對話的一種方式是設置一個方法,使其在執行過程中駐留 @Begin 注釋。還必須將組件本身放到該 conversation 作用域中。通過在 CourseAction 類定義頂部添加 @Scope(ScopeType.CONVERSATION) 注釋,就可以實現。使用長期運行的對話,允許變量保持作用域直至對話結束,而不僅僅是單個請求。對於 UIData 組件來說,這種跨多個請求的穩定性尤其重要。(請參閱 本系列第一篇文章 中關於有狀態組件的討論,了解數據不穩定可能對 UIData 組件的列隊執行事件所造成的問題。)
您希望允許用戶從課程目錄中選擇單個課程。要實現這項功能,在 h:commandLink 中包裝各個課程的名稱,h:commandLink 將方法綁定 #{courseAction.selectCourse} 指派成操作,如清單 9 所示。當用戶單擊其中一個鏈接時,就會觸發對支持 bean 的 selectCourse() 方法的調用過程。由於 Seam 控制著注入過程,所以與該行有關的課程數據將自動分配給帶有 @DataModelSelection 注釋的屬性。因此,不必執行任何查找,就能使用該屬性,詳細信息如清單 10 所示。
清單 9. 添加命令鏈接以選擇課程
<h2>Courses</h2>
<h:panelGroup rendered="#{courses.rowCount eq 0}">
No courses found.
</h:panelGroup>
<h:dataTable id="courses" var="_course" value="#{courses}"
rendered="#{courses.rowCount gt 0}">
<h:column>
<f:facet name="header">Course Name</f:facet>
<h:commandLink id="select"
action="#{courseAction.selectCourse}" value="#{_course.name}" />
</h:column>
<!-- additional properties -->
</h:dataTable>
向提供數據選擇的支持 bean 添加的內容主要是注釋;放到 conversation 作用域時,必須將該類序列化。
清單 10. 用於捕獲所選課程的 DataModelSelection 注釋
@Name("courseAction")
@Scope(ScopeType.CONVERSATION)
public class CourseAction implements Serializable {
// ...
@DataModel
private List<Course> 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";
}
}
對話的優點
在 清單 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,以表明該提取是有條件的。
默認情況下,@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. 有條件地為所選課程顯示課程詳細內容
<h:panelGroup rendered="#{course.id gt 0}">
<h3>Course Detail</h3>
<table class="detail">
<tr>
<th>Course Name</th>
<td>#{course.name}</td>
</tr>
<!-- additional properties -->
</h:panelGroup>
重新注入課程
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. 課程編輯器視圖
<h2><h:outputText value="#{course.id gt 0 ? 'Edit' : 'Create'} Course" /></h2>
<h:form id="course">
<s:validateAll>
<f:facet name="afterInvalidField">
<s:span styleClass="error">
<s:message showDetail="true" showSummary="false"/>
</s:span>
</f:facet>
<ul>
<li>
<h:outputLabel for="name" value="Course Name"/>
<s:decorate>
<h:inputText id="name" value="#{course.name}" required="true"/>
</s:decorate>
</li>
<li>
<h:outputLabel for="type" value="Type"/>
<s:decorate>
<h:selectOneMenu id="type" value="#{course.type}">
<s:convertEnum />
<s:enumItem enumValue="PUBLIC" label="Public" />
<s:enumItem enumValue="PRIVATE" label="Private" />
<s:enumItem enumValue="SEMI_PRIVATE" label="Semi-Private" />
<s:enumItem enumValue="RESORT" label="Resort" />
<s:enumItem enumValue="MILITARY" label="Military" />
</h:selectOneMenu>
</s:decorate>
</li>
<li>
<h:outputLabel for="uri" value="Website" />
<s:decorate>
<h:inputText id="uri" value="#{course.uri}"/>
</s:decorate>
</li>
<li>
<h:outputLabel for="phone" value="Phone Number" />
<s:decorate>
<h:inputText id="phone" value="#{course.phoneNumber}"/>
</s:decorate>
</li>
<li>
<h:outputLabel for="city" value="City" />
<s:decorate>
<h:inputText id="city" value="#{course.address.city}"/>
</s:decorate>
</li>
<li>
<h:outputLabel for="state" value="State" />
<s:decorate>
<h:selectOneMenu id="state" value="#{course.address.state}" required="true">
<s:selectItems var="state" value="#{states}" label="#{state}" />
</h:selectOneMenu>
</s:decorate>
</li>
<li>
<h:outputLabel for="zip" value="ZIP Code" />
<s:decorate>
<h:inputText id="zip" value="#{course.address.city}"/>
</s:decorate>
</li>
<li>
<h:outputLabel for="description" value="Description" />
<s:decorate>
<h:inputTextarea id="description" value="#{course.description}"/>
</s:decorate>
</li>
<ul>
</s:validateAll>
<p class="commands">
<h:commandButton id="save" action="#{courseAction.saveCourse}" value="Save"/>
<s:button id="cancel" view="/courses.jspx" value="Cancel"/>
</p>
</h:form>
添加刪除功能
回顧代碼片段,可以發現到目前為止重點內容大多涉及消除代碼、選擇,而不是通過注釋描述功能,並由框架負責處理細節。這種簡單性允許您集中精力處理更復雜的問題,並添加深受大家喜歡的奇特 Ajaxian 效果。您可能尚未認識到只需再做少量工作,就可以實現所有 CRUD 操作 —— 實際上即將到達最後階段!
在應用程序中實現刪除功能是一項簡單的事情。只需向每行添加另一個 h:commandLink,該命令鏈接能激活支持 bean 的刪除方法(deleteCourse())。我們已經實現了公開所選課程的工作,僅僅需要將綁定到課程屬性的課程對象傳遞給 CourseManager 以終止該課程,如清單 16 中所示:
清單 16. 向 deleteCourse 添加命令鏈接
<h:dataTable id="courses" var="_course" value="#{courses}"
rendered="#{courses.rowCount gt 0}">
<h:column>
<f:facet name="header">Course Name</f:facet>
<h:commandLink id="select"
action="#{courseAction.selectCourse}" value="#{_course.name}" />
</h:column>
<h:column>
<f:facet name="header">Actions</f:facet>
<h:commandLink id="delete" action="#{courseAction.deleteCourse}" value="Delete" />
</h:column>
<!-- additional properties -->
</h:dataTable>
在 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. 完整的課程列表視圖
<h2>Courses</h2>
<h:messages id="messages" globalOnly="true" />
<h:panelGroup rendered="#{courses.rowCount eq 0}">
No courses found.
</h:panelGroup>
<h:dataTable id="courses" var="_course" value="#{courses}"
rendered="#{courses.rowCount gt 0}">
<h:column>
<f:facet name="header">Course Name</f:facet>
<h:commandLink id="select"
action="#{courseAction.selectCourse}" value="#{_course.name}" />
</h:column>
<h:column>
<f:facet name="header">Location</f:facet>
<h:outputText value="#{course.address.city}, #{course.address.state}" />
</h:column>
<h:column>
<f:facet name="header">Phone Number</f:facet>
<h:outputText value="#{course.phoneNumber} />
</h:column>
<h:column>
<f:facet name="header">Actions</f:facet>
<h:panelGroup>
<h:commandLink id="edit" action="#{courseAction.editCourse}" value="Edit" />
<h:commandLink id="delete" action="#{courseAction.deleteCourse}" value="Delete" />
</h:panelGroup>
</h:column>
</h:dataTable>
<h:commandButton id="add" action="#{courseAction.addCourse}" value="Add Course" />
<h:panelGroup rendered="#{course.id gt 0}">
<h3>Course Detail</h3>
<table class="detail">
<col width="20%" />
<col width="80%" />
<tr>
<th>Course Name</th>
<td>#{course.name} <span class="notation">(#{course.type})</span></td>
</tr>
<tr>
<th>Website</th>
<td><h:outputLink value="#{course.uri}"
rendered="#{not empty course.uri}">#{course.uri}</h:outputLink></td>
</tr>
<tr>
<th>Phone</th>
<td>#{course.phoneNumber}</td>
</tr>
<tr>
<th>State</th>
<td>#{course.address.state}</td>
</tr>
<tr>
<th>City</th>
<td>#{course.address.city}</td>
</tr>
<tr>
<th>ZIP Code</th>
<td>#{course.address.postalCode}</td>
</tr>
</table>
<h:panelGroup rendered="#{not empty course.description}">
<p><q>...#{course.description}</q></p>
</h:panelGroup>
</h:panelGroup>
恭喜!您完成了第一個基於 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 5 注釋和捆綁的 JavaScript 庫如何指導浏覽器和服務器端組件之間的通信。
再見,同時祝您玩高爾夫愉快!
來源:http://www.ibm.com/developerworks/cn/java/j-seam2/