簡介:Java™Server Faces(JSF)技術是一種服務器端框架,它提供一種基於組件的 Web 用戶 界面開發方式。JSF 1.2(集成在 Java Enterprise Edition 5 中)糾正了 JSF 的一些缺陷並添加了一 些出色的特性。這個教程系列討論如何使用 JSF 1.2。本系列偏重示例,較少解釋理論 — 這是為 了幫助您盡快開始使用 JSF。
開始之前
關於本系列
這個教程系列討論 JavaServer Faces(JSF)技術的基礎知識。JSF 是一種用於 Java Web 應用程序 的服務器端用戶界面組件框架。本系列針對 JSF 的新手,幫助他們快速入門 — 使用 JSF 並不是必需的 ,但是使用 JSF 組件可以減少工作量。本系列只討論基礎知識並提供大量示例。
與 AWT、SWT 和 Swing 一樣,JSF 是一種比較傳統的 GUI 開發環境。它的主要好處之一是,它將困 難的工作交給框架開發人員而不是應用程序開發人員,從而簡化了 Web 開發。坦率地說,JSF 本身比許 多其他 Web 框架復雜,但是它對應用程序開發人員隱藏了復雜性。與大多數其他框架相比,用 JSF 開發 Web 應用程序要容易得多:需要的代碼更少,復雜性更低,配置更少。
如果您從事 Java 服務器端 Web 開發,那麼 JSF 是最容易掌握的框架。它非常適合創建 Web 應用程 序(不是 Web 站點本身)。它讓 Web 開發人員可以集中精力處理 Java 代碼,而不需要處理請求對象、 會話對象、請求參數或復雜的 XML 文件。與其他 Java Web 框架相比,使用 JSF 可以更快速地做更多事 情。
關於本教程
本教程介紹一種基本的 JSF 開發方法。在本教程中,不使用工具或 IDE 支持(盡管工具支持是 JSF 的主要好處之一)。我們將要進行純粹的編程!我只介紹基本知識,從而幫助您理解這裡的討論內容並有 效地使用 JSF 構建 Web 應用程序。您會驚奇地發現,即使不使用 IDE 工具,JSF 開發也比其他 Java Web 框架容易。
目標
在本教程中,我將概述 JSF 的特性並講解如何編寫基本的 JSF 應用程序。我將構建一個簡單的計算 器應用程序,逐步改進它的外觀和感覺,修改它的結構來添加依賴性注入,以及實現 JSF 的導航機制。 在第 2 部分中,將構建定制的轉換器、檢驗器和階段監聽器。
誰應該學習本教程?
如果您是 JSF 的初學者,那麼本教程正適合您。如果您用過 JSF,但是沒有用過 JSF 1.2 特性,或 者只用 GUI 工具構建過 JSF 應用程序,那麼也可能從本系列教程學到許多知識。
前提條件
本教程適合初級或中級水平的 Java 開發人員。您應該基本了解 Java 語言並擁有一定的 GUI 開發經 驗。
系統需求
要運行本教程中的示例,您需要一個 Java 開發環境(JDK)和 Apache Maven。擁有 Java IDE 會有 所幫助。本教程提供了 Maven 項目文件以及 Eclipse Java EE 和 Web Tools Project(WTP)項目文件 。
JSF 基礎
與 Swing 和 AWT 一樣,JSF 也是一種開發框架,它提供一套標准的、可重用的 GUI 組件,用來構建 Web 應用程序的界面。JSF 具有以下優點:
完全地隔離行為和表示
能夠對有狀態進行組件級控制
能夠輕松地將事件連接到服務器端代碼
使用大家熟悉的 UI 組件和 Web 層概念
提供多種標准的供應商實現
出色的 IDE 支持
典型的 JSF 應用程序由以下部分組成:
用來管理應用程序狀態和行為的 JavaBean
有狀態 GUI 組件
事件驅動的開發(像傳統 GUI 開發一樣通過監聽器)
表示 Model-View-Controller(MVC)視圖的頁面;頁面通過 JSF 組件樹引用視圖根(view root)
為了使用 JSF,可能需要克服一些概念方面的障礙,掌握這些概念是很值得的。JSF 的組件狀態管理 、易用的用戶輸入檢驗、細粒度的基於組件的事件處理以及可輕松擴展的體系結構,這些概念會大大簡化 Web 開發。本節詳細解釋最重要的 JSF 特性。
基於組件的體系結構
JSF 為標准 HTML 中可用的每個輸入字段都提供了組件標記。還可以針對應用程序特有的用途編寫定 制的組件,或者用多個 HTML 組件組合成一個復合組件 — 例如,用三個下拉菜單組成一個 Data Picker 組件。JSF 組件是有狀態的。它們的狀態是通過 JSF 框架提供的。JSF 使用組件生成 HTML 響應。還可 以使用許多第三方 JSF GUI 組件。
JSF 包括:
一個事件發布模型
一個輕量型反轉控制(inversion-of-control,IoC)容器
用於幾乎每種常用 GUI 特性的組件,包括(但不限於):
可插入的顯示
服務器端檢驗
數據轉換
頁面導航管理
作為一種基於組件的體系結構,JSF 具有很強的可配置性和可擴展性。大多數 JSF 功能 — 比如導航 和托管 bean 查找 — 都可以替換為可插入的組件。這種可插入性為構建 Web 應用程序 GUI 提供了很強 的靈活性,並允許輕松地將其他基於組件的技術結合到開發工作中。例如,可以將 JSF 的內置 IoC 框架 替換為更成熟的 IoC/面向方面編程(AOP)Spring 框架,以執行托管 bean 查找。我將在第 2 部分中討 論許多高級特性。
JSF 和 JSP 技術
JSF 應用程序的用戶界面由 JavaServer Pages(JSP)頁面組成。每個 JSP 頁面包含提供 GUI 功能 的 JSF 組件。在 JSP 頁面中,可以使用 JSF 定制標記庫來顯示 UI 組件、注冊事件處理函數、將組件 和檢驗器關聯起來、將組件和數據轉換器關聯起來等等。
JSF 並不一定要使用 JSP 技術。實際上,JSP 頁面使用的 JSF 標記僅僅引用組件,讓它們可以顯示 。組件的生命周期與 JSP 頁面很不一樣。
您可以這樣體會這一點:在 JSP 頁面中修改 JSF 組件的屬性並重新裝載頁面,這時什麼也不會發生 。這是因為標記在它的當前狀態中查找組件。如果組件已經存在了,定制標記就不修改它的狀態。組件模 型允許控制器代碼修改組件的狀態(例如,禁用一個文本字段),當顯示視圖時,會顯示組件樹的當前狀 態。
在典型的 JSF 應用程序中,不需要 Java 代碼,只需要非常少的統一 Expression Language(JSTL EL)UI 代碼。正如前面提到的,有許多 IDE 工具可以用來構建和組裝 JSF 應用程序,還有許多第三方 JSF GUI 組件。盡管 JSF 在設計時就考慮到了 WYSIWYG IDE 工具,但是也可以在不使用 WYSIWYG 工具 的情況下編寫 JSF 應用程序。
JSP 2.1 和 JSF 1.2 中的改進
JSP 2.1 添加了許多新特性來支持 JSF,包括統一 Expression Language(EL)API(JSF 1.2 也添加 了這個特性)。現在可以使用標准的 JSTL 標記循環處理列表並顯示 JSF 組件,這是用 JSF 1.1 和 JSP 2.0 無法實現的。關於 JSP 2.1 中的改進的更多信息參見 參考資料。(盡管有了這些改進,但是 Facelets 仍然更適合 JSF,而且 JSF 2.0 將融合許多來自 Facelets 的思想。)
JSF 和 MVC
JSF 是近幾年 Java 平台上 Web 開發技術迅速發展的成果。Java Web 開發技術首先從 JSP 技術開始 ,JSP 的主要好處是很容易在 HTML(和類似 HTML 的)頁面中混合 Java 代碼。下一步是 Model 1 體系 結構,它讓開發人員將大多數後端代碼放到 JavaBeans 組件中,然後用 <jsp:useBean> 標記將 JavaBeans 組件導入 Web 頁面。這個體系結構對於簡單的 Web 應用程序很合適,但是許多 Java 開發人 員討厭 JSP 技術中結合的 C++ 特性,比如靜態包含。所以產生了 Model 2 體系結構。
從本質上說,Model 2 體系結構是一種用於 Web 應用程序的簡化版 MVC。在 Model 2 體系結構中, 控制器由 servlet(或 Actions)表示,顯示由 JSP 頁面負責。Apache Struts 是一個簡化的 Model 2 實現,其中用 Actions 替代了 servlet。在 Struts 中,應用程序的控制器邏輯與它的數據(由 ActionForms 表示)分隔開。對 Struts 的主要批評意見是它太過程性了,不夠面向對象(它被戲稱為 “COBOL for the Web”)。WebWork 和 Spring MVC 是另兩種 Model 2 體系結構實現,它們降低了過程 性,但是它們沒有像 Struts 那樣得到廣泛應用。另外,它們沒有像 JSF 那樣提供有狀態組件模型。 (Struts 2 是在 WebWork 上構建的,原來的 Struts 代碼基已經被廢棄了。即使 Struts 也對自己不滿 意。)
大多數 Model 2 框架的真正問題是事件模型太過簡單(本質上是高度簡化的 MVC),而且它沒有提供 有狀態的 GUI 組件,因此把太多工作留給了開發人員。為了簡便地創建大多數用戶期望的交互方式,需 要一個更豐富的組件和事件模型。與 JSP 技術一樣,大多數 Model 2 框架允許非常容易地在 HTML 布局 和格式化代碼中混合 GUI 定制標記,這使代碼具備組件那樣的松散性,但它們不是有狀態的。而且,一 些 Model 2 體系結構(比如過去的 Struts)在行為和狀態的隔離方面犯了錯誤,這使許多 Java 開發人 員覺得他們是在使用 COBOL。
更豐富的 MVC 環境
JSF 提供一個組件模型和一個更豐富的 MVC 環境 — 比 Model 2 框架豐富得多。與 Model 2 體系結 構相比,JSF 更接近真正的 MVC 編程環境,盡管它仍然是在一個無狀態協議上構建的。JSF 還有助於構 建比 Model 2 框架更細粒度的事件驅動的 GUI。JSF 提供了大量事件 — 選擇菜單項、單擊按鈕、展開 樹節點等等 — 而大多數 Model 2 框架依賴於簡單的 “接收請求” 事件。
JSF 出色的事件模型讓應用程序依賴的 HTTP 細節更少,簡化了開發工作。JSF 還改進了傳統的 Model 2 體系結構,可以更輕松地將表示和業務邏輯移出控制器以及將業務邏輯移出 JSP 頁面。實際上 ,簡單的控制器類根本不連接 JSF,這使它們更容易測試。與真正的 MVC 體系結構不同,JSF 模型層不 太可能發出必須在多個視圖中解析的許多事件(但是,Crank 利用 JBoss ajax4JSF 的支持嘗試這麼做; 參見 參考資料)。另外,這也是不必要的,因為 JSF 使用的是無狀態協議。用來修改或更新視圖的系統 事件幾乎總是一個來自用戶的請求。
JSF MVC 實現的細節
在 JSF 的 MVC 實現中,映射托管 bean 在視圖和模型之間起協調作用。因此,一定要限制托管 bean 中與 JSF 連接的業務邏輯和持久性邏輯。一種常用的替代方法是將業務邏輯委托給應用程序模型。在這 種情況下,托管 bean 還映射模型對象,讓視圖可以顯示它們(作為托管 bean 的屬性)。我喜歡將我的 托管 bean 分為兩類:連接 JSF 的托管 bean(控制器)和不連接 JSF 的托管 bean(模型對象)。
與 JSP 技術不同,JSF 的視圖實現是一個有狀態組件模型。JSF 視圖由兩部分組成:視圖根 和 JSP 頁面。視圖根是一個 UI 組件集合,它維護 UI 的狀態。與 Swing 和 AWT 一樣,JSF 組件使用 Composite 設計模式管理一個組件樹(簡單地說:容器包含組件;容器也是組件)。JSP page 將 UI 組 件綁定到 JSP 頁面,允許將字段組件綁定到後端 bean 的屬性(更可能是屬性的屬性),以及將按鈕綁 定到事件處理函數和動作方法。
圖 1 從 MVC 的視角展示一個示例應用程序的結構(稍後將詳細了解這個程序):
圖 1. 示例應用程序的結構
如果第一節的內容讓您有點兒困惑,也不必擔心:最困難的部分已經過去了。了解了 JSF 的總體框架 ,實現這種技術的過程就完成了一大半兒 — 您不久就會發現克服這個障礙是值得的。現在,理論已經討 論夠了:我們來進行純粹的編程吧!
JSF 示例:逐步創建
本節討論用 JSF 創建應用程序的步驟。這個示例應用程序是一個簡單的計算器應用程序,它演示 JSF 技術的以下方面:
如何安排 JSF 應用程序的結構
如何配置 JSF 的 web.xml 文件
如何配置應用程序的 faces-config.xml 文件
編寫托管 bean(也稱為模型對象和控制器)
使用 JSP 技術構造視圖
使用定制的標記庫在視圖根中構造組件樹
對表單字段的默認檢驗
在後面幾節中,將通過幾次迭代改進這個應用程序,讓它具備更多 JSF 特性。圖 2 給出最終的 Calculator 示例應用程序的注解視圖。在 下載 中可以獲得應用程序源代碼。
圖 2. 最終的 Calculator 應用程序
Calculator 應用程序
最初的 Calculator 示例應用程序的目標是顯示一個頁面,讓用戶輸入兩個數字,然後將它們相加或 相乘。
這個頁面包含:
一個表單
兩個文本字段
兩個標簽
兩個錯誤消息位置
兩個 Submit 按鈕
一個結果面板
文本字段用來輸入數字。標簽表示文本字段的意義。錯誤消息位置用來顯示文本字段的檢驗或數據轉 換錯誤消息。有兩個 JSP 頁面:calculator.jsp 和 index.jsp,後者僅僅重定向到 calculator.jsp。 一個稱為 Calculator 的托管 bean 作為 calculator.jsp 的模型。這個簡單的示例沒有控制器層。
創建應用程序:概述
為了用 JSF 構建最初的 Calculator 應用程序,需要:
在 Web 應用程序部署描述符文件(web.xml)中聲明 Faces Servlet 並添加 Faces Servlet 映射
在 web.xml 文件中指定 faces-config.xml 文件
創建 Calculator 類
在 faces-config.xml 文件中聲明 Calculator bean
創建 index.jsp 頁面
創建 calculator.jsp 頁面
這個應用程序使用以下目錄結構:
+---src
+---main
+---java
+---webapp
+---pages
+---WEB-INF
+---lib
Java 代碼放在 src/main/java/ 下面。web.xml 文件放在 src/main/webapp/WEB-INF 目錄中。JSF 配置文件也放在 src/main/webapp/WEB-INF 下面。這個示例應用程序是用 Eclipse IDE for Java EE Developers(Eclipse JEE)創建的,這個 IDE 包含一個 JSF 項目創建向導,它會創建包含適當條目的 web.xml 文件和 faces-config.xml 文件。我假設您將使用支持 Java EE 5 的應用服務器,也就是說, 它有 JSF 和 JSTL JAR 文件。
聲明 Faces Servlet 和 servlet 映射
為了使用 Faces Servlet,首先需要在 web.xml 文件中聲明它,見清單 1:
清單 1. web.xml 中的 Faces Servlet 聲明
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
這與大多數 web.xml 描述符相似,但是要讓 JSF servlet 處理請求,而不是指定自己的 servlet。 對使用 <f:view> 標記(就像這個示例應用程序所做的)的 JSP 文件的所有請求必須通過這個 servlet。因此,需要添加一個映射並通過這個映射只裝載啟用 JSF 的 JSP 頁面,見清單 2:
清單 2. web.xml 中的 Faces Servlet 路徑映射
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
清單 2 讓 Faces Servlet 容器將所有以 /faces/ 開頭或以 *.jsf 結尾的請求發送到 Faces Servlet 進行處理。這讓 JSF 在顯示 JSF 頁面之前初始化 Faces 上下文和視圖根。視圖根包含 JSF 組 件樹。Faces 上下文是與 JSF 進行交互的方式。
這意味著,要想裝載 Calculator 應用程序,應該使用 http://localhost:8080/calculator/pages/calculator.jsf 或 http://localhost:8080/calculator/faces/pages/calculator.jsp — 而不是 http://localhost:8080/calculator/pages/calculator.jsp。如果在 JSF 上下文之外裝載 JSP 頁面, JSF 就沒有機會初始化 Faces 上下文或視圖根。
指定 faces-config.xml 文件
如果將 Faces 配置文件命名為 faces-config.xml 並把它放在 Web 應用程序的 WEB-INF 目錄中,那 麼 Faces Servlet 會自動地找到並使用它(因為這是默認設置)。另外,也可以在 web.xml 文件中的初 始化參數 javax.faces.application.CONFIG_FILES 中指定逗號分隔的文件列表,從而裝載一個或多個應 用程序配置文件。除了最簡單的 JSF 應用程序之外,對於所有 JSF 應用程序,很可能會使用第二種方法 。因為這個示例應用程序很簡單,我們使用默認的 faces-config.xml 文件位置 /src/main/webapp/WEB -INF。
創建 Calculator 類
現在,創建一個稱為 Calculator 的 POJO(普通 Java 對象),它根本不連接到 JSF。然後用方法綁 定和屬性綁定將它綁定到 JSF。這個簡單的類有兩個屬性:firstNumber 和 secondNumber。
我的目標是演示如何開始使用 JSF,所以讓模型對象盡可能簡單。這個應用程序的模型包含在一個模 型對象中,見清單 3。後面將把它分割成兩個類:控制器和模型。
清單 3. Calculator POJO
package com.arcmind.jsfquickstart.model;
/**
* Calculator. Simple POJO.
*
* @author Rick Hightower
*/
public class Calculator {
/** First number used in operation. */
private int firstNumber = 0;
/** Result of operation on first number and second number. */
private int result = 0;
/** Second number used in operation. */
private int secondNumber = 0;
/** Add the two numbers. */
public void add() {
result = firstNumber + secondNumber;
}
/** Multiply the two numbers. */
public void multiply() {
result = firstNumber * secondNumber;
}
/** Clear the results. */
public void clear() {
result = 0;
}
/* ---------- properties ------------- */
public int getFirstNumber() {
return firstNumber;
}
public void setFirstNumber(int firstNumber) {
this.firstNumber = firstNumber;
}
public int getResult() {
return result;
}
public void setResult(int result) {
this.result = result;
}
public int getSecondNumber() {
return secondNumber;
}
public void setSecondNumber(int secondNumber) {
this.secondNumber = secondNumber;
}
}
清單 3 非常簡單,不需要解釋;您只需閱讀代碼。但是要記住,Calculator POJO 並不處理 JSF。
在 faces-config.xml 文件中聲明 Calculator bean
清單 4 給出完整的 faces-config.xml 文件。可以看到,這個文件與 Java EE JSF XML 模式相關聯 。在 faces-config.xml 中,使用 <managed-bean> 元素聲明一個 bean,JSF 可以綁定到這個 bean:
清單 4. 包含托管 bean 聲明的 faces-config.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"
version="1.2">
<managed-bean>
<managed-bean-name>calculator</managed-bean-name>
<managed-bean-class>com.arcmind.jsfquickstart.model.Calculator</managed-bean- class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
</faces-config>
清單 4 中的 bean 聲明用 <managed-bean-name> 元素指定 bean 的名稱 calculator。還用 <managed-bean-class> 指定完全限定的類名。這個類必須有一個無參數構造函數。
<managed-bean> 元素的 <managed-bean-scope> 子元素指定 JSF 可以在哪裡找到這個 bean:request 范圍。如果將這個 bean 名稱綁定到一個視圖(本教程後面會這麼做),而且 JSF 無法 找到它,那麼 JSF 就會創建它。這是通過 JSF 和統一 EL API 實現的。request 范圍只針對一個請求。 這是放置不需要在頁面視圖之間維持狀態的 bean 的合適位置。
創建 index.jsp 頁面
在 Calculator 應用程序中,index.jsp 頁面的用途是確保 calculator.jsp 頁面裝載 JSF 上下文, 讓頁面能夠找到對應的視圖根。清單 5 給出 index.jsp 頁面:
清單 5. index 頁面重定向到 calculator.jsp
<jsp:forward page="/faces/calculator.jsp" />
這個頁面僅僅把用戶重定向到 faces Web 上下文中的 calculator.jsp。這將 calculator.jsp 頁面 放在 JSF 上下文路徑下面,它可以在這裡找到它的視圖根。
創建 calculator.jsp 頁面
calculator.jsp 頁面是 Calculator 應用程序視圖的核心。這個頁面接受用戶輸入的兩個數字,見圖 3:
圖 3. 在 Eclipse JEE/WTP 中運行的第一個 Calculator 應用程序
這個頁面的完整代碼見清單 6:
清單 6. /src/main/webapp/calculator.jsp
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Calculator Application</title>
</head>
<body>
<f:view>
<h:form id="calcForm">
<h4>Calculator</h4>
<table>
<tr>
<td><h:outputLabel value="First Number" for="firstNumber" /></td>
<td><h:inputText id="firstNumber"
value="#{calculator.firstNumber}" required="true" /></td>
<td><h:message for="firstNumber" /></td>
</tr>
<tr>
<td><h:outputLabel value="Second Number" for="secondNumber" />
</td>
<td><h:inputText id="secondNumber"
value="#{calculator.secondNumber}" required="true" /></td>
<td><h:message for="secondNumber" /></td>
</tr>
</table>
<div>
<h:commandButton action="#{calculator.add}" value="Add" />
<h:commandButton action="#{calculator.multiply}" value="Multiply" />
<h:commandButton action="#{calculator.clear}" value="Clear" immediate="true"/>
</div>
</h:form>
<h:panelGroup rendered="#{calculator.result != 0}">
<h4>Results</h4>
<table>
<tr><td>
First Number ${calculator.firstNumber}
</td></tr>
<tr><td>
Second Number ${calculator.secondNumber}
</td></tr>
<tr><td>
Result ${calculator.result}
</td></tr>
</table>
</h:panelGroup>
</f:view>
</body>
</html>
注意,這個文件中的大多數代碼是普通的 HTML(准確地說,是 XHTML)。可以在 <f:view>、 <h:form> 和 <h:panelGroup> 標記中使用 HTML。一種常見的誤解是,不能在 JSF 標記中 混合 HTML。實際上,在許多情況下都可以這麼做。但是,不能在 <h:commandButton> 中使用 HTML,因為這個標記只接受其他組件作為子元素。
因為這個頁面有點兒復雜,我來解釋一下如何構建它。
聲明標記庫
首先聲明 JSF 的標記庫,見清單 7:
清單 7. 將標記庫導入 calculator.jsp
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
清單 7 告訴 JSP 引擎您希望使用兩個 JSF 標記庫 html 和 core。html 標記庫包含用來處理表單和 其他 HTML 相關元素的所有標記。core 標記庫包含 JSF 特有的所有邏輯、檢驗、控制器和其他標記。
<f:view> 標記
在用一般的 HTML 布置頁面之後,要告訴 JSF 系統您希望使用 JSF 管理組件。這需要使用 <f:view> 標記,這個標記告訴容器希望使用 JSF 管理其中的組件。
如果沒有 <f:view>,JSF 就無法構建組件樹,以後也無法搜索已經創建的組件樹。使用 <f:view> 標記的方式見清單 8:
清單 8. calculator.jsp 的 <f:view> 標記
<f:view>
<h:form id="calcForm">
...
</h:form>
</f:view>
清單 8 中的第一行是 <f:view> 的聲明,它告訴容器它由 JSF 管理。
在 <f:view> 標記中:<h:form> 標記
清單 8 中的第二行是 <h:form> 標記,這告訴 JSF 這裡需要一個 HTML 表單。在顯示階段, 會搜索這個表單組件中包含的組件並要求它們顯示自己,這時它們會生成標准的 HTML。可以按照您喜歡 的任何方式布置表單組件。清單 9 是 Calculator 應用程序的輸入字段的布局:
清單 9. 在 calculator.jsp 的 <h:form> 標記中:輸入字段
<table>
<tr>
<td><h:outputLabel value="First Number" for="firstNumber" /></td>
<td><h:inputText id="firstNumber"
value="#{calculator.firstNumber}" required="true" /></td>
<td><h:message for="firstNumber" /></td>
</tr>
<tr>
<td><h:outputLabel value="Second Number" for="secondNumber" />
</td>
<td><h:inputText id="secondNumber"
value="#{calculator.secondNumber}" required="true" /></td>
<td><h:message for="secondNumber" /></td>
</tr>
</table>
請再次注意,這裡使用了大量 HTML。可以用 span、div、table 或其他元素布置應用程序的布局。 JSF 對設計人員沒什麼限制。一些工具甚至允許在 Dreamweaver 中使用 JSF(參見 參考資料)。在結合 使用 JSF 和 Facelets 時,JSF 甚至對設計人員更友好(可以在 JSF 1.1 和更高版本中使用 Facelets ,而且它將成為 JSF 2.0 的組成部分)。
在清單 9 中還要注意,兩個 inputText 的 value 屬性都使用 JSF EL(JavaServer Faces Expression Language)值表達式(例如,value="#{calculator.firstNumber}")。初看上去這很像 JSTL EL。但是,統一 EL 代碼實際上將字段與對應的後端 bean 的屬性值關聯起來。這種關聯是雙向的 ;也就是說,如果 firstNumber 的值是 100,那麼在顯示表單時會顯示 100。同樣,如果用戶提交了一 個有效的值,比如 200,那麼 200 會成為 firstNumber 屬性的新值(假設通過了轉換和檢驗過程,稍後 討論這個問題)。
除了字段之外,calcForm 還通過三個 commandButton 與三個動作相關聯,見清單 10:
清單 10. 在 calculator.jsp 的 <h:form> 標記中:按鈕
<div>
<h:commandButton action="#{calculator.add}" value="Add" />
<h:commandButton action="#{calculator.multiply}" value="Multiply" />
<h:commandButton action="#{calculator.clear}" value="Clear" immediate="true"/>
</div>
清單 10 中的代碼將三個按鈕綁定到 calculator 類的 add()、multiply() 和 clear() 方法,所以 當單擊按鈕時會調用對應的方法(假設成功執行了轉換和檢驗)。
在默認情況下,在執行任何動作方法(比如 add() 或 multiply())之前,JSF 會檢驗表單。但是, 如果使用 immediate="true"(就像清單 10 中的 Clear 按鈕那樣),那麼 JSF 會跳過檢驗階段並直接 執行方法(稍後會進一步討論)。
查看結果:<h:panelGroup> 標記
最後,在 <f:view> 標記中,用 <h:panelGroup> 顯示加法和乘法操作的結果,見清單 11:
清單 11. 顯示結果
<h:panelGroup rendered="#{calculator.result != 0}">
<h4>Results</h4>
<table>
<tr><td>
First Number ${calculator.firstNumber}
</td></tr>
<tr><td>
Second Number ${calculator.secondNumber}
</td></tr>
<tr><td>
Result ${calculator.result}
</td></tr>
</table>
</h:panelGroup>
這裡仍然主要使用 HTML。注意,可以在 <h:panelGroup> 的元素體中混合 JSP 樣式表達式。 與所有 JSF 組件一樣,<h:panelGroup> 有一個呈現表達式。因此,只有當表達式 calculator.result != 0 為 true 時,才會顯示結果部分。因此,當用戶初次裝載計算器頁面時,不會 顯示這個部分。當他們輸入值時,顯示結果部分(只要結果不是零)。(這個表達式的問題在於,它把邏 輯放在視圖中。另外,如果希望允許用戶輸入 0 + 0(或 7 * 0)並顯示結果,那麼應該怎麼辦?本教程 後面會糾正這個問題。)
運行應用程序
為了運行這個應用程序,應該訪問 WAR 文件映射到的頁面(在我的機器上,這個頁面是 http://localhost:8080/calculator0/)。這會導致 index.jsp 文件在 JSF 上下文中裝載 calculator.jsp 頁面。如果使用 Eclipse WTP 並設置了服務器,那麼在 Navigator 中右鍵單擊 calculator.jsp 頁面並選擇 Run As > Run On Server 選項。
如果在 First Number 字段或 Second Number 字段中輸入無效的文本(例如,abc)並提交,那麼會 返回到 /calculator.jsp 視圖,並在對應的字段旁邊顯示一個錯誤消息。因此,只需指定字段是必需的 並將字段綁定到 int 屬性,就會在 JSF 中看到一些自動的檢驗。圖 4 顯示這個應用程序如何處理檢驗 和數據轉換錯誤:
圖 4. 檢驗和數據轉換錯誤
請注意這些古怪的錯誤消息。轉換和必需值的默認錯誤消息並不便於用戶理解:
calcForm:firstNumber: 'abc' must be a number between -2147483648 and 2147483647 Example: 9346.
calcForm:secondNumber: Validation Error: Value is required.
在下一節中,將修改這些錯誤消息。
輸入兩個值(它們的和或乘積不是零)並提交之後,結果部分就會出現,見圖 5:
圖 5. 結果面板
改進 Calculator 示例
在本節中,將用 JSF 技術改進 Calculator 應用程序的外觀並簡化它。您將學習如何使用 CSS、設置 國際化(I18N)消息和以其他方式改進應用程序的外觀和感覺。還要改進默認的錯誤消息,以便於用戶理 解。
使用面板單元格
在前一節中,使用了大量 HTML 控制頁面布局。可以使用 HTML 代碼精確地控制頁面的布局。但是, Web 應用程序的布局可能並不太重要。為了簡化 Calculator 應用程序的 GUI,現在使用一個 <h:panelGrid> 在一個單元格中布置元素。
清單 12 演示如何修改輸入表單代碼來使用 <h:panelGrid>:
清單 12. 修改表單來使用 <h:panelGrid>
<h:form id="calcForm">
<h4>Calculator</h4>
<h:panelGrid columns="3">
<%-- First Number--%>
<h:outputLabel value="First Number" for="firstNumber" />
<h:inputText id="firstNumber"
value="#{calculator.firstNumber}" required="true" />
<h:message for="firstNumber" />
<%-- Second Number--%>
<h:outputLabel value="Second Number" for="secondNumber" />
<h:inputText id="secondNumber"
value="#{calculator.secondNumber}" required="true" />
<h:message for="secondNumber" />
</h:panelGrid>
清單 13 給出對結果部分的修改:
清單 13. 修改結果部分
<h:panelGroup rendered="#{calculator.result != 0}">
<h4>Results</h4>
<h:panelGrid columns="1">
<h:outputText value="First Number #{calculator.firstNumber}"/>
<h:outputText value="Second Number #{calculator.secondNumber}"/>
<h:outputText value="Result #{calculator.result}"/>
</h:panelGrid>
</h:panelGroup>
這就減少了大約 20 行代碼(大約三分之一),使代碼更容易閱讀了。而且,現在這個應用程序的 JSF “味道” 更強了,有些人(例如大多數開發人員)喜歡這樣,有些人(Web 設計人員)不喜歡。您 應該根據自己項目的需要權衡考慮應該怎麼做。項目的情況各不相同,所以要判斷是需要絕對控制(純 HTML 布局),還是需要容易維護的應用程序(更具 JSF 風格)。
這裡有一個需要注意的問題。<h:panelGrid> 只能包含組件,而 <h:form>、 <f:view> 和 <h:panelGroup> 可以包含 HTML 和組件。在這個表單中,第一個 <h:panelGrid> 有三列,當添加一個額外組件時,它會轉入下一行。第二個 <h:panelGrid> 只包含一列,所以每個組件都會添加到下一行中。<h:panelGrid> 顯示一個表格,所以輸出幾乎與 以前相同。(對於用戶,確實是相同的。)再次重申:不能在 <h:panelGrid> 中添加 HTML。它不 會按照您的期望顯示 HTML。它只接受組件。
用 CSS 修飾 GUI
如果您了解 HTML 和 CSS,就知道如何改進 Calculator 應用程序第一個版本的外觀和感覺。 <h:panelGrid> 也允許使用 CSS。可以導入一個 CSS 樣式表,然後將它用於 <h:panelGrid>。我們要讓 <h:panelGrid> 顯示一個邊框以及交替的銀色和白色行。
首先導入樣式表,見清單 14:
清單 14. 導入樣式表
<head>
<title>Calculator Application</title>
<link rel="stylesheet" type="text/css"
href="<%=request.getContextPath()%>/css/main.css" />
</head>
清單 15 是樣式表:
清單 15. CSS 樣式表
oddRow {
background-color: white;
}
evenRow {
background-color: silver;
}
formGrid {
border: solid #000 3px;
width: 400px;
}
resultGrid {
border: solid #000 1px;
width: 200px;
}
清單 15 定義了 oddRow 和 evenRow 樣式,讓奇數行顯示為白色,偶數行顯示為銀色。
現在將這些樣式應用於 panelGrid。清單 16 將它們添加到表單 panelGrid:
清單 16. 在表單 panelGrid 中使用樣式類
<h:panelGrid columns="3" rowClasses="oddRow, evenRow"
styleClass="formGrid">
...
</h:panelGrid>
清單 16 通過設置 rowClasses="oddRow, evenRow" 屬性,將 oddRow 和 evenRow 樣式應用於表單。 styleClass="formGrid" 會在表格周圍顯示邊框。結果 <h:panelGrid> 是相似的,它顯示比較小 的邊框,見清單 17:
清單 17. 在結果 panelGrid 中使用樣式類
<h:panelGrid columns="1" rowClasses="oddRow, evenRow"
styleClass="resultGrid">
...
</h:panelGrid>
圖 6 顯示 Calculator 應用程序現在的外觀:
圖 6. 使用一些樣式之後的 Calculator
我只觸及了 <panelGrid> 支持的樣式的皮毛。
改進錯誤消息
如果您的用戶是技術專家,那麼這些錯誤消息倒是很合適。否則,用戶很難理解它們的意思。可以以 幾種方式改進它們。可以先添加一個標簽,見清單 18:
清單 18. 添加一個標簽
<h:inputText id="firstNumber" label="First Number" ... />
...
<h:inputText id="secondNumber" label="Second Number" .../>
...
注意,在 h:inputText 字段中使用了 label="First Number" 屬性。現在看到的錯誤文本像圖 7 這 樣:
圖 7. 帶標簽的消息
標簽名不再是屬性名,對用戶更友好了。但是,既然錯誤消息總是出現在字段旁邊,那麼可能根本不 需要標簽。另外,錯誤消息非常長。可以用清單 19 中的代碼縮短它們:
清單 19. 顯示簡短的消息而不是細節
<h:outputLabel value="First Number" for="firstNumber" />
<h:inputText id="firstNumber" label="First Number"
value="#{calculator.firstNumber}" required="true" />
<h:message for="firstNumber" showSummary="true" showDetail="false"/>
注意,清單 19 將 h:message 組件的 showSummary 和 showDetail 屬性設置為 showSummary="true" showDetail="false"。對於轉換和必需字段 firstNumber 和 secondNumber,這會產生 “First Number: 'aaa' must be a number consisting of one or more digits.” 和 “Second Number: Validation Error: Value is required.” 這樣的消息。但是,這仍然不夠好。下面討論一種更好的替代方法。
覆蓋消息文本
JSF 1.2 添加了 requiredMessage 和 conversionMessage,所以我們可以根據不同的情況覆蓋消息, 見清單 20:
清單 20. 使用 requiredMessage 和 converterMessge 覆蓋消息
<%-- First Number--%>
<h:outputLabel value="First Number" for="firstNumber" />
<h:inputText id="firstNumber" label="First Number"
value="#{calculator.firstNumber}" required="true"
requiredMessage="required" converterMessage="not a valid number"
/>
<h:message for="firstNumber" />
<%-- Second Number--%>
<h:outputLabel value="Second Number" for="secondNumber" />
<h:inputText id="secondNumber" label="Second Number"
value="#{calculator.secondNumber}" required="true"
requiredMessage="required" converterMessage="not a valid number"
/>
<h:message for="secondNumber" />
注意,清單 20 中的 h:inputText 添加了 requiredMessage="required" converterMessage="not a valid number"。現在看起來不錯了,而且消息在 <h:panelGrid> 的上下文中是有意義的:它們出 現在字段的旁邊,所以用戶知道它們應用於哪個上下文(見圖 8):
圖 8. 更短的消息
這種方法的問題是,需要在每個 inputText 字段中添加 requiredMessage 和 converterMessage。對 於這個簡單的示例,這倒不是問題。但是對於真正的應用程序,就會在維護方面造成大問題,肯定會破壞 DRY(don't repeat yourself)原則。
以全局方式修改消息
為了以全局方式修改消息,需要在 faces-config.xml 文件中定義一個資源束並用它重新定義默認的 消息,見清單 21:
清單 21. 在 faces-config.xml 中配置消息
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"
version="1.2">
<application>
<message-bundle>messages</message-bundle>
</application>
...
message.properties 文件包含清單 22 所示的條目:
清單 22. 消息資源束(messages.properties)
javax.faces.component.UIInput.REQUIRED_detail=required
javax.faces.converter.IntegerConverter.INTEGER_detail=not a valid number
現在已經以全局方式修改了必需字段檢驗或整數轉換失敗時顯示的消息。
注意:如果使用 Eclipse JEE,那麼一定要將 src/main/resources/messages.properties 添加為源 文件夾。
本節中所做的改進增加了應用程序的 GUI 邏輯,所以在下一節中將添加一個 CalculatorController 類,它會注入 Calculator 類。
添加控制器
現在要重構這個應用程序,不要把純 Java Calculator 對象綁定到 JSF,以避免 JSF 和 POJO 之間 有緊密聯系。這需要創建一個控制器類並把純模型對象注入這個控制器類。這個控制器類將能夠感知 JSF ,但是模型類不了解 JSF 的任何情況。
本節討論:
使用 JSF 的依賴性注入容器
處理 JSF facesContext
添加 FacesMessage
使用 h:messages
將組件綁定到控制器
下面依次執行每個步驟。然後,我會回過頭來詳細解釋每個步驟。
在 Calculator 中添加一個 divide() 方法
首先,在 Calculator 中添加一個 divide() 方法(見清單 23),以便從 “被零除” 異常中恢復並 添加一個 FacesMessage 向用戶顯示消息:
清單 23. 在 Calculator POJO 中添加一個 divide() 方法
package com.arcmind.jsfquickstart.model;
/**
* Calculator. Simple POJO.
*
* @author Rick Hightower
*/
public class Calculator {
/** First number used in operation. */
private int firstNumber = 0;
/** Result of operation on first number and second number. */
private int result = 0;
...
/** Divide the two numbers. */
public void divide() {
this.result = this.firstNumber / this.secondNumber;
}
/** Clear the results. */
public void clear () {
result = 0;
}
...
}
創建控制器類
接下來,添加一個稱為 CalculatorController 的新類,它接收 Calculator POJO。
有三個 JSF 組件綁定到 CalculatorController。它能夠感知 JSF。它還通過將 FacesMessages 放在 FacesContext 中,從異常中恢復。
綁定到 CalculatorController 的三個 JSF 組件是:
resultsPanel,這是一個 UIPanel
firstNumberInput,這是一個 UIInput
secondNumberInput,這是一個 UInput
圖 9 顯示 Calculator 應用程序如何處理錯誤:
圖 9. “被零除” 異常
圖 10 顯示應用程序如何報告狀態:
圖 10. 顯示狀態消息的結果
CalculatorController(見清單 24)避免了 JSF 相關代碼混入 POJO 中。它能夠感知 JSF,與組件 綁定,並將錯誤和狀態消息放到 facesContext 中。
清單 24. 感知 JSF 的 CalculatorController
package com.arcmind.jsfquickstart.controller;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIInput;
import javax.faces.component.UIPanel;
import javax.faces.context.FacesContext;
import com.arcmind.jsfquickstart.model.Calculator;
public class CalculatorController {
private Calculator calculator;
private UIPanel resultsPanel;
private UIInput firstNumberInput;
private UIInput secondNumberInput;
public String add() {
FacesContext facesContext = FacesContext.getCurrentInstance();
try {
calculator.add();
resultsPanel.setRendered(true);
facesContext.addMessage(null, new FacesMessage(
FacesMessage.SEVERITY_INFO, "Added successfully", null));
} catch (Exception ex) {
resultsPanel.setRendered(false);
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, ex.getMessage(), null));
}
return null;
}
public String multiply() {
FacesContext facesContext = FacesContext.getCurrentInstance();
try {
calculator.multiply();
resultsPanel.setRendered(true);
facesContext.addMessage(null, new FacesMessage(
FacesMessage.SEVERITY_INFO, "Multiplied successfully", null));
} catch (Exception ex) {
resultsPanel.setRendered(false);
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, ex.getMessage(), null));
}
return null;
}
public String divide() {
FacesContext facesContext = FacesContext.getCurrentInstance();
try {
calculator.divide();
resultsPanel.setRendered(true);
facesContext.addMessage(null, new FacesMessage(
FacesMessage.SEVERITY_INFO, "Divided successfully", null));
} catch (Exception ex) {
resultsPanel.setRendered(false);
if (ex instanceof ArithmeticException) {
secondNumberInput.setValue(Integer.valueOf(1));
}
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, ex.getMessage(), null));
}
return null;
}
public String clear() {
FacesContext facesContext = FacesContext.getCurrentInstance();
try {
calculator.clear();
resultsPanel.setRendered(false);
facesContext.addMessage(null, new FacesMessage(
FacesMessage.SEVERITY_INFO, "Results cleared", null));
} catch (Exception ex) {
resultsPanel.setRendered(false);
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, ex.getMessage(), null));
}
return null;
}
public String getFirstNumberStyleClass() {
if (firstNumberInput.isValid()) {
return "labelClass";
} else {
return "errorClass";
}
}
//remove simple props
更新 calculator.jsp
接下來,更新 calculator.jsp(見清單 25),讓它顯示錯誤消息並綁定到 calculatorController, 而不是直接綁定到 Calculator POJO:
清單 25. 更新後的 calculator.jsp
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Calculator Application</title>
<link rel="stylesheet" type="text/css"
href="<%=request.getContextPath()%>/css/main.css" />
</head>
<body>
<f:view>
<h:form id="calcForm">
<h4>Calculator 3</h4>
<h:messages infoClass="infoClass" errorClass="errorClass"
layout="table" globalOnly="true"/>
<h:panelGrid columns="3" rowClasses="oddRow, evenRow"
styleClass="formGrid">
<%-- First Number--%>
<h:outputLabel value="First Number" for="firstNumber"
styleClass="#{calculatorController.firstNumberStyleClass}"/>
<h:inputText id="firstNumber" label="First Number"
value="#{calculatorController.calculator.firstNumber}" required="true"
binding="#{calculatorController.firstNumberInput}" />
<h:message for="firstNumber" errorClass="errorClass"/>
<%-- Second Number--%>
<h:outputLabel id="snl" value="Second Number" for="secondNumber"
styleClass="#{calculatorController.secondNumberStyleClass}"/>
<h:inputText id="secondNumber" label="Second Number"
value="#{calculatorController.calculator.secondNumber}" required="true"
binding="#{calculatorController.secondNumberInput}"/>
<h:message for="secondNumber" errorClass="errorClass"/>
</h:panelGrid>
<div>
<h:commandButton action="#{calculatorController.add}" value="Add" />
<h:commandButton action="#{calculatorController.multiply}" value="Multiply" />
<h:commandButton action="#{calculatorController.divide}" value="Divide" />
<h:commandButton action="#{calculatorController.clear}" value="Clear"
immediate="true"/>
</div>
</h:form>
<h:panelGroup binding="#{calculatorController.resultsPanel}" rendered="false">
<h4>Results</h4>
<h:panelGrid columns="1" rowClasses="oddRow, evenRow"
styleClass="resultGrid">
<h:outputText value="First Number # {calculatorController.calculator.firstNumber}"/>
<h:outputText value="Second Number # {calculatorController.calculator.secondNumber}"/>
<h:outputText value="Result #{calculatorController.calculator.result}"/>
</h:panelGrid>
</h:panelGroup>
</f:view>
</body>
</html>
在 faces-config.xml 中映射控制器
接下來,需要在 faces-config.xml 中映射新的控制器並在其中注入 calculator,見清單 26:
清單 26. 更新後的 faces-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"
version="1.2">
<application>
<message-bundle>messages</message-bundle>
</application>
<managed-bean>
<managed-bean-name>calculatorController</managed-bean-name>
<managed-bean-class>
com.arcmind.jsfquickstart.controller.CalculatorController
</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>calculator</property-name>
<value>#{calculator}</value>
</managed-property>
</managed-bean>
<managed-bean>
<managed-bean-name>calculator</managed-bean-name>
<managed-bean-class>
com.arcmind.jsfquickstart.model.Calculator
</managed-bean-class>
<managed-bean-scope>none</managed-bean-scope>
</managed-bean>
</faces-config>
既然已經修改了整個應用程序,現在就討論一下細節。
用 JSF 進行依賴性注入
JSF 支持依賴性注入。可以將 bean 注入其他 bean 的屬性。因為要將 calculator bean 注入 calculatorController,所以可以把它放到 none 范圍中。none 意味著在創建它時不把它放到范圍中。 清單 27 給出 faces-config.xml 的部分代碼,這些代碼注入托管 calculator bean,並使用 none 范圍 :
清單 27. 托管的 calculator,none 范圍
<managed-bean>
<managed-bean-name>calculator</managed-bean-name>
<managed-bean-class>
com.arcmind.jsfquickstart.model.Calculator
</managed-bean-class>
<managed-bean-scope>none</managed-bean-scope>
</managed-bean>
calculatorController 在 request 范圍下映射。將 calculator 注入 calculatorController 的方 法是使用 <managed-property> 並傳遞表達式 #{calculator}。這會創建一個 Calculator 對象並 使用 CalculatorController 的 setCalculator 方法把它注入 CalculatorController,見清單 28:
清單 28. 托管的 calculatorController,request 范圍,用 managed-property 注入
<managed-bean>
<managed-bean-name>calculatorController</managed-bean-name>
<managed-bean-class>
com.arcmind.jsfquickstart.controller.CalculatorController
</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>calculator</property-name>
<value>#{calculator}</value>
</managed-property>
</managed-bean>
CalculatorController 要使用 calculator,所以注入了 calculator。這樣就可以使用 calculator 並讓它與 JSF 相互隔離,這是良好的模型對象應該具備的性質。JSF 相關代碼只出現在 CalculatorController 中。這種良好的關注點隔離會使代碼的可測試性和可重用性更好。
CalculatorController 的 JSF 綁定組件
根據設計,CalculatorController 了解 JSF 的許多情況。CalculatorController 綁定三個 JSF 組 件,其中之一是 resultsPanel,它代表顯示計算器結果的面板,見清單 29:
清單 29. CalculatorController 的 resultsPanel
private UIPanel resultsPanel;
...
public UIPanel getResultsPanel() {
return resultsPanel;
}
public void setResultsPanel(UIPanel resultPanel) {
this.resultsPanel = resultPanel;
}
resultsPanel 通過 JSF 綁定到 CalculatorController,見清單 30 中的 binding 屬性:
清單 30. 把組件綁定到控制器
<h:panelGroup binding="#{calculatorController.resultsPanel}" rendered="false">
<h4>Results</h4>
<h:panelGrid columns="1" rowClasses="oddRow, evenRow"
styleClass="resultGrid">
<h:outputText value="First Number # {calculatorController.calculator.firstNumber}"/>
<h:outputText value="Second Number # {calculatorController.calculator.secondNumber}"/>
<h:outputText value="Result #{calculatorController.calculator.result}"/>
</h:panelGrid>
/h:panelGroup>
在清單 30 中,binding="#{calculatorController.resultsPanel}" 通過綁定關聯 resultsPanel 組 件。實際上,JSF 會看到這個表達式,在裝載頁面時,它通過調用 calculateController.setResultsPanel 方法注入 resultsPanel 組件。這個方便的機制讓我們能夠以程 序方式操作組件的狀態,不需要在組件樹中移動。
實際上,JSF 所做的操作是調用 calculateController.getResultsPanel。如果這個調用返回一個組 件,JSF 視圖就會使用這個組件。如果 calculateController.getResultsPanel 返回 null,JSF 就會創 建 resultPanel 組件,然後用基於綁定表達式的新組件調用 calculateController.setResultPanel。
清單 31 演示 CalculateController 的 add() 方法如何使用這種技術:
清單 31. CalculateController 的 add() 方法,關閉 resultsPanel
public String add() {
...
try {
calculator.add();
resultsPanel.setRendered(true);
...
} catch (Exception ex) {
...
resultsPanel.setRendered(false);
}
return null;
}
在清單 31 中,如果對 calculator.add 方法的調用成功,CalculateController.add 方法會調用 resultsPanel.setRendered(true),這會打開結果面板。如果調用失敗,那麼 CalculateController.add 調用 resultsPanel.setRendered(false),這個面板不再顯示。
現在稍微停一下。請注意:因為 JSF 是一個組件模型,而且組件是有狀態的,所以組件會記住它們的 狀態。不需要像前面的 Calculator 示例那樣包含邏輯。告訴組件不要顯示其本身,它就不會再顯示了。 如果告訴組件禁用其本身,那麼只要視圖是活動的,每次裝載視圖時這個組件都會被禁用。與 Model 2 框架相比,JSF 更接近傳統的 GUI 應用程序。需要編寫的代碼更少,就可以更快地開發 Web 應用程序。 JSF 使Web 應用程序 真正體現了應用程序 的思想。
CalculatorController 處理消息
JSF 提供一種向用戶顯示狀態消息的機制。CalculateController 使用 FacesContext 將消息添加到 FacesContext 中,這樣就可以用 <h:messages> 標記向用戶顯示這些消息。
JSF 在 ThreadLocal 變量中存儲一個 FacesContext,可以通過調用 FacesContext.getCurrentInstance() 方法訪問它。add() 方法使用當前的 FacesContext 添加消息,這 些消息可供當前請求使用,見清單 32:
清單 32. CalculateController 的 add() 方法添加 JSF 消息
public String add() {
FacesContext facesContext = FacesContext.getCurrentInstance();
try {
calculator.add();
facesContext.addMessage(null, new FacesMessage(
FacesMessage.SEVERITY_INFO, "Added successfully", null));
...
} catch (Exception ex) {
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, ex.getMessage(), null));
//Log the exception as well.
...
}
return null;
}
在清單 32 中,如果添加操作成功,就在 facesContext 中添加一個嚴重性級別為 INFO 的 FacesMessage;如果添加操作拋出異常,就添加一個嚴重性級別為 ERROR 的 FacesMessage。
用 <h:messages> 標記向用戶顯示消息,見清單 33:
清單 33. 向最終用戶顯示錯誤和狀態消息
<h:messages infoClass="infoClass" errorClass="errorClass"
layout="table" globalOnly="true"/>
如果將 globalOnly 屬性設置為 true,就只顯示不與特定組件連接的消息,比如在清單 32 中添加的 消息。注意,狀態消息和錯誤消息使用不同的樣式。
CalculatorController 糾正 “被零除” 異常
因為我們正在使用一個組件模型,所以可以根據顯示邏輯修改組件的值並進行初始化。當新的除法方 法拋出 “被零除” 異常時,可以通過將 secondNumberInput 值設置為 1 來恢復。
首先,需要將 secondNumberInput 綁定到 CalculatorController 類,見清單 34:
清單 34. 綁定輸入組件:binding="#{calculatorController.resultsPanel}"
<h:inputText id="secondNumber" label="Second Number"
value="#{calculatorController.calculator.secondNumber}" required="true"
binding="#{calculatorController.secondNumberInput}"/>
接下來,使用 secondNumberInput 組件。如果遇到 “被零除” 異常,就將 secondNumberInput 值 設置為 1,見清單 35:
清單 35. 新的 divide() 方法
public String divide() {
FacesContext facesContext = FacesContext.getCurrentInstance();
try {
calculator.divide();
facesContext.addMessage(null, new FacesMessage(
FacesMessage.SEVERITY_INFO, "Divided successfully", null));
resultsPanel.setRendered(true);
} catch (Exception ex) {
if (ex instanceof ArithmeticException) {
secondNumberInput.setValue(Integer.valueOf(1));
}
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, ex.getMessage(), null));
}
return null;
}
一定要認識到 JSF 更接近傳統的 GUI 組件模型,而不是 Model 2 的特殊版本。如果您一直牢記 JSF 是一個組件模型,就會發現許多可能性。在清單 35 中,可以設置 secondNumberInput 的值,這是因為 它是一個對象,而不是 JSP 中的 HTML 代碼。您可以操作它,它會記住它的值。它是有狀態的。
處理屬性
大多數 JSF 屬性接受表達式,所以如果在發生錯誤時希望將字段標簽變成紅色的,那麼很容易實現, 見清單 36:
清單 36. 將標簽變成紅色
<%-- First Number--%>
<h:outputLabel value="First Number" for="firstNumber"
styleClass="#{calculatorController.firstNumberStyleClass}"/>
...
注意,styleClass 屬性被設置為表達式 #{calculatorController.firstNumberStyleClass},這與清 單 37 中的方法綁定:
清單 37. 如果發生錯誤,就返回紅色的樣式類
public String getFirstNumberStyleClass() {
if (firstNumberInput.isValid()) {
return "labelClass";
} else {
return "errorClass";
}
}
清單 37 檢查 firstNumbedInput 組件的輸入是否有效,然後根據檢查的結果修改返回的 styleClass 。
JSF 中的導航
JSF 有一個導航機制(與 Struts 相似)。JSF 的導航機制提供邏輯結果,可以將邏輯結果映射到下 一個邏輯視圖。在本節中,我們將在 Calculator 應用程序中添加導航。
導航規則
圖 11 顯示將在 Calculator 應用程序中添加的導航規則:
圖 11. 在 Calculator 應用程序中添加的導航規則
可以用工具幫助布置 Web 應用程序的流程。許多 IDE 提供了用來繪制 JSF 應用程序的導航規則的工 具。圖 12 顯示 Eclipse JEE 中的 Navigation Rule Layout Tool:
圖 12. Eclipse 中的導航規則布局
在學完本節之後,圖 11 和圖 12 的意義就會明確了。
首先添加一個鏈接到計算器頁面的主頁。然後,將計算器頁面分割為兩個頁面:一個頁面顯示計算器 視圖,一個頁面顯示結果視圖。還需要通過導航規則在計算器頁面、結果頁面和主頁之間來回移動。
從主頁鏈接到計算器頁面
可以通過三種方式從主頁鏈接到計算器頁面:
通過一個 commandLink 和一個導航規則
通過一個 commandLink 和一個使用重定向的導航規則
通過一個 outputLink
通過 commandLink 和導航規則進行鏈接需要在 faces-config.xml 文件中添加一個導航規則,見清單 38:
清單 38. faces-config.xml 中定義的導航規則
<navigation-rule>
<navigation-case>
<from-outcome>CALCULATOR</from-outcome>
<to-view-id>/pages/calculator.jsp</to-view-id>
</navigation-case>
</navigation-rule>
清單 38 聲明,返回 CALCULATOR 的任何動作將導致 JSF 裝載 /pages/calculator.jsp 作為下一個 視圖。這個規則是全局的,也就是說,如果在應用程序中任何地方的任何動作返回了 CALCULATOR,就會 進入 /pages/calculator.jsp(除非應用了更特定的規則 — 在 JSF 中,可以添加 <from-view- id>,讓規則只應用於這個視圖;本節後面會討論更特定的規則。)清單 38 與 Struts 中的全局轉發 相似。
然後,添加清單 39 所示的 commandLink,它必須放在 <h:form> 中:
清單 39. 在主頁中使用 commandLink
<h:form>
<h:panelGrid columns="1">
<h:commandLink action="CALCULATOR" value="Calculator Application"/>
...
這可以實現所需的效果:在浏覽器中裝載 Calculator 應用程序。但是,盡管顯示了計算器視圖,而 浏覽器中的 URL 仍然是 http://localhost:8080/calculator3/home.jsf,這可能讓用戶覺得困惑。這可 能使用戶很不安,尤其是熟悉 Web 的用戶。另外,用戶可能希望用應用程序的計算器部分設置書簽,但 是他們不知道真正的 URL。
糾正這個問題的一種方法是在 faces-config.xml 導航規則中使用 redirect,見清單 40:
清單 40. 包含 redirect 元素的導航規則
<navigation-rule>
<navigation-case>
<from-outcome>CALCULATOR_REDIRECT</from-outcome>
<to-view-id>/pages/calculator.jsp</to-view-id>
<redirect/> <!-- LOOK HERE -->
</navigation-case>
</navigation-rule>
commandLink 使用這個導航規則的方法是指定結果字符串作為 action 屬性。當用戶單擊清單 41 中 的鏈接時,用戶會進入計算器頁面:
清單 41. 使用包含 redirect 元素的導航規則
<h:commandLink action="CALCULATOR_REDIRECT" value="Calculator Application (redirect)"/>
這解決了上述問題,但是增加了一次服務器訪問,這在慢速連接上可能要花費較長時間。但是,如果 正在構建內部應用程序,那麼這次訪問花不了多長時間。
如果不介意直接鏈接要裝載的頁面,就不需要兩次訪問服務器,見清單 42:
清單 42. 用 outputLink 直接鏈接
<h:outputLink value="pages/calculator.jsf">
<h:outputText value="Calculator Application (outputlink)"/>
</h:outputLink>
清單 42 直接鏈接下一個視圖。在 Model 2 體系結構和 JSF 中,從一個視圖直接鏈接下一個視圖被 認為是一種不好的做法。在通常情況下,控制器應該有機會為下一個視圖初始化模型,所以更好的方法是 通過動作方法。但是,清單 42 確實創建了一個鏈接,而且在浏覽器中會顯示正確的 URL。
無法給 JSF 應用程序加書簽一直是個問題。幾個框架解決了這個問題,包括 JBoss Seam。JSF 2 也 將解決這個小問題。
導航到結果頁面
在計算器頁面中執行計算之後,下一個視圖應該是結果頁面。為此,需要添加清單 43 中的導航規則 :
清單 43. 從計算器視圖中所有動作到結果頁面的導航規則
<navigation-rule>
<display-name>Calculator View</display-name>
<from-view-id>/pages/calculator.jsp</from-view-id>
<navigation-case>
<from-outcome>results</from-outcome>
<to-view-id>/pages/results.jsp</to-view-id>
</navigation-case>
</navigation-rule>
清單 43 聲明,如果當前視圖是計算器視圖(calculator.jsp),而且任何動作返回 results,那麼 下一個 JSF 視圖應該是結果頁面(results.jsp)。JSF 查看動作方法的返回值,將它們轉換為字符串( 如果它們不是 null 的話),然後使用這個字符串選擇下一個視圖。返回類型可以是任何對象,因為將調 用它的 toString 方法。(許多人使用 Enum。)
將所有操作改為返回 results,就像清單 44 中的 add() 方法這樣:
清單 44. 動作方法現在返回 results
public String add() {
FacesContext facesContext = FacesContext.getCurrentInstance();
try {
calculator.add();
facesContext.addMessage(null, new FacesMessage(
FacesMessage.SEVERITY_INFO, "Added successfully", null));
} catch (Exception ex) {
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, ex.getMessage(), null));
}
return "results";
}
注意,add() 方法現在返回 results,而 results 映射到 results.jsp。注意,Cancel 按鈕不返回 results。如果一個動作返回的值沒有映射到導航規則,JSF 就留在當前視圖上。
可以以更特定的方式指定規則,見清單 45:
清單 45. 與動作匹配的映射
<navigation-rule>
<display-name>Calculator View</display-name>
<from-view-id>/pages/calculator.jsp</from-view-id>
<navigation-case>
<from-action>#{calculatorController.add}</from-action>
<from-outcome>results</from-outcome>
<to-view-id>/pages/results.jsp</to-view-id>
</navigation-case>
</navigation-rule>
但是,現在需要為每個方法設置一個映射。這不符合 DRY 原則。
還希望在計算器頁面中添加一個返回主頁的 commandButton,見清單 46:
清單 46. 包含返回主頁鏈接的計算器動作
<div>
<h:commandButton action="#{calculatorController.add}" value="Add" />
<h:commandButton action="#{calculatorController.multiply}" value="Multiply" />
<h:commandButton action="#{calculatorController.divide}" value="Divide" />
<h:commandButton action="#{calculatorController.clear}" value="Clear" immediate="true"/>
<h:commandButton action="HOME" value="Home" immediate="true"/>
</div>
注意,主頁鏈接指定的動作值是 HOME。清單 47 所示的導航規則將主頁鏈接使用的 HOME 值映射到主 頁:
清單 47. 主頁映射導航規則
<navigation-rule>
<navigation-case>
<from-outcome>HOME</from-outcome>
<to-view-id>/home.jsp</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
注意,commandButton 和 commandLink 采用相同的動作處理和導航規則。
結果頁面有返回計算器頁面和主頁的鏈接,見清單 48:
清單 48. 可以從結果頁面返回計算器頁面和主頁
<h:panelGrid columns="1" rowClasses="oddRow, evenRow">
<h:commandLink action="calculator" value="Return to the calculator page"/>
<h:commandLink action="HOME" value="Go to the home page"/>
<h:commandLink action="calculatorMain" value="Go to main calculator page"/>
</h:panelGrid>
可以以兩種方式返回計算器頁面。一種方式與前面看到的相似,見清單 49:
清單 49. 返回計算器頁面的映射
<navigation-rule>
<display-name>Results Page</display-name>
<from-view-id>/pages/results.jsp</from-view-id>
<navigation-case>
<from-outcome>calculator</from-outcome>
<to-view-id>/pages/calculator.jsp</to-view-id>
</navigation-case>
</navigation-rule>
清單 49 聲明,如果當前在結果頁面(/pages/results.jsp)上而且一個動作返回 calculator,那麼 進入計算器頁面(/pages/calculator.jsp)。如果不希望采用清單 49 這麼特定的方式,也不希望采用 全局轉發,那麼可以使用清單 50:
清單 50. 從 pages/* 下的任何地方返回計算器頁面的映射
<navigation-rule>
<from-view-id>/pages/*</from-view-id>
<navigation-case>
<from-outcome>calculatorMain</from-outcome>
<to-view-id>/pages/calculator.jsp</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
可以使用清單 50 這樣的方式定義應用程序中的邏輯區域,並讓結果只應用於這些位置。
結束語
本教程討論了 JSF 並強調它是一個組件模型。因為 JSF 與 Swing、SWT 或 AWT 一樣是一個組件框架 ,它使 Web 應用程序開發更接近傳統的 GUI 開發,而不像傳統的 Web 開發。與用典型的 Model 2 框架 編寫的應用程序相比,用 JSF 編寫的應用程序更短,更容易理解和維護。因此,Java 社區對 JSF 很有 興趣,圍繞 JSF 展開的工作正在不斷增加。
正在開發的 JSF 2 將結合一些 Facelets 概念,添加本機 Ajax 支持,並使 JSF 組件開發更加簡便 。JSF 2 應該會進一步促進開發人員對 JSF 的熱情。新的模型能夠通過 Ajax 進行部分頁面顯示,這可 以通過 Ajax4JSF 等工具實現。
但是,JSF 並非沒有競爭對手。在各種服務器端組件模型中,Tapestry 5 看起來很有潛力,但是它與 Tapestry 4 不兼容。Wicket 也非常有趣,但是還不足以引起許多開發人員的關注。
還有非組件的服務器端 Java 框架。Struts 2.x 已經在改進 WebWork 方面取得了出色的成果,有些 人希望 Struts 2.1.x 進一步發展,但是 Struts 2 實際上基於 WebWork 而不是 Struts 1.x。Spring MVC 正在迅速發展,如果您需要使用非 GUI 組件的服務器端 Web 框架,它是不錯的選擇。
最後,還有將工作委托給服務器上的服務的純客戶端框架,比如 Google Web Toolkit(GWT)和 Adobe Flex。(這種體系結構與 JSF 不一樣,但是目標應用程序是相同的。)它們各有優缺點,這可能 會影響 JSF 的推廣。
但是,JSF 的前景還是不錯的,因為它是 Java EE 的標准,而且它背後有一個活躍的社區。業界對 JSF 的期望超過對 Tapestry、Spring MVC、Java Flex 和 GWT 的期望。JSF 2.0 會進一步促進 JSF 的 發展。
本教程系列的第 2 部分將討論 JSF 生命周期、檢驗器、轉換器、階段監聽器和其他高級 JSF 特性。
本文配套源碼