開發(fā)基于 JSF 的 Web 應(yīng)用程序,這些應(yīng)用程序使用 Oracle TopLink 和 JSTL 的 SQL 標(biāo)記更新和查詢關(guān)系數(shù)據(jù)庫
JavaServer Faces (JSF) 是一個人們期待已久的標(biāo)準(zhǔn) Java 技術(shù),用于創(chuàng)建基于 Web 的用戶界面。本文將介紹如何構(gòu)建 JSF 表單、如何使用 JSF 驗(yàn)證表單數(shù)據(jù)、如何實(shí)現(xiàn)訪問數(shù)據(jù)庫的 JSF 操作以及如何使用 JSF 呈現(xiàn) SQL 結(jié)果集。可以使用低級 API (JDBC)、對象關(guān)系 (O-R) 映射框架(如 Oracle TopLink)或 JSP 標(biāo)記庫(例如,JSTL)更新和查詢關(guān)系數(shù)據(jù)庫。本文通篇介紹了這些選件以及主要的 JSF 特性。
應(yīng)用程序概述
JSF 并非僅是另一個用于 Web 開發(fā)的標(biāo)記庫。它是一個基于 MVC 的框架,用于控制 Web 表單和管理 JavaBean。JSF 提供了一組有限的標(biāo)準(zhǔn) UI 組件,但您可以使用組件庫(如 Oracle 應(yīng)用程序開發(fā)框架 (ADF) Faces),還可以構(gòu)建您自己的定制組件。注意,由于所有這些 UI 組件均基于標(biāo)準(zhǔn)的 JSF API,因此它們可以在同一網(wǎng)頁中一起使用。
JSF 頁面是常規(guī)的 JSP,它們使用標(biāo)準(zhǔn) JSF 標(biāo)記庫和/或其他基于 JSF API 的庫。執(zhí)行 JSF 頁面時,JSF 框架嘗試獲取或恢復(fù)所謂的“組件樹”(或“視圖”)。它包含 Web 頁面組件的信息、必須執(zhí)行的數(shù)據(jù)轉(zhuǎn)換和驗(yàn)證、UI 狀態(tài)必須存儲到的 bean 屬性、注冊的事件監(jiān)聽器等。JSF 創(chuàng)建組件樹(如果它不存在),然后將它保存到客戶端(使用隱藏的表單域)或服務(wù)器(作為會話屬性)以備后續(xù)請求使用。
本文提供了一個供用戶訂閱幾個時事通訊的示例 Web 應(yīng)用程序。訂戶通過提供他們的電子郵件地址、姓名和首選項(xiàng)進(jìn)行注冊。他們還必須選擇一個口令,以便以后可以更改他們的配置文件。
圖 1 顯示了訂閱表單。該示例應(yīng)用程序不分發(fā)任何時事通訊,而實(shí)際的應(yīng)用程序?qū)⑾蚬芾碚摺㈤_發(fā)人員和管理員發(fā)送不同的時事通訊。(同一訂戶可能收到多個時事通訊。)該示例應(yīng)用程序?qū)⒂脩襞渲梦募4娴疥P(guān)系數(shù)據(jù)庫中,并允許訂戶更改他們的配置文件。
 |
