内容


用 Apache Derby、Apache MyFaces 和 Facelets 开发应用程序

使用这三种强大的技术精心打造基于 Java 的 MVC Web 应用程序

Comments

什么是 JSF、Facelets 和 Apache Derby?

本文中所演示的用于 Web 应用程序的三种技术是 JSF、Facelets 和 Java™ Database Connectivity (JDBC™)。JDBC 用于访问关系数据库 Apache Derby 中的数据。JSF 是用于构建用户界面(user interface,UI)的 Web 应用程序框架,Facelets 是专门为 JSF 而设计的表示技术。Apache Derby 是与纯 Java JDBC 兼容的数据库。这三种组件的综合使用为开发基于 Java 的 MVC Web 应用程序提供了理想的环境。

首先介绍 JSF,它解决了控制器和 MVC Web 应用程序视图层之间更清晰地分离这种需求。它还与服务器端的事件有关,而不是完全依赖客户端用于事件处理的 JavaScript™。但是,JSF 的主要贡献是其基于组件的模型,它促进了可重用性和可扩展性。使用 JSF 的缺点之一是用于呈现层的技术,默认情况下由 JavaServer Pages™ (JSP™) 提供。JSP 不是基于组件的系统,因此无法利用 JSF 模型的所有功能。JSF 应用程序中的 JSP 标记呈现了视图,并表示组件,但无法更改 JSF 组件的状态。

Facelets

这就是使用 Facelets 的地方。Facelets 是专门为考虑 JSF 的基于组件的技术而设计的,产生 Web 应用程序视图中所使用的组件树。对 JSP 进行编译以创建 servlet,且使用 JSP 来呈现动态内容,但此内容并不是始终与 JSF 所产生的组件树保持同步。Facelets 与 JSF 组件树相结合而运行,因此对呈现的输出不必感到奇怪 —— 在 JSF 中使用 JSP 进行呈现时会出现这种情况。

本文中所讨论的示例应用程序(可在本文结尾的下载部分获取)使用了 Facelets 的模板化功能,并且演示了应用改进的错误消息的示例,这些错误消息在 Facelets 开发中可捕获。还有许多可以使用的 Facelets 功能,在本文中没有进行讨论(如果您想学习更多有关内容,请参阅本文结尾的参考资料部分)。

使用 Apache MyFaces 的 JSF

Apache 的 MyFaces 项目提供了 JSF Web 应用程序框架规范 JSR 127(请参阅参考资料以获取链接)的开放源码实现。MyFaces 提供了规范所要求的全部类,以及被称为 Tomahawk 的其他 JSF 组件。其中一些组件提供了新功能,多于规范所要求的那些功能,还有一些组件提供了增强的功能。

您应该已经熟悉 JSF 的背景知识,想从本文获取更多知识。一个很好的开端是从参考资料部分中所列出的系列文章(由 Rick Hightower 提供)开始学习。

Apache Derby

Apache Derby 在本文示例应用程序中的 Model 层使用,该示例应用程序是一个虚拟的航班预订系统。Apache Derby 是零管理、纯 Java 关系数据库,由于其可嵌入能力及对 JDBC 标准的兼容性,它完全适合基于 Java 的 Web 应用程序开发。

本文的重点是如何 综合使用 MyFaces、Facelets 和 Derby 来构建 Web 应用程序;假定您已了解 Web 应用程序开发、JSF 以及通过 JDBC 访问数据库的基础知识。

Web 应用程序的组件

航班预订应用程序使用了以下软件组件和技术;列表还包括了此应用程序中所使用的一些特定功能。

  • Apache MyFaces JSF Implementation 1.1.4 Core 和 Tomahawk 1.1.3
    • Validators —— 包括正则表达式、Equals 及 Credit Card 验证。
    • updateActionListener —— 此侦听器可与 ActionSource UIComponent 相关联(通过链接或按钮),将值与属性联系起来。
    • Extended DataTable —— 将使用页眉对标准 JSF datatable 进行扩展,允许进行按列排序。
    • JavaScript Menu —— JSCookMenu 组件使用 CSS 和 JavaScript 来创建菜单项,从而创建动态菜单。
  • Apache Derby database engine 10.1.3.1
    • 使用 Derby EmbeddedDataSource
    • 通过 ServletContextListener 来启动和停止 Derby
    • JDBC Callable Statement 用于执行存储的过程,将 SQL 语句写入 Derby 的消息日志文件
    • JDBC Prepared Statements 用于插入或删除 Derby 记录。
  • Facelets —— JSF View Definition Framework 1.1.11
    • 模板化 —— 能够创建用于页面代码重用和取代的模板。
    • 改进的错误消息 —— 更易于调试。
  • Apache Tomcat servlet engine 5.0.28
    • 运行由可扩展超文本标记语言(Extensible Hypertext Markup Language,XHTML)页面、servlet 过滤器和侦听器、及 JSF 组件构成的 Web 应用程序。

软件需求

本节中描述的软件可以免费下载,在运行示例 Web 应用程序之前,必须安装这些软件。

  1. 以下两种 Java 开发工具包之一:
    • IBM SDK 1.5 或更高版本。
    • Sun JDK 1.5 或更高版本。
  2. Apache Tomcat。下载 5.0.28 版本(请参阅参考资料)。
  3. Facelets,JSF View Framework。下载 1.1.11 版本(请参阅参考资料)。
  4. tagHandlers-0.9.jar 包含了所需的标记库类,以便使用带有 Facelets 的 Tomahawk 组件 <t:updateActionListener>(请参阅参考资料)。
  5. 示例应用程序源代码和 Web 应用程序 .zip 文件
    • 将 Apache_Derby_MyFaces_Demo.zip 下载到您的文件系统(请参阅下载部分)。里面包括了所有 src 文件和 Web 应用程序文件。上述下载的其他库也是运行应用程序所必需的。

软件安装

全部所需组件下载完毕后,使用以下步骤进行安装(如果您尚未进行安装)。稍后您将找到有关配置的更多内容,并获得每个组件的详细说明。

  1. 安装 JDK。如果还没有安装 1.5 版本或更高版本的 JDK,则进行安装。JDK 是运行 Tomcat 所必需的软件。
  2. 安装 Apache Tomcat。解压缩或安装 Apache Tomcat。
  3. 将 facelets-1.1.11.zip 解压缩到目录中。稍后将三个 Facelets JAR 文件复制到 WEB-INF/lib 目录中。
  4. 解压缩 Apache_Derby_MyFaces_Demo.zip。这将创建顶级目录 Flight_Reservation,及下面的子目录:
    • src —— 应用程序中所使用的全部 Java 源文件的包:
      • org.apache.derby.demo.beans.model 和 org.apache.derby.demo.beans.view —— 受 JSF 管理的 bean 类及代表底层 Model(数据库表)的其他 Java bean 类。
      • org.apache.derby.demo.filters —— 实现 javax.Servlet.Filter 接口的 LoginFilter 类。
      • org.apache.derby.demo.persistence —— 实现 DatasourceObject 接口的 DataFactory ServletContextListenerDatasourceObject 接口及 DerbyDatabase 类。
      • org.apache.derby.demo.resource —— 利用资源包(属性文件),ErrorMessages 类使得错误消息可用于应用程序。
      • org.apache.derby.demo.validators —— ForwardDates 是定制 JSF 验证器类,确保预订的航班日期是今天或未来日期。
    • Derby_MyFaces —— 此目录包含 Web 应用程序的全部库(上述下载的库除外)、类、配置文件和 XHTML 文件。
    • Licenses —— 用于 Web 应用程序中所包含的第三方库。

