HTML5-和-JSF-高级教程-全-
HTML5 和 JSF 高级教程(全)
零、简介
对于打算用支持 HTML5 的 JSF 构建复杂的企业级 web 体验的 Java 开发人员来说,这本书是一个理想的资源。如今,Java web 开发人员拥有前所未有的各种库、框架和组件,可以用来创建丰富而有意义的应用。结果是一个生态系统,在个人层面上提供令人难以置信的生产力。
也就是说,大多数软件是在团队环境中构建的,具有共享的代码库、多个服务端点、复杂的视图和不同的功能区域。在团队环境中,大量的选择可能会适得其反,导致技术债务的增加和相似用例的多种实现模式。像 JavaServer Faces (JSF)这样的基于组件的框架旨在抑制团队环境中的这种行为,同时仍然给开发人员在用例需要时进行定制的自由。
在 Pro JSF 和 HTML5 中,我们向您展示了如何充分利用 JSF 的潜力,这是一个面向服务器端组件的 Java web 框架,为开发团队的生产力设定了标准。它为开发人员提供了丰富的标准和第三方界面组件,可以用来以直观、可重用和可重复的方式组成复杂的视图。最新的标准 JSF 2.2 允许在视图的组成中使用支持 HTML5 的组件。我们展示了开发人员如何将 HTML5 的功能和表达能力与 JSF 组件的规则和可重复性结合起来,以提供丰富的用户体验。
这本书为 JavaServer Faces(尤其是 JSF 2.2)的基础和高级主题提供了坚实的基础。前四章涵盖了 JSF 生命周期、架构、受管 beans 和与 CDI 的集成(上下文和依赖注入)、表达式语言、异常处理、转换和验证、JSF 事件(Faces 事件、Phase 事件和系统事件)以及视图参数。第五章涵盖了 JSF 2.2 的新特性,比如 faces flow、资源库契约、HTML5 友好标记、Ajax 文件上传等。
第六章、第七章和第八章详细介绍了在 JSF 2.2 世界中创建支持 HTML5 的组件的过程,并提供了一些交互示例。第九章介绍了两个流行的 JSF 组件库(PrimeFaces 和 RichFaces)的基础知识,并给出了两个交互示例。
在第十章中,开发了一个基本的 JSF 2.2 应用,该应用利用 Java EE 7 技术(CDI、JPA 2.1 和 EJB 3.2)来促进 JSF 应用中的 bean 管理、事务管理和持久性。第十一章涵盖了 JSF 的高级主题,比如应用设计考虑、单元测试和 Ajax 队列。
第十二章涵盖了与 JSF 应用安全相关的重要主题,包括认证、授权和数据保护。本章展示了如何在一个示例应用中应用容器管理的安全性。第十二章还介绍了 JSF 性能考虑因素,以帮助确保您的应用运行平稳、响应迅速。
最后,第十三章收集了书中涵盖的大部分主题,并以实现现实世界用例的高级应用的形式将它们付诸实践。
我们希望这本书成为面向组件框架初学者的渐进指南,并作为经验丰富的 JSF 开发者的参考,他们正在寻求最大化 JSF 2.2 显著升级的功能。我们祝您阅读愉快,编码高效!
一、JSF 简介
本章将解释什么是 JavaServer Faces (JSF) 框架,该框架如何随着时间的推移而发展,其架构的关键方面,以及关于其签名请求处理生命周期的细节。除了解释之外,我们将亲自动手,指导您使用 JSF 2.1 和 Maven 3 从头开始开发您的第一个 JSF 应用;您将学习如何在两个不同的 web 容器(GlassFish 和 Tomcat)上部署 JSF 2.1 应用。如果您已经在基本的组件级别上熟悉了 JSF,那么在处理更复杂的应用时,对请求生命周期的深入理解将会对您大有裨益。
什么是 JSF?
JSF 是一个服务器端的面向组件的 Java web 框架,它简化了开发富 Java 企业 web 应用的过程。JSF 擅长提供高度可定制的标准化方法来构建应用用户界面。用户界面层通常是任何应用中最具挑战性和最易变的部分。这也是被广泛采用并不断发展的成功应用与勉强采用并频繁更改以满足用户需求的应用之间的区别。
JSF 为解决 Java 企业 web 应用开发中经常出现的常见问题提供了一个强大的平台,例如验证、转换、导航、模板和页面流。JSF 为解决 web 应用开发的常见问题提供了一种标准的方法,这使得它成为一个优秀的框架,可以减少 web 应用的开发和维护时间。
当您的开发团队很大并且是分布式的时,尤其如此,这是企业中常见的情况。围绕一组标准化的 JSF 组件构建用户体验允许一定程度的定制和表达,但同时也为应用在不同实现间的外观、行为和响应建立了“共享 DNA”。
JSF 为以下应用提供 API 和标记库
- 为应用的快速开发提供 UI 组件。
- 将组件事件连接到 Java 服务器端代码中。
- 用 POJOs(普通旧 Java 对象)绑定 UI 组件。
- 提供了一组有用的内置验证器和转换器,并提供了一种创建自定义验证器和转换器的机制,以满足特定的需求。
- 处理异常。
- 处理应用页面之间的导航。
- 创建页面模板和应用模板。
- 以反映应用需求的方式定义页面流。
- 处理用户界面的本地化和国际化。
- 创建用来自后端和 API 的数据自动更新的“实时”页面。
- 通过扩展框架类并提供自定义组件视图、事件和状态的实现来创建自定义组件。
JSF 是一个大框架,允许开发者在不同的层次上使用它。根据应用的复杂程度和规模,可以将每个级别划分为不同的角色,由一个或多个开发人员来执行。图 1-1 显示了根据 JSF 规范的不同 JSF 角色。JSF 角色包括以下内容:
- 页面作者。
- 组件编写器。
- 应用开发人员。
- 工具提供商。
- JSF 实施者。
图 1-1 。JSF 角色
如图 1-1 所示,页面作者负责创建页面的用户界面。页面作者应该了解标记、样式和脚本语言,如 HTML5、CSS3 和 JavaScript。页面作者还应该了解 JavaServer Pages (JSP)等呈现技术。
与应用开发人员不同,页面作者专注于为应用开发令人愉快且有效的用户体验。JSF 的组件架构抽象出了大量的复杂性,使得页面作者即使不熟悉 Java 和 C#等编程语言也能高效工作。
组件编写者负责开发页面作者可以使用的可重用 JSF 组件。可重用的 JSF 组件可以收集到“组件库”中可以把它想象成一个组件面板,很容易发现、定制和集成到您的应用中。组件库是大型开发团队生产力倍增的一个重要因素。两个流行且全面的 JSF 组件库是 PrimeFaces 和 RichFaces。
可重复使用的 JSF 组件应支持以下功能:
- 编码:将内部组件的属性和特性转换成使用该组件的页面中合适的标记(比如 HTML)。
- 解码:将带有相关头和参数的传入请求转换成组件的相关属性和特性。
- 除此之外,组件应该支持请求时事件、验证、转换和状态。转换是将传入的请求转换为适合组件的形式的过程。有状态性意味着组件必须为新请求保留其原始状态。这可以通过跨不同请求保存和恢复组件状态来完成。
应用开发人员负责开发 JSF 应用的服务器端功能。应用开发人员专注于开发 Java、EJB 或任何其他能够在 JVM (Java 虚拟机)上运行的语言。除此之外,应用开发人员可以定义 JSF 应用的持久性存储机制(包括数据和内容模型),并从 JSF 页面公开要使用的数据和业务逻辑对象。
工具提供商负责开发工具,帮助 JSF 开发人员构建 JSF 应用。这些工具包括 IDE(集成开发环境)插件和扩展以及页面生成器。JSF 实现者负责为所有前面的角色提供符合标准的运行时或 JSF 规范的实现。Oracle Mojarra(javaserverfaces.java.net/
)和 Apache MyFaces(myfaces.apache.org/
)是 JSF 规范的可用实现的例子。
JSF 进化:1.0–2.2
JSF 1.0 于 2004 年 3 月发布;它代表了 web 层实现方式的重大发展。但是伴随着这些优势而来的还有一些限制,为了让 JSF 在社区中得到广泛的接受,这些限制需要被规避。这些限制中的一些与部件性能有关,另一些与开放式缺陷有关。
专家组努力制定规范,并于 2004 年 5 月发布了 1.1 版,该版本消除了 JSF 1.0 的一些最大的性能问题,并修复了许多缺陷,使 JSF 框架可用于下一代 web 应用。有了 JSF 1.1,专家组已经实现了他们在 Java 规范请求(JSR) 127 中设定的大部分早期目标。这些目标与开发工具支持的标准 GUI 组件框架的创建有关,开发工具允许 JSF 开发人员通过扩展基础框架组件、定义用于输入验证和转换的 API 以及指定 GUI 本地化和国际化的模型来创建自定义组件。
注在
jcp.org/en/jsr/detail?id=127
可以读到 JSR 127。
2006 年 5 月,JSF 1.2 和 Java 企业版 5.0 一起发布。JSF 1.2 对 JSF 1.1 进行了重大改进,解决了用户群体的一些现实问题。JSF 1.2 有许多特性,其中一些是
- JSF 和 JSP 之间的统一表达式语言。
- 解决 JSF 与 JSP 和 JSTL 的集成问题。
- 允许单个组件覆盖转换和验证消息。
- 增强客户端状态保存的安全性。
注 JSR 252 是 JSF 1.2 规格;你可以在 http://jcp.org/en/jsr/detail?id=252 的看到。
2009 年 7 月,JSF 2.0 与 Java 企业版 6.0 一同发布。JSF 2.0 引入了大量的特性和增强功能。一些特性和增强功能包括
- 复合部件。
- 完全模板支持。
- 完整的 Ajax 支持。
- 增强 JSF 导航。
- 支持视图参数。
- 在应用中支持更多的作用域。
- 提供异常处理机制。
- 通过 JSR 303 集成改进验证。
- 标准化资源加载机制。
- 通过支持大多数配置的注释来最小化 XML 的使用。
2010 年 7 月,JSF 2.1 主要是 JSF 2.0 的维护版本。它包括错误修复和增强。其中一些是
- 允许 JSP 文档语法(。jspx 文件)被视为 Facelets 文件。
- 可插拔的 Facelet 缓存机制。
注 JSR 314 规范在其最终版本中集合了 JSF 2.0,在其维护版本中集合了 JSF 2.1;你可以在 http://jcp.org/en/jsr/detail?id=314 阅读说明书。
截至本章撰写时,JSF 2.2 规范和实现仍在进行中,尚未发布。JSF 2.2 预计将与 Java 企业版 7.0 一起发布。JSF 2.2 的主要特点是
- 通过引入 FacesFlow 来标准化流 API。
- 多模板。
- 添加新的 JSF 元素和属性,这些元素和属性是特定于 HTML5 的。
注 JSR 344 是 JSF 2.2 规格;你可以在 http://jcp.org/en/jsr/detail?id=344 的看到它的初稿评论。
在本书的所有章节中,我们将使用实际的、易于理解的例子来更详细地讨论这些特性。
JSF 建筑
JSF 架构基于 MVC(模型视图控制器)2 模式。与 MVC1 模式不同,MVC2 模式将视图从控制器和模型中分离出来。图 1-2 显示了 JSF 的 MVC2 架构。
图 1-2 。JSF MVC2 架构
在 JSF,MVC2 模式实现如下:
- 控制器,由 JSF Faces Servlet 表示。Faces Servlet 负责处理请求分发和页面导航。Faces Servlet 通过调用负责处理 JSF 请求处理生命周期的 JSF 生命周期对象来编排 JSF 生命周期。
- 模型,由 JSF 管理的 beans 和后端代码表示。JSF 托管 bean 只是一个符合 JavaBeans 命名约定的 POJO,可以从 JSF 应用(页面和其他托管 bean)访问。JSF 管理的 bean 必须有一个控制其生命周期的范围;它可以在请求、视图、流、会话、应用或无范围内。每个 JSF 托管 bean 都应该在 faces-config . XML(JSF 配置文件)中注册或使用注释注册(托管 bean 将在第二章中详细介绍)。
- 视图,这是 JSF 的渲染技术。呈现技术定义了页面布局和内容。从 2.0 版本开始,JSF 的默认呈现技术是 Facelets XHTML(但是,您仍然可以选择使用 JSP 作为 JSF 呈现技术,尽管不建议这样做)。
您可能想知道 JSF 运行时如何简化 JSF 开发人员的应用开发,以及控制器如何协调 JSF 的模型和视图。这些问题将在“JSF 生命周期”部分得到解答。
开发您的第一个 JSF 应用
现在,是时候暂时停止这个理论,开始用 JSF 框架工作了。让我们看看如何开发和运行您的第一个 JSF 2.1 应用。
所需软件
在详细介绍您的第一个 JSF 2.1 应用示例之前,我想提一下,本书的所有示例都基于 Apache Maven 3 软件,版本 3.0.4,用于执行编译,并将编译后的源代码组装到可部署的 Java EE WAR 文件中。Maven 3 可以从maven.apache.org/download.html
下载。
Apache Maven 是一个强大的构建管理工具。每个 Maven 项目都有一个名为(pom.xml)的“项目对象模型”文件。(pom.xml)文件包括项目依赖项,用于将项目编译和构建到目标工件中。为了构建一个项目,Maven 从(pom.xml)文件中获取依赖项,然后如果在本地磁盘上没有找到这些依赖项,就将它们下载到本地磁盘上;之后,Maven 执行编译,并将编译后的源代码组装成目标工件。本书中所有示例的目标工件是 Java EE web 应用 WAR 文件。Maven 的强大特性之一是其应用的严格结构,如图图 1-3 所示。
图 1-3 。Maven 项目结构
如图所示,项目根包含两个主子文件夹(src 和 target)和(pom.xml)文件。src 目录包含应用的源代码,目标目录包含生成的工件。src 目录有许多子目录;这些目录中的每一个都有特定的用途:
- src/main/java: 包含应用的 java 源代码。
- src/main/resources: 包含应用需要的资源,比如资源包。
- src/main/filters: 它包括资源过滤器。
- src/main/config: 包含配置文件。
- src/main/webapp: 包含 JEE web 应用项目的文件。
- src/test: 包括应用的单元测试。
- src/site: 它包括用于生成 Maven 项目网站的文件。
除了 Apache Maven 3 软件,本书的所有示例都使用 Oracle jdk1.6.0_27(可以从www.oracle.com/technetwork/java/javase/downloads/index.html下载),这些示例可以运行在任何支持 JSF 2.1(JSF 2.2 示例使用 JSF 2.2)的运行时环境上。
注意 Oracle GlassFish v3.1(或更高版本)和 Apache Tomcat 7 能够运行 JSF 2.1 应用。我将向您展示如何在两个 Java web 容器上运行基于 JSF 2.1 的第一个应用。
开发第一个应用
第一个应用包含两个页面。在第一页中,您可以输入您的姓名和密码,如图 1-4 所示。
图 1-4 。登录页面
点击“登录”按钮后,将被重定向到欢迎页面,如图图 1-5 所示。
图 1-5 。欢迎页面
第一个应用具有以下 Maven 结构:
- first application/src/main/WEB app/we b-INF/faces-config . XML
- first application/src/main/WEB app/we b-INF/WEB . XML
- first application/src/main/WEB app/we b-INF/templates/simple . XHTML
- first application/src/main/web app/CSS/simple . CSS
- first application/src/main/web app/index . XHTML
- first application/src/main/web app/welcome . XHTML
- first application/src/main/Java/com/jsfprotml 5/first application/model/user . Java
- first application/src/main/resources/com/jsfprohtml 5/first application/messages . properties
图 1-6 显示了第一个应用的完整布局。
图 1-6 。第一个应用 Maven 结构
配置文件
第一个应用有两个配置文件,分别是 web.xml 和 Faces-config . XML。web . XML 文件是标准的 web 模块部署描述符,其中定义了 Faces Servlet。Faces Servlet 的主要目的是拦截对 JSF 页面的请求,以便在访问所请求的 JSF 页面之前准备好 JSF 上下文。清单 1-1 显示了第一个应用的 web.xml 文件。
清单 1-1。 第一个应用的 web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app FontName">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-app_3_0.xsd"
version="3.0">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
在清单 1-1 中,您需要知道两件主要的事情:第一件是 Faces Servlet 的定义及其使用< url-pattern >元素到(/faces/*) URL 的映射。第二个是 javax.faces.PROJECT_STAGE 上下文参数,它被设置为 Development(其他可能的值是 Production、SystemTest 和 UnitTest)。在开发模式中设置项目阶段使得 JSF 框架在发现常见的开发错误时在页面中生成额外的消息。这个特性可以帮助 JSF 开发人员在开发过程中提高工作效率。
最后,localhost:8080/first application/
的任何请求重定向到localhost:8080/first application/Faces/index.xhtml
,这将触发 Faces Servlet 在转到 index . XHTML 页面之前准备 JSF 上下文。
注意在任何 Servlet 3.0 容器比如 GlassFish v3 中,web.xml 文件都是可选的。如果省略 web.xml,Faces Servlet 将自动映射到。jsf,。faces 和/faces/* URL 模式。
现在,让我们转到 faces-config 文件,它包括相关的 JSF 配置。实际上,从 JSF 2.0 开始,faces-config 文件变成了可选的,因为大多数 JSF 配置都可以使用 Java 注释来定义。清单 1-2 显示了第一个 Application Faces 配置文件。
清单 1-2。 第一个应用面孔配置文件
<faces-config version="2.1"
FontName">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_2_1.xsd">
<application>
<resource-bundle>
<base-name>com.jsfprohtml5.firstapplication.messages</base-name>
<var>bundle</var>
</resource-bundle>
</application>
</faces-config>
清单 1-2 全局定义了应用资源包,以便从 JSF 表达式语言(#{...})使用捆绑变量(JSF 表达式语言将在第三章中详细说明)。
Facelets Pages
firstApplication 包含两个主页面:第一个页面(index.xhtml)表示主页,另一个页面是(welcome.xhtml)页面,表示欢迎页面。两个页面都引用了(first application/src/main/WEB app/we b-INF/templates/)下定义的模板(simple.xhtml)页面,如清单 1-3 所示。
清单 1-3。 简单. xhtml 模板文件
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title><ui:insert name="title">#{bundle['application.defaultpage.title']}</ui:insert></title>
<link href="#{request.contextPath}/css/simple.css" rel="stylesheet" type="text/css"/>
</h:head>
<h:body>
<div id="container">
<div id="header">
<ui:insert name="header">
<h1>#{bundle['application.defaultpage.header.content']}</h1>
</ui:insert>
</div>
<div id="content">
<ui:insert name="content">
#{bundle['application.defaultpage.body.content']}
</ui:insert>
</div>
<div id="footer">
<ui:insert name="footer">
#{bundle['application.defaultpage.footer.content']}
</ui:insert>
</div>
</div>
</h:body>
</html>
声明了模板 doctype:在 firstApplication 页面中,所有页面都使用这个 doctype,它代表 HTML5 doctype。为了包含 JSF HTML、core 和 Facelets UI 标签,使用了下面的声明 :
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html">
第一行是标准的 XHTML 实践,第二行声明 Facelets UI 标记,第三行声明 JSF 核心标记,最后一行声明模板页面中使用的 JSF HTML 标记。
和元素是 HTML 和元素的替代品。该模板包含一个容器元素,其 ID 为“container”。“容器”元素包含三个子元素:
- header 元素:页面的页眉定义在“header”div 元素里面。
- 内容元素:页面的内容在“content”div 元素中定义。
- 页脚元素:页面的页脚是在“footer”div 元素中定义的。
在 header、content 和 footer 元素中,有一个 Facelets
清单 1-4。message . properties 文件
user.name = Your name
user.password = password
user.name.validation = You need to enter a username
user.password.validation = You need to enter a password
application.login = Login
application.loginpage.title = Login page
application.welcome = Welcome
application.welcomepage.title = Welcome page
application.welcomepage.return = Back to home
application.defaultpage.title = Default Title
application.defaultpage.header.content = Welcome to the first application
application.defaultpage.body.content = Your content here ...
application.defaultpage.footer.content = Thanks for using the application
资源包是一组键和值对。使用 JSF 表达式语言,在运行时解析包键的值。例如,在运行时,将# { bundle[' application . default page . header . content ']}表达式计算为“欢迎使用第一个应用”。
模板文件还包括一个 CSS(层叠样式表)文件,就是 simple.css 文件。simple.css 负责模板页面布局。清单 1-5 显示了 simple.css 文件。
清单 1-5。simple . CSS 文件
h1, p, body, html {
margin:0;
padding:0;
}
body {
background-color:#EEEEEE;
}
#container {
width:100%;
}
#header {
background-color:#FFA500;
}
#header h1 {
margin-bottom: 0px;
}
#content {
float:left;
width:100%;
}
#footer {
clear:both; /*No floating elements are allowed on left or right*/
background-color:#FFA500;
text-align:center;
font-weight: bold;
}
.errorMessage {
color: red;
}
为了在不改变 web 应用的 HTML 代码的情况下随时改变页面布局,建议使用 CSS。
注意从 JSF 2.0 开始,支持页面模板。在 JSF 2.0 之前,JSF 的开发者必须在 JSF 应用中下载并配置一个模板库(比如 Facelets 库)来定义页面的布局。
清单 1-6 显示了 index.xhtml 页面代码,它代表了应用的介绍性页面。
清单 1-6。index . XHTML 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<ui:composition template="/WEB-INF/templates/simple.xhtml">
<ui:define name="title">
#{bundle['application.loginpage.title']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.name']}"></h:outputText>
<h:inputText id="userName"
value="#{user.name}"
required="true"
requiredMessage="#{bundle['user.name.validation']}">
</h:inputText>
<h:message for="userName" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.password']}"></h:outputText>
<h:inputSecret id="password"
value="#{user.password}"
required="true"
requiredMessage="#{bundle['user.password.validation']}">
</h:inputSecret>
<h:message for="password" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.login']}" action="welcome">
</h:commandButton> <br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
为了在 index.xhtml 页面(或任何其他 xhtml 页面)中包含 simple.xhtml 模板页面,使用了
在 index.xhtml 页面中,模板的“标题”和“内容”被页面标题和页面内容覆盖。页面内容包括一个
为了制作表单的内部布局,使用了< h:panelGrid >标签。标签是一个布局容器,它在由行和列组成的网格中呈现 JSF 组件。< h:panelGrid >标签有一个 columns 属性,指定网格的列数(在本例中是“3”)。在 index.xhtml 页面中,< h:panelGrid >中的每一行都代表一个带有标签和消息的输入字段。第一行如下:
<h:outputText value="#{bundle['user.name']}"></h:outputText>
<h:inputText id="userName"
value="#{user.name}"
required="true"
requiredMessage="#{bundle['user.name.validation']}">
</h:inputText>
<h:message for="userName" styleClass="errorMessage"/>
将
<h:commandButton value="#{bundle['application.login']}" action="welcome"></h:commandButton>
注JSF 导航是一个有很多细节的话题,在本书的下一章会有更详细的说明。JSF 验证和转换将在第三章的中详细说明。
清单 1-7 显示了 welcome.xhtml 页面代码,它表示应用的欢迎页面。
清单 1-7。welcome . XHTML 页面代码
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">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">
<ui:composition template="/WEB-INF/templates/simple.xhtml">
<ui:define name="title">
#{bundle['application.welcomepage.title']}
</ui:define>
<ui:define name="content">
#{bundle['application.welcome']}, #{user.name}! <br/>
<h:link value="#{bundle['application.welcomepage.return']}"
outcome="index"></h:link> <br/><br/>
</ui:define>
</ui:composition>
</html>
welcome.xhtml 页面包含使用
被管理的 bean
如 Facelets 页面部分所示,有一个用户管理的 bean,它与索引和欢迎页面的输入和输出组件绑定在一起。清单 1-8 显示了用户管理的 bean。
清单 1-8。 用户托管 Bean
package com.jsfprohtml5.firstapplication.model;
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
@ManagedBean
@SessionScoped
public class User implements Serializable {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
用户管理的 bean 是一个简单的 Java bean,有两个用于 name 和 password 属性的 setters 和 getters。@ManagedBean 注释用于将用户类注册为 JSF 管理的 Bean。
注意@ managed Bean 注释有一个可选的 name 属性,它描述了要在 JSF 表达式中使用的受管 bean 的名称。在用户管理的 bean 中,name 属性被省略;这意味着受管 bean 名称将与第一个字符为小写的类名相同,也就是说,它将在像#{user}这样的 JSF 表达式中使用。
@SessionScoped 注释用于在会话范围内设置受管 bean。其他可能的值可以是(@RequestScoped、@ViewScoped、@ApplicationScoped、@ none scoped[或@FlowScoped,这在 JSF 2.2 中受支持])。
依赖性
现在,让我们转到第一个应用的(pom.xml)依赖项。清单 1-9 显示了 GlassFish 3.1.2 上第一个应用所需的依赖关系。
清单 1-9。 在 pom.xml 文件中配置 GlassFish 3.1.2
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.faces</groupId>
<artifactId>javax.faces-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
如清单所示,以下依赖项仅用于编译,不会包含在 web 应用的 lib 文件夹中,因为 GlassFish 3.1.2 应用服务器已经附带了这些依赖项:
- Servlet API 版本 2.5。
- JSP API 版本 2.0。
- JavaEE Web API 版本 6。
- JSF API 版本 2.1。
清单 1-10 显示了 Tomcat 7 上第一个应用所需的依赖关系。
清单 1-10。 雄猫 7 配置
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.faces</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
如清单所示,这是几乎相同的依赖项集,只有一个区别,即替换了 JSF API v2.1 依赖项,该依赖项在提供的范围内:
<dependency>
<groupId>javax.faces</groupId>
<artifactId>javax.faces-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
在编译范围内具有以下依赖关系:
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.faces</artifactId>
<version>2.1.6</version>
</dependency>
这个替换告诉 Maven 使用 JSF 2.1.6 jar 来编译第一个应用,并将这个 jar 包含在 web 应用 lib 文件夹中。我特别选择 2.1.6 版本,因为它在 Tomcat 7 上运行良好。
构建和部署第一个应用
第一个 Application Maven 项目可以从本书的网站下载:www.apress.com/9781430250104。为了构建和部署第一个应用,您需要在您的系统中安装 Maven 3。
注配置 Maven 3 的详细步骤在 Maven 网站有描述:
maven.apache.org/download.html#Installation
。这些说明向您展示了如何在您的系统中安装 Maven,无论它是 Windows、Linux、Solaris 还是 Mac OS X。
在您的系统中安装了 Maven 3 之后,您可以通过从命令行执行以下 Maven 命令来构建第一个应用。这个命令应该从包含 pom.xml 文件的应用目录中执行:
mvn clean install
执行这个命令后,可以在目标文件夹中找到生成的 firstApplication-1.0.war 文件。让我们看看如何在 Apache Tomcat 7 和 Oracle GlassFish 3.1.2 上部署生成的 war 文件。
注意不要忘记使用“依赖项”一节中提到的适当的 pom.xml 依赖项来正确生成两个 war 文件(一个用于 Apache Tomcat 7,另一个用于 Oracle GlassFish 3.1.2)。
在 Tomcat 7 上部署应用
为了在 Apache Tomcat 7 上部署 firstApplication-1.0.war 文件,您需要执行以下操作:
- 将 firstApplication-1.0.war 文件复制到$ { Tomcat _ Installation _ Directory } \ web apps 目录。
- 从$ { Tomcat _ Installation _ Directory } \ bin 目录中执行以下命令,启动 Tomcat 7 服务器:
- startup.bat(适用于 Windows)
- startup.sh(适用于 Linux)
- 从以下 URL 访问第一个应用:
localhost:8080/first application-1.0/
在 GlassFish 3.1.2 上部署应用
为了在 Oracle GlassFish 3.1.2 上部署 firstApplication-1.0.war 文件,您需要执行以下操作:
- 将 firstApplication-1.0.war 文件复制到$ { GlassFish _ Installation _ Directory } \ domains **domain 1**\ auto deploy 目录中( domain1 可以改为任意域名)。
- 通过从${ GlassFish _ Installation _ Directory } \ bin 目录执行以下命令来启动 GlassFish 3.1.2 服务器:asadmin start-domain 域 1
- 从以下 URL 访问第一个应用:
localhost:8080/first application/
尽管第一个应用是一个简单的 JSF 应用,但它涵盖了 JSF 的许多基础知识。您现在知道了以下内容:
- 如何从头开始创建一个 JSF 应用?
- JSF 表达式的基础。
- JSF 管理的是如何创建和使用 JSF 应用。
- 如何创建一个 JSF 页面模板,并在应用页面中使用该模板。
- 如何从 JSF 应用中配置和使用资源包。
- 如何使用基本的 JSF HTML 组件标签?
- 如何使用 JSF 必填字段验证器来验证输入字段。
- 如何使用 Maven 来管理在不同的 JSF 2.1 web 容器上轻松部署 JSF 应用。
JSF 生命周期
现在,是时候了解 JSF 是如何在幕后工作的了。虽然开发 JSF 应用并不需要了解 JSF 生命周期的细节,但建议阅读本节,以便了解您开发的代码如何在 JSF 运行时容器中执行,并为高级 JSF 开发做好准备。JSF 请求处理生命周期有六个阶段,如图 1-7 所示。
图 1-7 。JSF 请求处理阶段
六个阶段如下:
- 恢复视图。
- 应用请求值。
- 流程验证。
- 更新模型值。
- 调用应用。
- 渲染响应。
恢复视图
每个 JSF 页面在服务器上都表示为一个 UI 组件树,它与客户机(浏览器)中的用户界面有一对一的映射。为了正确理解这一点,我们来看一个例子。清单 1-11 显示了一个 JSF XHTML 页面,允许用户输入他/她喜欢的食物、饮料和运动。
清单 1-11。 收藏夹. xhtml 页面
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Your Favorites</title>
</h:head>
<h:body>
<h:form id="favForm">
<h:panelGrid columns="3">
<h:outputText value="Favorite Food"></h:outputText>
<h:inputText id="favoriteFood" value="#{favorite.food}" required="true">
</h:inputText>
<h:message for="favoriteFood"/>
<h:outputText value="Favorite Beverage"></h:outputText>
<h:inputText id="favoriteBeverge" value="#{favorite.beverage}" required="true">
</h:inputText>
<h:message for="favoriteBeverge"/>
<h:outputText value="Favorite Sport"></h:outputText>
<h:inputText id="favoriteSport" value="#{favorite.sport}" required="true">
</h:inputText>
<h:message for="favoriteSport"/>
</h:panelGrid>
<h:commandButton value="Save my favorites" action="#{favorite.save}"/><br/><br/>
</h:form>
</h:body>
</html>
favorites.xhtml 页面中的代码表现为 UI 组件的树,如图图 1-8 所示。
图 1-8 。收藏夹页面的 UI 组件树
在“恢复视图”阶段,有两种情况:
- “非回发”请求:“非回发”请求是指对页面的新请求。如果是这种情况,还原视图阶段会创建一个空的 UI 组件树,并将其存储在当前的 FacesContext 实例中。对于“非回发”请求,JSF 生命周期直接进入最后一个阶段,即“呈现响应”阶段。在“呈现响应”阶段,空的 UI 组件树由页面中的 JSF 组件填充。此外,UI 组件树状态保存在 JSF 视图状态中,以供下一个请求使用。
- “回发”请求:当使用 HTTP POST 方法将表单内容提交到同一页面时,会发生“回发”请求。在这种情况下,还原视图阶段从 JSF 视图状态还原 UI 组件树,该视图状态是由前一个页面请求生成的。
应用请求值
在 UI 组件树恢复后,调用“应用请求值”阶段。在这个阶段,UI 组件树中的每个节点都被分配了从表单提交的值。图 1-9 显示了如果表单提交了值,UI 组件树是如何填充请求值的,例如,“favoriteFood”的“鱼”,“favoriteBeverge”的“橙汁”,“favoriteSport”的“足球”。
图 1-9 。用请求值填充 UI 组件树
注意“应用请求值”应用于所有具有值属性的组件。在 JSF 中,具有 value 属性的组件必须实现 ValueHolder 接口。为了将请求值应用到 UI 组件树中的所有值容器节点,JSF 运行时调用 UIViewRoot 的 processDecodes()方法,这导致子组件的 processDecodes()方法也被调用,以便为所有组件应用请求值。
流程验证
“流程验证”阶段在“应用请求值”阶段之后调用。在此阶段,转换和验证按顺序执行。在 favorites.xhtml 中,通过将“required”属性设置为 true,对所有输入字段执行验证,以保证它们总是具有非空值。
转换是将 HTTP 请求参数转换成相应的 Java 类型,以消除开发人员为每个 web 应用实现这一功能所需的开销。JSF 有许多内置的转换器,它提供了一个接口,以便开发定制的转换器。清单 1-12 显示了一个 JSF 内置日期转换器的例子。
清单 1-12。 内置日期转换器的例子
<h:inputText id="birthDate" value="#{user.birthDate}">
<f:convertDateTime pattern="dd/MM/yyyy"/>
</h:inputText>
在本例中,“birthDate”输入字段确保具有以下格式的日期“dd/MM/yyyy ”,并被转换为用户管理的 bean 中的 birthDate(日期对象)属性(验证和转换的更多细节将在第三章的中说明)。
当组件验证(或转换)失败时,组件错误消息(FacesMessage)将在 FacesContext 实例中排队。如果出现验证(或转换)错误,JSF 生命周期将直接进入“呈现响应”阶段,排队的 Faces 消息将显示在
注意为了在 UI 组件树中应用过程验证,JSF 运行时调用 UIViewRoot 的 processValidators()方法,这导致子组件的 processValidators()方法也被调用,以便将转换和验证应用于所有子组件。
更新模型值
在完成转换和验证 UI 组件树中的值之后,调用“更新模型值”阶段。在这个阶段,在 UI 组件树中的值和 JSF 模型(受管 beans)之间进行绑定。
图 1-10 显示了 JSF 管理的 bean 属性是如何用 UI 组件树的值更新的。
图 1-10 。JSF 管理的 bean 属性随 UI 组件树的值更新
注意为了执行模型值更新,JSF 运行时调用 UIViewRoot 的 processUpdates()方法,这导致子组件的 processUpdates()方法也被调用,以便将模型更新应用于所有子组件。但是,UIInput 组件覆盖 processUpdates()方法,以便调用 updateModel()将用户输入与受管 bean 属性绑定起来。
调用应用
在完成模型值更新之后,调用“调用应用”阶段。在这个阶段,执行动作代码。JSF 中的动作代码可以在动作方法和动作监听器中。
注意在 favorites.xhtml 中,动作代码在#{favorite.save}动作方法中表示。
在此阶段,导航是在执行自定义操作代码后由 JSF NavigationHandler 执行的。action 属性可以设置为文字值。在第一个应用中,您已经看到了将 action 属性设置为文字值的情况:
<h:commandButton value="#{bundle['application.login']}" action="welcome">
</h:commandButton>
在文本值的情况下,JSF 运行时直接将“welcome”文本值传递给 JSF NavigationHandler,以便导航到新页面。导航处理结果显示在“渲染响应”阶段。
如果操作结果没有隐式(结果匹配现有页面名称)或显式(通过匹配 faces-config 中定义的导航规则)匹配导航案例,NavigationHandler 将停留在同一页面上。
注意为了执行“调用应用”阶段,JSF 运行时调用 UIViewRoot 的 processApplication()方法,该方法通过调用 UIComponent 的 broadcast(FacesEvent event)方法,将排队的事件广播给 UICommand 组件(或任何其他实现 ActionSource 接口或 ActionSource2 接口[在 JSF 1.2 中引入]的 UIComponent)。broadcast 方法将操作事件广播给注册到该事件类型的所有操作侦听器进行处理。
渲染响应
最后,JSF 运行时调用“呈现响应”阶段,以便向用户呈现最终结果。通过在每个组件上调用 encodeXXX()方法,UI 组件树被呈现给客户端(encode 方法负责为每个组件生成合适的标记)。
除了呈现之外,“呈现响应”阶段还将 UI 组件树的状态存储在视图状态中,以便在下一个请求中恢复。
立即属性
有时,在您的 JSF 应用中,您可能希望跳过转换和验证,以便导航到另一个页面。例如,假设在 favorites.xhtml 页面中,您希望有一个导航到索引页面“index.xhtml”的“主页”按钮,如下所示:
<h:form id="favForm">
...
<h:inputText id="favoriteFood"
value="#{favorite.food}"
required="true">
</h:inputText>
... <!-- other required fields -->
<h:commandButton value="Save my favorites" action="#{favorite.save}"/>
<h:commandButton value="Go home" action="index"/>
</h:form>
如果您点击“回家”按钮,并将必填的输入字段留空,您将会看到如图图 1-11 所示的验证错误信息:
图 1-11 。由于没有“立即”为真而导致的验证错误
这是因为“回家”命令按钮会提交触发 JSF 生命周期的帖子,并且由于输入字段上的验证,“回家”操作无法完成。
JSF 框架提供了“立即”属性,允许跳过 JSF 生命周期的转换和验证。“immediate”属性实际上允许在“应用请求值”阶段执行动作事件。将 immediate 属性设置为 true 可以解决这个问题,如下所示:
<h:form id="favForm">
...
<h:inputText id="favoriteFood"
value="#{favorite.food}"
required="true">
</h:inputText>
... <!-- other required fields -->
<h:commandButton value="Save my favorites" action="#{favorite.save}"/>
<h:commandButton value="Go home" action="index" immediate="true"/>
</h:form>
注<h:链接>和< h:按钮>是从 JSF 2.0 开始引入的新部件;它们可以用于使用 outcome 属性实现到目标页面的 GET 导航(您已经在第一个应用中看到了一个< h:link >的例子)。因此,可以直接使用这些新组件来代替命令按钮和命令链接,并将 immediate 属性设置为 true 来进行导航,而无需执行转换和验证。
添加到 UICommand 组件中,immediate 属性可以应用于 EditableValueHolder 组件(如输入文本)。如果 EditableValueHolder 组件的 immediate 属性设置为 true,则 EditableValueHolder 组件的转换和验证将在“应用请求值”阶段(在“流程验证”阶段之前)执行。
注意 ValueHolder 组件是那些具有 value 属性如(标签和输出文本)的组件,它们实现 ValueHolder 接口。EditableValueHolder 组件是 ValueHolder 组件的一个子类型,可以由用户编辑其值,如(输入文本)。ActionSource 组件是那些可以进行诸如(命令按钮和命令链接)之类的操作的组件,它们实现 ActionSource 接口(或 JSF 1.2 以后的 ActionSource2)。
摘要
读完这一章,你就知道什么是 JSF,你也看到了 JSF 框架是如何随着时间的推移而演变的。您了解 JSF 架构,并学习了如何开发 JSF 应用,该应用涵盖了 JSF 世界中许多有趣的主题(基本 UI 组件、托管 beans、表达式语言、模板、资源包和验证)。最后,您知道了 JSF 请求处理生命周期在幕后是如何工作的。在接下来的章节中,本章中提到的所有 JSF 主题和其他高级主题都将有更详细的说明。
二、引擎盖下的 JSF——第一部分
本章阐述了 JSF 框架中的重要主题。在本章中,您将详细了解 JSF 管理的 beans 和表达式语言(EL)。您还将了解一些 JSF 导航。最后,您将了解如何在您的 JSF web 应用中利用 JSF 异常处理机制来增强应用的错误处理能力。
被管理的 bean
JSF 托管 bean 只是一个 POJO(普通旧 Java 对象),它符合 JavaBeans 命名约定,可以从 JSF 应用(页面和其他托管 bean)中访问。它被称为 managed ,因为它是由 JSF 框架管理的,当 JSF 应用需要使用它时,它会以一种懒惰的方式为您实例化 bean 类。下一节将详细介绍如何声明受管 bean,如何初始化受管 bean,如何管理不同受管 bean 之间的依赖关系,如何访问受管 bean,以及最后如何利用@Named 和@inject 注释来处理 JSF POJO 模型。
声明受管 Beans
在第一章中,我们在第一个应用中有一个托管 beans 用法的例子。清单 2-1 展示了用户管理的 bean。
清单 2-1。 用户托管 Bean
package com.jsfprohtml5.firstapplication.model;
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
@ManagedBean
@SessionScoped
public class User implements Serializable {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
用户管理的 bean 是一个 Java bean 类,有两个用于 name 和 password 属性的 setters 和 getters。@ManagedBean 注释用于将用户类注册为 JSF 管理的 Bean。@ManagedBean 注释有一个可选的 name 属性,它描述了将在 JSF 表达式中使用的受管 Bean 的名称。在用户管理的 bean 中,name 属性被省略;这意味着受管 bean 名称将与第一个字符为小写的类名相同,也就是说,它将在像#{user}这样的 JSF 表达式中使用。
一个 JSF 管理的 bean 必须有一个与之相关联的作用域,以控制它的生命周期。范围可以是:
- 请求范围(@RequestScoped),这意味着 bean 将被实例化,并且只要 HTTP 请求是活动的,它就将是活动的。
- 视图范围(@ViewScoped),这意味着只要用户停留在同一个视图中,bean 就会被实例化,并且是活动的。
- 会话范围(@SessionScoped),这意味着 bean 将被实例化,并且只要用户的 HTTP 会话是活动的,它就将是活动的。
- 应用范围(@ApplicationScoped),这意味着 bean 将被实例化,并将在应用的整个生命周期中保持活动状态。
- None scope (@NoneScoped),这意味着 bean 不会被实例化,也不会作为独立的实体存储在任何作用域中。无作用域的受管 bean 可以由另一个受管 bean 使用和实例化;在这种情况下,无作用域 bean 将具有实例化它的调用方受管 bean 的作用域(即,只要它们的调用方受管 bean 是活动的,无作用域受管 bean 就将是活动的)。在“管理受管 beans 的依赖性”一节中,您将看到这种情况的一个例子。
注在 JSF 2.2 中,有一个新的作用域叫做“流作用域”(@FlowScoped)。这个流程将在下一章详细说明。
除了注释之外,每个 JSF 管理的 bean 也可以在 faces-config . XML(JSF 配置文件)中注册。清单 2-2 展示了如何在 faces-config.xml 文件中定义用户管理的 bean,而不是使用注释。
清单 2-2。faces-config . XML 文件中的用户托管 Bean 定义
<managed-bean>
<managed-bean-name>user</managed-bean-name>
<managed-bean-class>com.jsfprohtml5.firstapplication.model.User</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
正在初始化受管 Beans
JSF 管理的 beans 可以从 faces 配置文件或使用注释进行初始化。清单 2-3 显示了如何从 faces 配置文件中用值“anonymous”初始化用户管理的 bean 的 name 属性。
清单 2-3。 初始化 faces-config.xml 中的 Name 属性
<managed-bean>
<managed-bean-name>user</managed-bean-name>
<managed-bean-class>com.jsfprohtml5.firstapplication.model.User</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>name</property-name>
<value>anonymous</value>
</managed-property>
</managed-bean>
元素可以用来初始化被管理的 bean 属性。它主要有两个子元素:
元素,它包括受管 bean 属性的名称。 元素,它包括受管 bean 属性的初始值。
初始化受管 bean 属性的另一种方法是使用注释。清单 2-4 展示了如何使用@ManagedProperty 注释用值“anonymous”初始化用户管理的 bean 的 name 属性。
清单 2-4。 使用@ManagedProperty 注释初始化 Name 属性
@ManagedBean
@SessionScoped
public class User implements Serializable {
@ManagedProperty(value="anonymous")
private String name;
private String password;
// The setters and the getters ...
}
使用注释,@ManagedProperty 注释用于使用 value 属性初始化用户管理的 bean 的 name 属性,其值为“anonymous”。
值得注意的是,JSF 管理的 beans 可以很好地与不同的 Java EE 注释一起工作。有两个与 JSF 受管 bean 生命周期相关的 Java EE 注释,可用于初始化和反初始化受管 bean:
- @PostConstruct
- @PreDestroy
清单 2-5 显示了用户管理 bean 中@PostConstruct 和@PostDestroy 注释的例子。
清单 2-5。@ post construct 和@ PostDestroy 注释
import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.SessionScoped;
@ManagedBean
@SessionScoped
public class User implements Serializable {
@ManagedProperty(value="anonymous")
private String name;
private String password;
// The setters and the getters ...
@PostConstruct
private void initialize() {
System.out.println("Bean is initialized with the following user name: " + name);
}
@PreDestroy
private void cleanUp() {
System.out.println("You can do the cleanup here");
}
}
当用户管理的 bean 被实例化和初始化时,调用@PostConstruct 方法。这意味着当初始化方法被调用时,输出控制台将显示以下消息:
"Bean is initialized with the following user name: anonymous"
注意正如你注意到的,@PostConstruct 和@PreDestroy 方法的返回类型是无效的,并且不带任何参数。这些方法也可以是私有的、公共的、受保护的或打包的。
在销毁用户管理的 bean 之前,调用@PreDestroy 方法。在这个方法中,您可以将受管 bean 所需的清理和反初始化代码。
还有许多其他与数据访问相关的 Java EE 注释,可以很好地与 JSF 管理的 beans 一起使用,例如@Resource、@PersistenceUnit、@PersistenceContext、@EJB 和@WebServiceRef。我们将在接下来的章节中讨论大部分注释。
除了初始化简单属性的能力之外,您还可以初始化受管 bean 的复杂类型,比如列表和映射。清单 2-6 为用户管理的 bean 引入了两个新属性:收藏夹体育列表和发言人语言映射。
清单 2-6。 用户用两个新属性管理 Bean
import java.util.List;
import java.util.Map;
//...
public class User implements Serializable {
private String name;
private String password;
private List<String> favoriteSports;
private Map<String, String> spokenLanguages;
// setters and getters ...
}
为了用一些初始值初始化 favoriteSports 列表,可以在
清单 2-7。Faces 配置文件中 List 属性的 初始化
<managed-property>
<property-name>favoriteSports</property-name>
<list-entries>
<value>Handball</value>
<value>Football</value>
<value>Basketball</value>
</list-entries>
</managed-property>
在 XHTML 页面中,可以使用< ui:repeat >迭代 favoriteSportlist,如清单 2-8 所示。
清单 2-8。 在 XHTML 页面中显示喜爱的运动项目
<b>You have the following initial list of favorite sports:</b>
<ul>
<ui:repeat value="#{user.favoriteSport}" var="sport">
<li>#{sport}</li>
</ui:repeat>
</ul>
如果要在收藏夹体育列表中显示特定项目,可以使用[]运算符。例如,下面的#{user.favoriteSport[0]}将显示 favoriteSport 数组中的第一项,即“手球”。为了用一些初始值初始化 spokenLanguages 映射,可以在
清单 2-9。 初始化 Faces 配置文件中的地图属性
<managed-property>
<property-name>spokenLanguages</property-name>
<map-entries>
<map-entry>
<key>EN</key>
<value>English</value>
</map-entry>
<map-entry>
<key>FR</key>
<value>French</value>
</map-entry>
</map-entries>
</managed-property>
在 XHTML 页面中,您可以使用#{user.spokenLanguages}来显示 spokenLanguages。如果想要显示特定的映射条目值,也可以使用[]操作符从它的键中获得它。例如,使用#{user.spokenLanguages['EN']}将显示“英语”。
注意对于<列表条目>和<地图条目>元素没有等价的 JSF 注释,所以初始化地图和列表的唯一方法是使用 faces 配置文件。
管理受管 Beans 依赖性
JSF 支持受管 bean 的 IoC(控制反转),这意味着受管 bean 可以在运行时耦合,而不需要从应用代码处理这种耦合。让我们看看如何在我们的 JSF 应用中利用 IoC。清单 2-10 为用户管理的 bean 引入了一个新的属性——职业属性。
清单 2-10。 用户管理 Bean 中的新职业属性
public class User implements Serializable {
...
private Profession profession;
...
public Profession getProfession() {
return profession;
}
public void setProfession(Profession profession) {
this.profession = profession;
}
}
清单 2-11 显示了职业管理 bean 类的属性。
清单 2-11。 职业管理豆
public class Profession implements Serializable {
private String title;
private String industry;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
}
可以使用 JSF 配置文件或 JSF 注释将专业管理的 bean 实例与用户管理的 bean 实例关联起来。清单 2-12 显示了 JSF 配置文件的一部分,它配置了在实例化时被注入到用户管理 bean 中的专业管理 bean。
清单 2-12。 配置要注入到用户实例中的职业实例
<managed-bean>
<managed-bean-name>user</managed-bean-name>
<managed-bean-class>com.jsfprohtml5.firstapplication.model.User</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
< managed-property>
<property-name>profession</property-name>
<value>#{profession}</value>
</managed-property>
...
</managed-bean>
<managed-bean>
<managed-bean-name>profession</managed-bean-name>
<managed-bean-class>com.jsfprohtml5.firstapplication.model.Profession</managed-bean-class>
<managed-bean-scope>none</managed-bean-scope>
<managed-property>
<property-name>title</property-name>
<value>Software Engineer</value>
</managed-property>
<managed-property>
<property-name>industry</property-name>
<value>IT</value>
</managed-property>
</managed-bean>
Profession 受管 bean 在 none 作用域中声明,以便拥有实例化它的调用者受管 bean 的作用域(在本例中是用户受管 bean)。在用户管理的 bean 中,为 profession 属性定义了属性初始化,并且通过使用
完成此配置后,当用户管理的 bean 被 JSF 框架实例化时,专业管理的 bean 将被实例化并设置在会话范围中。这意味着# { user . professional . title }表达式将返回职业头衔的初始值,即“软件工程师”。
从 Java 代码访问受管 Beans
了解如何从 Java 代码中访问受管 beans 是很重要的。如果您想要从另一个不直接引用 bean1 的受管 bean(例如 bean2)获取特定受管 bean(例如 bean1)的信息,这将非常有用。为了从 Java 代码中访问受管 beans,您需要使用 ValueExpression 类。清单 2-13 展示了如何从 Java 代码中获取用户管理的 bean 的信息。
清单 2-13。 从 Java 代码中获取用户管理的 Bean 信息
FacesContext context = FacesContext.getCurrentInstance();
Application application = context.getApplication();
ELContext elContext = context.getELContext();
ExpressionFactory expressionFactory = application.getExpressionFactory();
ValueExpression valueExpression = expressionFactory.createValueExpression
(elContext, "#{user}", User.class);
User user = (User) valueExpression.getValue(elContext);
为了检索用户管理的 bean 信息,使用 ExpressionFactory 类的 createValueExpression API 创建一个 ValueExpression 对象。createValueExpression API 采用以下参数:
- ELContext,指用于解析表达式的 EL 上下文。
- 要分析的表达式字符串。
- 在表达式求值之后,表达式的结果将被强制转换为的类型。
使用 ValueExpression 对象,可以使用 getValue(ELContext) API 获取表达式的值,还可以使用 ValueExpression 对象在运行时使用 setValue(ELContext,value) API 为表达式设置值。
@Named 和@inject 批注
在未来的 JSF 规范中,@ManagedBean 将被弃用,因此建议使用@Named CDI(上下文和依赖注入)批注,而不是@ManagedBean。您应该知道,@ManagedBean 是由 JSF 框架管理的,而@Named 批注不是由 JSF 框架管理的。@Named 注释由支持 CDI 的 JEE 应用服务器管理。CDI 优于 JSF 依赖注入的一个优点是,为了使用 CDI @inject 注释注入实例,该实例不需要用任何特定的注释进行注释,而在 JSF 中,@ManagedProperty 要求要注入的 bean 用@ManagedBean 注释进行注释。
让我们看看如何为用户管理的 bean 使用@Named 注释,而不是@ManagedBean。清单 2-14 显示了使用@Named 注释的用户管理 bean 的修改版本。
清单 2-14。@命名用户托管 Bean
import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
import javax.inject.Named;
@Named
@SessionScoped
public class User implements Serializable {
private String name;
private String password;
@Inject
private Profession profession;
// ...
public Profession getProfession() {
return profession;
}
public void setProfession(Profession profession) {
this.profession = profession;
}
//...
}
为了将 CDI 用于我们的用户管理 bean,我们需要注意一些应该进行的更改:
- @Named 批注替换了@ManagedBean 批注。
- 我们使用了 javax . enterprise . context . session scope,而不是 javax . faces . bean . session scope(在 CDI 中,不能使用 javax.faces.bean.xxx 包来指定 bean 作用域;而是要用 javax.enterprise.context.xxx 包)。
- 我们使用@inject 注释在用户管理的 bean 实例中注入专业管理的 bean 实例,而不是使用@ManagedProperty 注释。
注意使用 CDI 时,需要在 WEB 应用的 WEB-INF 文件夹下创建一个空的 beans.xml。
表达语言
有两套不同的El:
- JSP EL。
- JSF 埃尔。
这两种语言有许多不同之处。JSP EL 表达式 以美元符号($)开始,然后是左花括号({),然后是 JSP 表达式,最后以右花括号(})结束。当呈现页面时,JSP EL 立即执行(在页面编译时)。另一方面,JSF EL 表达式以散列(#)开始,然后是左花括号({),然后是 JSF 表达式,最后以右花括号(})结束。JSF EL 表达式的执行被延迟,这意味着表达式的计算基于 JSF 生命周期上下文。
更具体地说,JSF 延迟表达式在页面回发和页面初始呈现期间可用,这意味着 JSF 延迟表达式可以在 JSF 框架的请求处理和响应呈现阶段进行计算,而 JSP 即时表达式仅在页面呈现期间(而不是页面回发)可用,因为它们是在第一次呈现页面时计算的。这意味着 JSP EL 总是只读的值表达式,而 JSF EL 可以在读写模式下工作。
统一表达语言
多亏了 JSP 2.1,它是 Java EE 5 的一部分,这个不匹配的问题通过一个统一的 EL 解决了,这个 EL 本质上代表了 JSP 和 JSF EL 的联合。统一 EL 具有以下特征 :
- 表达式的延迟求值。
- 表达式可以在读写模式下工作。
- JSTL 标签可以处理延迟表达式。
清单 2-15 展示了一个例子,展示了< c:forEach > JSTL 标签如何与 JSF 延迟表达式#一起工作..}.
清单 2-15。 带有< c:forEach >标签的 JSF 延迟表达式
<b>You have the following initial list of favorite sports:</b>
<ul>
<c:forEach items="#{user.favoriteSport}" var="sport">
<li>#{sport}</li>
</c:forEach>
</ul>
如清单所示,这段代码可以替代清单 2-16 中的代码,它是第一个应用中 welcome.xhtml 页面的一部分。
清单 2-16。 原 JSF 延期表达式用< ui:重复>标签
<b>You have the following initial list of favorite sports:</b>
<ul>
<ui:repeat value="#{user.favoriteSport}" var="sport">
<li>#{sport}</li>
</ui:repeat>
</ul>
除此之外,您还可以选择将其他 JSTL 标签与 JSF 延迟表达式组合,如清单 2-17 中的所示。
清单 2-17。
<b>You have the following initial list of favorite sports:</b>
<ul>
<c:forEach items="#{user.favoriteSport}" var="sport">
<c:choose>
<c:when test="#{sport == 'Football'}">
<li><b><u>Popular in Africa:</u></b> #{sport}</li>
</c:when>
<c:otherwise>
<li>#{sport}</li>
</c:otherwise>
</c:choose>
</c:forEach>
</ul>
在示例中,以下 JSTL 标记与 JSF 延迟表达式一起使用:
用于执行迭代。 用于定义一组互斥的选择。它类似于 Java switch 语句。 ,类似于 Java 的 case 语句。 ,类似 Java 默认语句。
在这个例子中,
图 2-1 。高亮下划线的列表项
请注意,为了与 JSTL 合作,您需要将 JSTL·URI 包含在声明中,如下面的粗体文本所示:
<html FontName">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:c="[`java.sun.com/jsp/jstl/core`](http://java.sun.com/jsp/jstl/core)">
在《JSF 埃尔》中,有两种表达方式:
- 价值表达。
- 方法表达式。
在接下来的部分中,我们将深入研究值和方法表达式的细节。
值表达式
值表达式可用于设置受管 bean 属性的值,或显示受管 bean 属性或任何动态评估对象的值。到目前为止,我们已经看到了几个值表达式的例子。例如,firstApplication 的 welcome.xhtml 页面中的以下#{user.name}表达式表示显示用户管理的 bean 的名称的值表达式:
Welcome, #{user.name}!
而 firstApplication 的登录页面(index.xhtml)中的同一个#{user.name}表达式表示设置用户管理的 bean 的名称的值表达式:
<h:inputText id="userName" value="#{user.name}" required="true"/>
注从第一章我们知道,EditableValueHolder(比如 inputText 和 selectOneMenu)的值和 JSF EL 表达式(模型)之间的绑定是在 JSF 请求处理生命周期的更新模型值阶段执行的。
值表达式也可以访问隐式对象。表 2-1 显示了根据 JSF 规范可用于 JSF EL 的 JSF 隐式对象。
表 2-1 。JSF EL 可用隐式对象
|
隐含对象
|
表示
|
| --- | --- |
| Application
| ServletContext
如果您正在处理 Servlet 容器,或者PortletContext
如果您正在处理 Portlet 容器。 |
| applicationScope
| 应用范围的映射(应用属性映射)。 |
| Cookie
| HTTP cookie 映射。 |
| facesContext
| 当前FacesContext
实例。 |
| Component
| UIComponent
实例(将在第六章 - 第八章的“创建自定义 JSF 组件”中详细说明)。 |
| Cc
| 父复合组件(将在第六章 - 和第八章中的“创建定制 JSF 组件”中详述)。 |
| Flash
| flash 范围图(将在 flash 范围部分详细说明)。 |
| Header
| 请求 HTTP 头映射。 |
| headerValues
| 请求 HTTP 头映射。然而,map 的每个值都是一个代表头键值的String[]
。 |
| initParam
| 初始化应用的参数映射。 |
| Param
| 请求查询参数映射。 |
| paramValues
| 请求查询参数映射。但是,map 的每个值都是一个代表参数键值的String[]
。 |
| Request
| ServletRequest
如果您正在处理 Servlet 容器,或者PortletRequest
如果您正在处理 Portlet 容器。 |
| requestScope
| 请求范围映射(请求属性映射)。 |
| Resource
| 资源参考。 |
| Session
| HttpSession
如果您正在处理 Servlet 容器,或者PortletSession
如果您正在处理 Portlet 容器。 |
| sessionScope
| 会话范围映射(会话属性映射)。 |
| View
| UIViewRoot
为当前视图。 |
| viewScope
| 视图范围映射(视图属性映射)。 |
清单 2-18 中的示例显示了如何使用#{header}值表达式显示请求头信息。
清单 2-18。 使用表头隐式对象显示请求表头信息
<table border="1">
<th>Key</th>
<th>Value</th>
<c:forEach items="#{header}" var="header">
<tr>
<td>#{header.key}</td>
<td>#{header.value}</td>
</tr>
</c:forEach>
</table>
{header}表达式返回一个表示 HTTP 标头的映射,并使用 JSTL 标签显示标头键和值。图 2-2 显示了清单 2-18 的输出。
图 2-2 。HTTP 头信息
注意也可以用方括号([ ])代替(。)放在值表达式中。除了访问受管 bean 属性之外,方括号还可以用于访问映射和数组,正如我们在“受管 bean”一节的示例中看到的那样。
方法表达式
方法表达式可用于执行受管 beans 的公共非静态方法。方法表达式可以从
- ActionSource(或 ActionSource2)组件的 action 和 actionListener 属性,如(commandButton 和 commandLink)。
- EditableValueHolder 组件的 valueChangeListener 属性,如(inputText 和 selectOneMenu)。
- 标签的 beforePhase 和 afterPhase 属性。
在第一个应用中,我们可以修改 login commandButton 的 action 属性,用一个方法表达式代替登录页面 (index.xhtml)中的“welcome”字符串,如清单 2-19 中的粗体行所示。
清单 2-19。 修改登录页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<ui:composition template="/WEB-INF/templates/simple.xhtml">
<ui:define name="title">
#{bundle['application.loginpage.title']}
</ui:define>
<ui:define name="content">
...
<h:commandButton value="#{bundle['application.login']}" action="#{user.login}"/>
<br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
用户管理 bean 中的 login()方法很简单,如清单 2-20 所示。
清单 2-20。 用户管理 Bean 中的登录动作方法
public class User implements Serializable {
...
public String login() {
return "welcome";
}
...
}
如清单 2-20 所示,action 属性的方法有如下签名:
- 返回用于确定导航的字符串(结果)。
- 不需要争论。
此外,如果使用 EditableValueHolders,可以从 valueChangeListener 属性调用方法表达式。让我们看一个例子来解释这一点。清单 2-21 显示了饮料页面,该页面包含一个 selectOneMenu 组件,该组件包含一个饮料列表和一个显示所选饮料价格的输出文本。
清单 2-21。 饮料页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Beverages</title>
</h:head>
<h:body>
<h:form>
<h:outputText value="Select a beverage: " />
<h:selectOneMenu value="#{beverage.name}"
valueChangeListener="#{beverage.beverageSelected}"
onchange="submit()">
<f:selectItem itemLabel="---" itemValue="---"/>
<f:selectItem itemLabel="Tea" itemValue="tea" />
<f:selectItem itemLabel="Coffee" itemValue="coffee" />
<f:selectItem itemLabel="Coca-Cola" itemValue="cocacola" />
</h:selectOneMenu> <br/><br/>
<h:outputText value="You will have to pay: #{beverage.price} USD"
rendered="#{beverage.price ne null}"/>
</h:form>
</h:body>
</html>
selectOneMenu 组件呈现一个带有饮料项目列表(茶、咖啡、可口可乐)的组合框;当用户选择一种可用的饮料时,提交表单并执行 valueChangeListener 方法(饮料管理 bean 的 beverageSelected 方法)来计算所选饮料的价格。您可能会注意到,使用 rendered 属性时,如果没有饮料价格,则不会呈现 outputText。
注 ne 代表(不等于),它也相当于!=由 EL 支持。EL 还支持以下关系运算符:
- eq 代表(等于),它相当于==。
- lt 代表(小于),它相当于
- le 代表(小于或等于),它相当于< =。
- gt 代表(大于),它相当于>。
- ge 代表(大于或等于),它相当于> =。
清单 2-22 显示了饮料管理豆。
清单 2-22。 饮料管理豆
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.event.ValueChangeEvent;
@ManagedBean
@RequestScoped
public class Beverage {
private String name;
private Double price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public void beverageSelected(ValueChangeEvent event) {
String selectedBeverage = event.getNewValue().toString();
if ("tea".equals(selectedBeverage)) {
price = 2.0;
} else if ("coffee".equals(selectedBeverage)) {
price = 2.5;
} else if ("cocacola".equals(selectedBeverage)) {
price = 3.0;
}
}
}
如清单 2-22 所示,valueChangeListener 属性的方法具有以下签名:
- 返回 void。
- 接受作为 ValueChangeEvent 对象的单个参数。
ValueChangeEvent 对象保存旧值和新选择的值。可以使用 getNewValue()检索新选择的值。在 beverageSelected 方法中,检索新的选定值,并根据选定的饮料项目设置价格。图 2-3 显示了饮料示例输出。
图 2-3 。饮料示例输出
到目前为止,我们已经看到了 JSF 方法表达式的两个例子;在接下来的章节中,我们会看到许多其他的例子。
注意您不必提交整个页面来执行 EditableValueHolder 的 valueChangeListener 方法;否则,您可以使用< f:ajax >标签以 ajax 化的方式调用 valueChangeListener 方法(第五章将详细讨论< f:ajax >标签)。
需要注意的是,如果您正在使用包含 Unified EL 2.1 的 Java EE 6 容器(或更高版本),那么您可以使用参数调用任意方法。让我们看一个例子来解释这一点。清单 2-23 显示了一个定制的数学管理 bean 的 calculateAverage 方法。
清单 2-23。 自定义数学管理豆的计算平均法
@ManagedBean
@RequestScoped
public class Maths {
public Double calculateAverage (Double number1, Double number2) {
return (number1 + number2) / 2;
}
}
在 XHTML 页面中,可以使用#{maths.calculateAverage(10.5,12.3)}调用 calculateAverage 方法,输出 11.4。
闪光灯范围
闪光灯范围是自 JSF 2.0 以来引入的新范围。flash scope 概念的灵感来自 RoR (Ruby on Rails)。flash 作用域意味着“任何放置在 flash 作用域中的内容都将暴露给同一用户会话遇到的下一个视图,然后被清除。”换句话说,flash 范围内的对象对于同一浏览器窗口的下一个请求来说只可用。
如果只想为下一个请求保留短时间的信息,无论下一个请求是由 HTTP 重定向、JSF 窗体回发还是新页面的 HTTP GET 产生的,flash 范围都很有用。让我们看一个例子来理解 flash 的作用域。
调查应用是一个简单的应用,由三个页面组成:
- input.xhtml,它要求用户提供一些对调查有用的信息。
- confirm.xhtml,显示用户输入的信息,并要求用户确认或修改。
- final.xhtml,这是一个“感谢”页面。
清单 2-24 显示了 input.xhtml 页面。
清单 2-24。input . XHTML 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">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">
<ui:composition template="/WEB-INF/templates/simple.xhtml">
<ui:define name="title">
#{bundle['survey.input.page']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['survey.user.name']} "></h:outputText>
<h:inputText id="userName"
value="#{flash.userName}"
required="true">
</h:inputText>
<h:message for="userName" styleClass="errorMessage"/>
<h:outputText value="#{bundle['survey.user.age']}"></h:outputText>
<h:inputText id="age"
value="#{flash.age}"
required="true">
<f:convertNumber />
</h:inputText>
<h:message for="age" styleClass="errorMessage"/>
<h:outputText value="#{bundle['survey.user.sex']}"></h:outputText>
<h:selectOneMenu id="sex"
value="#{flash.sex}">
<f:selectItem itemLabel="Male" itemValue="male"/>
<f:selectItem itemLabel="Female" itemValue="female"/>
</h:selectOneMenu>
<h:message for="sex" styleClass="errorMessage"/>
<h:outputText value="#{bundle['survey.user.monthyIncome']}"></h:outputText>
<h:inputText id="monthlyIncome"
value="#{flash.monthlyIncome}"
required="true">
<f:convertNumber />
</h:inputText>
<h:message for="monthlyIncome" styleClass="errorMessage"/>
<h:outputText value="#{bundle['survey.user.yearlyTravelAbroad']}"></h:outputText>
<h:inputText id="yearlyTravelsAbroad"
value="#{flash.travelsAbroad}"
required="true">
<f:convertNumber />
</h:inputText>
<h:message for="yearlyTravelsAbroad" styleClass="errorMessage"/>
<h:outputText value="#{bundle['survey.user.oftenTravelBy']}"></h:outputText>
<h:selectOneMenu id="travelBy"
value="#{flash.travelBy}">
<f:selectItem itemLabel="#{bundle['survey.travelby.plane']}" itemValue="plane"/>
<f:selectItem itemLabel="#{bundle['survey.travelby.car']}" itemValue="car"/>
</h:selectOneMenu>
<h:message for="travelBy" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['survey.actions.next']}"
action="confirm?faces-redirect=true"/>
<br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
在 input.xhtml 页面中,要求用户输入姓名、年龄、性别、月收入、每年出国的次数以及用户旅行的频率。本页中最值得注意的是粗体行,其中#{flash} EL 用于在 EditableValueHolders 和 flash 范围之间进行值绑定。当表单数据有效,用户点击“下一步”命令按钮时,EditableValueHolder 值和 flash 属性之间的绑定完成,页面重定向到 confirm.xhtml 页面。清单 2-25 显示了 confirm.xhtml 页面的初始代码。
清单 2-25。??【confirm . XHTML 页面的初始代码
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<ui:composition template="/WEB-INF/templates/simple.xhtml">
<ui:define name="title">
#{bundle['survey.confirm.page']}
</ui:define>
<ui:define name="content">
<h:form>
<h:outputText value="#{bundle['survey.information.confirm']}"/>
<h:panelGrid columns="2">
<h:outputText value="#{bundle['survey.user.name']}"/>
<h:outputText value="#{flash.userName}"/>
<h:outputText value="#{bundle['survey.user.age']}"/>
<h:outputText value="#{flash.age}"/>
<h:outputText value="#{bundle['survey.user.sex']}"/>
<h:outputText value="#{flash.sex}"/>
<h:outputText value="#{bundle['survey.user.monthyIncome']}"/>
<h:outputText value="#{flash.monthlyIncome}"/>
<h:outputText value="#{bundle['survey.user.yearlyTravelAbroad']}"/>
<h:outputText value="#{flash.travelsAbroad}"/>
<h:outputText value="#{bundle['survey.user.oftenTravelBy']}"></h:outputText>
<h:outputText value="#{flash.travelBy}"/>
</h:panelGrid>
<h:commandButton value="#{bundle['survey.actions.save']}"
action="#{survey.save}"/>
<h:commandButton value="#{bundle['survey.actions.modify']}"
action="input?faces-redirect=true"/>
<br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
确认页面使用#{flash.attributeName}显示所有输入的调查信息。在这个页面中,用户可以保存输入的信息并导航到 final.xhtml 页面,也可以修改 input.xhtml 页面中的信息。
但是,如果您单击“修改”命令按钮来修改输入的信息,您会发现 input.xhtml 字段为空(换句话说,flash 信息丢失),这是合乎逻辑的,因为一旦呈现 confirm.xhtml 页面,flash 作用域的生命就结束了。为了解决这个问题,您只需使用 keep 关键字来为下一个请求保留 flash 信息,如下所示:#{flash.keep.attributeName}。清单 2-26 显示了修改后的 confirm.xhtml 页面。
清单 2-26。 修改确认. xhtml 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<ui:composition template="/WEB-INF/templates/simple.xhtml">
<ui:define name="title">
#{bundle['survey.confirm.page']}
</ui:define>
<ui:define name="content">
<h:form>
<h:outputText value="#{bundle['survey.information.confirm']}"/>
<h:panelGrid columns="2">
<h:outputText value="#{bundle['survey.user.name']}"/>
<h:outputText value="#{flash.keep.userName}"/>
<h:outputText value="#{bundle['survey.user.age']}"/>
<h:outputText value="#{flash.keep.age}"/>
<h:outputText value="#{bundle['survey.user.sex']}"/>
<h:outputText value="#{flash.keep.sex}"/>
<h:outputText value="#{bundle['survey.user.monthyIncome']}"/>
<h:outputText value="#{flash.keep.monthlyIncome}"/>
<h:outputText value="#{bundle['survey.user.yearlyTravelAbroad']}"/>
<h:outputText value="#{flash.keep.travelsAbroad}"/>
<h:outputText value="#{bundle['survey.user.oftenTravelBy']}"></h:outputText>
<h:outputText value="#{flash.keep.travelBy}"/>
</h:panelGrid>
<h:commandButton value="#{bundle['survey.actions.save']}"
action="#{survey.save}"/>
<h:commandButton value="#{bundle['survey.actions.modify']}"
action="input?faces-redirect=true"/>
<br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
使用#{flash.keep.attributeName}将确保为下一个请求保留闪存属性。
注意你不能为了保留 HTTP 重定向后的信息而使用请求作用域,因为 HTTP 重定向创建了一个新的请求,这意味着当前的请求信息将会丢失。
清单 2-27 显示了调查管理 bean 的代码,它包括一个单独的方法 save(),该方法检索 flash 属性,然后在控制台中打印它们。
清单 2-27。 调查托管豆
package com.jsfprohtml5.survey.model;
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.context.FacesContext;
import javax.faces.context.Flash;
@ManagedBean
public class Survey implements Serializable {
public String save() {
Flash flash = FacesContext.getCurrentInstance().getExternalContext().getFlash();
// Read the information from the flash
String userName = (String) flash.get("userName");
Number age = (Number) flash.get("age");
String sex = (String) flash.get("sex");
Number monthlyIncome = (Number) flash.get("monthlyIncome");
Number travelsAbroad = (Number) flash.get("travelsAbroad");
String travelBy = (String) flash.get("travelBy");
System.out.println("Flash information are: \n{\n" +
"Name: " + userName + ", \n" +
"Age: " + age + ", \n" +
"Sex: " + sex + ", \n" +
"monthlyIncome: " + monthlyIncome + ", \n" +
"travelsAbroad: " + travelsAbroad + ", \n" +
"travelBy: " + travelBy + "\n" +
"}");
// Save the information in the survey database ...
// ...
return "final?faces-redirect=true";
}
}
可以使用 ExternalContext 的 getFlash() API 获取 Flash 对象。之后,您可以使用 flash 对象的 get()方法检索 Flash 属性。最后,您可以对检索到的 flash 属性做任何您想做的事情(例如将它们保存在结构化数据库中,或者使用它们来启动工作流...).
注意调查应用的完整源代码可在本书网站www.apress.com/9781430250104获得(附在第二章源代码 zip 文件中)。
航行
在 JSF 框架中,有两种类型的导航:
- 隐式导航。
- 基于规则的导航。
接下来的部分举例说明了这两种导航类型,以及何时使用它们。
隐式导航
在第一个应用中,我们已经有了两个页面导航的例子。第一个是关于
<h:commandButton value="#{bundle['application.login']}" action="welcome"/>
第二个例子是关于,它从 welcome.xhtml 页面导航到 index.xhtml 页面:
<h:link value="#{bundle['application.welcomepage.return']}" outcome="index"/>
上面提到的两个例子代表了第一种类型的 JSF 导航,称为“隐式导航隐式导航是在 JSF 框架的 2.0 版本中引入的。之所以称之为隐式,是因为使用它,您不必在 JSF 配置文件(faces-config.xml)中定义导航规则;您所需要做的就是在动作或结果属性中指定目标页面的相对路径(您不需要提到。xhtml 扩展,因为 JSF 导航系统会为您添加它并导航到目标页面)。
使用隐式导航,您还可以将 HTTP 重定向到目标页面,而不是将 HTTP 请求转发到目标页面(这是默认行为)。这可以通过使用 faces-redirect 参数来执行,如下所示:
<h:commandButton value="#{bundle['application.login']}" action="welcome?faces-redirect=true"/>
将 faces-redirect 参数设置为 true 会告诉 JSF 导航系统进行 HTTP 重定向,而不是 HTTP 请求转发。
JSF 隐式导航最重要的优点是它的简单性;然而,它的缺点之一是不灵活,当您在 JSF 应用中有一个复杂的导航时,您需要重命名其中一个目标导航页面。在这种情况下,您必须重新访问包含目标导航页面的所有页面,以便将旧页面名称更改为新页面名称。因此,建议将 JSF 隐式导航用于小型应用、原型开发或概念验证。
基于规则的导航
在基于规则的导航 中,导航规则在 Faces 配置文件(faces-config.xml)中定义。基于规则的导航由一组导航规则组成。每个导航规则可以有一个或多个导航案例。清单 2-28 显示了一个基于规则的导航示例。
清单 2-28。 基于规则的导航示例 1
<faces-config ...>
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-action>#{exampleBean.doAction}</from-action>
<from-outcome>success</from-outcome>
<to-view-id>/welcome.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-action>#{exampleBean.doAction}</from-action>
<from-outcome>fail</from-outcome>
<to-view-id>/invalid.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
</faces-config>
如示例所示,导航规则可以包含以下元素:
(可选),表示导航开始的视图。 ,可以是导航规则内的 1 到 N。
导航案例可以包含以下元素:
(可选),保存 EL 表达式,引用返回字符串(outcome)的动作方法。 ,表示字符串文字结果。在存在 元素的情况下, 值与 返回的结果进行比较,如果两个值匹配,则导航进行到 。如果 元素不存在,则将 与动作源组件的动作属性进行比较,如果两个值匹配,则导航进行到 (将在下一个导航示例中说明)。 ,代表目标视图。
回头看示例,从 index.xhtml 页面开始,当执行#{exampleBean.doAction}操作方法并返回“success”结果时,第一个导航案例触发;在这种情况下,页面将被转发到 welcome.xhtml 页面。另一个导航案例在执行#{exampleBean.doAction}操作方法并返回“失败”结果时触发;在这种情况下,页面将被转发到 invalid.xhtml 页面。清单 2-29 显示了 ExampleBean 管理的 Bean 的重要部分。
清单 2-29。 比恩比恩
@ManagedBean
@RequestScoped
public class ExampleBean implements Serializable {
public String doAction() {
if (validateInformation()) {
return "success";
} else {
return "fail";
}
}
private boolean validateInformation() {
/* Some calls can be performed to the business services here ... */
}
}
如前所述,
清单 2-30。 基于规则的导航示例 2
<faces-config ...>
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/welcome.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>fail</from-outcome>
<to-view-id>/invalid.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
</faces-config>
当省略
除了动作源组件之外,
注意JSF(隐式和基于规则的)导航可以使用
- 通过动作属性的
和 。
action 和 outcome 属性都可以接受字符串文字或 EL 表达式,后者是指返回字符串(在 action 属性的情况下)或计算结果为字符串(在 outcome 属性的情况下)的 EL 表达式的方法。
元素也可以省略,如清单 2-31 中的所示。
清单 2-31。 基于规则的导航示例 3
<navigation-rule>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/welcome.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>fail</from-outcome>
<to-view-id>/invalid.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
当
注意重要的是要知道导航案例在<导航规则>中的执行顺序。
JSF 导航的一个很好的特性是支持
清单 2-32。 基于规则的导航示例 4
<navigation-rule>
<from-view-id>/common/*</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/welcome.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>fail</from-outcome>
<to-view-id>/invalid.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
前面的例子意味着,通过使用/common/*模式,导航案例将只应用于 common 文件夹下的所有页面。
基于规则的导航的默认行为是将请求转发到目标页面;但是,也可以通过使用 HTTP 重定向来改变这种行为。清单 2-33 展示了如何使用 HTTP 重定向进行导航。
清单 2-33。 基于规则的导航示例 5
<navigation-rule>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/welcome.xhtml</to-view-id>
<redirect/>
</navigation-case>
<navigation-case>
<from-outcome>fail</from-outcome>
<to-view-id>/invalid.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
如前面的清单所示,将导航案例行为从 HTTP 请求转发更改为 HTTP 重定向非常简单。使用
高级导航
我们想在 JSF 导航中介绍的最后一个特性是条件导航特性,它在处理复杂的导航情况时非常有用。为了理解这个特性,我们来看一个详细的例子。在这个例子中,用户输入(他/她的)每天的平均睡眠时间,以便知道(他/她的)睡眠时间是否是:
- 低于平均值(< 7 小时)。
- 正常(7 至 9 小时)。
- 高于平均值(> 9 小时)。
图 2-4 显示了示例流程。该流程由四页组成:
- xhtml 表示开始输入页面。在该页面中,用户输入(他/她)的睡眠时间,然后点击“检查我的睡眠时间”按钮进入下一页面。
- normal.xhtml 表示当用户输入 7 到 9 之间的数字时将显示的页面。
- xhtml 表示当用户输入大于 9 的数字时将显示的页面。
- xhtml 表示当用户输入小于 7 的数字时将显示的页面。
图 2-4 。该示例流程
清单 2-34 显示了开始输入页面的代码(input.xhtml)。
清单 2-34。 输入. xhtml 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Sleeping hours checker</title>
</h:head>
<h:body>
<h:form>
<h1>Sleeping hours checker</h1>
<h:outputText value="Enter your sleeping hours: "/>
<h:inputText id="sleepingHours"
value="#{sleeping.hours}"
required="true">
<f:convertNumber integerOnly="true" maxIntegerDigits="2" />
</h:inputText>
<br/>
<h:commandButton action="proceed" value="Check my sleeping hours"/>
</h:form>
</h:body>
</html>
该表单包含一个输入文本 sleepingHours,这是必需的,通过使用
注意从第一章可知,JSF 转换是关于将 HTTP 请求参数转换成相应的 Java 类型,以便消除开发人员为每个 web 应用实现该功能所需的开销。在本例中,< f:convertNumber/ >转换器向其父输入文本组件(sleepingHours)注册了一个数字转换器,通过将 integerOnly 属性设置为 true,只对输入文本值的整数部分进行格式化和解析。将 maxIntegerDigits 设置为 2 意味着要解析和格式化的整数的最大位数将为 2(转换和验证将在第三章的中详细介绍)。
当点击“检查我的睡眠时间”命令按钮时,它产生“继续”结果,根据用户输入,我们希望显示正确的结果页面。多亏了 JSF 条件导航,这可以从 Faces 配置文件中完成,如清单 2-35 所示。
清单 2-35。faces-config . XML 文件中的条件导航
<faces-config ...>
<navigation-rule>
<from-view-id>/input.xhtml</from-view-id>
<navigation-case>
<if>#{sleeping.hours le 9 and sleeping.hours ge 7}</if>
<from-outcome>proceed</from-outcome>
<to-view-id>/normal.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<if>#{sleeping.hours lt 7}</if>
<from-outcome>proceed</from-outcome>
<to-view-id>/below.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<if>#{sleeping.hours gt 9}</if>
<from-outcome>proceed</from-outcome>
<to-view-id>/above.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
</faces-config>
提示重要的是要知道< to-view-id >可以接受 JSF EL 表达式,JSF 导航处理程序将对该表达式进行求值,以便获得视图标识符。
在后台,JSF 导航由 NavigationHandler 类处理。如果 NavigationHandler 类匹配一个导航事例,它将通过用新视图调用 facescontext . setviewroot(UIViewRoot)API 来更改当前视图。了解如何使用 NavigationHandler 类是很有用的,因为您可能需要在 JSF 应用的许多地方直接使用它(例如阶段侦听器和异常处理程序[在“异常处理”一节中有一个这样的示例])。清单 2-36 显示了一个使用 NavigationHandler 的例子。
清单 2-36。 使用 NavigationHandler 类的例子
FacesContext context = FacesContext.getCurrentInstance();
NavigationHandler navigationHandler = context.getApplication().getNavigationHandler();
navigationHandler.handleNavigation(context, "#{myBean.handleFlow1}", "flow1");
NavigationHandler 类有一个 handleNavigation 方法,该方法带有以下参数:
- 表示当前的 JSF FacesContext。
- fromAction:一个字符串,表示产生指定结果的操作方法表达式(" #{myBean.handleFlow1} ")。它可以为空。
- outcome:表示结果的字符串(“flow1”)。它可以为空。
注意当一个特定动作的结果为 null 时,NavigationHandler 不做任何事情,这意味着当前视图将被重新显示。
异常处理
异常处理 是 Java EE web 应用中必须注意的最重要的问题之一。异常处理有许多好处:当出现应用错误时,它可以为应用最终用户显示友好的消息,从最终用户的角度来看,这增加了使用应用的信任;除此之外,异常处理允许应用开发人员轻松地排除和调试应用缺陷。从 JSF 2.0 开始,框架支持异常处理机制,以便在 JSF 应用中有一个集中的位置来处理异常。
让我们为第一个应用创建一个定制的 JSF 异常处理程序,以便理解如何在 JSF 应用中处理异常。为了给 JSF 应用创建一个定制的异常处理程序,我们需要做三件事:
- 创建处理应用异常的自定义异常处理程序类。这个处理程序类应该扩展一个异常处理包装类(比如 ExceptionHandlerWrapper 类)。
- 创建自定义异常处理程序工厂类,该类负责创建异常处理程序类的实例。自定义异常处理程序类应扩展 JSF ExceptionHandlerFactory 类。
最后,在 faces-config.xml 文件中注册自定义异常处理程序工厂类,该文件被添加到第一个应用示例中。清单 2-37 显示了自定义异常处理程序类,它扩展了添加到第一个应用例子中的 ExceptionHandlerWrapper 类。
清单 2-37。??【CustomExceptionHandler】类
package com.jsfprohtml5.firstapplication.exceptions;
import java.util.Iterator;
import javax.faces.FacesException;
import javax.faces.application.NavigationHandler;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerWrapper;
import javax.faces.context.FacesContext;
import javax.faces.event.ExceptionQueuedEvent;
import javax.faces.event.ExceptionQueuedEventContext;
public class CustomExceptionHandler extends ExceptionHandlerWrapper {
private ExceptionHandler wrapped;
public CustomExceptionHandler(ExceptionHandler wrapped) {
this.wrapped = wrapped;
}
@Override
public ExceptionHandler getWrapped() {
return wrapped;
}
@Override
public void handle() throws FacesException {
Iterator i = getUnhandledExceptionQueuedEvents().iterator();
while (i.hasNext()) {
ExceptionQueuedEvent event = (ExceptionQueuedEvent) i.next();
ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
Throwable t = context.getException();
FacesContext fc = FacesContext.getCurrentInstance();
try {
/* Here you can use the Throwable object in order to verify the exceptions you want to handle in the application */
NavigationHandler navigationHandler = fc.getApplication().getNavigationHandler();
navigationHandler.handleNavigation(fc, null, "error?faces-redirect=true");
fc.renderResponse();
} finally {
i.remove();
}
}
// Call the parent exception handler’s handle() method
getWrapped().handle();
}
}
CustomExceptionHandler 类的核心方法是 handle()方法,它负责处理 JSF 应用中的异常。值得注意的是,getUnhandledExceptionQueuedEvents()方法可用于获取 JSF 应用中所有未处理的异常。返回的 Iterable 对象中的每一项都代表一个 ExceptionQueuedEvent 对象。从 ExceptionQueuedEvent 对象中,可以获得 ExceptionQueuedEventContext 对象,从中可以检索 Throwable 对象。使用 Throwable 对象,您可以验证您想要在应用中处理的异常。最后,使用 NavigationHandler 导航到应用错误页面(error.xhtml),并从 Iterable 对象中删除 ExceptionQueuedEvent。清单 2-38 显示了 error.xhtml 页面。
清单 2-38。 申请错误页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Error</title>
<link href="#{request.contextPath}/css/simple.css" rel="stylesheet" type="text/css"/>
</h:head>
<h:body>
<h2 class="errorMessage">
An error occurs. return to <a href="index.xhtml">login</a> page.
</h2>
</h:body>
</html>
其次,我们需要创建自定义异常处理程序工厂类,该类负责创建 custom exception handler 类的实例。清单 2-39 显示了 CustomExceptionHandlerFactory 类。
清单 2-39。CustomExceptionHandlerFactory 类
package com.jsfprohtml5.firstapplication.exceptions;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerFactory;
public class CustomExceptionHandlerFactory extends ExceptionHandlerFactory {
private ExceptionHandlerFactory parent;
public CustomExceptionHandlerFactory(ExceptionHandlerFactory parent) {
this.parent = parent;
}
@Override
public ExceptionHandler getExceptionHandler() {
ExceptionHandler result = new CustomExceptionHandler(parent.getExceptionHandler());
return result;
}
}
最后,我们需要在 faces-config.xml 中注册自定义异常处理程序工厂类,如清单 2-40 所示。
清单 2-40。 在 JSF 配置文件中注册自定义异常处理程序工厂类
<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.1"
FontName">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_2_1.xsd">
<factory>
<exception-handler-factory>
com.jsfprohtml5.firstapplication.exceptions.MyExceptionHandlerFactory
</exception-handler-factory>
</factory>
<!-- ... -->
</faces-config>
在 firstApplication 中设置了这个异常处理机制后,如果 firstApplication 因为某种原因抛出异常,那么 error.xhtml 页面就会显示为图 2-5 。
图 2-5 。错误处理页面(error.xhtml)
注更新后的 firstApplication 示例的完整源代码可在 www.apress.com/9781430250104 的图书网站上获得(附在第二章源代码 zip 文件中)。
摘要
在本章中,您详细了解了声明、初始化和管理 JSF 管理的 beans 的依赖项的不同方法。您还详细了解了如何在您的 JSF 应用中使用 EL。现在,您还详细了解了如何有效地使用 JSF 导航系统,以及如何利用 JSF 异常处理机制来增强您的 JSF 应用的错误处理能力。
三、引擎盖下的 JSF——第二部分
JSF 转换和验证是 JSF 框架中最重要的主题之一,本章将详细介绍。本章说明了转换和验证在 JSF 请求处理生命周期中的工作原理。您将学习如何在您的 JSF 应用中使用和定制标准的 JSF 转换器和验证器,以及当标准的转换器和验证器不能完全满足应用的需求时,如何创建您自己的定制转换器和验证器。最后,您将了解如何利用 Java Bean Validation(JSR 303)API 来支持和标准化您的 JSF 应用验证。
JSF 生命周期中的转换和验证
正如我们从第一章中了解到的,转换是将 HTTP 请求参数转换为相应的 Java 类型,以消除开发人员为每个 web 应用实现该功能所需的开销,而验证是针对特定条件验证用户输入。JSF 生命周期中的转换和验证过程可以分三个阶段进行,如图图 3-1 所示。
图 3-1 。可能发生转换和验证的 JSF 阶段
下图说明了 JSF 转换和验证是如何在
- 流程验证阶段,针对所有未将 immediate 属性设置为 true 的组件。
- 为 immediate=“真”的组件应用请求值阶段。
- 渲染响应阶段。
在流程验证阶段和应用请求值阶段,都会发生从 HTTP 请求字符串到 Java 类型的转换(使用 JSF 转换器接口中的 getAsObject API),然后执行 JSF 验证。在呈现响应阶段,会发生从 Java 类型到字符串的转换(使用 JSF 转换器接口中的 getAsString API ),以便为呈现做好准备。
注意注意,immediate 属性既可以应用于 UICommand 组件(如 CommandButton 和 CommandLink),也可以应用于 EditableValueHolder 组件(如 inputText)。
转换可以应用于所有 ValueHolder 组件(这包括 UIOutput 和 UIInput—它扩展了 ui output—组件),而验证只能应用于 EditableValueHolder 组件(这包括 ui input 组件)。在接下来的部分中,将详细说明 JSF 转换和验证。
转换
为了理解 JSF 转换,我们需要知道三个主题:转换器接口 API,标准 JSF 转换器,以及最后如何在 JSF 构建一个定制的转换器。接下来的小节将详细阐述这些主题。
转换器接口
所有 JSF 转换器都必须实现 javax.faces.convert.Converter 接口。Converter 接口描述了一个 Java 类,该类可以在模型数据对象和适合于呈现的这些模型对象的字符串表示之间执行对象到字符串和字符串到对象的转换。清单 3-1 显示了 JSF 转换器接口。
清单 3-1。 JSF 转换器界面
package javax.faces.convert;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
public interface Converter {
public Object getAsObject(FacesContext context, UIComponent component, String value);
public String getAsString(FacesContext context, UIComponent component, Object value);
}
如转换器接口所示,它包含两个 API:
第一个 API 是 getAsObject,,它执行字符串到对象的转换;可以在流程验证阶段(或应用请求值阶段)调用此 API。它有三个参数:
- 上下文,表示此请求的 JSF FacesContext 实例。
- component,它表示其值将被转换的组件。如果转换器需要使用组件属性,可以使用这个组件实例来检索组件属性。
- 值,它表示要转换为对象的字符串值。
第二个 API 是 getAsString,,它执行对象到字符串的转换;这个 API 在渲染响应阶段被调用。它需要三个参数:
- 上下文,表示此请求的 JSF FacesContext 实例。
- component,它表示其值将被转换的组件。如果转换器需要使用组件属性,可以使用这个组件实例来检索组件属性。
- 值,它表示要转换为字符串的对象值。
每个组件可以有一个或多个转换器。如果由于错误导致转换无法执行,转换器必须抛出 converter exception;在这种情况下,拥有转换器的组件将被标记为无效,ConverterException 消息将被接收并添加到 FacesContext 消息中,以便由< h:message >和< h:messages >组件显示。清单 3-2 展示了一个例子,说明了 JSF 转换是如何为带有附加转换器的多个值持有者组件工作的。
清单 3-2。 多个 ValueHolder 组件的转换示例
<h:form>
<h1>Test form</h1>
<h:outputText value="Enter First Number: "/>
<h:inputText id="firstNumber"
value="#{testBean.firstNumber}">
<f:convertNumber/>
</h:inputText>
<h:message for="firstNumber"/>
<br/>
<h:outputText value="Enter Second Number: "/>
<h:inputText id="secondNumber"
value="#{testBean.secondNumber}">
<f:convertNumber/>
</h:inputText>
<h:message for="secondNumber"/>
<br/>
<h:commandButton value="submit"/>
</h:form>
在这个例子中,我们有一个包含两个 inputText 组件的表单。每个 inputText 组件都有一个附属的
标准 JSF 转换器
现在,让我们深入了解 JSF 标准转换器的细节。表 3-1 显示了 JSF 标准转换器。
表 3-1 。JSF 标准转换器
|
转换器 ID
|
描述
|
| --- | --- |
| javax.faces.Boolean
| 可以应用于Boolean
和boolean
Java 类型的隐式转换器。 |
| javax.faces.Byte
| 可以应用于Byte
和byte
Java 类型的隐式转换器。 |
| javax.faces.Character
| 可以应用于Character
和char
Java 类型的隐式转换器。 |
| javax.faces.Short
| 可以应用于Short
和short
Java 类型的隐式转换器。 |
| javax.faces.Integer
| 可以应用于Integer
和int
Java 类型的隐式转换器。 |
| javax.faces.Long
| 可以应用于Long
和long
Java 类型的隐式转换器。 |
| javax.faces.Float
| 可以应用于Float
和float
Java 类型的隐式转换器。 |
| javax.faces.Double
| 可以应用于Double
和double
Java 类型的隐式转换器。 |
| javax.faces.BigDecimal
| 可以应用于BigDecimal
Java 类型的隐式转换器。 |
| javax.faces.BigInteger
| 可以应用于BigInteger
Java 类型的隐式转换器。 |
| javax.faces.Number
| 可用于将用户输入转换为Number
Java 类型的显式转换器。 |
| javax.faces.DateTime
| 可用于将用户输入转换为java.util.Date
Java 类型的显式转换器。 |
如表所示,JSF 转换器有两种类型:
- 隐式转换器。
- 显式转换器。
隐式转换器自动应用于列出的值类型。例如,假设我们有下面的计算器管理 bean,如清单 3-3 所示。
清单 3-3。 计算器托管豆
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
@ManagedBean
@RequestScoped
public class Calculator implements Serializable {
private Double firstNumber;
private Double secondNumber;
private Double result;
public Double getFirstNumber() {
return firstNumber;
}
public void setFirstNumber(Double firstNumber) {
this.firstNumber = firstNumber;
}
public Double getSecondNumber() {
return secondNumber;
}
public void setSecondNumber(Double secondNumber) {
this.secondNumber = secondNumber;
}
public Double getResult() {
return result;
}
public void setResult(Double result) {
this.result = result;
}
public String calculateSum() {
result = firstNumber + secondNumber;
return null;
}
}
清单 3-4 显示了使用计算器管理 bean 的 XHTML 文件。
清单 3-4。 计算器 XHTML 关联页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Calculator</title>
</h:head>
<h:body>
<h:form>
<h:panelGrid columns="2">
<h:outputText value="First number:"/>
<h:inputText id="fNumber"
value="#{calculator.firstNumber}"
required="true">
</h:inputText>
<h:outputText value="Second number:"/>
<h:inputText id="sNumber"
value="#{calculator.secondNumber}"
required="true">
</h:inputText>
</h:panelGrid>
<h:commandButton action="#{calculator.calculateSum}" value="Sum"/><br/>
<h:outputText value="Result: #{calculator.result}"/>
</h:form>
</h:body>
</html>
使用隐式 javax.faces.Double 转换器将#{calculator.firstNumber}和#{calculator.secondNumber}表达式转换为 Double 后,这两个表达式会自动绑定到 calculator 托管 bean 的 firstNumber 和 secondNumber 属性。与 Double 类型一样,隐式转换器也应用于 Boolean、Byte、Character、Short、Integer、Long、Float、BigDecimal 和 BigInteger Java 类型。
显式转换器必须显式地附加到组件上。目前,JSF 核心标签库提供了以下代表 JSF 显式转换器的标签:
清单 3-5。 包含日期属性的托管 Bean 示例
import java.io.Serializable;
import java.util.Date;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
@ManagedBean
@RequestScoped
public class TestBean implements Serializable {
// ...
private Date birthDate;
public Date getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
// ...
}
为了将输入字符串转换为“dd-MM-yyyy”格式的生日属性,您可以将
<h:outputText value="Date of birth:"/>
<h:inputText id="birthDate"
value="#{testBean.birthDate}"
required="true">
<f:convertDateTime pattern="dd-MM-yyyy"/>
</h:inputText>
如果用户在“出生日期”字段中输入一个不能转换为“dd-MM-yyyy”格式的日期对象的值,将显示一条转换错误消息。清单 3-2 展示了一个< f:convertNumber >转换器的例子,它将输入字符串转换成一个 Java 数字对象。此外,它还有许多格式化功能。假设我们有 TestBean 管理的 bean #{testBean.someNumber}的 some number(Double 类型)属性,并且我们希望将其格式化为数字格式###,###。###;这可以通过使用< f:convertNumber >的模式属性来实现,如下所示:
<h:outputText value="#{testBean.someNumber}">
<f:convertNumber pattern="###,###.###"/>
</h:outputText>
假设#{testBean.someNumber}的计算结果为 123456.124(例如),它将显示为 123,456.124。
注你可以在
docs . Oracle . com/javase/7/docs/API/Java/text/decimal format . html
了解更多关于 Java NumberFormat 的信息
如果我们只想显示#{testBean.someNumber}数字的两位小数,我们可以使用
<h:outputText value="#{testBean.someNumber}">
<f:convertNumber maxFractionDigits="2"/>
</h:outputText>
例如,如果#{testBean.someNumber}的计算结果为 123456.124,它将显示为 123,456.12。
使用
<h:outputText value="#{testBean.someNumber}">
<f:convertNumber currencyCode="EGP" type="currency"/>
</h:outputText>
例如,如果#{testBean.someNumber}的计算结果为 2000,它将显示为 EGP2,000.00。
注货币代码在 ISO 4217 定义。你可以从 http://en.wikipedia.org/wiki/ISO_4217 得到货币代码的完整列表
使用
<h:outputText value="#{testBean.someNumber}">
<f:convertNumber type="percent"/>
</h:outputText>
例如,如果#{testBean.someNumber}的评估值为 0.3,它将显示为 30%。
注意注意,您可以通过使用 EditableValueHolders 的 converterMessage 属性来覆盖不同的转换消息。例如:<h:input text id = " some number " value = " # { bean . some number } " converter message = "不是数字!!!"/ >
如果数字转换失败,将显示转换错误消息“不是数字”。
构建定制 JSF 转换器
除了 JSF 框架提供的所有提到的隐式和显式转换器,JSF 允许开发人员创建他们自己的自定义转换器。让我们看一个例子来说明这个想法。假设我们需要将用户输入字符串转换为位置对象。为了达到这个要求,我们需要开发一个定制的转换器(例如 LocationConverter ),它将输入字符串转换为位置对象,然后将位置对象转换为友好的字符串,该字符串可以在呈现响应阶段显示给最终用户。清单 3-6 显示了我们需要用户输入转换到的位置类。
清单 3-6。 位置类
package com.jsfprohtml5.example.model;
public class Location {
private String address;
private String city;
private String country;
public Location() {
}
public Location(String address, String city, String country) {
this.address = address;
this.city = city;
this.country = country;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
Location 类是一个简单的 Java bean,包含三个属性(地址、城市、国家)。为了实现我们的 LocationConverter 自定义转换器,我们需要扩展 JSF 转换器接口并实现 getAsObject 和 getAsString 方法。清单 3-7 显示了 LocationConverter 的实现。
清单 3-7。 LocationConverter 类
package com.jsfprohtml5.example.converters;
import com.jsfprohtml5.example.model.Location;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.convert.FacesConverter;
@FacesConverter("com.jsfprohtml5.LocationConverter")
public class LocationConverter implements Converter {
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (null == value || 0 == value.length()) {
return null;
}
String locationParts[] = value.split(",");
if (locationParts.length != 3
|| locationParts[0].length() == 0
|| locationParts[1].length() == 0
|| locationParts[2].length() == 0) {
FacesMessage message = new FacesMessage("Invalid Location format (address, city, country).",
"Use the following format {address, city, country)}.");
message.setSeverity(FacesMessage.SEVERITY_ERROR);
throw new ConverterException(message);
}
String address = locationParts[0];
String city = locationParts[1];
String country = locationParts[2];
Location location = new Location(address, city, country);
return location;
}
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
Location location = (Location) value;
return location.getAddress() + ", " +
location.getCity() + ", " +
location.getCountry();
}
}
在流程验证阶段(或应用请求值阶段)调用的 getAsObject()方法中,来自输入字符串的转换被转换为 location 对象,其类在清单 3-6 中提到,如粗体行所示,如果输入字符串不符合 Location 格式规范,则抛出一个 ConverterException,并显示一条 faces 错误消息。位置格式被指定为以下形式:
Address, City, Country
在渲染响应阶段调用的 getAsString()方法中,执行从位置对象到输出渲染字符串的转换。注意@FacesConverter 注释很重要,它用于在 JSF 应用中注册转换器。 @FacesConverter 注释有两个主要属性:value()属性,作为转换器 ID;for class()属性,作为类的转换器。在本例中,我们只使用了 value()属性,并将转换器 ID 声明为“com . jsfprohtml 5 . location converter”。
不使用@FacesConverter 批注,您可以在 JSF faces-config.xml 文件中声明转换器,如下所示:
<faces-config ...>
<converter>
<converter-id>com.jsfprohtml5.LocationConverter</converter-id>
<converter-class>com.jsfprohtml5.example.converters.LocationConverter</converter-class>
</converter>
</faces-config>
现在,让我们看看如何在 JSF 应用中使用 LocationConverter。清单 3-8 显示了 TestBean 管理的 bean ,它包括一个位置属性(Location)。
清单 3-8。 TestBean 托管 Bean 类
package com.jsfprohtml5.example.model;
import java.io.Serializable;
import java.util.Date;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
@ManagedBean
@RequestScoped
public class TestBean implements Serializable {
// ...
private Location location;
// ...
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
// ...
public String proceed() {
return null;
}
}
清单 3-9 显示了 LocationConverter 转换器的 XHTML 测试页面。
清单 3-9。location converter XHTML 测试页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Location Converter Test</title>
</h:head>
<h:body>
<h:form>
<h1>Converter Test</h1>
<h:outputText value="Enter location: "/>
<h:inputText id="location"
value="#{testBean.location}"
required="true">
<f:converter converterId="com.jsfprohtml5.LocationConverter" />
</h:inputText>
<br/>
<h:commandButton action="#{testBean.proceed}" value="Proceed"/><br/>
<h:outputText value="#{testBean.location}">
<f:converter converterId="com.jsfprohtml5.LocationConverter" />
</h:outputText>
<h:messages style="color: red"/>
</h:form>
</h:body>
</html>
如粗体行所示,在转换器测试页面中,有一个输入文本和一个输出文本,它们使用了 LocationConverter 的
图 3-2 显示了当用户输入不符合位置(地址、城市、国家)格式的无效位置信息时将显示的转换错误。
图 3-2 。LocationConverter 错误消息
图 3-3 显示了当用户输入符合位置(地址、城市、国家)格式的有效位置信息时,LocationConverter 的行为。
图 3-3 。输入和输出文本的 LocationConverter 行为
验证
为了理解 JSF 验证,我们需要知道四个主题:验证器接口 API,标准的 JSF 验证器,如何在 JSF 构建一个定制的验证器,以及最后如何使用 Java Bean 验证(JSR 303)API。接下来的小节将详细阐述这些主题。
验证器接口
Java x . faces . validator . validator 接口是 JSF 验证器的核心接口。JSF 验证器接口描述了一个可以对 EditableValueHolder 组件执行验证(检查正确性)的 Java 类。单个 EditableValueHolder 在视图中可以有零个或多个验证器。清单 3-10 显示了 JSF 验证器接口。
清单 3-10。 JSF 验证器界面
package javax.faces.validator;
import java.util.EventListener;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
public interface Validator extends EventListener {
//...
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException;
}
如验证器接口所示,它包含一个 API。validate API 对父 EditableValueHolder 组件的值执行所需的验证;这个 API 可以在 流程验证阶段中调用(或者在应用请求值阶段中调用,如果 EditableValueHolder 的 immediate 属性设置为 true)。validate() API 有三个参数:
- 上下文,表示此请求的 JSF FacesContext 实例。
- 组件,它表示其值将被验证的组件。如果验证器需要使用组件属性,可以使用这个组件实例来检索组件属性。
- 值,它表示要验证的字符串值。
对于每个有验证器(或更多)的组件。如果验证失败,验证器必须抛出 ValidatorException 在这种情况下,拥有验证器的组件将被标记为无效,ValidatorException 消息将被接收并添加到 FacesContext 消息中,以便由与 EditableValueHolder 组件关联的< h:message >组件和< h:messages >组件显示。
清单 3-11 展示了一个例子来说明如何对多个带有验证器的 EditableValueHolder 组件进行验证。请注意,在本例中,number1 和 number2 是 TestBean 管理的 Bean 中的属性,属于 Long 类型。
清单 3-11。 多个 EditableValueHolder 组件的验证示例
<h:form>
<h:outputText value="Enter Number1: "/>
<h:inputText id="number1"
value="#{testBean.number1}">
<f:validateRequired/>
<f:validateLongRange minimum="0" maximum="999"/>
</h:inputText>
<h:message for="number1"/>
<br/>
<h:outputText value="Enter Number2: "/>
<h:inputText id="number2"
required="true"
value="#{testBean.number2}">
<f:validateLongRange minimum="0" maximum="999"/>
</h:inputText>
<h:message for="number2"/>
<br/>
<h:commandButton value="submit"/>
</h:form>
我们有一个包含两个 inputText 组件(number1 和 number2)的表单。每个 inputText 组件都有两个附加的验证器。每个输入文本都有以下验证器:
- < f:validateRequired >,用于验证 EditableValueHolder 组件不会包含空输入(使用 required="true "属性效果相同)。
- < f:validateLongRange >,用于验证长整型字段的值是否在指定的范围内(最小值和最大值)。
标准 JSF 验证器
现在,让我们深入 JSF 标准验证器的细节。表 3-2 显示了 JSF 标准验证器。
表 3-2 。JSF 标准验证器
|
验证器标签
|
描述
|
| --- | --- |
| <f:validateRequired>
| 用于验证EditableValueHolder
(如输入文本)值是必需的。设置required="true"
属性具有相同的效果。 |
| <f:validateLongRange>
| 用于验证长整型的EditableValueHolder
值是否在指定范围内。 |
| <f:validateDoubleRange>
| 用于验证双精度的EditableValueHolder
值是否在指定范围内。 |
| <f:validateLength>
| 用于验证EditableValueHolder
值是否在指定的长度范围内。 |
| <f:validateRegex>
| 用于验证EditableValueHolder
值是否符合指定的 Java 正则表达式。 |
| <f:validateBean>
| 用于将EditableValueHolder
本地值验证分配给 Java Bean 验证(JSR 303)API。 |
我们已经在清单 3-11 中看到了
<h:inputText id="address"
required="true"
value="#{person.address}">
<f:validateLength minimum="20" maximum="120"/>
</h:inputText>
如本例所示,在“address”输入文本中使用
<h:inputText id="email"
required="true"
value="#{person.email}">
<f:validateRegex pattern="(.+@.+\.[a-zA-Z]+)?"/>
</h:inputText>
如本例所示,使用“(”验证 person managed bean 的 email 属性是否具有有效的电子邮件。+@.+.[a-zA-Z]+)?”“email”输入文本的
您可以对必填字段验证错误消息使用 EditableValueHolder 的 requiredMessage 属性,或者对 EditableValueHolder 上的常规验证错误消息使用 validatorMessage 属性来覆盖验证消息。例如:
<h:inputText id="someNumber"
value="#{bean.someNumber}"
required="true"
requiredMessage="You have to enter a number"
validatorMessage="Number has to be minimum 10 and maximum 100">
<f:validateLongRange minimum="10" maximum="100"/>
</h:inputText>
如果用户没有在输入文本中输入值,将显示必填字段验证错误消息“您必须输入一个数字”,如果用户输入的数字超出范围(小于 10 或大于 100),将显示验证消息“数字必须最小为 10,最大为 100”。
构建自定义 JSF 验证器
除了 JSF 框架提供的所有提到的内置验证器,JSF 允许开发者创建他们自己的定制验证器。让我们看一个例子来说明这一点。假设我们想要一个 EmailValidator 自定义验证器,它验证用户输入是否符合电子邮件格式。
为了实现我们的 EmailValidator 自定义验证器,我们需要扩展验证器接口并实现 validate 方法。清单 3-12 展示了 EmailValidator 的实现。
清单 3-12。 EmailValidator 类
package com.jsfprohtml5.example.validators;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.FacesValidator;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
@FacesValidator("com.jsfprohtml5.EmailValidator")
public class EmailValidator implements Validator {
private static final String EMAIL_REGEX = "(.+@.+\\.[a-zA-Z]+)?";
private Pattern pattern;
private Matcher matcher;
public EmailValidator() {
pattern = Pattern.compile(EMAIL_REGEX);
}
@Override
public void validate(FacesContext context,
UIComponent component,
Object value)
throws ValidatorException {
matcher = pattern.matcher(value.toString());
if (! matcher.matches()) {
FacesMessage message = new FacesMessage("Invalid Email format",
"Use for example: xyz@company.com");
message.setSeverity(FacesMessage.SEVERITY_ERROR);
throw new ValidatorException(message);
}
}
}
在流程验证阶段(或应用请求值阶段)调用的 validate()方法中,会发生验证,如粗体行所示,如果输入字符串不符合电子邮件格式,则会抛出一个 ValidatorException 和一条 Faces 错误消息。注意@FacesValidator 注释很重要,它用于注册验证器。@FacesValidator 注释有一个主要属性,即 value()属性,它被视为验证器的 ID。对于本例,我们使用 value()属性,并将验证器 ID 声明为“com.jsfprohtml5.EmailValidator”。
不使用@FacesValidator 批注,您可以在 JSF faces-config.xml 文件中声明验证器,如下所示:
<faces-config ...>
...
<validator>
<validator-id>com.jsfprohtml5.EmailValidator</validator-id>
<validator-class>com.jsfprohtml5.example.validators.EmailValidator</validator-class>
</validator>
...
</faces-config>
现在,让我们看看如何在 JSF 应用中使用 EmailValidator。清单 3-13 显示了 TestBean 管理的 Bean 的更新版本(最初显示在清单 3-8 中),它包括一个类型为(String)的 email 属性。
清单 3-13。test Bean 托管 Bean 的更新版本
public class TestBean implements Serializable {
// ...
private String email;
// ...
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
// ...
}
清单 3-14 显示了 EmailValidator 验证器 XHTML 测试页面。
清单 3-14。 EmailValidator XHTML 测试页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Email Validator Test</title>
</h:head>
<h:body>
<h:form>
<h1>Validator Test</h1>
<h:outputText value="Enter Email: "/>
<h:inputText id="email"
value="#{testBean.email}" required="true">
<f:validator validatorId="com.jsfprohtml5.EmailValidator"/>
</h:inputText>
<br/>
<h:commandButton action="#{testBean.proceed}" value="Proceed"/><br/>
<h:messages style="color: red"/>
</h:form>
</h:body>
</html>
如粗体行所示,在测试页面中,有一个使用 EmailValidator 和
图 3-4 显示了当用户输入不符合电子邮件格式要求的无效电子邮件时将显示的验证错误。
图 3-4 。EmailValidator 错误消息
图 3-5 显示了当用户输入符合电子邮件格式要求的有效电子邮件信息时,EmailValidator 的行为。
图 3-5 。EmailValidator 有效表单
JSR 303 与 JSF 比恩验证
Java EE 6 中引入了 JSR 303 Bean 验证,以便在 Java 企业应用中支持构建验证。JSR 303 自带内置的验证器(约束),比如@NotNull、@Min、@Max、@Past、@Future、@Size 等。,它还允许在应用域模型上(或者通常在 POJO 上)创建自定义约束。除了 JSF 内置和自定义验证器,从 JSF 2.0 开始,JSF 和 JSR 303 Java Bean 验证 API 之间有了默认的集成。在接下来的小节中,这种集成将在一个示例订阅者应用中进行说明。
在订阅应用中,用户可以保存姓名、地址和电子邮件进行订阅,如图图 3-6 所示。
图 3-6 。订户应用
用户信息验证如下:
- 所有字段都是必填的。
- 用户名必须至少为 4 个字符,最多为 30 个字符。
- 地址必须至少为 12 个字符,最多为 120 个字符。
- 电子邮件必须有效。
从技术上讲,我们将使用 JSF 必填字段验证器来实现第一个验证需求。所有其他验证都将使用 JSR 303 API 来实现。在验证需求 2 和 3 中,将使用 JSR 303 @Size 注释,而对于需求 4,我们将实现一个定制的 JSR 303 约束。清单 3-15 显示了一个在应用主 XHTML 页面中使用的个人管理 bean。
清单 3-15。 人管豆
package com.jsfprohtml5.subscriber.model;
import com.jsfprohtml5.subscriber.bean.validation.custom.EmailAddress;
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.validation.constraints.Size;
@ManagedBean
@RequestScoped
public class Person implements Serializable {
@Size(min = 4, max = 30)
private String name;
@Size(min = 12, max = 120)
private String address;
@EmailAddress
private String email;
public Person() {
}
public String subscribe() {
return null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
如粗体行所示,为了控制姓名和地址字段的大小,我们使用了@size 内置 Java Bean 验证注释。@size 注释主要有两个属性,min 和 max,用来验证注释字段的长度。@EmailAddress 注释是一个自定义约束,我们用它来验证 Person managed bean 的 email 属性。清单 3-16 显示了@EmailAddress 注释的代码。
清单 3-16。 @EmailAddress 注释
package com.jsfprohtml5.subscriber.bean.validation.custom;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ElementType.FIELD, ElementType.METHOD})
@Constraint(validatedBy = EmailValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EmailAddress {
String message() default "{email.invalid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
如果您熟悉 Java 注释,使用@接口,您可以创建一个注释类型。@Target annotation 表示注释类型适用的 Java 程序元素(在本例中,这些元素是 Java 字段和方法)。Java Bean 验证规范(JSR 303)要求任何约束注释都要定义以下属性:
- 消息属性,默认情况下应该返回错误消息。它可以返回实际的错误信息文本,也可以通过使用如下的花括号“{key}”来返回错误信息密钥。在前面的代码清单中,它返回 email.invalid key。
- groups 属性,允许指定该约束所属的验证组。
- payload 属性,Java Bean Validation API 的客户端可以使用它将定制的有效负载对象分配给一个约束(超出了本书的范围)。
@Constraint 批注是一个 Java Bean 验证批注,它引用使用 validatedBy 属性执行验证逻辑的类。清单 3-17 展示了 EmailValidator 验证类的实现。
清单 3-17。 EmailValidator 验证类
package com.jsfprohtml5.subscriber.bean.validation.custom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EmailValidator implements ConstraintValidator<EmailAddress, String> {
private static final String EMAIL_REGEX = "(.+@.+\\.[a-zA-Z]+)?";
private Pattern pattern;
private Matcher matcher;
@Override
public void initialize(EmailAddress constraintAnnotation) {
pattern = Pattern.compile(EMAIL_REGEX);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
matcher = pattern.matcher(value);
if (! matcher.matches()) {
return false;
}
return true;
}
}
正如你会注意到的,EmailValidator 约束与我们在清单 3-12 中开发的 JSF EmailValidator 类具有相同的逻辑。然而,有一个主要的区别:在 JSF 验证器中,当验证失败时,验证器抛出异常,但是在 Java Bean 验证中,验证器返回 false。initialize()方法用于初始化自定义约束;值得注意的是,这个方法保证在任何其他约束实现方法之前被调用。
现在,在构建了自定义约束之后,我们就可以在我们的 JSF XHTML 页面中使用内置和自定义约束了。清单 3-18 显示了订阅者应用主页(index.xhtml)。
清单 3-18。 订阅者应用 XHTML 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">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">
<ui:composition template="/WEB-INF/templates/default.xhtml">
<ui:define name="title">
#{bundle['application.subscriber.title']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.name']}"></h:outputText>
<h:inputText id="userName"
value="#{person.name}"
required="true"
requiredMessage="#{bundle['user.name.required']}">
</h:inputText>
<h:message for="userName" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.address']}"></h:outputText>
<h:inputText id="address"
value="#{person.address}"
required="true"
requiredMessage="#{bundle['user.address.required']}">
</h:inputText>
<h:message for="address" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.email']}"></h:outputText>
<h:inputText id="email"
value="#{person.email}"
required="true"
requiredMessage="#{bundle['user.email.required']}">
</h:inputText>
<h:message for="email" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.subscribe']}"
action="#{person.subscribe}">
</h:commandButton>
<br/>
<h:messages styleClass="errorMessage"/>
</h:form>
</ui:define>
</ui:composition>
</html>
正如我们在代码清单中注意到的,我们没有做什么特别的事情;我们对所有字段使用了 JSF 必需字段验证器。值得注意的是,所有内置或定制的 JSR 303 验证器(@Size 和@EmailValidator)将自动应用于 JSF 组件,而无需任何额外的步骤。如图 3-7 所示,Java Bean 验证错误会自动附加到 JSF < h:message >和< h:messages >组件上。
图 3-7 。JSF 消息组件中的 Java Bean 验证错误
默认情况下,所有的 Java Bean 验证(JSR 303)验证器将在 JSF 管理的 Bean 中自动启用;为了禁用此行为,可以将 web.xml 中的 javax . faces . VALIDATOR . disable _ DEFAULT _ BEAN _ VALIDATOR 上下文参数设置为 true,如下所示:
<context-param>
<param-name>javax.faces.validator.DISABLE_DEFAULT_BEAN_VALIDATOR</param-name>
<param-value>true</param-value>
</context-param>
值得注意的一点是,Java Bean Validation 是一个独立的验证框架,它不是 JavaServer Faces 框架的一部分(尽管它们之间有很好的集成,正如我们在订阅者应用中看到的);这意味着您需要为 Java Bean 验证消息提供一个单独的属性文件。根据 JSR 303,这个属性文件应该命名为 ValidationMessages.properties,并带有处理不同语言环境的语言环境变量,Java Bean 验证属性文件应该放在 JSF 应用的默认包(类路径的根)下。
注涵盖 JSR 303 的全部特性不在本书讨论范围之内;你可以在 http://jcp.org/en/jsr/detail?id=303 的阅读完整的 JSR 303 规范。
清单 3-15 中的所有验证字段都属于 Java Bean 验证框架的默认验证组;但是,您可以选择为同一个约束条件指定多个验证组。验证组只不过是一个标记接口。让我们创建两个名为(LengthGroup)和(EmailGroup)的验证组。LengthGroup 将在个人管理的 bean 中对长度约束进行分组(@Size constraints),而 EmailGroup 将在个人管理的 bean 中包含电子邮件约束(@EmailAddress constraint)。然后将验证组附加到约束上,如清单 3-19 所示。
清单 3-19。 亲自验证团体托管豆
@ManagedBean
@RequestScoped
public class Person implements Serializable {
@Size(min = 4, max = 30, groups = LengthGroup.class)
private String name;
@Size(min = 12, max = 120, groups = LengthGroup.class)
private String address;
@Size(min = 5, max = 30, groups = LengthGroup.class)
@EmailAddress(groups = EmailGroup.class)
private String email;
public Person() {
}
public String subscribe() {
return null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
默认情况下,JSF 执行分组在默认验证组下的约束。为了在 EditableValueHolder 上运行特定的验证组,JSF 提供了一个
清单 3-20。更新 index.xhtml 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">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">
<ui:composition template="/WEB-INF/templates/default.xhtml">
<ui:define name="title">
#{bundle['application.subscriber.title']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.name']}"></h:outputText>
<h:inputText id="userName"
value="#{person.name}"
required="true"
requiredMessage="#{bundle['user.name.required']}">
<f:validateBean
validationGroups="com.jsfprohtml5.subscriber.bean.validation.groups.LengthGroup"/>
</h:inputText>
<h:message for="userName" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.address']}"></h:outputText>
<h:inputText id="address"
value="#{person.address}"
required="true"
requiredMessage="#{bundle['user.address.required']}">
<f:validateBean
validationGroups="com.jsfprohtml5.subscriber.bean.validation.groups.LengthGroup"/>
</h:inputText>
<h:message for="address" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.email']}"></h:outputText>
<h:inputText id="email"
value="#{person.email}"
required="true"
requiredMessage="#{bundle['user.email.required']}">
<f:validateBean
validationGroups="com.jsfprohtml5.subscriber.bean.validation.groups.LengthGroup,
com.jsfprohtml5.subscriber.bean.validation.groups.EmailGroup"/>
</h:inputText>
<h:message for="email" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.subscribe']}"
action="#{person.subscribe}">
</h:commandButton>
<br/>
<h:messages styleClass="errorMessage"/>
</h:form>
</ui:define>
</ui:composition>
</html>
如粗体行所示,
注订阅者应用的完整 Maven 项目包含在本书网页【apress.com】第三章 资源下。
覆盖标准消息
为了获得更好的用户体验,覆盖标准转换和验证错误消息总是有用的。虽然您可以通过使用 EditableValueHolder 组件的 validatorMessage、requiredMessage 和 converterMessage 属性来自定义转换和验证错误消息,但与在每个 EditableValueHolder 组件上相比,在应用级别上全局覆盖 JSF 标准转换和验证消息需要更少的工作量和更高的准确性。
为了全局覆盖 JSF 标准转换和验证消息,您必须使用标准消息关键字来覆盖应用消息包中的消息。表 3-3 显示了根据 JSF 规范的可能的 JSF 标准消息键。
表 3-3 。根据 JSF 规范的 JSF 标准消息密钥
|
钥匙
|
默认消息
|
| --- | --- |
| javax.faces.component.UIInput.CONVERSION
| {0}:出现转换错误。 |
| javax.faces.component.UIInput.REQUIRED
| {0}:验证错误:值是必需的。 |
| javax.faces.component.UIInput.UPDATE
| {0}:处理您提交的信息时出错。 |
| javax.faces.component.UISelectOne.INVALID
| {0}:验证错误:值无效 |
| javax.faces.component.UISelectMany.INVALID
| {0}:验证错误:值无效 |
| javax.faces.converter.BigDecimalConverter.DECIMAL
| { 2 }:“{ 0 }”必须是有符号的十进制数。 |
| javax.faces.converter.BigDecimalConverter.DECIMAL_detail
| { 2 }:“{ 0 }”必须是由零个或多个数字组成的有符号十进制数,后面可以跟一个小数点和分数。示例:{1} |
| javax.faces.converter.BigIntegerConverter.BIGINTEGER
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。javax . faces . converter . biinteger converter . big integer _ detail = { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。示例:{1} |
| javax.faces.converter.BooleanConverter.BOOLEAN
| { 1 }:“{ 0 }”必须为“true”或“false”。 |
| javax.faces.converter.BooleanConverter.BOOLEAN_detail
| { 1 }:“{ 0 }”必须为“true”或“false”。除“true”之外的任何值都将计算为“false”。 |
| javax.faces.converter.ByteConverter.BYTE
| { 2 }:“{ 0 }”必须是 0 到 255 之间的数字。 |
| javax.faces.converter.ByteConverter.BYTE_detail
| { 2 }:“{ 0 }”必须是 0 到 255 之间的数字。示例:{1} |
| javax.faces.converter.CharacterConverter.CHARACTER
| { 1 }:“{ 0 }”必须是有效字符。 |
| javax.faces.converter.CharacterConverter.CHARACTER_detail
| { 1 }:“{ 0 }”必须是有效的 ASCII 字符。 |
| javax.faces.converter.DateTimeConverter.DATE
| { 2 }:“{ 0 }”无法被理解为日期。 |
| javax.faces.converter.DateTimeConverter.DATE_detail
| { 2 }:“{ 0 }”无法被理解为日期。示例:{1} |
| javax.faces.converter.DateTimeConverter.TIME
| { 2 }:“{ 0 }”无法被理解为时间。 |
| javax.faces.converter.DateTimeConverter.TIME_detail
| { 2 }:“{ 0 }”无法被理解为时间。示例:{1} |
| javax.faces.converter.DateTimeConverter.DATETIME
| { 2 }:“{ 0 }”无法被理解为日期和时间。 |
| javax.faces.converter.DateTimeConverter.DATETIME_detail
| { 2 }:“{ 0 }”无法被理解为日期和时间。示例:{1} |
| javax.faces.converter.DateTimeConverter.PATTERN_TYPE
| {1}:必须指定“pattern”或“type”属性来转换值“{0}”。 |
| javax.faces.converter.DoubleConverter.DOUBLE
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.DoubleConverter.DOUBLE_detail
| { 2 }:“{ 0 }”必须是介于 4.9E-324 和 1.7976931348623157E308 之间的数字,例如:{1} |
| javax.faces.converter.EnumConverter.ENUM
| { 2 }:“{ 0 }”必须可转换为枚举。 |
| javax.faces.converter.EnumConverter.ENUM_detail
| { 2 }:“{ 0 }”必须可以从包含常数“{1}”的枚举转换为枚举。 |
| javax.faces.converter.EnumConverter.ENUM_NO_CLASS
| { 1 }:“{ 0 }”必须可从枚举转换为枚举,但未提供枚举类。 |
| javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail
| { 1 }:“{ 0 }”必须可从枚举转换为枚举,但未提供枚举类。 |
| javax.faces.converter.FloatConverter.FLOAT
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.FloatConverter.FLOAT_detail
| { 2 }:“{ 0 }”必须是介于 1.4E-45 和 3.4028235E38 之间的数字,例如:{1} |
| javax.faces.converter.IntegerConverter.INTEGER
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.IntegerConverter.INTEGER_detail
| { 2 }:“{ 0 }”必须是介于–2147483648 和 2147483647 之间的数字。示例:{1} |
| javax.faces.converter.LongConverter.LONG
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.LongConverter.LONG_detail
| { 2 }:“{ 0 }”必须是介于–9223372036854775808 和 9223372036854775807 之间的数字。示例:{1} |
| javax.faces.converter.NumberConverter.CURRENCY
| { 2 }:“{ 0 }”无法被理解为货币值。 |
| javax.faces.converter.NumberConverter.CURRENCY_detail
| { 2 }:“{ 0 }”无法被理解为货币值。示例:{1} |
| javax.faces.converter.NumberConverter.PERCENT
| { 2 }:“{ 0 }”无法理解为百分比。 |
| javax.faces.converter.NumberConverter.PERCENT_detail
| { 2 }:“{ 0 }”无法理解为百分比。示例:{1} |
| javax.faces.converter.NumberConverter.NUMBER
| { 2 }:“{ 0 }”不是一个数字。 |
| javax.faces.converter.NumberConverter.NUMBER_detail
| { 2 }:“{ 0 }”不是一个数字。示例:{1} |
| javax.faces.converter.NumberConverter.PATTERN
| { 2 }:“{ 0 }”不是数字模式。 |
| javax.faces.converter.NumberConverter.PATTERN_detail
| { 2 }:“{ 0 }”不是数字模式。示例:{1} |
| javax.faces.converter.ShortConverter.SHORT
| { 2 }:“{ 0 }”必须是由一个或多个数字组成的数字。 |
| javax.faces.converter.ShortConverter.SHORT_detail
| { 2 }:“{ 0 }”必须是介于–32768 和 32767 之间的数字例如:{1} |
| javax.faces.converter.STRING
| {1}:无法将“{0}”转换为字符串。 |
| javax.faces.validator.BeanValidator.MESSAGE
| {0} |
| javax.faces.validator.DoubleRangeValidator.MAXIMUM
| {1}:验证错误:值大于允许的最大值“{0}”。 |
| javax.faces.validator.DoubleRangeValidator.MINIMUM
| {1}:验证错误:值小于允许的最小值“{0}”。 |
| javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE
| {2}:验证错误:指定的属性不在{0}和{1}的预期值之间。 |
| javax.faces.validator.DoubleRangeValidator.TYPE
| {0}:验证错误:值的类型不正确。 |
| javax.faces.validator.LengthValidator.MAXIMUM
| {1}:验证错误:长度大于允许的最大值“{0}”。 |
| javax.faces.validator.LengthValidator.MINIMUM
| {1}:验证错误:长度小于允许的最小值“{0}”。 |
| javax.faces.validator.LongRangeValidator.MAXIMUM
| {1}:验证错误:值大于允许的最大值“{0}”。 |
| javax.faces.validator.LongRangeValidator.MINIMUM
| {1}:验证错误:值小于允许的最小值“{0}”。 |
| javax.faces.validator.LongRangeValidator.NOT_IN_RANGE
| {2}:验证错误:指定的属性不在{0}和{1}的预期值之间。 |
| javax.faces.validator.LongRangeValidator.TYPE
| {0}:验证错误:值的类型不正确。 |
让我们看一个例子,看看如何进行这种定制。下面的代码片段显示了一个输入文本,该文本被验证为必需的,并且值在 10 到 100 之间。
<h:inputText id="someNumber"
value="#{testBean.number}"
required="true">
<f:validateLongRange minimum="10" maximum="100"/>
</h:inputText>
<br/>
<h:commandButton action="#{testBean.proceed}" value="Proceed"/><br/>
当用户输入超出范围的数字,然后单击“继续”命令按钮时,将向用户显示以下 JSF 默认验证错误消息:
xxx:someNumber: Validation Error: Specified attribute is not between the expected values of 10 and 100.
为了改变信息,我们需要做以下工作:
-
覆盖应用消息包中的 javax . faces . validator . longrangevalidator . not _ IN _ RANGE 键,如下所示:
javax.faces.validator.LongRangeValidator.NOT_IN_RANGE = {2}''s value must be minimum {0} and maximum {1}.
-
在 faces-config.xml 文件中注册应用消息包,如下所示:
<faces-config ...> ... <application> <message-bundle>com.jsfprohtml5.application.Messages</message-bundle> </application> ... </faces-config>
在应用的消息包和 faces-config.xml 文件中完成这些更改之后,在输入文本中输入一个超出范围的数字,然后单击“proceed”命令按钮,最终的错误消息将是
xxx:someNumber's value must be minimum 10 and maximum 100.
摘要
在这一章中,你详细地学习了 JSF 验证和转换。您知道 JSF 转换器、JSF 转换器接口和 JSF 标准转换器的生命周期,并且了解如何构建定制的 JSF 转换器。您还学习了 JSF 验证、JSF 验证器接口和 JSF 标准验证器的生命周期;如何构建自定义 JSF 验证器;最后,如何在 JSF 应用中使用 Java Bean 验证(JSR 303)。在本章的最后,您学习了如何通过定制 JSF 框架标准转换和验证消息来增强您的 JSF 应用。
四、引擎盖下的 JSF——第三部分
在本章中,您将通过理解 JSF 事件模型,详细了解如何增强您的 JSF 应用。完成本章后,您将了解不同的 JSF 事件类型(JSF 面孔事件、相位事件和系统事件)。您将学习如何在 JSF 应用中处理 JSF 事件。在最后一节中,您将学习如何利用 JSF 视图参数来生成 RESTful JSF 页面,这些页面可以被最终用户添加为书签,也可以被 web 搜索爬虫编入索引。
JSF 事件
在深入 JSF 事件的细节之前,我们需要先了解什么是事件和什么是事件监听器。事件通常是用户执行的操作(如单击按钮或更改下拉值)。当事件发生时,事件源对象中的更改(或一组更改)会发生,并被事件对象捕获。事件对象应该告诉事件的源对象是什么,以及事件源发生了什么变化(如果有的话)。事件侦听器通常是一个在特定事件(事件侦听器类感兴趣的事件)发生时必须得到通知的类。
通常,在 Java 世界中,事件模型中的两个主要组件被表示为一个接口和一个类,如图图 4-1 所示。
图 4-1 。Java 事件模型主接口
EventListener 是一个标记接口,没有所有事件侦听器接口都必须扩展的方法,而 EventObject 类主要有一个方法 getSource(),它返回事件最初发生的对象(事件源)。为了处理 Java 事件模型,JSF 利用 EventObject 和 EventListener 来构建它的事件和侦听器模型。
为了定义 JSF 监听器,所有的 JSF 监听器接口都扩展了 EventListener 接口。图 4-2 显示了主要的 JSF 事件监听器(注意,为了简单起见,这个图没有显示 JSF 监听器的完整列表)。
图 4-2 。JSF 事件听众
有两种类型的 JSF 事件监听器:
- Faces 监听器是动作监听器、值更改监听器、系统事件监听器和组件系统事件监听器的基本接口。
- 阶段监听器是监听器接口,用于 JSF 请求处理生命周期的每个标准阶段的开始和结束。
让我们深入了解子侦听器的细节,以了解谁可以创建这些侦听器以及何时可以执行它们。图 4-3 显示了两种与应用相关的事件监听器:
- ActionListener 接口,负责接收动作事件。
- ValueChangeListener 接口,负责接收值变化事件。
图 4-3 。与应用相关的事件侦听器
如此图所示,ActionSource 组件(或 ActionSource2 组件)可以有一个或多个操作侦听器。EditableValueHolder 组件(如 UIInput 组件)或 ValueHolder 组件可能有一个或多个值更改侦听器。
根据定义,系统事件监听器监听系统事件。系统事件是在 JSF 2.0 中引入的,它为 JSF 生命周期提供了一个优雅的视图。例如,使用系统事件侦听器,JSF 开发人员可以编写自定义代码,这些代码将在应用启动和拆卸事件中执行,或者在应用中引发异常时执行。
SystemEventListener 是系统事件侦听器的主接口。SystemEventListener 可以侦听所有系统事件类型。系统事件可以在 JSF 应用级别触发(如应用启动、应用拆除或应用异常),也可以在 JSF 组件级别触发(如“验证组件之前”、“验证组件之后”或“呈现视图之前”)。如果系统事件发生在组件级,那么在这种情况下你可以使用更具体的事件监听器,这是 ComponentSystemEventListener 接口(如图图 4-2 所示)来处理。这是因为 ComponentSystemEventListener 侦听 ComponentSystemEvent(它扩展了 SystemEvent)。
阶段监听器允许处理不同的阶段事件。阶段事件发生在 JSF 请求处理生命周期的每个标准阶段的开始和结束。JSF 请求处理生命周期阶段有
- 恢复视图。
- 应用请求值。
- 流程验证。
- 更新模型值。
- 调用应用。
- 渲染响应。
如图 4-4 所示,生命周期实例可以有零个或多个附加的 phase listener,UIViewRoot 可以有零到两个 phase listener 实例。阶段侦听器将在“阶段事件”一节中详细说明。
图 4-4 。PhaseListener 接口
注意还有一个监听器扩展了 FacesListener(为了简单起见,在图 4-2 中省略了):这个监听器是 BehaviorListener。BehaviorListener 监听 JSF HTML 组件的所有行为事件。这些事件将在第五章中说明。
既然我们已经介绍了 JSF 事件监听器模型,那么 JSF 事件模型呢?图 4-5 显示了 JSF 事件层级中的主要类别。
图 4-5 。JSF 事件对象层次结构中的主要类
为了定义 JSF 事件对象,所有的 JSF 事件对象都扩展了 EventObject 类。有三件重要的事情需要注意:
- FacesEvent 是可由 UIComponents 触发的事件的基类。它有两个子类:ActionEvent 类,表示由 ActionSource2 组件触发的事件 ValueChangeEvent 类,表示由 ValueHolder 或 EditableValueHolder 组件触发的事件。
- SystemEvent 是所有系统事件的基类。ComponentSystemEvent 类(扩展 SystemEvent)代表特定于 UIComponents 的系统事件。
- PhaseEvent 是一个类,表示在 JSF 请求处理生命周期的每个标准阶段的开始和结束时发生的阶段事件。
注意重要的是要知道所有的 JSF 事件监听器接口和事件类都位于(javax.faces.event)包中。在本章中,为了简单起见,我们将只提及事件监听器接口名或事件类名,而不提及完全限定的接口(或类)名。
面孔事件
Faces 事件 是那些可以被 UIComponents 触发的事件。面孔事件包括两种类型的事件:
- 动作事件。
- 价值变化事件。
动作事件由 ActionSource2 组件触发,如(CommandButton 或 CommandLink 组件)。例如,当单击 UICommand 组件时,将触发 action 事件。值更改事件由 ValueHolder 组件(如 outputText 组件)或 EditableValueHolder 组件(如 inputText 或 selectOneMenu 组件)触发。当组件的值更改时,将触发值更改事件。在讨论这两种类型事件的示例之前,重要的是要知道这些事件在 JSF 生命周期的各个阶段中的触发时间。图 4-6 显示了动作和数值变化事件的执行时间。
图 4-6 。动作和值更改事件的执行时间
如图所示,动作事件和值更改事件在以下三个阶段结束时执行:
- 应用请求值。
- 流程验证。
- 调用应用。
这是两个事件的四个执行场景 :
- 当 EditableValueHolder(或 ValueHolder)组件的 immediate 属性设置为 true 时,将在“应用请求值”阶段结束时执行 ValueChangeEvent。
- 当 ActionSource2 组件的 immediate 属性设置为 true 时,ActionEvent 将在“应用请求值”阶段结束时执行。
- 当 EditableValueHolder(或 ValueHolder)组件的 immediate 属性设置为 false 时,将在“流程验证”阶段结束时执行 ValueChangeEvent。
- 当 ActionSource2 组件的 immediate 属性设置为 false 时,将在“调用应用”阶段结束时执行 ActionEvent。
在接下来的两节中,我们将看到如何为动作和值更改事件创建侦听器的不同示例。
注意根据对执行场景的解释,需要注意的是,默认情况下(当 immediate 属性设置为 false 时),动作事件和值更改事件都会排队,这意味着一旦用户(例如)对 ActionSource2 组件执行动作或对 EditableValueHolder 组件的值进行更改,这些事件将不会被触发。这两个事件都将排队,直到它们在 JSF 请求处理生命周期中的适当时间被触发,如上图所示。
动作事件
在前面的章节中,我们看到了一些带有动作方法的动作事件和动作监听器的例子。我们来回忆一下第二章的第一个应用例子。清单 4-1 展示了在第一个应用示例中带有动作方法的动作监听器的例子。
清单 4-1。 “第一次应用”示例中带有动作方法的动作监听器示例
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<ui:composition template="/WEB-INF/templates/simple.xhtml">
<ui:define name="title">
#{bundle['application.loginpage.title']}
</ui:define>
<ui:define name="content">
...
<h:commandButton value="#{bundle['application.login']}" action="#{user.login}"/>
<br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
正如我们在清单 4-1 中看到的,我们可以使用 UICommand (CommandButton) action 属性创建一个带有 action 方法的 action 监听器。清单 4-2 显示了#{user.login}动作方法代码。
清单 4-2。 #{user.login}动作方法代码
public class User implements Serializable {
...
public String login() {
return "welcome";
}
...
}
正如您将注意到的,action 方法是一种不带参数并返回表示结果的字符串的方法。
注意的动作方法是由 JSF 内置的默认动作监听器处理的。默认的操作侦听器从操作方法中获取返回的结果字符串,然后将其传递给 NavigationHandler 来处理导航(如果有)。
如果您的操作方法中不需要导航,那么您可以使用操作侦听器方法来代替。清单 4-3 显示了一个包含 CommandButton 和 action listener 方法的表单,用于计算输入字段的阶乘数。
清单 4-3。 一个动作监听器方法的例子
<h:form>
<h:outputText value="Enter Number:"/>
<h:inputText value="#{calc.number}">
<f:validateLongRange minimum="0" maximum="25"/>
</h:inputText>
<br/>
<h:commandButton value="Calculate Factorial"
actionListener="#{calc.findFactorial}">
</h:commandButton>
<br/>
<h:outputText value="Result is: #{calc.result}" rendered="#{calc.result ne 0}"/>
<h:messages/>
</h:form>
可以使用 UICommand 组件的 action listener 属性将操作侦听器方法附加到 ui command 组件。如前所述,操作侦听器方法执行操作而不返回 JSF 导航的任何结果,因此操作侦听器方法返回 void 并将 ActionEvent 作为参数。清单 4-4 显示了 Calc 管理的 bean,它包括 findFactorial 动作监听器。
清单 4-4。 Calc 托管 Bean
@ManagedBean
@RequestScoped
public class Calc implements Serializable {
private int number;
private long result;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public long getResult() {
return result;
}
public void setResult(long result) {
this.result = result;
}
public void findFactorial(ActionEvent event) {
result = 1;
for (int i = 1; i <= number; i++) {
result = result * i;
}
}
}
除了将默认操作侦听器与操作方法或操作侦听器方法一起使用之外,您还可以编写自己的自定义操作侦听器。这可以通过创建实现 ActionListener 接口的 action listener 类来实现。清单 4-5 展示了 CalcActionListener,它实现了 ActionListener。如图所示,processAction()获取当前输入数字,然后计算该数字的相应阶乘,最后在 Calc 托管 bean 的 result 属性中设置输出。
清单 4-5。 CalcActionListener 自定义动作监听器
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.faces.event.ActionListener;
public class CalcActionListener implements ActionListener {
@Override
public void processAction(ActionEvent event) throws AbortProcessingException {
FacesContext context = FacesContext.getCurrentInstance();
Calc calc = context.getApplication().evaluateExpressionGet(context,
"#{calc}",
Calc.class);
long result = 1;
for (int i = 1; i <= calc.getNumber(); i++) {
result = result * i;
}
calc.setResult(result);
}
}
为了将定制动作监听器附加到 UICommand 组件,可以在 UICommand 组件中使用
清单 4-6。XHTML 页面中自定义动作监听器的例子
<h:form>
<h:outputText value="Enter Number:"/>
<h:inputText value="#{calc.number}">
<f:validateLongRange minimum="0" maximum="25"/>
</h:inputText>
<br/>
<h:commandButton value="Calculate Factorial">
<f:actionListener type="com.jsfprohtml5.factorial.model.CalcActionListener"/>
</h:commandButton>
<br/>
<h:outputText value="Result is: #{calc.result}" rendered="#{calc.number ne 0}"/>
<h:messages/>
</h:form>
要遵循的最佳实践是使用操作方法来执行业务操作,这可能还包括导航到新页面,并使用操作侦听器方法(或自定义操作侦听器)在执行实际的业务操作之前为操作做一些初始化工作(例如记录操作)。知道动作监听器方法(或自定义动作监听器)总是在动作方法之前执行是很重要的,执行顺序与它们在视图中声明和附加到 ActionSource2 组件的顺序相同。清单 4-7 显示了在清单 4-3 中提到的阶乘计算表单,它结合了“计算阶乘”命令按钮组件上的动作方法和动作监听器。
清单 4-7。 动作方法和动作监听器组合在“计算阶乘”命令组件上
<h:form>
<h:outputText value="Enter Number:"/>
<h:inputText value="#{calc.number}">
<f:validateLongRange minimum="0" maximum="25"/>
</h:inputText>
<br/>
<h:commandButton value="Calculate Factorial"
actionListener="#{calc.logFindFactorial}"
action="#{calc.findFactorial}">
</h:commandButton>
<br/>
<h:outputText value="Result is: #{calc.result}" rendered="#{calc.number ne 0}"/>
<h:messages/>
</h:form>
记录 FindFactorial 操作的# { calc.logFindFactorial }的执行将在# { calc.findFactorial }进行实际阶乘计算之前执行。清单 4-8 显示了最初在清单 4-4 中显示的 Calc 受管 bean 的更新。
清单 4-8。 更新了 Calc 托管 Bean
@ManagedBean
@RequestScoped
public class Calc implements Serializable {
private long number;
private long result;
public long getNumber() {
return number;
}
public void setNumber(long number) {
this.number = number;
}
public long getResult() {
return result;
}
public void setResult(long result) {
this.result = result;
}
public void logFindFactorial(ActionEvent event) {
System.out.println("Getting the factorial for: " + number);
}
public String findFactorial() {
result = 1;
for (int i = 1; i <= number; i++) {
result = result * i;
}
System.out.println("Factorial(" + number + ") = " + result);
return null;
}
}
在“数字”字段中输入一个数字,然后单击“计算阶乘”命令按钮,将在控制台中打印以下行,如下所示:
Getting the factorial for: 3
Factorial(3) = 6
有时,您可能需要在执行操作方法之前直接在托管 bean 属性中设置一个值;如果您遇到这种情况,那么您可以在 ActionSource2 组件中使用
表 4-1 。<f:setPropertyActionListener>标签的主要属性
|
属性
|
描述
|
| --- | --- |
| 价值* | 表示要存储为target
属性的值的ValueExpression
。 |
| 目标* | 表示作为value
属性的目的地的ValueExpression
。 |
为了理解如何使用
清单 4-9。??<f:setPropertyActionListener>的一个例子
<h:commandButton value="Say Hi" action="page2">
<f:setPropertyActionListener target="#{person.name}" value="Some user"/>
</h:commandButton>
如前面的代码清单所示,当单击 CommandButton 时,Person managed bean 的 name 属性将被设置为“Some user ”,然后当前页面将被转到 page2。清单 4-10 显示了个人管理的 bean。
清单 4-10。 人管豆
@ManagedBean
@SessionScoped
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Person managed bean 是一个简单的 bean,只有一个属性名及其 setter 和 getter。在使用
Hello, #{person.name}
这将产生
Hello, Some user
数值变化事件
值更改事件是当 ValueHolder(或 EditableValueHolder)组件的值更改时触发的事件。让我们看一个值改变监听器的例子。假设我们想要显示一组国家的首都,当用户选择这些国家中的一个,然后单击一个命令按钮来获取所选国家的首都时,这些国家的首都显示在 JSF 的 SelectOneMenu 组件中。清单 4-11 显示了包含国家列表的表单。
清单 4-11。 价值改变听者的例子
<h:form>
<h:outputLabel for="countries" value="Select a country: "/>
<h:selectOneMenu id="countries" value="#{country.name}"
valueChangeListener="#{country.findCapital}">
<f:selectItem itemLabel="---" itemValue="---"/>
<f:selectItem itemLabel="United States" itemValue="USA"/>
<f:selectItem itemLabel="Egypt" itemValue="Egypt"/>
<f:selectItem itemLabel="Denmark" itemValue="Denmark"/>
</h:selectOneMenu>
<h:commandButton value="Find Capital" /> <br/>
<h:outputText value="Capital of #{country.name} is #{country.capital}"
rendered="#{country.capital ne null}"/>
</h:form>
正如我们在粗体行中看到的,我们有一个 selectOneMenu 组件,它有四个使用
清单 4-12。 国家托管豆
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.event.ValueChangeEvent;
@ManagedBean
@RequestScoped
public class Country {
private String name;
private String capital;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCapital() {
return capital;
}
public void setCapital(String capital) {
this.capital = capital;
}
public void findCapital(ValueChangeEvent event) {
System.out.println("Old selected value is: " + event.getOldValue());
System.out.println("New selected value is: " + event.getNewValue());
String selectedCountryName = (String) event.getNewValue();
if ("USA".equals(selectedCountryName)) {
capital = "Washington";
} else if ("Egypt".equals(selectedCountryName)) {
capital = "Cairo";
} else if ("Denmark".equals(selectedCountryName)) {
capital = "Copenhagen";
}
}
}
正如您在粗体行中注意到的,值更改侦听器方法返回 void,并将 ValueChangeEvent 作为参数。使用 ValueChangeEvent 的 getOldValue()和 getNewValue()方法,可以获得 ValueHolder(或 EditableValueHolder)组件的旧值和新值。在我们的示例中,我们获取代表新国家选择的新值,然后获取所选国家的合适的资本,最后在资本属性中设置结果以便由页面显示,如清单 4-11 所示。
而不是通过更改 ValueHolder(或 EditableValueHolder)组件的值并单击 CommandButton 或 CommandLink 来触发值更改事件。当 ValueHolder(或 EditableValueHolder)组件的值更改时,可以通过在组件的值更改时提交表单来触发值更改事件。清单 4-13 显示了如何通过移除 CommandButton 并提交值更改表单来应用此行为。
清单 4-13。 通过提交 SelectOneMenu 的值更改表单来执行值更改监听器
<h:form>
<h:outputLabel for="countries" value="Select a country: "/>
<h:selectOneMenu id="countries" value="#{country.name}"
valueChangeListener="#{country.findCapital}"
onchange="submit();">
<f:selectItem itemLabel="---" itemValue="---"/>
<f:selectItem itemLabel="United States" itemValue="USA"/>
<f:selectItem itemLabel="Egypt" itemValue="Egypt"/>
<f:selectItem itemLabel="Denmark" itemValue="Denmark"/>
</h:selectOneMenu> <br/>
<h:outputText value="Capital of #{country.name} is #{country.capital}"
rendered="#{country.capital ne null}"/>
</h:form>
除了使用默认值更改侦听器之外,您还可以编写自己的自定义值更改侦听器。这可以通过创建实现 value change listener 接口的自定义值更改侦听器类来实现。清单 4-14 显示了 CountryValueChangeListener,它利用 ValueChangeListener 并实现 processValueChange(),后者获取新选择的国家,然后查找其首都,最后将结果设置在国家管理的 bean 的 capital 属性中。
清单 4-14。CountryValueChangeListener 自定义监听器
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ValueChangeEvent;
import javax.faces.event.ValueChangeListener;
public class CountryValueChangeListener implements ValueChangeListener {
@Override
public void processValueChange(ValueChangeEvent event) throws AbortProcessingException {
FacesContext context = FacesContext.getCurrentInstance();
Country country = context.getApplication().evaluateExpressionGet(context,
"#{country}",
Country.class);
String selectedCountryName = (String) event.getNewValue();
if ("USA".equals(selectedCountryName)) {
country.setCapital("Washington");
} else if ("Egypt".equals(selectedCountryName)) {
country.setCapital("Cairo");
} else if ("Denmark".equals(selectedCountryName)) {
country.setCapital("Copenhagen");
}
}
}
为了将自定义值更改监听器附加到 ValueHolder(或 EditableValueHolder)组件,可以在组件内部使用
清单 4-15。XHTML 页面中自定义值改变监听器的例子
<h:form>
<h:outputLabel for="countries" value="Select a country: "/>
<h:selectOneMenu id="countries" value="#{country.name}"
onchange="submit();">
<f:selectItem itemLabel="---" itemValue="---"/>
<f:selectItem itemLabel="United States" itemValue="USA"/>
<f:selectItem itemLabel="Egypt" itemValue="Egypt"/>
<f:selectItem itemLabel="Denmark" itemValue="Denmark"/>
<f:valueChangeListener type="com.jsfprohtml5.factorial.model.CountryValueChangeListener"/>
</h:selectOneMenu> <br/>
<h:outputText value="Capital of #{country.name} is #{country.capital}"
rendered="#{country.capital ne null}"/>
</h:form>
如前面加粗的行所示,使用
阶段事件
阶段事件发生在 JSF 请求处理生命周期的每个标准阶段的开始和结束,如图图 4-7 所示。
图 4-7 。阶段事件执行时间
阶段事件由阶段监听器处理。如图 4-4 所示,生命周期实例可以有零个或多个附加的阶段监听器,UIViewRoot 可以有零到两个阶段监听器实例。为了创建一个相位监听器,你需要实现 JSF 的相位监听器接口。清单 4-16 显示了 JSF PhaseListener 接口 ?? 的代码。
清单 4-16。 相位监听器接口
package javax.faces.event;
import java.io.Serializable;
import java.util.EventListener;
public interface PhaseListener extends EventListener, Serializable {
public void afterPhase(PhaseEvent event);
public void beforePhase(PhaseEvent event);
public PhaseId getPhaseId();
}
如清单 4-16 所示,PhaseListener 接口有以下方法:
- getPhaseId():该方法返回请求处理阶段的标识符,在此期间,该侦听器对处理 PhaseEvent 事件感兴趣。合法值是由 PhaseId 类定义的单例实例,包括 PhaseId。ANY_PHASE 表示对所有标准阶段 的通知感兴趣。
- beforePhase():当请求处理生命周期的特定阶段的处理即将开始时,将执行该方法。
- afterPhase():这个方法将在特定阶段的处理刚刚完成时执行。
阶段侦听器对于调试不同 JSF 生命周期阶段的执行非常有用。它还可以用于授权 JSF 应用页面。让我们看一个例子,看看如何使用阶段侦听器来授权 JSF 页面。让我们回到第二章的第一个应用例子。该应用缺少的功能之一是用户可以直接打开欢迎应用,而不必通过登录页面。为了保护应用中的欢迎页面(或者其他页面),我们可以为此创建一个阶段侦听器。我们需要在应用中修改的一件事是设置一个会话标志,表明用户通过在登录页面中输入非空的用户名和密码进行了身份验证。清单 4-17 显示了更新后的用户管理 bean。
清单 4-17。 更新用户管理的 Bean
public class User implements Serializable {
private String name;
private String password;
private Profession profession;
private List<String> favoriteSports;
private Map<String, String> spokenLanguages;
...
public String login() {
FacesContext context = FacesContext.getCurrentInstance();
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
// User passes through the login page and clicks the "login" button.
session.setAttribute("isAuthenticated", true);
return "welcome";
}
...
}
如前面的代码清单所示,添加了一个会话属性“isAuthenticated ”,用于在 login()方法中将用户标记为已通过身份验证(本例中接受任何输入的非空用户名和密码)。清单 4-18 显示了授权监听器阶段监听器代码。
清单 4-18。 授权监听器相位监听器
package com.jsfprohtml5.firstapplication.model;
import javax.faces.application.NavigationHandler;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpSession;
public class AuthorizationListener implements PhaseListener {
@Override
public void afterPhase(PhaseEvent event) {
FacesContext context = event.getFacesContext();
String currentPage = context.getViewRoot().getViewId();
boolean isLoginPage = currentPage.endsWith("index.xhtml");
HttpSession session = (HttpSession) context.getExternalContext().getSession(true);
Object isAuthenticated = session.getAttribute("isAuthenticated");
if (!isLoginPage && isAuthenticated == null) {
NavigationHandler navigationHandler = context.getApplication().getNavigationHandler();
navigationHandler.handleNavigation(context, null, "index");
}
}
@Override
public void beforePhase(PhaseEvent event) {
//Nothing ...
}
@Override
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
}
为了实现页面授权,我们需要在 RESTORE_VIEW 阶段完成后,在 JSF 生命周期上创建一个阶段侦听器。在 afterPhase() API 中,使用 context.getViewRoot()检索当前页面。getViewId()。当页面不是登录页面(index.xhtml)并且用户未通过身份验证时,则使用 NavigationHandler 将用户转发到登录页面。为了在 JSF 生命周期上安装阶段监听器,您需要在 faces 配置文件、中定义它,如清单 4-19 所示。
清单 4-19。 在 Faces 配置文件中定义阶段监听器
<faces-config ...>
...
<lifecycle>
<phase-listener>
com.jsfprohtml5.firstapplication.model.AuthorizationListener
</phase-listener>
</lifecycle>
</faces-config>
提示如果你想在一个特定的视图上应用一个相位监听器,而不是在所有的页面上应用,你可以通过使用如下的< f:phaseListener >标签来实现:<f:phase listener type = " package。customphasetlistener ">(其中 type 属性表示要创建和注册的完全限定的阶段监听器 Java 类名)。标记在嵌套了该标记的 UIViewRoot 上注册了一个 phaseListener 实例。
系统事件
系统事件 在 JSF 2.0 中引入,以允许 JSF 开发者监听高级生命周期事件并对其做出反应。如本章前面所述,系统事件可以发生在 JSF 应用级别(如应用启动或应用拆除)或 JSF 组件级别。与 Faces 事件不同,系统事件会立即发布,这意味着它们不会排队等待生命周期的后续处理阶段。表 4-2 显示了应用级可能发生的不同类型的系统事件(直接扩展 SystemEvent)。
表 4-2 。应用级别的 JSF 系统事件(扩展 SystemEvent)
|
系统事件
|
描述
|
| --- | --- |
| PostConstructApplicationEvent
| 应用启动完成后立即发布。 |
| PreDestroyApplicationEvent
| 在应用关闭之前立即发布。 |
| ExceptionQueuedEvent
| 当 JSF 应用中出现意外异常时发布。这可能发生在 JSF 生命周期处理的任何阶段。 |
系统事件也可以发生在组件级 。表 4-3 显示了组件级最常见的系统事件类型。以下所有事件都从 ComponentSystemEvent 扩展而来。
表 4-3 。组件级的 JSF 系统事件(扩展组件系统事件)
|
系统事件
|
描述
|
| --- | --- |
| PreRenderComponentEvent
| 该事件在组件呈现之前发布。 |
| PostAddToViewEvent
| 这个事件是在组件被添加到 JSF 视图之后发布的。 |
| PreValidateEvent
| 该事件在组件即将被验证之前发布。 |
| PostValidateEvent
| 该事件在组件通过验证后立即发布。 |
| PreDestroyViewMapEvent
| 此事件在视图范围映射即将被销毁之前发布。 |
| PostConstructViewMapEvent
| 此事件在视图范围映射创建后立即发布。 |
| PreRenderViewEvent
| 该事件在视图(UIViewRoot
)即将呈现之前发布。 |
| PostRestoreStateEvent
| 该事件在组件状态恢复后立即发布。 |
为了了解我们如何使用系统事件来支持我们的 JSF 应用,让我们回到我们在第三章中创建的订阅者应用。假设我们想要引入一个新的下拉项目,它将包括职业列表,如图图 4-8 所示。职业列表在应用中是静态的,所以会在应用启动时加载一次,在应用关闭 ?? 之前卸载。
图 4-8 。更新的订户应用屏幕
为了实现这个功能,我们可以使用 PostConstructApplicationEvent 在应用启动后加载静态列表数据,使用 PreDestroyApplicationEvent 在应用关闭前进行清理。
清单 4-20 显示了我们的应用的定制系统事件监听器,它将在应用启动后和关闭前被调用。
清单 4-20。 订阅者自定义系统事件监听器
package com.jsfprohtml5.subscriber.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.faces.application.Application;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PostConstructApplicationEvent;
import javax.faces.event.PreDestroyApplicationEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
import javax.faces.model.SelectItem;
public class ListingLoader implements SystemEventListener {
public static final String PROFESSIONS_KEY = "professions";
@Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
Map<String, Object> applicationMap = FacesContext.getCurrentInstance().
getExternalContext().getApplicationMap();
if (event instanceof PostConstructApplicationEvent) {
//Load the listing data in the startup ...
applicationMap.put(PROFESSIONS_KEY, getSampleProfessionList());
} else if (event instanceof PreDestroyApplicationEvent) {
//Unload the listing data in the shutdown ...
applicationMap.remove(PROFESSIONS_KEY);
}
}
@Override
public boolean isListenerForSource(Object source) {
return source instanceof Application;
}
private List<SelectItem> getSampleProfessionList() {
List<SelectItem> sampleProfessions = new ArrayList<SelectItem>();
sampleProfessions.add(new SelectItem("Profession1"));
sampleProfessions.add(new SelectItem("Profession2"));
sampleProfessions.add(new SelectItem("Profession3"));
sampleProfessions.add(new SelectItem("Other"));
return sampleProfessions;
}
}
为了实现 SystemEventListener 接口,我们需要为两个方法提供实现:
- isListenerForSource(对象源):如果事件侦听器对从源对象接收事件感兴趣,则该方法应该返回 true。对于我们的侦听器来说,事件侦听器只对接收来自应用对象的事件感兴趣。
processEvent(SystemEvent 事件):一旦接收到 SystemEvent 并准备好进行处理,就会调用该方法。在我们的侦听器中,事件对象被检查为 PostConstructApplicationEvent 或 PreDestroyApplicationEvent。如果是 PostConstructApplicationEvent,则在应用映射中键为 PROFESSIONS_KEY 的映射条目中设置专业列表。在 PreDestroyApplicationEvent 中,可以执行任何可能的清理。在我们的侦听器中,职业列表只是从应用地图中移除。
为了允许我们的应用的定制系统事件监听器在 PostConstructApplicationEvent 或 PreDestroyApplicationEvent 中执行,我们需要在 faces 配置文件(faces-config.xml)中注册我们的定制系统事件监听器,如清单 4-21 所示。
清单 4-21。 在 faces-config.xml 中注册我们的定制系统事件监听器
<faces-config ...>
<application>
<system-event-listener>
<system-event-class>javax.faces.event.PostConstructApplicationEvent</system-event-class>
<system-event-listener-class>com.jsfprohtml5.subscriber.model.ListingLoader</system-event-listener-class>
</system-event-listener>
<system-event-listener>
<system-event-class>javax.faces.event.PreDestroyApplicationEvent</system-event-class>
<system-event-listener-class>com.jsfprohtml5.subscriber.model.ListingLoader</system-event-listener-class>
</system-event-listener>
...
</application>
</faces-config>
使用
在我们的示例中,我们声明了两个
注意<系统-事件-监听器>下还有一个可选元素,就是<源类>元素。< source-class >元素可用于指定事件源的全限定类名。
既然我们已经看到了可以在应用级别发生的系统事件的示例,那么让我们看看如何利用可以在组件级别发生的系统事件。假设我们想要定制订户申请表单的输入字段上的错误显示方式,以便当我们在验证中有一个错误(或一组错误)时,输入字段将被突出显示。这实际上是组件系统事件的一个完美用例。为了实现这个用例,我们需要利用 postValidate 事件。
JSF 引入了标签,我们可以直接将它作为任何 JSF HTML 组件的子组件,以便在其上安装 ComponentSystemEventListener 实例。表 4-4 显示了< f:event >标签的属性。注意< f:event >标签提到的属性是强制的 。
表 4-4 。< f:事件>标签属性
|
属性
|
描述
|
| --- | --- |
| 类型* | 计算结果为字符串的值表达式,该字符串表示要为其安装侦听器的事件的名称。有效值为preRenderComponent
、preRenderView
、postAddToView
、preValidate
和postValidate
。除了提到的有效值,在用@NamedEvent
注释对扩展类进行注释后,扩展ComponentSystemEvent
的任何 java 类的全限定类名都可以用作“type”属性的值。 |
| 听众* | 方法表达式,必须计算为采用ComponentSystemEvent
作为参数、返回类型为void
的公共方法,或者不采用参数、返回类型为void
的公共方法。 |
为了改变输入组件的风格,我们可以在其中放置一个 和一个< f:event >标签。清单 4-22 显示了如何监听用户名输入字段中的 postValidate 事件。
清单 4-22。 监听用户名输入字段中的 postValidate 事件
<h:inputText id="userName"
value="#{person.name}"
required="true"
requiredMessage="#{bundle['user.name.required']}">
<f:event type="postValidate" listener="#{person.checkName}"/>
<f:validateBean validationGroups="com.jsfprohtml5.subscriber.bean.validation.groups.LengthGroup"/>
</h:inputText>
JSF 方法表达式#{person.checkName}检查用户名输入是否有效。如果用户名无效,则在输入字段中添加一个特定的样式类。清单 4-23 显示了 checkName 方法的代码。
清单 4-23。 检查名称方法代码
@ManagedBean
@RequestScoped
public class Person implements Serializable {
...
public void checkName(ComponentSystemEvent componentSystemEvent) {
UIComponent component = componentSystemEvent.getComponent();
if (component instanceof EditableValueHolder) {
EditableValueHolder editableValueHolder = (EditableValueHolder) component;
if (! editableValueHolder.isValid()) {
component.getAttributes().put("styleClass", "invalidInput");
} else {
component.getAttributes().put("styleClass", "");
}
}
}
}
正如我们在清单中看到的,listener 方法将 ComponentSystemEvent 作为参数。使用 getComponent(),我们可以检索组件实例,然后使用 EditableValueHolder 的 is valid()方法检查组件是否有效(通过验证阶段)。如果组件无效,则将 invalidInput CSS 样式类添加到组件的 style class 属性中。invalidInput CSS 样式类很简单,如清单 4-24 中的 所示。
清单 4-24。 invalidInput 风格类
.invalidInput {
background-color: red;
color: white;
}
图 4-9 显示了用户名输入字段没有通过验证时的样子。
图 4-9 。出现错误时设置用户名输入字段的样式
在每个输入组件上安装组件系统事件监听器,在出现错误的情况下使用
为了统一控制所有输入字段中的错误显示,我们可以在 PostValidateEvent 上创建一个定制的系统事件侦听器,并通过将 input text class 设置为事件源,将其应用于应用中的所有输入文本元素。清单 4-25 显示了 ErrorDisplayListener 类(我们定制的系统事件监听器类)。
清单 4-25。ErrorDisplayListener 类
package com.jsfprohtml5.subscriber.model;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
public class ErrorDisplayListener implements SystemEventListener {
@Override
public void processEvent(SystemEvent event) throws AbortProcessingException {
UIComponent component = (UIComponent) event.getSource();
if (component instanceof EditableValueHolder) {
EditableValueHolder editableValueHolder = (EditableValueHolder) component;
if (! editableValueHolder.isValid()) {
component.getAttributes().put("styleClass", "invalidInput");
} else {
component.getAttributes().put("styleClass", "");
}
}
}
@Override
public boolean isListenerForSource(Object source) {
return source instanceof UIComponent;
}
}
如前面的清单所示,在 processEvent()中,任何具有验证错误的 EditableValueHolder 组件都将附加 invalidInput 样式。最后,为了将系统事件监听器应用到输入字段,我们需要在 faces 配置(faces-config.xml)中注册系统事件监听器,如清单 4-26 所示。
清单 4-26。 在 Faces 配置文件中注册 ErrorDisplayListener
<faces-config ...>
<application>
...
<system-event-listener>
<source-class>javax.faces.component.html.HtmlInputText</source-class>
<system-event-class>javax.faces.event.PostValidateEvent</system-event-class>
<system-event-listener-class>com.jsfprohtml5.subscriber.model.ErrorDisplayListener
</system-event-listener-class>
</system-event-listener>
</application>
</faces-config>
使用
图 4-10 。当我们有一个或多个验证错误时,样式化的输入元素
注更新后的应用(首次应用和订阅应用)可从该书网站
www.apress.com/9781430250104
下载;你可以在第四章的压缩文件中找到完整的源代码。
查看参数
为了支持可加书签的页面,JSF 2.0 中引入了视图参数来支持可寻址页面。视图参数允许 JSF 页面是 RESTful 的,这意味着它们可以被最终用户在浏览器中加入书签,以便他们以后可以随时返回使用这些页面。可以使用< f:viewParam >标签在 JSF Facelets 页面中创建视图参数。清单 4-27 展示了如何在 JSF 页面(car.xhtml)中定义< f:viewParam >标签。
清单 4-27。 在 JSF 页面内使用< f:viewParam >标签(car.xhtml)
<html FontName">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">
<f:metadata>
<f:viewParam name="model" value="#{car.model}"/>
<f:viewParam name="color" value="#{car.color}"/>
</f:metadata>
<h:head>
<title>Car Information</title>
</h:head>
<h:body>
<p>
Car Model: #{car.model} <br/>
Car Color: #{car.color}
</p>
</h:body>
</html>
在前面的代码中,我们在页面中定义了两个视图参数。每个视图参数都有两个主要属性,name 属性指定请求参数的名称,value 属性表示请求参数的值将绑定到的值表达式。这意味着名称为 model 的请求参数的值将绑定到#{car.model}表达式,名称为 color 的请求参数将绑定到#{car.color}表达式。清单 4-28 显示汽车管理 bean 。
清单 4-28。 车飞龙豆
@ManagedBean
@RequestScoped
public class Car {
private String model;
private String color;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
使用以下参数从浏览器调用 car.xhtml 页面后:
/car.xhtml?model=300D&color=black
这将在 car.xhtml 页面中产生以下内容:
Car Model: 300D
Car Color: black
注意重要的是要知道< f:viewParam >导致一个 uiviewparam 作为当前视图的元数据被附加。UIViewParameter 扩展了 UIInput,这意味着通常对 UIInput 实例采取的任何操作对该类的实例都有效。因此,您可以将转换器、验证器和值更改监听器附加到< f:viewParam >标签上。
如前一篇技巧文章所示,我们可以将转换器和验证器都添加到
清单 4-29。 修改汽车管理豆
@ManagedBean
@RequestScoped
public class Car {
//...
private Long number;
//...
public Long getNumber() {
return number;
}
public void setNumber(Long number) {
this.number = number;
}
}
为了强制要求所有的汽车属性都是强制性的,就像我们处理其他 EditableValueHolder 组件一样,我们可以将 required 属性设置为 true。为了验证汽车号码属性在特定的数字范围内是一个有效的数字,我们可以在
清单 4-30。 修改 car.xhtml 页面以利用验证
<html FontName">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">
<f:metadata>
<f:viewParam name="model" value="#{car.model}"
required="true"
requiredMessage="You need to specify car model"/>
<f:viewParam name="color" value="#{car.color}"
required="true"
requiredMessage="You need to specify car color"/>
<f:viewParam name="number" value="#{car.number}"
required="true"
requiredMessage="You need to specify car number">
<f:validateLongRange minimum="1" maximum="9999999999"/>
</f:viewParam>
</f:metadata>
<h:head>
<title>Car Information</title>
</h:head>
<h:body>
<p>
<h:outputText value="Car Model: #{car.model}" rendered="#{car.model ne null}" /> <br/>
<h:outputText value="Car Color: #{car.color}" rendered="#{car.color ne null}"/> <br/>
<h:outputText value="Car Number: #{car.number}" rendered="#{car.number ne null}"/>
</p>
<h:messages styleClass="errorMessage"/>
</h:body>
</html>
验证错误将显示在
为了支持浏览器书签功能和搜索引擎网络爬虫,JSF 2.0(及更高版本)提供了组件。如果您将组件的 includeViewParams 属性设置为 true,这将生成页面视图参数,作为< h:link >组件生成的 URL 的一部分。例如,如果我们将清单 4-30 中的< h:link >组件添加到 car.xhtml 页面,如下所示:
<h:link includeViewParams="true" value="Can be bookmarked"/>
这将按照以下模式生成一个 URL:
<a href="/contextPath/car.xhtml?model=xxx&color=yyy&number=zzz">Can be bookmarked</a>
注意像< h:link >组件、< h:button >组件都有 includeViewParams 属性;但是,< h:button >为了查看目标页面,会生成一个依赖于 JavaScript onclick 动作的 HTML 按钮,这意味着 web 搜索爬虫无法到达。
除了和
清单 4-31。【JSF】用于输入汽车信息的输入表单(intro.xhtml)
<h:form>
<h:panelGrid columns="3">
<h:outputText value="Model:"></h:outputText>
<h:inputText id="model"
value="#{car.model}"
required="true"
requiredMessage="You need to specify car model">
</h:inputText>
<h:message for="model" styleClass="errorMessage"/>
<h:outputText value="Color:"></h:outputText>
<h:inputText id="color"
value="#{car.color}"
required="true"
requiredMessage="You need to specify car color">
</h:inputText>
<h:message for="color" styleClass="errorMessage"/>
<h:outputText value="Car Number:"></h:outputText>
<h:inputText id="number"
value="#{car.number}"
required="true"
requiredMessage="You need to specify car number">
<f:validateLongRange minimum="1" maximum="9999999999"/>
</h:inputText>
<h:message for="number" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="View car details"
action="car?faces-redirect=true&includeViewParams=true" />
</h:form>
如粗体行所示,为了允许 JSF 命令按钮以 RESTful 方式导航到我们的 RESTful 页面(car.xhtml)(目标页面名称和参数将出现在浏览器地址栏中),我们需要向 UICommand 的 action outcome 添加以下两个参数:
- faces-redirect:将 faces-redirect 参数设置为 true 允许当前页面被重定向(而不是转发)到目标页面(我们从第二章中知道)。
- includeViewParams:在操作结果内将 includeViewParams 设置为 true 允许 ActionSource2 组件在执行导航时包含视图参数。请记住,包含的视图参数必须在目标 JSF 页面中声明,在我们的示例中是(car.xhtml)。
假设在 intro.xhtml 页面中输入了有效数据,然后点击“查看汽车详情”按钮,页面将被重定向到(car.xhtml)页面,浏览器地址栏中有以下参数:
contextPath/car.xhtml?model=xxx&color=yyy&number=zzz
这将在 car.xhtml 页面中产生以下内容:
Car Model: xxx
Car Color: yyy
Car Number: zzz
注意知道 JSF 2.x Facelets 是一种基于 XML 的视图技术是很重要的。这意味着(&)字符被解释为 XML 实体的开始。这导致为了表示实际的(&)字符,您必须使用& amp,如最后一个代码示例所示。
摘要
在本章中,您详细学习了 JSF 事件模型。您了解了如何在 JSF 应用中处理行动和价值变更事件。现在,您知道了如何利用 JSF 阶段监听器在您的 JSF 应用中实现一些好东西,比如授权或日志记录。您已经学习了不同类型的 JSF 系统事件,现在知道如何使用应用级和组件级系统事件。最后,您了解了如何使用 JSF 视图参数来使您的 JSF 页面 RESTful。
五、JSF 2.2:有什么新内容?
JSF 2.2 的工作始于 2011 年初,就在 JSF 2.0 第二次维护发布后的几个月。经过两年的规范和参考实现(Mojarra 2.2)工作,JSF 2.2 于 2013 年 5 月下旬发布。JSF 2.2 也是 2013 年 6 月发布的 Java EE 7 的一部分。JSF 2.2 提供了一些令人兴奋的新特性(也称为大牌特性),许多重大改进,以及相当多的规范说明和错误修复。JSF 2.2 向后兼容早期的 2.0 版本。这是一个好消息,因为您不必重写 JSF 2.0 应用来支持 2.2 的新特性。有几个例外情况,您必须对您的应用进行一些小的更改。
在本章中,我们将介绍和演示 JSF 2.2 的主要特性,并触及一些最重要的变化。最后,我们将看看为了与 JSF 2.2 完全兼容,您需要在 JSF 2.0 应用中实现哪些更改。
关于 JSF 2.2 规范的所有细节,也称为 JSR 344,你可以访问位于 http://jcp.org/en/jsr/detail?id=344 的 Java 社区进程网站。
大额特征
JSF 2.2 是一个小升级,向后兼容 JSF 2.0。它建立在已经存在的 JSF 功能之上。然而,JSF 2.2 包含了四个新的主要特性。这些是
- HTML5 友好标记:该特性通过允许将任意属性和元素从 Facelet 视图中的 JSF 组件传递到呈现的 HTML 输出,从而为 JSF 添加了 HTML5 支持。
- 资源库契约:这个特性建立在 Facelets 提供的模板化特性之上。使用资源库契约,可以随应用一起提供模板集,或者在应用的类路径中包含的单独 jar 文件中提供模板集。
- Faces Flow:这个特性类似于资源库契约,允许定义和打包用户流以供重用。引入了一个新的 bean 范围来管理流中涉及的 bean 的生命周期。
- 无状态视图:这个特性允许将视图标记为无状态的,因此是暂时的。
JSF 2.2 中还有许多其他较小的变化,我们已经在主要特性之后的章节中介绍了最重要的变化。
HTML5 友好标记
HTML5 肯定是目前最热门的话题之一。这一趋势没有被 JSF 参考小组忽略,他们优先考虑在 JSF 2.2 中实现支持 HTML5 友好标记的特性。
说实话,实现的 HTML5 支持特性也可以用在与 HTML5 不同的上下文中。多年来,web 设计人员一直使用 JavaScript 框架在自定义属性中存储特定于应用的数据。例如,一个网页设计者可能已经选择了包含关于一个图像的附加信息,当这个图像被点击时,这些信息被提取并显示出来(见清单 5-1 )。
清单 5-1。 包含 JavaScript 框架可以提取的数据的自定义属性
<img src="product/1234.png"
title="Click to see more information"
data-popup-title="The product is available for shipping within 24 hours"/>
在 HTML5 之前,自定义属性是非标准的,并且由于浏览器对它们的解释不同,输出通常是不可预测的。HTML5 标准引入了一组新的全局属性和元素,确保浏览器在解释自定义属性时的一致性。大多数新的属性和元素没有 JSF 等价项,因为它们携带的值在服务器端是不相关的。JSF 2.2 通过提供将属性和元素从 Facelet 视图传递到 HTML 代码的能力,引入了对 HTML5 标记的支持。
注意只有用 Facelets 编写的视图才支持属性和元素的传递,用 JSP 编写的视图不支持。
在 JSF 2.2 之前,创建定制属性和元素的唯一方法是将输出包装在一个复合组件中,在该组件中,定制属性和元素以普通的 HTML 格式引入。清单 5-2 和 5-3 显示了包装定制 HTML 属性的复合组件。这种变通办法的问题是它引入了过多的代码,并且输出的代码没有作为一个组件存储在 JSF 组件树中。
清单 5-2。 复合组件包装定制属性(resources/jsf22/img2.xhtml)
<?xml version='1.0' encoding='UTF-8' ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//
EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite">
<cc:interface>
<cc:attribute name="src" type="java.lang.String" required="true" />
<cc:attribute name="title" type="java.lang.String" default="" />
<cc:attribute name="popupTitle" type="java.lang.String" default="" />
</cc:interface>
<cc:implementation>
<img src="#{cc.attrs.src}"
title="#{cc.attrs.title}"s
data-popup-title="#{cc.attrs.popupTitle}" />
</cc:implementation>
</html>
清单 5-3。 Facelets 视图使用 img2 复合组件(img2example.xhtml)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:jsf22="http://java.sun.com/jsf/composite/jsf22">
<h:head>
<title>Example of using img2</title>
</h:head>
<h:body>
<jsf22:img2 src="product/1234.png"
title="Click to see more information"
popupTitle="The product is available for shipping within 24 hours" />
</h:body>
</html>
在接下来的几节中,您将看到 JSF 2.2 如何通过引入传递 HTML 元素和属性的能力,使输出 HTML5 友好的标记变得更加容易。
传递属性
自定义属性可以通过三种不同的方式输出。所有这三种方法的输出是相同的,但是每种方法都有自己最方便的级别,如下所示。
方法 1:使用
JSF 2.2 引入了一个类似于的新标签,叫做
清单 5-4。 使用< f:PassThroughAttribute / >向 UIComponent 添加两个自定义属性
<h:graphicImage value="product/1234.png"
title="Click to see more information">
<f:passThroughAttribute name="data-popup-title"
value="The product is available for shipping within 24 hours" />
<f:passThroughAttribute name="data-product-id"
value="1234" />
</h:graphicImage>
方法 2:使用
方法 1 适合于添加一些属性,但是如果您有许多必须添加到组件的属性,并且这些值是在服务器端确定的,那么您可以使用
清单 5-5。 使用< f:PassThroughAttributes / >向 UIComponent 添加属性映射
<h:graphicImage value="product/1234.png"
title="Click to see more information">
<f:passThroughAttributes value="#{productDisplay.productAttributes}" />
</h:graphicImage>
清单 5-6。 ViewScoped 托管 Bean 公开了一个产品属性映射,该映射可由< f:PassThroughAttributes / >使用
@ManagedBean
@ViewScoped
public class ProductDisplay {
private Map<String, Object> attributes;
public Map<String, Object> getProductAttributes() {
if (this.attributes == null) {
this.attributes = new HashMap<>();
this.attributes.put("data-popup-title", "Click to see more information");
this.attributes.put("data-product-id", "1234");
this.attributes.put("data-product-name", "Blu-ray Player");
this.attributes.put("data-product-desc", "Complimment your entertainment...");
}
return this.attributes;
}
}
方法 3:使用带前缀的属性直接向 UIComponents 添加自定义属性
作为方法 1 的替代方法,您可以使用带前缀的属性将自定义属性直接添加到 UIComponent 中。属性的前缀是 p,XML 命名空间 URI 是xmlns.jcp.org/jsf/passthrough
。这种方法对某些人来说似乎更自然,因为你是直接在 UIComponent 上添加属性,而不包含任何嵌套标签,参见清单 5-7 。
清单 5-7。 使用前缀属性添加自定义属性
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="[`xmlns.jcp.org/jsf/passthrough`](http://xmlns.jcp.org/jsf/passthrough)">
<h:body>
<h:graphicImage value="product/1234.png"
title="Click to see more information"
p:data-popup-title="Available for shipping within 24 hours"
p:data-product-id="1234"/>
</h:body>
</html>
穿过元素
HTML5 引入了 JSF 没有的新元素,比如
清单 5-8。 使用 jsf 属性命名空间传递元素
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<h:body>
<h:form>
<progress jsf:id="progressbar"
value="#{imageGeneration.progress}"
max="100" />
</h:form>
</h:body>
</html>
从技术上来说,Facelets TagDecorator 负责将定制元素视为 UIComponents。在某些情况下,TagDecorator 会识别出与 HTML 标记完全相同的 UIComponent。在清单 5-9 中,HTML 标签和 JSF 标签将产生相同的输出和组件树。如果你喜欢尽可能接近 HTML 来写你的观点,这是对 JSF 的一个很好的补充。
清单 5-9。HTML 和 JSF 标签之间的自动映射
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<h:body>
<h:form>
<input type="text" jsf:value="#{registration.firstName}" />
<h:inputText value="#{registration.lastName}" />
</h:form>
</h:body>
</html>
资源库契约
JSF 2.0 引入了资源库,级联样式表、javascripts、图像和复合组件驻留在 resources/目录中,或者打包在 JAR 文件的 META-INF/resources 目录中。资源库契约通过引入拥有多个资源库的可能性,将这一特性向前推进了一步。使用资源库契约,您可以将模板映射到应用中的特定视图。例如,您可以为匿名用户和经过身份验证的用户或者应用的不同部分使用单独的模板和资源。与普通资源一样,您可以在 contracts/目录下的应用中包含资源库协定,或者通过将协定打包到 META-INF/contracts 目录下的 JAR 文件中。将 JAR 文件放在 WEB-INF/lib 目录中,应用会自动发现它。
提示为了加快资源库契约的发现,在包含契约的目录中放置一个名为 javax.faces.contract.xml 的文件。目前该文件没有任何内容,但在即将推出的版本中可能会有所改变。
有两种使用合同的方法。第一种方法将通过 URL 模式自动将契约映射到视图上。第二种方法在视图中显式声明契约。这些方法可以结合使用,以获得最大的灵活性。首先,我们将看看如何创建资源库契约;然后,我们将看看在应用中应用它们的两种方法。
资源库契约的目标是使一组模板可用,这些模板可由不知道可用资源库契约中正在使用的确切模板的模板客户端重用。因此,资源库契约应该使用相同的模板和内容区域名称。也就是说,模板文件必须具有相同的文件名,并且
例如,我们将在一个应用中创建两个合同。第一个合同在清单 5-10 和清单 5-11 实施,第二个合同在清单 5-12 和清单 5-13 实施。两个模板的区别在于配色方案和帮助文本。
清单 5-10。 文件结构中的"基础"资源库合同应用目录
| contracts/
| contracts/basic/page-template.xhtml
| contracts/basic/layout.css
| contracts/basic/page.css
清单 5-11。 合同目录/基础/页面-模板. xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:p="http://xmlns.jcp.org/jsf/passthrough"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<h:outputStylesheet name="page.css" />
<h:outputStylesheet name="layout.css" />
<title><ui:insert name="page-title" /></title>
</h:head>
<h:body>
<div id="top" class="top">
<ui:insert name="top" />
</div>
<div id="content" class="center_content">
<ui:insert name="content" />
</div>
</h:body>
</html>
清单 5-12。 文件结构资源库合同后添加一个"基本-附加"合同
| contracts/
| contracts/basic/page-template.xhtml
| contracts/basic/layout.css
| contracts/basic/page.css
| contracts/basic-plus/page-template.xhtml
| contracts/basic-plus/layout.css
| contracts/basic-plus/page.css
| contracts/basic-plus/logo.png
清单 5-13。 合同目录/基础-附加/页面-模板. xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:p="http://xmlns.jcp.org/jsf/passthrough"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<h:outputStylesheet name="page.css" />
<h:outputStylesheet name="layout.css" />
<title><ui:insert name="page-title" /></title>
</h:head>
<h:body>
<div id="top" class="top">
<h:graphicImage id="logo" name="logo.png" />
<ui:insert name="top" />
</div>
<div id="help" class="left">
Welcome to JSF 2.2\. This example demonstrates how to use Resource Library Contracts.
</div>
<div id="content" class="right ">
<ui:insert name="content" />
</div>
<div id="top" class="top">
You can find more information about JSF 2.2 at the
<a href="http://jcp.org/en/jsr/detail?id=344">JCP website</a>
</div>
</h:body>
</html>
从清单 5-11 中可以看出,资源库契约模板就像普通的 Facelets 模板一样。由于这个模板将是我们合同的基础,我们必须记下并重用模板文件名以及内容区域。也就是说,我们的模板文件必须命名为 page-template.xhtml,并且我们必须坚持使用<ui:insert name = " page-title "/>来插入页面的标题,< ui:insert name="top" / >来插入页面的页眉,< ui:insert name="content" / >来插入页面的内容。您可以自由地更改资源库合同中的所有内容,包括样式表和图像。我们资源库契约的唯一名称是/contracts 下的目录名,也就是 basic。要创建另一个资源库契约,只需在/contracts 下创建一个具有唯一名称的目录,其中包含同名的模板。清单 5-12 显示了在创建了基本加合同之后合同目录的结构。
正如您在清单 5-13 中看到的,模板名称是相同的(page-template.xhtml),内容区域也是相同的(page-title,top,content)。basic 和 basic-plus 遵循相同的约定,可以由相同的模板客户端使用。
资源库契约已经就绪,可以使用了。
方法 1:通过 URL 模式在视图上映射契约
可以通过 URL 模式指定使用哪个契约 。当您希望将不同的资源库应用于不同的部分或访问级别时,这很有用。例如,您可能希望对匿名用户和管理员应用单独的资源库。您可以将资源库契约映射到 applications 标记内 faces-config.xml 中的视图。
在清单 5-14 中,当访问/admin 下的视图时,应用基本的资源库契约。所有其他视图都使用 basic-plus 资源库,其中包含更多帮助信息。
清单 5-14。 对通过/admin/*访问的视图和其余视图应用单独的契约
<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2"
FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
<application>
<resource-library-contracts>
<contract-mapping>
<url-pattern>/admin/*</url-pattern>
<contracts>basic</contracts>
</contract-mapping>
<contract-mapping>
<url-pattern>*</url-pattern>
<contracts>basic-plus</contracts>
</contract-mapping>
</resource-library-contracts>
</application>
</faces-config>
提示可以将多个合同映射到一个映射。在这种情况下,它将依次检查每个合同,寻找所需的模板。一旦找到模板,它将停止处理其他合同。
方法二:在每个视图上指定合同
**通过在每个视图上指定契约,您可以让您的应用由用户来设置皮肤。也就是说,您可以允许用户选择为您的应用应用哪个合同。通过将视图包含在一个
清单 5-15。 允许用户选择要应用到视图的合同
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:p="http://xmlns.jcp.org/jsf/passthrough">
<f:view contracts="#{userSession.contract}">
<ui:composition template="/page-template.xhtml">
<ui:define name="page-title">Welcome to JSF 2.2</ui:define>
<ui:define name="content">
<h:form>
Select a template
<h:selectOneRadio value="#{userSession.contract}" layout="pageDirection" required="true">
<f:selectItem itemValue="basic" itemLabel="Basic" />
<f:selectItem itemValue="basic-plus" itemLabel="Basic Plus" />
</h:selectOneRadio>
<h:commandButton value="Save" />
</h:form>
</ui:define>
<ui:define name="top">Template: #{userSession.contract}</ui:define>
</ui:composition>
</f:view>
</html>
清单 5-16。 会话范围的托管 Bean 用于存储选中的合同
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
@ManagedBean
@SessionScoped
public class UserSession {
private String contract = "basic";
public String getContract() {
return contract;
}
public void setContract(String contract) {
this.contract = contract;
}
}
清单 5-16 可以扩展为从 cookie 或数据库中选择合同,这样用户就不必每次使用应用时都选择要应用的合同。
将合同映射到视图的方法取决于您的应用需求。您可以混合使用这两种方法来获得最大的灵活性。
面流
自从引入 JavaServer Faces 以来,开发人员一直抱怨缺少对覆盖用户流的范围的支持,比如向导和多步注册表单。在 JSF 2.0 中,引入了@ViewScope 注释来支持在同一视图中的可变持久性。@ConversationScoped 是在 JSF 2.0 中为 CDI(组件依赖注入)bean 引入的,通过将 javax . enterprise . context . conversation 接口注入受管 bean,可以开始和结束长时间运行的对话。使用@ViewScope 和@ConversationScoped scopes,您可以实现多步注册表单和向导,但是一旦实现,您会发现最终产品相当分散,不容易重用。Faces flows 通过提供一个完全集成的解决方案来解决这些缺点,在这个解决方案中,您可以在一个受托管 beanss 支持的流定义中指定多个用户流,受托管 bean 用@FlowScoped 进行了注释,并且能够将流打包到单独的目录和 JAR 中。流还可以使用入站和出站参数进行交互。
注意 @FlowScoped 是一个 CDI 作用域,因此您必须在您的应用中启用 CDI,方法是将 beans.xml 包含在 WEB-INF/目录中,或者如果流被打包在 JAR 中,则包含在 META-INF/目录中。
流程定义
您可以在 XML 文件(与其他流文件以-flow.xml 为后缀)中定义流,也可以在用@FlowDefinition 注释的类中定义流。在用@FlowDefinition 注释的类中,您使用 FlowBuilder API 指定流,而 XML 文件使用xmlns.jcp.org/jsf/flow
XML 名称空间和模式定义流。使用 FlowBuilder 的好处是,您可以完全编程控制如何定义流。也就是说,您可以基于运行时信息构建您的流。缺点是,不像 XML 版本,仅仅通过查看代码来快速获得流程的概述要困难得多。清单 5-17 显示了用 XML 表达的流程定义。
清单 5-17。XML 中的流程定义
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:j="http://java.sun.com/jsf/flow">
<f:metadata>
<j:faces-flow-definition id="newEntryFlow">
<!-- Method to execute when the flow is initialized -->
<j:initializer>#{addressBook.newEntry}</j:initializer>
<!-- Specifies the first node of the flow -->
<j:start-node>newEntryStart</j:start-node>
<!-- Using a switch you can dynamically determine the next node -->
<j:switch id="newEntryStart">
<!-- Go to newEntryHelp if this is the first time the user
is using the wizard -->
<j:navigation-case>
<j:if>#{addressBook.newEntryFirstTime}</j:if>
<j:from-outcome>newEntryHelp</j:from-outcome>
</j:navigation-case>
<!-- Go to basicDetails if this is not the first time that
the user has used the wizard -->
<j:navigation-case>
<j:if>#{!addressBook.newEntryFirstTime}</j:if>
<j:from-outcome>basicDetails</j:from-outcome>
</j:navigation-case>
</j:switch>
<j:view id="newEntryHelp">
<j:vdl-document>newEntryHelp.xhtml</j:vdl-document>
</j:view>
<j:view id="basicDetails">
<j:vdl-document>create-entry-1.xhtml</j:vdl-document>
</j:view>
<j:view id="contactDetails">
<j:vdl-document>create-entry-2.xhtml</j:vdl-document>
</j:view>
<j:view id="contactPhoto">
<j:vdl-document>create-entry-3.xhtml</j:vdl-document>
</j:view>
<!-- The flow can end by navigating to the cancel flow -->
<j:faces-flow-return id="cancel">
<j:navigation-case>
<j:from-outcome>/cancel</j:from-outcome>
</j:navigation-case>
</j:faces-flow-return>
<!-- Method to execute when the flow has ended -->
<j:finalizer>#{addressBook.newEntryFinished}</j:finalizer>
</j:faces-flow-definition>
</f:metadata>
</html>
开始和结束流程
您可以通过在一个动作中调用流的 ID 来启动流。您可以通过返回流程定义中 faces-flow-return 中定义的结果来结束流程。清单 5-18 显示了如何使用命令链接来开始和结束一个流程。
清单 5-18。 您可以通过将流 ID 设置为 UICommand 的动作来启动新的流
<h:commandLink value="Click to add a new entry in the address book" action="newEntryFlow" />
<h:commandLink value="Cancel creating a new entry" action="/cancel" />
遍历流程并存储数据
流量数据有两种存储方式 。您可以将它作为属性存储在用@FlowScoped 注释的 CDI beans 上,如清单 5-19 所示,或者您可以将数据添加到流映射中,该映射保存您放入的任何数据,如清单 5-20 所示。一旦流程结束,地图将被清除。
清单 5-19。 流作用域 Bean 控制逻辑和存储数据
@Named
@FlowScoped(id = "newEntryFlow")
public class AddressBook implements Serializable {
private AddressBookEntry entry;
/**
* Initialiser for the flow.
*/
public void newEntry() {
this.entry = new AddressBookEntry();
...
}
/**
* Determines if this is the first time the new entry flow is being used.
*/
public boolean isNewEntryFirstTime() {
...
}
public AddressBookEntry getEntry() {
return this.entry;
}
}
清单 5-20。 flowScope 可以用来存储任何一种流动期间的物体
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<head>
<title>Enter your name</title>
</head>
<body>
<h:form>
<h:outputLabel for="firstName" value="First name: " />
<h:inputText id="firstName" value="#{flowScope.firstName}" />
<h:outputLabel for="LastName" value="Last name: " />
<h:inputText id="lastName" value="#{flowScope.lastName}" />
<h:commandButton value="Next" action="contactDetails" />
<h:commandButton value="Cancel" action="cancel" />
</h:form>
</body>
</html>
包装
流资源可以驻留在 web 应用中 web 根目录下的目录中,如清单 5-21 所示,也可以打包在一个 JAR 文件中,然后放在/WEB-INF/lib 中,如清单 5-22 所示。页面作者不需要担心流资源如何与应用打包在一起。流资源的引用是相同的,不管它们是如何打包的。
清单 5-21。 驻留在 Web 应用内部的几个流的文件布局
| newEntryFlow/newEntryFlow-flow.xml
| newEntryFlow/newEntryHelp.xhtml
| newEntryFlow/create-entry-1.xhtml
| newEntryFlow/create-entry-2.xhtml
| newEntryFlow/create-entry-3.xhtml
| modifyEntryFlow/modifyEntryFlow-flow.xml
| modifyEntryFlow/modifyEntryHelp.xhtml
| modifyEntryFlow/modify-entry-1.xhtml
| modifyEntryFlow/modify-entry-2.xhtml
| modifyEntryFlow/modify-entry-3.xhtml
清单 5-22。 驻留在 JAR 文件中的流的文件布局
| META-INF/flows/beans.xml
| META-INF/faces-config.xml
| META-INF/flows/newEntryFlow/newEntryFlow-flow.xml
| META-INF/flows/newEntryFlow/newEntryHelp.xhtml
| META-INF/flows/newEntryFlow/create-entry-1.xhtml
| META-INF/flows/newEntryFlow/create-entry-2.xhtml
| META-INF/flows/newEntryFlow/create-entry-3.xhtml
| META-INF/flows/modifyEntryFlow/modifyEntryFlow-flow.xml
| META-INF/flows/modifyEntryFlow/modifyEntryHelp.xhtml
| META-INF/flows/modifyEntryFlow/modify-entry-1.xhtml
| META-INF/flows/modifyEntryFlow/modify-entry-2.xhtml
| META-INF/flows/modifyEntryFlow/modify-entry-3.xhtml
| myflow/NewEntryFlow.class
| myflow/ModifyEntryFlow.class
无状态视图
当请求视图时,JSF 通常会检查状态的副本是否可用(在服务器或客户机上,具体取决于 javax . faces . state _ SAVING _ METHOD 上下文参数的值)。如果请求的视图不存在,则创建该视图,并存储视图中组件的详细信息,供以后检索和处理。在某些情况下,视图可能已经过期,您将会收到可怕的视图过期异常。在高负载应用中,保存和恢复视图所涉及的所有处理都会产生不必要的开销。JSF 2.2 引入了一个简单但强大的特性,叫做无状态视图。使用无状态视图,您可以指定其状态不应被管理的视图。相反,每次请求视图时,视图的状态都被设置为初始状态。通过将
清单 5-23。 无状态查看报名简讯
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<f:view transient="true">
<ui:composition template="/page-template.xhtml">
<ui:define name="page-title">Newsletter Sign-up</ui:define>
<ui:define name="content">
<h:form>
Your e-mail address:
<h:inputText value="#{newsletterSubscription.email}" />
<h:commandButton action="#{newsletterSubscription.subscribe}"
value="Subscribe" />
<h:commandButton action="#{newsletterSubscription.unsubscribe}"
value="Unsubscribe" />
</h:form>
</ui:define>
</ui:composition>
</f:view>
</html>
警告无状态视图是一个全新的特性,并不是所有的组件都经过了彻底的测试,在某些情况下这可能会导致不可预知的行为。在对第三方 JSF 组件库使用无状态视图时,应该特别小心。
其他重大变化
除了大型功能之外,还有许多较小的增强。以下是最重要的小变化的总结。
UIData 支持集合接口,不支持列表
从 UIData 派生的组件现在支持 java.util.Collections 作为内部数据模型。在 JSF 2.2 之前,java.util.List 是唯一受支持的集合。这一变化表明 ORM 通常使用 java.util.Set 集合来映射相关数据。
WAI-ARIA 支持
JSF 2.2 在 HTML 组件上实现了角色属性,以支持 Web 可访问性倡议——可访问的富互联网应用套件(WAI-ARIA)。角色属性用于描述 HTML 标签的用途。更多关于 WAI-ARIA 的信息可以在 http://www.w3.org/WAI/intro/aria 的找到。清单 5-24 展示了一个如何使用角色属性给面板网格添加含义的例子。
清单 5-24。 表示 panelGrid(表格)是包含选项的菜单
<h:panelGrid role="menu">
<h:commandLink role="menuitem"value="Home" action="/home" />
<h:commandLink role="menuitem"value="Registration" action="/registration" />
...
</h:panelGrid>
JSF 2.2 引入了一个新的视图元数据标签
清单 5-25。 Facelet 视图使用 f:viewAction 加载记录进行显示
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<f:view>
<f:metadata>
<f:viewParam name="id" value="#{recordDisplay.id}" />
<f:viewAction execute="#{recordDisplay.load}" onPostback="false" />
</f:metadata>
</f:view>
<h:head>
<title>View Record #{recordDisplay.id}</title>
</h:head>
<h:body>
<h1>Record ##{recordDisplay.id}</h1>
<h:panelGrid columns="2">
<h:outputText value="Name:" />
<h:outputText value="#{recordDisplay.record.name}" />
<h:outputText value="Description:" />
<h:outputText value="#{recordDisplay.record.description}" escape="false" />
</h:panelGrid>
</h:body>
</html>
清单 5-26 包含当试图访问一个不存在的记录时应该使用的导航规则。该规则规定,如果返回 false,浏览器应该被重定向到/not-found.xhtml 文件。
清单 5-26。faces-config . XML 中的导航案例,如果无法加载实体,该案例会重定向用户
<faces-config>
<navigation-rule>
<navigation-case>
<from-action>#{recordDisplay.load}</from-action>
<from-outcome>false</from-outcome>
<to-view-id>/not-found.xhtml</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
</faces-config>
清单 5-27 是包含导航规则中使用的逻辑的受管 bean。它还是被访问的 Facelets 视图的后台 bean。
清单 5-27。 用于加载记录的动作方法,如果记录被加载则发出信号
@ManagedBean
@RequestScoped
public class RecordDisplay {
@EJB private RecordService recordService;
private Long id;
private Record record;
public Long getId() {
return id;
}
/**
* Used by f:viewParam to set the ID of the record to load.
*
* @param id Unique identifier of the record to load
*/
public void setId(Long id) {
this.id = id;
}
/**
* Loads the record with the ID specified in the viewParam.
*
* @return true if the record was loaded successfully, otherwise false if it wasn’t found
*/
public boolean load() {
try {
record = recordService.findById(this.id);
return true;
} catch (EntityNotFoundException ex) {
return false;
}
}
}
该功能与
- 使用
,开发者有责任在先决条件失败的情况下重定向导航。 在组件树生成后执行(即在渲染响应阶段),而 在组件树生成前执行(即在应用阶段)。
文件上传
最后,在 JSF 几乎十年没有标准文件上传组件之后,JSF 2.2 引入了
使用该组件相当简单。在 enctype 设置为“multipart/form-data”的表单中包含
例如,清单 5-28 显示了一个包含输入文件组件的表单。该示例通过在启动上传的 commandButton 中包含 f:ajax 组件来使用 Ajax 上传文件。
清单 5-28。 将照片上传到托管 Bean 的表单
<h:form id="frm-photo-upload"
enctype="multipart/form-data">
<h:outputLabel for="photo" value="Please select your photo and click Upload Photo" />
<h:inputFile id="photo" value="#{myProfile.photo}" validator="#{myProfile.validatePhoto}" />
<h:commandButton value="Upload Photo" action="#{myProfile.uploadPhoto}">
<!-- Remove the f:ajax tag for plain old file upload -->
<f:ajax execute="photo" render="@all" />
</h:commandButton>
<h:messages />
</h:form>
清单 5-29 是上传表单的管理 bean。它包含用于验证上传的文件是一个图像并且大小小于 2 MB 的方法,以及一个上传方法,其中上传的文件的内容是使用来自 Apache Commons IO 项目的 IOUtils 类提取的。 1
清单 5-29。 托管 Bean 接收并处理文件上传
import java.io.IOException;
import javax.ejb.EJB;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.servlet.http.Part;
import org.apache.commons.io.IOUtils;
@ManagedBean
@RequestScoped
public class MyProfile {
@EJB private UserProfileService userProfileService;
private UserProfile userProfile;
private Part photo;
public String uploadPhoto() throws IOException {
// Uploading file. You don't have to do anything here, but you could
// use it for post processing. Don't use this method for validating
// the uploaded file.
byte[] photoContents = IOUtils.toByteArray(photo.getInputStream());
userProfileService.savePhoto(userProfile, photoContents);
FacesContext.getCurrentInstance().addMessage("frm-photo-upload",
new FacesMessage(FacesMessage.SEVERITY_INFO, "Photo uploaded successfully",
"Name: " + photo.getName() + " Size: " + (photo.getSize()/1024) + " KB"));
return "/photo-uploaded";
}
/**
* Validator for checking that the file uploaded is a photo and that the file
* size is less than 2MB.
*/
public void validatePhoto(FacesContext ctx, UIComponent comp, Object value) {
// List of possible validation errors
List<FacesMessage> msgs = new ArrayList<>();
// Retrieve the uploaded file from passed value object
Part photo = (Part)value;
// Ensure that the file is an image
if (!"image/".startsWith(file.getContentType())) {
msgs.add(new FacesMessage("The uploaded file must be an image"));
}
// Ensure that the file is less than 2 MB
if (file.getSize() > 2048) {
msgs.add(new FacesMessage("The uploaded file is larger than 2MB"));
}
// Determine if a validation exception should be thrown
if (!msgs.isEmpty()) {
throw new ValidatorException(msgs);
}
}
public Part getPhoto() {
return photo;
}
public void setPhoto(Part photo) {
this.photo = photo;
}
public UserProfile getUserProfile() {
return this.userProfile;
}
public void setUserProfile(UserProfile userProfile) {
this.userProfile = userProfile;
}
}
Ajax 请求延迟
在 JSF 2.2 中,延迟属性被添加到了标签中。该属性接受一个整数,该整数包含 Ajax 请求执行前等待的毫秒数。当用户使用键盘输入 Ajax 时,这非常有用。它不是在用户输入一个键时立即执行请求,而是在执行请求之前等待几毫秒,看看是否输入了另一个键。如果输入另一个键,前面的 Ajax 请求被取消,只执行最后一个请求。清单 5-30 展示了一个 Ajax 请求在按下一个键后被延迟 1.5 秒的例子,让用户有时间在发送请求前完成输入。
清单 5-30。 将 Ajax 请求延迟 1.5 秒
<h:inputText value="#{registrationBean.username}" >
<f:ajax event="keyup" delay="1500"render="confirmation" />
</h:inputText>
<h:outputText id="confirmation" value="#{registrationBean.confirmationMsg}" />
新的 XML 名称空间
在前面的例子中,您可能已经注意到,JSF 2.2 引入了新的 XML 名称空间。旧的名称空间以 http://java.sun.com 的开始,而新的名称空间以 http://xmlns.jcp.org 的开始。旧的名称空间现在仍然有效,但是看起来在未来的版本中将会被删除。新的 XML 名称空间在表 5-1 中列出。
表 5-1 。JSF 图书馆的新 XML 名称空间
|
图书馆
|
老 URI
|
新建 URI
|
| --- | --- | --- |
| 复合组件 | http://java.sun.com/jsf/composite
| http://xmlns.jcp.org/jsf/composite
|
| 面孔核心 | http://java.sun.com/jsf/core
| http://xmlns.jcp.org/jsf/core
|
| Faces HTML | http://java.sun.com/jsf/html
| http://xmlns.jcp.org/jsf/html
|
| JSTL 核心 | http://java.sun.com/jsp/jstl/core
| http://xmlns.jcp.org/jsp/jstl/core
|
| JSTL 函数 | http://java.sun.com/jsp/jstl/functions
| http://xmlns.jcp.org/jsp/jstl/functions
|
| Facelets 模板 | http://java.sun.com/jsf/facelets
| http://xmlns.jcp.org/jsf/facelets
|
| 传递属性 | http://java.sun.com/jsf/passthrough
| http://xmlns.jcp.org/jsf/passthrough
|
| 传递元素 | http://java.sun.com/jsf
| http://xmlns.jcp.org/jsf
|
新的 XML 名称空间应用于清单 5-31 中的空 Facelets 文件。
清单 5-31。 Facelet 视图使用了新的 XML 名称空间
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://xmlns.jcp.org/jsf/passthrough"
xmlns:jsf="http://xmlns.jcp.org/jsf">
</html>
向后兼容性
有两个问题影响了向后兼容性,根源在于 JSF 早期版本中不明确的规范。大多数应用将完全不受这两个问题的影响,但是如果您的应用受到影响,下面将描述这些问题。
第一个问题是由于规范中的一个错误,其中异常以静默方式结束(javax . faces . event . methodexpressionvalueechangelistener . processValueChange()和 javax . faces . event . methodexpressionlistener . process action())。随着 JSF 2.2 规范的澄清,以前被吞咽的异常现在被扔给异常处理程序。任何依赖于被吞咽的异常的应用都必须实现一个安全措施,以避免异常无法处理。
第二个问题是由于基于先前 JSF 规范的意外行为。在 JSF 2.2 中,规范被阐明,因此一些返回类型必须改变。具体来说,它是在访问复合接口内复合组件的属性时返回的 PropertyDescriptors。getValue()和 getValue(java.lang.String)现在将分别返回 javax.el.ValueExpression 和 java.lang.Class。任何直接访问 PropertyDescriptors 的应用都必须考虑已更改的返回类型。
摘要
尽管 JSF 2.2 对 JSF 来说并不是一个重大的改进,但很明显 2.2 版本提供了许多社区多年来一直要求的新功能和变化。值得注意的是四大特色,包括 HTML5 友好标记、资源库契约、faces flows 和无状态视图。在其他重大变化中,我们关注了最终进入 JSF 核心的文件上传组件。文件上传组件不仅支持传统的文件上传,还支持基于 Ajax 的上传。
想要升级他们的 JSF 2.0 和 2.1 应用的开发者几乎可以无缝升级。如“向后兼容性”一节所述,很少有问题会破坏向后兼容性,据估计,受影响的应用也很少。
Apache Commons IO 项目是一个使用 IO 功能的库的集合。该项目可以在以下网址找到:【http://commons.apache.org/proper/commons-io/ **
六、深入:JSF 定制组件
到目前为止,我们已经从页面作者和应用开发人员的角度看了 Java Server Faces。页面作者关心用户界面的创作,即在 Facelet 视图中构建标记、脚本和样式,在 Facelet 视图中利用 JSF 组件引入动态内容和行为。应用开发人员关心的是编写应用的服务器端行为。这包括构建页面作者的 Facelet 视图直接使用的托管 JSF bean,以及包含业务逻辑和持久性机制(如 JPA 实体)的企业 Java bean(EJB)。
在第六章、第七章和第八章中,我们将关注组件编写人员的职责。组件编写器的作用是创建可重用组件的库,这些组件或者支持特定的应用,或者可以在多个应用之间重用。第七章和 8 展示了复合材料组件的开发。本章的其余部分将关注如何创建非复合组件。第九章介绍了两个流行的带有可重用通用组件的库。这些库可供页面作者和应用开发人员使用,不需要组件编写器。本章介绍了 JSF 组件模型以及如何实现定制的可重用组件。在我们深入 JSF 组件模型之前,我们将首先看看面向组件的软件开发的一些特征。当开发组件时,重点是创建一个具有单一明确职责的软件包。该包应该具有最小的相互依赖性,以确保组件和使用该组件的环境之间的低耦合。有了单一责任和低耦合,就有可能实现全面的单元测试,覆盖组件的许多方面,如果不是所有方面的话。它还提供了一个机会来完整地记录如何使用组件以及它是如何设计的。最后,在开发组件时,组件编写人员必须考虑到组件可能会用在不可预见的场景中。这是应用和组件开发的主要区别之一。在应用开发中,可以完全控制提供给用户的应用和功能。在对象之间存在高耦合性和低内聚性的情况下,您可以轻松地编写应用。当编写一个组件时,范围受到单一职责和与外界良好定义的接口的限制。这允许低耦合和高内聚。
换句话说,当您创建一个组件时,您不能对外部环境做任何假设,它必须有一个定义良好的输入和输出接口,这样外部环境就不必考虑组件是如何实现的,而只需依赖组件所公开的契约。
我们将考虑两种类型的定制组件:用户界面(UI)组件,它们对用户来说是可视化的,以及非用户界面(非 UI)组件,它们实现非功能性或开发人员需求。
了解 JSF 组件架构
开发 JSF 是为了解决 web 应用开发过程中出现的实际问题。JSF 解决的问题之一是对可重用组件的支持,这些组件封装了页面作者的实际实现。JSF 附带了一组标准组件,但是当你着手构建一个简单的 web 应用时,这些组件是远远不够的。在开始开发定制组件之前,您必须了解 JSF 组件架构以及组件作者打算如何使用它。
作为组件编写人员,您可以编写两种类型的定制组件,非复合组件和复合组件。非复合部件是 JSF 第一版的一部分,所有标准部件都是非复合部件。JSF 2 中引入了复合组件,并通过 Facelets 视图声明语言(VDL)简化了组件的创建。非复合组件是用 Java 代码实现的,需要了解 JSF 组件架构。
当您开发 JSF 定制组件时,您必须熟悉几个核心类和文件。它们是 UIComponent、渲染器和标记库描述符(TLD)。
UIComponent 是一个抽象类,所有 JSF 组件都扩展了它。UIComponent 负责组件的数据、状态和行为。在一些简单的情况下,它可能还负责渲染输出,但应该避免使用渲染器的功能。Renderer 也是一个抽象类,自定义组件可以创建它来控制向用户呈现组件的 UI。每个组件(或组件库)都必须有一个 TLD 文件,该文件将 UIComponent 公开为一个标记,并将每个 UIComponent 与适当的呈现器相匹配。TLD 文件打包在独立库的 META-INF/目录中,或者如果它与使用组件的 WEB 应用打包在一起,则打包在 WEB-INF/目录中。TLD 文件的名称通常为 COMPONENT-NAME.taglib.xml,例如 my components . taglib . XML。TLD 文件在 web.xml 文件的上下文参数中引用。三个实体之间的关系见图 6-1 。
图 6-1 。组件开发中的两个核心类以及它们是如何绑定在一起的
您还可以将定制组件与其他定制帮助器耦合,例如验证器、转换器和事件监听器,如第三章和第四章中所述。
创建 JSF 定制组件需要四个步骤。
- 创建组件模型和逻辑
- 创建自定义组件类(从 UIComponent 或其子类型之一派生)
- 如果自定义组件委托呈现,则创建自定义呈现器类
- 创建一个 TLD 文件,该文件将组件和渲染器定义和公开为一个标记
现在让我们更详细地了解这些步骤。
RandomText 自定义组件
我们将通过逐步开发一个组件来演示定制组件的实现,该组件从一个名为 random text 的 web 服务生成随机文本,该 web 服务位于 www.randomtext.me 。该组件的目的是根据用户或页面作者的输入生成一些随机文本。该组件可以简单地用于生成随机文本,或者页面作者可以在开发过程中使用它在页面上插入占位符。
步骤 1—创建组件模型和逻辑
当创建组件时,很容易将组件逻辑与必须实现的类混合起来,以使其在框架上工作。然而,这使得组件很难测试,并且您将框架特性与可以封装和重用的逻辑混在了一起。如果框架突然对如何实现组件有了新的或不同的需求,这也使得升级变得困难。
组件创建的第一步是在了解模型和逻辑如何与 UIComponent 和 Renderer 类交互之前构建模型和逻辑。RandomText 组件的模型相当简单。这是一个名为 RandomTextAPI 的简单类,它有一个调用 RandomText REST 服务并返回从该服务接收的输出的方法。清单 6-1 显示了简化服务的源代码。
清单 6-1。RandomTextAPI.java 实现了一个简化的 API,用于从在线 randomtext.me REST 服务中获取随机文本
package com.apress.projsf2html5.chapter6.components;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
/**
* Simple API for obtaining random text from an online REST service.
*
* @see <a href="http://www.randomtext.me">RandomText</a>
*/
public class RandomTextAPI {
/**
* Enumeration containing the type of text to return.
*/
public enum TextType {
/** Return gibberish text. */
gibberish,
/** Return lorem ipsum text. */
lorem
};
/**
* Enumeration containing the type of formatting to return.
*/
public enum OutputTag {
/** Return the output as paragraphs. */
p,
/** Return the output as items in an unordered list. */
ul
}
/**
* URL to the REST service with five parameters.
*/
private static final String API_URL = "http://www.randomtext.me/api/%s/%s-%d/%d-%d";
/**
* Property in the JSON output that we will extract and return.
*/
private static final String PROPERTY_CONTAINING_OUTPUT = "text_out";
/**
* Google's JSON parser for parsing the result from the service.
*/
private JsonParser jsonParser = new JsonParser();
/**
* Gets a random text from the RandomText.me web service.
*
* @param type Type of random text to return, {@link TextType#gibberish} or
* {@link TextType#lorem}
* @param output Type of output to produce, {@link OutputTag#p} (paragraph
* tags) or {@link OutputTag#ul} (a list)
* @param outputCount Number of outputs to produce (i.e. number of
* paragraphs or list items)
* @param wordCountLower Lowest number of words in a single paragraph or
* list item
* @param wordCountUpper Highest number of words in a single paragraph or
* list item
* @return Random text formatted as {@code type} and {@code output}
* @throws IOException
*/
public String getRandomText(TextType type, OutputTag output, int outputCount, int wordCountLower, int wordCountUpper) throws IOException {
// Generate URL based on method inptu
String url = String.format(API_URL, type, output, outputCount, wordCountLower, wordCountUpper);
// Prepare request to the randomtext.me
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
// Process response by reading the content into a StringBuilder
BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
StringBuilder apiResult = new StringBuilder();
String line;
while ((line = rd.readLine()) != null) {
apiResult.append(line);
}
// Use the GSON Library to parse the JSON response from randomtext.me
JsonElement jsonElement = jsonParser.parse(apiResult.toString());
JsonObject jsonObject = jsonElement.getAsJsonObject();
return jsonObject.get(PROPERTY_CONTAINING_OUTPUT).getAsString();
}
}
这可以很容易地在下一步中直接复制到 UIComponent 的实现中,但是最终会将业务逻辑与组件表示混合起来,当您需要升级组件时,这很可能会带来维护方面的挑战。为了展示将模型和组件的开发分开的好处,您将在清单 6-2 中找到一个测试用例,如果业务逻辑与 UIComponent 实现混合在一起,这个测试用例将更难实现。
清单 6-2。 针对 RandomTextAPI.java 的简单单元测试
package com.apress.projsf2html5.chapter6.components;
import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import static org.junit.Assert.*;
public class RandomTextAPITest {
int outputCount = 10;
int wordCountLower = 3;
int wordCountUpper = 15;
@Test
public void testListRandomText() throws Exception {
RandomTextAPI.TextType type = RandomTextAPI.TextType.gibberish;
RandomTextAPI.OutputTag output = RandomTextAPI.OutputTag.ul;
RandomTextAPI instance = new RandomTextAPI();
String result = instance.getRandomText(type, output, outputCount, wordCountLower, wordCountUpper);
int paragraphCount = StringUtils.countMatches(result, "<li>");
assertEquals("Incorrect number of items in the list", outputCount, paragraphCount);
}
@Test
public void testGibberishParagraphsRandomText() throws Exception {
RandomTextAPI.TextType type = RandomTextAPI.TextType.gibberish;
RandomTextAPI.OutputTag output = RandomTextAPI.OutputTag.p;
RandomTextAPI instance = new RandomTextAPI();
String result = instance.getRandomText(type, output, outputCount, wordCountLower, wordCountUpper);
int paragraphCount = StringUtils.countMatches(result, "<p>");
assertEquals("Incorrect number of paragraphs", outputCount, paragraphCount);
}
@Test
public void testLoremIpsumParagraphsRandomText() throws Exception {
RandomTextAPI.TextType type = RandomTextAPI.TextType.lorem;
RandomTextAPI.OutputTag output = RandomTextAPI.OutputTag.p;
RandomTextAPI instance = new RandomTextAPI();
String result = instance.getRandomText(type, output, outputCount, wordCountLower, wordCountUpper);
int paragraphCount = StringUtils.countMatches(result, "<p>");
boolean containsLoremIpsum = result.contains("Lorem ipsum");
assertEquals("Incorrect number of paragraphs", outputCount, paragraphCount);
assertTrue("Lorem Ipsum was not found in the result", containsLoremIpsum);
}
}
步骤 2—创建自定义组件
现在我们已经有了自己的逻辑,我们可以继续实现组件了。如前所述,所有组件都必须扩展 UIComponent 或它的一个子类型。UIComponentBase 是 UIComponent 的一个子类型。UIComponentBase 提供了除 getFamily()之外的所有抽象方法的默认实现。当您需要一个在 UIComponentBase 实现中无法满足的完整定制解决方案时,您可以实现 UIComponent。在大多数情况下,您会希望实现 UIComponentBase 或一个标准组件,如用于输入组件的 UIInput 或用于输出组件的 UIOutput。在图 6-2 中,你可以看到 UIComponent 类层次结构的 UML 图。
图 6-2 。UIComponent 层次结构的 UML 图
对于我们的随机文本组件,我们将扩展 UIComponentBase 类。这样我们只剩下一个方法要实现,即 getFamily()。每个组件必须有“组件系列”标识符。标识符用于将组件与渲染器相匹配。组件和渲染器的配对在 TLD 完成,您将在步骤 4 中看到。从 JSF 2.0 开始,您必须用@FacesComponent 注释来注释组件。这为您省去了创建标记处理程序的麻烦,而这在以前的 JSF 版本中是必需的。@FacesComponent 需要一个值,即“组件类型”标识符。这类似于“组件系列”标识符。“组件类型”的目的是允许 JSF 应用 singleton 在运行时基于其类型实例化组件。基于清单 6-3 中的代码,可以通过执行以下代码实例化一个新的 ui component:ui component my comp = context . get application()。create component(RandomTextComponent。COMPONENT _ TYPE);
在组件上,我们还必须为我们希望从页面作者那里公开和收集的属性实现 getters 和 setters。带有描述和默认值的属性可在表 6-1 中找到
表 6-1 。随机文本组件的属性
|
属性
|
描述
|
默认
|
| --- | --- | --- |
| 文字类型 | 要生成的随机文本类型 | 莫名其妙的话 |
| 输出标签 | 要生成的 HTML 类型。p 代表段落,ul 代表无序列表。 | P |
| 数数 | 要返回的段落或项目数 | Ten |
| minWords | 每个段落或项目中返回的最小字数。 | five |
| maxWords | 每个段落或项目中返回的最大字数。 | Ten |
.
最后,我们需要一种方法来获得随机文本。该方法将收集属性并调用 RandomTextAPI。RandomTextComponent 的完整列表可以在列表 6-3 中找到。
清单 6-3。randomtextcomponent . Java—我们组件的 UIComponent 实现
package com.apress.projsf2html5.chapter6.components;
import java.io.IOException;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponentBase;
// "RandomText" is the Component Type
@FacesComponent(RandomTextComponent.COMPONENT_TYPE)
public class RandomTextComponent extends UIComponentBase {
/** Component family of {@link RandomTextComponent}. */
public static final String COMPONENT_FAMILY = "RandomText";
/** Component type of {@link RandomTextComponent}. */
public static final String COMPONENT_TYPE = "RandomText";
/** Attribute name constant for textType. */
private static final String ATTR_TEXT_TYPE = "textType";
/** Default value for the textType attribute. */
private static final String ATTR_TEXT_TYPE_DEFAULT = "lorem";
/** Attribute name constant for outputTag. */
private static final String ATTR_OUTPUT_TAG = "outputTag";
/** Default value for the outputTag attribute. */
private static final String ATTR_OUTPUT_TAG_DEFAULT = "p";
/** Attribute name constant for count. */
private static final String ATTR_COUNT = "count";
/** Default value for the count attribute. */
private static final Integer ATTR_COUNT_DEFAULT = 10;
/** Attribute name constant for minWords. */
private static final String ATTR_MIN_WORDS = "minWords";
/** Default value for the minWords attribute. */
private static final Integer ATTR_MIN_WORDS_DEFAULT = 5;
/** Attribute name constant for maxWords. */
private static final String ATTR_MAX_WORDS = "maxWords";
/** Default value for the maxWords attribute. */
private static final Integer ATTR_MAX_WORDS_DEFAULT = 10;
@Override
public String getFamily() {
return RandomTextComponent.COMPONENT_FAMILY;
}
// LOGIC
public String getRandomText() throws IOException {
RandomTextAPI api = new RandomTextAPI();
return api.getRandomText(
RandomTextAPI.TextType.valueOf(getTextType()),
RandomTextAPI.OutputTag.valueOf(getOutputTag()),
getCount(),
getMinWords(),
getMaxWords());
}
// ATTRIBUTES
public String getTextType() {
return (String) getStateHelper().eval(ATTR_TEXT_TYPE, ATTR_TEXT_TYPE_DEFAULT);
}
public void setTextType(String textType) {
getStateHelper().put(ATTR_TEXT_TYPE, textType);
}
public String getOutputTag() {
return (String) getStateHelper().eval(ATTR_OUTPUT_TAG, ATTR_OUTPUT_TAG_DEFAULT);
}
public void setOutputTag(String outputTag) {
getStateHelper().put(ATTR_OUTPUT_TAG, outputTag);
}
public Integer getCount() {
return (Integer) getStateHelper().eval(ATTR_COUNT, ATTR_COUNT_DEFAULT);
}
public void setCount(Integer count) {
getStateHelper().put(ATTR_COUNT, count);
}
public Integer getMinWords() {
return (Integer) getStateHelper().eval(ATTR_MIN_WORDS, ATTR_MIN_WORDS_DEFAULT);
}
public void setMinWords(Integer minWords) {
getStateHelper().put(ATTR_MIN_WORDS, minWords);
}
public Integer getMaxWords() {
return (Integer) getStateHelper().eval(ATTR_MAX_WORDS, ATTR_MAX_WORDS_DEFAULT);
}
public void setMaxWords(Integer maxWords) {
getStateHelper().put(ATTR_MAX_WORDS, maxWords);
}
}
你可能已经注意到清单 6-3 中的 getter 和 setter 并不是封装类成员的典型 getter 和 setter。相反,它们使用 UIComponent 类上公开的 StateHelper。如果属性只是使用类成员来存储它们的值,那么这些值会在每次请求后消失,因为它们不会持久存储在任何地方。所有 UIComponents 都实现 PartialStateHolder 接口,目的是每个 UIComponent 都必须管理自己的状态。所有标准组件都实现 PartialStateHolder,并使用 StateHelper 来保存和检索必要的数据。但是,如果您扩展 UIComponent 而不是标准组件,您必须自己管理组件的状态。考虑到 JSF 实现可能在客户端或服务器端存储组件状态(取决于 javax . faces . state _ SAVING _ METHOD 上下文参数的值),组件编写器可能需要做大量工作来实现状态管理。幸运的是,JSF 的作者意识到了这一点,并为任何实现 UIComponent 的类提供了 StateHelper 类。StateHelper 透明地负责保存和恢复视图间组件的状态。参见图 6-3 中的 StateHolder 类层次和方法。基本上,StateHelper 允许我们将一个对象放入一个具有可序列化名称的映射中。稍后,我们可以使用相同的可序列化名称获取(评估)对象。如果请求的名称不可用,则返回空对象。为了避免检查空值,StateHelper 有一个重载的 eval 方法,您可以在该方法中指定要查找的对象的名称,以及在没有找到所请求的对象时应该返回的值。这对于为属性提供默认值很方便。
图 6-3 。StateHolder 和 StateHelper 类
使用 StateHelper 存储和检索值可以通过使用 put 和 eval 方法来实现,如清单 6-4 所示。
清单 6-4。 使用 StateHelper 存储和检索状态值
public void setTextType(String textType) {
// Store the textType value under the constant ATTR_TEXT_TYPE
getStateHelper().put(ATTR_TEXT_TYPE, textType);
}
public String getTextType() {
// Retrieve the value stored under the constant ATTR_TEXT_TYPE
return (String) getStateHelper().eval(ATTR_TEXT_TYPE);
}
public void setOutputTag(String outputTag) {
// Store the outputTag value under the constant ATTR_OUTPUT_TAG
getStateHelper().put(ATTR_OUTPUT_TAG, outputTag);
}
public String getOutputTag() {
// Retrieve the value stored under the constant ATTR_OUTPUT_TAG
// If the ATTR_OUTPUT_TAG constant could not be found, the value
// in the constant ATTR_OUTPUT_TAG_DEFAULT will be returned instead.
return (String) getStateHelper().eval(ATTR_OUTPUT_TAG, ATTR_OUTPUT_TAG_DEFAULT);
}
步骤 3-创建自定义渲染器类
我们已经实现了从页面作者处获取输入并使用 StateHelper 安全存储值的逻辑和组件。接下来,我们需要实现呈现器,将组件可视化呈现给用户。通过在 UIComponent 上实现 encodeXXX 和 decode 方法,实际上可以在没有渲染器的情况下做到这一点。这对于较小的组件可能很有效,对于我们的例子肯定也是如此,但是这种方法是不可伸缩的。呈现器的目的是将组件逻辑从用户界面的呈现中分离出来。此外,单个组件可以具有多个呈现器,用于在不同的客户端设备上创建不同的呈现。为桌面 web 浏览器呈现的标记可能与为移动 web 浏览器呈现的标记不同。因此,即使你有一个像我们的 RandomText 组件这样的小组件,最好还是把渲染器从 UIComponent 中分离出来。图 6-4 显示了抽象渲染器类,它必须被扩展以实现 RandomText 组件的渲染器。
图 6-4 。抽象渲染器类,必须对其进行扩展才能为自定义组件创建渲染器
在渲染器中覆盖的关键方法在表 6-2 中列出。
表 6-2 。为自定义组件创建渲染器时要重写的主要方法
|
方法
|
描述
|
| --- | --- |
| Decode
| 为当前请求解码自定义组件上的任何新状态。当您希望接收来自用户的输入时,重写此方法。 |
| encodeBegin
| 将自定义组件的开头呈现给响应流。如果希望对子组件进行编码,并且希望在编码之前向用户输出响应,请重写此方法。 |
| encodeChildren
| 呈现自定义组件的子组件。当您想要更改子组件的编码方式时,请重写此方法。默认情况下,子组件使用各自的渲染器递归编码。通常不需要重写此方法,除非您想要阻止子级或类似的编码。 |
| encodeEnd
| 将自定义组件的结尾呈现给响应流。这是最常见的重写方法。它是渲染器上调用的最后一个编码方法,通常是生成自定义标记并将其添加到响应流的地方。 |
RandomTextRenderer 将扩展 Renderer,并且像组件一样,我们将使用@FacesRenderer 注释对 Renderer 进行注释。该注释有两个强制属性:componentFamily 和 rendererType。组件系列用于指示渲染器针对哪个组件系列。渲染器类型是一个标识符,用于将渲染器与 TLD 中的组件进行匹配。步骤 4 将说明如何使用渲染类型和组件族匹配渲染和组件。
RandomTextComponent 不需要任何子组件,也不需要用户的任何输入。因此,唯一要重写的方法是 encodeEnd 方法。encodeEnd 方法必须输出一个包含类似于清单 6-5 中的简单标记的响应。
清单 6-5。 示例输出 RandomTextRenderer 的标记
<div id="unique-identifier-of-the-component">
... Random text generated by the component ...
</div>
既然我们知道了标记应该是什么样子,我们就可以实现呈现器了。清单 6-6 展示了 RandomTextRenderer 的实现。
清单 6-6。 实现 RandomTextRenderer.java
package com.apress.projsf2html5.chapter6.components;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.FacesRenderer;
import javax.faces.render.Renderer;
@FacesRenderer(componentFamily = RandomTextComponent.COMPONENT_FAMILY, rendererType = RandomTextRenderer.RENDERER_TYPE)
public class RandomTextRenderer extends Renderer {
/** Renderer type of {@link RandomTextRenderer}. */
public static final String RENDERER_TYPE = "com.apress.projsf2html5.components.RandomTextRenderer";
private static final Logger LOG = Logger.getLogger(RandomTextRenderer.class.getName());
@Override
public void encodeEnd(FacesContext context, UIComponent uicomponent) throws IOException {
ResponseWriter writer = context.getResponseWriter();
RandomTextComponent component = (RandomTextComponent) uicomponent;
try {
writer.startElement("div", component);
writer.writeAttribute("id", component.getClientId(), "id");
try {
writer.write(component.getRandomText());
} catch (IOException randomTextException) {
writer.write(randomTextException.getMessage());
LOG.log(Level.SEVERE, "Could not generate random text", randomTextException);
}
writer.endElement("div");
} catch (IOException ex) {
LOG.log(Level.SEVERE, "Could not generate markup", ex);
}
}
}
使用 FacesContext 上的 ResponseWriter,输出所需的标记相当简单。ResponseWriter 包含启动新元素、向现有元素添加属性和结束现有元素的方法。我们需要提供的只是元素或属性的名称和值,以及它们属于哪个组件。为了创建
元素,我们使用 startElement 方法。当元素尚未关闭时,可以使用 writeAttribute 方法添加属性。若要在元素中添加文本,可以使用 write 或 writeText 方法。writeText 将对字符串中的任何 HTML 进行转义,而 write 将转储给定的内容而不进行转义。最后,可以使用 endElement 方法关闭当前打开的元素。在输出中可以包含任意数量的元素和嵌套元素,但是最好将组件放在一个 div 中,并将 ID 属性设置为组件的 clientId 属性。这使得定位组件以及使用 Ajax 更新组件变得容易。
注意当你从用户那里接收内容时,你应该总是使用 writeText,除非你相信用户不会给内容添加 HTML 和 Javascripts。
步骤 4—创建标签库描述符
TLD 的目的是将组件和渲染器作为标签定义和公开给 JSF 框架和页面作者。
TLD 文件用给定名称空间的所有标签定义了单个名称空间。该命名空间基于标准的 XML 命名空间,以避免当您拥有由不同开发人员提供的多个标记库时出现命名冲突。XML 名称空间是唯一的 URI,由标记库的供应商定义。在我们的例子中,我们选择了名称空间com . a press . projsf 2 html 5/random text
。要使用标签库,页面作者必须声明他想要使用名称空间和前缀来调用库。在清单 6-7 中可以看到一个声明使用带有 rt 前缀的标签库的例子。您可以为名称空间选择任何想要的前缀。
清单 6-7。 声明使用标签库
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:rt="http://com.apress.projsf2html5/randomtext">
...
...
</html>
定义了名称空间后,列出了名称空间中的每个标签。必须使用表 6-3 中列出的信息声明标签。
表 6-3 。TLD 中的标签详细信息
|
元素
|
描述
|
| --- | --- |
| <tag>
| 包围单个标签的外部元素 |
| <tag-name />
| 标签的名称 |
| <component>
| 包含标签中组件详细信息的外部元素 |
| <component-type />
| 标记中包含的组件类型。必须与 UIComponent 实现中设置的组件类型相匹配 |
| <renderer-type />
| 用于呈现标记的呈现器类型。必须与渲染器实现中设置的渲染器类型相匹配 |
| </component>
| |
| <attribute>
| 包含标签上单个属性详细信息的外部元素。对于应该向 JSF 框架和页面作者公开的所有属性,应该重复这一部分 |
| <name />
| 属性的名称 |
| <type />
| 属性的类型(例如 java.lang.String) |
| <method-signature / >
| 如果输入是方法而不是类型,则为方法签名 |
| <description />
| 属性的描述。这将出现在页面作者的代码帮助中 |
| <required / >
| 确定属性是否必需的布尔值 |
| <display-name / >
| ide 使用的属性的用户友好名称 |
| <icon / >
| ide 使用的属性的图形表示 |
| </attribute>
| |
| </tag>
| |
完整的 TLD 文件可以在清单 6-8 中看到。
清单 6-8。/we b-INF/randomtext . taglib . XML
<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib
FontName">http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
version="2.0">
<namespace>http://com.apress.projsf2html5/randomtext</namespace>
<tag>
<tag-name>randomtext</tag-name>
<component>
<component-type>RandomText</component-type>
<renderer-type>com.apress.projsf2html5.components.RandomTextRenderer</renderer-type>
</component>
<attribute>
<name>textType</name>
<type>java.lang.String</type>
<description>Type of random text to generate. Either gibberish or lorem</description>
<required>true</required>
</attribute>
<attribute>
<name>outputTag</name>
<type>java.lang.String</type>
<description>Type of HTML to generate. Either p for paras or ul for list.</description>
</attribute>
<attribute>
<name>count</name>
<type>java.lang.Integer</type>
<description>Number of paragraphs or items to return</description>
<required>true</required>
</attribute>
<attribute>
<name>minWords</name>
<type>java.lang.Integer</type>
<description>Minimum number of words to return in each para or item.</description>
</attribute>
<attribute>
<name>maxWords</name>
<type>java.lang.Integer</type>
<description>Maximum number of words to return in each para or item.</description>
</attribute>
</tag>
</facelet-taglib>
将 randomtext.taglib.xml 文件放在/WEB-INF 目录中。将文件放在此目录中并不会使其自动被 JSF 实现发现。我们必须首先告诉 JSF 实现通过 web.xml 中的 javax.faces.FACELETS_LIBRARIES 上下文参数来查看 taglib 文件;参见清单 6-9 。
清单 6-9。 /WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
<context-param>
<param-name>javax.faces.FACELETS_LIBRARIES</param-name>
<param-value>/WEB-INF/randomtext.taglib.xml</param-value>
</context-param>
<welcome-file-list>
<welcome-file>faces/example.xhtml</welcome-file>
</welcome-file-list>
</web-app>
我们现在已经实现了组件模型、定制组件、定制组件呈现器和组件的 TLD。最终组件的类图见图 6-5 。最后,我们必须实现一个简单的应用来测试组件是否按预期工作。
图 6-5 。组成 RandomText 自定义组件的类和文件的 UML 图
使用 RandomText 组件的示例
为了演示 RandomText 组件,我们将创建一个应用,其中组件属性可以通过输入控件来控制;参见图 6-6 。
图 6-6 。用于测试 RandomText 组件的输入控件
用户将能够选择要生成的文本类型(文本类型)、如何格式化输出(输出标签)、应该输出多少生成的文本(计数)以及生成的文本的每个块中的最小和最大字数(最小字数和最大字数)。单击“生成”按钮将调用组件,并根据输入控件中的值生成文本。
我们需要一个托管 bean 来保存输入控件中的值。被管理的 bean 可以在清单 6-10 中看到。
清单 6-10。example . Java—会话范围的受管 Bean,用于保存输入控件的值
package com.apress.projsf2html5.chapter6.beans;
import javax.inject.Named;
import javax.enterprise.context.SessionScoped;
import java.io.Serializable;
@Named(value = "example")
@SessionScoped
public class Example implements Serializable {
private String textType = "gibberish";
private String outputTag = "p";
private Integer count = 10;
private Integer minWords = 5;
private Integer maxWords = 10;
public String getTextType() {
return textType;
}
public void setTextType(String textType) {
this.textType = textType;
}
public String getOutputTag() {
return outputTag;
}
public void setOutputTag(String outputTag) {
this.outputTag = outputTag;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getMinWords() {
return minWords;
}
public void setMinWords(Integer minWords) {
this.minWords = minWords;
}
public Integer getMaxWords() {
return maxWords;
}
public void setMaxWords(Integer maxWords) {
this.maxWords = maxWords;
}
}
受管 bean 是一个普通会话范围的 bean,为每个输入控件提供 getters 和 setters,以显示给用户。带有输入控件的 Facelets 视图可以在清单 6-11 中看到。清单还展示了定制组件的用法,首先在 rt XML 前缀下声明它的用法,然后调用组件并设置其 ID。所有其他属性都是从支持 bean 中复制的。当调用“Generate”按钮时,backing bean 上的值将通过 Ajax 刷新。Ajax 组件告诉页面执行表单上所有的模型视图更新,然后通过其客户端 ID (rt1)呈现 RandomText 组件。
清单 6-11。 Facelets 视图演示了 RandomText 组件
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:rt="http://com.apress.projsf2html5/randomtext">
<h:head>
<title>RandomText Component Demo</title>
</h:head>
<h:body>
<h1>Example page for the RandomText component</h1>
<h:form>
<h:panelGrid columns="5">
<h:outputText value="Text Type:" />
<h:outputText value="Output Tag:" />
<h:outputText value="Count:" />
<h:outputText value="Min words:" />
<h:outputText value="Max words:" />
<h:selectOneMenu value="#{example.textType}">
<f:selectItem itemValue="gibberish" itemLabel="Gibberish" />
<f:selectItem itemValue="lorem" itemLabel="Lorem Ipsum" />
</h:selectOneMenu>
<h:selectOneMenu value="#{example.outputTag}">
<f:selectItem itemValue="p" itemLabel="Paragraph" />
<f:selectItem itemValue="ul" itemLabel="Unordered List" />
</h:selectOneMenu>
<h:inputText value="#{example.count}">
<f:convertNumber integerOnly="true" />
</h:inputText>
<h:inputText value="#{example.minWords}">
<f:convertNumber integerOnly="true" />
</h:inputText>
<h:inputText value="#{example.maxWords}">
<f:convertNumber integerOnly="true" />
</h:inputText>
<h:commandButton value="Generate">
<f:ajax render=":rt1" execute="@form" />
</h:commandButton>
</h:panelGrid>
</h:form>
<rt:randomtext id="rt1"
textType="#{example.textType}"
outputTag="#{example.outputTag}"
count="#{example.count}"
minWords="#{example.minWords}"
maxWords="#{example.maxWords}" />
</h:body>
</html>
点击生成按钮时的输出示例见图 6-7 和图 6-8 。
图 6-7 。RandomText 组件已经生成了 10 段单词长度在 8 到 13 之间的乱码
图 6-8 。RandomText 组件已经生成了 16 个单词长度在 2 到 15 之间的 Lorem Ipsum 列表项
包装组件
当您有了一个可重用的组件时,下一步自然是在多个项目中使用它,或者将它分发给其他开发人员供一般使用。把你的类放在包里。您必须重新定位的唯一文件是 randomtext.taglib.xml。将该文件放在包结构的/META-INF 目录中。确保 TLD 文件以. taglib.xml 结尾,这是 JSF 实现在检测外部标记库时正在搜索的内容。
清单 6-12。 文件结构用于封装组件
| src/main/java/com/apress/projsf2html5/chapter6/components/RandomTextAPI.java
| src/main/java/com/apress/projsf2html5/chapter6/components/RandomTextComponent.java
| src/main/java/com/apress/projsf2html5/chapter6/components/RandomTextRenderer.java
| src/main/resources/META-INF/randomtext.taglib.xml
摘要
在本章中,我们探讨了构成 JSF 组件架构的各种类和接口。我们提到了两个关键类,UIComponent 和 renderer,以及 TLD 文件。当把类和文件放在一起时,可以为任何目的产生可重用的定制组件。例如,我们开发了一个定制组件,它与 REST 网站对话,根据几个参数生成随机文本。
七、基本 JSF2 HTML5 组件
HTML5 引入了新的 web 表单元素来迎合常见的输入类型。这些新元素都没有 JSF 元素。在这一章中,我们将研究四种新的 HTML5 输入类型,并将它们实现为复合组件。本章中实现的输入类型是输入颜色、日期选择器、滑块和微调器类型。其他组件留给读者作为练习来实现。新输入类型的完整列表可在表 7-1 中找到。
表 7-1 。HTML5 中的新输入类型
输入颜色自定义组件
在本节中,我们将把 HTML5 颜色输入元素实现为一个复合组件。颜色输入的目的是允许用户使用浏览器内置的原生颜色选择器选择简单的颜色。表 7-2 概述了颜色输入元素可用的属性 。不是所有的浏览器都支持颜色选择器,在本章的最后,我们将实现一个后备,以防浏览器不支持这个特性。
表 7-2 。颜色输入元素支持的属性
|
属性
|
数据类型
|
| --- | --- |
| autocomplete
| 布尔代数学体系的 |
| list
| 字符串(对数据列表的引用) |
| value
| 字符串(具有 8 位红色、绿色和蓝色成分的 sRGB 颜色,例如#ff0000 代表红色) |
使用输入元素有两种主要方式:要么允许用户选择任何颜色,要么将颜色数量限制在预定义的颜色列表中。清单 7-1 展示了两者的一个例子。示例结果见图 7-1 。
清单 7-1。 使用 HTML5 颜色输入的例子
<section>
<label for="all-colors">All colors: </label>
<input id="all-colors" type="color" value="#00ff00" />
</section>
<section>
<label for="limit-colors">Limited colors: </label>
<input id="limit-colors" type="color" value="#00ff00" list="basic-colors" />
</section>
<datalist id="basic-colors">
<option value="#000000" label="Black" />
<option value="#ff0000" label="Red" />
<option value="#00ff00" label="Green" />
<option value="#0000ff" label="Blue" />
</datalist>
图 7-1 。HTML5 颜色输入。所有颜色将显示所有颜色的调色板,受限颜色将显示引用的数据列表中指定的调色板
创建复合组件
基于清单 7-1 中的例子,我们需要为颜色输入元素、数据列表元素和选项元素创建一个复合组件 。我们将从创建不使用数据列表的元素的基本版本开始。
清单 7-2。 基本颜色输入元素的复合组件(resources/projs html 5/Input Color . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="#000000" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="#{cc.id}" name="#{cc.id}"
jsf:value="#{cc.attrs.value}" type="color" />
</div>
</cc:implementation>
</html>
在接口中,组件公开了一个属性(值),所选的颜色将存储在该属性中。默认情况下,颜色设置为黑色(#000000)。在实现中,组件首先由一个普通的
包装,客户端 DOM ID 作为标识符。默认情况下,包装器什么也不做,但是当页面作者需要使用 CSS 来设计页面样式时,它可能会派上用场。在包装器内部,输出实际的颜色元素。通过使用 jsf 属性名称空间,我们告诉 Facelets 输入元素应该被视为 JSF 组件。jsf:value 属性的值引用接口中的 value 属性,以便可以设置和提取选定的颜色。
使用复合组件
使用复合组件 很容易,如清单 7-3 所示。导入复合组件的名称空间(即/resources 下存储组件的目录)。导入名称空间后,可以在<名称空间:componentFileName / >访问组件。
清单 7-3。 使用颜色输入复合构件
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:projsfhtml5="[`xmlns.jcp.org/jsf/composite/projsfhtml5`](http://xmlns.jcp.org/jsf/composite/projsfhtml5)">
<h:head>
<title>Input Color Custom Component</title>
</h:head>
<h:body>
<h1>Input Color Custom Component</h1>
<h:form id="frm">
<projsfhtml5:inputColor id="ic-favcolor" value="#{componentInputColor.color}" />
<h:commandButton value="Submit" />
<h:outputText id="selected-color" value="Color is: #{componentInputColor.color}" />
</h:form>
</h:body>
</html>
复合组件引用了清单 7-4 中受管 bean 上的一个普通字符串属性,名为 ComponentInputColor#color。点击提交按钮时,选择的颜色被保存在属性中进行处理,如图图 7-2 所示。
清单 7-4。【ComponentInputColor.java】代表存储所选颜色的支持 Bean
package projsfandhtml5.chapter7;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
@ManagedBean
@ViewScoped
public class ComponentInputColor {
private String color = "";
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
图 7-2 。运行中的颜色输入复合组件。用户通过单击颜色控件选择颜色,然后单击提交。所选颜色的代码显示在提交按钮旁边
支持列表
许多新的 HTML5 输入元素支持使用列表。列表的目的是限制给定控件中的用户选择。在本节中,我们将创建一个通用组件,它可以被任何支持 list 属性的输入元素重用。
该实现需要两个复合组件:一个表示
清单 7-5。 表示嵌套的<选项>标签的复合组件(resources/projsfhtml 5/option . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="" />
<cc:attribute name="label" type="java.lang.String" default="" />
</cc:interface>
<cc:implementation>
<option value="#{cc.attrs.value}" label="#{cc.attrs.label}" />
</cc:implementation>
</html>
option composite 组件在其接口中定义了两个属性:一个属性用于标签,一个属性用于值。label 的目的是提供一个用户友好的值表示,该值是在后台使用的机器友好的值。
清单 7-6。 复合组件,表示颜色输入将为选项引用的外部数据列表(resources/projsfhtml 5/Datalist . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/
xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite">
<cc:interface>
</cc:interface>
<cc:implementation>
<datalist id="#{cc.id}">
<cc:insertChildren />
</datalist>
</cc:implementation>
</html>
关于 datalist 组件,值得注意的一点是,它使用复合组件的 ID 作为自己的 ID。这意味着输入组件可以使用 datalist 的 ID 引用 datalist,而无需考虑名称空间。另一件要注意的事情是,它将嵌套在复合组件中的所有内容插入到 datalist 元素中。
清单 7-7。 使用 Datalist 和 Option 复合组件。选项嵌套在数据列表中
<projsfhtml5:dataList id="available-colors">
<projsfhtml5:option value="#ff0000" label="Red"/>
<projsfhtml5:option value="#00ff00" label="Green"/>
<projsfhtml5:option value="#0000ff" label="Blue"/>
</projsfhtml5:dataList>
清单 7-8。 HTML 从清单 7-7 中的复合组件输出
<datalist id="available-colors">
<option value="#ff0000" label="Red"></option>
<option value="#00ff00" label="Green"></option>
<option value="#0000ff" label="Blue"></option>
</datalist>
剩下的惟一事情是支持 inputColor 组件中的列表。这是通过在接口中引入一个名为 list 的新属性来实现的,并在 color 元素的实现中引用该属性。
清单 7-9。 在 inputColor 组件中实现对列表的支持(resources/projs html 5/input color . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="#000000" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:clientBehavior name="change"
event="change"
targets="#{cc.id}" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="#{cc.id}" name="#{cc.id}" jsf:value="#{cc.attrs.value}" type="color"
list="#{cc.attrs.list}"/>
</div>
</cc:implementation>
</html>
Ajax——启用组件
JSF 2 引入了带有< f:ajax/ >标签的原生 Ajax 请求 。您可以在复合组件中使用 Ajax 标记,方法是声明您想要广播的事件以及该事件来自复合组件内部的何处。使用 clientBehavior 标记在复合组件的接口中完成公告。
清单 7-10。 当颜色输入的值改变时,广播一个事件进行 Ajax 处理
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="#000000" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:clientBehavior name="change" targets="#{cc.id}" event="change" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="#{cc.id}" name="#{cc.id}" jsf:value="#{cc.attrs.value}" type="color"
list="#{cc.attrs.list}"/>
</div>
</cc:implementation>
</html>
clientBehavior 有三个属性:name,包含可以在组件外部监听的事件的名称;目标,包含被监视组件的列表;和 event,JavaScript 事件的名称(没有 on 位,例如 onchange 事件应该是 change)被捕获并转发给监听该类型事件的任何人。
清单 7-11。 监听变更事件,执行并呈现组件和输出文本
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:projsfhtml5="http://xmlns.jcp.org/jsf/composite/projsfhtml5"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
<title>Input Color Custom Component with DataList and Ajax support</title>
</h:head>
<h:body>
<h1>Input Color Custom Component with DataList and Ajax support</h1>
<h:form id="frm">
<projsfhtml5:inputColor id="ic-favcolor" value="#{componentInputColor.color}"
list="available-colors">
<f:ajax event="change" render=":frm:selected-color" execute="@this " />
</projsfhtml5:inputColor>
<projsfhtml5:dataList id="available-colors">
<projsfhtml5:option value="#ff0000" label="Red"/>
<projsfhtml5:option value="#00ff00" label="Green"/>
<projsfhtml5:option value="#0000ff" label="Blue"/>
</projsfhtml5:dataList>
<h:commandButton value="Submit" />
<h:outputText id="selected-color" value="Color is: #{componentInputColor.color}" />
</h:form>
</h:body>
</html>
使用 inputColor 组件接口中的 clientBehavior,您可以用一个标签来嵌套它,在这里您可以监听广播的事件。
不支持的浏览器的回退
如果您在不支持 HTML5 颜色输入的浏览器中尝试了上一章中的示例,您将在屏幕上看到一个文本字段,如图图 7-3 所示,而不是一个颜色选择器。
图 7-3 。在不支持的浏览器上呈现的颜色输入
支持回退要求能够检测浏览器是否支持给定的特性。如果它不支持自定义组件提供的功能,则应提供替代显示。
在回退示例清单 7-12 中,我们首先检查浏览器是否支持颜色输入。如果不支持颜色输入,我们使用 jscolor 库(在 www.jscolor.com 的可以免费获得)提供一个后备,如图图 7-4 所示。
清单 7-12。 支持回退到 JavaScript 颜色选择器
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="value" type="java.lang.String" default="#000000" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:clientBehavior name="change" targets="#{cc.id}" event="change" />
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:outputScript library="js" name="jscolor.js" />
<div id="#{cc.clientId}">
<input jsf:id="#{cc.id}" jsf:value="#{cc.attrs.value}" type="color"
list="#{cc.attrs.list}"/>
<script type="text/javascript">
function html5_supports_input(type) {
var i = document.createElement("input");
i.setAttribute("type", type);
return i.type === type;
}
if (!html5_supports_input('color')) {
// The color input is not supported on the browser.
// Provide an alternative way of rendering the color picker,
// e.g. jscolor ([`jscolor.com/`](http://jscolor.com/))
var componentId = '${cc.clientId}:${cc.id}'.replace(/:/g, "\\:");
new jscolor.color(document.getElementById('${cc.clientId}:${cc.id}'), {})
}
</script>
</div>
</cc:implementation>
</html>
图 7-4 。inputColor 组件的回退版本 ??
提示您可以使用像 Modernizr(www.modernizr.com)这样的 JavaScript 库来检测 HTML5 和 CSS3 的本地实现的可用性,而不是实现您自己的 HTML5 特性检测算法。
输入日期选择器自定义组件
在本节中,我们将把 HTML5 日期选择器输入元素实现为一个复合组件。日期选择器输入的目的是允许用户使用浏览器内置的本地日期选择器选择日期。不是所有的浏览器都支持日期选择器,所以像颜色选择器一样,我们将提供一个后备,这次使用 JQuery-UI。像颜色输入组件一样,日期输入组件也支持 change 事件,我们将捕捉并广播到支持 Ajax 的组件。
表 7-3 包含日期输入元素支持的属性列表。我们将在复合组件中实现所有属性。如何使用属性的例子可以在清单 7-13 中找到。
表 7-3 。日期输入元素支持的属性
|
属性
|
数据类型
|
| --- | --- |
| Autocomplete
| 布尔代数学体系的 |
| List
| 字符串(对数据列表的引用) |
| Value
| 日期字符串(年-月数-日数,例如 1970-01-10 是 1970 年 1 月 10 日日 |
| Max
| 日期字符串(用户可以选择的最晚可能日期) |
| Min
| 日期字符串(用户可以选择的最早日期) |
| Readonly
| 布尔代数学体系的 |
| Required
| 布尔代数学体系的 |
| Step
| 整数(每步改变的天数) |
清单 7-13。 利用 HTML5 输入日期的例子
<section>
<label for="without-value">Date (without a preset date): </label>
<input id="withouth-value" type="date" value="" />
</section>
<section>
<label for="with-value">Date (with a preset date): </label>
<input id="with-value" type="date" value="2013-05-03" step="10" />
</section>
<section>
<label for="with-constraints">Date (with a constraints): </label>
<input id="with-constraints" type="date" value="2013-05-03"
min="2013-05-01" max="2013-05-31" />
</section>
<section>
<label for="readonly">Date (readonly): </label>
<input id="readonly" type="date" value="2013-05-03"
readonly />
</section>
<section>
<label for="with-list">Date (with list of dates): </label>
<input id="with-list" type="date" value="" list="available-dates" />
</section>
<datalist id="available-dates">
<option value="2013-01-01" label="1st Option" />
<option value="2013-03-10" label="2nd Option" />
<option value="2013-06-19" label="3rd Option" />
<option value="2013-10-10" label="4th Option" />
</datalist>
</section>
创建复合组件
基于清单 7-13 中的 HTML5 示例,我们可以重用颜色输入组件中的 datalist 和 option 组件的实现。请注意,HTML5 中输入的日期值是一个年-月-日格式的字符串(例如 2013-04-20)。为了避免不必要的数据转换,我们将使用日期/时间转换器,以便组件的值可以设置为 java.util.Date 。
清单 7-14 显示了一个支持 HTML5 中指定属性的基本实现。组件的值是一个 java.util.Date,并使用 input 元素中嵌套的< f:convertDateTime / >转换器自动转换。required 和 readonly 属性具有 jsf 等效项,因此通过在属性前添加 JSF 前缀(即 JSF:readonly = " # { cc . attrs . readonly } ")将它们直接映射到 JSF。您可能已经注意到,min 和 max 属性并没有从接口直接映射到实现。不可能将转换器应用于属性值。这意味着我们不能简单地将接口中 min 属性的 java.util.Date 值赋给实现中的 min 属性。如果我们直接映射属性,则实现中 min 属性的值将呈现为 java.util.Date 值的“toString()”表示,其形式为:星期几+月名+月日+小时:分钟:秒+时区+年,例如,星期六 Jun 15 11:24:21 CET 2013。此格式与 HTML5 指定的格式(即年-月-日)不一致。因为我们不能在属性值上应用转换器,所以我们必须为负责数据对话的组件实现一个后备 bean。
清单 7-14。 复合组件为 inputDate 组件(resources/projs html 5/input date . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface>
<cc:attribute name="value" type="java.util.Date" required="true" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.util.Date" />
<cc:attribute name="max" type="java.util.Date" />
<cc:attribute name="readonly" type="java.lang.String" default="false" />
<cc:attribute name="required" type="java.lang.String" default="false" />
<cc:clientBehavior name="change" targets="date" event="change" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="date"
type="date"
jsf:value="#{cc.attrs.value}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true' : 'false'}"
jsf:required="#{cc.attrs.required != 'false' ? 'true' : 'false'}"
step="#{cc.attrs.step}"
list="#{cc.attrs.list}">
<f:convertDateTime pattern="yyyy-MM-dd" />
</input>
</div>
</cc:implementation>
</html>
为复合组件创建支持 Bean
复合组件支持 bean 必须扩展 javax . faces . component . uinamingcontainer。按照惯例,通过将支持 bean 命名为与复合组件相同的名称,并将支持 bean 放在与复合组件所在目录同名的包中,可以自动将支持 bean 映射到复合组件。或者,可以用@FacesComponent 注释来注释支持 bean,并将组件类型指定为注释的值。然后可以在复合组件的接口中指定组件类型。手动映射支持 bean 的好处是,其他复合组件可以重用支持 bean,并且不必将支持 bean 命名为与复合组件相同的名称。
清单 7-15 显示了一个为 inputDate 类型的复合组件声明的后备 bean 的基本示例。通过将接口的 componentType 属性设置为@FacesComponent 注释中声明的名称,复合组件映射到支持 bean,例如<cc:interface component type = " input date ">。
清单 7-15。 为 inputDate 组件声明的空支持 Bean
package projsfhtml5;
import javax.faces.component.FacesComponent;
import javax.faces.component.UINamingContainer;
@FacesComponent("inputDate")
public class InputDateComponent extends UINamingContainer {
}
为了支持 min 和 max 属性,我们需要 backing bean 上的两个属性:一个用于保存最小日期的字符串表示,另一个用于保存最大日期的字符串表示。我们将属性命名为 minDate 和 maxDate。通过为这两个属性提供 getters,它们将自动对复合组件可用。然后,我们将覆盖 encodeBegin 方法,从接口中提取 java.util.Dates,并将它们转换为可以在实现中使用的 HTML5 格式的日期。
清单 7-16。 Backing Bean 转换并暴露最小和最大日期
package projsfhtml5;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import javax.faces.component.FacesComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
@FacesComponent("inputDate")
public class InputDateComponent extends UINamingContainer {
private static final DateFormat HTML5_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
private String minDate = "";
private String maxDate = "";
@Override
public void encodeBegin(FacesContext context) throws IOException {
// Extract the minimum date from the interface
java.util.Date attrsMin = (java.util.Date) getAttributes().get("min");
if (attrsMin != null) {
// Convert the date to an HTML5 date
minDate = HTML5_FORMAT.format(attrsMin);
}
// Extract the maximum date from the interface
java.util.Date attrsMax = (java.util.Date) getAttributes().get("max");
if (attrsMax != null) {
// Convert the date to an HTML5 date
maxDate = HTML5_FORMAT.format(attrsMax);
}
super.encodeBegin(context);
}
/**
* Gets the minimum date selectable in the date picker.
*
* @return Date formatted using the {@link inputDate#HTML5_FORMAT}
*/
public String getMinDate() {
return minDate;
}
/**
* Gets the maximum date selectable in the date picker.
*
* @return Date formatted using the {@link inputDate#HTML5_FORMAT}
*/
public String getMaxDate() {
return maxDate;
}
}
使用复合组件
有了后备 bean,我们就可以访问包含转换后的日期的 minDate 和 maxDate 属性,并将它们插入到复合组件的实现中。
清单 7-17。 复合组件使用 Backing Bean 并访问转换后的值
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface componentType="inputDate">
<cc:attribute name="value" type="java.util.Date" required="true" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.util.Date" />
<cc:attribute name="max" type="java.util.Date" />
<cc:attribute name="readonly" type="java.lang.String" default="false" />
<cc:attribute name="required" type="java.lang.String" default="false" />
<cc:clientBehavior name="change" targets="date" event="change" />
</cc:interface>
<cc:implementation>
<div id="#{cc.clientId}">
<input jsf:id="date"
type="date"
jsf:value="#{cc.attrs.value}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true' : 'false'}"
jsf:required="#{cc.attrs.required != 'false' ? 'true' : 'false'}"
step="#{cc.attrs.step}"
min="#{cc.minDate}"
max="#{cc.maxDate}"
list="#{cc.attrs.list}">
<f:convertDateTime pattern="yyyy-MM-dd" />
</input>
</div>
</cc:implementation>
</html>
有了复合组件和后台 bean,我们就可以使用该组件了。清单 7-18 显示了一些示例,并附有图 7-5 中的截图。
清单 7-18。 使用 inputDate 复合组件的例子
<h:form>
<h1>JSF Component</h1>
<section>
<label>Date (with a null java.util.Date): </label>
<projsfhtml5:inputDate value="#{componentInputDate.emptyDate}" />
<h:outputLabel value="Selected date:" />
<h:outputText value="#{componentInputDate.emptyDate}">
<f:convertDateTime dateStyle="short" />
</h:outputText>
</section>
<section>
<label>Date (with a java.util.Date): </label>
<projsfhtml5:inputDate value="#{componentInputDate.selectedDate}"
step="10"
min="#{componentInputDate.minDate}"
max="#{componentInputDate.maxDate}" />
<h:outputLabel value="Selected date:" />
<h:outputText value="#{componentInputDate.selectedDate}">
<f:convertDateTime dateStyle="short" />
</h:outputText>
</section>
<section>
<label>Date (readonly): </label>
<projsfhtml5:inputDate value="#{componentInputDate.selectedDate}"
readonly="true" />
</section>
<section>
<label>Date (with a list of dates): </label>
<projsfhtml5:inputDate value="#{componentInputDate.selectedDate2}"
list="available-dates" />
<projsfhtml5:dataList id="available-dates">
<projsfhtml5:option label="1st Option" value="2012-01-01" />
<projsfhtml5:option label="2nd Option" value="2012-01-02" />
<projsfhtml5:option label="3rd Option" value="2012-01-03" />
</projsfhtml5:dataList>
<h:outputLabel value="Selected date:" />
<h:outputText value="#{componentInputDate.selectedDate2}">
<f:convertDateTime dateStyle="short" />
</h:outputText>
</section>
<h:commandButton value="Submit" />
</h:form>
图 7-5 。运行中的<项目 html5:输入日期/ >组件
不支持的浏览器的回退
不支持 HTML 日期输入的浏览器会呈现一个带有日期的纯文本字段,就像上一节中的颜色输入一样。为了提供后备,我们可以再次使用 JavaScript 来检查浏览器是否支持该特性,如果不支持,我们可以提供控件的替代显示。在下面的例子中,我们使用 Modernizr 检查浏览器是否支持该特性,如果不支持,我们使用 JQuery-UI 的 DatePicker 组件,如图 7-6 所示。
图 7-6 。如果浏览器不支持 HTML5 日期输入,则返回 JQuery-UI
清单 7-19。 使用 JQuery-UI 和 Modernizr 支持回退
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface componentType="inputDate">
<cc:attribute name="value" type="java.util.Date" required="true" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.util.Date" />
<cc:attribute name="max" type="java.util.Date" />
<cc:attribute name="readonly" type="java.lang.String" default="false" />
<cc:attribute name="required" type="java.lang.String" default="false" />
<cc:clientBehavior name="change" targets="date" event="change" />
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="css" name="jquery-ui.css" />
<h:outputScript target="head" library="js" name="modernizr.js" />
<h:outputScript target="head" library="js" name="jquery-1.9.1.js" />
<h:outputScript target="head" library="js" name="jquery-ui.js" />
<script type="text/javascript">
if (!Modernizr.inputtypes.date) {
jQuery(function() {
var id = '${cc.clientId}:date'.replace(/:/g, "\\:");
jQuery("#" + id).datepicker({ dateFormat: 'yy-mm-dd' });
});
}
</script>
<div id="#{cc.clientId}">
<input jsf:id="date" type="date" jsf:value="#{cc.attrs.value}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true' : 'false'}"
jsf:required="#{cc.attrs.required != 'false' ? 'true' : 'false'}"
step="#{cc.attrs.step}" min="#{cc.minDate}" max="#{cc.maxDate}"
list="#{cc.attrs.list}">
<f:convertDateTime pattern="yyyy-MM-dd" />
</input></div>
</cc:implementation>
</html>
注意在复合组件中使用< h:outputScript/ >和< h:outputStylesheet/ >的一个好处是,即使在一个页面中多次使用该组件,它也只会被包含一次。
滑块自定义组件
在本节中,我们将把 HTML5 range input 元素实现为一个复合组件。范围输入的目的是允许用户使用浏览器内置的本地滑块控件在数字范围内选择一个值。不是所有的浏览器都支持范围输入,所以像日期选择器一样,我们将使用 JQuery-UI 提供一个后备。我们还将支持 Ajax 事件,因为它们对于滑块向用户提供关于所选值的即时反馈特别有用。
表 7-4 包含范围输入元素支持的属性列表。我们将在复合组件中实现所有属性。如何使用属性 的例子可以在清单 7-20 中找到
表 7-4 。范围输入元素支持的属性
|
属性
|
数据类型
|
| --- | --- |
| autocomplete
| 布尔代数学体系的 |
| list
| 字符串(对数据列表的引用) |
| value
| 代表范围中所选值的数字 |
| max
| 范围控件中的上限 |
| min
| 范围控件的下限 |
| step
| 使用范围控制时要步进的数字 |
清单 7-20。 使用 HTML5 范围输入的例子
<section>
<label for="without-value">Range (without value): </label>
<input id="withouth-value" type="range" />
</section>
<section>
<label for="min-to-max">Range (-10 to 10): </label>
<input id="min-to-max" type="range" min="-10" max="10" value="0" />
</section>
<section>
<label for="range-list">Range (list): </label>
<input id="range-list" type="range" value="0" list="list" />
</section>
<datalist id="list">
<option value="-100" />
<option value="-75" />
<option value="-50" />
<option value="-25" />
<option value="0" />
</datalist>
创建复合组件
范围组件的实现非常简单。相关的属性被添加到接口中,同时广播 JavaScript change 事件。该实现包含 JavaScript 库,用于在浏览器不支持 HTML5 输入范围控件的情况下进行后备。它还包括内联 JavaScript 检查输入范围控件是否受支持,如果不受支持,则使用 JQuery 和 JQuery UI 将
元素转换为范围控件。这个组件中的内联 JavaScript 比前面的组件稍微复杂一些。这是因为 JQuery UI 中的 slider 控件不是应用于一个元素,而是应用于一个元素。这意味着每次移动滑块时,我们都需要更新元素并触发一个 change 事件,以保持功能与原生 HTML5 版本一致。
清单 7-21。 用于 inputRange 的复合组件(resources/projs html 5/input range . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.lang.String" />
<cc:attribute name="max" type="java.lang.String" />
<cc:clientBehavior name="change" targets="range" event="change" />
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="css" name="jquery-ui.css" />
<h:outputScript target="head" library="js" name="modernizr.js" />
<h:outputScript target="head" library="js" name="jquery-1.9.1.js" />
<h:outputScript target="head" library="js" name="jquery-ui.js" />
<script type="text/javascript">
if (!Modernizr.inputtypes.range) {
jQuery(function() {
var rangeId = '${cc.clientId}:range'.replace(/:/g, "\\:");
var hideId = '${cc.clientId}'.replace(/:/g, "\\:");
jQuery("#" + hideId).hide();
var id = '${cc.clientId}:fallback'.replace(/:/g, "\\:");
jQuery("#" + id).slider({
min: #{cc.attrs.min},
max: #{cc.attrs.max},
step: #{cc.attrs.step},
slide: function(event, ui) {
// Update the value of the input element and fire a change event
jQuery("#" + rangeId).val(ui.value).change();
}});
});
}
</script>
<!-- Div used for fallback in case the HTML5 range control is not supported -->
<div id="#{cc.clientId}:fallback" style="display: block"></div>
<div id="#{cc.clientId}">
<input jsf:id="range" type="range" jsf:value="#{cc.attrs.value}"
step="#{cc.attrs.step}" min="#{cc.attrs.min}" max="#{cc.attrs.max}"
list="#{cc.attrs.list}" />
</div>
</cc:implementation>
</html>
使用复合组件
清单 7-22 展示了一些复合控件的例子,并附带了图 7-7 (原生 HTML5 支持)和图 7-8 (回退)中的截图。
清单 7-22。 使用 inputRange 复合组件的例子
<h1>JSF Composite Control</h1>
<section>
<label for="min-to-max">Range (-10 to 10): </label>
<projsfhtml5:inputRange value="#{componentInputRange.range1}" min="-10" max="10">
<f:ajax event="change" execute="@this" render=":frm:selectedValue" />
</projsfhtml5:inputRange>
<h:outputText id="selectedValue" value="Selected value: #{componentInputRange.range1}"/>
</section>
<section>
<label for="with-list">Range (with List): </label>
<projsfhtml5:inputRange value="#{componentInputRange.range2}" min="-100" max="10" list="range-options">
<f:ajax event="change" execute="@this" render=":frm:selectedValue2" />
</projsfhtml5:inputRange>
<h:outputText id="selectedValue2" value="Selected value: #{componentInputRange.range2}"/>
<projsfhtml5:dataList id="range-options">
<projsfhtml5:option value="-100" />
<projsfhtml5:option value="-75" />
<projsfhtml5:option value="-50" />
<projsfhtml5:option value="-25" />
<projsfhtml5:option value="0" />
<projsfhtml5:option value="10" />
</projsfhtml5:dataList>
</section>
<h:commandButton value="Submit" />
图 7-7 。原生 HTML5 支持
图 7-8 。回退到 JQueryUI 滑块
微调器自定义组件
最后,我们将把 HTML5 数字输入元素实现为一个复合组件。数字输入的目的是允许用户使用微调控件在数字范围内选择一个值,用户可以单击向上或向下,之后所选的值递增或递减。像所有其他控件一样,所有浏览器还不支持数字控件,所以我们将再次使用 JQuery-UI 提供一个后备。
表 7-5 包含数字输入元素支持的属性列表。我们将在复合组件中实现所有属性。如何使用属性 的例子可以在清单 7-23 中看到。
表 7-5 。数字输入元素支持的属性
|
属性
|
数据类型
|
| --- | --- |
| Autocomplete
| 布尔代数学体系的 |
| List
| 字符串(对数据列表的引用) |
| value
| 代表所选值的数字 |
| max
| 微调控制项中的上限 |
| min
| 微调控制项的下限 |
| step
| 使用微调控件时要步进的数字 |
| readonly
| 布尔代数学体系的 |
| required
| 布尔代数学体系的 |
清单 7-23。 利用 HTML5 输入数字的例子
<section>
<label for="without-value">Number (without value): </label>
<input id="withouth-value" type="number" />
</section>
<section>
<label for="min-to-max">Number (-10 to 10): </label>
<input id="min-to-max" type="number" min="-10" max="10" value="0" />
</section>
<section>
<label for="preselect-list">Range (list): </label>
<input id="preselect-list" type="number" value="0" list="list" />
</section>
<datalist id="list">
<option value="-100" />
<option value="-75" />
<option value="-50" />
<option value="-25" />
<option value="0" />
</datalist>
创建复合组件
数字组件的实现类似于范围组件。相关的属性被添加到接口中,同时广播 JavaScript change 事件。该实现包含 JavaScript 库,用于在浏览器不支持 HTML5 输入数字控件的情况下进行后备。它还包括内联 JavaScript 检查是否支持输入数字控件,如果不支持,则使用 JQuery 和 JQuery UI 将输入字段转换为微调控件。每当 spinner 控件用于确保 Ajax 调用被广播时,还会挂接一个事件来触发一个 change 事件。
清单 7-24。 复合组件为 input number(resources/projs html 5/input number . XHTML)
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/
xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface>
<cc:attribute name="value" type="java.lang.String" />
<cc:attribute name="list" type="java.lang.String" default="" />
<cc:attribute name="step" type="java.lang.String" default="1" />
<cc:attribute name="min" type="java.lang.String" />
<cc:attribute name="max" type="java.lang.String" />
<cc:attribute name="readonly" type="java.lang.String" default="false" />
<cc:attribute name="required" type="java.lang.String" default="false" />
<cc:clientBehavior name="change" targets="number" event="change" />
</cc:interface>
<cc:implementation>
<h:outputStylesheet library="css" name="jquery-ui.css" />
<h:outputScript target="head" library="js" name="modernizr.js" />
<h:outputScript target="head" library="js" name="jquery-1.9.1.js" />
<h:outputScript target="head" library="js" name="jquery-ui.js" />
<script type="text/javascript">
if (!Modernizr.inputtypes.number) {
jQuery(function() {
var id = '${cc.clientId}:number'.replace(/:/g, "\\:");
jQuery("#" + id).spinner();
jQuery('.ui-spinner-button').click(function() {
jQuery(this).siblings('input').change();
});
});
}
</script>
<div id="#{cc.clientId}">
<input jsf:id="number" type="number" jsf:value="#{cc.attrs.value}"
step="#{cc.attrs.step}" min="#{cc.attrs.min}" max="#{cc.attrs.max}"
list="#{cc.attrs.list}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true' : 'false'}"
jsf:required="#{cc.attrs.required != 'false' ? 'true' : 'false'}"/>
</div>
</cc:implementation>
</html>
使用复合组件
清单 7-25 展示了复合控件 的一些例子,并附有截图图 7-9 (原生 HTML5 支持)和图 7-10 (回退)。
清单 7-25。 使用 inputNumber 复合组件的例子
<section>
<label for="min-to-max">Number (-10 to 10): </label>
<projsfhtml5:inputNumber value="#{componentInputNumber.number1}" min="-10" max="10">
<f:ajax event="change" execute="@this" render=":frm:selectedValue" />
</projsfhtml5:inputNumber>
<h:outputText id="selectedValue" value="Selected value: #{componentInputNumber.number1}"/>
</section>
<section>
<label for="with-list">Number (with List): </label>
<projsfhtml5:inputNumber value="#{componentInputNumber.number2}" min="-100" max="10"
list="number-options">
<f:ajax event="change" execute="@this" render=":frm:selectedValue2" />
</projsfhtml5:inputNumber>
<h:outputText id="selectedValue2" value="Selected value: #{componentInputNumber.number2}"/>
<projsfhtml5:dataList id="number-options">
<projsfhtml5:option value="-100" />
<projsfhtml5:option value="-75" />
<projsfhtml5:option value="-50" />
<projsfhtml5:option value="-25" />
<projsfhtml5:option value="0" />
<projsfhtml5:option value="10" />
</projsfhtml5:dataList>
</section>
<h:commandButton value="Submit" />
图 7-9 。原生 HTML5 支持
图 7-10 。回退到 JQueryUI 滑块
摘要
在这一章中,我们已经看到了将一些新的 HTML5 输入类型转换成 JSF 复合组件。在这一章中,我们研究了基本复合组件、嵌套组件和支持 Ajax 的组件的实现。复合组件为页面作者隐藏了不必要的逻辑。我们研究了如何检测浏览器是否支持我们输出的输入元素,如果不支持,就提供另一个视图。如果没有复合组件,您可能需要在每个页面上编写额外的 JavaScript 来检查兼容性。使用复合组件,所有的逻辑都包含在一个地方,很容易使用复合组件进行一个简单的更改,然后级联到所有的页面。
八、高级 JSF2 HTML5 组件
在前一章中,我们为 HTML5 中引入的一些新输入元素构建了 JSF2 组件。在本章中,我们将继续构建 JSF2 组件,这些组件利用了一些新的非输入 HTML5 元素。
媒体组件
随着移动设备使用的增加,HTML 的一个弱点变得越来越明显,那就是缺乏实现网页媒体播放器的标准化方法。在 HTML5 之前,网页作者必须使用 object 或 embed 元素通过 Java 小程序或 Flash SWFs 显示嵌入的媒体内容。如果你是一个经验丰富的网页作者,你应该知道要确保你的网页在所有浏览器上都能正常工作需要经历的所有麻烦。随着移动设备的出现,这个问题变得更加严重。尽管在过去十年中进行了各种 web 标准化努力,但您仍然必须在 web 代码中编写一些变通方法和故障转移,以确保它们在最流行的 web 浏览器和移动设备上运行良好。其中一个流行的移动平台是苹果的 iOS。早在 2010 年 4 月,史蒂夫·乔布斯写了一封公开信 1 解释苹果对 Flash 的想法,基本上是说 iOS 设备将不支持 Flash,未来应该使用新的开放标准(如 HTML5)来创建图形丰富的应用和游戏。
HTML5 中引入的媒体元素
用于播放视频和音频。HTML5 引入了四个新元素:音频、视频、源和轨道。音频和视频元素定义了视频或音频剪辑应该如何播放,以及网页访问者可以使用的控件。源元素嵌套在音频和视频元素中,可以插入源元素以提供媒体备选方案,web 浏览器可以根据其支持的媒体类型和编解码器从中进行选择。
注你可以在 W3C 网站www.w3.org/TR/html5/embedded-content-0.html#media-elements上找到 HTML5 规范中关于媒体元素的规范细节。
音频和视频元素都实现了 HTML5 规范中指定的 HTMLMediaElement 接口。该接口定义了回放音频和视频剪辑的通用属性和方法(见表 8-1 )。可以使用音频和视频元素的属性 设置可写属性。可以通过 JavaScript 在 DOM 中访问只读属性和方法。
表 8-1 。媒体元素(音频和视频)的通用属性列表
video 元素包含一些附加属性,用于指定视频和海报(如果有)的尺寸,以便在视频回放开始之前显示。附加视频属性在表 8-2 中列出。
表 8-2 。仅适用于视频元素的附加属性列表
在清单 8-1 中,你会看到一个 HTML5 中引入的音频和视频元素的例子。该示例还展示了如何使用 JavaScript 控制媒体元素。
清单 8-1。html 5 中引入的音频和视频元素的基本示例,可在不使用 Java 小程序或 Flash SWFs 等插件的情况下播放音频和视频剪辑
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<title>Media Element</title>
<style>
video, audio { display: block }
</style>
<script type="text/javascript">
// Obtaining the MediaElement via JavaScript and invoking methods
function togglePlay(source, elementId) {
var mediaElement = document.getElementById(elementId);
if (mediaElement.paused) {
mediaElement.play();
source.innerText = "Pause";
} else {
mediaElement.pause();
source.innerText = "Play";
}
}
</script>
</head>
<body>
<article>
<h2>Video Example</h2>
<video id="video-example"
controls="controls"
poster="media/poster.png"
src="media/trailer.mp4">
<!-- Provide fallback for browsers that doesn't support the video element -->
<p>Browser not support the HTML5 Video element.</p>
</video>
<button onclick="togglePlay(this, 'video-example');">Play</button>
</article>
<article>
<h2>Audio Example</h2>
<audio id="audio-example"
controls="controls"
src="media/04-Death_Becomes_Fur.mp4">
<!-- Provide fallback for browsers that doesn't support the audio element -->
<p>Browser not support the HTML5 Audio element.</p>
</audio>
<button onclick="togglePlay(this, 'audio-example');">Play</button>
</article>
</body>
</html>
不同的网络浏览器和设备支持不同的媒体格式。可以指定多个媒体源,而不是进行设备和浏览器检测并相应地更新 src 属性。浏览器可以根据其支持的格式和编解码器自由选择最合适的媒体源。指定源时,没有必要在音频或视频元素的 src 属性中指定媒体文件。最常支持的格式和编解码器是 WebM 格式(使用 VP8 视频编解码器和 Vorbis 音频编解码器)和 MP4 格式(使用 H.264 视频编解码器和 ACC 或 MP3 音频编解码器)。
清单 8-2 是一个为提供多个视频和音频源的例子,这些视频和音频源将根据访问页面的浏览器所支持的格式自动选择。
清单 8-2。 可提供多种媒体源,确保跨浏览器和设备的最佳播放效果
<video id="video-example" controls="controls" poster="media/poster.png">
<source src="media/trailer.mp4" type="video/mp4" />
<source src="media/trailer.webm" type="video/webm" />
<!-- Provide fallback for browsers that doesn't support the video element -->
<p>Browser not support the HTML5 Video element.</p>
</video>
<audio id="audio-example" controls="controls">
<source src="media/04-Death_Becomes_Fur.mp4" type="audio/mp4" />
<source src="media/04-Death_Becomes_Fur.oga" type="audio/ogg; codecs=vorbis" />
<!-- Provide fallback for browsers that doesn't support the audio element -->
<p>Browser not support the HTML5 Audio element.</p>
</audio>
使用 track 元素和 Web Video Text Tracks (WebVTT)格式的文件,可以将定时文本轨道(如字幕)添加到音频和视频元素中。关于这种格式的细节可以在 W3C 网站的 http://dev.w3.org/html5/webvtt/找到。通过指定轨道的种类(字幕、说明、元数据、章节和描述),轨道可以用于不同的目的。也可以通过指定音轨的语言来本地化音轨。将根据浏览器中的语言设置自动选择曲目。
清单 8-3 是一个提供多个字幕的例子,这些字幕由浏览器根据用户偏好的地区自动选择。如果用户的首选语言环境是丹麦语,浏览器将选择丹麦语字幕。在所有其他情况下,它将回落到英文字幕。
清单 8-3。 轨道元素可以用来指定多种语言的字幕;浏览器将根据其语言设置自动检测最合适的语言
<video id="video-example" controls="controls" poster="media/poster.png">
<source src="media/trailer.mp4" type="video/mp4" />
<source src="media/trailer.webm" type="video/webm" />
<track src="media/subtitles_en.vtt"
kind="subtitles" default="default"
label="English" srclang="en" />
<track src="media/subtitles_da.vtt"
kind="subtitles"
label="Dansk" srclang="da" />
<!-- Provide fallback for browsers that doesn't support the video element -->
<p>Browser not support the HTML5 Video element.</p>
</video>
提示 WebVTT 是取代 SRT 字幕格式的简单格式。WebVTT 文件包含一个提示列表,它指定了文本的时间。下面是在第 12 秒和第 16 秒之间以及第 18 秒和第 21 秒之间显示的两个文本的示例。
WEBVTT
00:12.000 --> 00:16.000
What brings you to the land of the gatekeepers?
00:18.000 --> 00:21.000
I'm searching for someone
如果浏览器不支持视频或音频元素,您可以通过在视频和音频元素中添加必要的代码来指定后备。例如,你可以插入一个旧的学校 Flash SWF ,如清单 8-4 中的所示。
清单 8-4。 浏览器不支持视频元素时回退到 Flash SWF 的例子
<video id="video-example" controls="controls" poster="media/poster.png" src="media/trailer.mp4">
<!-- Provide fallback for browsers that doesn't support the video element -->
<object width="300" height="150" data="video-player.swf"
type="application/x-shockwave-flash" title="Video Player ">
<param name="movie" value="media/trailer.mp4" />
<param name="height" value="300" />
<param name="width" value="150" />
<param name="menu" value="true" />
</object>
</video>
创建 JSF 媒体组件
对于 HTML5 媒体元素的 JSF 支持,我们将为音频和视频元素创建一个组件。这两个组件将具有 HTML5 规范中的 HTMLMediaElement 中定义的公共属性。视频组件将有三个额外的属性来指定视频的尺寸,并在回放之前显示海报。为了支持媒体源和音轨,我们将在组件的接口中包含两个集合:一个用于指定可用的媒体源,一个用于可用的文本音轨。
在这种情况下,我们面临着新的挑战。html 5 中的布尔属性并不是简单的表示为 attribute="true "和 attribute="false "。HTML5 规范声明
“元素上出现布尔属性代表真值,没有属性值代表假值。”
这意味着我们不能将 autoplay 和 output 之类的属性作为 autoplay="true "或 autoplay="false ",因为浏览器会评估两个版本,就好像 autoplay 为 true 一样。挑战在于,我们必须读取传递给组件的值,并评估它是否应该插入到定制组件的输出中。有两种方法可以做到这一点。您可以在标签下面包含条件性的<f:passThroughAttribute/>标签,也可以在复合组件根中操作输出的元素,复合组件根是组件的一种托管 bean。我们将展示这两种方法,但是首先我们将定义复合组件的接口。
在清单 8-5 中组件接口的定义中,也有一个组件的最小实现。我们添加到视频标签的属性只在客户端有意义。也就是说,它们不会在服务器端增加任何价值。我们不需要跟踪 HTML5 属性的状态。因此,我们可以使用 JSF 2.2 中添加的传递功能来输出必要的 HTML5 属性。在清单 8-1 的最小实现中,视频标签有两个属性。这些属性以 jsf:为前缀,这告诉 Facelets TagDecorator 这些属性不是传递属性,它们应该与组件的 id 和 value 属性相匹配。TagDecorator 负责将组件映射到一个已知的 JSF 组件。例如,在前一章中创建的输入组件都映射到了< h:inputText / >或 HtmlInputText 组件。TagDecorator 不熟悉 HTML5 < video / >元素,并将它映射到一个名为 PassThroughElement 的后备类。
清单 8-5。 复合组件接口,可用于视频和音频组件
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite">
<cc:interface componentType="UIMediaComponent">
<cc:attribute name="value" type="java.lang.String"
shortDescription="URL of the video file to display" />
<cc:attribute name="crossorigin" type="java.lang.String" default="anonymous"
shortDescription="Specifying how to deal with cross origin requests.
anonymous (default) or use-credentials." />
<cc:attribute name="preload" type="java.lang.String" default="auto"
shortDescription="Preload the video file. none, metadata or auto" />
<cc:attribute name="autoplay" type="java.lang.Boolean" default="false"
shortDescription="Start playback as soon as the page has loaded" />
<cc:attribute name="mediagroup" type="java.lang.String" default=""
shortDescription="Media group for which the video file belong" />
<cc:attribute name="loop" type="java.lang.Boolean" default="false"
shortDescription="Restart the video once it reaches the end" />
<cc:attribute name="muted" type="java.lang.Boolean" default="false"
shortDescription="Mute the audio of the video" />
<cc:attribute name="controls" type="java.lang.Boolean" default="false"
shortDescription="Display user controls" />
<cc:attribute name="poster" type="java.lang.String"
shortDescription="URL of a poster (image) to display before playback" />
<cc:attribute name="width" type="java.lang.String"
shortDescription="Width of the video" />
<cc:attribute name="height" type="java.lang.String"
shortDescription="Height of the video" />
<cc:attribute name="sources" type="java.util.Collection"
shortDescription="Collection of alternative MediaSources" />
<cc:attribute name="tracks" type="java.util.Collection"
shortDescription="Collection of MediaTracks" />
</cc:interface>
<cc:implementation >
<div id="#{cc.clientId}">
<video jsf:id="media-player"
jsf:value="#{cc.attrs.value}">
</video>
</div>
</cc:implementation>
</html>
下面演示了在复合组件的输出中有条件地包含属性的两种方法。
方法一:
每个属性都可以通过在
清单 8-6。 使用条件< f:passThroughAttribute / >元素
<cc:implementation >
<div id="#{cc.clientId}">
<video jsf:id="media-player"
jsf:value="#{cc.attrs.value}"
crossorigin="#{cc.attrs.crossorigin}"
preload="#{cc.attrs.preload}"
mediagroup="#{cc.attrs.mediagroup}"
src="#{cc.attrs.value}">
<c:if test="#{cc.attrs.autoplay}">
<f:passThroughAttribute name="autoplay" value="true" />
</c:if>
<c:if test="#{cc.attrs.loop}">
<f:passThroughAttribute name="loop" value="true" />
</c:if>
<c:if test="#{cc.attrs.muted}">
<f:passThroughAttribute name="muted" value="true" />
</c:if>
<c:if test="#{cc.attrs.controls}">
<f:passThroughAttribute name="controls" value="true" />
</c:if>
</video>
</div>
</cc:implementation>
这种方法的好处是您不需要在组件后面有一个 UIComponent 类,并且您可以快速地改变如何输出
方法二:实现复合组件根
第二种方法实现了一个复合组件根,它是位于组件后面的 UIComponent 类。在这个类中,你可以实现任何你能想到的逻辑。在清单 8-7 中,你可以看到如何指定位于复合组件后面的 UIComponent。复合组件背后的实际 UIComponent 可以在清单 8-8 中看到。
清单 8-7。 组件组件根被指定为复合组件接口中的 componentType
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<cc:interface componentType="UIMediaComponent">
<!-- OMITTED FOR READABILITY //-->
</cc:interface>
<cc:implementation >
<div id="#{cc.clientId}">
<video jsf:id="#{cc.elementId} "
jsf:value="#{cc.attrs.value}"
crossorigin="#{cc.attrs.crossorigin}"
preload="#{cc.attrs.preload}"
mediagroup="#{cc.attrs.mediagroup}"
src="#{cc.attrs.value}">
</video>
</div>
</cc:implementation>
</html>
清单 8-8。【UIMediaComponent.java】表示复合组件根,在清单 8-3 中指定为 componentType
package com.apress.projsf2html5.components.media;
import java.io.IOException;
import javax.el.ValueExpression;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
/**
* Composite component for the {@code <audio/>} and {@code <video/>} elements.
*/
@FacesComponent("UIMediaComponent")
public class UIMediaComponent extends UINamingContainer {
private static final String ELEMENT_ID = "media-player";
private static final String ATTRIBUTE_AUTOPLAY = "autoplay";
private static final String ATTRIBUTE_LOOP = "loop";
private static final String ATTRIBUTE_MUTED = "muted";
private static final String ATTRIBUTE_CONTROLS = "controls";
private static final String ATTRIBUTE_POSTER = "poster";
private static final String ATTRIBUTE_WIDTH = "width";
private static final String ATTRIBUTE_HEIGHT = "height";
public String getElementId() {
return ELEMENT_ID;
}
@Override
public void encodeBegin(FacesContext context) throws IOException {
super.encodeBegin(context);
UIComponent element = findMediaElement();
addAttributeIfTrue(element, ATTRIBUTE_AUTOPLAY);
addAttributeIfTrue(element, ATTRIBUTE_LOOP);
addAttributeIfTrue(element, ATTRIBUTE_MUTED);
addAttributeIfTrue(element, ATTRIBUTE_CONTROLS);
addAttributeIfNotNull(element, ATTRIBUTE_POSTER);
addAttributeIfNotNull(element, ATTRIBUTE_WIDTH);
addAttributeIfNotNull(element, ATTRIBUTE_HEIGHT);
}
private void addAttributeIfNotNull(UIComponent component, String attributeName) {
Object attributeValue = getAttribute(attributeName);
if (attributeValue != null) {
component.getPassThroughAttributes().put(attributeName, attributeValue);
}
}
private void addAttributeIfTrue(UIComponent component, String attributeName) {
if (isAttributeTrue(attributeName)) {
component.getPassThroughAttributes().put(attributeName, attributeName);
}
}
/**
* Finds the {@code <video/>} or {@code <audio/>} element in the
* composite component.
*
* @return {@link UIComponent} representing the {@code <video/>} or
* {@code <audio/>} element
* @throws IOException If {@link UIComponent} could not be found.
* There is no way to recover from this exception at run-time. Ensure that
* the ID of the element corresponds to ID specified in the
* {@link #ELEMENT_ID} constant
*/
private UIComponent findMediaElement() throws IOException {
UIComponent element = findComponent(getElementId());
if (element == null) {
throw new IOException("Media element with ID "
+ getElementId() + " could not be found");
}
return element;
}
/**
* Utility method for retrieving the attributes of a component. This method
* first checks if the attribute is an EL Expression followed by checking if
* it is a simple value.
*
* @param name Name of the attribute to retrieve
* @return The value of the attribute. If the contents of the attribute is
* an EL Expression, the expression will be executed and returned. If the
* contents of the attribute is a simple value, it will be returned as is.
* If the attribute cannot be found {@code null} is returned.
*/
private Object getAttribute(String name) {
ValueExpression ve = getValueExpression(name);
if (ve != null) {
// Attribute is a value expression
return ve.getValue(getFacesContext().getELContext());
} else if (getAttributes().containsKey(name)) {
// Attribute is a fixed value
return getAttributes().get(name);
} else {
// Attribute doesn't exist
return null;
}
}
/**
* Utility method that evaluates if the value in the given attribute is
* {@link Boolean.TRUE}.
*
* @param attributeName Name of the attribute to evaluate
* @return {@code true} if the value of the attribute evaluates to
* {@link Boolean.TRUE}, otherwise {@code false} is returned
*/
private boolean isAttributeTrue(String attributeName) {
boolean isBoolean = getAttribute(attributeName) instanceof java.lang.Boolean;
boolean isTrue = ((boolean) getAttribute(attributeName)) == Boolean.TRUE;
return isBoolean && isTrue;
}
}
这种方法的好处是,您可以轻松地对组件背后的逻辑进行单元测试。您还可以为多个组件重用复合组件根。例如,我们已经为音频和视频组件使用了前面的类。缺点是对于简单的组件来说,这可能是额外的工作,就代码行而言。此外,从复合组件输出的确切内容可能并不明显,因为输出现在是从 Facelets 视图和复合组件根生成和操作的。
支持源和轨道
除了视频和音频元素,HTML 5 还引入了可以嵌入视频和音频元素中的源和轨道元素,以支持多种媒体格式并提供文本轨道。我们已经在复合组件的接口中包含了可用属性的源和跟踪。在这些属性中,我们将期望对象的集合,因为每个视频和音频很可能有多个源和轨道。集合中的对象必须公开所需的属性来呈现源和轨道元素,所以我们创建了两个数据传输对象来保存关于源的细节,如清单 8-9 所示,以及关于轨道的细节,如清单 8-10 所示。
清单 8-9。MediaSource.java 是一个简单的数据传输对象,用于保存媒体源的信息
package com.apress.projsf2html5.components.media;
import java.util.Objects;
/**
* {@linkplain MediaSource Media source} used to provide the
* {@link UIMediaComponent} with alternative media files.
*/
public class MediaSource {
private String source;
private String type;
/**
* Creates a new instance of {@link MediaSource} with a blank source and
* mime type.
*/
public MediaSource() {
this("", "");
}
/**
* Creates a new instance of {@link MediaSource} with a preset source and
* MIME type.
*
* @param source URL of the media file to play
* @param type MIME type of the media file
*/
public MediaSource(String source, String type) {
this.source = source;
this.type = type;
}
/**
* Gets the URL of the media file to play.
*
* @return URL of the media file to play
*/
public String getSource() {
return source;
}
/**
* Sets the URL of the media file to play.
*
* @param source URL of the media file to play
*/
public void setSource(String source) {
this.source = source;
}
/**
* Gets the MIME type of the media file specified as the source.
*
* @return MIME type of the media file specified as the source
* @see <a href="http://www.iana.org/assignments/media-types/">IANA MIME
* types</a>
*/
public String getType() {
return type;
}
/**
* Sets the MIME type of the media file specified as the source.
*
* @param type MIME type of the media file specified as the source
* @see <a href="http://www.iana.org/assignments/media-types/">IANA MIME
* types</a>
*/
public void setType(String type) {
this.type = type;
}
}
清单 8-10。MediaTrack.java 是一个简单的数据传输对象,用于保存文本轨道的详细信息
package com.apress.projsf2html5.components.media;
import java.util.Locale;
/**
* {@linkplain MediaTrack Text track } used to provide the
* {@link UIMediaComponent} with localized text tracks for captioning, metadata,
* subtitles, etc.
*/
public class MediaTrack {
private String source;
private MediaTrackKind kind;
private boolean defaultTrack;
private String label;
private Locale locale;
/**
* Creates a new instance of {@link MediaTrack} with no details set.
*/
public MediaTrack() {
this("", null);
}
/**
* Creates a new instance of {@link MediaTrack} with the track and kind
* preset.
*
* @param source URL to the VTT text track
* @param kind Kind of text track
*/
public MediaTrack(String source, MediaTrackKind kind) {
this(source, kind, "", null, false);
}
/**
* Creates a new instance of {@link MediaTrack} with the track, kind, and
* label preset.
*
* @param source URL to the VTT text track
* @param kind Kind of text track
* @param label Label of the track (for display)
*/
public MediaTrack(String source, MediaTrackKind kind, String label) {
this(source, kind, label, null, false);
}
/**
* Creates a new instance of {@link MediaTrack} with the track, kind, and
* label preset.
*
* @param source URL to the VTT text track
* @param kind Kind of text track
* @param label Label of the track (for display)
* @param locale Locale of the VTT text track
*/
public MediaTrack(String source, MediaTrackKind kind, String label, Locale locale) {
this(source, kind, label, locale, false);
}
/**
* Creates a new instance of {@link MediaTrack} with the source, kind,
* label, {@link Locale} and default track preset.
*
* @param source URL to the VTT text track
* @param kind Kind of text track
* @param label Label of the track (for display)
* @param locale Locale of the VTT text track
* @param defaultTrack Is the track the default track?
*/
public MediaTrack(String source, MediaTrackKind kind, String label, Locale locale, boolean defaultTrack) {
this.source = source;
this.kind = kind;
this.defaultTrack = defaultTrack;
this.label = label;
this.locale = locale;
}
/**
* Determine if the {@link MediaTrack} is the default track to use if the
* browser could not match the appropriate track based on the
* {@link Locale}.
*
* @return {@link Boolean#TRUE} if this is the default track, otherwise
* {@link Boolean#FALSE}
*/
public boolean isDefaultTrack() {
return defaultTrack;
}
/**
* Sets the Default Track indicator of text track.
*
* @param defaultTrack {@link Boolean#TRUE} if this is the default track,
* otherwise {@link Boolean#FALSE}
*/
public void setDefaultTrack(boolean defaultTrack) {
this.defaultTrack = defaultTrack;
}
/**
* Gets the URL of the VTT text track.
*
* @return URL of the VTT text track
* @see <a href="http://dev.w3.org/html5/webvtt/">WebVTT: The Web Video Text
* Tracks Format</a>
*/
public String getSource() {
return source;
}
/**
* Sets the URL of the VTT text track.
*
* @param source URL of the VTT text track
* @see <a href="http://dev.w3.org/html5/webvtt/">WebVTT: The Web Video Text
* Tracks Format</a>
*/
public void setSource(String source) {
this.source = source;
}
/**
* Gets the kind of text track.
*
* @return Kind of text track
*/
public MediaTrackKind getKind() {
return kind;
}
/**
* Sets the kind of text track.
*
* @param kind Kind of text track
*/
public void setKind(MediaTrackKind kind) {
this.kind = kind;
}
/**
* Gets the label of the text track for display.
*
* @return Label of the text track for display.
*/
public String getLabel() {
return label;
}
/**
* Sets the label of the text track for display.
*
* @param label Label of the text track for display
*/
public void setLabel(String label) {
this.label = label;
}
/**
* Gets the {@link Locale} of the text track.
*
* @return {@link Locale} of the text track
*/
public Locale getLocale() {
return locale;
}
/**
* Sets the {@link Locale} of the text track.
*
* @param locale {@link Locale} of the text track
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
}
sources 和 tracks 集合嵌入在 video 元素中,sources 和 tracks 属性被添加到组件中,如清单 8-11 所示。
清单 8-11。 支持复合组件中的源和轨道集合
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<cc:interface componentType="UIMediaComponent">
<!-- OMITTED FOR READABILITY //-->
<cc:attribute name="sources" type="java.util.Collection"
shortDescription="Collection of alternative MediaSources. " />
<cc:attribute name="tracks" type="java.util.Collection"
shortDescription="Collection of MediaTracks. " />
</cc:interface>
<cc:implementation >
<div id="#{cc.clientId}">
<video jsf:id="#{cc.elementId} "
jsf:value="#{cc.attrs.value}"
crossorigin="#{cc.attrs.crossorigin}"
preload="#{cc.attrs.preload}"
mediagroup="#{cc.attrs.mediagroup}"
src="#{cc.attrs.value}">
<c:forEach items="#{cc.attrs.sources}" var="source" >
<source src="#{source.source}" type="#{source.type}" />
</c:forEach>
<c:forEach items="#{cc.attrs.tracks}" var="track" >
<track jsf:value="#{track.source}" src="#{track.source}">
<c:if test="#{track.kind != null}">
<f:passThroughAttribute name="kind" value="#{track.kind.toString()}" />
</c:if>
<c:if test="#{track.locale != null}">
<f:passThroughAttribute name="srclang"
value="#{track.locale.toString()}" />
</c:if>
<c:if test="#{track.defaultTrack}">
<f:passThroughAttribute name="defaultTrack" value="defaultTrack" />
</c:if>
</track>
</c:forEach>
</video>
</div>
</cc:implementation>
</html>
使用视频组件的示例
使用视频组件很简单。如果您想要向组件提供媒体源和文本轨道,您必须使用一个托管 bean 来包含您的集合;否则,组件可以与受管 bean 一起使用。清单 8-12 显示了一个例子,其中媒体源和文本轨道的集合是从后台 bean 中获取的。清单 8-13 中的后台 bean 将属性公开为可能来自数据库的集合。
清单 8-12。 使用视频组件
<h2>Video with a single media file</h2>
<projsfhtml5:video value="media/trailer.mp4"
autoplay="true"
controls="true" />
<h2>Video with multiple media sources and tracks</h2>
<projsfhtml5:video value="media/trailer.mp4"
controls="true"
sources="#{exampleVideoComponent.mediaSources}"
tracks="#{exampleVideoComponent.textTracks}"/>
清单 8-13。 为视频组件撑腰豆
package com.apress.projsf2html5.jsf;
import com.apress.projsf2html5.components.media.MediaSource;
import com.apress.projsf2html5.components.media.MediaTrack;
import com.apress.projsf2html5.components.media.MediaTrackKind;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named(value = "exampleVideoComponent")
@RequestScoped
public class ExampleVideoComponent {
public Collection<MediaSource> getMediaSources() {
Collection<MediaSource> sources = new ArrayList<>();
sources.add(new MediaSource("media/trailer.mp4", "video/mp4"));
sources.add(new MediaSource("media/trailer.webm", "video/webm"));
return sources;
}
public Collection<MediaTrack> getTextTracks() {
Collection<MediaTrack> tracks = new ArrayList<>();
tracks.add(new MediaTrack("media/subtitles_da.vtt", MediaTrackKind.subtitles,
"Dansk", new Locale("da"), false));
tracks.add(new MediaTrack("media/subtitles_en.vtt", MediaTrackKind.subtitles,
"English", Locale.ENGLISH, true));
return tracks;
}
}
进度条组件
大多数 JSF UI 框架都带有进度条组件。在 HTML5 之前,这些框架使用小部件框架,如 JQueryUI。HTML5 引入了一个新元素来表示进度条,这就是
图 8-1 。进度指标示例
progress 元素有两个简单的属性。省略这些属性会创建一个不确定的进度条。通过使用两个属性,您可以指定进度条的最大值和进度条的当前值(即位置)。关于属性的详细信息可以在表 8-3 中看到。
表 8-3 。进度元素的属性列表
progress 元素相当简单,可以快速转换成 JSF 组件。为了使组件有用,我们将添加一个附加的“for”属性,该属性可以指向另一个组件,如视频或音频组件。进度条将根据视频或音频组件的播放自动更新其进度。
该组件有一个复合组件视图、一个 JavaScript 和一个 FacesComponent。复合视图显示在清单 8-14 的中。注意组件的 JavaScript 是如何被分离到它自己的 JavaScript 文件中并使用 outputScript 组件导入的。outputScript 确保 JavaScript 文件只被导入一次,不管组件在一个页面上被使用多少次。
清单 8-14。 复合进度组件 resources/projs html 5/Progress . XHTML
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<cc:interface componentType="UIProgress">
<cc:attribute name="value" type="java.lang.String"
shortDescription="How much of the task has been completed" />
<cc:attribute name="max" type="java.lang.String"
shortDescription="The maximum number of the progress bar indicating that
the task has completed" />
<cc:attribute name="for" type="java.lang.String"
shortDescription="ID of the media component for which the progress bar
should automatically update" />
</cc:interface>
<cc:implementation >
<h:outputScript name="progress.js" library="projsfhtml5/progress" target="head"/>
<div id="#{cc.clientId}">
<progress jsf:id="progress">
<c:if test="#{cc.attrs.value != null}">
<f:passThroughAttribute name="value" value="#{cc.attrs.value}" />
</c:if>
<c:if test="#{cc.attrs.max != null}">
<f:passThroughAttribute name="max" value="#{cc.attrs.max}" />
</c:if>
<cc:insertChildren />
</progress>
<c:if test="#{cc.attrs.for != null}">
<script type="text/javascript">
progressBar.init("#{cc.clientId}", "#{cc.forClientId}");
</script>
</c:if>
</div>
</cc:implementation>
</html>
该组件支持通过 for 属性自动更新进度条。自动更新是通过清单 8-15 中的 progress.js JavaScript 配置的,该清单包含一个带两个参数的 JavaScript 闭包。第一个参数是进度条的标识符,第二个参数是进度条应该自动更新的源组件的标识符。在下面的 JavaScript 中,只支持音频和视频组件作为更新进度条的来源。
清单 8-15。 支持复合组件的资源/projs html 5/progress/progress . js JavaScript
if (!window["progressBar"]) {
var progressBar = {};
}
progressBar.init = function init(componentId, forId) {
var media = document.getElementById(forId + "\:media-player");
var bar = document.getElementById(componentId + "\:progress");
// Add an event listener for the “timeupdate” event of the media player
media.addEventListener("timeupdate", function() {
var percent = Math.floor((100 / media.duration) * media.currentTime);
bar.value = percent;
});
}
清单 8-16 中复合组件后面的 FacesComponent 是计算 for 属性中指定的组件的客户端标识符所必需的。
清单 8-16。ui Progress faces 进度复合组件所使用的组件
package com.apress.projsf2html5.components.progress;
import java.io.IOException;
import javax.el.ValueExpression;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UINamingContainer;
/**
* Composite component for the {@code <progress/>} element.
*/
@FacesComponent("UIProgress")
public class UIProgress extends UINamingContainer {
private static final String ATTRIBUTE_FOR = "for";
private UIComponent forComponent;
/**
* Finds the component specified in the {@code for} attribute.
*
* @return {@link UIComponent} specified in the {@code for} attribute
* @throws IOException If a {@link UIComponent} with the name specified in
* the {@code for} attribute could not be found
*/
public UIComponent getForComponent() throws IOException {
if (getAttributes().containsKey(ATTRIBUTE_FOR)) {
String forAttribute = (String) getAttribute(ATTRIBUTE_FOR);
this.forComponent = findComponent(forAttribute);
if (this.forComponent == null) {
throw new IOException("Component with ID "
+ forAttribute + " could not be found");
}
} else {
throw new IOException("The for attribute was not set on the component");
}
return forComponent;
}
/**
* Gets the client id of the {@link #getForComponent()}
*
* @return Client id of the {@link #getForComponent()}
* @throws IOException If the component specified in the {@code for}
* attribute could not be found
*/
public String getForClientId() throws IOException {
UIComponent element = getForComponent();
return element.getClientId(getFacesContext());
}
/**
* Utility method for retrieving the attributes of a component. This method
* first checks if the attribute is an EL Expression followed by checking if
* it is a simple value.
*
* @param name Name of the attribute to retrieve
* @return The value of the attribute. If the contents of the attribute is
* an EL Expression, the expression will be executed and returned. If the
* contents of the attribute is a simple value, it will be returned as is.
* If the attribute cannot be found {@code null} is returned.
*/
private Object getAttribute(String name) {
ValueExpression ve = getValueExpression(name);
if (ve != null) {
// Attribute is a value expression
return ve.getValue(getFacesContext().getELContext());
} else if (getAttributes().containsKey(name)) {
// Attribute is a fixed value
return getAttributes().get(name);
} else {
// Attribute doesn't exist
return null;
}
}
}
注意for 属性只支持媒体组件。为了使组件可以投入生产,您应该实现对其他组件的支持,比如文件上传、表单完成和 Ajax 请求。
该组件的使用非常简单。通过不向组件提供任何属性,它可以用作不确定的进度条。通过提供 value 和 max 属性,它可以用作手动更新的确定性条。最后,还可以通过将 for 属性指向媒体组件来使用它。图 8-2 是使用清单 8-17 所示组件的截图。
图 8-2 。进度组件截图(版权 Blender Foundation | www.sintel.org
)
清单 8-17。 使用进度组件的例子
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:projsfhtml5="http://xmlns.jcp.org/jsf/composite/projsfhtml5">
<ui:composition template="/base.xhtml">
<ui:define name="title">
Progress Component
</ui:define>
<ui:define name="top">
Progress Component
</ui:define>
<ui:define name="content">
<h2>Progress bar (indeterminate)</h2>
<projsfhtml5:progress id="indeterminate" />
<h2>Progress bar (determinate)</h2>
<projsfhtml5:progress id="determinate" value="56" max="100"/>
<h2>Progress bar (using for)</h2>
<projsfhtml5:video id="video" value="media/trailer.mp4"
autoplay="true"
controls="true">
</projsfhtml5:video>
<projsfhtml5:progress id="video-progress"
value="0" max="100"
for=":video" />
</ui:define>
</ui:composition>
</html>
摘要
在这一章中,我们已经学习了创建高级 HTML5 组件。这些示例演示了如何用 FacesComponents 支持复合组件视图,您可以在其中实现高级逻辑,否则在 Facelets 视图中很难或不可能实现。应该清楚的是,创建 JSF 2.x 组件比早期版本的 JSF 要容易得多。然而,仍然需要大量的工作来创建可以重用和扩展用于多种目的的组件。作为一名组件开发人员,您将在指定接口和实现组件时面临选择。值得注意的是,如果不考虑页面作者将如何使用组件,简单地将 HTML5 元素直接转换为 JSF 组件可能没有多大用处。在开发视频和音频组件时,我们面临着像 HTML5 一样将轨道指定为嵌入式子元素的选择,但在大多数情况下,JSF 页面作者在托管 bean 的集合中已经有了轨道和源;因此,我们在接口中包含了音轨和源作为属性获取集合。
12010 年 4 月史蒂夫·乔布斯对 Flash 的思考【http://www.apple.com/hotnews/thoughts-on-flash/
九、JSF 组件库
在这一章中,你将简要学习如何利用 JSF 组件库来生成漂亮的 web 应用。将向您介绍两个最著名的 JSF 开源组件库,它们是 PrimeFaces 和 RichFaces。虽然深入这些框架的细节超出了本书的范围,因为这些框架实在太大,无法在一章中涵盖,但是我们将向您概述每个组件库附带的不同组件,以及如何使用这些库来创建漂亮的 JSF 2.2 web 应用。
素数面
PrimeFaces 是一个开源的 JSF 组件库,具有许多不同的功能。PrimeFaces 拥有丰富的组件集,内置基于标准 JSF 2.0 Ajax API 的 Ajax,以及使用 web sockets 的 Ajax 推送支持;最后,PrimeFaces 包括移动用户界面 renderkit,允许 JSF 开发者在移动设备上创建 JSF 应用。PrimeFaces 还包括一个皮肤框架,它有超过 35 个内置主题。
为了配置 PrimeFaces,您需要下载 prime faces jar(prime faces-XXX . jar)。有两种方法可以下载这个 jar:你可以从 http://primefaces.org/downloads.html 下载,或者如果你是一个 Maven 用户,你可以将它定义为一个 Maven 依赖项。依赖项的组 ID 是 org.primefaces,工件 ID 是 primefaces,如下所示。
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>3.5</version>
</dependency>
除了前面的配置之外,您还需要将 PrimeFaces Maven 存储库添加到您的 Maven 配置的存储库列表中,以便 Maven 可以如下下载它。
<repository>
<id>prime-repo</id>
<name>Prime Repo</name>
<url>http://repository.primefaces.org</url>
</repository>
PrimeFaces 需要 Java 5+运行时和 JSF 2.x 实现作为强制依赖。为了支持 PrimeFaces 中的一些特性,你可以包含一些可选的库,如表 9-1 所示。
表 9-1 。可选的 PrimeFaces 库
|
图书馆
|
特征
|
| --- | --- |
| itext 版本 2.1.7 | 资料转存(PDF) |
| Apache 3.7 版 | 资料汇出(Excel) |
| 罗马 1.9 版 | feed reader(feed reader) |
| Commons 文件上传版本 1.2.1 Commons io 版本 1.4 | FileUpload(文件上载) |
组件概述
PrimeFaces 有 100 多个 UI 组件,可以用来创建丰富的 Web 2.0 应用。深入这些组件的细节超出了本书的范围。然而,我们将在表 9-2 中给出这些组件的简短列表供您参考,根据 PrimeFaces 文档按字母顺序排序。
表 9-2 。PrimeFaces 组件概述
|
成分
|
描述
|
| --- | --- |
| AccordionPanel
| AccordionPanel 是一个以堆叠格式显示内容的容器组件。 |
| AutoComplete
| AutoComplete 在输入时提供实时建议。 |
| BreadCrumb
| Breadcrumb 是一个导航组件,提供关于工作流中页面层次结构的上下文信息。 |
| Button
| Button 是标准 h:button 组件的扩展,具有皮肤功能。 |
| Calendar
| Calendar 是一个用于选择日期的输入组件,具有显示模式、分页、本地化、Ajax 选择等功能。 |
| Captcha
| Captcha 是一个基于 Recaptcha API 的表单验证组件。 |
| Carousel
| Carousel 是一个多用途组件,以幻灯片效果显示一组数据或一般内容。 |
| Chart
| 图表用于显示图形数据。有各种图表类型,如饼图、条形图、折线图等。 |
| Clock
| 时钟显示服务器或客户端的实时日期/时间。 |
| Color Picker
| ColorPicker 是一个带有调色板的输入组件。 |
| CommandButton
| CommandButton 是标准 CommandButton 的扩展版本,具有 Ajax 和主题功能。 |
| CommandLink
| CommandLink 用 Ajax 功能扩展了标准的 JSF commandLink。 |
| ConfirmDialog:
| ConfirmDialog 是对传统 JavaScript 确认框的替代。换肤、定制和避免弹出窗口拦截器是传统 JavaScript 确认的显著优势。 |
| ContextMenu
| ContextMenu 提供鼠标右键事件时显示的覆盖菜单。 |
| Dashboard
| Dashboard 提供了一个类似门户的布局,具有基于拖放的重新排序功能。 |
| DataExporter
| DataExporter 可以方便地将使用 PrimeFaces 数据表列出的数据导出为各种格式,如 excel、pdf、csv 和 xml。 |
| DataGrid
| DataGrid 在网格布局中显示数据集合。 |
| DataList
| DataList 在列表布局中以几种显示类型呈现数据集合。 |
| DataTable
| DataTable 是标准 DataTable 的增强版本,它为许多常见的用例提供了内置的解决方案,如分页、排序、选择、延迟加载、过滤。 |
| DefaultCommand
| 当按下回车键时使用哪个命令提交表单是 web 应用中的一个常见问题,而不仅仅是 JSF 特有的问题。浏览器倾向于表现不同,因为似乎没有标准,即使标准存在,IE 可能也不会关心它。有一些丑陋的解决办法,比如在你的应用中放置一个隐藏的按钮,并为每个表单编写 JavaScript。DefaultCommand 通过规范化命令(如按钮或链接)来解决这个问题,通过按回车键提交表单。 |
| Dialog
| 对话框是一个面板组件,可以覆盖页面上的其他元素。 |
| Drag&Drop
| PrimeFaces 的拖放工具包括两个组件:Draggable 和 Droppable。 |
| Dock
| Dock 组件模仿了众所周知的 Mac OS X 的 dock 接口。 |
| Editor
| 编辑器是一个输入组件,具有丰富的文本编辑功能。 |
| FeedReader
| FeedReader 用于显示提要中的内容。 |
| Fieldset
| Fieldset 是一个分组组件,是 html fieldset 的扩展。 |
| FileDownload
| 向客户机呈现动态二进制数据的传统方式是编写一个 servlet 或过滤器,并流式传输二进制数据。FileDownload 提供了一种更简单的方法。 |
| FileUpload
| FileUpload 超越了浏览器输入 type="file "功能,并提供了一个基于 html5 的丰富解决方案,可针对传统浏览器进行适度降级。 |
| Focus
| 焦点是一个实用组件,它使得管理 JSF 页面上的元素焦点变得很容易。 |
| Galleria
| Galleria 用于显示一组图像。 |
| GMap
| GMap 是与谷歌地图 API V3 集成的地图组件。 |
| GraphicImage
| GraphicImage 扩展了标准的 JSF 图形图像组件,能够像 inputstream 一样显示二进制数据。GraphicImage 的主要用途是使显示存储在数据库中的图像或动态图像更容易。传统的方法是用一个 servlet 来实现流;GraphicImage 不需要 servlet 就能完成所有繁重的工作。 |
| Growl
| Growl 基于 Mac 的 Growl 通知小工具,用于以叠加方式显示面部信息。 |
| HotKey
| 热键是一个通用的键绑定组件,可以将任意形式的键绑定到 JavaScript 事件处理程序或 Ajax 调用。 |
| IdleMonitor
| IdleMonitor 监视页面上的用户操作,并在用户再次空闲或活跃时通知回调。 |
| ImageCompare
| ImageCompare 提供了一个丰富的用户界面来比较两幅图像。 |
| ImageCropper
| ImageCropper 允许裁剪图像的某个区域。创建一个包含裁剪区域的新图像,并将其分配给服务器端的 CroppedImage 实例。 |
| ImageSwitch
| ImageSwitch 是一个简单的图片库组件。 |
| Inplace
| Inplace 提供简单的 Inplace 编辑和内嵌内容显示。Inplace 由两个成员组成:显示元素是初始的可点击标签,内联元素是显示元素切换时显示的隐藏内容。Inplace 提供简单的 inplace 编辑和内嵌内容显示。Inplace 由两个成员组成:显示元素是初始的可点击标签,内联元素是显示元素切换时显示的隐藏内容。 |
| InputMask
| 输入掩码强制输入符合定义的掩码模板。 |
| InputText
| InputText 是标准 inputText 的扩展,具有皮肤功能。 |
| InputTextarea
| InputTextarea 是标准 inputTextarea 的扩展,具有自动完成、自动调整大小、剩余字符计数器和主题化功能。 |
| Keyboard
| 键盘是使用虚拟键盘提供输入的输入组件。显著的特点是可定制的布局和皮肤功能。 |
| Layout
| 布局组件具有高度可定制的 borderLayout 模型,即使你不熟悉网页设计,也可以非常容易地创建复杂的布局。 |
| LightBox
| LightBox 是一个强大的覆盖图,可以显示图像、多媒体内容、自定义内容和外部 URL。 |
| Log
| 日志组件是一个可视化控制台,用于在 JSF 页面上显示日志。 |
| Media
| 媒体组件用于嵌入多媒体内容。 |
| MegaMenu
| MegaMenu 是一个水平导航组件,可以一起显示子菜单。 |
| Menu
| 菜单是一个导航组件,具有多种定制模式,如多层、iPod 风格的滑动和叠加。 |
| Menubar
| 菜单栏是一个水平导航组件。 |
| MenuButton
| MenuButton 在弹出菜单中显示不同的命令。 |
| Message
| Message 是标准 JSF 消息组件的预皮肤扩展版本。 |
| Messages
| Messages 是标准 JSF 消息组件的预皮肤扩展版本。 |
| Mindmap
| 思维导图是一个交互式工具,可以可视化思维导图数据,包括延迟加载、回调和动画。 |
| NotificationBar
| 通知栏显示多用途固定位置面板,用于通知。 |
| OrderList
| OrderList 用于对具有基于拖放的重新排序、过渡效果和 POJO 支持的集合进行排序。 |
| OutputLabel
| OutputLabel 是标准 outputLabel 组件的扩展。 |
| OutputPanel
| OutputPanel 是一个具有自动更新能力的面板组件。 |
| OverlayPanel
| OverlayPanel 是一个通用的面板组件,可以显示在其他内容之上。 |
| Panel
| Panel 是一个分组组件,具有内容切换、关闭和菜单集成功能。 |
| PanelGrid
| PanelGrid 是标准 PanelGrid 组件的扩展,增加了主题化和 colspan-rowspan 等特性。 |
| PanelMenu
| PanelMenu 是 accordionPanel 和 tree 组件的混合组件。 |
| Password
| 密码组件是标准 inputSecret 组件的扩展版本,具有主题集成和强度指示器。 |
| PhotoCam
| PhotoCam 用于通过网络摄像头拍摄照片,并将其发送到 JSF 后端模块。 |
| PickList
| 选项列表用于在两个不同的集合之间传输数据。 |
| Poll
| Poll 是一个 Ajax 组件,能够定期发送 Ajax 请求。 |
| Printer
| 打印机允许向打印机发送特定的 JSF 组件,而不是整个页面。 |
| ProgressBar
| ProgressBar 是一个进程状态指示器,既可以在客户端工作,也可以使用 Ajax 与服务器端交互。 |
| Rating
| 评级组件采用基于星级的评级系统。 |
| RemoteCommand
| RemoteCommand 提供了一种直接从 JavaScript 执行 JSF 支持 bean 方法的方法。 |
| ResetInput
| 当验证失败时,输入组件将它们的本地值保持在状态。ResetInput 用于从状态中清除缓存的值,以便组件从后备 bean 模型中检索它们的值。 |
| Resizable
| 可调整大小的组件用于使另一个 JSF 组件可调整大小。 |
| Ring
| 环是一个带有圆形动画的数据显示组件。 |
| Schedule
| Schedule 提供了一个 Outlook 日历,类似 iCal 的 JSF 组件来管理事件。 |
| SelectBooleanButton
| SelectBooleanButton 用于通过切换按钮选择二元决策。 |
| SelectBooleanCheckbox
| SelectBooleanCheckbox 是带有主题集成的标准复选框的扩展版本。 |
| SelectCheckboxMenu
| SelectCheckboxMenu 是一个多选组件,以叠加方式显示选项。 |
| SelectManyButton
| SelectManyButton 是一个使用按钮 UI 的多选组件。 |
| SelectManyCheckbox
| SelectManyCheckbox 是带有主题集成的标准 SelectManyCheckbox 的扩展版本。 |
| SelectManyMenu
| SelectManyMenu 是标准 SelectManyMenu 的扩展版本,具有主题集成。 |
| SelectOneButton
| SelectOneButton 是一个进行单项选择的输入组件。 |
| SelectOneListbox
| SelectOneListbox 是标准 SelectOneListbox 的扩展版本,具有主题集成。 |
| SelectOneMenu
| SelectOneMenu 是标准 SelectOneMenu 的扩展版本,具有主题集成。 |
| SelectOneRadio
| SelectOneRadio 是标准 SelectOneRadio 的扩展版本,具有主题集成。 |
| Separator
| 分隔符显示一条水平线来分隔内容。 |
| SlideMenu
| SlideMenu 用于显示带有滑动动画的嵌套子菜单。 |
| Slider
| 滑块用于为输入提供各种定制选项,如方向、显示模式和皮肤。 |
| Socket
| Socket 组件是在服务器和客户端之间创建通道的代理。 |
| Spacer
| Spacer 用于在元素之间放置空格。 |
| Spinner
| 微调器是一个输入组件,通过增量和减量按钮提供数字输入。 |
| SplitButton
| 默认情况下,SplitButton 显示一个命令,并在覆盖图中显示其他命令。 |
| Stack
| Stack 是一个导航组件,它模仿了 Mac OS X 中的 stacks 特性。 |
| TabMenu
| TabMenu 是一个导航组件,它将菜单项显示为选项卡。 |
| TabView
| TabView 是一个选项卡式面板组件,具有客户端选项卡、Ajax 动态内容加载和内容转换效果。 |
| TagCloud
| TagCloud 显示不同强度的标签集合。 |
| Terminal
| Terminal 是一个 Ajax 驱动的基于 web 的终端,它将桌面终端带到了 JSF。 |
| ThemeSwitcher
| ThemeSwitcher 支持动态切换 PrimeFaces 主题,无需刷新页面。 |
| Toolbar
| 工具栏是命令和其他内容的水平分组组件。 |
| Tooltip
| Tooltip 通过提供自定义效果、事件、html 内容和高级主题支持,超越了传统的 html 标题属性。 |
| Tree
| 树用于显示分层数据和创建站点导航。 |
| TreeTable
| TreeTable 用于以表格格式显示分层数据。 |
| Watermark
| 水印在输入字段上显示提示。 |
| Wizard
| Wizard 提供了一个 Ajax 增强的 UI,可以在单个页面中轻松实现工作流。向导由几个子选项卡组件组成,其中每个选项卡代表流程中的一个步骤。 |
在下一节中,我们将展示一个 PrimeFaces 应用示例,向您展示如何利用这个库来创建漂亮的 JSF 2.2 web 应用。
注为了获得完整的质数面示例文档,你可以查看来自
primefaces.org/documentation.html
的用户指南文档。您可以访问完整的 PrimeFaces 展示区,其中包括来自primefaces.org/showcase/ui/home.jsf
的几乎所有 PrimeFaces 组件的示例。
集成和定制 PrimeFaces
Country Navigator 应用是一个 PrimeFaces 应用,它允许用户在点击任何一个可用国家的旗帜后,获得一个可用国家的可用城市列表(带有一些信息),如图图 9-1 所示。
图 9-1 。县导航应用
如截图所示,应用页面主要由两个 UI 组件 : 组成
- Ring 组件,显示可用国家/地区的列表(德国、埃及和巴西)。
- DataTable 组件,该组件显示所选国家/地区的可用城市列表。
清单 9-1 显示了代表显示可用国家列表的 ring 组件的代码片段。
清单 9-1。 显示不同国家的环形组件
<p:ring id="custom" value="#{countryNavigator.countries}" var="country"
styleClass="image-ring" easing="swing">
<p:commandLink update=":form:detail">
<p:graphicImage valueimg/#{country.name}.gif"
styleClass="flagIcon" />
<f:setPropertyActionListener value="#{country}"
target="#{countryNavigator.activeCountry}" />
</p:commandLink>
</p:ring>
{ countryNavigator.countries }表达式表示可用国家的列表。当单击一个环项目,然后通过 p:commandLink 的操作,所选的 country #{country}将在 countryNavigator 的 activeCountry 属性中设置,并且“detail”面板(包含城市数据表)将重新呈现所选国家的新城市。清单 9-2 显示了“细节”面板部分。
清单 9-2。 【细节】面板部分
<p:outputPanel id="detail" styleClass="detailsPanel" layout="block">
<p:dataTable var="city" value="#{countryNavigator.activeCountry.cities}"
rendered="#{countryNavigator.activeCountry ne null}">
<f:facet name="header">
#{countryNavigator.activeCountry.name}
</f:facet>
<p:column headerText="City">
<h:outputText value="#{city.name}" />
</p:column>
<p:column headerText="Population">
<h:outputText value="#{city.population}" />
</p:column>
</p:dataTable>
</p:outputPanel>
当 countryNavigator 对象中存在可用的 activeCountry 时,将呈现“city”数据表,该表将显示所选国家的可用城市信息。清单 9-3 显示了完整的 CountryNavigator 托管 bean 代码。
清单 9-3。 CountryNavigator 托管 Bean
package com.jsfprohtml5.countrynavigator.model;
import java.util.List;
public class CountryNavigator {
private List<Country> countries;
private Country activeCountry;
public List<Country> getCountries() {
return countries;
}
public void setCountries(List<Country> countries) {
this.countries = countries;
}
public Country getActiveCountry() {
return activeCountry;
}
public void setActiveCountry(Country activeCountry) {
this.activeCountry = activeCountry;
}
}
Country managed bean 保存国家类的名称、人口和城市,如清单 9-4 所示。
清单 9-4。 国家托管豆
public class Country {
private String name;
private long population;
private List<City> cities;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getPopulation() {
return population;
}
public void setPopulation(long population) {
this.population = population;
}
public List<City> getCities() {
return cities;
}
public void setCities(List<City> cities) {
this.cities = cities;
}
}
城市管理 bean 保存城市类的名称和人口,如清单 9-5 中的所示。
清单 9-5。 城市管理豆
public class City {
private String name;
private long population;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getPopulation() {
return population;
}
public void setPopulation(long population) {
this.population = population;
}
}
清单 9-6 显示了 Country Navigator 应用页面的完整代码,它将 ring 组件与“details”面板部分合并在一起。
清单 9-6。 国家导航器应用页面代码
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<h:head>
<title>Welcome to the Country Navigator</title>
<h:outputStylesheet library="css" name="countryNavigator.css" />
</h:head>
<h:body>
<h:form id="form">
<h2>Click on a country to get its details</h2>
<p:ring id="custom" value="#{countryNavigator.countries}" var="country"
styleClass="image-ring" easing="swing">
<p:commandLink update=":form:detail">
<p:graphicImage valueimg/#{country.name}.gif"
styleClass="flagIcon" />
<f:setPropertyActionListener value="#{country}"
target="#{countryNavigator.activeCountry}" />
</p:commandLink>
</p:ring>
<p:outputPanel id="detail" styleClass="detailsPanel" layout="block">
<p:dataTable var="city" value="#{countryNavigator.activeCountry.cities}"
rendered="#{countryNavigator.activeCountry ne null}">
<f:facet name="header">
#{countryNavigator.activeCountry.name}
</f:facet>
<p:column headerText="City">
<h:outputText value="#{city.name}" />
</p:column>
<p:column headerText="Population">
<h:outputText value="#{city.population}" />
</p:column>
</p:dataTable>
</p:outputPanel>
</h:form>
</h:body>
</html>
为了填充国家和城市数据,创建并初始化 CountryNavigator 托管 bean 实例,如清单 9-7 中的 Faces 配置所示(注意,为了节省空间,省略了一些行)。
清单 9-7。 面孔配置文件
<?xml version='1.0' encoding='UTF-8'?>
<faces-config ...>
<managed-bean>
<managed-bean-name>countryNavigator</managed-bean-name>
<managed-bean-class>com.jsfprohtml5.countrynavigator.model.CountryNavigator</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>countries</property-name>
<list-entries>
<value>#{egypt}</value>
<value-class>com.jsfprohtml5.countrynavigator.model.Country</value-class>
<value>#{germany}</value>
<value-class>com.jsfprohtml5.countrynavigator.model.Country</value-class>
<value>#{brazil}</value>
<value-class>com.jsfprohtml5.countrynavigator.model.Country</value-class>
</list-entries>
</managed-property>
</managed-bean>
<!-- Egypt -->
<managed-bean>
<managed-bean-name>egypt</managed-bean-name>
<managed-bean-class>com.jsfprohtml5.countrynavigator.model.Country</managed-bean-class>
<managed-bean-scope>none</managed-bean-scope>
<managed-property>
<property-name>name</property-name>
<value>Egypt</value>
</managed-property>
<managed-property>
<property-name>population</property-name>
<value>82000000</value>
</managed-property>
<managed-property>
<property-name>cities</property-name>
<list-entries>
<value>#{cairo}</value>
<value-class>com.jsfprohtml5.countrynavigator.model.City</value-class>
<value>#{alexandria}</value>
<value-class>com.jsfprohtml5.countrynavigator.model.City</value-class>
<value>#{aswan}</value>
<value-class>com.jsfprohtml5.countrynavigator.model.City</value-class>
</list-entries>
</managed-property>
</managed-bean>
<managed-bean>
<managed-bean-name>cairo</managed-bean-name>
<managed-bean-class>com.jsfprohtml5.countrynavigator.model.City</managed-bean-class>
<managed-bean-scope>none</managed-bean-scope>
<managed-property>
<property-name>name</property-name>
<value>Cairo</value>
</managed-property>
<managed-property>
<property-name>population</property-name>
<value>8000000</value>
</managed-property>
</managed-bean>
<!-- Other configuration data is not shown ... -->
</faces-config>
注意 CountryNavigator 应用是一个 Maven web 应用:您可以从图书资源中下载,然后使用 mvn install 命令构建应用。最后,您可以在 GlassFish 版上部署最终的 country navigator-1.0-snapshot . war 来检查它是如何工作的。为了正确运行 Maven 命令,您必须确保 JAVA_HOME 指向安装在您的操作系统中的 Java 7 目录。
RichFaces
RichFaces 是一个开源的 JSF 组件库,拥有许多不同的功能。RichFaces 包括两个主要的标签库:
- a4j 标记库,它提供了 Ajax 功能和通用工具。
- 丰富的标记库,它提供了一组与 Ajax 完全集成的自包含的丰富组件。
为了配置 RichFaces,您需要首先理解它的依赖关系 jars。RichFaces 依赖于四个主要的 jar,它们代表 RichFaces 核心和组件的 API 和实现,如下所示:
- richfaces-core-api.jar
- richfaces 核心 impl.jar
- rich faces-组件-api.jar
- richfaces-components-ui.jar
RichFaces jars 具有以下强制性依赖项:
- Java Server Faces 2.x 实现:javax . Faces . jar(2 . 1 . 5 或更高版本)或 myfaces-impl . jar(2 . 1 . 5 或更高版本)
- 谷歌番石榴:Guava . jar(10 . 0 . 1 版本)。
- CSS 剖析器:cssparser.jar(版本 0.9.5)。
- CSS 的简单 API:sac . jar(1.3 版)
以及运行某些功能可能需要的以下可选 jar:
- 客户端验证(JSR-303 API 和实现)的 Bean 验证(JSR-303)集成:验证-api.jar(版本 1.0.0.GA)和 hibernate-validator.jar(版本 4.2.0.Final 或更高)。
- 推送传输库—Atmosphere(无依赖):atmosphere-runtime.jar(版本 1.0.10)(所选兼容模块 atmosphere-compat-*。罐子可能是必要的)。
- 推送 JMS 集成 (JMS API 和实现):JMS . jar(1.1 版)和 hornetq-JMS . jar(2 . 2 . 7 . final 或更高版本)
- 推送 CDI 集成 (CDI API 及实现):cdi-api.jar(版本 1.0-SP4)和 javax.inject.jar(版本 1)和 jsr-250-api.jar(版本 1.0)和 weld-servlet.jar(版本 1.1.4.Final)。
- 扩展缓存(EhCache): ehcache.jar(版本 1.6.0)。
注意要知道,前面提到的一些依赖关系是 Java EE 6 规范的一部分,所以如果你在一个像 GlassFish 这样的 Java EE 6 应用服务器上工作(不仅仅是一个 servlet 容器),那么就没有必要添加这些依赖关系。
有两种方式下载 RichFaces 依赖项:你可以直接从www.jboss.org/richfaces/download.html下载,或者如果你是 Maven 用户,你可以使用 RichFaces Maven 原型(RichFaces 需要 Maven 3.0.3 或更高版本)。使用名为 richfaces-archetype-simpleapp 的 Maven 原型,您可以生成 richfaces 应用项目的基本结构和需求。
为了运行 RichFaces Maven 原型,需要将 JBoss 存储库添加到 Maven 配置中。在${ Maven _ Installation _ Dir }/conf/settings . XML 文件中的
清单 9-8。 将 JBoss 资源库添加到 Maven 配置中
<profiles>
...
<profile>
<id>jboss-public-repository</id>
<repositories>
<repository>
<id>jboss-public-repository-group</id>
<name>JBoss Public Maven Repository Group</name>
<url>https://repository.jboss.org/nexus/content/groups/public/</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>jboss-public-repository-group</id>
<name>JBoss Public Maven Repository Group</name>
<url>https://repository.jboss.org/nexus/content/groups/public/</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
除了创建 jboss-public-repository 概要文件之外,还需要将其添加到
清单 9-9。 激活 jboss-public-repository 概要
<activeProfiles>
<activeProfile>jboss-public-repository</activeProfile>
</activeProfiles>
在创建并激活 jboss-public-repository 之后,可以使用 rich faces-architect-simple app 原型生成项目。为此,为您的项目创建一个新目录,然后在该目录中运行以下 Maven 命令:
mvn archetype:generate -DarchetypeGroupId=org.richfaces.archetypes -DarchetypeArtifactId=richfaces-archetype-simpleapp -DarchetypeVersion=4.3.2.Final -DgroupId=com.jsfprohtml5.richfacesapp -DartifactId=richFacesApp
可以使用-DgroupId 参数来定义应用管理的 beans 的包,而可以使用-DartifactId 来定义项目的名称。
前面的 richfaces-archetype-simpleapp 命令生成一个新的 richfaces 项目,其结构如下。
richFacesApp
├── pom.xml
├── readme.txt
└── src
└── main
├── java
│ └── com
│ └── jsfprohtml5
│ └── richfacesapp
│ └── RichBean.java
└── webapp
├── index.xhtml
├── templates
│ └── template.xhtml
└── WEB-INF
├── faces-config.xml
└── web.xml
这就是创建 RichFaces 应用所需的全部内容;现在,您可以使用普通的 mvn install 命令构建生成的 Maven 项目。
组件概述
RichFaces 有 40 多个 UI 组件,可以用来创建丰富的 Web 2.0 应用。深入这些组件的细节超出了本书的范围。但是,我们将在表 9-3 中给出一些组件的简短列表,供您参考,根据 RichFaces 文档按字母顺序排序(注意,所有这些组件都是 rich 标签库的一部分)。
表 9-3 。RichFaces 组件概述
|
成分
|
描述
|
| --- | --- |
| Accordion
|
| Autocomplete
| 组件是一个内置 Ajax 的自动完成输入框。 |
| Calendar
|
| CollapsiblePanel
| 组件
| ContextMenu
| 组件用于创建一个层次化的上下文菜单,该菜单在 onmouseover、onclick 等事件上被激活。该组件可以应用于页面上的任何元素。 |
| DataGrid
|
| DataScroller
|
| DataTable
| 组件用来呈现一个高度可定制的表格,包括表格的标题。它与
| DragIndicator
|
| DragSource
| 可以将
| DropDownMenu
| 组件
| DropTarget
| 可以将
| Editor
|
| ExtendedDataTable
|
| FileUpload
|
| Focus
|
| HashParam
|
| HotKey
|
| InplaceInput
|
| InplaceSelect
| 除了
| InputNumberSlider
|
| InputNumberSpinner
|
| jQuery
|
| List
| 组件呈现一个项目列表。该列表可以是按数字排序的列表、无序的项目符号列表或数据定义列表。该组件使用数据模型来管理列表项,该模型可以动态更新。
|
| Message
|
| Messages
|
| Notify
|
| NotifyMessage
|
| NotifyMessages
|
| NotifyStack
| 由
| OrderingList
|
| Panel
|
| PanelMenu
| 组件
| PickList
|
| Placeholder
|
| PopupPanel
|
| ProgressBar
|
| Select
| 组件可以被配置为一个组合框,它将接受键入的输入。 |
| TabPanel
|
| TogglePanel
|
| Toolbar
|
| Tooltip
|
| Tree
|
在下一节中,我们将展示一个 RichFaces 应用示例,向您展示如何利用这个库。
注为了获得 RichFaces 的完整文档,可以查看来自
www.jboss.org/richfaces/docs
的文档。您可以访问完整的 RichFaces 展示,其中包括来自showcase.richfaces.org
的几乎所有 RichFaces 组件的示例。
集成和定制 RichFaces
RightCountry 应用是一个 RichFaces 应用,它允许用户将一个可用地点列表拖动到相应的国家,如图 9-2 所示。
图 9-2 。正确的国家应用
该应用不允许用户将一个地方拖放到该地方不属于的国家。清单 9-10 显示了代表拖放源面板(在左边)的代码片段。
清单 9-10。 【下拉源面板代码(在左边)
<rich:panel styleClass="dropSourcePanel">
<f:facet name="header">
<h:outputText value="Places" />
</f:facet>
<h:dataTable id="places" columns="1"
value="#{rightCountry.places}"
var="place" footerClass="footerClass">
<h:column>
<a4j:outputPanel styleClass="placesContainer"
layout="block">
<rich:dragSource type="#{place.country}"
dragValue="#{place}"
dragIndicator="ind"/>
<h:outputText value="#{place.name}"></h:outputText>
</a4j:outputPanel>
</h:column>
</h:dataTable>
</rich:panel>
“拖放源”面板包含以下主要组件:
- “地点”数据表,列出了要显示的不同地点。
- dragSource,它允许数据表中的位置是可拖动的;在这个例子中,我们主要使用了 dragSource 的两个属性;dragValue,表示在拖放事件完成后要发送到拖放区的数据;dragIndicator,表示在拖动操作期间用作拖动指针的 drag indicator 组件的组件 ID。
除了拥有单个拖放源面板之外,我们还有三个拖放目标面板,它们将从拖放源面板接收项目;每个放置目标代表放置源项目的相应国家(埃及、德国和巴西)。清单 9-11 显示了第一个拖放目标面板(它将接收与埃及相关的拖放源项目)。
清单 9-11。 首先放下目标面板
<rich:panel styleClass="dropTargetPanel">
<f:facet name="header">
<h:outputText value="Egypt" />
</f:facet>
<rich:dropTarget acceptedTypes="Egypt" dropValue="Egypt"
dropListener="#{rightCountry.processDrop}"
render="places, egyptPlaces, germanyPlaces, brazilPlaces"/>
<h:dataTable id="egyptPlaces" columns="1"
value="#{rightCountry.egyptPlaces}"
var="place" footerClass="footerClass">
<h:column>
<h:outputText value="#{place.name}"></h:outputText>
</h:column>
</h:dataTable>
</rich:panel>
如前面的代码片段所示,拖放目标面板包含以下组件:
- “egyptPlaces”数据表,显示属于埃及的地方。
- dragTarget,它定义了可放置的区域;在示例中,我们使用了 dragTarget 的四个属性;acceptedTypes,定义可拖放区可接受的元素类型(如果与 dragSource 类型匹配,则可拖放区将接受 dragSource 项);dropValue,表示 drop 事件完成后要处理的数据;dropListener,与表示操作监听器方法的 MethodExpression 绑定,该方法将在 drop 操作完成后被通知;最后是 render 属性,它定义了将参与请求处理生命周期的“呈现”部分的组件的 id。
在#{rightCountry.processDrop}方法表达式(将在拖放完成后执行)中,从 places 列表(与拖动源数据表绑定)中删除所选的 place 对象,并将其放入 egyptPlaces 列表中,以便用新的 place 对象更新“egyptPlaces”数据表,如清单 9-12 所示。
清单 9-12。right country 托管 Bean 的 processDrop 方法
public void processDrop(DropEvent event) {
Place place = (Place) event.getDragValue();
String dropValue = (String) event.getDropValue();
switch (dropValue) {
case "Egypt":
egyptPlaces.add(place);
places.remove(place);
break;
//...
}
if (places.size() == 0) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Congratulations! You are done."));
initialize();
}
}
您可能会注意到,在使用完 drag source 面板中的所有元素(拖放到相应的合适目标)之后,会创建一条 Faces 消息,向用户显示“恭喜!你完了。”并使用 initialize()方法重置页面信息;该 Faces 消息将由
与埃及下降目标一样,德国和巴西的其他下降目标也是如此。清单 9-13 显示了右国申请页面的完整代码。
清单 9-13。 右国申请页面
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title>Welcome to the Right Country application</title>
<h:outputStylesheet library="css" name="rightCountry.css" />
</h:head>
<h:body>
<rich:dragIndicator id="ind" acceptClass="accept" rejectClass="reject"
draggingClass="default">
Drag the place to the right country
</rich:dragIndicator>
<h:form id="form">
<h2>Drag the places to the right country</h2>
<h:panelGrid columnClasses="column" columns="4"
styleClass="containerPanel">
<rich:panel styleClass="dropSourcePanel">
<f:facet name="header">
<h:outputText value="Places" />
</f:facet>
<h:dataTable id="places" columns="1"
value="#{rightCountry.places}"
var="place" footerClass="footerClass">
<h:column>
<a4j:outputPanel styleClass="placesContainer"
layout="block">
<rich:dragSource type="#{place.country}"
dragValue="#{place}"
dragIndicator="ind"/>
<h:outputText value="#{place.name}"></h:outputText>
</a4j:outputPanel>
</h:column>
</h:dataTable>
</rich:panel>
<rich:panel styleClass="dropTargetPanel">
<f:facet name="header">
<h:outputText value="Egypt" />
</f:facet>
<rich:dropTarget acceptedTypes="Egypt" dropValue="Egypt"
dropListener="#{rightCountry.processDrop}"
render="places, egyptPlaces, germanyPlaces, brazilPlaces"/>
<h:dataTable id="egyptPlaces" columns="1"
value="#{rightCountry.egyptPlaces}"
var="place" footerClass="footerClass">
<h:column>
<h:outputText value="#{place.name}"></h:outputText>
</h:column>
</h:dataTable>
</rich:panel>
<rich:panel styleClass="dropTargetPanel">
<f:facet name="header">
<h:outputText value="Germany" />
</f:facet>
<rich:dropTarget acceptedTypes="Germany" dropValue="Germany"
dropListener="#{rightCountry.processDrop}"
render="places, egyptPlaces, germanyPlaces, brazilPlaces"/>
<h:dataTable id="germanyPlaces" columns="1"
value="#{rightCountry.germanyPlaces}"
var="place" footerClass="footerClass">
<h:column>
<h:outputText value="#{place.name}"></h:outputText>
</h:column>
</h:dataTable>
</rich:panel>
<rich:panel styleClass="dropTargetPanel">
<f:facet name="header">
<h:outputText value="Brazil" />
</f:facet>
<rich:dropTarget acceptedTypes="Brazil" dropValue="Brazil"
dropListener="#{rightCountry.processDrop}"
render="places, egyptPlaces, germanyPlaces, brazilPlaces"/>
<h:dataTable id="brazilPlaces" columns="1"
value="#{rightCountry.brazilPlaces}"
var="place" footerClass="footerClass">
<h:column>
<h:outputText value="#{place.name}"></h:outputText>
</h:column>
</h:dataTable>
</rich:panel>
</h:panelGrid>
<rich:notifyMessages stayTime="2000" nonblocking="true" />
</h:form>
</h:body>
</html>
RightCountry 托管 bean 包含四个列表,这四个列表与拖动源和四个放置目标绑定在一起。清单 9-14 显示了完整的 RightCountry 托管 bean 代码。
清单 9-14。 RightCountry 托管豆
package com.jsfprohtml5.rightcountry.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import org.richfaces.event.DropEvent;
public class RightCountry implements Serializable {
private List<Place> places;
private List<Place> egyptPlaces;
private List<Place> germanyPlaces;
private List<Place> brazilPlaces;
public RightCountry() {
initialize();
}
public List<Place> getPlaces() {
return places;
}
public void setPlaces(List<Place> places) {
this.places = places;
}
public List<Place> getEgyptPlaces() {
return egyptPlaces;
}
public void setEgyptPlaces(List<Place> egyptPlaces) {
this.egyptPlaces = egyptPlaces;
}
public List<Place> getGermanyPlaces() {
return germanyPlaces;
}
public void setGermanyPlaces(List<Place> germanyPlaces) {
this.germanyPlaces = germanyPlaces;
}
public List<Place> getBrazilPlaces() {
return brazilPlaces;
}
public void setBrazilPlaces(List<Place> brazilPlaces) {
this.brazilPlaces = brazilPlaces;
}
public void processDrop(DropEvent event) {
Place place = (Place) event.getDragValue();
String dropValue = (String) event.getDropValue();
switch (dropValue) {
case "Egypt":
egyptPlaces.add(place);
places.remove(place);
break;
case "Germany":
germanyPlaces.add(place);
places.remove(place);
break;
case "Brazil":
brazilPlaces.add(place);
places.remove(place);
break;
}
if (places.size() == 0) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Congratulations! You are done."));
initialize();
}
}
private void initialize () {
egyptPlaces = new ArrayList<>();
germanyPlaces = new ArrayList<>();
brazilPlaces = new ArrayList<>();
places = new ArrayList<>();
places.add(new Place("The Great Pyramids of Giza", "Egypt"));
places.add(new Place("Western Pomerania Lagoon Area National Park", "Germany"));
places.add(new Place("Catete Palace", "Brazil"));
places.add(new Place("Saxon Switzerland National Park", "Germany"));
places.add(new Place("Luxor Temple", "Egypt"));
places.add(new Place("Mariano Procópio Museum", "Brazil"));
places.add(new Place("Bavarian Forest National Park", "Germany"));
places.add(new Place("Museu do Índio", "Brazil"));
places.add(new Place("Cairo Tower", "Egypt"));
}
}
最后,清单 9-15 展示了 RightCountry 应用的 CSS 样式类。
清单 9-15。right country 应用的 CSS 样式类
.column {
width: 25%;
vertical-align: top;
}
.dropTargetPanel {
width: 90%;
}
.dropSourcePanel {
width: 133px;
}
.containerPanel {
width: 100%;
}
.placesContainer {
width: 100px;
border: 1px solid gray;
padding: 2px
}
.footerClass {
text-align: center;
padding-top: 5px;
}
.default {
padding-left:30px;
background-position: 5px;
background-repeat: no-repeat;
}
.accept {
background-position: 5px;
background-repeat: no-repeat;
border:2px solid green
}
.reject {
border:2px solid red;
background-position: 5px;
background-repeat: no-repeat;
}
注意像 CountryNavigator 应用一样,RightCountry 应用是一个 Maven web 应用,你可以在www.apress.com/9781430250104从图书网站下载;它可以像 CountryNavigator 应用一样进行构建和部署。
摘要
在这一章中,向您介绍了两个最流行的开源 JSF 组件库(PrimeFaces 和 RichFaces)。尽管深入研究这些框架的细节超出了本书的范围,但是我们开发了两个应用(一个用于 PrimeFaces,另一个用于 RichFaces ),以便向您展示如何利用这些组件库在 JSF 2.2 世界中生成漂亮的 web 应用。
十、创建基本的 JSF 2.2 应用
在本章中,您将详细了解如何在 Java EE 7 环境中创建一个基本的 JSF 2.2 应用。这个应用将向你展示如何在 Java EE 7 环境中设计和开发你的 JSF 应用。该应用利用 JSF 2.2 创建页面和处理页面流,CDI(上下文和依赖注入)进行 bean 管理,EJB 3.2 进行事务处理,JPA 2.1 进行数据持久化。
结构化天气应用
基本应用是关于显示保存在他/她的简档中的用户地点的天气信息的应用。在天气应用中,用户需要首先在应用中注册。为了在应用中注册,用户需要在由三个页面组成的流程中输入他/她的信息。如图图 10-1 所示,在第一页中,用户必须输入他/她的首选用户名、密码和电子邮件。
图 10-1 。天气应用注册(首页)
如果用户输入空用户名、密码或电子邮件,将显示必填字段消息,并且当用户输入无效格式的电子邮件时,将显示无效电子邮件格式消息。
在第一页输入信息后,用户进入流程的第二页,用户输入他/她的名字、姓氏和职业,如图图 10-2 所示。
图 10-2 。天气应用注册(第二页)
最后,在注册流程的最后一页,用户在最后一页输入他/她的邮政编码,如图 10-3 所示,然后点击“完成”按钮。
图 10-3 。天气应用注册(第三页)
在应用中注册后,用户将能够使用他/她的用户名和密码登录应用,如图 10-4 所示。
图 10-4 。天气应用登录页面
登录应用后,如图 10-5 所示,用户将被转到天气屏幕,在该屏幕中,用户将能够了解他在注册最终页面中输入的地方的天气信息。
图 10-5 。天气应用主页
现在,在浏览完天气应用的页面后,让我们看看如何构建它。图 10-6 显示了天气应用的结构。
图 10-6 。天气应用结构
如上图所示,该应用具有以下结构:
- XHTML 页面: 这些表示天气应用页面。它使用 JSF 表达式语言(EL)来使用支持 bean 和受管 bean。
- Backing bean:这些是普通的托管 bean,它们在概念上与 UI 页面相关,并且不是应用模型的一部分。Backing beans 是集中处理页面操作的理想选择。在天气应用中,backing beans 主要获取托管 bean 的实例,这些实例携带用户输入的数据,然后调用 UserManager EJB 来执行所需的操作。
- 用户经理 EJB: 为了执行不同的业务操作,backing beans 调用用户经理 EJB。用户管理器 EJB 是一个无状态会话 EJB*,它使用 JPA 实体和 JPA EntityManager 来执行所需的数据库操作。
- JPA 实体(CDI 托管 bean):JPA 实体表示映射到数据库表的数据类。在 weather 应用中,JPA 实体被用作应用的 CDI 托管 beanss,这些 bean 使用 EL 与 XHTML 页面绑定在一起。
注意,为了简单起见,应用使用 Oracle Java DB。Java DB 是 Oracle 支持的 Apache Derby 开源数据库的发行版。它通过 JDBC 和 Java EE APIs 支持标准 ANSI/ISO SQL,并包含在 JDK 中。
注意要知道 JPA 可以不用 EJBs 然而,在 JPA 应用中使用 EJB 有一个很大的优势,那就是通过 EJB 容器隐式地处理应用事务(容器管理的事务)。尽管天气应用是一个基本的 JSF 2.2 应用,但我们坚持引入包含 JPA 的 EJB,以便向您展示这些技术如何在 JSF 2.2 应用中协同工作。
在接下来的部分中,我们将深入应用组件的细节。
注意需要注意的是,深入 EJB 和 JPA 的细节超出了本书的范围。为了了解学习它们的能力,我们推荐你阅读 Oracle Java EE 教程:
docs.oracle.com/javaee/7/tutorial/doc/
。
构建 JSF 页面
天气应用有以下 XHTML 页面:
- 主页(home.xhtml):表示应用的登录页面,如图图 10-4 所示。
- 注册页面(/registration/*。xhtml):它们表示包含注册流程的页面;注册页面包括以下页面:
- a.registration.xhtml 页面,表示图 10-1 所示流程中的第一个注册页面。
- b.extraInfo.xhtml 页面,表示图 10-2 所示流程中的第二个注册页面。
- c.final.xhtml 页面,表示图 10-3 所示流程中的最终注册页面。
- 天气页面(/protected/weather.xhtml),代表图 10-5 所示的天气页面。
清单 10-1 显示了 home.xhtml 页面代码。
清单 10-1。 首页 XHTML 代码
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<ui:composition template="/WEB-INF/templates/main.xhtml">
<ui:define name="title">
#{bundle['application.loginpage.title']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.id']}"></h:outputText>
<h:inputText id="userID"
value="#{appUser.id}"
required="true"
requiredMessage="#{bundle['user.id.validation']}">
</h:inputText>
<h:message for="userID" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.password']}"></h:outputText>
<h:inputSecret id="password"
value="#{appUser.password}"
required="true"
requiredMessage="#{bundle['user.password.validation']}">
</h:inputSecret>
<h:message for="password" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.login']}" action="#{loginBacking.login}"/> <br/>
<h:link value="#{bundle['application.loginpage.register']}" outcome="registration"/>
<br/><br/>
<h:messages styleClass="errorMessage"/>
</h:form>
</ui:define>
</ui:composition>
</html>
如前面的代码所示,天气应用的主页包含用户名 InputText 和密码 InputSecret。login CommandButton 调用 LoginBacking bean 的 login 方法,registration 链接导航到“registration”流(将在下一节中详细说明)。清单 10-2 显示了 LoginBacking bean。
清单 10-2。 登录备份豆
package com.jsfprohtml5.weather.backing;
import com.jsfprohtml5.weather.model.AppUser;
import com.jsfprohtml5.weather.model.UserManagerLocal;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.inject.Named;
@Named
@RequestScoped
public class LoginBacking extends BaseBacking {
@EJB
private UserManagerLocal userManager;
public String login() {
AppUser currentAppUser = (AppUser) evaluateEL("#{appUser}", AppUser.class);
try {
AppUser appUser = userManager.getUser(currentAppUser.getId(), currentAppUser.getPassword());
if (appUser == null) {
getContext().addMessage(null, new FacesMessage(INVALID_USERNAME_OR_PASSWORD));
return null;
}
//Set Necessary user information
currentAppUser.setEmail(appUser.getEmail());
currentAppUser.setFirstName(appUser.getFirstName());
currentAppUser.setLastName(appUser.getLastName());
currentAppUser.setZipCode(appUser.getZipCode());
currentAppUser.setProfession(appUser.getProfession());
} catch (Exception ex) {
Logger.getLogger(LoginBacking.class.getName()).log(Level.SEVERE, null, ex);
getContext().addMessage(null, new FacesMessage(SYSTEM_ERROR));
return null;
}
return "/protected/weather";
}
...
}
LoginBacking bean 是处理登录操作的后备 bean。它调用用户管理器 EJB,以便知道用户是否使用 getUser()方法在系统中注册(用户管理器 EJB 将在“应用后端”一节中详细说明)。为了获取与用户名和密码字段绑定的 AppUser 的 CDI 托管 bean 实例,调用 evaluateEL()方法来计算#{appUser}表达式。evaluateEL()方法位于 base backing bean (BaseBacking)类中。
如果 getUser()方法返回 null,这意味着用户没有使用输入的用户名和密码组合在系统中注册,并且为用户显示无效的用户名或密码消息。如果用户名和密码组合有效,则在#{appUser}托管 bean 实例(appUser 既是请求范围的 CDI 托管 bean,也是 JPA 实体类,将在“应用后端”一节中详细说明)中检索和设置用户信息,并将页面转发到天气页面。
注意 @EJB 注释可以用来注释 bean 的实例变量,以指定对 EJB 的依赖。Application Server 使用依赖注入,通过引用它所依赖的 EJB,自动初始化带注释的变量。这个初始化发生在调用 bean 的任何业务方法之前和设置 bean 的 EJBContext 之后。
如图 10-7 所示,所有天气应用的后台 beans 都从 BaseBacking 类扩展而来。
图 10-7 。天气应用的支持 beans
清单 10-3 显示了 BaseBacking 类的代码。
清单 10-3。base backing Bean 类
package com.jsfprohtml5.weather.backing;
import java.util.Map;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpSession;
public class BaseBacking {
protected FacesContext getContext() {
return FacesContext.getCurrentInstance();
}
protected Map getRequestMap() {
return getContext().getExternalContext().getRequestMap();
}
protected HttpSession getSession() {
return (HttpSession) getContext().getExternalContext().getSession(false);
}
protected Object evaluateEL(String elExpression, Class beanClazz) {
return getContext().getApplication().evaluateExpressionGet(getContext(), elExpression, beanClazz);
}
...
}
BaseBacking class 是一个基类,它包含获取 JSF Faces 上下文、获取 HTTP 会话、获取 HTTP 请求映射和评估 JSF 表达式的快捷方式。清单 10-4 显示了 weather.xhtml 页面代码。
清单 10-4。 天气主页
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:mashup="http://code.google.com/p/mashups4jsf/">
<ui:composition template="/WEB-INF/templates/main.xhtml">
<ui:define name="title">
#{bundle['application.weatherpage.title']}
</ui:define>
<ui:define name="content">
<h:form>
#{bundle['application.welcome']}, #{appUser.firstName} #{appUser.lastName}! <br/><br/>
#{bundle['application.weatherpage.currentInfo']} for #{appUser.zipCode}:
<mashup:yahooWeather temperatureType="c" locationCode="#{appUser.zipCode}"/> <br/><br/>
<h:commandLink value="#{bundle['application.weatherpage.logout']}"
action="#{weatherBacking.logout}"></h:commandLink> <br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
天气页面向用户和 Yahoo!使用 Mashups4JSF 库的 yahooWeather 组件(code.google.com/p/mashups4jsf/
)来检索天气信息。
注意 Mashups4JSF 是一个开源项目,旨在集成 Mashup 服务和 JavaServer Faces 应用。使用 Mashups4JSF,JSF 开发人员将能够通过使用简单的标记来构建丰富的定制 Mashups。Mashups4JSF 还允许通过用@Feed 注释对应用域类进行注释,将 Java 企业应用数据作为 Mashup feeds 导出。更多信息请查看项目主页:
code.google.com/p/mashups4jsf/
。
yahooWeather 组件使您能够查看世界上特定位置的当前天气状况(使用 Yahoo!引擎盖下的天气服务)使用其邮政编码。它有两个主要属性,如表 10-1 所示。
表 10-1 。Mashups4JSF yahooWeather 组件
|
组件属性
|
描述
|
| --- | --- |
| 位置代码 | 该位置的邮政编码 |
| 温度类型 | 以华氏(f
)或摄氏(c
)为单位的温度。默认是c
。 |
为了在我们的 JSF 应用中配置 Mashups4JSF,我们需要向您的 web 应用的 lib 文件夹添加两个 jar:
- Mashups4JSF 1.0.0 核心 jar。
- 罗马 0.9 jar。
如果我们的应用是一个 Maven 应用,我们需要将这些 jar 添加到应用的 pom.xml 中,如清单 10-5 所示。
清单 10-5。POM . XML 中的 Mashups4JSF 依赖
<project ...>
...
<dependencies>
...
<dependency>
<groupId>com.googlecode.mashups4jsf</groupId>
<artifactId>mashups4jsf-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>0.9</version>
</dependency>
</dependencies>
<repositories>
...
<repository>
<id>googlecode.com</id>
<url>[`mashups4jsf.googlecode.com/svn/trunk/mashups4jsf-repo</url`](http://mashups4jsf.googlecode.com/svn/trunk/mashups4jsf-repo</url) >
</repository>
</repositories>
</project>
将 Mashups4JSF jars 添加到应用的依赖项之后,我们可以将它包含在 XHTML 页面中,如下所示:
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:mashup="http://code.google.com/p/mashups4jsf/">
天气页面有一个 logout CommandLink,其操作与 WeatherBacking bean 类的 logout 方法绑定在一起。清单 10-6 显示了 WeatherBacking bean 类。
清单 10-6。 逆风豆类
package com.jsfprohtml5.weather.backing;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named
@RequestScoped
public class WeatherBacking extends BaseBacking {
public String logout() {
getSession().invalidate();
return "/home.xhtml?faces-redirect=true";
}
}
在 logout()方法中,会话被无效,用户被转到主页。请注意,所有应用页面都使用/WEB-INF/templates 文件夹下的 main.xhtml 模板。清单 10-7 显示了 main.xhtml 模板页面。
清单 10-7。 main.xhtml 模板页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<title><ui:insert name="title">#{bundle['application.defaultpage.title']}</ui:insert></title>
<link href="#{request.contextPath}/css/main.css" rel="stylesheet" type="text/css"/>
</h:head>
<h:body>
<div id="container">
<div id="header">
<ui:insert name="header">
<h1>#{bundle['application.defaultpage.header.content']}</h1>
</ui:insert>
</div>
<div id="content">
<ui:insert name="content">
#{bundle['application.defaultpage.body.content']}
</ui:insert>
</div>
<div id="footer">
<ui:insert name="footer">
#{bundle['application.defaultpage.footer.content']}
</ui:insert>
</div>
</div>
</h:body>
</html>
该模板使用 main.css 样式文件,它有三个主要部分:页眉、页脚和内容。应用中的不同页面(主页、天气和注册页面)应该替换这些内容。应用的文本在 messages.properties 文件中具体化,如清单 10-8 中的所示。
清单 10-8。 messages.properties 文件
user.id = Username
user.password = Password
user.email = Email
user.fname = First name
user.lname = Last name
user.profession = Profession
user.zipCode = Zip code
user.id.validation = You need to enter a username
user.password.validation = You need to enter a password
user.email.validation = You need to enter an email
user.email.invalid = Invalid Email
user.fname.validation = You need to enter first name
user.lname.validation = You need to enter last name
user.zipCode.validation = You need to enter zip code
user.profession.profession1 = Software Engineer
user.profession.profession2 = Project Manager
user.profession.profession3 = Other
application.next = Next
application.back = Back
application.cancel = Cancel
application.finish = Finish
application.login = Login
application.loginpage.title = Login page
application.loginpage.register = New user? register now!
application.welcome = Welcome
application.weatherpage.title = Weather page
application.weatherpage.logout = Logout
application.weatherpage.currentInfo = Current Weather Information
application.register = Register
application.register.title = Registration page
application.register.return = Back to home
application.defaultpage.title = Default Title
application.defaultpage.header.content = Welcome to the weather application
application.defaultpage.body.content = Your content here ...
application.defaultpage.footer.content = Thanks for using the application
模板的 main.css 文件用于处理外观和布局,如清单 10-9 所示。
清单 10-9。 main.css 样式文件
h1, p, body, html {
margin:0;
padding:0;
font-family: sans-serif;
}
body {
background-color: #B3B1B2;
}
#container {
width:100%;
}
a {
font-size: 12px;
}
#header {
background-color: #84978F;
padding: 50px;
}
#header h1 {
margin-bottom: 0px;
text-align: center;
}
#content {
float: left;
margin: 10px;
height: 400px;
width: 100%;
}
#footer {
clear:both; /*No floating elements are allowed on left or right*/
background-color: #84978F;
text-align:center;
font-weight: bold;
padding: 10px;
}
.errorMessage {
font-size: 12px;
color: red;
font-family: sans-serif;
}
为了保护天气页面,它被放在一个名为("/protected ")的自定义文件夹中,并创建了一个自定义 JSF 相位监听器来保护页面,如清单 10-10 中的所示。
清单 10-10。 授权监听器类
package com.jsfprohtml5.weather.util;
import javax.faces.application.NavigationHandler;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
public class AuthorizationListener implements PhaseListener {
@Override
public void afterPhase(PhaseEvent event) {
FacesContext context = event.getFacesContext();
NavigationHandler navigationHandler = context.getApplication().getNavigationHandler();
String currentPage = context.getViewRoot().getViewId();
boolean isProtectedPage = currentPage.contains("/protected/");
//Restrict access to protected pages ...
if (isProtectedPage) {
navigationHandler.handleNavigation(context, null, "/home?faces-redirect=true");
}
}
@Override
public void beforePhase(PhaseEvent event) {
//Nothing ...
}
@Override
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
}
AuthorizationListener 阶段侦听器禁止任何用户直接访问受保护文件夹中的页面。
在下一节中,我们将浏览注册页面,以了解如何利用 JSF 2.2 Faces 流在我们的天气应用中实现注册流行为。
利用面流
正如您从第五章中了解到的,JSF 2.2 中引入了 Faces Flow 来支持 JSF 应用中的流管理。过去,为了在 JSF 应用中实现流,JSF 开发人员要么使用额外的框架,如 Spring Web Flow 或 ADF Task Flows,要么使用 HTTP 会话手动实现。使用 HTTP 会话手动实现它不是一种有效的实现方式,因为 JSF 开发人员必须在流完成或退出后处理会话清理。
在 JSF Faces Flow 中,开发人员可以在一组相关页面(或视图或节点)上定义 流,并定义好入口点和出口点。在天气应用中,我们将流页面打包在单个目录(/registration)中,以方便遵守 JSF 流公约规则,这些规则如下:
- 流目录中的每个 XHTML 文件都充当流的视图节点。
- 流的开始节点是视图,其名称与流的名称(registration.xhtml)相同。
- 流目录中页面之间的导航被视为流内的导航。
- 导航到流程目录之外的视图被认为是流程的出口。
最后,为了定义 Faces 流,您应该在 Faces 配置文件(faces-config.xml)中声明它,如清单 10-11 所示。
清单 10-11。 在 Faces 配置文件 中定义 Faces 流
<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2"
FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
<flow-definition id="registration">
<flow-return id="flowReturn">
<from-outcome>/home</from-outcome>
</flow-return>
</flow-definition>
...
</faces-config>
为了定义流,您使用
清单 10-12。 registration.xhtml 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">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">
<ui:composition template="/WEB-INF/templates/main.xhtml">
<ui:define name="title">
#{bundle['application.register.title']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.id']}"></h:outputText>
<h:inputText id="userID"
value="#{flowScope.id}"
required="true"
requiredMessage="#{bundle['user.id.validation']}">
</h:inputText>
<h:message for="userID" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.password']}"></h:outputText>
<h:inputSecret id="password"
value="#{flowScope.password}"
required="true"
requiredMessage="#{bundle['user.password.validation']}">
</h:inputSecret>
<h:message for="password" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.email']}"></h:outputText>
<h:inputText id="email"
value="#{flowScope.email}"
required="true"
requiredMessage="#{bundle['user.email.validation']}"
validatorMessage="#{bundle['user.email.invalid']}">
<f:validateRegex pattern="[\w\.-]*[a-zA-Z0-9_]@[\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]*[a-zA-Z]"/>
</h:inputText>
<h:message for="email" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.cancel']}" action="flowReturn"
immediate="true"/>
<h:commandButton value="#{bundle['application.next']}" action="extraInfo"/> <br/>
</h:form>
</ui:define>
</ui:composition>
</html>
使用#{flowScope} EL 对象,我们可以将对象存储在流作用域中,它相当于 facesContext.getApplication()。getFlowHandler()。getCurrentFlowScope() API。表达式#{flowScope.id}、#{flowScope.password}和#{flowScope.email}与用户 id、密码和电子邮件输入字段绑定。另一个需要注意的重要事情是“cancel”command button 的动作,它被设置为注册流返回 ID(“flow return”);这意味着当点击“取消”命令按钮时,用户将被转到主页。清单 10-13 显示了注册流程(extraInfo.xhtml)页面中的第二页。
清单 10-13。 extraInfo.xhtml 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">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">
<ui:composition template="/WEB-INF/templates/main.xhtml">
<ui:define name="title">
#{bundle['application.register.title']}
</ui:define>
<ui:define name="content">
<h:form>
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.fname']}"></h:outputText>
<h:inputText id="fname"
value="#{flowScope.fname}"
required="true"
requiredMessage="#{bundle['user.fname.validation']}">
</h:inputText>
<h:message for="fname" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.lname']}"></h:outputText>
<h:inputText id="lname"
value="#{flowScope.lname}"
required="true"
requiredMessage="#{bundle['user.lname.validation']}">
</h:inputText>
<h:message for="lname" styleClass="errorMessage"/>
<h:outputText value="#{bundle['user.profession']}"></h:outputText>
<h:selectOneMenu id="profession"
value="#{flowScope.profession}">
<f:selectItem itemLabel="#{bundle['user.profession.profession1']}" itemValue="SE"/>
<f:selectItem itemLabel="#{bundle['user.profession.profession2']}" itemValue="PM"/>
<f:selectItem itemLabel="#{bundle['user.profession.profession3']}" itemValue="OT"/>
</h:selectOneMenu>
<h:message for="profession" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.cancel']}" action="flowReturn"
immediate="true" />
<h:commandButton value="#{bundle['application.back']}" action="registration"
immediate="true" />
<h:commandButton value="#{bundle['application.next']}" action="final"/> <br/>
</h:form>
</ui:define>
</ui:composition>
</html>
表达式#{flowScope.fname}、#{flowScope.lname}和#{flowScope.profession}与用户名、姓和职业输入字段绑定。需要注意的一点是,只要用户在流页面之间导航,流数据就是活动的;这意味着如果用户单击 back 按钮转到初始注册,那么他将能够看到他之前在初始页面中输入的数据。清单 10-14 显示了注册流程(final.xhtml)页面的最后一页。
清单 10-14。final . XHTML 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html">
<ui:composition template="/WEB-INF/templates/main.xhtml">
<ui:define name="title">
#{bundle['application.register.title']}
</ui:define>
<ui:define name="content">
<h:form prependId="false">
<h:panelGrid columns="3">
<h:outputText value="#{bundle['user.zipCode']}"></h:outputText>
<h:inputText id="woeid"
value="#{flowScope.zipCode}"
required="true"
requiredMessage="#{bundle['user.zipCode.validation']}">
</h:inputText>
<h:message for="woeid" styleClass="errorMessage"/>
</h:panelGrid>
<h:commandButton value="#{bundle['application.cancel']}"
immediate="true" action="flowReturn" />
<h:commandButton value="#{bundle['application.back']}" immediate="true" action="extraInfo"/>
<h:commandButton value="#{bundle['application.finish']}" action="#{registrationBacking.register}"/> <br/>
<h:messages styleClass="errorMessage"/>
</h:form>
</ui:define>
</ui:composition>
</html>
最后,#{flowScope.zipCode}与用户邮政编码输入文本绑定。当用户单击“Finish”命令按钮时,将调用 RegistrationBacking bean 的 register()方法在应用中注册用户。清单 10-15 显示了注册支持 bean。
清单 10-15。 注册后台 Bean 类
package com.jsfprohtml5.weather.backing;
import com.jsfprohtml5.weather.model.AppUser;
import com.jsfprohtml5.weather.model.UserExistsException;
import com.jsfprohtml5.weather.model.UserManagerLocal;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.inject.Named;
@Named
@RequestScoped
public class RegistrationBacking extends BaseBacking {
@EJB
private UserManagerLocal userManager;
public String register() {
FacesContext context = FacesContext.getCurrentInstance();
Map<Object, Object> flowScope = context.getApplication().getFlowHandler().getCurrentFlowScope();
AppUser appUser = new AppUser();
appUser.setId((String) flowScope.get("id"));
appUser.setPassword((String) flowScope.get("password"));
appUser.setEmail((String) flowScope.get("email"));
appUser.setFirstName((String) flowScope.get("fname"));
appUser.setLastName((String) flowScope.get("lname"));
appUser.setProfession((String) flowScope.get("profession"));
appUser.setZipCode((String) flowScope.get("zipCode"));
try {
userManager.registerUser(appUser);
} catch (UserExistsException ex) {
Logger.getLogger(RegistrationBacking.class.getName()).log(Level.SEVERE, null, ex);
context.addMessage(null, new FacesMessage(USERNAME_ALREADY_EXISTS));
return null;
} catch (Exception ex) {
Logger.getLogger(RegistrationBacking.class.getName()).log(Level.SEVERE, null, ex);
context.addMessage(null, new FacesMessage(SYSTEM_ERROR));
return null;
}
return "flowReturn";
}
...
}
RegistrationBacking bean 是一个处理用户注册的 Backing bean。为了获得流数据,使用 context.getApplication()检索流范围。getFlowHandler()。getCurrentFlowScope() API。AppUser JPA 实体类用来自流范围的用户数据进行实例化和传播,然后传递给 UserManager EJB 的 regiserUser()方法。如果注册成功,则返回注册流程,并将用户转到主页。
AppUser JPA 实体类将在下一节中说明。
构成受管 bean(JPA 实体 bean)
在天气应用中,我们有一个单独的托管 bean(和 JPA 实体类),它是 AppUser 类。清单 10-16 显示了 AppUser 类。
清单 10-16。 AppUser 实体类
package com.jsfprohtml5.weather.model;
import java.io.Serializable;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Entity
@Table(name = "APP_USER")
@Named
@RequestScoped
public class AppUser implements Serializable {
private static final long serialVersionUID = 134523456789194332L;
@Id
@NotNull
@Size(min = 1, max = 64)
@Column(name = "ID")
private String id;
@NotNull
@Size(min = 1, max = 32)
@Column(name = "FIRST_NAME")
private String firstName;
@NotNull
@Size(min = 1, max = 32)
@Column(name = "LAST_NAME")
private String lastName;
@NotNull
@Size(min = 1, max = 32)
@Column(name = "PASSWORD")
private String password;
@NotNull
@Size(min = 1, max = 32)
@Column(name = "PROFESSION")
private String profession;
@NotNull
@Size(max = 64)
@Column(name = "EMAIL")
private String email;
@NotNull
@Size(max = 32)
@Column(name = "ZIP_CODE")
private String zipCode;
public AppUser() {
}
public AppUser(String id) {
this.id = id;
}
public AppUser(String id, String firstName, String lastName, String password, String profession, String zipCode) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.password = password;
this.profession = profession;
this.zipCode = zipCode;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getProfession() {
return profession;
}
public void setProfession(String profession) {
this.profession = profession;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getZipCode() {
return zipCode;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
@Override
public String toString() {
return "ID = " + id;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof AppUser)) {
return false;
}
AppUser other = (AppUser) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
}
AppUser 是一个 JPA 实体类。@Entity 批注用于将类标记为实体类;@Table 注释用于显式设置 JPA 实体映射到的表名。@Named 和@RequestScoped 都用于在请求范围内将 AppUser 类声明为 CDI 托管 bean。如果我们研究 AppUser 类属性,我们会发现以下 JPA 注释:
- @Id 注释用于将类属性标记为唯一标识符。
- @Column 注释用于显式设置 JPA 类属性映射到的列名。
在下一节中,我们将看到如何配置 JPA 持久性单元并创建用户管理器 EJB。
应用后端(EJB 3.2 + JPA 2.1)
现在我们来看关于应用后端的部分,它使用 EJB 3.2 和 JPA 2.1,它们是 Java EE 7 平台的一部分。在上一节中,我们已经看到了应用的单个 JPA 实体(AppUser)类,但是我们不知道如何使用实体 bean 来执行不同的数据库操作。为了执行数据库操作,我们需要定义 persistence.xml 文件,如清单 10-17 所示。
清单 10-17。persistence . XML 文件
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" FontName">http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="weatherUnit" transaction-type="JTA">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/weatherDB</jta-data-source>
</persistence-unit>
</persistence>
在 persistence.xml 文件中(在/resources/META-INF 下),定义了持久性单元名称“weatherUnit”,事务类型设置为“JTA”(Java 事务 API)。在持久性单元内部,我们定义了 JPA provider 类(org . eclipse . persistence . JPA . persistence provider)和
清单 10-18。 UserManager 本地 EJB 接口
package com.jsfprohtml5.weather.model;
import javax.ejb.Local;
@Local
public interface UserManagerLocal {
public AppUser getUser(String userID, String password);
public void registerUser(AppUser user) throws UserExistsException;
}
用户管理器 EJB 实现了用户管理器本地接口,如清单 10-19 所示。
清单 10-19。 用户经理 EJB
package com.jsfprohtml5.weather.model;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
@Stateless
public class UserManager implements UserManagerLocal {
@PersistenceContext(unitName = "weatherUnit")
EntityManager em;
@Override
public AppUser getUser(String userID, String password) {
Query query = em.createQuery("select appUser from AppUser appUser where "
+ "appUser.id = :id and appUser.password = :password");
query.setParameter("id", userID);
query.setParameter("password", password);
List<AppUser> result = query.getResultList();
if (result != null && result.size() > 0) {
return result.get(0);
}
return null;
}
@Override
public void registerUser(AppUser appUser) throws UserExistsException {
Query query = em.createQuery("select appUser from AppUser appUser where "
+ "appUser.id = :id");
query.setParameter("id", appUser.getId());
List<AppUser> result = query.getResultList();
if (result != null && result.size() > 0) {
throw new UserExistsException();
}
em.persist(appUser);
}
}
@Stateless annotation 将 UserManager 类定义为无状态会话 EJB。@PersistenceContext 注释用于注入容器管理的实体管理器实例。使用注入的实体管理器实例,我们将能够执行数据库操作。在用户管理器 EJB 中,有两种主要方法:
- getUser()方法,,该方法使用用户名和密码从数据库中检索用户。如果用户不存在,则返回 null。
- registerUser()方法,该方法执行以下操作:
- 如果用户 ID 已经存在,它将抛出 UserExistsException。
- 如果用户 ID 不存在,那么用户将被保存在数据库中。
清单 10-20 显示了用户存在异常类。
清单 10-20。userixsexception 类
package com.jsfprohtml5.weather.model;
public class UserExistsException extends Exception {
}
UserExistsException 是一个简单的自定义异常,它扩展了 Exception 类。
天气应用是在 GlassFish 版下开发的。清单 10-21 显示了定义应用数据源的 glassfish-resources.xml。
清单 10-21。 天气应用 glassfish-resources.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource
Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
<jdbc-connection-pool ...>
<property name="serverName" value="localhost"/>
<property name="portNumber" value="1527"/>
<property name="databaseName" value="weatherDB"/>
<property name="User" value="weather"/>
<property name="Password" value="password"/>
<property name="URL" value="jdbc:derby://localhost:1527/weatherDB"/>
<property name="driverClass" value="org.apache.derby.jdbc.ClientDriver"/>
</jdbc-connection-pool>
<jdbc-resource enabled="true" jndi-name="jdbc/weatherDB" object-type="user" pool-name="derby_net_weatherDB_weatherPool"/>
</resources>
为了在 glassfish 4 中添加 glassfish-resources.xml 的已定义资源,您需要通过从服务器的 bin 目录运行以下命令来启动 GlassFish 服务器。
> asadmin start-domain
服务器启动后,您可以使用 asadmin add-resources 命令,如下所示,以便在服务器中添加已定义的资源。
> asadmin add-resources <<full path>>/glassfish-resources.xml
运行前面的命令后,资源将被添加到您的 GlassFish 服务器中。最后清单 10-22 显示了我们在 weatherDB 中的单个 APP_USER 表。
清单 10-22。APP _ 用户表 DDL 脚本
CREATE TABLE APP_USER (
ID VARCHAR(64) PRIMARY KEY,
FIRST_NAME VARCHAR(32),
LAST_NAME VARCHAR(32),
PASSWORD VARCHAR(32),
PROFESSION VARCHAR(32),
EMAIL VARCHAR(64),
ZIP_CODE VARCHAR(32)
);
weatherDB 是一个 JavaDB Derby 数据库;它包含在(src/main/database)目录下的应用源代码中,供您参考。为了在 GlassFish 服务器中安装数据库,如果 GlassFish 服务器正在运行,请按如下方式停止它:
> asadmin stop-domain domain1
我们在这里假设您的 GlassFish 域名是(domain1)。停止服务器后,还要停止 GlassFish Java DB,如下所示:
> asadmin stop-database
停止服务器和 Java DB 后,将位于(src/main/database)目录下的 weatherDB 目录复制到您的([GlassFish server]/GlassFish/databases)目录,然后启动服务器和 Java DB。Java DB 可以使用以下命令启动:
> asadmin start-database
启动服务器和 Java DB 后,您可以在 GlassFish 服务器中部署天气应用并开始使用它。
注意天气应用是一个 Maven web 项目,因此为了构建它,您可以使用 mvn clean install 命令,然后在您的 GlassFish 4 服务器中部署输出 war 文件(weather-1.0-SNAPSHOT.war)。请注意,输出 war 文件将位于目标目录下。为了正确运行 Maven 命令,请确保 JAVA_HOME 指向安装在您的操作系统中的 Java 7 目录。
摘要
在本章中,你详细地学习了如何在 Java EE 7 环境中创建一个基本的 JSF 2.2 应用。你了解了如何构建 JSF 2.2 应用。您学习了如何利用 JSF 2.2 Faces 流来处理公共页面之间的流。最后,您了解了如何利用不同的 Java EE 7 技术(CDI、JPA 2.1 和 EJB 3.2)来促进 JSF 应用中的 bean 管理、事务管理和持久性。
十一、JSF2 高级主题
本章收集了 JSF 应用作者在编写现实生活中的 JSF 应用时必须考虑的高级主题。我们还将看看如何使用< f:ajax >标签来 ajax 化并改善用户体验。Ajax 部分之后是使用 JavaScript 开发 JSF JavaScript API 的例子。最后,如果没有彻底的测试,你就无法构建一个真实世界的应用。我们将通过 JBoss 社区研究由 Red Hat 赞助的 Arquillian 测试框架。
JSF 应用的设计考虑因素
本节重点介绍在构建 JSF 应用时应该考虑的设计注意事项。我们将触及最小化会话数据使用的重要性,如何实现安全性,在哪里保存状态视图,以及最后托管 bean 作用域与 CDI 作用域的比较。
最小化会话范围的使用
当开发 JSF 应用时,你应该特别小心使用会话范围的 bean。将对象存储在用户会话中可能很诱人,因为在整个应用中都可以方便地使用它。这种便利的问题是每个用户的内存占用越来越多。您可能会得到大型的会话对象,直到用户结束会话时才被收集。这将限制并发用户的数量,因为访问系统的用户越多,应用服务器需要的物理内存就越多,从而使应用不可伸缩。会话范围的对象应该只用于存储应该从用户会话开始到结束都有效的数据。贯穿整个用户会话的公共数据可以是用户名、人名、登录时间和首选项。您应该避免使用会话范围的对象来存储有关主-详细信息中选定对象的数据,或者存储可能非常大的二进制对象,如用户配置文件图片。根据经验,在您的 JSF 应用中应该只有一个会话范围的受管 bean。如果您觉得需要多个会话范围的 beans,请认真考虑您的应用的架构。
提示对于开发人员来说,直到软件开发生命周期结束时才解决应用性能问题是很常见的。然而,当您在 JSF 中使用会话对象时,您必须在开发过程的早期和整个过程中注意应用的性能测试和概要分析。在模拟多个用户会话时分析内存占用尤其重要。有许多可用的 Java 概要文件。我们建议您使用与您的开发环境很好地集成的分析器,以便在开发过程中尽可能容易地分析您的应用。如果 profiler 使用起来很慢而且很麻烦,您可能会避免使用它,并且您也不会尽早发现可伸缩性问题。一些 ide 有内置的分析器,可以很容易地分析内存和 CPU 消耗,如图 11-1 所示。要模拟多个用户会话,您可以使用像 Apache jMeter(
jmeter.apache.org
)这样的开源工具,在这里您可以构建一个测试计划,通过在一段时间内生成多个线程来模拟多个用户。
图 11-1 。具有内置探查器的 IDE (NetBeans)示例
当在集群环境中操作时,使用会话范围的 beans 还会增加会话复制的复杂性。在集群环境中,流量通常会在集群中的可用节点之间保持平衡。即使群集试图确保单个用户会话从启动它的节点提供服务,您仍然会遇到这样的情况:一个节点出现故障,流量必须重定向到另一个节点。为了避免用户会话在节点故障期间丢失,必须将会话设置为在节点之间复制。幸运的是,这由应用服务器负责,但是这是在使用会话范围的 beans 时必须解决的另一个问题。
容器管理的安全性
许多应用需要使用用户名和密码或通过客户端证书来保护部分或全部功能。Java EE 规范的创建者定义了一个容器管理的安全框架 ,使得应用开发人员更容易保护他们的应用。或者,应用开发人员将实现一个自定义的安全模型,也称为应用管理的安全性。实现应用管理的安全性需要大量的工作和技能,最终它可能无法提供通过容器管理的安全性无法实现的任何功能或保护。
容器管理的安全性基于一种模型,其中资源(URL)由定义的用户角色保护。登录后,用户被分配到用户角色,剩下的工作由应用服务器负责。应用开发人员唯一需要关心的是定义哪些资源受哪些用户角色的保护。当容器检测到用户未被授权访问所请求的资源时,它会自动将用户定向到登录机制。登录机制可以是基本身份验证、表单身份验证或客户端证书身份验证。基本身份验证将通过浏览器中的本地用户名和密码对话框提示用户输入用户名和密码。表单认证允许应用开发人员提供自己的登录表单,该表单至少必须包含用户名和密码的输入字段。最后,客户端证书身份验证使用 X.509 证书来执行公钥身份验证。安全基础设施的其余部分对应用开发人员来说是完全隐藏的。应用开发人员通过指定安全领域与应用服务器进行通信。安全领域是在应用外部的应用服务器中配置的。安全领域可以指定用户位于 SQL 数据库、LDAP 目录甚至纯文本文件中。应用服务器负责提供安全领域。应用服务器通常为开发人员提供接口来实现他们自己的安全领域,以防您对用户应该如何登录有特殊要求。例如,您可以实现一个安全领域,通过在线服务(如 Google 或 Yahoo)进行身份验证。该实现是应用服务器定制的,但是抽象出了如何处理身份验证。这使得基于容器的安全性非常灵活,并降低了应用开发人员的复杂性。应用开发人员在/WEB-INFO/web.xml 中配置基于容器的安全性。所有符合 JEE 标准的 web 应用服务器都支持容器管理安全性的概念,因此是可移植的。
清单 11-1。 基于容器的安全性,为具有几个受保护资源的简单应用配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" FontName">http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
<!--
Security Contraints (protection) for the CUSTOMER role.
-->
<security-constraint>
<display-name>Customer Constraints</display-name>
<web-resource-collection>
<web-resource-name>MyAccount</web-resource-name>
<description>Account Pages</description>
<url-pattern>/myaccount/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<description/>
<role-name>CUSTOMER</role-name>
</auth-constraint>
<!--
This section switches the transport from HTTP to HTTPS, thereby
encrypting the traffic between the browser and server.
-->
<user-data-constraint>
<description>Must switch to HTTPS as the page may contain confidential information.</description>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!--
Security Contraints (protection) for the ADMINISTRATOR role.
-->
<security-constraint>
<display-name>Administrator Constrains</display-name>
<web-resource-collection>
<web-resource-name>AdministratorSection</web-resource-name>
<description>Administrator pages</description>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<description/>
<role-name>ADMINISTRATOR</role-name>
</auth-constraint>
</security-constraint>
<!--
Specify which Login Mechanism and Security Realm to use. The details of
the Realm itself is configured on the application server (outside the
application).
-->
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>CRMRealm</realm-name>
</login-config>
<!--
Definition of the Security Roles used in the application
-->
<security-role>
<description>A customer accessing the application</description>
<role-name>CUSTOMER</role-name>
</security-role>
<security-role>
<description>An administrator of the application</description>
<role-name>ADMINISTRATOR</role-name>
</security-role>
</web-app>
国家储蓄
正如我们在第五章中提到的,JSF 2.2 引入了无状态视图的概念。你可以通过指定一个视图应该是瞬态的来使一个视图无状态,如清单 5-23 所示。这提高了应用的性能,因为它不必存储请求之间的视图状态。这显然不适用于所有视图,因为我们必须为一些视图保留状态,但是您应该仔细考虑是否您的每个视图都需要保留状态。
在您确实需要状态保存的情况下,您应该在服务器端而不是客户端启用状态保存。当视图状态保存在客户端时,它被序列化为一个字符串,并存储在一个名为 javax.faces.ViewState 的隐藏输入字段中。首先,每次处理视图时,序列化和反序列化视图状态都会产生开销。其次,您将使用更多的带宽在浏览器和服务器之间来回发送状态。使用客户端状态保存的好处是可以最小化应用的内存占用。您应该仔细考虑什么对您的应用最重要。清单 11-2 显示了用于在/WEB-INF/web.xml 中配置状态保存的上下文参数
清单 11-2。 启用服务器端视图状态保存示例
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
...
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<!-- Replace server with client below to enable client-side state saving -->
<param-value>server</param-value>
</context-param>
...
</web-app>
上下文和依赖注入(CDI)
当您开发 JSF 2.x 应用时,您可以选择使用内置的托管 bean 作用域(@RequestScoped、@SessionScoped 和@ViewScoped)或使用 JSR 299 中定义的上下文和依赖注入(CDI)服务 。CDI 提供了一个架构,其中所有的 Java EE 组件(Servlets、Enterprise JavaBeans、managed beans)都遵循相同的编程模型和生命周期,并具有定义良好的作用域。它允许 Java EE 组件松散耦合,并在需要的地方注入。CDI 已经被证明是如此成功,以至于内置的托管作用域将在 JSF 的未来版本中被弃用。如果你开始开发一个新的应用,你应该从一开始就使用 CDI 服务。如果您正在处理一个必须继续维护的现有应用,那么您应该开始计划从内置受管 bean 作用域到 CDI 作用域的迁移。
在 JSF 启用 CDI 很简单。创建/WEB-INF/beans.xml,指定 CDI beans 应该如何被发现,如清单 11-3 所示。一旦创建了文件,就可以开始在应用中使用 CDI 作用域。
清单 11-3。 /WEB-INF/beans.xml 在您的 JSF 应用中启用 CDI
<?xml version="1.0" encoding="UTF-8"?>
<beans FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="annotated">
</beans>
警告不要试图混合搭配 CDI 作用域和内置的 JSF 管理的 bean 作用域。当你的 JSF bean 因为活跃在不同的作用域而开始行为不端时,这将会导致混乱。
Ajax 化 JSF 应用
在 JSF 2.0 之前,你必须实现自己对 Ajax 的支持,或者使用第三方库,比如 RichFaces。从 JSF 2.0 开始,Ajax 就已经被使用< f:ajax >标签或 JavaScript API 支持了。
使用 <标签
标签为配置 Ajax 行为提供了一个小而强大的属性选择。表 11-1 概述了可用的属性。
表 11-1 。< f:ajax/ >标签的属性
|
属性
|
描述
|
| --- | --- |
| disabled
| 确定是否不应该呈现 Ajax 行为。默认值为 false。 |
| delay
| Ajax 请求延迟的毫秒数。如果在延迟期间有多个请求进入,那么只有最后一个请求会被执行。 |
| event
| 指定 Ajax 行为应该响应的 DOM 事件的字符串。请务必注意,它只是事件名称(例如,“click”而不是“onclick”)。必须使用标记从 UIComponent 发出该事件;否则 Ajax 行为永远不会被触发。 |
| execute
| 当 Ajax 行为被触发时应该执行的以空格分隔的组件列表。 |
| immediate
| 确定 Ajax 行为是在应用请求值阶段(true)还是在调用应用阶段(false)被触发。默认值为 false。 |
| listener
| 对应该处理由 Ajax 行为触发的 AjaxBehaviorEvent 的侦听器的引用。 |
| onevent
| JavaScript 函数的名称,该函数应处理执行 Ajax 请求时发出的事件。 |
| onerror
| JavaScript 函数的名称,该函数应处理执行 Ajax 请求时发出的错误。 |
| render
| Ajax 请求成功时应该(重新)呈现的以空格分隔的组件列表。 |
清单 11-4 展示了一个使用< f:ajax >标签根据输入文本组件中输入的内容更新输出面板的例子。Ajax 行为与 keyup DOM 事件挂钩(键入一个键)。当接收到事件时,将执行输入文本组件,输入的值存储在 ajaxDemo 托管 bean 的 outputMessage 属性中。当请求返回成功时,将重新呈现输出消息面板,并在面板中显示输入文本中输入的消息。该示例还通过将所有 Ajax 事件传递给一个名为 processInput 的 JavaScript 函数来演示 onevent 属性的用途,该函数在请求开始和成功完成时切换 Ajax 微调器的可见性。
清单 11-4。 使用< f:ajax/ >标签在更新文本字段时注册 ajax 行为的例子
<h:form id="my-message">
<h:outputLabel value="Your message" for="input-message" />
<h:inputText id="input-message" value="#{ajaxDemo.outputMessage}">
<f:ajax event="keyup"
onevent="function(data) { processInput(data, 'my-message:busy'); }"
render="output-message" execute="@this" />
</h:inputText>
<h:graphicImage id="busy" library="images" name="spinner.gif" style="display: none; float: left;" />
<h:panelGroup id="output-message">#{ajaxDemo.outputMessage}</h:panelGroup>
</h:form>
<script type="text/javascript">
// Handle for onevent
function processInput(data, id) {
if (data.status === 'begin') {
toggle_visibility(id);
} else if (data.status === 'success') {
toggle_visibility(id);
}
}
// Utility function for toggling the visibility of an element
function toggle_visibility(id) {
var e = document.getElementById(id);
if (e.style.display == 'block')
e.style.display = 'none';
else
e.style.display = 'block';
}
</script>
您会注意到我们在 execute 属性中使用了一个特殊的值。execute 和 render 属性都支持一些特殊的值。这些特殊值是为了方便起见,这样您就不必为通常受 Ajax 行为影响的组件输入特定的组件标识符。这些值在表 11-2 中列出。
表 11-2 。执行/呈现特殊值
|
关键字
|
描述
|
| --- | --- |
| @all
| 执行或渲染所有组件 |
| @none
| 不要执行或呈现任何组件 |
| @this
| 执行或呈现触发 Ajax 行为的组件 |
| @form
| 以触发 Ajax 行为的组件的形式执行或呈现所有组件 |
使用 JavaScript API
JSF 附带了一个 JavaScript API ,可以一起使用,也可以代替< f:ajax >标签。JavaScript API 在 jsf 名称空间下的所有页面上都是可用的。像< f:ajax >一样,JavaScript API 可以用来发起 ajax 请求。JavaScript API 也可以用于监控 Ajax 请求和处理错误。
使用 JavaScript API 发起 Ajax 请求的方法签名如清单 11-5 所示,表 11-3 解释了该方法的输入参数。
清单 11-5。 使用 JavaScript 发起 Ajax 请求的方法签名
jsf.ajax.request(source, event, {options});
表 11-3 。jsf.ajax.request 的输入参数
清单 11-6 展示了一个使用 JavaScript API 执行更新面板网格脚本的按钮示例。
清单 11-6。 使用 JavaScript API 更新面板网格
<h:panelGroup id="clicks" layout="block">
<h:outputLink id="refresh" onclick="refreshClicks(this, event); return false;">
Refresh:
</h:outputLink>
<h:outputText value="#{javaScriptApiDemo.clicks}" />
</h:panelGroup>
<script type="text/javascript">
function refreshClicks(source, event) {
jsf.ajax.request(source, event, {render: 'clicks'});
}
</script>
在清单 11-6 中,我们看到 JavaScript refreshClicks 函数在刷新输出链接的 onclick 事件中被调用。它将自己作为源和 onclick 生成的事件传递给刷新函数。它以返回 false 结束,这样单击链接就不会调用完整的 HTTP 请求。所有神奇的事情都发生在 refreshClicks 函数中。这里使用传递给函数的源和事件来触发 Ajax 请求。Ajax 请求有一个单独的选项,声明从 Ajax 请求返回时,应该呈现带有 ID clicks 的元素。ID 为 clicks 的元素是一个面板组,包含链接和从名为 javaScriptApiDemo 的受管 bean 中检索的值。如果受管 bean 中的点击值增加了,refreshClicks 函数将更新显示并显示当前的点击次数。
提示在 JSF 应用中修改 Ajax 请求时,你最终会得到一个 httpError,说明“Http 传输返回一个 0 状态码”。这通常是混合 ajax 和完整请求的结果。出于性能和数据完整性的原因,这通常是不希望的”(见图 11-2 )。这条警告消息可能看起来很隐晦,但它只是说,您试图在执行完整的 HTTP 请求时执行 Ajax 请求。如果您忘记在调用 Ajax 请求的 onclick 事件的末尾包含 return false,就会发生这种情况
错:
您还可以通过使用清单 11-7 中所示的执行选项,在选定的组件上执行请求生命周期。
图 11-2 。同时执行 Ajax 和完整 HTTP 请求时显示错误
清单 11-7。 使用执行选项在选定的组件上执行 JSF 请求生命周期
<h:form id="my-name-form">
<h:outputLink onclick="saveName(this, event); return false;">Save name</h:outputLink>
<h:inputText id="my-name" value="#{javaScriptApiDemo.myName}" />
<h:panelGroup id="my-name-display">Your name is: #{javaScriptApiDemo.myName}</h:panelGroup>
</h:form>
<script type="text/javascript">
function saveName(source, event) {
jsf.ajax.request(source, event, {
execute: '@form',
render: 'my-name-form:my-name-display'
});
}
</script>
清单 11-7 中的例子显示了一个输入框,用户可以在其中输入自己的名字。该名称被映射到 javaScriptApiDemo 受管 bean 上一个名为 myName 的属性。当前存储在 myName 属性中的名称显示在面板组中输入字段的下方。当点击“保存名称”链接时,将调用保存名称功能。saveName 函数中的 Ajax 请求将选项 execute 设置为@form,表示发出请求的表单应该执行 JSF 请求生命周期。从请求返回后,my-name-form 表单中的 my-name-display 面板被更新。值得注意的是,清单 11-7 完全等同于清单 11-8 。使用< f:ajax >标签和 JavaScript API 之间的选择取决于您试图解决的特定任务。如果同时控制页面的其他方面,将所有功能收集到逻辑分组的 JavaScript 函数中,那么使用 JavaScript API 可能更有意义。如果页面没有任何其他正在执行的 JavaScript,那么使用 JavaScript API 而不是坚持使用< f:ajax >标签可能是多余的。
清单 11-8。 与清单 11-x 相同的例子,但是使用了< f:ajax >标签来代替 JavaScript API
<h:form id="my-name-form-pure-jsf">
<h:outputLink>
<f:ajax render="my-name-display-pure-jsf" execute="my-name-pure-jsf" />
Save name
</h:outputLink>
<h:inputText id="my-name-pure-jsf" value="#{javaScriptApiDemo.myName}" />
<h:panelGroup id="my-name-display-pure-jsf">
Your name is: #{javaScriptApiDemo.myName}
</h:panelGroup>
</h:form>
监控 Ajax 事件
使用
表 11-4 。从 Ajax 请求事件侦听器发出的事件
|
事件
|
描述
|
| --- | --- |
| begin
| 每当 Ajax 请求开始时发出 |
| complete
| 每当 Ajax 请求完成时发出 |
| success
| 每当 Ajax 请求成功完成时发出 |
表 11-5 。从服务器错误事件侦听器发出的事件
|
事件
|
描述
|
| --- | --- |
| httpError
| 如果 HTTP 状态不在 2xx 成功范围内,则发出此事件 |
| serverError
| 当服务器端发生错误或异常时发出 |
| malformedXML
| 当服务器返回不正确的 XML 响应时发出 |
| emptyResponse
| 当服务器没有返回响应时发出 |
清单 11-9 是一个挂钩到两个事件处理程序的 JavaScript 的例子。通过使用< h:outputScript/ >标签,JavaScript 可以被任何 Facelets 页面使用,如清单 11-10 所示。
清单 11-9。 JavaScript 挂钩到 JSF JavaScript API 公开的事件监听器
function outputAjaxEvent(data) {
console.log(data);
}
function outputError(errorData) {
console.log(errorData.type + " (" + errorData.status + "): " + errorName + ". " + errorDescription);
// Register error on a remote error logging server
}
function showProgress(data) {
if (data.status === 'begin') {
toggle_visibility('in-progress');
} else if (data.status === 'success') {
toggle_visibility('in-progress');
}
}
// Utility function for toggling the visibility of an element
function toggle_visibility(id) {
var e = document.getElementById(id);
if (e.style.display == 'block')
e.style.display = 'none';
else
e.style.display = 'block';
}
jsf.ajax.addOnEvent(outputAjaxEvent);
jsf.ajax.addOnEvent(showProgress);
jsf.ajax.addOnError(outputError);
清单 11-10。 使用 JavaScript 文件的 Facelets 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<ui:composition template="/base.xhtml">
<ui:define name="title">
Chapter 11 - JavaScript API Demo
</ui:define>
<ui:define name="top">
Chapter 11 - JavaScript API Demo
</ui:define>
<ui:define name="content">
<h:outputScript name="events.js" library="js" />
<h:form id="my-name-form-pure-jsf">
<h:outputLink>
<f:ajax render="my-name-display-pure-jsf" execute="my-name-pure-jsf" />
Save name
</h:outputLink>
<h:inputText id="my-name-pure-jsf" value="#{javaScriptApiDemo.myName}" />
<h:panelGroup id="my-name-display-pure-jsf">
Your name is: #{javaScriptApiDemo.myName}
</h:panelGroup>
</h:form>
<!—
Hidden by default. This is only shown when an Ajax
request begins and hidden when a request completes successfully
-->
<h:panelGroup id="in-progress" layout="block" style="display: none;">
<h:panelGroup style="font-weight: bold;">
PLEASE WAIT - THE PAGE IS LOADING
</h:panelGroup>
</h:panelGroup>
</ui:define>
</ui:composition>
</html>
测试 JSF 应用
任何真实世界的应用都必须经过一定程度的测试。开发 web 应用时,编写单元测试来验证功能背后的单个类通常是不够的。您可以通过使用 JUnit 测试组件和 beans 背后的逻辑,但是当您有一个 JSF 应用时,这通常是不够的。测试 JSF 应用有很多方面,不仅仅是确保后端逻辑正确。在 JSF 应用中,HTTP(客户端-服务器交互)、Ajax 请求和 web 浏览器差异增加了复杂性。因此,真正的测试需要一个框架,能够测试应用的部署版本,发起请求(Ajax 和完整的 HTTP)并在请求后测试应用的状态。有许多流行的功能测试框架可用,如 Selenium、FitNesse 和 Cucumber。这些测试框架可以通过验证应用在用户端的行为是否符合预期来帮助我们进行黑盒测试。理想情况下,我们希望有一个集成测试框架,允许我们验证应用及其组件的行为,就像它们在应用服务器上部署时的行为一样。这将允许更精确的测试。
什么是 Arquillian?
Arquillian 是一个完整的 Java EE 应用容器内测试平台。Arquillian 与测试框架(如 JUnit 和 TestNG)集成在一起,使得编写单元测试的任何人都可以轻松采用。Arquillian 使用您选择的嵌入式应用服务器来部署要测试的类和资源。一旦在嵌入式应用服务器中部署了 JSF 应用,就可以通过调用类和资源并检查它们的响应来测试系统的行为。现成的 Arquillian 能够测试 JEE 组件,如 EJB 和 CDI beans。已经为 Arquillian 构建了几个扩展来支持功能测试。表 11-6 概述了 Arquillian 的流行扩展。
表 11-6 。Arquillian 的流行扩展
|
延长
|
目的
|
成熟度
|
| --- | --- | --- |
| 雄蜂 | Selenium 也使用 WebDriver API 的包装器。这个扩展使得创建简单的功能测试成为可能。
链接: http://arquillian.org/modules/drone-extension/
| 稳定的 |
| 弯曲 | 模拟客户端的交互,同时在 JSF 请求生命周期的不同阶段检查服务器端的状态变化。
链接: http://arquillian.org/modules/warp-extension/
| 希腊字母的第一个字母 |
| 石墨烯 | 通过保护和拦截请求来优雅地支持 Ajax,从而增强了 Drone 扩展。
链接: http://arquillian.org/modules/graphene-extension/
| 稳定的 |
| 坚持 | 验证应用的持久层。允许使用 XML、XLS、YAML、JSON 和 SQL 等常见数据格式植入数据库。
链接: http://arquillian.org/modules/persistence-extension/
| 希腊字母的第一个字母 |
| 表演 | 验证测试在给定的时间范围内执行。将在回归测试中发现性能问题。
链接: http://arquillian.org/modules/performance-extension/
| 贝塔 |
| 蒸汽 2 | 允许测试 Seam 库和注入点。
链接: http://arquillian.org/modules/seam2-extension/
| 贝塔 |
在这一节中,我们将探索如何使用 Arquillian 和 Drone 扩展对 JSF 应用进行黑盒测试。首先,我们将看看如何为 Maven 项目设置 Arquillian 和 Drone ,然后看看如何使用 Drone 扩展编写合理的 JUnit 测试。
注意 Arquillian Warp Extension 是 JSFUnit 项目的官方替代品,不再被维护。在撰写本文时,Warp 扩展仍处于 Alpha 状态,没有可用的产品示例。
设置阿奎利亚人和无人机
在这一节中,我们将看看如何在 Maven 项目中设置 Arquillian 和 Drone 。如果你不使用 Maven,你可以在 Arquillian 网站(【http://www.arquillian.org】??)上找到在你的项目中包含必要依赖项的指南。
清单 11-11 显示了 Maven 项目对象模型(POM) ,用于在您的项目中包含 Arquillian 和 Drone。
清单 11-11。 pom.xml 包含使用 Arquillian 和 Drone 所需的依赖项
<?xml version="1.0" encoding="UTF-8"?>
<project FontName">http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.apress.projsf2html5</groupId>
<artifactId>chapter11</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>chapter11</name>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.arquillian</groupId>
<artifactId>arquillian-bom</artifactId>
<version>1.1.1.Final</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-bom</artifactId>
<version>1.2.0.CR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.main.extras</groupId>
<artifactId>glassfish-embedded-all</artifactId>
<version>4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-container</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.container</groupId>
<artifactId>arquillian-glassfish-embedded-3.1</artifactId>
<version>1.0.0.CR4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.descriptors</groupId>
<artifactId>shrinkwrap-descriptors-api-javaee</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.descriptors</groupId>
<artifactId>shrinkwrap-descriptors-impl-javaee</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap</groupId>
<artifactId>shrinkwrap-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-webdriver-depchain</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-selenium</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-selenium-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-server</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.mortbay.jetty</groupId>
<artifactId>servlet-api-2.5</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.6.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
...
</project>
使用 Arquillian 和 Drone 编写测试
我们将使用 Arquillian 来模拟 web 应用到 web 容器的部署。一旦 web 应用被部署在嵌入式容器中,我们将开始使用无人机扩展运行功能测试,方法是对部署的应用执行请求,并验证它是否以预期的输出进行响应。作为一个例子,我们将测试一个简单的应用,它要求用户输入他的名字,然后问候用户。用户界面的实体模型如图图 11-3 所示。基于实体模型,我们需要一个 Facelets 文件向用户显示 UI 和一个 CDI bean 来存储名称,如图图 11-4 所示。
图 11-3 。测试应用的 UI 模型
图 11-4 。包含用于测试应用的 CDI Bean 和 Facelets 页面的类图
基于实体模型和类图,我们可以使用 Gherkin 格式编写几个伪测试,稍后我们将使用 Drone 实现这些测试。
| 场景:输入我的名字 |
| 假设我在页面上输入我的名字“离合器电源” |
| 当我按下提交按钮时 |
| 然后应用会向我问候“你好,离合器电源” |
| 场景:我进入页面 |
| 假定我进入页面 |
| 当我无所事事时 |
| 然后将不会显示任何问候语 |
CDI bean 的实际实现可以在清单 11-12 中看到。它是一个简单的请求范围 bean,只有一个名为 name 的属性。清单 11-13 显示了 Facelets 文件,它向用户显示输入字段,包括一个提交按钮。注意,我们有一个面板组,仅当 CDI bean 的 name 属性不为空时才显示。
清单 11-12。 简单请求作用域 CDI Bean 公开一个名称属性
package com.apress.projsf2html5.chapter11.jsf;
import java.io.Serializable;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named(value = "helloYou")
@RequestScoped
public class HelloYou implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
清单 11-13。 Facelets 文件向用户显示输入框和提交按钮
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<ui:composition template="/base.xhtml">
<ui:define name="title">
Chapter 11 - Testing - Hello You
</ui:define>
<ui:define name="top">
Chapter 11 - Testing - Hello You
</ui:define>
<ui:define name="content">
<h:form id="hello-form">
<h:outputLabel value="What's your name?" for="input-name" />
<h:inputText id="input-name" value="#{helloYou.name}" />
<h:commandButton id="submit" value="Submit" />
<h:panelGroup id="output-message" rendered="#{not empty helloYou.name}">
Hello #{helloYou.name}
</h:panelGroup>
</h:form>
</ui:define>
</ui:composition>
</html>
CDI bean 和 Facelet 页面的结果可以在图 11-5 中看到。
图 11-5 。你好应用
我们现在准备测试应用。我们的测试将用@RunWith 注释,告诉 JUnit 我们将使用 Arquillian 作为我们的测试运行程序。我们还使用@RunAsClient 注释告诉 Arquillian 我们不会在服务器端进行测试,而是作为 web 客户端发送请求。这是使用无人机扩展进行功能测试所必需的。接下来,我们将指定我们想要测试的资源和类。这是通过使用包膜 API 创建 WebArchive 对象来完成的。WebArchive 应该包含所有的资源和类,并且只包含那些。目的是隔离被测试的文件,避免增加不必要的复杂性。创建 WebArchive 的方法必须被注释为@Deployment,以便 Arquillian 在执行测试之前检测必须部署的内容。最后,我们将编写实际的测试。完整的测试可以在 清单 11-14 中看到。
清单 11-14。 测试 Hello You 应用的 Arquillian 测试用例
package com.apress.projsf2html5.chapter11;
import com.apress.projsf2html5.chapter11.jsf.HelloYou;
import com.thoughtworks.selenium.DefaultSelenium;
import java.io.File;
import java.net.URL;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(Arquillian.class)
@RunAsClient
public class HelloYouTest {
/** This will give us the contextPath where the web application was installed. */
@ArquillianResource
URL contextPath;
/** This will give us access to a Drone that simulates a browser. */
@Drone
private DefaultSelenium browser;
/**
* The method annotated with Deployment outputs the web archive representing
* the application.The archive must contain all the resource and classes
* being tested.
*
* @return {@link WebArchive} containing the resources and classes
* representing the Hello You application
*/
@Deployment
public static WebArchive createDeployment() {
return ShrinkWrap.create(WebArchive.class, "hello-you.war")
.addClasses(HelloYou.class)
.addAsWebResource(new File("src/main/webapp/hello-you.xhtml"))
.addAsWebResource(new File("src/main/webapp/contracts/basic/base.xhtml"), "contracts/basic/base.xhtml")
.addAsWebResource(new File("src/main/webapp/contracts/basic/cssLayout.css"), "contracts/basic/cssLayout.css")
.addAsWebResource(new File("src/main/webapp/contracts/basic/default.css"), "contracts/basic/default.css")
.addAsWebInfResource(new File("src/main/webapp/WEB-INF/web.xml"))
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
}
/**
* Scenario: Entering my name.
* Given that I enter my name 'Clutch Powers' on the page
* When I press the Submit Button
* Then I will be greeted 'Hello Clutch Powers' by the application
*/
@Test
public void helloyou_EnterName_GreetingFound() {
String startUrl = contextPath.toString() + "faces/hello-you.xhtml";
// Open the hello-you page
browser.open(startUrl);
// Type name in the input field
browser.type("id=hello-form:input-name", "Clutch Powers");
// Click the submit button
browser.click("id=hello-form:submit");
// Wait for the page to load (max 5 seconds)
browser.waitForPageToLoad("5000");
// Check that the "Hello <name>" element is displayed on screen
assertTrue(browser.isVisible("id=hello-form:output-message"));
// Check that the name entered is the one expected
assertEquals("Welcome message missing",
browser.getText("id=hello-form:output-message"),
"Hello Clutch Powers");
}
/**
* Scenario: I enter the page.
* Given that I enter page
* When I do nothing
* Then I there will be no greeting displayed
*/
@Test
public void helloyou_OpenPage_GreetingHidden() {
String startUrl = contextPath.toString() + "faces/hello-you.xhtml";
// Open the hello-you page
browser.open(startUrl);
// Check that the "Hello <name>" element is NOT displayed on the screen
assertFalse(browser.isVisible("id=hello-form:output-message"));
}
}
DefaultSelenium 无人机能够模拟多种浏览器交互。有趣的方法示例在 表 11-7 中突出显示。
表 11-7 。用于模拟用户交互的 DefaultSelenium 方法示例
|
方法
|
描述
|
| --- | --- |
| attachFile(fieldLocator, fileLocation)
| 用于在文件输入表单字段中附加文件 |
| Click(locator)
| 单击具有指定定位器的给定元素 |
| doubleClick(locator)
| 双击具有指定定位器的给定元素 |
| dragAndDrop(locator, movements)
| 模拟从源到位置的拖放 |
| getText(locator)
| 获取给定元素中的文本 |
| isVisible(locator)
| 已确定给定元素在屏幕上是否可见 |
| Open(url)
| 打开给定的页面 |
| typeKeys(location, value)
| 在给定的输入字段中键入值 |
| waitForPageToLoad(timeout)
| 等待页面加载给定的毫秒数 |
摘要
在本章中,我们探讨了开发 JSF 应用的设计考虑因素,如安全性、性能和内存消耗。我们还看了如何使用标签来 Ajax 化 JSF 应用。使用标签可以在单个以及一组 JSF 组件上设置 Ajax 请求。为了补充标签,我们来到幕后探索 JSF JavaScript API。最后,我们看了使用 Aqullian 测试框架结合无人机扩展来测试 JSF 应用,无人机扩展支持 JSF 应用的功能测试。
十二、JSF2 安全性和性能
在本章中,您将学习如何使用 Java EE 容器提供的安全特性来保护您的 JSF 应用。您知道如何在第十章中介绍的天气应用中应用容器管理的认证、授权和数据保护。在这一章中,你还将学习如何调整你的 JSF 应用的性能,以使你的 JSF 页面响应更快。
JSF 应用安全
Web 应用安全 可以分为三个主要方面,我们将在本节详细阐述:
- 认证 是向系统确认用户身份真实的行为。
- 授权 定义了用户在执行身份验证后被允许访问系统的哪些部分。
- 数据保护 是关于确保用户和系统之间的数据不能被未授权方修改或伪造。
在 Java EE 中,您可以依赖 Java EE 容器提供的安全特性,以便在您的 Java EE 应用中实现安全需求(如果您的 Java EE 应用依赖于 Java EE 容器提供的安全特性;这意味着您的 Java EE 应用正在使用“容器管理的安全性”)。除了在容器级别管理安全性,您还可以在应用级别管理安全性(这种方法称为应用管理的安全性)。应用管理的安全性并不意味着从头开始实现所有的应用安全特性;应用管理的安全性通常利用 Java EE 容器提供的安全特性,以便在应用中实现客户需要的自定义安全特性。
注意如果从客户需求来看,没有理由实现定制的安全解决方案,那么强烈建议 Java EE 应用使用容器管理的安全性。
Java EE 容器管理的安全性提供了容器管理的身份验证、授权和数据保护。在接下来的小节中,我们将详细解释这些术语。
证明
Java EE 容器提供了不同类型的认证机制:
- HTTP 基本
- 基于表单
- 摘要
HTTP 基本身份验证
在 HTTP 基本身份验证中,服务器从 web 客户端请求用户名和密码,并通过与指定或默认领域中授权用户的数据库进行比较来验证用户名和密码是否有效。当您没有在 web 配置文件中指定身份验证机制时,基本身份验证是默认的。
使用基本身份验证时,会发生以下步骤:
- 客户端请求访问受保护的资源。
- web 服务器返回一个对话框,要求输入用户名和密码。
- 客户端向服务器提交用户名和密码。
- 服务器对指定领域中的用户进行身份验证,如果成功,则返回请求的资源。
注意领域是系统的用户和组的存储。
基于表单的身份验证
在基于表单的身份验证中,您可以在应用中开发和自定义登录和错误页面。当在 web 配置文件中声明基于表单的身份验证时,会发生以下步骤:
- 客户端请求访问受保护的资源。
- 如果客户端未经身份验证,服务器会将客户端重定向到登录页面。
- 客户端将登录表单提交给服务器。
- 服务器尝试对用户进行身份验证。
- 如果身份验证成功,将检查通过身份验证的用户的主体,以确保其角色有权访问资源(授权)。如果用户得到授权,服务器将使用存储的 URL 路径将客户端重定向到资源。
- 如果身份验证失败,客户端将被转发或重定向到错误页面。
有关基于表单的身份验证的完整示例,请查看“在天气应用中应用托管安全性”一节
注意需要注意的是,HTTP 基本认证以 Base64 编码的文本形式发送用户名和密码;而基于表单的身份验证是以纯文本的形式发送它们,这意味着它们是不安全的,所以建议使用安全传输机制(如 SSL)。为了配置 SSL,您需要查看应用服务器的文档,因为它是针对每个应用服务器的;例如,为了在 Tomcat 7 中配置 SSL,请检查以下链接:
tomcat.apache.org/tomcat-7.0-doc/ssl-howto.html
。
摘要认证
摘要认证基于用户名和密码认证用户(类似于基本的 HTTP 认证)。但是,与基本身份验证不同,摘要式身份验证不通过网络发送用户密码。相反,客户端发送密码的单向加密哈希。
配置身份验证方法
为了在您的 Java EE web 应用中配置认证方法,您可以使用< login-config >元素,如 web 配置文件(web.xml)中所示:
<login-config>
<auth-method>FORM</auth-method>
<realm-name>jdbcRealm</realm-name>
<form-login-config>
<form-login-page>/home.xhtml</form-login-page>
<form-error-page>/error.xhtml</form-error-page>
</form-login-config>
</login-config>
您可能已经注意到,
- 元素指定了 web 应用的认证机制。它可以是消化的,基本的或形式的或没有。
元素指定领域名。 元素指定了登录和错误页面。当使用基于表单的登录时,应该使用它。
批准
授权定义了基于角色的访问控制,它决定了用户可以访问系统的哪些部分。在 Java EE 中,为了实现这一点,可以在 web.xml 中使用<安全约束>元素,如清单 12-1 所示。
清单 12-1。 范例<范例>范例
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" FontName">http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
...
<security-constraint>
<display-name>securityConstraint</display-name>
<web-resource-collection>
<web-resource-name>resources</web-resource-name>
<url-pattern>/protected/*</url-pattern>
<http-method>PUT</http-method>
<http-method>DELETE</http-method>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>weatherUserRole</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>WeatherRealm</realm-name>
<form-login-config>
<form-login-page>/home.xhtml</form-login-page>
<form-error-page>/error.xhtml</form-error-page>
</form-login-config>
</login-config>
...
</web-app>
- Web 资源集合(
):描述一组要保护的资源的 URL 模式和 HTTP 操作的列表。 - 授权约束(
):指定是否使用身份验证,并指定被授权执行受约束请求的角色。 - 用户数据约束(
):指定在客户端和服务器之间传输数据时如何保护数据(将在“数据保护”一节中说明)。
一个 web 资源集合(
(可选)是您用于 web 资源的名称。 是要保护的 URL。 用于指定哪些方法应该被保护。
授权约束 ( <授权约束>)包含<角色名称>元素。您可以根据需要在< auth-constraint >元素中使用任意数量的< role-name >元素。为应用定义的角色必须映射到应用服务器上定义的用户和组(每个应用服务器都有自己的方式向用户和组映射声明此角色;查看“在天气应用中应用托管安全性”一节,了解如何在 GlassFish application server 版上实现这一点。
数据保护
数据保护是指保护在客户端和服务器之间传输的数据。在 Java EE 中,为了进行数据保护,可以使用 web.xml 中
清单 12-2。web.xml 中<安全约束>元素的 <用户数据约束>
<security-constraint>
<display-name>securityConstraint</display-name>
<web-resource-collection>
<web-resource-name>resources</web-resource-name>
<url-pattern>/protected/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>weatherUserRole</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
如粗体行所示,
在天气应用中应用托管安全性
在第十章中,我们介绍了天气应用,作为基本 JSF 2.2 应用的一个例子。在天气应用中,我们从应用代码中处理认证(应用登录)和授权(访问天气页面)。不建议从应用代码中处理安全性,尤其是当我们讨论没有自定义安全性需求的典型身份验证和授权场景时;因此,让我们将容器管理的安全性(基于表单的身份验证和授权)应用于天气应用。
首先,让我们修改 home.xhtml 以包含基于表单的身份验证的 html 形式,而不是从应用代码中处理登录要求。清单 12-3 显示了 home.xhtml 的更新。
清单 12-3。更新至 home.xhtml 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<ui:composition template="/WEB-INF/templates/main.xhtml">
<ui:define name="title">
#{bundle['application.loginpage.title']}
</ui:define>
<ui:define name="content">
<!-- Form authentication -->
<form action="j_security_check" method="POST">
Username:<input type="text" name="j_username"></input><br/>
Password:<input type="password" name="j_password"></input><br/>
<input type="submit" value="#{bundle['application.login']}"></input>
</form>
<h:link value="#{bundle['application.loginpage.register']}" outcome="registration"/>
</ui:define>
</ui:composition>
</html>
如粗体行所示,为了使用基于表单的身份验证,按照 servlet 规范,我们必须使用 HTML
清单 12-4。 天气应用的基于表单的认证配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" ...>
...
<security-constraint>
<display-name>securityConstraint</display-name>
<web-resource-collection>
<web-resource-name>resources</web-resource-name>
<url-pattern>/protected/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>weatherUser</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>WeatherRealm</realm-name>
<form-login-config>
<form-login-page>/home.xhtml</form-login-page>
<form-error-page>/error.xhtml</form-error-page>
</form-login-config>
</login-config>
<welcome-file-list>
<welcome-file>protected/weather.xhtml</welcome-file>
</welcome-file-list>
...
</web-app>
如粗体行所示,在安全约束部分,只有 weatherUser 角色能够访问受保护文件夹(/protected/*)下的资源。在登录配置部分,认证方式设置为 FORM(即基于表单的认证),领域名称设置为 WeatherRealm,最后在表单登录配置中,登录页面设置为 home.xhtml(如清单 12-3 所示),错误页面(用户登录失败时会显示)设置为 error.xhtml,当用户登录成功时,用户会被转发到受保护文件夹下的 weather.xhtml 页面。
为应用定义的 weatherUser 角色必须映射到应用服务器上定义的组。对于 GlassFish,您可以在配置文件(glassfish-web.xml)中定义角色和组之间的映射,如清单 12-5 所示。
清单 12-5。 glassfish-web.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC ...>
<glassfish-web-app error-url="">
<context-root>/weather</context-root>
<security-role-mapping>
<role-name>weatherUser</role-name>
<group-name>weather_user</group-name>
</security-role-mapping>
...
</glassfish-web-app>
如配置文件所示,角色名(weatherUser)被映射到领域存储库(WeatherRealm)中的实际组名(weather_user)。
WeatherRealm 是天气应用的用户和组的存储;您可能还记得,我们有一个 APP_USER 表,用来存储应用用户。由于 JDBC realm(GlassFish 和其他一些 Java EE 应用服务器支持它),您可以将现有的用户/组数据库变成一个领域;然而,我们需要添加另一个数据库表(APP_GROUP)来定义用户组,如图图 12-1 所示。
图 12-1 。天气应用数据模型的修改
清单 12-6 显示了包含 APP_USER 和 APP_GROUP 属性以及它们之间关系的 SQL 语句。
清单 12-6。 天气应用数据模型的 SQL 语句
CREATE TABLE APP_USER (
ID VARCHAR(64) PRIMARY KEY,
FIRST_NAME VARCHAR(32),
LAST_NAME VARCHAR(32),
PASSWORD VARCHAR(32),
PROFESSION VARCHAR(32),
EMAIL VARCHAR(64),
ZIP_CODE VARCHAR(32)
);
CREATE TABLE APP_GROUP(userid varchar(64) not null, groupid varchar(64) not null, primary key(userid, groupid));
ALTER TABLE APP_GROUP add constraint FK_USERID foreign key(userid) references APP_USER(id);
最后,为了在 GlassFish 4.0 中创建我们的自定义领域,单击“配置->服务器配置->安全性->领域”,输入合适的领域信息,最后保存领域,如图 12-2 所示。
图 12-2 。在 GlassFish 版中定义新领域
表 12-1 显示了 WeatherRealm 的配置属性。
表 12-1 。自定义领域配置属性
|
财产
|
价值
|
| --- | --- |
| JAAS 背景 | jdbcrealm-JDBC 范围 |
| 命名服务 | jdbc/weatherDB |
| 用户表 | 天气。应用 _ 用户 |
| 用户名列 | 身份 |
| 密码栏 | 密码 |
| 组表 | 天气。APP_GROUP |
| 组表用户名列 | 使用者辩证码 |
| 组名列 | 组名 |
| 密码加密算法 | 没有人 |
| 分配组 | [将此字段留空] |
| 数据库用户 | 天气 |
| 数据库密码 | 密码 |
| 摘要算法 | 没有人 |
| 编码 | [将此字段留空] |
| 字符集 | [将此字段留空] |
现在,我们为天气应用配置了容器管理的安全性,因此我们应该从 faces-config.xml 中删除 AuthorizationListener 类及其引用。清单 12-7 显示了更新后的受天气控制的 bean。
清单 12-7。 更新了防风类
@Named
@RequestScoped
public class WeatherBacking extends BaseBacking {
@EJB
private UserManagerLocal userManager;
@PostConstruct
public void loadUser(ComponentSystemEvent event) {
try {
String userID = getRequest().getUserPrincipal().getName();
AppUser sourceAppUser = userManager.getUser(userID);
AppUser targetAppUser = (AppUser) evaluateEL("#{appUser}", AppUser.class);
targetAppUser.setFirstName(sourceAppUser.getFirstName());
targetAppUser.setLastName(sourceAppUser.getLastName());
targetAppUser.setZipCode(sourceAppUser.getZipCode());
} catch (Exception ex) {
Logger.getLogger(WeatherBacking.class.getName()).log(Level.SEVERE, null, ex);
getContext().addMessage(null, new FacesMessage(SYSTEM_ERROR));
}
}
public String logout() {
try {
getRequest().logout();
return "/home.xhtml?faces-redirect=true";
} catch (ServletException ex) {
Logger.getLogger(WeatherBacking.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
}
如代码所示,loadUser()方法使用其 ID 检索当前用户信息(用户 ID 可以从 java.security.Principal 检索,可以使用 HTTPServletRequest 的 getUserPrincipal() API 获取)。logout()调用 HTTPServletRequest 的 logout()方法,以便将用户从当前已验证的会话中注销。清单 12-8 显示了更新后的 weather.xhtml 页面。
清单 12-8。 更新 weather.xhtml 页面
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html FontName">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:mashup="http://code.google.com/p/mashups4jsf/">
<ui:composition template="/WEB-INF/templates/main.xhtml">
<ui:define name="title">
#{bundle['application.weatherpage.title']}
</ui:define>
<ui:define name="content">
<f:event listener="#{weatherBacking.loadUser}" type="preRenderView" />
<h:form>
#{bundle['application.welcome']}, #{appUser.firstName} #{appUser.lastName}! <br/><br/>
#{bundle['application.weatherpage.currentInfo']} for #{appUser.zipCode}:
<mashup:yahooWeather temperatureType="c" locationCode="#{appUser.zipCode}"/> <br/><br/>
<h:commandLink value="#{bundle['application.weatherpage.logout']}"
action="#{weatherBacking.logout}"></h:commandLink> <br/><br/>
</h:form>
</ui:define>
</ui:composition>
</html>
如粗体行所示,为了检索当前用户信息,在 preRenderView 事件中调用 weatherBacking bean 的 loadUser()方法(每次在视图呈现之前调用)。RegistrationBacking 类也必须更新。清单 12-9 显示了更新后的注册支持 bean。
清单 12-9。 更新注册支持 Bean
@Named
@RequestScoped
public class RegistrationBacking extends BaseBacking {
@EJB
private UserManagerLocal userManager;
public String register() {
FacesContext context = FacesContext.getCurrentInstance();
Map<Object, Object> flowScope = context.getApplication().getFlowHandler().getCurrentFlowScope();
AppUser appUser = new AppUser();
appUser.setId((String) flowScope.get("id"));
appUser.setPassword((String) flowScope.get("password"));
appUser.setEmail((String) flowScope.get("email"));
appUser.setFirstName((String) flowScope.get("fname"));
appUser.setLastName((String) flowScope.get("lname"));
appUser.setProfession((String) flowScope.get("profession"));
appUser.setZipCode((String) flowScope.get("zipCode"));
//Assign a group to the user ...
AppGroup appGroup = new AppGroup(appUser.getId(), "weather_user");
List<AppGroup> appGroups = new ArrayList<>();
appGroups.add(appGroup);
appUser.setAppGroupList(appGroups);
try {
userManager.registerUser(appUser);
} catch (UserExistsException ex) {
Logger.getLogger(RegistrationBacking.class.getName()).log(Level.SEVERE, null, ex);
context.addMessage(null, new FacesMessage(USERNAME_ALREADY_EXISTS));
return null;
} catch (Exception ex) {
Logger.getLogger(RegistrationBacking.class.getName()).log(Level.SEVERE, null, ex);
context.addMessage(null, new FacesMessage(SYSTEM_ERROR));
return null;
}
return "flowReturn";
}
//...
}
如粗体行所示,在创建 appUser 对象并用用户信息填充后,它被分配给“weather_user”组,该组在清单 12-5 的映射文件中提到。
为了在天气应用中应用容器管理的安全性,我们需要做的就是这些。为了获得天气应用的完整源代码,请从 www.apress.com/9781430250104的图书网站第十二章源代码下载。
JSF 应用性能
调优 JSF 应用的性能是每个 JSF 开发者需要了解的最重要的方面之一。在这一部分中,我们将讨论为了增强 JSF 2.x 应用的性能而可以调整的最重要的方面。
刷新周期
这个时间间隔指定了 Facelets 编译器在检查页面中的更改之前必须等待的时间。在开发过程中,建议将(javax . faces . facelets _ REFRESH _ PERIOD)参数的值设置为较低的值,以便在开发过程中帮助 JSF 开发人员能够在应用运行时编辑页面。在生产中,为了获得更好的性能,建议将(javax . faces . facelets _ REFRESH _ PERIOD)参数的值设置为-1(这意味着您不希望编译器在编译页面后检查更改),如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>javax.faces.FACELETS_REFRESH_PERIOD</param-name>
<param-value>-1</param-value>
</context-param>
...
</web-app>
跳过评论
将(javax.faces.FACELETS _ SKIP _ COMMENTS)参数设置为 true 有助于通过从 FACELETS 页面中删除注释来减少通过网络发送的数据量。这些注释有助于在开发过程中理解代码,但在部署过程中是不必要的,同时,由于允许系统用户查看源代码注释,它们会带来安全风险。由于对安全性和性能都有影响,因此将该参数设置为 true 很重要,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
...
</web-app>
设计阶段
将 javax.faces.PROJECT_STAGE 参数设置为“Development”允许 JSF 环境在页面中打印出调试信息。这在开发过程中很有帮助,但是在部署之后没有任何用处,除非您在测试环境中对错误或问题进行故障排除。在生产中,始终将该参数值设置为“生产”,以提高生产过程中的性能,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
...
</web-app>
状态保存方法
将 javax . faces . state _ SAVING _ METHOD 参数设置为“server”(这是默认值)比将该参数设置为“client”提供了更好的性能。这是因为服务器状态保存不需要状态的序列化。以下是将状态保存方法设置为“服务器”的示例。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>
...
</web-app>
但是,重要的是要知道,如果服务器中没有足够的内存,可以将状态保存方法设置为“client”。
响应缓冲器
建议增加响应缓冲区大小,以减少渲染时的内存重新分配,这可以通过将 javax . faces . facelets _ BUFFER _ SIZE 参数(如果使用 Mojarra,则为 com . sun . faces . responsebuffersize 参数)设置为适合应用服务器内存容量的适当值来实现,如下例所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>javax.faces.FACELETS_BUFFER_SIZE</param-name>
<param-value>500000</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.responseBufferSize</param-name>
<param-value>500000</param-value>
</context-param>
...
</web-app>
如示例所示,javax.faces.FACELETS _ BUFFER _ SIZE 和 com . sun . faces . responsebuffersize 参数都设置为 500000 字节。
会话中的视图数量
会话中的视图数量由 Apache MyFaces 和 Oracle Mojarra 中的两个不同的上下文参数表示:
- org . Apache . MyFaces . number _ OF _ VIEWS _ IN _ SESSION(在 Apache MyFaces 中)
- com . sun . faces . numberofviewsinsession(Oracle Mojarra 中)
仅当状态保存方法设置为“服务器”时,这些参数才起作用。它定义了存储在会话中的序列化视图的最大数量。默认情况下,它被设置为 20(在 Apache MyFaces 中)或 15(在 Oracle Mojarra 中)。对于许多应用来说,将此参数设置为 15 或 20 可能不合适,因此,如果您的 JSF 应用不要求会话中具有如此数量的序列化视图,则建议减少此参数以节省服务器内存,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION</param-name>
<param-value>3</param-value>
</context-param>
...
</web-app>
在 Mojarra 中,还有另一个相关的上下文参数(也适用于服务器端状态保存)可以优化,这就是(com . sun . faces . numberoflogicalviews)参数。此参数表示存储在会话中的应用逻辑视图的数量。默认情况下,它设置为 15。为了节省服务器内存,您可以尽可能减少这个数字,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>com.sun.faces.numberOfLogicalViews</param-name>
<param-value>3</param-value>
</context-param>
...
</web-app>
理解这两个参数的语义很重要:
- numberOfLogicalViews 参数是指会话中逻辑视图的数量,您可以通过在不同的浏览器标签中打开您的 JSF 应用来进行试验;每个浏览器标签代表一个逻辑视图(获取视图)。例如,如果 numberOfLogicalViews 参数设置为三,并且您依次打开四个不同的浏览器选项卡,转到第一个选项卡,并提交表单(假设页面包含表单),那么您将得到 ViewExpiredException,因为代表第一个逻辑视图的第一个选项卡已从逻辑视图的 LRU(最近最少使用)映射中删除。这也意味着,如果您依次打开三个不同的浏览器选项卡,并转到其中任何一个选项卡来提交表单,您将不会遇到此异常,因为您没有超过逻辑视图的最大数量,即三个。
- numberOfViewsInSession 参数指的是会话中帖子的浏览量,您可以通过在页面中多次提交表单来进行试验。例如,如果 numberOfViewsInSession 参数设置为 3,并且您提交了一个页面表单四次,按下浏览器的后退按钮四次,然后重新提交第一个页面表单,您将得到 ViewExpiredException,因为代表第一个视图的第一个页面表单已从帖子视图的 LRU 地图中删除。这也意味着,如果您提交表单三次,然后返回重新提交第一页表单,您将不会遇到此异常,因为您没有超过帖子查看次数的最大值,即三次。
Apache MyFaces 特定调优
如果应用服务器中有足够的内存,并且因为压缩会消耗 CPU 时间,那么可以禁用服务器状态压缩,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>
org.apache.myfaces.COMPRESS_STATE_IN_SESSION
</param-name>
<param-value>false</param-value>
</context-param>
...
</web-app>
注意,当状态保存方法设置为“server”时,( org . Apache . myfaces . compress _ STATE _ IN _ SESSION)参数起作用。当状态保存方法设置为“server”时,另一个需要注意的重要参数是(org . Apache . myfaces . serialize _ STATE _ IN _ SESSION)。将 org . Apache . myfaces . serialize _ STATE _ IN _ SESSION 参数设置为 false,可以禁止序列化会话中的状态,这也将提供更好的性能,如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<context-param>
<param-name>
org.apache.myfaces.SERIALIZE_STATE_IN_SESSION
</param-name>
<param-value>false</param-value>
</context-param>
...
</web-app>
无国籍的 JSF
JSF 2.2 的一个有用特性是创建无状态视图的能力。创建无状态视图有两个主要优点:
-
无状态视图比默认的有状态视图具有更好的性能,因为没有花费时间来保存和恢复
-
无状态视图比默认的有状态视图(使用
虽然对于中小型页面来说,性能和内存的提升相对较小,但是当您的应用中有很多组件的大型页面时,以及当您的 JSF 应用有很多硬件能力有限的并发用户时,这种提升会非常显著;这意味着无状态视图可以使 JSF 应用更具可伸缩性。
注意如果你想使用 JSF 开发公共网站,无状态视图是最强大的选择之一。
因为无状态视图没有状态,所以认识到它们不能同时用于视图和会话范围的 beans 是很重要的;这意味着您应该知道您的托管 beans 是在请求范围内设置的。
注意无状态视图可能与 JSF 组件库不兼容,如 PrimeFaces 或 RichFaces。因此,通常,您必须验证您正在使用的 JSF 组件在无状态模式下是否工作良好。
为了将无状态行为应用到您的 JSF 视图,您需要将
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<f:view FontName">http://www.w3.org/1999/xhtml"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
transient="true">
<html>
<h:head>
<title>Stateless Page</title>
</h:head>
<h:body>
<!-- JSF HTML components -->
</h:body>
</html>
</f:view>
为了让你的 JSF 视图无状态,这就是你需要做的一切。
最佳实践
除了上述所有建议之外,您还需要考虑以下几点:
- 不要在托管 beans 的 getters 中执行 I/O 操作的业务逻辑,因为在请求处理生命周期中可能会多次调用它们,这会降低应用的整体性能。业务逻辑必须转移到 JSF 动作方法或事件侦听器中。
- 避免复杂的 EL 表达式。如果您有一个复杂的表达式,将其逻辑移到 Java 托管 beans 中。
- 如果必须显示包含大量记录的数据表,请始终使用分页。
- 如果可能的话,使用 Ajax (
)只发送您希望服务器处理的页面部分,并且只呈现应该重新呈现的页面部分(而不是整个页面)。 - 最小化使用会话范围的受管 beans,以便最小化服务器内存的使用,并提高应用的可伸缩性。
摘要
在本章中,您了解了身份验证、授权和数据保护之间的区别。您了解了如何使用 Java EE 容器提供的安全特性来保护您的 JSF 应用。您知道如何在第十章中介绍的天气应用中应用容器管理的认证、授权和数据保护。您还了解了如何通过修改默认的 JSF 上下文参数和应用一组最佳实践来优化 JSF 应用的性能。
十三、应用全部:大型应用
在这一章中,我们将在一个我们称之为 Mega App 的示例应用中应用我们在前面章节中学到的大部分知识。我们将从应用规范和线框开始,在了解应用需求后,我们将开发应用架构,包括数据模型、后端服务、前端模板、应用安全性(包括身份验证、授权和错误处理)。我们将看到如何使用 Java EE 7 技术实现这个架构。Mega App 应用利用了许多 Java EE 7 技术:它使用 JSF 2.2 在 Twitter Bootstrap 的帮助下处理用户界面交互,使用 EJB 3.2 处理事务,使用 JPA 2.1 处理持久性,使用 CDI 1.1 处理 bean 管理,使用 Java Bean 验证处理模型验证。在本章中,您将学习如何在 Java EE 7 空间中为 JSF 2.2 应用创建合适的应用架构和实现。
大型应用规范
Mega App 是一个在线图书馆应用,允许其用户搜索书籍。从搜索中获得可用图书后,用户可以从搜索结果中请求一本或多本图书的副本。在后台,应用管理员可以批准或拒绝对图书的请求。如果一本书(或更多)的请求被批准,那么 Mega App 用户将能够从他/她的批准的书列表中下载批准的书。图 13-1 从应用用户的角度展示了大型应用预订请求流。
图 13-1 。大型应用预订请求流程
Mega App 应用有两个角色:
- 应用用户
- 应用管理员
图 13-2 显示了应用用户和应用管理员的用例图。
图 13-2 。大型应用用例图
如用例图所示,应用用户可以执行以下操作:
- 在应用中注册。
- 搜书。
- 发送订购书籍的请求。
- 下载批准的书籍。
应用管理员可以执行以下操作:
- 添加图书。
- 删除图书。
- 批准图书申请。
- 拒绝图书请求。
- 搜书。
- 下载书籍。
- 在应用中注册用户。
- 查看所有用户。
- 删除用户。
注意理解一个角色(或一个参与者)可以在系统中包括一个或多个用户是很重要的。这意味着在 Mega App 中,我们可以有许多应用用户和应用管理员。
大型应用线框
现在,让我们浏览一下应用线框,以便对应用规范有一个清晰的理解。我们将举例说明应用中的每个参与者(应用用户和应用管理员)可以与之交互的应用页面。首先,为了访问应用页面,用户需要使用注册页面注册自己。图 13-3 显示了注册页面。
图 13-3 。注册页面
为了进行注册,用户必须提供以下信息:
- 登录名,对于应用中的每个用户都必须是唯一的。
- 两个相同的密码。
- 用户的名字。
- 用户的姓氏。
如果用户输入正确的信息,应用将通知用户他/她的帐户已成功创建。注册成功后,用户应该能够登录到系统。图 13-4 显示 Mega App 登录页面。
图 13-4 。登录页面
登录应用后,应用用户被引入主页,如图图 13-5 所示。
图 13-5 。应用用户主页
如页面所示,用户可以使用侧菜单进行如下操作:
- 搜书。
- 查看待定请求。
- 查看批准的请求。
- 查看被拒绝的请求。
- 注销。
用户可以通过输入准确的书名或只输入书名的一部分来按书名搜索书籍。当用户输入书名并点击“搜索书籍”按钮时,搜索表将被结果填充。对于搜索结果中的每本书,应用用户都可以选择请求图书副本。一旦用户请求图书副本,管理员用户将能够在他/她的待定请求收件箱中看到用户请求,如图图 13-6 所示。
图 13-6 。应用管理员待定请求收件箱
如上图所示,管理员用户可以对应用用户的待处理请求执行以下操作:
- 批准用户请求。
- 拒绝用户请求。
- 下载用户希望拥有副本的图书。
除此之外,管理员用户在侧面菜单中还有以下操作:
- 搜书。
- 查看待定请求。
- 查看批准的请求。
- 查看被拒绝的请求。
- 加一本书。
- 查看用户。
- 注销。
管理员操作的细节将在接下来的几段中说明。
一旦应用管理员批准了图书请求,应用用户将能够在他/她的批准请求收件箱中看到他/她的请求,如图图 13-7 所示。
图 13-7 。应用用户的已批准图书请求
在批准的请求收件箱中,应用用户可以下载图书副本。如果应用管理员拒绝了用户的预订请求,那么用户将在其“查看拒绝的请求”收件箱中看到他/她的拒绝请求。应用用户的“查看待定请求”收件箱显示了他/她当前的待定图书请求,这些请求尚未被应用管理员批准或拒绝。
现在,让我们转到管理员用户的页面:管理员用户成功登录后,管理员用户可以按书名(或部分书名)搜索图书,如图图 13-8 所示。
图 13-8 。应用管理员主页
管理员用户可以搜索图书,以便下载或从应用中删除它们。管理员用户的待处理请求收件箱已在图 13-6 中说明。在这个收件箱中,管理员用户可以批准或拒绝图书请求,也可以下载请求的图书。
图 13-9 显示了管理员用户的批准请求收件箱。
图 13-9 。管理员用户的批准请求收件箱
管理员用户的“批准的请求收件箱”包括所有批准的图书请求列表。管理员用户也有一个被拒绝的请求收件箱,其中包含所有被拒绝的图书请求。批准或拒绝的图书申请信息如下:
- 书名。
- 响应时间。
- 请求时间。
- 请求所有者。
- 可以对请求执行的可用操作。
管理员用户可以添加图书,如图 13-10 所示。
图 13-10 。管理员添加图书页面
管理员可以通过输入以下图书信息来添加图书:
- 图书 ISBN。
- 书名。
- 图书作者。
- 发布者名称。
- 书籍语言。
- 书籍内容(简单地表示一个 PDF 文件)。
一旦创建了图书,就可以在图书搜索页面上找到它。最后,管理员用户可以查看应用的所有用户,如图图 13-11 所示。
图 13-11 。管理员用户的用户管理页面
管理员用户可以查看应用的所有用户,也可以删除其中的任何用户。
大型应用架构
Mega App 应用利用了以下 Java EE 7 技术:
- 用于处理用户界面交互的 JSF 2.2。
- 用于事务处理的 EJB 3.2。
- 用于数据持久性的 JPA 2.1。
- 用于 bean 管理的 CDI 1.1。
- Bean Validation 1.1 用于处理模型验证。
为了拥有俏皮的 HTML5/CSS3 用户界面,Mega App 使用了 Twitter Bootstrap 库:getbootstrap.com
。Twitter Bootstrap 是一个轻量级的 HTML5/CSS3 库,包含漂亮的排版、表单、按钮、导航和其他界面组件的设计模板,以及可选的 JavaScript 扩展。
为了简单起见,Mega App 使用 Oracle Java DB。Java DB 是 Oracle 支持的 Apache Derby 开源数据库的发行版。它通过 JDBC 和 Java EE APIs 支持标准 ANSI/ISO SQL,并包含在 JDK 中。图 13-12 显示了 Mega App 应用的高级组件。
图 13-12 。Mega App 高级组件
如上图所示,应用具有以下高级组件:
- Facelets 页面:它们代表大型应用页面。他们通过 JSF 表达式语言(EL)使用支持 bean 和 JPA beans(使用 CDI 管理)。他们还利用应用 Facelets 模板。
- Facelets 模板:它们代表应用页面使用的模板。在 Mega App 中,有两个模板(一个用于公共页面,另一个用于受保护页面)。
- Backing Beans:它们是普通的托管 bean,在概念上与 UI 页面相关,不是应用模型的一部分。backing bean 是集中处理页面动作的理想选择(有时 backing bean 被称为控制器类)。在 Mega App 中,backing beans 主要使用 JPA CDI 托管 bean 来调用业务管理器(服务)EJB,以便执行所需的操作。
- 业务管理器 EJB:为了执行业务操作,支持 beans 调用业务管理器 EJB。业务管理器 EJB 是无状态会话 EJB,它使用 JPA 实体和 JPA EntityManager 来执行所需的数据库操作。在 Mega App 中,我们有以下业务管理器 EJB:
- 图书管理员 EJB,处理图书管理操作,如注册图书、更新图书、删除图书、获取图书信息。。。等等)。
- 图书请求经理 EJB 处理图书请求流程操作(sendBookRequest、approveBookRequest 和 viewRequests)。
- 大用户经理 EJB 负责处理大应用用户管理操作(获取大用户、检索大用户、注册大用户、删除大用户)。
- JPA 实体(CDI 托管 bean):JPA 实体表示映射到数据库表的数据类。在 Mega App 中,JPA 实体被用作应用的 CDI 托管 beanss,这些 bean 使用 EL 与 Facelets 页面绑定在一起。
构建数据模型
先从创建 Mega App 的数据模型开始。图 13-13 显示了 Mega App 逻辑数据模型。
图 13-13 。大型应用逻辑数据模型
如图所示,Mega App 数据模型包含四个实体:
-
BOOK Entity: BOOK entity is responsible for storing the book data. It has the attributes shown in Table 13-1.
表 13-1 。图书实体属性
图书实体与 BOOK_REQUEST 的关系类型是“一对多”,即一本书可以有 0 个或多个图书请求。
-
BOOK_REQUEST Entity: BOOK_REQUEST entity is responsible for storing the requests for the book that are performed by the application users. It has the attributes shown in Table 13-2.
表 13-2 。预订 _ 请求表
-
USER Entity: USER entity is responsible for storing the Mega App users. It has the following attributes as shown in Table 13-3.
表 13-3 。大型用户实体
用户实体与 BOOK_REQUEST 的关系类型是“一对多”,即一个用户可以执行 0 个或多个图书请求。
用户实体与 USER_GROUP 的关系类型是“一对多”,即一个用户可以属于 0 个或多个组。
注意Mega App 代码确保用户始终是一个群组中的成员。
-
USER_GROUP Entity: USER_GROUP entity is responsible for storing the Mega App user groups. It has the attributes shown in Table 13-4.
表 13-4 。用户组实体
清单 13-1 显示了 Java DB 逻辑数据模型的等效 SQL DDL(数据定义语言)脚本。
清单 13-1。 Mega App SQL DDL 脚本
-- Create Table: USER_GROUP
--------------------------------------------------------------------------------
CREATE TABLE USER_GROUP
( ID INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1)
, USER_ID VARCHAR(64)
, "GROUP_ID" INTEGER
);
-- Create Table: BOOK
--------------------------------------------------------------------------------
CREATE TABLE BOOK
(
ID INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1)
,ISBN VARCHAR(64) NOT NULL
,TITLE VARCHAR(128) NOT NULL
,AUTHOR VARCHAR(128) NOT NULL
,PUBLISHER VARCHAR(64) NOT NULL
,LANG VARCHAR(64) NOT NULL
,CONTENT blob(1M) NOT NULL
);
-- Create Table: MEGA_USER
--------------------------------------------------------------------------------
CREATE TABLE MEGA_USER
(
ID VARCHAR(64) NOT NULL PRIMARY KEY
,FIRST_NAME VARCHAR(32) NOT NULL
,LAST_NAME VARCHAR(32) NOT NULL
,PASSWORD VARCHAR(32) NOT NULL
);
-- Create Table: BOOK_REQUEST
--------------------------------------------------------------------------------
CREATE TABLE BOOK_REQUEST
(
ID INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1)
,BOOK_ID INTEGER NOT NULL
,USER_ID VARCHAR(64) NOT NULL
,REQUEST_TIME BIGINT
,RESPONSE_TIME BIGINT
,STATUS INTEGER NOT NULL
);
-- Create Foreign Key: BOOK_REQUEST.USER_ID -> MEGA_USER.ID
ALTER TABLE BOOK_REQUEST ADD CONSTRAINT FK_BOOK_REQUEST_USER_ID_MEGA_USER_ID FOREIGN KEY (USER_ID) REFERENCES MEGA_USER(ID);
-- Create Foreign Key: BOOK_REQUEST.BOOK_ID -> BOOK.ID
ALTER TABLE BOOK_REQUEST ADD CONSTRAINT FK_BOOK_REQUEST_BOOK_ID_BOOK_ID FOREIGN KEY (BOOK_ID) REFERENCES BOOK(ID);
-- Create Foreign Key: USER_GROUP.USER_ID -> MEGA_USER.ID
ALTER TABLE USER_GROUP ADD CONSTRAINT FK_USER_GROUP_USER_ID FOREIGN KEY (USER_ID) REFERENCES MEGA_USER(ID);
构建服务层(EJB)
创建数据模型后,我们需要定义将从 JSF 支持 beans 中使用的应用 API(服务)。Mega App 利用 EJB 技术为客户端提供应用服务(JSF 支持 beans)。在 Mega App 中,我们有以下三个业务管理器 EJB:
- 图书经理 EJB。
- 图书需求经理 EJB。
- 超级用户经理 EJB。
图书管理员 EJB 负责处理图书管理操作,包括
- 登记新书。
- 更新图书信息。
- 移除书籍。
- 获取图书信息。
- 获取图书内容。
- 获取所有书籍。
图书需求经理 EJB 负责处理图书需求流程操作,包括
- 应用用户发送图书请求。
- 批准应用管理员的图书请求。
- 拒绝应用管理员的图书请求。
- 应用管理员删除图书。
用户经理 EJB 负责处理用户管理操作,包括
- 注册大型应用用户。
- 获取大量应用用户信息。
- 移除大型应用用户。
- 找回百万应用用户。
清单 13-2 显示了 persistence.xml 文件(位于/resources/META-INF 下)。persistence.xml 文件使用 JPA 2.1 版,它定义了使用“JTA”事务类型的“megaAppUnit”持久性单元。使用“JTA”事务类型意味着容器将为您处理 EntityManager 的创建和跟踪,并且您可以使用@PersistenceContext 注释获得它的实例。JTA 数据源设置为 jdbc/mega 数据源,使用它我们将能够访问 Mega App 数据库。
清单 13-2。persistence . XML 文件
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" FontName">http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="megaAppUnit" transaction-type="JTA">
<jta-data-source>jdbc/mega</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties/>
</persistence-unit>
</persistence>
注意值得注意的是,在 Java EE 7 中,大部分部署描述符名称空间都从前缀
java.sun.com
改为xmlns.jcp.org
。
清单 13-3 显示了图书管理器 EJB 的 EJB 接口,使用@Local 注释将其定义为 EJB 本地接口。
清单 13-3。 EJB 地方接口的编书人 EJB
package com.jsfprohtml5.megaapp.service;
import com.jsfprohtml5.megaapp.model.Book;
import com.jsfprohtml5.megaapp.service.exception.BookAlreadyExists;
import com.jsfprohtml5.megaapp.service.exception.BookNotFound;
import java.util.List;
import javax.ejb.Local;
@Local
public interface BookManagerLocal {
public Book getBookInformation(Integer bookID) throws BookNotFound;
public Book registerBook(Book book) throws BookAlreadyExists;
public Book updateBook(Book book) throws BookNotFound;
public void removeBook(Integer bookID) throws BookNotFound;
public byte[] getBookContent(Integer bookID) throws BookNotFound;
public List<Book> getAllBooks(Book book);
}
提示当 EJB 客户端运行在 EJB 本身的同一个 JVM 中时,使用@Local 注释是合适的。@Local 批注比@Remote 批注更有效,因为它不需要参数编组、传输和取消编组。
清单 13-4 显示了 BookManager EJB 类的 registerBook 和 updateBook 方法。
清单 13-4。 图书管理员 EJB 登记和更新图书的方法
package com.jsfprohtml5.megaapp.service;
import com.jsfprohtml5.megaapp.model.Book;
import com.jsfprohtml5.megaapp.service.exception.BookAlreadyExists;
import com.jsfprohtml5.megaapp.service.exception.BookNotFound;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
@Stateless
public class BookManager implements BookManagerLocal {
@PersistenceContext(unitName = "megaAppUnit")
EntityManager em;
//Other interface methods ...
@Override
public Book registerBook(Book book) throws BookAlreadyExists {
Query query = em.createQuery("select book from Book book where "
+ "book.isbn = :isbn");
query.setParameter("isbn", book.getIsbn());
try {
query.getSingleResult();
throw new BookAlreadyExists();
} catch (NoResultException exception) {
Logger.getLogger(BookManager.class.getName()).log(Level.FINER, "No similar books found");
}
em.persist(book);
em.flush();
return book;
}
@Override
public Book updateBook(Book book) throws BookNotFound {
Book updatableBook = em.find(Book.class, book.getId());
if (updatableBook == null) {
throw new BookNotFound();
}
mergeBookAttrs(book, updatableBook);
em.merge(updatableBook);
em.flush();
return book;
}
private void mergeBookAttrs(Book book, Book updatableBook) {
if (book.getAuthor() != null) {
updatableBook.setAuthor(book.getAuthor());
}
if (book.getContent() != null) {
updatableBook.setContent(book.getContent());
}
if (book.getIsbn() != null) {
updatableBook.setIsbn(book.getIsbn());
}
if (book.getLang() != null) {
updatableBook.setLang(book.getLang());
}
if (book.getPublisher() != null) {
updatableBook.setPublisher(book.getPublisher());
}
if (book.getTitle() != null) {
updatableBook.setTitle(book.getTitle());
}
}
}
@Stateless annotation 将 BookManager 类定义为无状态会话 EJB。@PersistenceContext 注释用于注入容器管理的实体管理器实例,使用它我们将能够执行数据库操作。registerBook 方法检查现有图书的 ISBN 是否与新图书的 ISBN 匹配,如果找到匹配的情况,则抛出 BookAlreadyExists 异常,否则将新书数据保存在数据库中。如果数据库中不存在要更新的图书,updateBook 方法将引发 BookNotFound 异常;如果目标图书存在,它将使用 mergeBookAttrs 方法将新数据更新与现有图书数据合并。清单 13-5 显示了图书管理员 EJB 的剩余方法。
清单 13-5。 著书家 EJB 的剩余方法
@Stateless
public class BookManager implements BookManagerLocal {
@PersistenceContext(unitName = "megaAppUnit")
EntityManager em;
@Override
public Book getBookInformation(Integer bookID) throws BookNotFound {
Query query = em.createQuery("select book.id, book.isbn, book.title, "
+ "book.author, book.publisher, book.lang from Book book where "
+ "book.id = :id");
query.setParameter("id", bookID);
Object[] bookInfo = null;
try {
bookInfo = (Object[]) query.getSingleResult();
} catch (NoResultException exception) {
throw new BookNotFound(exception.getMessage());
}
Book book = new Book(
(Integer) bookInfo[0],
(String) bookInfo[1],
(String) bookInfo[2],
(String) bookInfo[3],
(String) bookInfo[4],
(String) bookInfo[5],
null);
return book;
}
@Override
public void removeBook(Integer bookID) throws BookNotFound {
Book book = em.find(Book.class, bookID);
if (book == null) {
throw new BookNotFound();
}
em.remove(book);
em.flush();
}
@Override
public byte[] getBookContent(Integer bookID) throws BookNotFound {
byte[] content = null;
try {
content = (byte[]) em.createQuery("Select book.content from Book book where book.id=:id")
.setParameter("id", bookID)
.getSingleResult();
} catch (NoResultException exception) {
throw new BookNotFound(exception.getMessage());
}
return content;
}
@Override
public List<Book> getAllBooks(Book searchableBook) {
List<Book> books = new ArrayList<Book>();
String searchableTitle = searchableBook.getTitle();
Query query = em.createQuery("select book.id, book.isbn, book.title, "
+ "book.author, book.publisher, book.lang from Book book where "
+ "book.title like :title");
query.setParameter("title", "%" + searchableTitle + "%");
List<Object[]> bookList = (List<Object[]>) query.getResultList();
if (bookList == null) {
return books;
}
for (Object[] bookInfo : bookList) {
Book book = new Book(
(Integer) bookInfo[0],
(String) bookInfo[1],
(String) bookInfo[2],
(String) bookInfo[3],
(String) bookInfo[4],
(String) bookInfo[5],
null);
books.add(book);
}
return books;
}
//...
}
书商 EJB 的其他方法如下:
- getBookInformation:获取图书的元数据(没有图书内容的图书数据)。如果没有找到目标书,它抛出 BookNotFound 异常。
- getBookContent :仅获取书籍内容作为字节数组。如果没有找到目标书,它抛出 BookNotFound 异常。
- removeBook :从数据库中删除图书。如果没有找到目标书,它抛出 BookNotFound 异常。
- getAllBooks :接受一个 book 对象作为参数,允许 API 使用任何 book 属性进行搜索。但是,现在,getAllBooks()实现只使用图书的 title 属性进行搜索,使用 SQL like 操作符。如果没有结果,API 将返回一个空列表。
现在,让我们检查 Book JPA 实体的详细信息,该实体包含 Book 属性和使用 Java Bean 验证 API 的验证约束。清单 13-6 显示了图书 JPA 实体代码。
清单 13-6。 图书 JPA 实体
package com.jsfprohtml5.megaapp.model;
import java.io.Serializable;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Entity
@Table(name = "BOOK")
public class Book implements Serializable {
private static final long serialVersionUID = 197654646546456456L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Integer id;
@Basic(optional = false)
@NotNull
@Size(min = 10, max = 20, message = "ISBN must be between 10 and 20 characters")
@Column(name = "ISBN")
private String isbn;
@Basic(optional = false)
@NotNull
@Size(min = 5, max = 128, message = "Book title must be between 5 and 128 characters")
@Column(name = "TITLE")
private String title;
@Basic(optional = false)
@NotNull
@Size(min = 3, max = 128, message = "Book author must be between 3 and 128 characters")
@Column(name = "AUTHOR")
private String author;
@Basic(optional = false)
@NotNull
@Size(min = 3, max = 64, message = "Book publisher must be between 3 and 64 characters")
@Column(name = "PUBLISHER")
private String publisher;
@Basic(optional = false)
@NotNull
@Size(min = 3, max = 64, message = "Book language must be between 3 and 64 characters")
@Column(name = "LANG")
private String lang;
@Basic(optional = false)
@Lob
@Column(name = "CONTENT")
private Serializable content;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "bookId")
private List<BookRequest> bookRequestList;
public Book() {
}
public Book(Integer id) {
this.id = id;
}
public Book(Integer id, String isbn, String title, String author, String publisher, String lang, Serializable content) {
this.id = id;
this.isbn = isbn;
this.title = title;
this.author = author;
this.publisher = publisher;
this.lang = lang;
this.content = content;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getPublisher() {
return publisher;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public String getLang() {
return lang;
}
public void setLang(String lang) {
this.lang = lang;
}
public Serializable getContent() {
return content;
}
public void setContent(Serializable content) {
this.content = content;
}
public List<BookRequest> getBookRequestList() {
return bookRequestList;
}
public void setBookRequestList(List<BookRequest> bookRequestList) {
this.bookRequestList = bookRequestList;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof Book)) {
return false;
}
Book other = (Book) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "com.jsfprohtml5.megaapp.model.Book[ id=" + id + " ]";
}
}
Book JPA 实体类有以下属性:
- id:使用@Id 批注标记为实体的主键。它是自动生成的属性。
- ISBN:验证为非空。ISBN 验证为 10 到 20 个字符。
- 标题:验证为非空。图书标题的长度在 5 到 128 个字符之间。
- 作者:验证为非空。图书作者被验证为介于 3 到 128 个字符之间。
- 发布者:验证为非空。图书出版商被验证为介于 3 到 64 个字符之间。
- lang:表示图书语言,并被验证为非空。图书语言被验证为 3 到 64 个字符。
- content:表示书籍内容,它被标记为 Lob(大对象)。
- bookRequestList:表示书的关联请求;它的级联类型设置为“All”,这意味着所有的操作(合并、持久化、移除、刷新)都将级联。
注意需要注意的是,以下所有 JPA 实体都将被用作应用 Facelets 页面中的 CDI beans:有关更多信息,请查看“编写页面模板”一节。
现在,让我们来看第二位 EJB 经理(MegaUserManager) EJB。清单 13-7 显示了 EJB 的 EJB 本地接口。
清单 13-7。EJB 本地界面
package com.jsfprohtml5.megaapp.service;
import com.jsfprohtml5.megaapp.model.MegaUser;
import com.jsfprohtml5.megaapp.service.exception.UserAlreadyExists;
import com.jsfprohtml5.megaapp.service.exception.UserNotFound;
import java.util.List;
import javax.ejb.Local;
@Local
public interface MegaUserManagerLocal {
public MegaUser getMegaUser(String userID) throws UserNotFound;
public List<MegaUser> retrieveMegaUsers();
public MegaUser registerMegaUser(MegaUser user) throws UserAlreadyExists;
public void removeMegaUser(String userID) throws UserNotFound;
}
清单 13-8 显示了 EJB 类的实现。
清单 13-8。EJB 类 MegaUserManager
package com.jsfprohtml5.megaapp.service;
import com.jsfprohtml5.megaapp.model.Constants;
import com.jsfprohtml5.megaapp.model.MegaUser;
import com.jsfprohtml5.megaapp.model.UserGroup;
import com.jsfprohtml5.megaapp.service.exception.UserAlreadyExists;
import com.jsfprohtml5.megaapp.service.exception.UserNotFound;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
@Stateless
public class MegaUserManager implements MegaUserManagerLocal {
@PersistenceContext(unitName = "megaAppUnit")
EntityManager em;
@Override
public MegaUser getMegaUser(String userID) throws UserNotFound {
Query query = em.createQuery("select megaUser.id, megaUser.firstName"
+ ", megaUser.lastName from MegaUser megaUser where "
+ "megaUser.id = :id");
query.setParameter("id", userID);
Object[] megaUserInfo;
try {
megaUserInfo = (Object[]) query.getSingleResult();
} catch (NoResultException exception) {
throw new UserNotFound(exception.getMessage());
}
MegaUser megaUser = new MegaUser(
(String) megaUserInfo[0],
(String) megaUserInfo[1],
(String) megaUserInfo[2],
null);
return megaUser;
}
@Override
public MegaUser registerMegaUser(MegaUser user) throws UserAlreadyExists {
Query query = em.createQuery("select megaUser from MegaUser megaUser where "
+ "megaUser.id = :userID");
query.setParameter("userID", user.getId());
try {
query.getSingleResult();
throw new UserAlreadyExists();
} catch (NoResultException exception) {
Logger.getLogger(BookManager.class.getName()).log(Level.FINER, "No user found");
}
List<UserGroup> userGroups = new ArrayList<UserGroup>();
UserGroup userGroup = new UserGroup();
userGroup.setUserId(user);
userGroup.setGroupId(Constants.USER_GROUP);
userGroups.add(userGroup);
user.setUserGroupList(userGroups);
em.persist(user);
em.flush();
return user;
}
@Override
public void removeMegaUser(String userID) throws UserNotFound {
MegaUser megaUser = em.find(MegaUser.class, userID);
if (megaUser == null) {
throw new UserNotFound();
}
em.remove(megaUser);
em.flush();
}
@Override
public List<MegaUser> retrieveMegaUsers() {
Query query = em.createQuery("select megaUser from MegaUser megaUser", MegaUser.class);
List<MegaUser> result = query.getResultList();
if (result == null) {
return new ArrayList<MegaUser>();
}
return result;
}
}
用户管理器 EJB 的方法如下:
- getMegaUser:检索用户信息。如果没有找到目标用户,它抛出 UserNotFound 异常。
- registerMegaUser :将 Mega App 数据库中的 Mega App 用户注册为应用用户(即,使用户成为“用户”组中的成员)。如果用户 ID 已经存在于数据库中,它将抛出 UserAlreadyExists 异常。
- removeMegaUser :从数据库中删除 Mega App 用户。如果没有找到目标用户,它抛出 UserNotFound 异常。
- retrieveMegaUsers :检索 Mega App 的所有用户(无论是普通用户还是管理员用户)。
让我们检查 MegaUser JPA 实体的细节,它保存了用户属性及其相关的验证约束。清单 13-9 显示了 MegaUser JPA 实体类。
清单 13-9。 MegaUser JPA 实体类
package com.jsfprohtml5.megaapp.model;
import java.io.Serializable;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Entity
@Table(name = "MEGA_USER")
public class MegaUser implements Serializable {
private static final long serialVersionUID = 109890766546456L;
@Id
@Basic(optional = false)
@Size(min = 3, max = 64, message = "ID must be between 3 and 64 characters")
@Column(name = "ID")
private String id;
@Basic(optional = false)
@NotNull
@Size(min = 3, max = 32, message = "First name must be between 3 and 32 characters")
@Column(name = "FIRST_NAME")
private String firstName;
@Basic(optional = false)
@NotNull
@Size(min = 3, max = 32, message = "Last name must be between 3 and 32 characters")
@Column(name = "LAST_NAME")
private String lastName;
@Basic(optional = false)
@NotNull
@Size(min = 6, max = 32, message = "Password must be between 6 and 32 characters")
@Column(name = "PASSWORD")
private String password;
@Transient
private String password2;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "userId")
private List<UserGroup> userGroupList;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "userId")
private List<BookRequest> bookRequestList;
public MegaUser() {
}
public MegaUser(String id) {
this.id = id;
}
public MegaUser(String id, String firstName, String lastName, String password) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.password = password;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword2() {
return password2;
}
public void setPassword2(String password2) {
this.password2 = password2;
}
public List<UserGroup> getUserGroupList() {
return userGroupList;
}
public void setUserGroupList(List<UserGroup> userGroupList) {
this.userGroupList = userGroupList;
}
public List<BookRequest> getBookRequestList() {
return bookRequestList;
}
public void setBookRequestList(List<BookRequest> bookRequestList) {
this.bookRequestList = bookRequestList;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof MegaUser)) {
return false;
}
MegaUser other = (MegaUser) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "com.jsfprohtml5.megaapp.model.MegaUser[ id=" + id + " ]";
}
}
MegaUser JPA 实体类具有以下属性:
- id:使用@Id 批注标记为实体的主键。它被验证为介于 3 到 64 个字符之间。
- firstName:验证为不为空。它被验证为介于 3 到 32 个字符之间。
- lastName:验证为不为空。它被验证为介于 3 到 32 个字符之间。
- 密码:验证为非空。它被验证为介于 6 到 32 个字符之间
- userGroupList:表示用户的关联组,其级联类型设置为“All”,这意味着所有的操作(合并、持久化、移除、刷新)将被级联(如清单 13-8 所示,用户属于一个组)。
- bookRequestList:表示用户的相关图书请求,其级联类型设置为“All”,这意味着所有操作(合并、持久化、移除、刷新)都将级联。
注意:MegaUser 实体类中的 password2 属性用于确认注册页面中的密码输入。如图所示,该属性用@Transient 注释进行了注释,以表明该字段不是持久的。
用户组 JPA 实体类映射到 USER_GROUP 表,该表将应用用户映射到应用组,如清单 13-10 所示。
清单 13-10。 用户组 JPA 实体类
package com.jsfprohtml5.megaapp.model;
import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "USER_GROUP")
public class UserGroup implements Serializable {
private static final long serialVersionUID = 198213812312319321L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Integer id;
@Column(name = "GROUP_ID")
private Integer groupId;
@JoinColumn(name = "USER_ID", referencedColumnName = "ID")
@ManyToOne
private MegaUser userId;
public UserGroup() {
}
public UserGroup(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getGroupId() {
return groupId;
}
public void setGroupId(Integer groupId) {
this.groupId = groupId;
}
public MegaUser getUserId() {
return userId;
}
public void setUserId(MegaUser userId) {
this.userId = userId;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof UserGroup)) {
return false;
}
UserGroup other = (UserGroup) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "com.jsfprohtml5.megaapp.model.UserGroup[ id=" + id + " ]";
}
}
用户组 JPA 实体类具有以下属性:
- id:使用@Id 批注标记为实体的主键。它是自动生成的属性。
- groupId:表示组 Id(用户组可以是 1,管理员组可以是 2)。
- userId:表示关联的用户对象。
第三位也是最后一位 EJB 经理是 EJB。清单 13-11 显示了 EJB 的 EJB 本地接口。
清单 13-11。 BookRequestManager 本地 EJB 接口
package com.jsfprohtml5.megaapp.service;
import com.jsfprohtml5.megaapp.model.BookRequest;
import com.jsfprohtml5.megaapp.service.exception.BookRequestAlreadyExists;
import com.jsfprohtml5.megaapp.service.exception.BookRequestNotFound;
import java.util.List;
import javax.ejb.Local;
@Local
public interface BookRequestManagerLocal {
public BookRequest sendBookRequest(BookRequest bookRequest) throws BookRequestAlreadyExists;
public void approveBookRequest(Integer bookRequestNumber) throws BookRequestNotFound;
public void rejectBookRequest(Integer bookRequestNumber) throws BookRequestNotFound;
public List<BookRequest> viewRequests(String userName, int status);
}
清单 13-12 显示了 BookRequestManager EJB 类的第一部分。
清单 13-12。 第一部分 BookRequestManager EJB 类
package com.jsfprohtml5.megaapp.service;
import com.jsfprohtml5.megaapp.model.Book;
import com.jsfprohtml5.megaapp.model.BookRequest;
import com.jsfprohtml5.megaapp.model.Constants;
import com.jsfprohtml5.megaapp.model.MegaUser;
import com.jsfprohtml5.megaapp.model.UserGroup;
import com.jsfprohtml5.megaapp.service.exception.BookRequestAlreadyExists;
import com.jsfprohtml5.megaapp.service.exception.BookRequestNotFound;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
@Stateless
public class BookRequestManager implements BookRequestManagerLocal {
@PersistenceContext(unitName = "megaAppUnit")
EntityManager em;
@Override
public BookRequest sendBookRequest(BookRequest bookRequest) throws BookRequestAlreadyExists {
Query query = em.createQuery("select bookRequest from BookRequest bookRequest where "
+ "bookRequest.bookId.id = :bookId and bookRequest.userId.id = :userId");
query.setParameter("bookId", bookRequest.getBookId().getId());
query.setParameter("userId", bookRequest.getUserId().getId());
try {
query.getSingleResult();
throw new BookRequestAlreadyExists();
} catch (NoResultException exception) {
Logger.getLogger(BookManager.class.getName()).log(Level.FINER, "No book request found");
}
bookRequest.setRequestTime(System.currentTimeMillis());
bookRequest.setStatus(Constants.PENDING_REQUEST); //pending status...
em.persist(bookRequest);
em.flush();
return bookRequest;
}
@Override
public void approveBookRequest(Integer bookRequestNumber) throws BookRequestNotFound {
BookRequest updatableBookRequest = em.find(BookRequest.class, bookRequestNumber);
if (updatableBookRequest == null) {
throw new BookRequestNotFound();
}
updatableBookRequest.setStatus(Constants.APPROVED_REQUEST); //approved status
updatableBookRequest.setResponseTime(System.currentTimeMillis());
em.merge(updatableBookRequest);
em.flush();
}
@Override
public void rejectBookRequest(Integer bookRequestNumber) throws BookRequestNotFound {
BookRequest updatableBookRequest = em.find(BookRequest.class, bookRequestNumber);
if (updatableBookRequest == null) {
throw new BookRequestNotFound();
}
updatableBookRequest.setStatus(Constants.REJECTED_REQUEST); //rejected status
updatableBookRequest.setResponseTime(System.currentTimeMillis());
em.merge(updatableBookRequest);
em.flush();
}
//...
}
BookRequestManager EJB 的第一种方法如下:
- sendBookRequest:在 BOOK_REQUEST 表中创建一个图书请求,将其状态设置为 1(表示待定),并将请求时间设置为当前系统时间。如果用户已经发送了对目标图书的请求,它将抛出 BookRequestAlreadyExists 异常。
- approveBookRequest:将图书请求状态设置为 3(表示已批准),并将响应时间设置为当前系统时间。如果没有找到目标图书请求,它抛出 BookRequestNotFound 异常。
- rejectBookRequest:将图书请求状态设置为 2(表示被拒绝),并将响应时间设置为当前系统时间。如果没有找到目标图书请求,它抛出 BookRequestNotFound 异常。
清单 13-13 显示了 BookRequestManager EJB 类的第二部分。
清单 13-13。 第二部分 BookRequestManager EJB 类
@Stateless
public class BookRequestManager implements BookRequestManagerLocal {
@PersistenceContext(unitName = "megaAppUnit")
EntityManager em;
// ...
@Override
public List<BookRequest> viewRequests(String userID, int status) {
String requestQuery = "select bookRequest.id, book.id, book.title, bookRequest.requestTime, bookRequest.responseTime, bookRequest.userId.id "
+ "from BookRequest bookRequest JOIN bookRequest.bookId book JOIN bookRequest.userId user "
+ "where bookRequest.status = :statusID";
Query query = null;
UserGroup group = getUserGroup(userID);
if (group.getGroupId() == Constants.USER_GROUP) {
requestQuery += " and bookRequest.userId.id = :userId";
query = em.createQuery(requestQuery);
query.setParameter("statusID", status);
query.setParameter("userId", userID);
} else {
query = em.createQuery(requestQuery);
query.setParameter("statusID", status);
}
List<BookRequest> bookRequests = new ArrayList<BookRequest>();
List<Object []> results = (List<Object []>) query.getResultList();
if (results == null) {
return bookRequests;
}
for (Object[] result : results) {
BookRequest bookRequest = new BookRequest((Integer) result[0]);
Book book = new Book();
book.setId((Integer) result[1]);
book.setTitle((String) result[2]);
bookRequest.setBookId(book);
bookRequest.setRequestTime((Long) result[3]);
bookRequest.setResponseTime((Long) result[4]);
bookRequest.setUserId(new MegaUser((String) result[5]));
bookRequests.add(bookRequest);
}
return bookRequests;
}
private UserGroup getUserGroup(String userID) {
Query query = em.createQuery("Select userGroup from UserGroup userGroup where userGroup.userId.id=:userID", UserGroup.class);
query.setParameter("userID", userID);
UserGroup group;
try {
group = (UserGroup) query.getSingleResult();
} catch (NoResultException exception) {
throw new IllegalStateException(userID + " state is invalid as user does not belong to any group!!!");
}
return group;
}
}
viewRequest 方法用于查看普通用户和管理员用户的图书请求。它需要两个参数:
- 用户 ID:表示请求查看图书请求的用户 ID。
- status:表示将被检索的图书请求的状态。它可以是三个值之一(1 表示待定,2 表示拒绝,3 表示批准;这些值在列表 13-14 ) 中显示的常量界面中编码。
清单 13-14。 常量界面
package com.jsfprohtml5.megaapp.model;
public interface Constants {
public static int USER_GROUP = 1;
public static int ADMIN_GROUP = 2;
public static int PENDING_REQUEST = 1;
public static int REJECTED_REQUEST = 2;
public static int APPROVED_REQUEST = 3;
public static String APP_PDF_TYPE = "application/pdf";
}
viewRequest 方法通过调用 getUserGroup 私有方法,使用提供的用户 ID 获取用户角色。如果用户角色是 ADMIN_GROUP,则用户将检索所提供状态(待定、拒绝或批准)下的所有请求。如果用户角色是 USER_GROUP,则用户将只检索由所提供的用户 ID 在所提供的状态(待处理、拒绝或批准)下发送的请求。
将从 backing beans 调用 viewRequest 方法,以便查看当前登录用户的待定、拒绝和批准的图书请求。
让我们检查 BookRequest JPA 实体的详细信息,它保存了图书请求属性。清单 13-15 显示了 BookRequest JPA 实体类。
清单 13-15。 BookRequest JPA 实体类
package com.jsfprohtml5.megaapp.model;
import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
@Entity
@Table(name = "BOOK_REQUEST")
public class BookRequest implements Serializable {
private static final long serialVersionUID = 132123123120090L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Integer id;
@Basic(optional = false)
@Column(name = "REQUEST_TIME")
private long requestTime;
@Basic(optional = false)
@Column(name = "RESPONSE_TIME")
private long responseTime;
@Basic(optional = false)
@NotNull
@Column(name = "STATUS")
private int status;
@JoinColumn(name = "USER_ID", referencedColumnName = "ID")
@ManyToOne(optional = false)
private MegaUser userId;
@JoinColumn(name = "BOOK_ID", referencedColumnName = "ID")
@ManyToOne(optional = false)
private Book bookId;
public BookRequest() {
}
public BookRequest(Integer id) {
this.id = id;
}
public BookRequest(Integer id, long requestTime, long responseTime, int status) {
this.id = id;
this.requestTime = requestTime;
this.responseTime = responseTime;
this.status = status;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public long getRequestTime() {
return requestTime;
}
public void setRequestTime(long requestTime) {
this.requestTime = requestTime;
}
public long getResponseTime() {
return responseTime;
}
public void setResponseTime(long responseTime) {
this.responseTime = responseTime;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public MegaUser getUserId() {
return userId;
}
public void setUserId(MegaUser userId) {
this.userId = userId;
}
public Book getBookId() {
return bookId;
}
public void setBookId(Book bookId) {
this.bookId = bookId;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof BookRequest)) {
return false;
}
BookRequest other = (BookRequest) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "com.jsfprohtml5.megaapp.model.BookRequest[ id=" + id + " ]";
}
}
BookRequest JPA 实体类具有以下属性:
- id:使用@Id 批注标记为实体的主键。它是自动生成的属性。
- requestTime:表示请求时间的长类型字段。
- responseTime:表示响应时间的长类型字段。
- status:表示请求状态的整数字段。它可以有三个值之一(1 表示待定,2 表示拒绝,3 表示批准)。
- userId:表示与请求相关的用户对象。
- bookId:表示请求关联的 book 对象。
这些是可用的三个服务 EJBs 正如在前面的代码片段中可以注意到的,管理器 EJB 的许多方法都会抛出以下定制异常的实例:
- 图书已经存在
- 找不到书
- bookrequestalreadyeexists
- BookRequestNotFound
- 用户已经存在
- 用户未找到
所有这些自定义异常都继承自 java.lang.Exception. 清单 13-16 显示了 BookAlreadyExists 异常作为一个例子。
清单 13-16。 图书已经存在异常
package com.jsfprohtml5.megaapp.service.exception;
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class BookAlreadyExists extends Exception {
public BookAlreadyExists () {
this.message = "Book already exists";
}
public BookAlreadyExists(String message) {
this.message = message;
}
@Override
public String getMessage() {
return this.message;
}
private String message;
}
注意建议用@ApplicationException 注释自定义应用异常,以避免将它们包装在容器异常中。这样做将允许 EJB 客户端直接捕获抛出的异常,而不必打开容器异常。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
2023-08-19 赏味不足:详细来聊下轻资产运作,我从不做重资产
2023-08-19 老隋:什么赚钱就做什么,记住轻资产运营,试错成本低
2023-08-19 iBooker 技术评论 20230819:打工是风险最高的事情
2023-08-19 卓钥商学苑:创业期间被合伙人背叛了怎么办?处理方式有哪些?