圖 1:“訂閱”窗體 |
應(yīng)用程序配置
為使 JSF 控制您的 Web 表單,必須在 web.xml 應(yīng)用程序描述文件中配置 Faces Servlet。您的 Web 頁面將具有 .jsp 擴(kuò)展名,但 URL 將使用 .faces 后綴或 /faces/ 前綴。必須將 JSF 控制器 servlet 映射到 web.xml 文件中的 *.faces 或 /faces/*。本文提供的示例應(yīng)用程序使用后綴映射,并將 JSF 視圖保存到客戶端上:
<web-app>
...
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<servlet>
<servlet-name>FacesServlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>FacesServlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>
...
</web-app>
模型視圖分離
Java Web 應(yīng)用程序(包括那些基于 JSF 的應(yīng)用程序)使用 JSP 和 JavaBean 將表現(xiàn)形式與應(yīng)用程序邏輯分離。使用類似 JSP 2.0 EL 的表達(dá)式語言 (EL) 可以將 JSF 標(biāo)記的屬性綁定到 bean 屬性(有時為 bean 方法)。JSF EL 通常使用 #{...}語法(而非 ${...} 構(gòu)造),以便用于 JSP 1.2 和 JSP 2.0。它還允許 JSF 在任何必要的情況下求解表達(dá)式(和重新求解),而不是讓 JSP 容器控制表達(dá)式的求解。在有意義的情況下,JSF EL 綁定是雙向的。例如,UI 組件可以取得 bean 屬性的值并將其作為默認(rèn)值提供給用戶。當(dāng)用戶提交表單數(shù)據(jù)時,此 UI 組件會更新 bean 屬性,以便應(yīng)用程序邏輯可以處理該新值。
JavaBean(模型)、JSF 頁面(視圖)和 Faces Servlet(控制器)具有定義良好的角色,從而分離了對象模型、表現(xiàn)方式和請求處理。您可以更深入一步,將 UI 特有的 Java 代碼與其實(shí)例維護(hù)和處理數(shù)據(jù)的類分離。一個可行的解決方案是構(gòu)建對用戶界面一無所知的模型 bean,然后使用 JSF 特有的方法(如操作和驗(yàn)證器,本文稍后將對其進(jìn)行介紹)擴(kuò)展這些模型類。
該示例應(yīng)用程序有兩個模型 bean(LoginInfo 和 Subscriber),它們由兩個視圖 bean(LoginInfoBean 和 SubscriberBean)擴(kuò)展,JSF 對這兩個視圖 bean 的實(shí)例進(jìn)行管理。每個視圖 bean 實(shí)例都有一個標(biāo)識符和一個作用域(您可以在表 1 中找到它們)。請注意,JSF 規(guī)范在提到視圖 bean 時使用了術(shù)語“托管 bean”(managed bean) 或“輔助 bean”(backing bean)。JSF 對“模型 bean”和“視圖 bean”不作區(qū)分。實(shí)際上,您可能將應(yīng)用程序邏輯和 UI 代碼置于同一個類中,但這可能會降低類的可重用性,使維護(hù)變得更加困難,并且指定的開發(fā)人員無法將精力集中于他們的主要任務(wù)(如應(yīng)用程序邏輯或 Web 設(shè)計)。
模型 bean 類 |
視圖 bean 類 |
Bean 標(biāo)識符 |
JSF 作用域 |
LoginInfo |
LoginInfoBean |
loginInfo |
request |
Subscriber |
SubscriberBean |
subscriber |
session |
表 1:示例應(yīng)用程序的模型 bean 和視圖 bean
常規(guī) JSP 頁面使用 <jsp:useBean> 標(biāo)準(zhǔn)操作實(shí)例化 JavaBean。使用 JSF 框架時,您不必在網(wǎng)頁中指定類名。而是在 XML 文件(通常名為 faces-config.xml)中配置 bean 實(shí)例。.(如果您開發(fā)大型應(yīng)用程序,則可能使用多個配置文件。這種情況下,必須在 web.xml 文件中添加一個 javax.faces.CONFIG_FILES 參數(shù)。)對于每個 bean 實(shí)例,您必須指定標(biāo)識符(bean 名)、類名和一個有效的 JSF 作用域(application、session、request 或 none)。當(dāng)在 JSF 表達(dá)式 (#{...}) 中引用 bean 時,JSF 框架將驗(yàn)證該 bean 實(shí)例在給定作用域中是否存在,如果未找到,則創(chuàng)建它并使用默認(rèn)值(這些默認(rèn)值也可以在 JSF 配置文件中指定)對其進(jìn)行初始化:
<faces-config>
...
<managed-bean>
<managed-bean-name>loginInfo</managed-bean-name>
<managed-bean-class>jsfdb.view.LoginInfoBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>subscriber</managed-bean-name>
<managed-bean-class>jsfdb.view.SubscriberBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>email</property-name>
<null-value/>
</managed-property>
...
<managed-property>
<property-name>subscriptionType</property-name>
<value>1</value>
</managed-property>
</managed-bean>
...
</faces-config>
由于可以將未聲明的屬性綁定到 JSF 頁面的 UI 組件,因此不必在 JSF 配置文件中將每個 bean 屬性聲明為托管屬性。僅當(dāng)您要使用默認(rèn)值初始化 bean 屬性時,才需要在 faces-config.xml 文件中指定它們。
Bean 與頁面的關(guān)系
數(shù)據(jù)訪問方法(在下一部分中介紹)從視圖 bean 的操作方法中調(diào)用。每個操作方法與 JSP 頁面中的一個提交按鈕綁定(如表 2 所示)。當(dāng)用戶單擊該按鈕時,Web 瀏覽器將表單數(shù)據(jù)發(fā)送給服務(wù)器。JSF 框架驗(yàn)證表單數(shù)據(jù),并在出錯情況下將該表單返回給用戶。否則,則將有效的表單數(shù)據(jù)存儲到托管 bean 的屬性中,且 JSF 將調(diào)用綁定到被單擊按鈕的操作方法。因此,您的操作方法應(yīng)該實(shí)現(xiàn)應(yīng)用程序邏輯,以處理該 bean 的屬性。
數(shù)據(jù)訪問方法 |
托管 bean 和 JSF 操作 |
JSF 頁面 |
SubscriberDAO.select() |
LoginInfoBean.loginAction() |
login.jsp |
SubscriberDAO.insert() |
SubscriberBean.subscribeAction() |
subscribe.jsp |
SubscriberDAO.update() |
SubscriberBean.profileAction() |
profile.jsp |
SubscriberDAO.delete() |
SubscriberBean.unsubscribeAction() |
unsubscribe.jsp |
表 2:綁定到 JSF 頁面中的提交按鈕的 JSF 操作使用的 DAO 方法
頁面到頁面的導(dǎo)航
每個操作方法都返回一個名為“結(jié)果”的字符串。JSF 使用導(dǎo)航處理程序確定對每個結(jié)果所執(zhí)行的操作。如果操作方法返回 null,則必須重新顯示同一頁面。否則,會根據(jù)返回的結(jié)果顯示其他頁面。表 3 包含該示例 Web 應(yīng)用程序的 JSF 表單、可能的結(jié)果以及針對每個結(jié)果顯示的頁面。
JSF 表單 |
可能的結(jié)果 |
顯示的頁面 |
subscribe.jsp |
subscribed |
subscribed.jsp |
login.jsp |
profile |
profile.jsp |
login.jsp |
list |
list.jsp |
profile.jsp |
login |
login.jsp |
unsubscribe.jsp |
login |
login.jsp |
unsubscribe.jsp |
unsubscribed |
unsubscribed.jsp |
unsubscribe.jsp |
cancel |
profile.jsp |
表 3:當(dāng)用戶單擊表單的提交按鈕時,JSF 將驗(yàn)證表單數(shù)據(jù),
并在未出錯的情況下,調(diào)用綁定到被單擊按鈕的操作方法。然后,JSF 將使用該操作方法返回的結(jié)果
確定接下來應(yīng)顯示的頁面。
默認(rèn) JSF 導(dǎo)航處理程序使用在 JSF 配置文件中指定的一組導(dǎo)航規(guī)則。例如,當(dāng)用戶填寫登錄表單 (login.jsp) 并單擊 Login 按鈕時,loginAction() 方法會根據(jù)登錄用戶的角色(訂戶或管理員)返回 profile 或 list。請注意,如果用戶是未知的或者口令錯誤,則 loginAction() 將返回 null。這種情況下,JSF 將顯示同一登錄表單。如果身份驗(yàn)證成功,則 JSF 將根據(jù) loginAction() 返回的結(jié)果將該請求轉(zhuǎn)給 profile.jsp 或 list.jsp。以下是 faces-config.xml 中規(guī)定的導(dǎo)航規(guī)則:
<faces-config>
...
<navigation-rule>
<from-view-id>/login.jsp</from-view-id>
<navigation-case>
<from-outcome>profile</from-outcome>
<to-view-id>/profile.jsp</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>list</from-outcome>
<to-view-id>/list.jsp</to-view-id>
</navigation-case>
</navigation-rule>
...
</faces-config>
JavaBean 和數(shù)據(jù)訪問對象 (DAO)
本部分介紹了如何創(chuàng)建模型 bean 和視圖 bean,以及如何實(shí)現(xiàn) bean 的持久性。模型 bean 定義必須保存到數(shù)據(jù)庫中的屬性。視圖 bean 使用 UI 特有的代碼(操作、驗(yàn)證器等)擴(kuò)展模型 bean。JSF 創(chuàng)建 faces-config.xml 文件中指定的視圖 bean 的實(shí)例,而該示例應(yīng)用程序的持久層使用模型 bean。因此,該應(yīng)用程序需要一個類似 ModelUtils.copy() 的實(shí)用方法,該方法將 JSF 實(shí)例化的視圖 bean 的屬性復(fù)制到持久層創(chuàng)建的模型對象,反之亦然。通過 ModelUtils 類,您還可以從名為 ModelResources 的資源包中獲取模型資源(如配置參數(shù)、SQL 語句和錯誤消息)。最后,ModelUtils 包含一個 getSubscriberDAO() 方法,該方法返回 SubscriberDAO 接口的一個實(shí)例,該接口定義了在關(guān)系數(shù)據(jù)庫中選擇、刪除、插入、更新 Subscriber 對象的方法。
該示例應(yīng)用程序提供了 SubscriberDAO 接口的兩個實(shí)現(xiàn),一個基于 JDBC API,另一個使用 Oracle TopLink。這兩個實(shí)現(xiàn)不在同一應(yīng)用程序?qū)嵗惺褂谩?/span>ModelResources 包有一個 DAO 參數(shù),用于指定要使用的 DAO 實(shí)現(xiàn)。基于 JDBC 的 DAO 的唯一好處是它只使用標(biāo)準(zhǔn)的 Java API 和 SQL。TopLink 也在內(nèi)部使用 JDBC,但它提供了很多好處:
- 由于使用可視化工具(Mapping Workbench 和 Session Editor)定義對象關(guān)系映射和配置應(yīng)用程序,因此提高了工作效率。這顯著減少了手動編寫的代碼數(shù)量;
- 不必再使用類似 JDBC 這樣的低級 API;
- 因?yàn)?TopLink 緩存從數(shù)據(jù)庫讀取的對象,所以應(yīng)用程序速度更快。此外,TopLink 在內(nèi)部生成了非常高效的 SQL 語句;
- 除 SQL 以外,TopLink 還支持幾種其他查詢機(jī)制,如“按示例查詢”、基于 Java 表達(dá)式的查詢和 EJB QL;
- 對多服務(wù)器實(shí)例(集群)的支持使您能夠構(gòu)建可伸縮的應(yīng)用程序。
創(chuàng)建模型 Bean 該示例 Web 應(yīng)用程序包含兩個未使用任何 JSF API 的模型 bean(LoginInfo 和 Subscriber)。LoginInfo 類定義了兩個屬性(email 和 password),并同任何 JavaBean 一樣,借助于 java.io.Serializable 接口聲明為可串行化:
package jsfdb.model;
public class LoginInfo implements java.io.Serializable {
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Subscriber 類擴(kuò)展了 LoginInfo、定義了五個其他屬性(name、manager、developer、administrator 和 subscriptionType),并有一個名為 countNewsletters() 的方法:
package jsfdb.model;
public class Subscriber extends LoginInfo {
public static final int TYPE_DAILY = 1;
public static final int TYPE_WEEKLY = 2;
public static final int TYPE_MONTHLY = 3;
private String name;
private boolean manager;
private boolean developer;
private boolean administrator;
private int subscriptionType;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
...
public int countNewsletters() {
int count = 0;
if (isManager())
count++;
if (isDeveloper())
count++;
if (isAdministrator())
count++;
return count;
}
}
Subscriber 的實(shí)例被保存到一個表中,可以使用以下 SQL 語句創(chuàng)建該表:
CREATE TABLE subscribers (
subscriberEmail VARCHAR2(80) PRIMARY KEY,
subscriberPassword VARCHAR2(20),
subscriberName VARCHAR2(80),
managerFlag NUMBER(1),
developerFlag NUMBER(1),
administratorFlag NUMBER(1),
subscriptionType NUMBER(1) )
通常,該 bean 的屬性和相應(yīng)的表列應(yīng)具有相同的名稱。本文使用了不同的名稱,以便您可以輕松地辨認(rèn)哪些地方使用了屬性、哪些地方使用了列。
使用數(shù)據(jù)訪問對象SubscriberDAO 接口定義視圖 bean 為保存和檢索從模型 bean 繼承的屬性而調(diào)用的方法:
package jsfdb.model.dao;
import jsfdb.model.LoginInfo;
import jsfdb.model.Subscriber;
import jsfdb.model.err.IncorrectPasswordException;
import jsfdb.model.err.LoginException;
import jsfdb.model.err.ProfileException;
import jsfdb.model.err.SubscribeException;
import jsfdb.model.err.UnknownSubscriberException;
import jsfdb.model.err.UnsubscribeException;
public interface SubscriberDAO {
public Subscriber select(LoginInfo loginInfo)
throws LoginException,
UnknownSubscriberException,
IncorrectPasswordException;
public void insert(Subscriber subscriber)
throws SubscribeException;
public void update(Subscriber subscriber)
throws ProfileException;
public void delete(Subscriber subscriber)
throws UnsubscribeException;
}
getSubscriberDAO() 方法加載一個 DAO 實(shí)現(xiàn)(TopLinkSubscriberDAO 或 JDBCSubscriberDAO)并返回所加載類的實(shí)例:
package jsfdb.model;
import jsfdb.model.dao.SubscriberDAO;
...
public class ModelUtils {
...
private static SubscriberDAO subscriberDAO;
...
public synchronized static SubscriberDAO getSubscriberDAO() {
if (subscriberDAO == null)
try {
Class daoClass = Class.forName(getResource("DAO"));
subscriberDAO
= (SubscriberDAO) daoClass.newInstance();
} catch (Exception x) {
log(x);
throw new InternalError(x.getMessage());
} catch (Exception x) {
log(x);
throw new InternalError(x.getMessage());
} catch (Exception x) {
log(x);
throw new InternalError(x.getMessage());
}
return subscriberDAO;
}
...
}
getSubscriberDAO() 方法使用 getResource()(使用 getResources() 獲取模型資源)獲取 DAO 實(shí)現(xiàn)的名稱。使用 log() 方法記錄任何意外錯誤:
package jsfdb.model;
...
import java.util.ResourceBundle;
import java.util.MissingResourceException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ModelUtils {
public static final String RESOURCES
= ModelUtils.class.getPackage().getName()
+ ".res.ModelResources";
private static ResourceBundle resources;
public static void log(Throwable x) {
Logger.global.log(Level.SEVERE, x.getMessage(), x);
}
public static synchronized ResourceBundle getResources() {
if (resources == null)
try {
resources = ResourceBundle.getBundle(RESOURCES);
} catch (MissingResourceException x) {
log(x);
throw new InternalError(x.getMessage());
}
return resources;
}
public static String getResource(String key) {
return getResources().getString(key);
}
...
}
ModelResources 包包含 DAO 參數(shù)、基于 JDBC 的 DAO 使用的 SQL 語句以及由 DAO 方法拋出的異常消息:
DAO=jsfdb.model.dao.TopLinkSubscriberDAO
TopLinkSession=JSFDBSession
# DAO=jsfdb.model.dao.JDBCSubscriberDAO
JavaCompEnv=java:comp/env
DataSource=jdbc/OracleDS
SelectStatement=...
InsertStatement=...
UpdateStatement=...
DeleteStatement=...
SubscribeException=Subscription failed. \
Please try another email address.
ProfileException=Couln't update your profile. \
Please contact the Webmaster.
UnsubscribeException=Unsubscription failed. \
Please contact the Webmaster.
LoginException=Login failed. \
Please contact the Webmaster.
UnknownSubscriberException=Unknown subscriber. \
Please subscribe.
IncorrectPasswordException=Incorrect password. \
Please try to login again.
所有異常類均擴(kuò)展了 ModelException:
package jsfdb.model.err;
import jsfdb.model.ModelUtils;
public class ModelException extends Exception {
public ModelException(String messageKey) {
super(ModelUtils.getResource(messageKey));
}
}
每個異常類只包含一個構(gòu)造函數(shù),該函數(shù)將類名傳遞給 ModelException 超類的構(gòu)造函數(shù):
package jsfdb.model.err;
public class LoginException extends ModelException {
public LoginException() {
super("LoginException");
}
}
JDBCSubscriberDAO 類包含一個具有 JNDI 的 DataSource,并實(shí)現(xiàn)由 SubscriberDAO 定義的方法。請參閱本文附帶的可下載存檔中提供的 JDBCSubscriberDAO.java 源代碼。
TopLinkSubscriberDAO 類也實(shí)現(xiàn) SubscriberDAO 接口。TopLinkSubscriberDAO() 構(gòu)造函數(shù)獲取一個服務(wù)器會話,并添加一個將在 JVM 停止時關(guān)閉會話的關(guān)閉鉤子 (hook)。基于 TopLink 的 DAO 包含用于獲取由實(shí)現(xiàn) SubscriberDAO 接口的方法使用的客戶端會話和工作單元的實(shí)用方法:
package jsfdb.model.dao;
...
import oracle.toplink.sessions.UnitOfWork;
import oracle.toplink.threetier.ClientSession;
import oracle.toplink.threetier.Server;
import oracle.toplink.tools.sessionmanagement.SessionManager;
public class TopLinkSubscriberDAO implements SubscriberDAO {
private Server serverSession;
public TopLinkSubscriberDAO() {
SessionManager manager = SessionManager.getManager();
String id = ModelUtils.getResource("TopLinkSession");
ClassLoader loader = this.getClass().getClassLoader();
serverSession = (Server) manager.getSession(id, loader);
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
serverSession.logout();
SessionManager.getManager().getSessions().remove(
ModelUtils.getResource("TopLinkSession"));
}
});
}
private ClientSession acquireClientSession() {
return serverSession.acquireClientSession();
}
private UnitOfWork acquireUnitOfWork() {
return acquireClientSession().acquireUnitOfWork();
}
...
}
創(chuàng)建視圖 Bean 如前所述,這兩個視圖 bean(LoginInfoBean 和 SubscriberBean)使用 UI 特有的代碼(如用于驗(yàn)證數(shù)據(jù)的方法(驗(yàn)證器)和用于處理數(shù)據(jù)的方法(操作))擴(kuò)展了模型 bean(LoginInfo 和 Subscriber)。LoginInfoBean 類包含一個名為 loginAction() 的操作方法。另一個視圖 bean (SubscriberBean) 定義另一個屬性 (loggedIn),實(shí)現(xiàn) emailValidator() 方法(用于驗(yàn)證電子郵件地址中是否存在字符 @)、提供幾個操作方法,并將從 Subscriber 繼承的常量作為只讀屬性公開,以便可以在 JSF 頁面中使用 EL 訪問它們:
package jsfdb.view;
import jsfdb.model.Subscriber;
...
public class SubscriberBean extends Subscriber {
...
private transient boolean loggedIn = false;
public boolean isLoggedIn() {
return loggedIn;
}
public void setLoggedIn(boolean loggedIn) {
this.loggedIn = loggedIn;
}
public void emailValidator(FacesContext context,
UIComponent comp, Object value) {
...
}
public String subscribeAction() {
...
}
public String profileAction() {
...
}
public String unsubscribeAction() {
...
}
public String cancelAction() {
if (!loggedIn)
return "login";
else
return "cancel";
}
public int getDailyConst() {
return TYPE_DAILY;
}
public int getWeeklyConst() {
return TYPE_WEEKLY;
}
public int getMonthlyConst() {
return TYPE_MONTHLY;
}
}
本部分的下列段落將詳細(xì)介紹這兩個視圖 bean 的操作方法。具體說來,您將了解
- loginAction() 如何選擇行,
- subscribeAction() 如何插入新行,
- profileAction() 如何更新現(xiàn)有行,
- unsubscribeAction() 如何刪除行。
下一部分(JSF 視圖和數(shù)據(jù)驗(yàn)證)
- 提供該示例應(yīng)用程序的 JSF 頁面,
- 介紹了如何借助 JSF 對數(shù)據(jù)進(jìn)行驗(yàn)證,
- 演示了如何創(chuàng)建 UI 組件和 bean 屬性之間的綁定,
- 演示了如何將操作方法綁定到 JSF 表單的提交按鈕。
選擇行(登錄操作)JDBCSubscriberDAO 的 select() 方法執(zhí)行一個 SQL 查詢,選擇具有給定電子郵件地址的訂戶,然后驗(yàn)證口令。以下是基于 JDBC 的 DAO 使用的 SELECT 語句:
SELECT subscriberPassword,
subscriberName,
managerFlag,
developerFlag,
administratorFlag,
subscriptionType
FROM subscribers
WHERE subscriberEmail=?
使用 TopLink 時,盡管您可以使用 SQL,但不必構(gòu)建 SQL 語句。如果在私有 read() 方法中使用查詢 API(它從 TopLinkSubscriberDAO 的多個公共方法中調(diào)用,執(zhí)行一個返回具有給定電子郵件地址的訂戶的查詢),則 TopLink 框架將生成 SQL 查詢:
package jsfdb.model.dao;
...
import oracle.toplink.expressions.ExpressionBuilder;
import oracle.toplink.queryframework.ReadObjectQuery;
import oracle.toplink.sessions.Session;
public class TopLinkSubscriberDAO implements SubscriberDAO {
...
private Subscriber read(Session session, String email) {
ReadObjectQuery query
= new ReadObjectQuery(Subscriber.class);
ExpressionBuilder builder = new ExpressionBuilder();
query.setSelectionCriteria(
builder.get("email").equal(email));
return (Subscriber) session.executeQuery(query);
}
...
}
TopLinkSubscriberDAO 的 select() 方法取得客戶端會話并調(diào)用 read():
package jsfdb.model.dao;
...
public class TopLinkSubscriberDAO implements SubscriberDAO {
...
public Subscriber select(LoginInfo loginInfo)
throws LoginException,
UnknownSubscriberException,
IncorrectPasswordException {
Subscriber s = null;
try {
ClientSession session = acquireClientSession();
s = read(session, loginInfo.getEmail());
} catch (Exception x) {
ModelUtils.log(x);
throw new LoginException();
}
if (s == null)
throw new UnknownSubscriberException();
if (!s.getPassword().equals(loginInfo.getPassword()))
throw new IncorrectPasswordException();
return s;
}
...
}
LoginInfoBean 類包含 loginAction() 方法,該方法使用 ViewUtils.eval() 獲取 SubscriberBean 類(由 JSF 管理)的實(shí)例,并獲取 adminEmail 初始化參數(shù)的值。隨后,loginAction() 調(diào)用 select(),使用 ModelUtils.copy() 將選定訂戶的屬性復(fù)制到由 JSF 管理的 JavaBean,將 loggedIn 標(biāo)記設(shè)置為 true,然后根據(jù)訂戶的電子郵件返回 list 或 profile 結(jié)果:
package jsfdb.view;
import jsfdb.model.LoginInfo;
import jsfdb.model.Subscriber;
import jsfdb.model.ModelUtils;
import jsfdb.model.err.LoginException;
import jsfdb.model.err.IncorrectPasswordException;
import jsfdb.model.err.UnknownSubscriberException;
public class LoginInfoBean extends LoginInfo {
public String loginAction() {
SubscriberBean subscriber
= (SubscriberBean) ViewUtils.eval("#{subscriber}");
String adminEmail
= (String) ViewUtils.eval("#{initParam.adminEmail}");
try {
Subscriber selectedSubscriber
= ModelUtils.getSubscriberDAO().select(this);
ModelUtils.copy(selectedSubscriber, subscriber);
subscriber.setLoggedIn(true);
if (subscriber.getEmail().equals(adminEmail))
return "list";
else
return "profile";
} catch (LoginException x) {
ViewUtils.addExceptionMessage(x);
return null;
} catch (UnknownSubscriberException x) {
ViewUtils.addExceptionMessage(x);
return null;
} catch (IncorrectPasswordException x) {
ViewUtils.addExceptionMessage(x);
return null;
}
}
}
ViewUtils 類提供用于求解 JSF 表達(dá)式和將錯誤消息添加到 JSF 上下文的實(shí)用方法:
package jsfdb.view;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import java.util.ResourceBundle;
public class ViewUtils {
public static Object eval(String expr) {
FacesContext context
= FacesContext.getCurrentInstance();
ValueBinding binding
= context.getApplication().createValueBinding(expr);
return binding.getValue(context);
}
public static void addErrorMessage(FacesContext context,
String compId, String messageId) {
ResourceBundle bundle = ResourceBundle.getBundle(
context.getApplication().getMessageBundle());
FacesMessage message = new FacesMessage(
bundle.getString(messageId));
message.setSeverity(FacesMessage.SEVERITY_ERROR);
context.addMessage(compId, message);
}
public static void addExceptionMessage(Exception x) {
FacesContext context
= FacesContext.getCurrentInstance();
FacesMessage message
= new FacesMessage(x.getMessage());
message.setSeverity(FacesMessage.SEVERITY_FATAL);
context.addMessage(null, message);
}
}
ModelUtils 的 copy() 方法獲取源 bean 的屬性,并使用 JavaBean Introspection API 和 Java Reflection API 設(shè)置另一個 bean 的屬性:
package jsfdb.model;
...
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.IntrospectionException;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
...
public class ModelUtils {
...
public static void copy(Object source, Object dest) {
try {
Class sourceClass = source.getClass();
Class destClass = dest.getClass();
BeanInfo info = Introspector.getBeanInfo(sourceClass);
PropertyDescriptor props[]
= info.getPropertyDescriptors();
Object noParams[] = new Object[0];
Object oneParam[] = new Object[1];
for (int i = 0; i < props.length; i++) {
Method getter = props[i].getReadMethod();
if (getter == null)
continue;
Object value = getter.invoke(source, noParams);
Method setter = props[i].getWriteMethod();
if (setter != null && sourceClass != destClass)
try {
setter = destClass.getMethod(
setter.getName(),
setter.getParameterTypes());
} catch (NoSuchMethodException x) {
setter = null;
}
if (setter != null) {
oneParam[0] = value;
setter.invoke(dest, oneParam);
}
}
} catch (IntrospectionException x) {
log(x);
throw new InternalError(x.getMessage());
} catch (IllegalAccessException x) {
log(x);
throw new InternalError(x.getMessage());
} catch (IllegalArgumentException x) {
log(x);
throw new InternalError(x.getMessage());
} catch (SecurityException x) {
log(x);
throw new InternalError(x.getMessage());
} catch (InvocationTargetException x) {
log(x.getTargetException());
throw new InternalError(
x.getTargetException().getMessage());
}
}
}
如果所有屬性都是基元對象或不變對象(如 String),則 copy() 方法將比較適用。如果某個 bean 包含可變對象(如子 bean 或集合),則應(yīng)克隆這些屬性的值并將它們的克隆存儲到 dest bean 中。
插入新行(訂閱操作)JDBCSubscriberDAO 的 insert() 方法執(zhí)行一個 SQL 語句,向數(shù)據(jù)庫中插入一個新訂戶:
INSERT INTO subscribers (
subscriberEmail,
subscriberPassword,
subscriberName,
managerFlag,
developerFlag,
administratorFlag,
subscriptionType )
VALUES (?, ?, ?, ?, ?, ?, ?)
使用 TopLink 時,在必須使用 TopLink Mapping Workbench 工具定義的對象關(guān)系映射中將自動生成 SQL 語句。TopLinkSubscriberDAO 的 insert() 方法獲取一個工作單元、創(chuàng)建一個新的 Subscriber 實(shí)例、設(shè)置它的屬性、注冊新對象并調(diào)用 commit():
package jsfdb.model.dao;
...
public class TopLinkSubscriberDAO implements SubscriberDAO {
...
public void insert(Subscriber subscriber)
throws SubscribeException {
try {
UnitOfWork uow = acquireUnitOfWork();
Subscriber s = new Subscriber();
ModelUtils.copy(subscriber, s);
uow.registerObject(s);
uow.commit();
} catch (Exception x) {
ModelUtils.log(x);
throw new SubscribeException();
}
}
...
}
SubscriberBean 的 subscribeAction() 方法調(diào)用 insert()、將 loggedIn 標(biāo)記設(shè)置為 true,并在未出錯的情況下返回 subscribed 結(jié)果:
package jsfdb.view;
...
public class SubscriberBean extends Subscriber {
...
public final static String SELECT_NEWSLETTER_ID
= "jsfdb.view.SubscriberBean.SELECT_NEWSLETTER";
...
public String subscribeAction() {
if (countNewsletters() == 0) {
ViewUtils.addErrorMessage(
FacesContext.getCurrentInstance(),
null, SELECT_NEWSLETTER_ID);
return null;
}
try {
ModelUtils.getSubscriberDAO().insert(this);
setLoggedIn(true);
return "subscribed";
} catch (SubscribeException x) {
ViewUtils.addExceptionMessage(x);
return null;
}
}
...
}
更新現(xiàn)有行(配置文件操作)JDBCSubscriberDAO 的 update() 方法執(zhí)行一個 SQL 語句,更新現(xiàn)有訂戶的配置文件:
UPDATE subscribers SET
subscriberPassword=?,
subscriberName=?,
managerFlag=?,
developerFlag=?,
administratorFlag=?,
subscriptionType=?
WHERE subscriberEmail=?
TopLinkSubscriberDAO 的 update() 方法獲取一個工作單元、調(diào)用私有 read() 方法獲取具有給定電子郵件的 bean、更新 bean 屬性并調(diào)用 commit():
package jsfdb.model.dao;
...
public class TopLinkSubscriberDAO implements SubscriberDAO {
...
public void update(Subscriber subscriber)
throws ProfileException {
try {
UnitOfWork uow = acquireUnitOfWork();
Subscriber s = read(uow, subscriber.getEmail());
ModelUtils.copy(subscriber, s);
uow.commit();
} catch (Exception x) {
ModelUtils.log(x);
throw new ProfileException();
}
}
...
}
SubscriberBean 的 profileAction() 方法調(diào)用 update() 并返回 null 結(jié)果:
package jsfdb.view;
...
public class SubscriberBean extends Subscriber {
...
public final static String SELECT_NEWSLETTER_ID
= "jsfdb.view.SubscriberBean.SELECT_NEWSLETTER";
...
public String profileAction() {
if (!loggedIn)
return "login";
if (countNewsletters() == 0) {
ViewUtils.addErrorMessage(
FacesContext.getCurrentInstance(),
null, SELECT_NEWSLETTER_ID);
return null;
}
try {
ModelUtils.getSubscriberDAO().update(this);
return null;
} catch (ProfileException x) {
ViewUtils.addExceptionMessage(x);
return null;
}
}
...
}
刪除行(取消訂閱操作)JDBCSubscriberDAO 的 delete() 方法執(zhí)行一個 SQL 語句,從數(shù)據(jù)庫中刪除一個訂戶:
DELETE FROM subscribers WHERE subscriberEmail=?
TopLinkSubscriberDAO 的 delete() 方法獲取一個工作單元、調(diào)用私有 read() 方法獲取具有給定電子郵件的 bean、刪除 bean 屬性并調(diào)用 commit():
package jsfdb.model.dao;
...
public class TopLinkSubscriberDAO implements SubscriberDAO {
...
public void delete(Subscriber subscriber)
throws UnsubscribeException {
try {
UnitOfWork uow = acquireUnitOfWork();
Subscriber s = read(uow, subscriber.getEmail());
uow.deleteObject(s);
uow.commit();
} catch (Exception x) {
ModelUtils.log(x);
throw new UnsubscribeException();
}
}
}
SubscriberBean 的 unsubscribeAction() 方法調(diào)用 delete() 并在未出錯的情況下返回 unsubscribed 結(jié)果:
package jsfdb.view;
...
public class SubscriberBean extends Subscriber {
...
public String unsubscribeAction() {
if (!loggedIn)
return "login";
try {
ModelUtils.getSubscriberDAO().delete(this);
return "unsubscribed";
} catch (UnsubscribeException x) {
ViewUtils.addExceptionMessage(x);
return null;
}
}
...
}
創(chuàng)建 TopLink 項(xiàng)目以下步驟說明了如何使用 Mapping Workbench 創(chuàng)建一個 TopLink 項(xiàng)目。
第 1 步:啟動 OracleAS TopLink Mapping Workbench。然后,從主菜單中選擇 File 和 New Project...。在 Create New Project 對話框中,提供現(xiàn)有數(shù)據(jù)庫的名稱(例如,orcl),然后單擊 OK。
第 2 步:Mapping Workbench 將顯示一個窗口讓您保存該項(xiàng)目。選擇一個目錄,提供一個文件名(例如,jsfdb),然后單擊 Save。
第 3 步:在左側(cè) Navigator 窗格中選擇該數(shù)據(jù)庫,單擊 Add...,輸入一個登錄標(biāo)簽(例如,orclLogin),然后單擊 OK。
第 4 步:在主窗口中,選擇新建的登錄,并提供 JDBC 驅(qū)動程序類的名稱、數(shù)據(jù)庫 URL、用戶名和口令。別忘了選中 Save Password 復(fù)選框。
第 5 步:在 Navigator 窗格中右鍵單擊該數(shù)據(jù)庫名稱,然后單擊 Log In to Database。
第 6 步:在 Navigator 窗格中再次右鍵單擊該數(shù)據(jù)庫名稱,然后單擊 Add or Update Existing Tables from Database。
第 7 步:在 Import Tables from Database 窗口中的 Table Name Pattern 域中,輸入 SUBSCRIBERS,單擊 Get Table Names,從左側(cè) Available Tables 窗格中選擇 SUBSCRIBERS 表,將它添加到右側(cè)的 Selected Tables 窗格,然后單擊 OK。
第 8 步:在 Navigator 窗格中選擇該項(xiàng)目 (jsfdb),確保 General 是右側(cè)窗格的當(dāng)前選項(xiàng)卡,單擊 Add Entries...,在顯示的目錄樹中尋找,選擇包含該 Web 應(yīng)用程序已編譯的類的目錄,然后單擊 OK。選中的目錄將被添加到主窗口的 Class Path 列表中。
第 9 步:在 Navigator 窗格中再次右鍵單擊該項(xiàng)目,然后單擊 Add or Refresh Classes...。
第 10 步:在 Select Classes 窗口左側(cè)的 Available Packages/Classes 窗格中選擇 LoginInfo 和 Subscriber 類。
第 11 步:將 LoginInfo 和 Subscriber 類移到右側(cè)的 Selected Classes 窗格中,然后單擊 OK。只有 Subscriber 類被映射到了表上,但由于 LoginInfo 是 Subscriber 的超類,因此我們還需要將其映射到表上。
第 12 步:在主窗口的 Navigator 窗格中選擇 Subscriber 類。在右側(cè)的 Editor 窗格中,確保 Descriptor 是當(dāng)前選項(xiàng)卡,并在 Associated Table 列表中選擇 SUBSCRIBERS。
第 13 步:在 Navigator 窗格中,右鍵單擊 Subscriber 類,選擇 Map Inherited Attributes 并單擊 To Superclass。
第 14 步:右鍵單擊 Subscriber 類的每個 bean 屬性(例如,administrator),選擇 Map As...,然后單擊 Direct to Field。
第 15 步:在 Navigator 窗格中選擇 Subscriber 類的每個 bean 屬性(例如,email),然后在右側(cè)的 Editor 窗格中設(shè)置相應(yīng)的 Database Field(例如,SUBSCRIBEREMAIL)。
第 16 步:由于 LoginInfo 類未映射到任何表,因此必須從項(xiàng)目中刪除它。在 Navigator 窗格中右鍵單擊 LoginInfo,然后單擊 Remove Class。當(dāng)顯示 Remove Descriptors 對話框時,單擊 Remove。
第 17 步:在主菜單中,選擇 File、Export、Project Deployment XML... 或使用 CTRL+D。輸入 JSFDBProject.xml 并單擊 OK。然后,選擇要將該 XML 文件保存到的目錄。
第 18 步:使用 File 和 Save 或使用 CTRL+S 保存該項(xiàng)目。除了將項(xiàng)目設(shè)置存儲到 jsfdb.mwp 文件中以外,保存操作還將在 class、descriptor 和 table 子目錄中創(chuàng)建幾個 XML 文件。現(xiàn)在,您可以選擇 OracleAS TopLink Mapping Workbench 了。
配置 TopLink 會話下列步驟說明了如何使用 Sessions Editor 配置一個 TopLink 會話。
第 1 步:啟動 OracleAS TopLink Sessions Editor。從主菜單中選擇 File 和 New...(或使用 CTRL+N)。在 New 對話框中,保持 sessions.xml 文件名不變,單擊 Browse 按鈕為該文件選擇 Location,在 Session Name 中輸入 JSFDBSession,然后單擊 OK。
第 2 步:在 Navigator 窗格中選擇 JSFDBSession,確保 General 是 Editor 窗格的當(dāng)前選項(xiàng)卡,然后在 Project Type 組的 XML 域中輸入 JSFDBProject.xml。
第 3 步:選擇 Editor 窗格的 Logging 選項(xiàng)卡,然后在Enable Logging 列表中選擇 True。
第 4 步:選擇 Editor 窗格的 Login 選項(xiàng)卡,選中 Database Platform 復(fù)選框,在相應(yīng)的列表中選擇 Oracle,然后在 Data Source 域中輸入 java:comp/env/jdbc/OracleDS。
第 5 步:從主菜單中選擇 File 和 Save(或使用 CTRL+S)保存 sessions.xml 文件。現(xiàn)在,您可以關(guān)閉 OracleAS TopLink Sessions Editor 了。
JSF 視圖和數(shù)據(jù)驗(yàn)證
該示例 Web 應(yīng)用程序包含一個訂閱頁面 (subscribe.jsp)、一個登錄頁面 (login.jsp)、一個配置文件編輯頁面 (profile.jsp)、一個取消訂閱頁面 (unsubscribe.jsp、兩個確認(rèn)頁面(subscribed.jsp 和 unsubscribed.jsp)、一個列出所有訂戶的頁面 (list.jsp) 和一個注銷頁面 (logout.jsp)。由 JSF 頁面創(chuàng)建的某些表單借助于先前部分中的 DAO 方法觸發(fā)執(zhí)行基本數(shù)據(jù)庫操作(INSERT、SELECT、UPDATE 和 DELETE)的操作。
在網(wǎng)頁中使用 JSFJSF 定義了兩個標(biāo)準(zhǔn)標(biāo)記庫(Core 和 HTML),您必須在 JSP 頁面中使用 <%@taglib%> 指令聲明它們。JSF Core 庫包含不依賴任何標(biāo)記語言的標(biāo)記,而 JSF HTML 庫是為在 Web 瀏覽器中查看的頁面設(shè)計的。這兩個標(biāo)記庫的標(biāo)準(zhǔn)前綴分別是 f(用于 JSF Core)和 h(用于 JSF HTML)。所有 JSF 標(biāo)記必須嵌套在 <f:view> 元素的內(nèi)部。通過 <f:view> 標(biāo)記,JSF 框架可以將 UI 組件的狀態(tài)作為 HTTP 請求響應(yīng)的一部分保存。
該示例 Web 應(yīng)用程序的頁面使用 JSF 和 JSTL Core 庫創(chuàng)建 HTML 頭:
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
<f:view>
<f:loadBundle var="labels" basename="jsfdb.view.res.Labels"/>
<c:set var="stylesheet"
value="${pageContext.request.contextPath}/stylesheet.css"/>
<html>
<head>
<title><h:outputText value="#{labels.subscribe}"/></title>
<link rel="stylesheet" type="text/css"
href="<c:out value='${stylesheet}'/>">
</head>
<body>
...
</body>
</html>
</f:view>
JSF 的 <h:outputText> 標(biāo)記與 JSTL 的 <c:out> 標(biāo)記非常相似,但也有一些差別。例如,<h:outputText> 使用 JSF EL,而 <c:out> 使用最初為 JSTL 1.0 創(chuàng)建并在隨后被 JSP 2.0 采用的表達(dá)式語言。在上一個代碼段中,<f:loadBundle> 標(biāo)記加載了一個包含所有 JSF 頁面的標(biāo)簽和標(biāo)題的資源包:
subscribe=Subscribe
subscribed=Subscribed
login=Login
logout=Logout
profile=Profile
update=Update
unsubscribe=Unsubscribe
unsubscribed=Unsubscribed
cancel=Cancel
email=Email
password=Password
passwordDetail=Useful to change your profile
name=Name
newsletters=Newsletters
manager=Manager
developer=Developer
administrator=Administrator
subscriptionType=Subscription Type
daily=Daily
weekly=Weekly
monthly=Monthly
list=List
使用 JSF 標(biāo)記構(gòu)建表單許多應(yīng)用程序的網(wǎng)頁使用 JSF 創(chuàng)建表單。例如,login.jsp 使用 <h:inputText> 和 <h:inputSecret>,它們呈現(xiàn)兩個類型分別為 text 和 password 的 HTML <input> 元素。每個表單都有一個使用 <h:outputLabel> 呈現(xiàn)的標(biāo)簽。所有錯誤消息都使用 <h:message> 和 <h:messages> 呈現(xiàn)。<f:validateLength> 標(biāo)記驗(yàn)證用戶在表單域中輸入的字符串的長度。<h:commandButton> 呈現(xiàn)一個 HTML 提交按鈕。所有這些標(biāo)記都位于 <h:form> 元素(呈現(xiàn) HTML <form> 和 </form> 標(biāo)記)中。除了 HTML 表單以外,login.jsp 頁面還提供了訂閱表單的鏈接,并使用 <h:outputLink> 和 <h:outputText> 呈現(xiàn) HTML <a> 元素及其內(nèi)容:
<!-- login.jsp -->
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
<c:remove var="subscriber" scope="session"/>
<f:view>
...
<h1><h:outputText value="#{labels.login}"/></h1>
<h:outputLink value="subscribe.faces">
<h:outputText value="#{labels.subscribe}"/>
</h:outputLink>
<h:form id="login">
<h:messages globalOnly="true" styleClass="message"/>
<p><h:outputLabel for="email"
value="#{labels.email}"/>
<h:message for="email" styleClass="message"/><br>
<h:inputText id="email" required="true"
value="#{loginInfo.email}"
size="40" maxlength="80">
<f:validateLength minimum="1" maximum="80"/>
</h:inputText>
<p><h:outputLabel for="password"
value="#{labels.password}"/>
<h:message for="password" styleClass="message"/><br>
<h:inputSecret id="password" required="true"
value="#{loginInfo.password}"
size="10" maxlength="20">
<f:validateLength minimum="6" maximum="20"/>
</h:inputSecret>
<p><h:commandButton id="command"
value="#{labels.login}"
action="#{loginInfo.loginAction}"/>
</h:form>
...
</f:view>
當(dāng)執(zhí)行 login.jsp 頁面時,JSF 創(chuàng)建 LoginInfoBean 實(shí)例,該實(shí)例的屬性綁定到 UI 組件。當(dāng)單擊 Login 按鈕時,Web 瀏覽器將用戶的電子郵件地址和口令提交給服務(wù)器。JSF 框架驗(yàn)證用戶是否為這兩個必填域提供了值。如果字符串值的長度通過了 <f:validateLength> 執(zhí)行的驗(yàn)證,則 JSF 設(shè)置 bean 屬性并調(diào)用 loginAction() 方法對用戶進(jìn)行身份驗(yàn)證。
請注意,login.jsp 使用 JSTL 的 <c:remove> 標(biāo)記從 session 作用域中刪除任何 subscriber bean。如果用戶首次訪問該 Web 應(yīng)用程序,或者如果他的前一個會話已經(jīng)過期,則不存在這樣的 bean 實(shí)例,因此 <c:remove> 將不起作用。否則,該標(biāo)記清理該會話作用域。由于在 loginAction() 中求解的一個 EL 表達(dá)式中引用了 subscriber bean,因此 JSF 將創(chuàng)建 SubscriberBean(在 faces-config.xml 文件中指定)的實(shí)例。loginAction() 方法從數(shù)據(jù)庫中讀取訂戶的配置文件并設(shè)置該 bean 的屬性。subscriber bean 將由其他 JSF 頁面使用,并將保存在 session 作用域中,直到 logout.jsp 刪除它:
<!-- logout.jsp -->
...
<f:view>
...
</f:view>
<c:remove var="subscriber" scope="session"/>
subscribe.jsp 和 profile.jsp 頁面使用 JSF 呈現(xiàn)其他表單元素,如復(fù)選框和列表。JSF 的 <h:panelGrid> 和 <h:panelGroup> 標(biāo)記用于呈現(xiàn)其單元格包含訂閱表單的三個復(fù)選框的 HTML 表格。<h:selectBooleanCheckbox> 標(biāo)記呈現(xiàn) checkbox 類型的 <input> 元素:
<!-- subscribe.jsp -->
...
<f:view>
...
<h:form id="subscribe">
...
<p><h:outputText value="#{labels.newsletters}"/>
<h:message for="newsletters" styleClass="message"/><br>
<h:panelGrid id="newsletters"
columns="3" border="0" cellspacing="5">
<h:panelGroup>
<h:selectBooleanCheckbox id="manager"
value="#{subscriber.manager}"/>
<h:outputLabel for="manager"
value="#{labels.manager}"/>
</h:panelGroup>
<h:panelGroup>
<h:selectBooleanCheckbox id="developer"
value="#{subscriber.developer}"/>
<h:outputLabel for="developer"
value="#{labels.developer}"/>
</h:panelGroup>
<h:panelGroup>
<h:selectBooleanCheckbox id="administrator"
value="#{subscriber.administrator}"/>
<h:outputLabel for="administrator"
value="#{labels.administrator}"/>
</h:panelGroup>
</h:panelGrid>
...
</h:form>
...
</f:view>
<h:selectOneMenu> 和 <f:selectItem> 標(biāo)記呈現(xiàn)一個下拉式列表,用戶可以通過該列表選擇他們的訂閱類型:每日、每周或每月:
<!-- subscribe.jsp -->
...
<f:view>
...
<h:form id="subscribe">
...
<p><h:outputLabel for="subscriptionType"
value="#{labels.subscriptionType}"/>
<h:message for="subscriptionType"
styleClass="message"/><br>
<h:selectOneMenu id="subscriptionType"
value="#{subscriber.subscriptionType}"
required="true">
<f:validateLongRange minimum="1" maximum="3"/>
<f:selectItem itemLabel="#{labels.daily}"
itemValue="#{subscriber.dailyConst}"/>
<f:selectItem itemLabel="#{labels.weekly}"
itemValue="#{subscriber.weeklyConst}"/>
<f:selectItem itemLabel="#{labels.monthly}"
itemValue="#{subscriber.monthlyConst}"/>
</h:selectOneMenu>
...
</h:form>
...
</f:view>
subscribeAction() 方法(在 subscribe.jsp 中使用)將新訂戶的配置文件保存到數(shù)據(jù)庫中。profile.jsp 頁面顯示由 subscriber bean 保持的首選設(shè)置,并允許用戶更改他的配置文件(profileAction() 方法更新存儲在數(shù)據(jù)庫中的信息)。
由 unsubscribe.jsp 呈現(xiàn)的表單包含兩個綁定到不同操作方法的提交按鈕:
<!-- unsubscribe.jsp -->
...
<f:view>
...
<h:form id="unsubscribe">
...
<p><h:commandButton id="command"
value="#{labels.unsubscribe}"
action="#{subscriber.unsubscribeAction}"/>
<h:commandButton id="cancel"
value="#{labels.cancel}"
action="#{subscriber.cancelAction}"/>
</h:form>
...
</f:view>
unsubscribeAction() 方法從數(shù)據(jù)庫中刪除訂戶的配置文件。先前部分(JavaBean 和數(shù)據(jù)訪問對象)介紹了所有操作方法的代碼。
使用標(biāo)準(zhǔn)的和定制的驗(yàn)證器 JSF 提供了幾個驗(yàn)證器標(biāo)記,如在示例應(yīng)用程序的 JSF 頁面中使用的 <f:validateLength> 和 <f:validateLongRange>。此外,許多 HTML 標(biāo)記支持 required 屬性。通過該屬性可以指定用戶是否必須總要為該 UI 組件提供值。但在大多數(shù)情況下,您需要應(yīng)用程序特有的驗(yàn)證。您可以構(gòu)建定制驗(yàn)證標(biāo)記,但 validator 屬性(許多標(biāo)準(zhǔn) JSF 標(biāo)記都支持它)更容易使用。您可以使用該屬性指定一個將用于驗(yàn)證用戶輸入值的方法:
<!-- subscribe.jsp -->
...
<f:view>
...
<h:form id="subscribe">
...
<p><h:outputLabel for="email"
value="#{labels.email}"/>
<h:message for="email" styleClass="message"/><br>
<h:inputText id="email" required="true"
validator="#{subscriber.emailValidator}"
value="#{subscriber.email}"
size="40" maxlength="80">
<f:validateLength minimum="1" maximum="80"/>
</h:inputText>
...
</h:form>
...
</f:view>
SubscriberBean 的 emailValidator() 方法驗(yàn)證電子郵件地址是否包含 @ 字符:
package jsfdb.view;
...
import javax.faces.component.UIComponent;
import javax.faces.component.EditableValueHolder;
import javax.faces.context.FacesContext;
public class SubscriberBean extends Subscriber {
public final static String INVALID_EMAIL_ID
= "jsfdb.view.SubscriberBean.INVALID_EMAIL";
...
public void emailValidator(FacesContext context,
UIComponent comp, Object value) {
String email = (String) value;
if (email.indexOf("@") == -1) {
String compId = comp.getClientId(context);
ViewUtils.addErrorMessage(
context, compId, INVALID_EMAIL_ID);
((EditableValueHolder) comp).setValid(false);
}
}
...
}
錯誤消息通過 <h:message/> 在 JSF 頁面中呈現(xiàn),并從應(yīng)用程序消息包中被獲取:
jsfdb.view.SubscriberBean.INVALID_EMAIL=invalid
jsfdb.view.SubscriberBean.SELECT_NEWSLETTER=\
You must subscribe to at least one newsletter.
javax.faces.component.UIInput.REQUIRED=required
javax.faces.validator.LengthValidator.MAXIMUM=\
may contain maximum {0} characters
javax.faces.validator.LengthValidator.MINIMUM=\
must contain minimum {0} characters
除了應(yīng)用程序特有的消息外,該資源包還包含幾個默認(rèn) JSF 消息的替代形式。例如,javax.faces.component.UIInput.REQUIRED 關(guān)鍵字之后是一個錯誤消息,它將在用戶沒有為必填表單元素提供值時呈現(xiàn)。必須在 faces-config.xml 中配置應(yīng)用程序消息包:
<faces-config>
<application>
<locale-config>
<default-locale>en</default-locale>
</locale-config>
<message-bundle>jsfdb.view.res.Messages</message-bundle>
</application>
...
</faces-config>
驗(yàn)證器標(biāo)記和方法驗(yàn)證單個 UI 組件的值。但有時您不得不一起驗(yàn)證一組組件。這可以在一個操作方法中完成。例如,用戶可能沒有選中訂閱表單的任何復(fù)選框,但至少應(yīng)訂閱一種時事通訊。如果 countNewsletters() 返回 0,則 subscribeAction() 方法將發(fā)出錯誤消息:
package jsfdb.view;
...
public class SubscriberBean extends Subscriber {
...
public final static String SELECT_NEWSLETTER_ID
= "jsfdb.view.SubscriberBean.SELECT_NEWSLETTER";
...
public String subscribeAction() {
if (countNewsletters() == 0) {
ViewUtils.addErrorMessage(
FacesContext.getCurrentInstance(),
null, SELECT_NEWSLETTER_ID);
return null;
}
...
}
...
}
未關(guān)聯(lián)到任何特定 UI 組件的錯誤消息可以在 JSF 頁面中使用 <h:messages globalOnly="true"/> 呈現(xiàn)。
結(jié)合使用 JSF 與 JSTL 的 SQL 標(biāo)記 JSF API 為表定義了一個抽象數(shù)據(jù)模型 (javax.faces.model.DataModel),并提供了用于包裝數(shù)組、列表、JDBC 結(jié)果集、JSTL 的 <sql:query> 標(biāo)記的結(jié)果以及標(biāo)量的實(shí)現(xiàn)。JSF 數(shù)據(jù)模型包含用于訪問行數(shù)據(jù)的方法,但行的表示方式由具體的模型實(shí)現(xiàn)決定。當(dāng)使用數(shù)組或列表時,每個元素都是一行。當(dāng)使用結(jié)果集時,每一行都表示為一個其關(guān)鍵字為表列的 java.util.Map。由于不限制行模型,因此 Web 開發(fā)人員應(yīng)負(fù)責(zé)使用 <h:column> 標(biāo)記(必須嵌套在 <h:dataTable> 元素的內(nèi)部)定義列在 JSF 頁面中的呈現(xiàn)方式。
該示例應(yīng)用程序的 web.xml 文件使用 <resource-ref> 聲明一個數(shù)據(jù)源,并使用一個名為 javax.servlet.jsp.jstl.sql.dataSource 的上下文參數(shù)將它配置為默認(rèn)的 JSTL 數(shù)據(jù)源。另一個參數(shù) (adminEmail) 指定一個電子郵件地址,用于與登錄訂戶的 email 屬性進(jìn)行比較(在 list.jsp 中):
<web-app>
<context-param>
<param-name>adminEmail</param-name>
<param-value>admin@localhost</param-value>
</context-param>
<context-param>
<param-name>javax.servlet.jsp.jstl.sql.dataSource</param-name>
<param-value>jdbc/OracleDS</param-value>
</context-param>
...
<resource-ref>
<res-ref-name>jdbc/OracleDS</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
...
</web-app>
只有在 web.xml 中指定了電子郵件地址的用戶才可以執(zhí)行 list.jsp 頁面。因此,您必須在部署應(yīng)用程序后以 admin@localhost 的身份進(jìn)行訂閱。當(dāng)使用 adminEmail 地址登錄時,login.jsp 頁面將該請求轉(zhuǎn)給 list.jsp。該示例應(yīng)用程序中使用的此身份驗(yàn)證機(jī)制演示了操作方法(如 loginAction())如何根據(jù)可編程條件返回不同的結(jié)果(list 或 profile)。實(shí)際的應(yīng)用程序?qū)⑹褂没?HTTP 或基于表單的身份驗(yàn)證驗(yàn)證管理員的身份。
list.jsp 頁面使用 JSTL 標(biāo)記執(zhí)行 SQL 查詢,并創(chuàng)建一個用于保存結(jié)果集的 subscriberList 變量。該變量被傳遞給 <h:dataTable>,后者迭代結(jié)果集的各行,并調(diào)用每個行的標(biāo)記主體。每個 <h:column> 標(biāo)記都包含一個 <h:outputText value="#{row...}"/> 標(biāo)記(用于呈現(xiàn)當(dāng)前單元格的值)和一個作為表格的表頭的一部分呈現(xiàn)的 facet:
<!-- list.jsp -->
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="sql" uri="http://java.sun.com/jstl/sql" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %>
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
<c:if test="${subscriber == null || !subscriber.loggedIn}">
<c:redirect url="/login.faces"/>
</c:if>
<c:if test="${subscriber.email != initParam.adminEmail}">
<c:redirect url="/profile.faces"/>
</c:if>
<sql:query var="subscriberList" scope="request">
SELECT * FROM subscribers ORDER BY subscriberEmail
</sql:query>
<f:view>
...
<h:form id="list">
<h:dataTable id="table" var="row"
value="#{subscriberList}"
border="1" cellpadding="5">
<h:column>
<f:facet name="header">
<h:outputText value="#{labels.email}"/>
</f:facet>
<h:outputText value="#{row.subscriberEmail}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="#{labels.password}"/>
</f:facet>
<h:outputText value="#{row.subscriberPassword}"/>
</h:column>
...
</h:dataTable>
</h:form>
</body>
</html>
</f:view>
由于 JSF 不支持 page 作用域,因此 <sql:query> 標(biāo)記使用 list.jsp 頁面中的 request 作用域。如果您忘了指定 JSF 支持的作用域,則 JSTL 標(biāo)記將使用默認(rèn)的頁面作用域,且這兩個標(biāo)記庫的標(biāo)記將無法通信。因此,請確保結(jié)合使用 JSF 和 JSTL 的標(biāo)記時使用一個通用的作用域(request、session 或 application)。
總結(jié)
本文演示了一個基于 JSF 的 Web 應(yīng)用程序,它使用 DAO 模式、JDBC、SQL、TopLink 和 JSTL 訪問關(guān)系數(shù)據(jù)庫。您可以在開發(fā)實(shí)際應(yīng)用程序時應(yīng)用本文介紹的技術(shù),并且您可以使用 Oracle 數(shù)據(jù)庫、Oracle Application Server Containers for J2EE、TopLink 對象關(guān)系映射框架和 JSF 引用實(shí)現(xiàn)測試本文的示例。
Andrei Cioroianu (devtools@devsphere.com) 是 Devsphere 的創(chuàng)始人,這是一家 Java 框架、XML 咨詢和 Web 開發(fā)服務(wù)的供應(yīng)商。Andrei 編寫了許多 Java 文章,分別由 Oracle 技術(shù)網(wǎng)、ONJava、JavaWorld 和 Java Developer's Journal 發(fā)表。他還與別人合著了 Java XML Programmer's Reference 和 Professional Java XML 兩書(均由 Wrox Press 出版)。