在您开始学习应用程序详细情况之前,了解一下关于此架构的简短说明,将有助于理解各方面是如何配合工作的。航班预订演示系统是以我在“用 Eclipse、WTP 和 Derby 构建 Web 应用程序”(developerWorks,2005年9月)一文中所编写的应用程序为基础进行开发的。对该架构进行了更改和改进,使用 JSF 和 Facelets 而不是 servlet 和 JSP 作为 Controller 层和 View 层;这也免除了配置 Tomcat 以便将 Derby 作为数据源的需求。本文中使用的 JSF 和 Facelets 基于组件的方法展示了在简化开发方面的改进,该方法在实际使用中优于较传统的 Servlet 和 JSP 方法。

航班预定系统架构

使用 Apache Derby 和 MyFaces 开发的航班预订系统是一个经过简化的航班预订应用程序,允许用户进行如下操作:

  • 登录应用程序。
  • 创建帐户。
  • 根据日期和出发城市,查看可预订的航班。
  • 根据出发城市和日期,选择目的机场。
  • 通过提供信用卡信息来预定航班。
  • 查看每个用户所预定的全部航班。
  • 登出应用程序。

设计及使用的技术

图 1 从所使用的技术和组件以及一些特定于实现的文件名和库的角度,展示了航班预订应用程序的总体设计。

图 1. 航班预订应用程序的技术、组件及实现
航班预订应用程序的技术、组件及实现
航班预订应用程序的技术、组件及实现

View —— Facelets

使用 Facelets 技术和 XHTML Web 页面实现了 View 层。若要使用 Facelets,需要在 Web 应用程序中包括 jsf-facelets.jar。因为可以使用任何 XML 工具来验证页面,并且 XHTML 语法通常比 JSP 语法更容易,所以在开发过程可以将 XHTML 用于表示层,以取代 JSP。

Controller

Controller 是由 JSF 层及 servlet 侦听器和 servlet 过滤器组成,servlet 侦听器和 servlet 过滤器与 JSF 无关,但增强了 Web 应用程序的功能。

JSF —— MyFaces

图 1 展示了 JSF 中由受管 bean 工具控制的 Java 类的部分清单。 应用程序使用了一些 Tomahawk 验证器及一个定制 validator 类。像通常的 JSF 应用程序一样, 在 faces-config.xml 文件中将导航映射出来。 XHTML 页面中使用的组件是一些标准的 JSF 组件(如 html formhtml PanelGroup),以及一些 Tomahawk 组件(如 inputCalendar,它用于显示选定日期的日历,和 selectOneListbox)。

使用 Web 应用程序生命周期事件

航班预订系统使用 servlet 容器(本例中为 Apache Tomcat)可用的 Web 应用程序生命周期,来初始化数据源和 JSF 环境,并强制用户在使用应用程序之前进行登录。

使用了两个 servlet 过滤器,一个是 LoginFilter 类,它通过 Login 页面发送应用程序的全部请求,另一个是 MyFaces ExtensionsFilter,用于装入一些 Tomahawk 组件所需的外部资源,例如图像和 JavaScript 文件。

应用程序中所使用的 servlet 上下文侦听器是 MyFaces StartupServletContextListener(用于初始化 JSF 环境),以及 DataFactory 应用程序类(用于读取属性文件,以便动态配置所使用的数据源)。在此应用程序中,通过 javax.sql.DataSource 接口的 Derby EmbeddedDataSource 实现,将 Apache Derby 用作数据源。不过,应用程序的设计允许灵活插入其他数据源 —— 例如,Java Persistent Objects (JPOX),它实现了 Java Data Objects (JDO) 规范,可使用 Derby 作为后端数据源,从而取代通过 JDBC 对数据库的直接访问。

因为 DataFactory 类实现了 ServletContextListener 接口,所以可以保证在第一次创建 servlet 上下文时调用其 init 方法。稍后将更详细地研究该类,但是通过实现 ServletContextListener 接口的类来初始化数据库资源,可以确保这些资源可用于 Web 应用程序的整个生命周期。

Model

Model 层包括 Derby 数据库引擎,由单个 .jar 文件(即 derby.jar)组成。用于对 Derby 进行 JDBC 调用的 POJO 类叫做 DerbyDatabase,它实现了另一个应用程序类 DatasourceObject 接口。

运行应用程序

将 Web 应用程序复制到 Tomcat webapps 目录

将 Flight_Reservation 目录中的整个 Derby_MyFaces 目录复制到 %TOMCAT_HOME%/webapps 目录,Flight_Reservation 目录是解压缩 .zip 文件时创建的。

现在已经拥有所有 Apache 1.1 和 2.0 授权的 JAR 文件,且捆绑到应用程序;但是需要添加您已经下载的 4 个附加 JAR 文件。

需要将下面所有的 JAR 文件复制到 %TOMCAT_HOME%/webapps/Derby_MyFaces/WEB-INF/lib 目录:

  • facelets-1.1.11 目录中的 jsf-facelets.jar
  • facelets-1.1.11/lib 目录中的 el-api.jar 和 el-ri.jar
  • 您在软件需求部分所下载的 tagHandlers-0.9.jar

现在您可以建立应用程序来确保已正确安装所有软件,且已将文件复制到正确位置。启动 Tomcat,在浏览器中打开 URL http://<hostname>:<port>/Derby_MyFaces/,用您的环境取代 hostnameport

在安装 Tomcat 时,我使用了标准配置,因此在我的环境中,URL 为 http://localhost:8080/Derby_MyFaces/。

应用程序的第一个页面如图 2 所示,输入 apacheu 作为用户名和密码。

图 2. 航班预订演示系统的第一个页面
航班预订演示系统的第一个页面
航班预订演示系统的第一个页面

开始工作吧!在您的系统上输入 apacheu 作为用户名和密码,然后单击 Submit 按钮。这将测试与 Derby 数据库的连接,因为这些值代表 USERS 表中的一行,对该表进行选择将会验证这些值是否正确。叫做 airlinesDB 的 Derby 数据库是作为 Web 应用程序的一部分进行安装的。在 Derby 中,将数据库表示为磁盘上的目录。如果配置是正确的,则应用程序的下一个页面将允许您选择出发日期和启程机场,如图 3 所示。

图 3. 选择出发日期和启程机场,SelectDateAirport
选择出发日期和启程机场,SelectDateAirport
选择出发日期和启程机场,SelectDateAirport

在继续深入学习应用程序前,请看一下在 Model 层当前场景的背后所使用的一些类。

Model —— Derby 数据源

用于数据源的三个应用程序类是:

  • DatasourceObject.java
  • DerbyDatabase.java
  • DataFactory.java

注意:在本文中,术语 data source 通常是指数据源。当引用实现 javax.sql.DataSource 接口或接口本身的类时,将使用该接口名称。

如果您想对这些 Java 文件进行更深入的研究,则浏览到解压缩 Flight_Reservation.zip 文件的目录,然后查看 Flight_Reservation/src 目录下的 org/apache/derby/demo/persistence 目录。

如前所述,应用程序的设计允许使用其他数据源,适宜于可扩展架构。若要如此,数据源必须实现 DatasourceObject 接口,如清单 1 所示。

initialize 方法用于初始化并启动数据源,shutdown 方法用于停止或关闭数据源。

清单 1. DatasourceObject 接口
package org.apache.derby.demo.persistence;

public interface DatasourceObject {

  public void initialize(String pathtoResource);

  public void shutdown();

  public boolean checkUserName(String userName);

  public UserBean getUserPassword(String userName);

  public int insertUser(String firstName, String lastName, String userName,
			String email, String password);

  public int insertUserCreditCard(String lastName, String userName,
			String creditCardType, String creditCardNum,
			String creditCardDisplay);

  public CityBean[] destAirports(String origAirport);

  public CityBean[] cityList();

  public FlightsBean[] origDestFlightList(String origAirport,
			String destAirport, Date beginDate);

  public FlightHistoryBean[] fetchFlightHistory(String userName);

  public int insertUserFlightHistory(String userName,
     FlightsBean flightsBean, String creditCardType, String creditCardNum);

}

清单 2 展示了 DerbyDatabase 类的 initialize 方法,这是实现 DatasourceObject 接口的必需条件。

清单 2. DerbyDatabase 类的 initialize 方法
public class DerbyDatabase implements DatasourceObject {

  private static EmbeddedDataSource ds = null;

  ...

  private static final String databaseName = "airlinesDB";

  public void initialize(String filePathToWebApp) {
    if (isInitialized) {
      return;
    }

    try {
      if (ds == null) {
        ds = new EmbeddedDataSource();
        ds.setDatabaseName(filePathToWebApp + "/" + databaseName);

        // Call this method only during development
	logStatements();
      }
    } catch (Exception except) {
     except.printStackTrace();
    }

    isInitialized = true;
  }
  ...
}

initialize 方法创建了 org.apache.derby.jdbc.EmbeddedDataSource,通过将数据库名添加到 Web 应用程序的完整路径中,为要使用的 javax.sql.DataSource 设置数据库名,然后调用 logStatements 方法。稍后将介绍该方法。

是什么调用了 DerbyDatabase initialize 方法,从而创建 DataSource?这就是 Web 应用程序生命周期事件开始起作用的地方。 清单 3 中所显示的 DataFactory 类实现了 ServletContextListener 接口,因此必须提供 contextInitialized 方法的实现,在第一次装入 Web 应用程序时,由 Web 容器调用该实现。

contextInitialized 方法调用了 DataFactory 类的 init 方法。然后依次调用 createDatasourceObject 方法(读取属性文件),查找字符串 datasource-type,并创建该类型的实例。在本应用程序中,此属性文件中仅有一个条目,如下所示,但是通过实现 DatasourceObject 接口,实现其他接口也是有可能的。

datasource-type=org.apache.derby.demo.persistence.DerbyDatabase

创建 DatasourceObject 接口的实现之后,调用该实现的 initialize 方法,如 initializeDatasourceObject 方法所示(请参见清单 3)。这正好返回到 DerbyDatabase initialize 方法,如清单 2 所示。

清单 3. 通过 DataFactory 类创建数据源
public class DataFactory implements ServletContextListener {

  public static final String DATASOURCEOBJECT_TYPE = "datasource-type";

  private static final String FACTORY_CONFIG = "factory.properties";

  private ServletContext servletContext;

  private static DatasourceObject datasourceObject;
  
  ...

  private synchronized void init() {

    if (isInitialized)
      return;

    createDatasourceObject();

    initializeDatasourceObject(servletContext.getRealPath(""));

    isInitialized = true;
  }

  private void createDatasourceObject() {

    InputStream is = this.getClass().getResourceAsStream(FACTORY_CONFIG);

    prop = new Properties();
    try {
      prop.load(is);
    } catch (IOException exception) {

    throw new RuntimeException(Messages
     .getString("FACTORY_CONFIG_NOT_FOUND"), exception);

    }

    String datasourceType = prop.getProperty(DATASOURCEOBJECT_TYPE);

    if (datasourceType == null)
      throw new RuntimeException(Messages
        .getString("DATASOURCEOBJECT_TYPE_NOT_FOUND"));

      datasourceObject = (DatasourceObject) createInstance(datasourceType,
				DatasourceObject.class);

  } 

  ...

  private void initializeDatasourceObject(String pathToResource) {
    datasourceObject.initialize(pathToResource);
  }
  ...

  public void contextInitialized(ServletContextEvent sce) {
    servletContext = sce.getServletContext();
    System.out.println("contextInitialized called in 
     org.apache.derby.demo.persistence.DataFactory");
    init();
  }

  ...

  public void contextDestroyed(ServletContextEvent sce) {
    datasourceObject.shutdown();
  }
}

通过提供 ServletContextListener 接口的 contextInitialized 方法的实现,在首次装入 Web 应用程序时,将创建并初始化 DatasourceObject 接口的实现(在本例中为 DerbyDatabase 类),并为该 Web 应用程序的第一次请求做好准备。

清单 2 中,在 DerbyDatabase 类的 initialize 方法中调用了 logStatements 方法。仅当您要将应用程序中的数据库查询记入 derby.log 时,才在开发环境中使用该方法,derby.log 是用于 Derby 的消息文件。这里使用它是为了说明使用 SQL CALL 语法来调用 SQL CallableStatement 的用法,如清单 4 所示。

清单 4. DerbyDatabase 类的 logStatements 和 getUserPassword 方法
  /* call this method only during development
   * logs all sql statements to derby.log for debugging purposes
   */

  public void logStatements() {
    String query = "CALL 
     SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.language.logStatementText', 'true')";
    Connection conn;

    try {
      conn = ds.getConnection();
      CallableStatement cs = conn.prepareCall(query);
      cs.execute();
      cs.close();
      conn.close();
    } catch (SQLException sqlExcept) {
        sqlExcept.printStackTrace();
      }
  }

  public UserBean getUserPassword(String userName) {
    String query = "select username, password from APP.USERS where username = ?";
    UserBean userBean = new UserBean();
    Connection conn;

    try {
      conn = ds.getConnection();
      PreparedStatement prepStmt = conn.prepareStatement(query);
      prepStmt.setString(1, userName);
      ResultSet results = prepStmt.executeQuery();

      while (results.next()) {
        String username = results.getString(1);
	String password = results.getString(2);
	userBean.setUserName(username);
	userBean.setPassword(password);
	}

	results.close();
        prepStmt.close();
        conn.close();
    } catch (SQLException sqlExcept) {
        System.out.println("Exception in getUserPassword");
	sqlExcept.printStackTrace();
      }

    return userBean;

  }

  ...
}

清单 5 展示了调用 Derby 系统程序 SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTYderby.log 文件的输出片段。它显示了使用一个参数 apacheu 来选择 USERS 表。

清单 5. 调用系统程序 SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY 时的 derby.log 输出
...
2006-08-25 00:04:42.139 GMT Thread[http-8080-Processor24,5,main] (XID = 1439), 
(SESSIONID = 1), 
(DATABASE = C:/jakarta-tomcat-5.0.28/webapps/Derby_MyFaces/airlinesDB), 
(DRDAID = null), 
Begin compiling prepared statement: select username, password from APP.USERS 
where username = ? 
:End prepared statement
2006-08-25 00:04:42.369 GMT Thread[http-8080-Processor24,5,main] (XID = 1439), 
(SESSIONID = 1), 
(DATABASE = C:/jakarta-tomcat-5.0.28/webapps/Derby_MyFaces/airlinesDB), 
(DRDAID = null), 
End compiling prepared statement: select username, password from APP.USERS 
where username = ? 
:End prepared statement
2006-08-25 00:04:42.399 GMT Thread[http-8080-Processor24,5,main] (XID = 1439), 
(SESSIONID = 1), 
(DATABASE = C:/jakarta-tomcat-5.0.28/webapps/Derby_MyFaces/airlinesDB), 
(DRDAID = null), 
Executing prepared statement: select username, password from APP.USERS where username = ? 
:End prepared statement 
with 1 parameters begin parameter #1: apacheu :end parameter 
2006-08-25 00:04:42.439 GMT Thread[http-8080-Processor24,5,main] (XID = 1439), 
(SESSIONID = 1), 
(DATABASE = C:/jakarta-tomcat-5.0.28/webapps/Derby_MyFaces/airlinesDB), 
(DRDAID = null), Committing
...

清单 4 中所示的 getUserPassword 方法是 DatasourceObject 接口所需要的方法之一。它展示了使用 SQL PreparedStatement 对 Derby 数据库进行查询的标准方式,您可能已经发现,这就是在第一个页面上输入用户名和密码时所调用的方法,该方法在 derby.log 文件中产生输出!

清单 6 中所示的 DerbyDatabase 类的 shutdown 方法演示了如何关闭由 Tomcat 启动的 airlinesDB。 使用字符串“shutdown”来调用 setShutdownDatabase 方法,然后需要调用 getConnection 来执行关闭操作。Derby 关闭数据库时,将抛出 SQLException,使用 08006 作为 SQLState,这就是 catch 块忽略此异常的原因。

表 6. 关闭 airlinesDB Derby 数据库
public void shutdown() {
  if (isShutdown) {
    return;
  }
  try {
    ds.setShutdownDatabase("shutdown");
    // necessary to actually shut down the derby database
    ds.getConnection();
  } catch (SQLException except) {
    if (except.getSQLState().equals("08006")) {
      // ignore, this is the SQLState derby throws when shutting down the database
      System.out.println("Derby database shut down.");
      isShutdown = true;
    }
    else {
      except.printStackTrace();
    }
  }
}

类似于使用 contextInitialized Web 生命周期事件来调用 DerbyDatabase 类的 initialize 方法,使用 DataFactory 类中的 contextDestroyed 生命周期事件来调用清单 6 中的 shutdown 方法,如下所示。因此,Tomcat 关闭并卸载 Web 应用程序时,将调用该类用于实现 DatasourceObject 接口的 shutdown 方法,如下所示:

public void contextDestroyed(ServletContextEvent sce) {
datasourceObject.shutdown();
}

以上涵盖了 Model 层中大部分令人感兴趣的详细信息。接下来将研究 Controller。

Controller —— 生命周期事件和 servlet 过滤器

在前面的章节中,您学习了如何使用 contextInitializedcontextDestroyed 方法,在初始化 Model 层的设置中,这些方法被称为是创建和销毁 Web 应用程序的一部分。因此,尽管我认为这些方法是 Controller 层的一部分,但在本文的 Model 部分进行了讨论。

也可以将 servlet 过滤器看作是 Web 应用程序的 Controller 层的一部分。通常,基于用户类型(或身份验证和授权)或页面功能将 servlet 过滤器用于控制应用程序流。servlet 容器提供了 javax.servlet.FilterChain 对象,使用该对象来调用链中的下一个过滤器。若要将过滤器注册作为 Web 应用程序的一部分 —— 从而调用其 doFilter 方法作为 FilterChain 的一部分 —— 必须将其添加到应用程序的 web.xml 文件。

web.xml 文件将该条目用于 LoginFilter 类,此类实现了 javax.servlet.Filter 接口。 这些条目表示凡是具有 *.jsf/Derby_MyFaces/* URL 模式的任何条目都从属于 LoginFilter 类(请参见清单 7)。

清单 7. web.xml 中的 LoginFilter 条目
<filter>
  <filter-name>login</filter-name>
  <filter-class>org.apache.derby.demo.filters.LoginFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>login</filter-name>
  <url-pattern>*.jsf</url-pattern>
</filter-mapping>

<filter-mapping>
  <filter-name>login</filter-name>
  <url-pattern>/Derby_MyFaces/*</url-pattern>
</filter-mapping>

清单 8 展示了 LoginFilter 类,负责以下操作:

  • 如果直接请求 Registration 页面 Reister.jsf,则将请求发送到该页面,同时不调用 FilterChain 对象中的任何其他 servlet 过滤器。
  • 如果会话属性“login-status”为真,则通过调用链中下一个过滤器,允许请求继续进行。
  • 如果会话属性“login-status”为空或非真,则将请求发到登录页 Welcome.jsf,同时不调用链中下一个过滤器的 doFilter 方法。
清单 8. LoginFilter 类
public class LoginFilter implements Filter {

  private FilterConfig config;

  private RequestDispatcher dispatcherLogin;

  private RequestDispatcher dispatcherRegister;

  private static final String LOGIN_PAGE = "/login/Welcome.jsf";

  private static final String REGISTER_PAGE = "/login/Register.jsf";

  public static final String AUTH_STATUS = "login-status";

  public void init(FilterConfig filterConfig) throws ServletException {

    config = filterConfig;

    dispatcherLogin = config.getServletContext().getRequestDispatcher(LOGIN_PAGE);

    dispatcherRegister = config.getServletContext().getRequestDispatcher(REGISTER_PAGE);

    } 

  public void doFilter(ServletRequest req, ServletResponse res,
      FilterChain chain) throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest) req;

    // if a request is made directly for Register.jsf send them to it
    if (request.getServletPath().equals(REGISTER_PAGE)) {
      dispatcherRegister.forward(req, res);
    }
    // otherwise if their login-status is not null
    else if (request.getSession(true).getAttribute(AUTH_STATUS) != null) {
      // if it is true, send them on to the next filter in the chain
      if (request.getSession(true).getAttribute(AUTH_STATUS) == Boolean.TRUE) {
        chain.doFilter(req, res);
        // otherwise send them to the login page
      } else {
        dispatcherLogin.forward(req, res);
      }
    }
    // if login-status is not set at all send them to the login page
    else {
      dispatcherLogin.forward(req, res);
    }

}

Controller —— 使用 Apache MyFaces 的 JSF

页面流和导航

回过头看一下图 1,JSF 是该应用程序中 Controller 层的中心。要理解应用程序的页面流,请从理解 JSF 配置文件 faces-config.xml 开始。(如果您不熟悉 JSF 且不了解 faces-config.xml 文件的用途,请参见参考资料部分,获得学习 JSF 基本知识的链接。)

简单地说,这里将研究该文件中的导航指令,这些指令映射为特定操作或结果,从而对页面进行导航。下面列出了应用程序中的页面流和每个页面的功能。

  1. Welcome.xhtml:登录现有的用户。
  2. Register.xhtml:注册新用户。
  3. SelectDateAirport.xhtml:选择出发日期和启程机场。
  4. DestinationAirport.xhtml:根据启程机场,显示目的机场,允许您选择其中一个机场。
  5. FlightList.xhtml:根据所选择的目的机场和启程机场,显示可用的航班。
  6. CreditCard.xhtml:根据所选择的航班,允许用户输入信用卡信息。
  7. FlightHistory.xhtml:显示该用户预订的所有航班。
  8. LoggedOut.xhtml:退出应用程序。

图 4 展示了 faces-config.xml 文件中导航部分的图形化表示。

图 4 中上半部分的页面涉及注册、登录和退出。沿着与航班选择、预订和支付相关的页面流由顶部开始前进,查看下半部分,先是左象限,然后是右下象限。

举例说明该图,找到表示 /flights/SelectDateAirport.xhtml 页面的橙色方块。指向此方块的两个箭头表示两个可接受的结果,其中任意一个(即 logged_in 和 back_to_flights)都必须在使用页面之前 就存在。这些就是清单 9 中以文本表示的 faces-config.xml 文件中展示的所需结果。在下一节,您将看到如何获得这些所需结果之一。

图 4. faces-config.xml 文件中的页面导航
faces-config.xml 文件中的页面导航
faces-config.xml 文件中的页面导航
清单 9. faces-config.xml 中的两个导航例子,用于 SelectDateAirport 页面
<navigation-case>
    <from-outcome>logged_in</from-outcome>
    <to-view-id>/flights/SelectDateAirport.xhtml</to-view-id>
</navigation-case>

<navigation-case>
    <from-outcome>back_to_flights</from-outcome>
    <to-view-id>/flights/SelectDateAirport.xhtml</to-view-id>
</navigation-case>

受管 bean、操作和结果

进行引用时,JSF 的受管 bean 创建工具将自动创建 bean,进行初始化,然后将其存储在适当的域。创建了 bean 且不再使用它时,该 bean 将返回给此工具。

受管 bean 创建工具的一个有利因素是能够使用 JSF 的表达式语言语法来引用 bean,您将发现大多数 XHTML 页面都使用了 JSF 的表达式语言语法。

现在我们来看一下与清单 9 中产生的 <from-outcome> 相关的应用程序的一个受管 bean。 清单 10 展示了 faces-config.xml 文件的另一个片段,它演示了 loginBean,其中对 JSP 应用程序中所使用的全部受管 bean 都进行描述。

清单 10. 在 loginBean 中对 JSF 应用程序中所使用的全部受管 bean 都进行描述
<managed-bean>
  <managed-bean-name>loginBean</managed-bean-name>
  <managed-bean-class>org.apache.derby.demo.beans.model.LoginBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

bean 具有 session 域,这样对用户是否登录进行跟踪才有意义。用户会话结束(通过注销或关闭浏览器)后,必须为该用户重建 bean。

清单 11 中展示了 loginBeanauthenticate 方法。按下图 2 中的 Submit 按钮后,将调用该方法。

清单 11. LoginBean 类的部分清单
public class LoginBean implements Serializable {

  private String username;

  private String password;

  private String loggedIn = "false";

  public LoginBean() {
    username = "";
    password = "";
  }

  public LoginBean(String userName, String passWord) {
    username = userName;
    password = passWord;
  }

  // verify if the username already exists
  // verify if the username and password match what is in the data source
  public String authenticate() {
    String result = "failure";
    FacesContext context = FacesContext.getCurrentInstance();
    DatasourceObject datasourceObject = DataFactory.getDatasourceObject();

    // create a UserBean by retrieving the username and password from
    // the database if the username entered on the page is in the db
    UserBean userBean = datasourceObject.getUserPassword(username);
    String userName = userBean.getUserName();

    // verify the name entered on the page and the one in the db
    // are the same
    if (userName != null && userName.equals(username)) {

      String actualPassword = userBean.getPassword();
      // verify the passwords are the same
      if (actualPassword != null && actualPassword.equals(password)) {
        // set the session attribute LoginFilter.AUTH_STATUS to true
        ((HttpSession) context.getExternalContext().getSession(true))
          .setAttribute(org.apache.derby.demo.filters.LoginFilter.AUTH_STATUS,
				Boolean.TRUE);
        setLoggedIn("true");
	// set 'result' to "logged_in" which maps to the faces-config navigation case
	// for the /flights/SelectDateAirport.xhtml <from-outcome> value.
        result = "logged_in";
      }
      // the userName and the username match ... but the passwords don't
      else {
        String failureMsg = ErrorMessages.getString("LoginBean.LOGIN_FAILURE");
	FacesMessage message = new FacesMessage(
	    FacesMessage.SEVERITY_ERROR, failureMsg, failureMsg);
	context.addMessage("login_form:loginButton", message);
      }
      // the userName is not null but it does not match the username
    } else {
      String failureMsg = ErrorMessages.getString("LoginBean.LOGIN_FAILURE");
      FacesMessage message = new FacesMessage(
        FacesMessage.SEVERITY_ERROR, failureMsg, failureMsg);
      context.addMessage("login_form:loginButton", message);
    }
    return result;
    }  
 ...

DatasourceObject(在本例中是 DerbyDatabase 类)上调用 getUserPassword 方法来创建 UserBean。该 UserBean 表示从 Derby 数据库返回单行或者不返回任何行,这取决于在表单中输入的用户名是否与数据库中的用户名相匹配。如果从数据库返回了一行,则使用非空的用户名和密码成员变量来填充 UserBean

如果从数据库检索到一行,则说明 userName 变量和 actualPassword 变量是非空的。这些值与表单中的值相比较。如果相匹配,则在用户的会话中设置属性,指示已登录。请记得如何在 doFilter 方法中检查该会话属性(清单 8)?authenticate 方法就位于设置它的地方。最后,因为成功进行了身份验证,所以将字符串变量 result 设置为 logged_in。(如果身份验证不成功,则保持 failure 值)。

logged_in 值就是您在清单 9 中看到的所需值,它允许导航到下一页,即 /flights/SelectDateAirport.xhtml,如图 3 所示。

MyFaces 验证器

Web 应用程序中表单条目的验证将确保在处理数据前,数据的格式或范围是正确的;例如,向数据库插入值前。使用灵活性以及将错误消息与正在验证的组件相关联的功能,是 JSF 在进行验证时所采用的方法的理想特征。JSF 的 MyFaces 实现(特别是 Tomahawk 组件)包括随时可用的验证器,这减少了为很多公共数据条目类型编写定制验证器的需求。

查看图 2 所示的应用程序的第一个页面,您可以选择注册新用户。单击 Register 按钮,将转到 Register.xhtml 页面。图 5 展示了使用不正确的值填写文本字段所得到的结果以及单击 Submit 按钮所得到的结果。在该页面上使用了 validateRegExprvalidateEmailvalidateEqual Tomahawk 组件。

图 5. 使用 MyFaces Tomahawk 验证器进行验证
使用 MyFaces Tomahawk 验证器进行验证
使用 MyFaces Tomahawk 验证器进行验证

若要了解正则表达式验证器的用法,以及为 lastname 文本字段所显示的相关错误消息,请查看 Register.xhtml 的源代码片段,如清单 12 所示。

清单 12. Register.xhtml 的部分源代码
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<t:document xmlns="http://www.w3.org/1999/xhtml" 
            xmlns:ui="http://java.sun.com/jsf/facelets" 
            xmlns:h="http://java.sun.com/jsf/html" 
            xmlns:f="http://java.sun.com/jsf/core" 
            xmlns:t="http://myfaces.apache.org/tomahawk">
...

<h:form id="register_form">
 ...

<h:outputLabel for="lastname" styleClass="standard" 
   value="#{messages['register_lastname']}" />
<h:inputText id="lastname" value="#{userBean.lastName}" required="true" maxlength="40">
  <t:validateRegExpr pattern='[a-zA-Z]+' />
</h:inputText>
<t:message id="lastNameError" for="lastname" styleClass="error" />
...

</h:form>

JSF HTML 标记 <h:outputLabel> 指定了所关联的组件、用于标签的 CSS 样式以及从 messages 消息包检索到的标签值。

HTML inputText 字段拥有 lastname 的 ID,并且将该文本字段的值指定给 UserBeanlastName 属性。下一行代码展示了 Tomahawk validateRegExpr 验证器和用于加强验证的正则表达式模式的用法。这里,文本字段的字符串必须是任何 alpha 字符,其长度为 1 到 40 个字符,如 inputText maxlength 属性中所指定的。

Tomahawk message 组件必须与另一个组件相关联,以便报告错误。使用其 for 属性完成了上述工作,该属性与所关联组件的 id 属性相匹配。现在清楚了在页面上为什么文本字段不允许使用值 !Cline,以及为什么用红色(红色用于 CSS error 样式)报告错误消息了吧!

正则表达式的一个优点是能够定制所使用的正则表示式。例如,对于 username 和 password 文本字段,我指定了模式 pattern='[a-zA-Z0-9]+' 作为正则表达式,这样提高了该组件的灵活性和可用性,以便用于多种情形。

多个验证器可以用于同一个组件,如清单 13 中所示的 passwordVerify inputSecret 标记。(添加换行符是为了增加可读性。)

清单 13. 多个验证器用于同一个组件
<h:outputLabel for="password" styleClass="standard" 
   value="#{messages['register_password']}" />

<h:inputSecret id="password" value="#{userBean.password}" required="true" maxlength="20">
  <t:validateRegExpr pattern='[a-zA-Z0-9]+'>
</h:inputSecret>

<t:message id="passwordError" for="password" styleClass="error" />

<h:outputLabel for="passwordVerify" styleClass="standard" 
  value="#{messages['register_password_verify']}" />
<h:inputSecret id="passwordVerify" value="#{userBean.passwordVerify}" required="true" 
    maxlength="20">
  <t:validateRegExpr pattern='[a-zA-Z0-9]+' />
  <t:validateEqual for="password" />
</h:inputSecret>

<t:message id="passwordVerifyError" for="passwordVerify" styleClass="error" />

更多 Tomahawk 组件

在探索更多 Tomahawk 组件之前,我们更深入地研究一下应用程序。回到您在应用程序中作为 apacheu 用户的地方。在图 3 中,您打算选择 9 月28 日从 Toronto 出发的航班。图 6 展示了选择的结果。

图 6. 从 Toronto 出发的可供预订的航班
从 Toronto 出发的可供预订的航班
从 Toronto 出发的可供预订的航班

该页面没有什么很值得注意的地方 —— 它使用了标准 JSF 组件。如果已经安装了该应用程序,那么您可以看一下此页面的源代码。每个页面的右下角都有一个链接,即 Show Source。 选择该链接,您可以查看当前所显示页面的 XHTML 源代码。

单击该页面的 Go 按钮,转到 FlightList.xhtml。此页面使用了 Tomahawk dataTablecommandLinkupdateActionListener 组件。

图 7. 从 Toronto 到 London 的航班列表 —— 其他 Tomahawk 组件
从 Toronto 到 London 的航班列表 —— 其他 Tomahawk 组件
从 Toronto 到 London 的航班列表 —— 其他 Tomahawk 组件

从现在开始,忽视清单 14 中的 dataTable 组件。它实质上表示了具有一些其他功能的 HTML 表格,稍后将讨论这些功能。这里,看一下嵌套在 commandLink 组件中的 updateActionListener 组件。

清单 14. commandLink 和 updateActionListener 组件
...

<t:dataTable id="data" styleClass="scrollerTable" headerClass="standardTable_Header" 
 footerClass="standardTable_Header" rowClasses="standardTable_Row1,standardTable_Row2" 
 columnClasses="standardTable_ColumnCentered" var="flight"
 value="#{flightConfig.availableFlights}" preserveDataModel="true" rows="5">

 <h:column>
  <f:facet name="header">
   <h:outputText value="#{messages['flight_id']}" />
  </f:facet>
  <t:commandLink id="command_link" action="#{flight.selectedFlight}" immediate="true">
   <h:outputText value="#{flight.flightId}" />
   <t:updateActionListener property="#{flightBean}" value="#{flight}" />
  </t:commandLink>
</h:column>

...

updateActionListener 组件必须有 ActionSourcecommandLinkcommandButton)作为其父组件。commandLinkflight 变量的 flightId 输出文本值相关联,单击后,将发生以下情况:

  • 可能将 commandLink 操作的值指定为方法绑定,在本例中,对应的是 FlightsBean 类中的 selectedFlight() 方法。 该方法所做的事情就是返回字符串,用作下一个页面的导航结果。查找图 4 中的字符串 get_selected_flight,以便查看要导航到的页面。
  • updateActionListener 组件将其 value 属性指定给 property 属性。 这意味着将包含在 flight 变量中的值指定给受管 bean,即 flightBeanflight 变量表示了 FlightsBeanjava.util.List 中的单个 FlightsBean 对象,FlightsBeanflightConfig bean 的 availableFlights 变量来表示。这是通过 dataTable 组件来实现的,该组件遍历 availableFlights 列表中所包含的 FlightsBeans,并将每个 FlightsBean 实例放入 flight 变量中。

为什么 updateActionListener 组件在这里很重要?一旦获取了信用卡信息,那么下一个页面将允许用户预定航班。如果未能传递指定的航班 (FlightsBean),则失去信用卡信息。

图 8 展示了选择航班号 US1592(从 Toronto 飞往 London 的航班)的结果,填写信用卡信息,然后单击 Charge My Credit Card 按钮。

图 8. 预订航班
预订航班
预订航班

Tomahawk CreditCard Validator

显然,有一个用于 Credit Card 字段的验证器,如图 8 所示。清单 15 展示了 CreditCard.xhtml 页面的部分源代码。

清单 15. CreditCard.xhtml、validateCreditCard Tomahawk 标记
...
<h:panelGrid columns="3">
 <h:outputLabel for="creditCardNumber" styleClass="standard" 
   value="#{messages['validate_credit']}" />
  <h:inputText id="creditCardNumber" value="#{creditBean.creditCardNumber}" 
    required="true" size="16" maxlength="16" immediate="true">
   <t:validateCreditCard amex="true" visa="true" mastercard="true" discover="false" />
  </h:inputText>
  <t:message id="creditCardNumberError" for="creditCardNumber" styleClass="error" />
...

<h:form id="validate_cc">
 <h:panelGrid columns="1">
  <h:selectBooleanCheckbox id="r2" value="#{creditBean.creditCardValidate}"  
   immediate="true" 
    onclick="submit()" valueChangeListener="#{creditBean.handleValidation}">
   <h:outputText value="#{messages['validate_cc']}" styleClass="standard"/>
  </h:selectBooleanCheckbox>
  <br/>
 </h:panelGrid>
</h:form>

validateCreditCard 组件将接受 5 个属性 —— 其中 4 个如清单 15 所示,还有一个是 none。我希望接受 Amex、Visa 和 MasterCard 信用卡,但不接受 Discover。该组件使用 Jakarta Commons Validation。

注意:此验证确实工作!除非您禁用验证或输入有效的信用卡卡号,否则不要期望能够进入下一个页面。

清单 15 中的第二个代码片段使用了 HTML selectBooleanCheckbox,以便动态处理禁用或启用验证操作。只要激活事件(immediate 属性值为 true),onclick 属性就会提交复选框 #{creditBean.creditCardValidate} 的值(布尔值)。valueChangeListener 属性指定了用于接收此事件的方法,即 UserCreditCardBeanhandleValidation 方法。

若要测试上述内容,则通过取消选中 Enable Validation of Credit Card 复选框来禁用信用卡验证,然后单击 Charge My Credit Card 按钮。为了解动态验证的工作方式,请看一下清单 16UserCreditCardBean 类的 handleValidation 方法,因为它是选中或取消选中该复选框时所调用的方法。

清单 16. UserCreditCardBean 处理动态验证
public void handleValidation(ValueChangeEvent event) {
  Object value = event.getNewValue();
		
  if (value == Boolean.TRUE) {
    enableValidateCC();
  }
  else {
    disableValidateCC();
  }
		
}
...
public String enableValidateCC() {

  FacesContext facesContext = FacesContext.getCurrentInstance();

  UIInput creditCardNumber = (UIInput) facesContext.getViewRoot()
   .findComponent("credit_card_form:creditCardNumber");
  Validator[] validators = creditCardNumber.getValidators();
  if (validators == null || validators.length == 0) {
    creditCardNumber.addValidator(new CreditCardValidator());
  }
  return "ok";
}
...
public String disableValidateCC() {

  FacesContext facesContext = FacesContext.getCurrentInstance();
		
  UIInput creditCardNumber = (UIInput) facesContext.getViewRoot()
   .findComponent("credit_card_form:creditCardNumber");
  Validator[] validators = creditCardNumber.getValidators();
  if (validators != null) {
    for (int i = 0; i < validators.length; i++) {
      Validator validator = validators[i];
      creditCardNumber.removeValidator(validator);
    }
  }

  return "disabled";
}

使用与传递到 handleValidation 方法的事件相关联的新值来确定是否启用或禁用信用卡验证。这是复选框组件的 Boolean 值。两个方法(enableValidateCCdisableValidateCC)获取 FacesContextcurrentInstance,然后得到 creditCardNumber 输入文本字段(参见清单 15)对应的 UIInput 组件,并且根据所进行的调用是启用或禁用验证来添加或删除验证器。

Tomahawk 的扩展 DataTableJSCookMenu 组件

禁用信用卡验证后,我预订了从 Toronto 飞往 London 的航班,然后我决定预订一些其他航班。图 9 展示了上述结果。该页面重点说明了 Tomahawk 的扩展 DataTableJSCookMenu 组件的用法。

图 9. 航班历史 —— JSCookMenu 及可排序的 DataTable
航班历史 —— JSCookMenu 及可排序的 DataTable
航班历史 —— JSCookMenu 及可排序的 DataTable

Tomahawk DataTable 组件为标准 JSF DataTable 添加了两项功能:

  • 能够保存 DataModel 的状态,当数据被数据库连接退回时,此功能是很重要的,因为自最后一次请求以来,数据库中的数据可能已经更改。
  • 支持单击即可排序的头 部

清单 17 展示了 FlightList.xhtml 页面中一些 Tomahawk dataTable 组件的 XHTML。 我已经将 preserveDataModel 属性设置为 true,并且将 sortColumnsortAscending 属性的值绑定指定为 FlightsHistory 受管 bean。同时请注意 dataTable 标记中 Tomahawk commandSortHeader 标记的用法。该组件派生自 commandLink actionSource 组件。

清单 17. FlightList.xhtml 页面中 Tomahawk dataTable 组件的部分 XHTML
<t:dataTable id="data" styleClass="scrollerTable" headerClass="standardTable_Header" 
  footerClass="standardTable_Header" rowClasses="standardTable_Row1,standardTable_Row2" 
  columnClasses="standardTable_ColumnCentered" var="bookedFlight"
  value="#{flightHistory.bookedFlights}" preserveDataModel="true" 
  rows="8" sortColumn="#{flightHistory.sort}" 
  sortAscending="#{flightHistory.ascending}" preserveSort="true">

  <h:column>
   <f:facet name="header"> 
    <t:commandSortHeader columnName="flight_id" arrow="true"> 
     <h:outputText value="#{messages['flight_id']}" /> 
    </t:commandSortHeader> 
   </f:facet>
   <h:outputText value="#{bookedFlight.flightId}" />
  </h:column>
...

表的排序是如何进行的呢?单击 Charge My Credit Card 按钮(参见图 8),调用 UserCreditCardBean 类中的方法 submitCC()。这调用了 FlightsHistory 类中 findBookedFlights 方法。该方法使用该用户所预订的全部航班来填充 java.util.List。单击其中一个 commandSortHeader 链接,将基于该列中的值对行进行排序。发生此事件时,需要从 flightHistory bean 中检索 bookedFlights。这期间,它调用了 FlightsHistory 类的 getSort() 方法。根据所选择的表头,getSort() 方法将使用该类中所实现的 java.util.Comparator 对列表进行排序。若要自己研究代码,请打开 FlightsHistory.java 文件。

单击表头进行试验,以查看如何进行排序。

您将学习的最后一个 Tomahawk 组件是 JSCookMenu,如图 9 底部所示。菜单项 Logout 被高亮显示。JSCookMenu 使用组件中的 JavaScript 和 CSS 来创建外观好看的菜单项。

FlightHistory.xhtml 中使用 JSCookMenu 标记的源代码如清单 18 所示。

清单 18. JSCookMenu 标记
<h:form id="dummy2">
 <h:panelGrid columns="2">
  <t:jscookMenu layout="hbr" theme="ThemeOffice">
   <t:navigationMenuItem id="nav_1" itemLabel="#{messages['nav_back_to_flights']}" 
     action="back_to_flights" />
    <t:navigationMenuItem id="nav_2" itemLabel="#{messages['nav_other_choices']}">
    <t:navigationMenuItem id="nav_2_1" itemLabel="#{messages['nav_register_user']}" 
     action="register_new_user" />
    <t:navigationMenuItem id="nav_2_2" itemLabel="#{messages['nav_logout']}" 
    action="logout" split="true" />
   </t:navigationMenuItem>
  </t:jscookMenu>
 </h:panelGrid>
</h:form>

jscookMenu 的 layout 属性设置为 hbr,表示菜单是水平布局的,子菜单向底部、向右布局。其他值为 hbl(水平、底部、向左)、hur(水平、顶部、向右)、hul(水平、顶部、向左)、vbr(垂直、底部、向右)、vbl(垂直、底部、向左)、vur(垂直、顶部、向右)和 vul(垂直、顶部、向左)。theme 属性指定了使用 CSS 的组件的外观。图 10 展示了如果将 theme 切换到 ThemeMiniBlackJSCookMenu 在页面上的外观。

图 10. 具有 ThemeMiniBlack 的 JSCookMenu
ThemeMiniBlack
ThemeMiniBlack

View —— Facelets 和 XHTML

到目前为止,您已经学习了应用程序的架构以及 Model、Controller 和 MyFaces 组件。同时,由于 JSF 组件公开为标记库,因此我们查看了一些 XHTML 页面的源代码。现在,开始探索如何使用 Facelets,以及它使 JSF 应用程序开发变得更容易的原因。

前面我已经说过我对该应用程序进行了改造,从严格使用 servlet 和 JSP 更改为使用 MyFaces 和 Facelets。首先,我将非 JSF JSP 移植到基于 JSF 的 JSP,然后学习 Facelets,并将其从 JSP 更改为 XHTML 文件。这毫不费力,因为 Facelets 以 JSP 标记所采用的方式来支持所有的 JSF UIComponents。实际上,我只需对我的 JSP 页面中几行代码进行更改,但是可以创建模板,以便模块化更多页面(较之使用 JSP 时要多)。

为 Facelets 配置 JSF 应用程序

由于 JSF 的默认视图处理器是 JSP,需要在 faces-config.xml 文件进行更改,如清单 19 所示。<view-handler> 标记指定了用于视图的 FaceletsViewHandler 类。可以在刚才下载的且已复制到 Tomcat webapps/Derby_MyFaces/WEB-INF/lib 目录的 jsf-facelets.jar 文件中找到该类。

清单 19. 修改 faces-config.xml 文件
<application>
  <!-- Use Facelets instead of JSP-->
  <view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>

除了 faces-config.xml 文件需要其他条目,应用程序的 web.xml 文件也需要一些附加条目。为了将视图所使用的页面类型前缀由 JSP 更改为 XHTML,您必须添加 javax.faces.DEFAULT_SUFFIX 参数。使用带有 Facelets 的标记库时所需要的另一个参数是 facelets.LIBRARIES 参数。用于 MyFaces Tomahawk 组件且带有 Facelets 的标记库是 tomahawk.taglib.xml,它包含了其他 Tomahawk 组件的映射。将标记名映射到实现组件的 Java 类及呈现类(如果是可视化组件)。最后一个参数 facelets.DEVELOPMENT 不是必需的,但是它允许您使用 Facelets 的错误处理工具。清单 20 展示了 web.xml 文件中的这些参数和值。

清单 20. web.xml 文件的其他条目,用于 Facelets 和 Tomahawk
<context-param>
  <param-name>javax.faces.DEFAULT_SUFFIX</param-name>
  <param-value>.xhtml</param-value>
</context-param>

<context-param>
  <param-name>facelets.LIBRARIES</param-name>
  <param-value>/WEB-INF/tomahawk.taglib.xml</param-value>
</context-param>

<context-param>
  <param-name>facelets.DEVELOPMENT</param-name>
  <param-value>true</param-value>
</context-param>

如果您想学习更多详细内容,请参阅参考资料部分,获得到 MyFaces wiki 条目的链接,里面有关于启用 Tomahawk 应用程序以使用 Facelets 的详细内容。

既然您准备使用 Facelets,请查看为使用它而创建的一些 XHTML 页面和模板。

XHTML 和 Facelets 模板化

清单 21 展示了应用程序中所有 XHTML 文件的前几行。引用的名称空间是用于 JSF HTML 和 core 标记、Tomahawk 标记和 Facelets 标记的。虽然 HTML 和 core 标记引用了 Sun 名称空间,使用了 MyFaces 库,但实际上是由 Apache 实现的。

清单 21. XHTML DOCTYPE 和 XML 名称空间
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
   http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <t:document xmlns="http://www.w3.org/1999/xhtml" 
  xmlns:ui="http://java.sun.com/jsf/facelets" 
  xmlns:h="http://java.sun.com/jsf/html" 
  xmlns:f="http://java.sun.com/jsf/core" 
  xmlns:t="http://myfaces.apache.org/tomahawk">
 ...
</t:document>

我使用了 Facelet 的模板化功能来删除 JSP 中标题、页眉和页脚区域中的多余文本,同时允许页面主体中的内容不同。若要使用 Facelets 中的模板,首先您必须创建一个页面,作为应用程序中页面内容的结构。思路是将模板页划分为 Web 页面的标准区域,如标题、页眉、主体和页脚,然后为每个区域提供默认内容,同时为重写该内容留出空间。

清单 22 是 template.xhtml 的简化版本(已经删除名称空间声明和 JavaScript 函数)。

清单 22. 应用程序的模板文件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
...
<t:documentHead>
  <link rel="stylesheet" type="text/css" href="../css/basic.css" />
  <title>
   The Apache Derby and MyFaces Flight Reservation Demo
  </title>
</t:documentHead>

<t:documentBody>

  <div id="header">
   <ui:insert name="page_title">
    <ui:include src="pageTitle.xhtml" />
   </ui:insert>
  </div>

<div id="body">
  <br />
   <ui:insert name="body">
   </ui:insert>
  <br />
</div>


<div id="footer">
  <ui:insert name="footer">
   <div class="pageFooter">
    <script type="text/javascript">
document.write
("<a href='" + getSourceUrl() + "#{mypage}.txt'>Show Source</a>");
    </script>
   </div>
  </ui:insert>
</div>

</t:documentBody>
</html>

该模板页面使用了 Facelets 库中的 insertinclude 标记,与 ui 名称空间相关联。insert 标记允许将内容插入到由 insertname 属性所引用的页面区域。例如,一个准备使用该模板的页面需要使用 define 标记,将其 name 属性指定为 insert 标记中所引用的同一 name 值。一会儿,您将看到一个使用 define 标记的页面示例,用来说明其工作方式。

看一下清单 22 中其 idheaderdiv 标记。该标记中嵌套了 insertinclude 标记。insert 标记有 page_title 名称,include 标记指向 pageTitle.xhtmlsrc。这表示具有 header id 的 div 包括了 pageTitle.xhtml 页面的内容,也就是文本 The Apache Derby and MyFaces Flight Reservation Demo,同时允许用其他内容重写该内容。

现在看一下 Register.xhtml,该页面引用并使用了模板页面 template.xhtml,且引入了 definecomposition 标记(参见清单 23)。

清单 23. Register.xhtml,使用了 composition、define 和 param 标记
...
 <ui:composition template="../format/template.xhtml">
  <ui:define name="page_title">
   <div class="pageHeader">
    Register to use the Apache Derby and MyFaces Flight Reservation Demo
   </div>
   </ui:define>

  <ui:define name="body">
   <f:view>

   ...

   </f:view>
  </ui:define>

  <ui:param name="mypage" value="Register.jsf" />
 </ui:composition>
</t:document>

清单 23 中所示的 composition 标记有一个 template 属性,指向清单 22 中所示的模板页面。看一下 ui:define 标记在两个地方的用法。第一个的名称是 page_title,允许您重写页眉的值。在清单 22 中,页眉是 The Apache Derby and MyFaces Flight Reservation Demo。在这里,我将其更改为 Register to use the Apache Derby and MyFaces Flight Reservation Demo。第二个 ui:define 标记让您指定页面主体,在模板页面中没有默认内容。这表示如果从页面忽略名为 bodyui:define,则在其 composition 标记中引用 template.xhtml 的任何页面的主体部分都不会有输出。

清单 23 中需要查看的最后一个标记是 ui:param 标记。该标记的 namevalue 属性都是必需的。在本例中,我使用了 param 标记,将当前页面的名称传递给模板页面,以便动态输出链接,用于每个页面底部的 Show Source 功能。

若要了解使用参数的地方,请查看清单 22 中的 footer div。通过使用 document.write 函数,一小部分 JavaScript 用于动态写入输出。通过调用函数 getSourceUrl()(未显示)来构造 URL,该函数获取 Web 应用程序的实际 URL。JSF EL 表达式 #{mypage} 用于输出通过 ui:param 标记传递的页面名称。向其添加 .txt 扩展名,并创建正确格式的 HTML <a href> 标记,用于输出 Show Source 链接。

Facelets 错误消息处理

本文的最后一个主题是 Facelets 中改进的错误消息处理,这通过将 facelets.DEVELOPMENT 参数添加到 web.xml 文件完成,如清单 20 所示。为了演示在使用 Facelets 的浏览器中错误是如何出现的。我故意在 Welcome.xhtml 文件中将 LoginBean 的属性名由正确值 username 更改为错误值 usersname。下面是 Welcome.xhtml 中的一行代码,现在是不正确的:

<h:inputText id="username" value="#{loginBean.usersname}" required="true" maxlength="20">

图 11 展示了调用应用程序第一个页面 Welcome.xhtml 时的输出,它启用了错误处理功能。将其与图 12 (禁用错误处理)中的普通 HTTP Status 500 错误和栈跟踪进行对比。

图 11. Facelets 错误消息显示
Facelets 错误显示
Facelets 错误显示
图 12. 禁用 Facelets 错误消息工具
Facelets 错误显示
Facelets 错误显示

虽然在栈跟踪中直接报告了错误的根本原因,但是显然 Facelets 错误报告是个进步。它不仅可以立即告诉您错误原因(通过展开 + 符号来实现),而且还允许您查看组件树、请求参数和属性,以及会话和应用程序属性。最后,请注意页面怎样开始呈现 —— 您可以看到没有错误的页眉和开始文本。这有助于您可视化地确定问题在页面上处于什么位置,这是附加的信息源。

结束语

本文演示了如何使用 Apache MyFaces、Apache Derby 和 Facelets 来开发 JSF Web 应用程序。使用 EmbeddedDataSource 对一些 Derby 功能进行了探索,通过 ServletContextListener 来启动和停止 Derby,还使用了 JDBC Prepared 和 Callable Statements。

本文演示了 Apache MyFaces Tomahawk 验证器和组件的用法以及浏览器的可视化输出。研究了 Web 应用程序的导航,并在 JSF 配置文件 faces-config.xml 中映射出来。还研究了一些 Facelets 功能,例如模板化和错误处理,以及如何配置基于 Tomahawk 的 JSF 应用程序,以便使用 Facelets。

Apache Derby 的轻松开发、嵌入式本质和零管理特征;MyFaces 组件广泛的强大功能;Facelets 的易用性和可用性。这些功能共同构建了一个功能强大的三合一技术,用于开发稳定的、动态的、基于 JSF 的 Web 应用程序。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Information Management
ArticleID=187991
ArticleTitle=用 Apache Derby、Apache MyFaces 和 Facelets 开发应用程序
publish-date=01112007