精通-JavaServerFace-2-2-全-
精通 JavaServerFace 2.2(全)
原文:
zh.annas-archive.org/md5/fe421522870475d16db17cfb1bb1c0fb
译者:飞龙
前言
本书将涵盖开发 JSF 2.2 应用程序所涉及的所有重要方面(重要特性)。它提供了充分利用 JSF 2.2 的清晰指导,并提供了许多练习(超过 300 个完整应用程序)来构建令人印象深刻的基于 JSF 的 Web 应用程序。
我们首先从关于表达式语言(EL)的章节开始,涵盖了 EL 2.2 和 EL 3.0 最重要的方面。接着,我们进行了一篇关于 JSF 通信的全面论文,随后是一章关于 JSF 2.2 作用域的精彩章节。在此阶段,我们讨论了大多数 JSF 组件和配置。进一步地,我们开始了一系列非常有趣的主题,例如 HTML5 和 AJAX。之后,我们剖析了 JSF 视图状态的概念,并学习如何处理这个微妙的 JSF 话题。此外,我们将详细讨论自定义组件和组合组件。之后,我们将讨论 JSF 2.2 资源的库合约(主题)。最后,最后一章将加强你对 JSF 2.2 Facelets 的知识。
本书涵盖的内容
第一章, 通过表达式语言(EL 3.0)动态访问 JSF 应用程序数据,涵盖了表达式语言(EL)的主要方面。我们将涵盖 EL 2.2 和 EL 3.0,包括新操作符、lambda 表达式和集合对象支持。
第二章, JSF 中的通信,代表了对 JSF 机制的分析,这些机制用于确保 JSF 组件之间的通信。因此,我们将涵盖上下文参数、请求参数、JSF 2.2 对 GET 请求的操作(视图操作)等。
第三章, JSF 作用域 – 生命周期和在托管 Bean 通信中的应用,教你区分使用 JSF 和 CDI 作用域的良莠之别。我们将详细讨论 JSF 作用域与 CDI 作用域、请求、会话、视图作用域(包括新的 JSF 2.2 视图作用域)、应用作用域、会话作用域、JSF 2.2 流程作用域(重要特性)以及更多内容。
第四章, 使用 XML 文件和注解配置 JSF – 第一部分,以学习示例的方式描述了 JSF 组件的配置方面。在faces-config.xml
文件中配置 JSF 组件相当直接且乏味,但如果我们将每个组件及其在多个用例中的潜力进行利用,那么事情就会变得更有趣。
第五章, 使用 XML 文件和注解配置 JSF – 第二部分,作为前一章的延续。在这里,我们将讨论配置资源处理器(JSF 2.2 的新javax.faces.WEBAPP_RESOURCES_DIRECTORY
上下文参数),配置闪存(JSF 2.2 的FlashFactory
、FlashWrapper
和闪存系统事件),JSF 2.2 窗口 ID API,注入机制(从 JSF 2.2 开始,在大多数 JSF 组件中都是可能的),以及更多。
第六章, 处理表格数据,向<h:dataTable>
标签致敬。在这里,我们将专注于 JSF 2.2 的CollectionDataModel
API(它支持UIData
中的Collection
接口)。此外,我们还将了解表格分页、删除/编辑/更新表格行、过滤和样式化 JSF 表格。
第七章, JSF 和 AJAX,利用 JSF 2.2 的delay
属性进行 AJAX 请求的队列控制。它讨论了如何使用 JSF 2.2 重置值属性(输入字段可以在验证错误后使用 AJAX 更新),AJAX 和 JSF 2.2 流程作用域,如何自定义 AJAX 脚本,以及更多。这是几乎所有 JSF 书籍中的经典章节。
第八章, JSF 2.2 – HTML5 和上传,将主题分为两部分。第一部分完全致力于大票功能,HTML5 和 JSF 2.2(透传属性和元素)。第二部分致力于 JSF 2.2 的新上传组件<h:inputFile>
。
第九章, JSF 状态管理,详细阐述了 JSF 视图状态。本章标题将涉及 JSF 的保存视图状态(包括 JSF 2.2 对状态保存方法的不区分大小写和标准化的服务器状态序列化)以及 JSF 2.2 无状态视图(大票功能)。
第十章, JSF 自定义组件,是任何 JSF 书籍中的另一个经典章节。显然,主要话题旨在构建自定义和组合组件。我们将专注于基于新的 JSF 2.2 方法(Facelet 的组件标签可以通过注解声明)开发几种组件。
第十一章, JSF 2.2 资源库合约 – 主题,专注于新的 JSF 2.2 资源库合约功能(大票功能)。你将学习如何使用合约,使用合约样式化 JSF 表格和 UI 组件,在不同类型的设备上样式化合约,以及更多。
第十二章, Facelets 模板,描述了 Facelets 模板的病毒式特性。我们将专注于 Facelets 的声明性和程序性方面。
附录, JSF 生命周期,涵盖不同 JSF 阶段的图表。
你需要这本书的内容
为了运行本书中的应用程序,你需要以下软件应用:
-
NetBeans IDE(推荐版本为 8.0 或更高)
-
GlassFish 4.0
-
JSF Mojarra 2.2.6(推荐)/ MyFaces 2.2.2
这本书面向谁
这本书是 JSF 2.0 和 2.2 之间的完美结合。它献给那些有先验经验的 JSF 开发者,他们希望将自己的知识升级到新的 JSF 2.2。通过巩固你在 JSF 2.0 上的知识,并加入 JSF 2.2 的力量,你很快就会成为 JSF 专家。
术语表
在这本书中,你会发现许多不同风格的文本,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“例如,在以下示例中,你调用一个名为firstLambdaAction
的方法——lambda 表达式从这个方法中调用。”
代码块设置如下:
<ui:repeat value="#{get_sublist(myList, from, to)}" var="t">
#{t}
</ui:repeat>
当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
<h:dataTable value="#{playersBean.dataArrayList}" binding="#{table}" var="t">
新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“当点击登录按钮时,JSF 将调用playerLogin
方法。”
注意
警告或重要注意事项以如下框中的形式出现。
小贴士
小贴士和技巧看起来是这样的。
读者反馈
我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或者可能不喜欢什么。读者的反馈对我们开发你真正能从中获得最大价值的标题非常重要。
要发送给我们一般性的反馈,只需发送一封电子邮件到 <feedback@packtpub.com>
,并在邮件的主题中提及书名。
如果你在某个主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲所有者,我们有一些事情可以帮助你从你的购买中获得最大价值。
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com
。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给你。
错误表
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过从www.packtpub.com/support
中选择您的标题来查看任何现有勘误。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
如果您发现任何疑似盗版材料,请通过发送链接到<copyright@packtpub.com>
与我们联系。
我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过发送链接到<questions@packtpub.com>
与我们联系,我们将尽力解决。
第一章:动态访问 JSF 应用程序数据通过表达式语言(EL 3.0)
Java 表达式语言(EL)是一种紧凑且强大的机制,它使得在 JSP 和 JSF 应用程序(包括基于 JSF 的开发框架,如 PrimeFaces、ICEfaces 和 RichFaces)中实现动态通信成为可能;我们在表示层嵌入表达式以与应用程序逻辑层进行通信。EL 提供双向通信,这意味着我们可以将应用程序逻辑数据暴露给用户,同时也可以提交用户数据以进行处理。一般来说,EL 可以用来用用户数据填充 HTTP 请求,从 HTTP 响应中提取和暴露数据,更新 HTML DOM,条件处理数据等等。
注意
通常,EL 表达式会出现在 JSP 和 JSF 页面中,但它们也可以出现在外部,例如在faces-config.xml
中。
在本章中,您将了解如何在网页中使用 EL 与托管 Bean 进行通信,这是 JSF 应用程序中最常见的情况。我们将涵盖以下主题:
-
EL 语法、运算符和保留字
-
EL 即时和延迟求值
-
EL 值和方法表达式
-
JSF 中的条件文本
-
编写自定义 EL 解析器
EL 语法
在本节中,您可以了解 EL 2.2 和 3.0 语法的概述。EL 支持一些运算符和保留字。以下将简要描述这些内容(更多详情请参考 EL 规范文档(download.oracle.com/otndocs/jcp/el-3_0-fr-eval-spec/index.html
)).
EL 运算符
EL 支持以下几类运算符—算术、关系、逻辑、条件、空值,从 EL 3.0 开始增加了字符串连接、赋值和分号运算符:
文本 | 描述 | 符号 |
---|---|---|
A + B |
加法 | + |
A - B |
减法 | - |
A * B |
乘法 | * |
A {div , / } B |
算术运算符除法 | /, div |
A {mod , % } B |
算术运算符取模 | % , mod |
A {and , && } B |
逻辑与 | && , and |
A {or , || } B |
逻辑或 | || , or |
{not , ! } A |
逻辑非 | ! , not |
A {lt , < } B |
关系小于 | < , lt |
A {gt , > } B |
关系大于 | > , gt |
A {le , <= } B |
关系小于等于 | <= , le |
A {ge , >= } B |
关系大于等于 | >= , ge |
A {eq , == } B |
等于 | == , eq |
A {ne , != } B |
不等于 | != , ne |
A = B |
赋值(EL 3.0) | = |
A ; B |
分号(EL 3.0) | ; |
A += B |
字符串连接(EL 3.0) | += |
A -> B |
Lambda 表达式(EL 3.0) | -> |
empty A |
判断一个值是否为空或空值 | |
A ? B : C |
根据 A 的评估结果评估 B 或 C。称为三元运算符。 | ?: |
用于编写 EL 表达式时使用 | . |
|
用于编写 EL 表达式时使用 | [] |
EL 运算符优先级
符合 EL 规范,运算符的优先级从高到低,从左到右如下:
-
[].
-
()
(用于改变运算符的优先级) -
-
(一元)not ! empty
-
* / div % mod
-
+
-
(二元) -
+=
-
< > <= >= lt gt le ge
-
== != eq ne
-
&& 和
-
|| 或
-
?
:
-
->
(lambda 表达式) -
=
-
;
EL 保留字
EL 定义以下保留字:
and
、or
、not
、eq
、ne
、lt
、gt
、le
、ge
、true
(布尔字面量)、false
(布尔字面量)、null
、instanceof
(用于在对象之间进行类比较的 Java 关键字)、empty
、div
和mod
EL 立即和延迟评估
EL 将表达式评估为立即或延迟。
立即评估在页面首次渲染时立即返回结果。这类表达式是只读值表达式,并且它们只能存在于接受运行时表达式的标签中。它们在 ${}
符号之后很容易识别。通常,它们用于 JSP 页面中的算术和逻辑运算。
延迟评估可以在页面生命周期不同阶段返回结果,具体取决于使用表达式的技术。JSF 可以在生命周期的不同阶段评估表达式(例如,在渲染和回传阶段),具体取决于在页面中使用表达式的方式。这类表达式可以是值表达式和方法表达式,并且它们由 #{}
符号标记。
注意
在 Facelets 中,${}
和 #{}
的行为相同。
EL 值表达式
值表达式可能是使用最频繁的,它们引用对象及其属性和属性。这类表达式在运行时动态用于评估结果或设置 Bean 属性。通过值表达式,您可以轻松访问 JavaBeans 组件、集合和 Java SE 枚举类型。此外,EL 提供了一组隐式对象,可用于从不同作用域获取属性和参数值。此外,您将看到 EL 如何处理这些对象中的每一个。
注意
可以读取数据但不能写入的值表达式称为右值(${}
表达式始终是右值),而可以读取和写入数据的值表达式称为左值(#{}
表达式可以是右值和/或左值)。
引用管理 Bean
引用管理 Bean 并非一个非常有用的例子,但它是一个很好的起点。最常见的情况是,您的管理 Bean 将类似于以下代码(在这种情况下,Bean 的类名为 PlayersBean
):
@ManagedBean
//some scope
public class PlayersBean{
...
}
或者,在 CDI 版本中,你的管理 Bean 将如下所示:
@Named
//some scope
public class PlayersBean{
...
}
或者,使用显式名称,你的管理 Bean 将如下所示:
@ManagedBean(name = "myPlayersBean")
//some scope
public class PlayersBean{
...
}
@Named(value = "myPlayersBean")
//some scope
public class PlayersBean{
...
}
现在,对于前两个例子,EL 引用PlayersBean
管理 Bean,如下所示——名称是从完全限定类名中提取非限定类名部分,并将第一个字符转换为小写得到的:
#{playersBean}
此外,对于接下来的两个例子,EL 使用以下显式名称:
#{myPlayersBean}
注意
你应该尽可能使用 CDI Bean,因为它们比 JSF 管理 Bean 更灵活,并且因为javax.faces.bean
的注解将在未来的 JSF 版本中弃用。因此,推荐使用 CDI Bean。
当引用的管理 Bean 在任何范围内找不到时,将返回一个null
值。
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com
。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support
并注册,以便直接将文件通过电子邮件发送给你。
引用管理 Bean 的属性
如人所知,管理 Bean 通常包含私有字段,这些字段可以通过 getter 和 setter 方法作为 Bean 属性访问,以及一些公共方法,这些方法利用这些属性来执行不同的逻辑任务。
可以访问这些属性的 EL 表达式包含点或方括号符号[]
。例如,假设PlayersBean
管理 Bean 包含以下定义的两个字段:
private String playerName = "Rafael";
private String playerSurname = "Nadal";
EL 可以通过它们的 getter 方法访问这些字段;因此,你需要按照以下代码定义它们:
public String getPlayerName() {
return playerName;
}
public String getPlayerSurname() {
return playerSurname;
}
现在,一个访问playerName
属性的表示式可以使用点符号(.
)来引用它,如下面的代码行所示:
#{playersBean.playerName}
或者,这个表示式可以使用方括号符号[]
,如下面的代码行所示:
#{playersBean['playerName']}
注意
JSF 从左到右评估这个表达式。首先,它在所有可用的范围内(如请求、会话和应用)搜索playersBean
。然后,实例化 Bean 并调用getPlayerName
/getPlayerSurname
getter 方法(对于布尔属性,getter 方法将命名为is
XXX)。当你使用[]
符号时,你可以使用单引号或双引号。只需记住在如下引号的情况下正确交替使用即可。
一个错误的引号(你无法在双引号内使用双引号)是:
<h:outputText value="#{playersBean["playerName"]}"/>
一个错误的引号(你无法在单引号内使用单引号)是:
<h:outputText value='#{playersBean['playerName']}'/>
一个正确的引号(你可以在双引号内使用单引号)是:
<h:outputText value="#{playersBean['playerName']}"/>
一个正确的引号(你可以在单引号内使用双引号)是:
<h:outputText value='#{playersBean["playerName"]}'/>
引用管理 Bean 的嵌套属性
通常,管理 Bean 使用嵌套属性。这些属性可以通过 EL 在同一个表达式中多次使用.
和[]
符号来访问。
例如,PlayersBean
托管 Bean 可能代表网球运动员的一般数据,如姓名、姓氏、冠军和决赛。更详细的信息,如生日、出生地、身高和体重,可以通过名为PlayersDetails
的不同类来表示。现在,PlayersBean
托管 Bean 包含一个类型为PlayersDetails
的字段,这意味着生日、出生地等成为PlayersBean
的嵌套属性。用代码行来说,PlayersDetails
类的相关部分如下:
public class PlayerDetails {
private Date birthday;
private String birthplace;
...
public Date getBirthday() {
return birthday;
}
public String getBirthplace() {
return birthplace;
}
...
}
PlayersBean
类的托管 Bean 如下:
@Named
public class PlayersBean{
private String playerName = "Rafael";
private String playerSurname = "Nadal";
private PlayerDetails playerDetails;
public String getPlayerName() {
return playerName;
}
public String getPlayerSurname() {
return playerSurname;
}
public PlayerDetails getPlayerDetails() {
return playerDetails;
}
...
}
你已经知道如何使用.
和[]
符号表示法调用playerName
和playerSurname
属性。接下来,你可以使用相同的符号表示法来访问嵌套属性birthday
和birthplace
,如下面的代码所示:
#{playersBean.playerDetails.birthday}
#{playersBean.playerDetails.birthplace}
#{playersBean['playerDetails']['birthday']}
#{playersBean['playerDetails']['birthplace']}
或者,你可以在同一个表达式中使用这两种符号表示法,如下面的代码所示:
#{playersBean.playerDetails['birthday']}
#{playersBean.playerDetails['birthplace']}
#{playersBean['playerDetails'].birthday}
#{playersBean['playerDetails'].birthplace}
当然,PlayerDetails
类可以包含其自己的嵌套属性等。在这种情况下,只需使用.
和[]
符号表示法来深入到对象的层次结构中,直到达到所需的属性。
在前面的表达式中,JSF 在所有可用的作用域(请求、会话、应用程序等)中搜索playersBean
,并获取其实例。之后,它调用getPlayerDetails
方法,并在getPlayerDetails
方法的返回结果上调用getBirthday
方法(对于birthplace
属性也是如此)。
引用 Java SE 枚举类型
EL 可以使用字符串字面量访问 Java SE 枚举类型。例如,让我们假设在PlayersBean
中定义了一个枚举类型,如下面的代码所示:
public enum Plays {
Left, Right
};
private Plays play;
...
play = Plays.Left;//initialization can be done in constructor
...
public Plays getPlay() {
return play;
}
...
你可以轻松地输出play
值,如下面的代码行所示:
#{playersBean.play}
要引用Plays
常量Plays.Left
,使用表达式中的字符串字面量Left
(或Right
用于Plays.Right
),例如,你可以测试play
是否为Left
或Right
,如下面的代码所示:
#{playersBean.play == 'Left'} //return true
#{playersBean.play == 'Right'}//return false
引用集合
集合项(数组、列表、映射、集合等)可以通过指定一个可以转换为整数或使用整数和空格的[]
符号表示法(无需引号)的文本值从 EL 表达式中访问。
例如,假设PlayersBean
托管 Bean 包含一个名为titles_2013
的数组,该数组保存了 2013 年一名球员赢得的冠军。数组定义如下面的代码所示:
private String[] titles_2013 = {"Sao Paulo", "Acapulco", "ATP World Tour Masters 1000 Indian Wells", "Barcelona", ...};
...
public String[] getTitles_2013() {
return titles_2013;
}
现在,你可以通过指定数组中的位置来访问数组的第一个标题,该位置是0
:
#{playersBean.titles_2013[0]}
这在 Java 中相当于获取或设置titles_2013[0]
的值。
然而,有时你需要遍历数组而不是访问特定的项。这可以通过 c:forEach
JSTL 标签(www.oracle.com/technetwork/java/index-jsp-135995.html
)轻松实现。以下代码片段遍历 titles_2013
数组并输出每个项(这是一个相当不常见的用法,所以不要在生产环境中尝试):
<c:forEach begin="0"
end="${fn:length(playersBean.titles_2013)-1}"
var="i">
#{playersBean.titles_2013[i]},
</c:forEach>
你可以将其简化如下代码所示:
<c:forEach var="title" items="#{playersBean.titles_2013}">
<i>#{title}</i>,
</c:forEach>
你也可以使用如下代码中所示的 <ui:repeat>
标签:
<ui:repeat var="title" value="#{playersBean.titles_2013}">
<i>#{title}</i>,
</ui:repeat>
此标签在 Facelets 模板化 的 使用 ui:repeat 遍历 部分的第十二章(ch12.html "Chapter 12. Facelets Templating")中有详细说明。
你可以为每个 List
使用相同的方法。例如,在 List
的情况下,表达式 #{playersBean.titles_2013[0]}
在 Java 中等同于 titles_2013.get(0)
和 titles_2013.set(0,
some_value)
。
在类型为键值对的集合(例如,Map
)的情况下,EL 表达式通过键来获取项。例如,让我们在 PlayersBean
中添加一个 Map
,它存储了一些玩家的比赛事实。它可以定义如下代码所示:
private Map<String, String> matchfacts = new HashMap<>();
...
matchfacts.put("Aces", "12");
matchfacts.put("Double Faults", "2");
matchfacts.put("1st Serve", "70%");
...
public Map<String, String> getMatchfacts() {
return matchfacts;
}
现在,一个访问键为 Aces
的项的 EL 表达式可以写成以下代码行:
#{playersBean.matchfacts.Aces}
注意
注意,此方法不支持数组或列表。例如,#{playersBean.titles_2013.0}
是不正确的。
当键不是一个可接受的变量名时(例如,Double Faults
),你需要使用括号和引号,如下代码所示:
#{playersBean.matchfacts["Double Faults"]}
EL 隐式对象
JSF 提供了与当前请求和环境相关的几个对象。EL 提供了这些对象(称为 隐式对象),可以在 Facelet、servlet 或后端 bean 中在运行时访问——这些对象通过值表达式访问,并由容器管理。对于每个表达式,EL 首先检查基础值是否是这些隐式对象之一,如果不是,它将逐级检查更广泛的范围中的 beans(从请求到视图,最后到应用程序范围)。
注意
在 EL 中,点或方括号之前的部分被称为 基础,它通常表示应该位于哪个 bean 实例。第一个点或方括号之后的部分称为 属性,并且递归地分解成更小的部分,代表从基础获取的 bean 属性。
你可以在以下表格中看到这些对象的简要概述:
隐式对象 EL | 类型 | 描述 |
---|---|---|
#{application} |
ServletContext 或 PortletContext |
这是 ServletContext 或 PortletContext 的实例。 |
#{facesContext} |
FacesContext |
这是一个 FacesContext 的实例。 |
#{initParam} |
Map |
这是 getInitParameterMap 返回的上下文初始化参数映射。 |
#{session} |
HttpSession 或PortletSession |
这是一个HttpSession 或PortletSession 的实例。 |
#{view} |
UIViewRoot |
这是指定的当前UIViewRoot (UIComponent 树的根)。 |
#{component} |
UIComponent |
这是指定的当前UIComponent 。 |
#{cc} |
UIComponent |
这是指定正在处理的复合组件。 |
#{request} |
ServletRequest 或PortletRequest |
这是一个ServletRequest 或PortletRequest 的实例。 |
#{applicationScope} |
Map |
这是一个映射,用于存储由getApplicationMap 返回的应用程序范围数据。 |
#{sessionScope} |
Map |
这是一个映射,用于存储由getSessionMap 返回的会话范围数据。 |
#{viewScope} |
Map |
这是一个映射,用于存储由getViewMap 返回的当前视图范围数据。 |
#{requestScope} |
Map |
这是一个映射,用于存储由getRequestMap 返回的请求范围数据。 |
#{flowScope} |
Map |
这是一个映射,用于存储由facesContext.getApplication().getFlowHandler().getCurrentFlowScope() 返回的流程范围数据。 |
#{flash} |
Map |
这是一个只包含“下一个”请求中存在的值的映射。 |
#{param} |
Map |
这是此请求的所有查询参数的映射视图。它由getRequestParameterMap 返回。 |
#{paramValues} |
Map |
这是getRequestParameterValuesMap 返回的请求参数值映射。 |
#{header} |
Map |
这是此请求所有 HTTP 头部的映射视图,由getRequestHeaderMap 返回。 |
#{headerValue} |
Map |
这是getRequestHeaderValuesMap 返回的请求头部值映射。映射中的每个值都是一个包含该键所有值的字符串数组。 |
#{cookie} |
Map |
这是一个映射视图,显示了由getRequestCookieMap 返回的 HTTP Set-Cookie 头中的值。 |
#{resource} |
Resource |
这是一个指向具体资源 URL 的 JSF 资源标识符。 |
EL 方法表达式
使用 EL 表达式,我们可以调用托管 bean 上服务器端的任意静态和公共方法。此类表达式通常存在于标签的属性中(即,在action
或actionListener
属性内部)并且必须使用延迟评估语法,因为方法可以在生命周期的不同阶段被调用。通常,方法被调用以响应不同类型的事件和自动页面导航。
让我们看看使用 EL 调用 bean 方法的几个示例(所有方法都在PlayersBean
托管 bean 中定义):
-
调用无参数的
vamosRafa_1
void bean 方法,如下面的代码所示:public void vamosRafa_1(){ System.out.println("Vamos Rafa!"); } #{playersBean.vamosRafa_1()}
-
调用无参数的
vamosRafa_2
bean 方法。它返回一个字符串,如下面的代码所示:public String vamosRafa_2() { return "Vamos Rafa!"; } #{playersBean.vamosRafa_2()}
返回的字符串
Vamos Rafa!
可以在网页上显示或用于其他目的。换句话说,表达式将被评估为这个字符串。 -
使用一个参数调用
vamosRafa_3
Bean 方法。它返回 void,如下面的代码所示:public void vamosRafa_3(String text) { System.out.println(text); } #{playersBean.vamosRafa_3('Vamos Rafa!')}
注意,
String
参数是通过使用引号传递的。注意
String
常量用单引号或双引号传递! -
使用两个参数调用
vamosRafa_4
Bean 方法。它返回一个字符串,如下面的代码所示:public String vamosRafa_4(String name, String surname) { return "Vamos " + name + " " + surname + "!"; } #{playersBean.vamosRafa_4(playersBean.playerName, playersBean.playerSurname)}
该表达式将被评估为字符串,
Vamos Rafael Nadal!
。 -
调用
vamosRafa_5
Bean 方法进行自动导航。首先,在管理 Bean 中定义该方法以返回一个视图(结果)名称(vamos
是vamos.xhtml
文件的视图名称),如下面的代码所示:public String vamosRafa_5(){ return "vamos"; }
此外,从任何 JSF UI 组件的 action
属性中提取视图名称,如下面的代码所示:
<h:form>
<h:commandButton action="#{playersBean.vamosRafa_5()}" value="Vamos ..." />
</h:form>
现在,当点击标记为 Vamos... 的按钮时,JSF 将解析视图名称 vamos
到 vamos.xhtml
文件。此外,JSF 将在当前目录中查找 vamos.xhtml
文件,并将其导航到该文件。通常,这些导航方法用于在 JSF 页面之间进行条件导航。
注意
我们甚至在没有参数的情况下也使用了括号来调用方法。一个特殊情况是包含 ActionEvent
参数的方法。这些方法应该不带括号调用,除非你完全通过传递和指定自定义参数来覆盖 ActionEvent
参数。
EL 表达式也可以用在 JavaScript 函数调用中。例如,当你想将 Bean 属性传递给 JavaScript 函数时,需要将它们放在引号之间,如下面的代码所示:
<h:form>
<h:commandButton type="button" value="Click Me!" onclick="infoJS('#{playersBean.playerName}', '#{playersBean.playerSurname}')"/>
</h:form>
这个 JavaScript 函数的代码如下所示:
<script type="text/javascript">
function infoJS(name, surname) {
alert("Name: " + name + " Surname: " + surname);
}
</script>
JSF 中的条件文本
当你需要输出条件性文本(不包含 HTML 内容)时,可以使用 EL 三元运算符,其语法如下:
boolean_test ? result_for_true : result_for_false
例如,你可以使用这个运算符在两个 CSS 类之间进行选择,如下面的代码所示:
.red { color:#cc0000; }
.blue { color: #0000cc; }
现在,你想要条件性地输出红色或蓝色文本,如下面的代码所示:
<h:outputText styleClass="#{playersBean.play == 'Left' ? 'red': 'blue'}" value="#{playersBean.play}"/>
因此,如果 play
的值为 Left
,则将使用 red
CSS 类显示文本,如果不是 Left
,则使用 blue
类。
注意
请记住,不建议使用 HTML 内容(出于安全原因,不要使用 escape="false"
),并且不能省略条件的 else
部分。
为了更好地理解,让我们看另一个例子。记住你已经遍历了 titles_2013
数组,并按如下代码所示输出每个项目:
<c:forEach var="title" items="#{playersBean.titles_2013}">
<i>#{title}</i>,
</c:forEach>
好吧,这段代码的输出将类似于以下截图:
除了最后一个逗号外,一切看起来都正常,因为这个逗号不应该出现,因为 US Open 是要显示的最后一个项目。你可以通过 EL 三元运算符轻松解决这个问题,如下面的代码所示:
<c:forEach var="title" items="#{playersBean.titles_2013}" varStatus="v">
<i>#{title}</i>
#{v.last ? '':','}
</c:forEach>
有时你只需要根据条件显示或隐藏文本。为此,你可以将布尔表达式作为rendered
属性的值(所有 JSF UI 组件都有这个属性)。例如,以下代码行将仅在存在 Facebook 地址时输出玩家的 Facebook 地址:
<h:outputText value="Facebook address: #{playersBean.facebook}" rendered="#{!empty playersBean.facebook}" />
另一个常见的情况是使用两个类型为"显示某物..."和"隐藏某物..."的按钮来显示或隐藏非 HTML 文本。例如,你可以有一个标签为显示职业生涯奖金的按钮和一个标签为隐藏职业生涯奖金的按钮。显然,当你点击第一个按钮时,你希望显示职业生涯奖金;当你点击第二个按钮时,你希望隐藏职业生涯奖金。为此,你可以使用rendered
属性,如下面的代码所示:
<h:form id="prizeFormId">
<h:commandButton value="Show Career Prize Money">
<f:ajax render="rnprizeid" listener="#{playersBean.showPrizeMoney()}"/>
</h:commandButton>
<h:panelGrid id="rnprizeid">
<h:outputText value="#{playersBean.prize}" rendered="#{playersBean.show_prize}">
<f:convertNumber type="currency" currencySymbol="$" />
</h:outputText>
</h:panelGrid>
<h:commandButton value="Hide Career Prize Money">
<f:ajax render="rnprizeid"
listener="#{playersBean.hidePrizeMoney()}"/>
</h:commandButton>
</h:form>
两个按钮都使用 AJAX 机制和 EL 方法表达式来调用showPrizeMoney
和hidePrizeMoney
方法。这些方法只是修改一个名为show_prize
的boolean
属性值,如下面的代码所示:
private boolean show_prize = false;
...
public boolean isShow_prize() {
return show_prize;
}
...
public void showPrizeMoney(){
this.show_prize = true;
}
public void hidePrizeMoney(){
this.show_prize = false;
}
请求完成后,JSF 将重新渲染 ID 为rnprizeid
的面板网格组件;这已在f:ajax
标签的render
属性中指示。如你所见,重新渲染的组件是一个包含简单h:outputText
标签的面板,该标签根据rendered
属性中 EL 表达式的布尔值输出prize
属性,如下面的代码所示:
private int prize = 60941937;
...
public int getPrize() {
return prize;
}
显示和隐藏文本可能很有用,但不足以满足需求。通常,我们需要显示或隐藏 HTML 内容。例如,你可能需要显示或隐藏一张图片:
<img src="img/babolat.jpg" width="290" height="174"/>
这个任务可以通过将 HTML 代码嵌套在支持rendered
属性的 Facelets ui:fragment
标签内轻松完成,如下面的代码所示:
<ui:fragment rendered="#{playersBean.show_racquet}">
<img src="img/#{resource['images:babolat.jpg']}" width="290" height="174"/>
</ui:fragment>
如你所见,rendered
属性的 EL 表达式表示PlayersBean
管理 Bean 的boolean
属性,如下面的代码所示:
private boolean show_racquet = false;
...
public boolean isShow_racquet() {
return show_racquet;
}
现在,你可以让用户决定何时显示或隐藏图片。你可以轻松地修改前面的示例,添加两个标签为显示图片和隐藏图片的按钮,或者更优雅的做法是使用复选框,如下面的代码所示:
...
<h:form>
<h:selectBooleanCheckbox label="Show Image"valueChangeListener="#{playersBean.showHideRacquetPicture}">
<f:ajax render="racquetId"/>
</h:selectBooleanCheckbox>
<h:panelGroup id="racquetId">
<ui:fragment rendered="#{playersBean.show_racquet}">
<img src="img/babolat.jpg" width="290" height="174"/>
</ui:fragment>
</h:panelGroup>
</h:form>
...
showHideRacquetPicture
方法根据复选框的状态将show_racquet
属性设置为true
或false
。在此方法执行后,JSF 将重新渲染ui:fragment
标签的内容——这是通过<h:panelGroup>
标签渲染的 HTML 内容来实现的,因为<ui:fragment>
标签不渲染 HTML 内容;因此,它不能通过 ID 引用。以下为showHideRacquetPicture
方法的代码:
public void showHideRacquetPicture(ValueChangeEvent e){
if(e.getNewValue() == Boolean.TRUE){
this.show_racquet=true;
} else {
this.show_racquet=false;
}
}
因此,我们可以得出结论,rendered
属性可以用来有条件地输出 HTML/非 HTML 内容。用户交互和内部条件可以用来操作这个属性值。
完整的应用程序命名为ch1_1
。
编写自定义 EL 解析器
通过扩展自定义隐式变量、属性和方法调用,可以测试 EL 的灵活性。这可以通过扩展 VariableResolver
或 PropertyResolver
类,或者更好的是,扩展 ELResolver
类来实现,这给我们提供了在不同任务中重用相同实现的能力。以下是将自定义隐式变量添加的三个简单步骤:
-
创建一个继承自
ELResolver
类的自己的类。 -
实现继承的抽象方法。
-
在
faces-config.xml
中添加ELResolver
类。
接下来,你将看到如何根据这些步骤通过扩展 EL 添加自定义隐式变量。在这个例子中,你想要通过 EL 直接在你的 JSF 页面中检索包含 ATP 单打排名的集合。用于访问集合的变量名将是 atp
。
首先,你需要创建一个继承自 javax.el.ELResolver
类的类。这非常简单。ATPVarResolver
类的代码如下:
public class ATPVarResolver extends ELResolver {
private static final Logger logger = Logger.getLogger(ATPVarResolver.class.getName());
private static final String PLAYERS = "atp";
private final Class<?> CONTENT = List.class;
...
}
其次,你需要实现六个抽象方法:
-
getValue
: 此方法定义如下:public abstract Object getValue(ELContext context, Object base, Object property)
这是
ELResolver
类最重要的方法。在getValue
方法的实现中,如果你请求的属性名为atp
,你将返回 ATP 项目。因此,实现将如下:@Override public Object getValue(ELContext ctx, Object base, Object property) { logger.log(Level.INFO, "Get Value property : {0}", property); if ((base == null) && property.equals(PLAYERS)) { logger.log(Level.INFO, "Found request {0}", base); ctx.setPropertyResolved(true); List<String> values = ATPSinglesRankings.getSinglesRankings(); return values; } return null; }
-
getType
: 此方法定义如下:public abstract Class<?> getType(ELContext context, Object base,Object property)
此方法确定我们属性的最一般可接受类型。此方法的作用域是确定调用
setValue
方法是否安全,不会抛出ClassCastException
。由于我们返回一个集合,我们可以说一般可接受类型是List
。getType
方法的实现如下:@Override public Class<?> getType(ELContext ctx, Object base, Object property) { if (base != null) { return null; } if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } if ((base == null) && property.equals(PLAYERS)) { ctx.setPropertyResolved(true); return CONTENT; } return null; }
-
setValue
: 此方法定义如下:public abstract void setValue(ELContext context, Object base, Object property, Object value)
此方法尝试为给定的属性和基础设置值。对于只读变量,如
atp
,你需要抛出PropertyNotWritableException
类型的异常。setValue
方法的实现如下:@Override public void setValue(ELContext ctx, Object base, Object property, Object value) { if (base != null) { return; } ctx.setPropertyResolved(false); if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } if (PLAYERS.equals(property)) { throw new PropertyNotWritableException((String) property); } }
-
isReadOnly
: 此方法定义如下:public abstract boolean isReadOnly(ELContext context, Object base, Object property)
此方法如果变量是只读的则返回
true
,否则返回false
。由于atp
变量是只读的,所以实现很明显。此方法与setValue
方法直接相关,意味着它表示是否安全调用setValue
方法,而不会得到PropertyNotWritableException
作为响应。isReadOnly
方法的实现如下:@Override public boolean isReadOnly(ELContext ctx, Object base, Object property) { return true; }
-
getFeatureDescriptors
: 此方法定义如下:public abstract Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base
此方法返回一组关于可以解析的变量或属性的信息(通常它被设计时工具(例如,JDeveloper 有这样的工具)用于允许表达式代码补全)。在这种情况下,你可以返回
null
。getFeatureDescriptors
方法的实现如下:@Override public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext ctx, Object base) { return null; }
-
getCommonPropertyType
: 此方法定义如下:public abstract Class<?> getCommonPropertyType(ELContext context, Object base)
此方法返回此解析器接受的类型中最一般化的类型。
getCommonPropertyType
方法的实现如下:@Override public Class<?> getCommonPropertyType(ELContext ctx, Object base) { if (base != null) { return null; } return String.class; }
注意
你如何知道ELResolver
类是作为VariableResolver
类(这两个类在 JSF 2.2 中已弃用)还是作为PropertyResolver
类来操作?答案在于表达式的第一部分(称为基本参数),在我们的例子中是null
(基本参数位于第一个点或方括号之前,而属性位于此点或方括号之后)。当基本参数为null
时,ELresolver
类作为VariableResolver
类来操作;否则,它作为PropertyResolver
类来操作。
getSinglesRankings
方法(用于填充集合)是从getValue
方法中调用的,并在以下ATPSinglesRankings
类中定义:
public class ATPSinglesRankings {
public static List<String> getSinglesRankings(){
List<String> atp_ranking= new ArrayList<>();
atp_ranking.add("1 Nadal, Rafael (ESP)");
...
return atp_ranking;
}
}
第三,你需要在faces-config.xml
中使用<el-resolver>
标签注册自定义的ELResolver
类,并指定相应类的完全限定名。换句话说,你将ELResolver
类添加到责任链中,这代表了 JSF 处理ELResolvers
所使用的模式:
<application>
<el-resolver>book.beans.ATPVarResolver</el-resolver>
</application>
注意
每当需要解析一个表达式时,JSF 都会调用默认的表达式语言解析器实现。每个值表达式都由getValue
方法在幕后进行评估。当存在<el-resolver>
标签时,自定义解析器被添加到责任链中。EL 实现管理不同类型表达式元素的责任链中的解析器实例。对于表达式的每一部分,EL 都会遍历链,直到找到能够解析该部分的解析器。能够处理该部分的解析器会将true
传递给setPropertyResolved
方法;此方法在ELContext
级别上充当一个标志。
此外,EL 实现通过getPropertyResolved
方法在每次解析器调用后检查此标志的值。当标志为true
时,EL 实现将重复对表达式下一部分的过程。
完成!接下来,你只需简单地以数据表的形式输出集合项,如下面的代码所示:
<h:dataTable id="atpTableId" value="#{atp}" var="t">
<h:column>
#{t}
</h:column>
</h:dataTable>
好吧,到目前为止一切顺利!现在,我们的自定义 EL 解析器返回了 ATP 排名的普通列表。但是,如果我们需要以相反的顺序排列列表项,或者需要将项转换为大写,或者需要获取一个随机列表,我们该怎么办?答案可能在于将前面的 EL 解析器适应这种情况。
首先,你需要修改getValue
方法。此时,它返回List
,但你需要获取ATPSinglesRankings
类的实例。因此,按照以下代码进行修改:
public Object getValue(ELContext ctx, Object base, Object property) {
if ((base == null) && property.equals(PLAYERS)) {
ctx.setPropertyResolved(true);
return new ATPSinglesRankings();
}
return null;
}
此外,你还需要根据以下代码行重新定义CONTENT
常量:
private final Class<?> CONTENT = ATPSinglesRankings.class;
接下来,ATPSinglesRankings
类可以包含每个情况的方法,如下面的代码所示:
public class ATPSinglesRankings {
public List<String> getSinglesRankings(){
List<String> atp_ranking= new ArrayList<>();
atp_ranking.add("1 Nadal, Rafael (ESP)");
...
return atp_ranking;
}
public List<String> getSinglesRankingsReversed(){
List<String> atp_ranking= new ArrayList<>();
atp_ranking.add("5 Del Potro, Juan Martin (ARG)");
atp_ranking.add("4 Murray, Andy (GBR)");
...
return atp_ranking;
}
public List<String> getSinglesRankingsUpperCase(){
List<String> atp_ranking= new ArrayList<>();
atp_ranking.add("5 Del Potro, Juan Martin (ARG)".toUpperCase());
atp_ranking.add("4 Murray, Andy (GBR)".toUpperCase());
...
return atp_ranking;
}
...
}
由于 EL 解析器在 getValue
方法中返回 ATPSinglesRankings
类的实例,你可以轻松地从你的 EL 表达式中直接调用 getSinglesRankings
、getSinglesRankingsReversed
和 getSinglesRankingsUpperCase
方法,如下所示:
<b>Ordered:</b><br/>
<h:dataTable id="atpTableId1" value="#{atp.singlesRankings}"var="t">
<h:column>#{t}</h:column>
</h:dataTable>
<br/><br/><b>Reversed:</b><br/>
<h:dataTable id="atpTableId2" value="#{atp.singlesRankingsReversed}" var="t">
<h:column>#{t}</h:column>
</h:dataTable>
<br/><br/><b>UpperCase:</b><br/>
<h:dataTable id="atpTableId3" value="#{atp.singlesRankingsUpperCase}" var="t">
<h:column>#{t}</h:column>
</h:dataTable>
用于演示自定义 ELResolvers
的完整应用程序包含在本章的代码包中,并命名为 ch1_2
和 ch1_3
。
为了开发编写自定义解析器的最后一个示例,让我们设想以下场景:我们想要将 ELContext
对象作为隐式对象访问,通过编写 #{elContext}
而不是 #{facesContext.ELContext}
。为此,我们可以利用前两个示例积累的知识来编写以下自定义解析器:
public class ELContextResolver extends ELResolver {
private static final String EL_CONTEXT_NAME = "elContext";
@Override
public Class<?> getCommonPropertyType(ELContext ctx,Object base){
if (base != null) {
return null;
}
return String.class;
}
@Override
public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext ctx, Object base) {
if (base != null) {
return null;
}
ArrayList<FeatureDescriptor> list = new ArrayList<>(1);
list.add(Util.getFeatureDescriptor("elContext", "elContext","elContext", false, false, true,
ELContext.class, Boolean.TRUE));
return list.iterator();
}
@Override
public Class<?> getType(ELContext ctx, Object base, Object property) {
if (base != null) {
return null;
}
if (property == null) {
String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property");
throw new PropertyNotFoundException(message);
}
if ((base == null) && property.equals(EL_CONTEXT_NAME)) {
ctx.setPropertyResolved(true);
}
return null;
}
@Override
public Object getValue(ELContext ctx, Object base, Object property) {
if ((base == null) && property.equals(EL_CONTEXT_NAME)) {
ctx.setPropertyResolved(true);
FacesContext facesContext = FacesContext.getCurrentInstance();
return facesContext.getELContext();
}
return null;
}
@Override
public boolean isReadOnly(ELContext ctx, Object base, Object property) {
if (base != null) {
return false;
}
if (property == null) {
String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property");
throw new PropertyNotFoundException(message);
}
if (EL_CONTEXT_NAME.equals(property)) {
ctx.setPropertyResolved(true);
return true;
}
return false;
}
@Override
public void setValue(ELContext ctx, Object base, Object property, Object value) {
if (base != null) {
return;
}
ctx.setPropertyResolved(false);
if (property == null) {
String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property");
throw new PropertyNotFoundException(message);
}
if (EL_CONTEXT_NAME.equals(property)) {
throw new PropertyNotWritableException((String) property);
}
}
}
完整的应用程序命名为 ch1_6
。这三个示例的目的是让你熟悉编写自定义解析器的关键步骤。在 第三章,JSF 作用域 – 在管理 Bean 通信中的生命周期和使用,你将看到如何编写自定义作用域的解析器。
EL 3.0 概述
EL 3.0(JSR 341,Java EE 7 的一部分)是 EL 2.2 的一次重大提升。EL 3.0 的主要特性如下:
-
新运算符
+
、=
和;
-
Lambda 表达式
-
集合对象支持
-
独立环境的 API
在接下来的章节中,你将看到如何在 JSF 页面中使用 EL 3.0 特性。
使用赋值运算符
在类型为的表达式 x = y
中,赋值运算符(=
)将 y
的值赋给 x
。为了避免 PropertyNotWritableException
类型的错误,x
的值必须是一个 lvalue。以下示例展示了如何在两个简单表达式中使用此运算符:
-
#{x = 3}
的结果为 3 -
#{y = x + 5}
的结果为 8
赋值运算符是右结合的(z = y = x
等价于 z = (y = x)
)。例如,#{z = y = x + 4}
的结果为 7。
使用字符串连接运算符
在类型为的表达式 x += y
中,字符串连接操作符(+=
)返回 x
和 y
的连接字符串。例如:
-
#{x += y}
的结果为 37 -
#{0 += 0 +=0 += 1 += 1 += 0 += 0 += 0}
的结果为 00011000
在 EL 2.2 中,你可以使用以下代码来完成此操作:
#{'0'.concat(0).concat(0).concat(1).concat(1).concat(0).concat(0).concat(0)}
使用分号运算符
在类型为的表达式 x; y
中,首先评估 x
,然后丢弃其值。接下来,评估 y
并返回其值。例如,#{x = 5; y = 3; z = x + y}
的结果为 8。
探索 Lambda 表达式
Lambda 表达式可以分解为三个主要部分:参数、Lambda 操作符(->
)和函数体。
在 Java 语言中,Lambda 表达式代表一个在匿名实现功能接口中的方法。在 EL 中,Lambda 表达式被简化为一个可以作为方法参数传递的匿名函数。
重要的是不要混淆 Java 8 lambda 表达式和 EL lambda 表达式,但为了理解下一个示例,了解 Java 8 lambda 表达式的基础知识docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
是重要的。它们的语法不同,但足够相似,在我们需要在它们之间切换时不会引起明显的困扰。
EL lambda 表达式是一个参数化的ValueExpression
对象。EL lambda 表达式的主体是一个 EL 表达式。EL 支持几种类型的 lambda 表达式。EL lambda 表达式的最简单类型是立即调用的,例如:
-
#{(x->x+1)(3)}
计算结果为 4 -
#{((x,y,z)->x-y*z)(1,7,3)}
计算结果为-20
此外,我们还有已分配的 lambda 表达式。这些是通过间接调用的。例如,#{q = x->x+1; q(3)}
计算结果为 4。
间接地,调用可以用来编写函数。例如,我们可以编写一个函数来计算n mod m
(不使用%
运算符)。以下示例计算结果为 3:
#{modulus = (n,m) -> m eq 0 ? 0 : (n lt m ? n: (modulus(n-m, m))); modulus(13,5)}
我们可以从其他表达式中调用这个函数。例如,如果我们想计算两个数的最大公约数,我们可以利用前面的函数;以下示例计算结果为 5:
#{gcd = (n,m) -> modulus(n,m) == 0 ? m: (gcd(m, modulus(n,m))); gcd(10, 15)}
Lambda 表达式可以作为方法的参数传递。例如,在以下示例中,你调用一个名为firstLambdaAction
的方法——lambda 表达式从这个方法中调用:
#{lambdaBean.firstLambdaAction(modulus = (n,m) -> m eq 0 ? 0 : (n lt m ? n: (modulus(n-m, m))))}
现在,firstLambdaAction
方法如下:
public Object firstLambdaAction(LambdaExpression lambdaExpression) {
//useful in case of a custom ELContext
FacesContext facesContext = FacesContext.getCurrentInstance();
ELContext elContext = facesContext.getELContext();
return lambdaExpression.invoke(elContext, 8, 3);
//or simply, default ELContext:
//return lambdaExpression.invoke(8, 3);
}
Lambda 表达式的一个强大特性是嵌套的 lambda 表达式。例如(首先,计算内部表达式为 7,然后计算外部表达式为 as,10 - 7):#{(x->x-((x,y)->(x+y))(4,3))(10)}
计算结果为 3。
你认为 EL lambda 表达式很酷吗?好吧,准备好迎接更多。真正的力量只有在我们将集合对象带入方程时才会释放出来。
处理集合对象
EL 3.0 通过在管道中应用操作为操作集合对象提供了强大的支持。支持集合操作的方法实现为ELResolvers
,lambda 表达式作为这些方法的参数。
操作集合对象的主要思想是基于流。更准确地说,具体操作是通过调用从集合中获取的元素流的方法来完成的。许多操作返回流,这些流可以用在其他返回流的操作中,依此类推。在这种情况下,我们可以说我们有一个流链或管道。管道的入口称为源,管道的出口称为终端操作(此操作不返回流)。在源和终端操作之间,我们可能有零个或多个中间操作(所有这些操作都返回流)。
管道执行开始于终端操作启动时。由于中间操作是惰性评估的,它们不会保留操作的中间结果(一个例外是排序操作,它需要所有元素来排序任务)。
现在,让我们看看一些示例。我们首先声明一个集合、一个列表和一个映射——EL 包含动态构建集合、列表和映射的语法,如下所示:
#{nr_set = {1,2,3,4,5,6,7,8,9,10}}
#{nr_list = [1,2,3,4,5,6,7,8,9,10]}
#{nr_map = {"one":1,"two":2,"three":3,"four":4,"five":5,"six":6,"seven":7,"eight":8,"nine":9,"ten":10}}
现在,让我们更进一步,按升序/降序对列表进行排序。为此,我们使用 stream
、sorted
(这类似于 SQL 的 ORDER BY
语句)和 toList
方法(后者返回一个包含源流元素的 List
),如下面的代码所示:
#{nr_list.stream().sorted((i,j)->i-j).toList()}
#{ nr_list.stream().sorted((i,j)->j-i).toList()}
此外,假设我们有一个名为 LambdaBean
的管理 Bean 中的以下列表:
List<Integer> costBeforeVAT = Arrays.asList(34, 2200, 1350, 430, 57, 10000, 23, 15222, 1);
接下来,我们可以应用 24% 的增值税,并使用 filter
(这类似于 SQL 的 WHERE
和 GROUP BY
语句)、map
(这类似于 SQL 的 SELECT
语句)和 reduce
(这类似于聚合函数)方法来计算超过 1,000 的总成本。这些方法的使用如下:
#{(lambdaBean.costBeforeVAT.stream().filter((cost)-> cost gt 1000).map((cost) -> cost + .24*cost)).reduce((sum, cost) -> sum + cost).get()}
这些只是 EL 3.0 中使用集合对象的几个示例。本章代码包中提供了一个名为 ch1_4
的完整应用程序,可供下载。由于在这个应用程序中你可以看到超过 70 个示例,我建议你查看一下。此外,一个很好的示例可以在 Michael Müller 的博客上找到,网址为 blog.mueller-bruehl.de/web-development/using-lambda-expressions-with-jsf-2-2/
。
但是,如果我们想利用 lambda 表达式,但又不喜欢编写这样的表达式呢?嗯,一个解决方案可以是基于 lambda 表达式编写参数化函数,并以 JSTL 风格调用它们。例如,以下函数能够从一个 List
中提取子列表:
#{get_sublist = (list, left, right)->list.stream().substream(left, right).toList()}
现在,我们可以像以下代码所示进行调用:
<ui:repeat value="#{get_sublist(myList, from, to)}" var="t">
#{t}
</ui:repeat>
在名为 ch1_5
的完整应用程序中,你可以看到一些可以与 List
s 一起使用的参数化函数。
摘要
在本章中,我们了解到 EL 2.2 表达式可以用来动态访问存储在 JavaBeans 组件中的数据(读取和写入),调用任意的静态和公共方法,以及执行算术和逻辑操作。最后,我们了解到 EL 允许我们通过自定义解析器扩展其功能。从 EL 3.0 开始,我们可以利用新的运算符、lambda 表达式以及与集合对象一起工作的支持。
在阅读这本书的过程中,你将看到许多 EL 表达式在实际案例中的应用。例如,在下一章中,你将使用 EL 表达式来探索 JSF 通信功能。
欢迎在下一章中相见,我们将讨论 JSF 通信。
第二章. JSF 中的通信
通信是 JSF 应用程序的核心,也是决定此类应用程序架构的主要方面之一。从大局出发,你需要从一开始就确定主要部分以及它们将如何相互以及与最终用户进行通信。在选择了设计模式、绘制 UML 图、草拟架构和应用流程之后,是时候开始工作并开始使用表单、参数、参数、值、页面、Bean 等实现通信管道了。
幸运的是,JSF 提供了许多解决方案,以确保 JSF 组件之间以及 JSF 和 XHTML 页面、JavaScript 代码和其他第三方组件之间有一个强大且灵活的通信层。在本章中,我们将涵盖以下主题:
-
使用上下文参数
-
使用
<f:param>
标签传递请求参数 -
与视图参数一起工作
-
在 GET 请求中调用动作
-
使用
<f:attribute>
标签传递属性 -
通过动作监听器设置属性值
-
使用 Flash 范围传递参数
-
用 JSTL
<c:set>
标签替换<f:param>
标签 -
通过 Cookie 发送数据
-
与隐藏字段一起工作
-
发送密码
-
以编程方式访问 UI 组件属性
-
通过方法表达式传递参数
-
通过
binding
属性进行通信
传递和获取参数
正如你将在下一节中看到的,JSF 提供了多种方法来传递/获取参数到/从 Facelets、管理 Bean、UI 组件等。
使用上下文参数
上下文参数使用 <context-param>
标签在 web.xml
文件中定义。此标签允许两个重要的子标签:<param-name>
,表示参数名称,以及 <param-value>
,表示参数值。例如,一个用户定义的上下文参数如下所示:
<context-param>
<param-name>number.one.in.ATP</param-name>
<param-value>Rafael Nadal</param-value>
</context-param>
现在,在 JSF 页面中,你可以像以下代码所示访问此参数:
<h:outputText value="#{initParam['number.one.in.ATP']}"/>
<h:outputText value="#{facesContext.externalContext.initParameterMap['number.one.in.ATP']}"/>
在一个管理 Bean 中,可以通过 getInitParameter
方法访问相同的上下文参数:
facesContext.getExternalContext().getInitParameter("number.one.in.ATP");
完整的应用程序命名为 ch2_27
。
使用 <f:param>
标签传递请求参数
有时,你需要从 Facelet 将参数传递到管理 Bean 或另一个 Facelet。在这种情况下,你可能需要 <f:param>
标签,它可以用于向请求添加查询字符串名称-值对,或者简单地说,发送请求参数。通常,<f:param>
标签用于 <h:commandButton>
和 <h:commandLink>
标签内部,用于向管理 Bean 发送请求参数。例如,以下代码片段在表单提交时向请求添加两个参数。这些参数在 PlayersBean
Bean 中访问;第一个参数名为 playerNameParam
,第二个参数名为 playerSurnameParam
。
<h:form>
Click to send name, 'Rafael' surname, 'Nadal', with f:param:
<h:commandButton value="Send Rafael Nadal" action="#{playersBean.parametersAction()}">
<f:param id="playerName" name="playerNameParam" value="Rafael"/>
<f:param id="playerSurname" name="playerSurnameParam" value="Nadal"/>
</h:commandButton>
</h:form>
如你所见,当按钮被点击时,请求参数被发送并且调用parametersAction
方法(通过action
或actionListener
)。当应用程序流程到达此方法时,两个请求参数已经可用于使用。你可以通过当前FacesContext
实例访问请求参数映射,如以下代码所示来轻松提取它们:
private String playerName;
private String playerSurname;
...
//getter and setter
...
public String parametersAction() {
FacesContext fc = FacesContext.getCurrentInstance();
Map<String, String> params = fc.getExternalContext().getRequestParameterMap();
playerName = params.get("playerNameParam");
playerSurname = params.get("playerSurnameParam");
return "some_page";
}
这两个参数的值都存储在playerName
和playerSurname
托管 Bean 的属性中(这些可以进一步修改而不会影响原始参数),但你可以通过在some_page中使用param
EL 保留字轻松显示参数值(记住第一章动态访问 JSF 应用程序数据")中的EL 隐含对象部分,通过表达式语言(EL 3.0)动态访问 JSF 应用程序数据,该部分解释了param
是一个预定义变量,它引用请求参数映射):
Name: #{param.playerNameParam}
Surname: #{param.playerSurnameParam}
<f:param>
标签也可以在<h:outputFormat>
标签内部使用,以替换消息参数;<f:param>
用于将参数传递给 UI 组件,如下所示:
<h:outputFormat value="Name: {0} Surname: {1}">
<f:param value="#{playersBean.playerName}" />
<f:param value="#{playersBean.playerSurname}" />
</h:outputFormat>
上述代码的输出如下:
姓名:拉斐尔 姓氏:纳达尔
注意
如果你想在设置托管 Bean 属性之后但在调用动作方法之前(如果存在的话)执行一些初始化任务(或其他操作),那么你可以定义一个带有@PostConstruct
注解的 public void 方法。在此示例中,init
方法将在parametersAction
方法之前被调用,并且传递的请求参数可以通过请求映射获得。
init
方法如下所示:
@PostConstruct
public void init(){
//do something with playerNameParam and playerSurnameParam
}
此示例被封装在名为ch2_1
的应用程序中。
如果你认为在托管 Bean 中访问请求映射不是一个非常方便的方法,那么你可以使用@ManagedProperty
,它将参数设置为托管 Bean 属性并将其值链接到请求参数:
@ManagedProperty(value = "#{param.playerNameParam}")
private String playerName;
@ManagedProperty(value = "#{param.playerSurnameParam}")
private String playerSurname;
这些值在 Bean 构造后立即设置,并在@PostConstruct
期间可用,但请注意,@ManagedProperty
仅适用于由 JSF 管理的 Bean(@ManagedBean
),而不是由 CDI 管理的 Bean(@Named
)。
此示例被封装在名为ch2_2
的应用程序中,该应用程序位于本章的代码包中。你可能还对名为ch2_3
的应用程序感兴趣,它是使用<f:param>
、@ManagedProperty
和@PostConstruct
的另一个示例。在此示例中,<h:commandButton>
动作指示另一个 JSF 页面而不是托管 Bean 方法。
<f:param>
标签可以用来在 Facelets 之间直接传递请求参数,而不涉及托管 Bean。通常,这发生在<h:link>
标签中,如下所示:
<h:link value="Send Rafael Nadal" outcome="result">
<f:param id="playerName" name="playerNameParam" value="Rafael"/>
<f:param id="playerSurname" name="playerSurnameParam" value="Nadal"/>
</h:link>
当点击发送拉斐尔·纳达尔链接时,JSF 将使用包含 result.xhtml
文件资源名称和请求参数 playerNameParam
和 playerSurnameParam
的准备好的 URL。这两个参数在 result.xhtml
文件中如下显示:
Name: #{param.playerNameParam}
Surname: #{param.playerSurnameParam}
如果你检查浏览器地址栏中由 <h:link>
标签生成的 URL,你将看到如下类似的 URL:
http://
主机名/ch2_4/faces/result.xhtml?playerNameParam=Rafael&playerSurnameParam=Nadal
此示例被包裹在名为 ch2_4
的应用程序中。在那个应用程序中,你还可以看到一个使用 <h:commandButton>
标签的示例。请注意,在这种情况下,我们需要将 <h:commandButton>
标签包裹在 <h:form>
标签中,该表单通过 POST 请求提交;因此,请求参数不再可见于 URL 中。
注意
<f:param>
标签不能通过声明性/命令性验证和/或转换来加强。你需要自己完成这个任务。
不要尝试将 <f:param>
标签放置在 <h:inputText>
标签或任何其他输入组件内部。那样根本不会起作用。
与视图参数一起工作
从 JSF 2.0 开始,我们可以使用一类新的参数,称为视图参数。这类参数由 UIViewParameter
类(该类扩展了 UIInput
类)实现,并在 Facelets 中使用 <f:viewParam>
标签定义。通过这个标签,我们可以声明性地将 UIViewParameter
类注册为父视图的元数据;这就是为什么 <f:viewParam>
标签嵌套在 <f:metadata>
标签中的原因。
从 JSF 2.0 开始,元数据概念在视图的一部分中实现,这提供了以下两个主要优势(该部分由 <f:metadata>
标签界定):
-
此部分的内容在没有整个视图的情况下也是可读的
-
在初始请求时,该部分的组件在视图渲染之前可以完成不同的事情
注意
从 JSF 2.2 开始,元数据部分(以及随后的组件)通过一个名为 hasMetadata
的公共静态方法(UIViewRoot
)检测。此方法添加在 javax.faces.view.ViewMetadata
中,如果存在元数据部分则返回 true
,否则返回 false
。除了其他好处外,使用 <f:viewParam>
标签的主要优势是支持 URL 书签。
为了更好地理解,让我们看看使用 <f:viewParam>
标签的一个简单示例。以下代码片段来自同一页面,index.xhtml
:
<f:metadata>
<f:viewParam name="playernameparam" value="#{playersBean.playerName}"/>
<f:viewParam name="playersurnameparam" value="#{playersBean.playerSurname}"/>
</f:metadata>
...
<h:body>
You requested name: <h:outputText value="#{playersBean.playerName}"/><br/>
You requested surname: <h:outputText value="#{playersBean.playerSurname}"/>
</h:body>
现在,让我们看看初始请求时发生了什么。首先,让我们关注第一段代码:在这里,JSF 通过名称(playernameparam
和 playersurnameparam
)从页面 URL 中获取请求参数的值,并应用指定的转换器/验证器(这些是可选的)。在转换/验证成功后,在视图渲染之前,JSF 通过调用 setPlayerName
和 setPlayerSurname
方法(仅在 URL 中提供请求参数时调用)将 playernameparam
和 playersurnameparam
请求参数的值绑定到管理 Bean 属性 playerName
和 playerSurname
。如果缺少 value
属性,则 JSF 将请求参数作为请求属性设置在名称 playernameparam
和 playersurnameparam
上,可通过 #{playernameparam}
和 #{playersurnameparam}
访问。
页面的初始 URL 应该类似于以下内容:
http://
hostname/ch2_5/?playernameparam=Rafael&playersurnameparam=Nadal
在第二段代码中,显示管理 Bean 属性 playerName
和 playerSurname
的值(调用 getPlayerName
和 getPlayerSurname
方法);它们应该反映请求参数的值。
注意
由于 UIViewParameter
类扩展了 UIInput
类,管理 Bean 属性仅在 更新模型 阶段设置。
此示例包含在名为 ch2_5
的应用程序中。
视图参数可以通过在 <h:link>
标签中使用 includeViewParams="true"
属性或在任何 URL 中使用 includeViewParams=true
请求参数包含在链接(GET 查询字符串)中。这两种情况将在下面的示例中看到。
在 index.xhtml
文件中,你可以有如下代码,其中通过请求参数包含视图参数:
<f:metadata>
<f:viewParam name="playernameparam" value="#{playersBean.playerName}"/>
<f:viewParam name="playersurnameparam" value="#{playersBean.playerSurname}"/>
</f:metadata>
...
<h:body>
<h:form>
Enter name:<h:inputText value="#{playersBean.playerName}"/>
Enter name:<h:inputText value="#{playersBean.playerSurname}"/>
<h:commandButton value="Submit" action="results?faces-redirect=true&includeViewParams=true"/>
</h:form>
</h:body>
初始 URL 可以是:
http://
hostname/ch2_6/?playernameparam=Rafael&playersurnameparam=Nadal
视图参数,playernameparam
和 playersurnameparam
,将从此 URL 中提取并绑定到管理 Bean 属性,playerName
和 playerSurname
。可选地,这两个属性可以通过两个 <h:inputText>
标签或其它 UI 组件由用户进一步修改。如果初始 URL 不包含视图参数,则由 <h:inputText>
生成的字段将为空。通过 <h:commandButton>
标签渲染的按钮将重定向到 results.xhtml
页面,并将视图参数包含在新 URL 中。视图参数的值将反映相应管理 Bean 属性的值,因为表单是在以下 URL 组合之前提交的:
http://
hostname/ch2_6/faces/results.xhtml?playernameparam=Rafael&playersurnameparam=Nadal
results.xhtml
文件(或 index.xhtml
文件指向的任何其他页面)将使用 <f:viewParam>
标签从 GET 请求中获取参数到绑定属性,如下面的代码所示:
<f:metadata>
<f:viewParam name="playernameparam" value="#{playersBean.playerName}"/>
<f:viewParam name="playersurnameparam" value="#{playersBean.playerSurname}"/>
</f:metadata>
...
<h:body>
You requested name: <h:outputTextvalue="#{playersBean.playerName}"/><br/>
You requested surname: <h:outputText value="#{playersBean.playerSurname}"/>
</h:body>
如果你希望使用<h:link>
标签,并且将includeViewParams
属性设置为true
,那么index.xhtml
文件将如下所示(在这种情况下,没有表单提交和 POST 请求):
<f:metadata>
<f:viewParam name="playernameparam" value="#{playersBean.playerName}"/>
<f:viewParam name="playersurnameparam" value="#{playersBean.playerSurname}"/>
</f:metadata>
...
<h:body>
<h:link value="Send"outcome="results?faces-redirect=true" includeViewParams="true"/>
</h:body>
这些示例被封装在名为ch2_6
的应用程序中。
你可以在任何 URL 中使用includeViewParams
请求参数,这意味着你可以在管理 Bean 中使用它,在导航链接中包含视图参数,如下所示:
<f:metadata>
<f:viewParam name="playernameparam" value="#{playersBean.playerName}"/>
<f:viewParam name="playersurnameparam" value="#{playersBean.playerSurname}"/>
</f:metadata>
...
<h:body>
<h:form>
Enter name:<h:inputText value="#{playersBean.playerName}"/>
Enter name:<h:inputText value="#{playersBean.playerSurname}"/>
<h:commandButton value="Submit" action="#{playersBean.toUpperCase()}"/>
</h:form>
</h:body>
操作方法如下:
public String toUpperCase(){
playerName=playerName.toUpperCase();
playerSurname=playerSurname.toUpperCase();
return "results?faces-redirect=true&includeViewParams=true";
}
完整的应用程序名为ch2_7
,可在 Packt Publishing 网站的该章节代码包中找到。
如前所述的代码所示,UIViewParameter
类扩展了UIInput
类,这意味着它继承了所有属性,例如required
和requiredMessage
。当 URL 必须包含视图参数时,你可以使用这两个属性来确保应用程序流程得到控制,并且用户得到正确通知。以下是一个示例代码:
<f:metadata>
<f:viewParam name="playernameparam" required="true" requiredMessage="Player name required!" value="#{playersBean.playerName}"/>
<f:viewParam name="playersurnameparam" required="true" requiredMessage="Player surname required!" value="#{playersBean.playerSurname}"/>
</f:metadata>
如果初始 URL 不包含视图参数(一个或两个),那么你将收到一条消息,报告这一事实。此示例被封装在名为ch2_9
的应用程序中。
此外,视图参数支持细粒度的转换和验证。你可以使用<f:validator>
和<f:converter>
,或者从UIInput
类继承的validator
和converter
属性。假设你有一个名为PlayerValidator
的自定义验证器(其实际实现并不重要),以下是其代码:
@FacesValidator("playerValidator")
public class PlayerValidator implements Validator {
@Override
public void validate(FacesContext context, UIComponent component,
Object value) throws ValidatorException {
//validation conditions
...
然后,你可以将其附加到视图参数上,如下所示:
<f:metadata>
<f:viewParam id="nameId" name="playernameparam" validator="playerValidator" value="#{playersBean.playerName}"/>
<f:viewParam id="surnameId" name="playersurnameparam" validator="playerValidator"value="#{playersBean.playerSurname}"/>
</f:metadata>
以下代码片段完成了以下任务:
-
通过名称获取请求参数的值,
playernameparam
和playersurnameparam
-
转换并验证(在这种情况下,验证)参数
-
如果转换和验证成功结束,则参数被设置在管理 Bean 属性中
-
任何验证失败都将导致显示一条消息
注意
对于自定义消息样式,你可以将<h:message>
标签附加到<f:viewParam>
标签上。
此示例被封装在名为ch2_10
的应用程序中。
注意
如果你希望保留验证失败后的视图参数,那么你需要使用比@RequestScoped
更广泛的范围,例如@ViewScoped
,或者通过命令组件中的<f:param>
标签手动保留请求参数以供后续请求使用。
有时,你可能需要一个视图参数的转换器。例如,如果你尝试从一个管理 Bean 中将java.util.Date
参数作为视图参数传递,你可能会这样编写代码:
private Date date = new Date();
...
public String sendDate() {
String dateAsString = new SimpleDateFormat("dd-MM-yyyy").format(date);
return "date.xhtml?faces-redirect=true&date=" + dateAsString;
}
现在,在date.xhtml
文件中,你需要将视图参数从字符串转换为date
,为此,你可以使用以下代码中的<f:convertDateTime>
转换器:
<f:viewParam name="date" value="#{dateBean.date}">
<f:convertDateTime pattern="dd-MM-yyyy" />
</f:viewParam>
当然,也可以使用自定义转换器。完整的应用程序名为ch2_29
。
在使用 <f:viewParam>
标签的许多优点中,我们有一个差距。当视图参数设置在管理 Bean 属性中时,设置的值在 @PostConstruct
中不可用;因此,你无法直接执行初始化或预加载任务。你可以通过附加 preRenderView
事件监听器来快速修复这个问题,如下面的代码所示:
<f:metadata>
<f:viewParam name="playernameparam" value="#{playersBean.playerName}"/>
<f:viewParam name="playersurnameparam" value="#{playersBean.playerSurname}"/>
<f:event type="preRenderView" listener="#{playersBean.init()}"/>
</f:metadata>
init
方法如下所示:
public void init() {
// do something with playerName and playerSurname
}
注意
使用 <f:viewParam>
标签时,设置的值在 @PostConstruct
中不可用。你可以通过附加 preRenderView
事件监听器来修复这个问题,或者,如你将看到的,通过 <f:viewAction>
标签。
此示例包含在名为 ch2_8
的应用程序中。
好吧,这里还有一个我想讨论的方面。UIViewParameter
类 (<f:viewParam>
) 是一个有状态的组件,它将值存储在状态中。这很好,因为即使在值不再来自页面 URL 或管理 Bean 是请求作用域的情况下,值仍然在回发中可用。因此,你只需要指示一次视图参数,而不是每次请求都要指示。但是,这种行为的缺点有几个——最显著的是在每次回发时调用设置方法(你不想在视图 Bean 中这样做)。另一个缺点是在每次回发时调用通过 preRenderView
事件处理器指示的方法;这可以通过以下代码中的测试来修复。完整的应用程序命名为 ch2_28
。
public void init() {
if (!FacesContext.getCurrentInstance().isPostback()) {
// do something with playerName and playerSurname
}
}
可能最痛苦的缺点是在每次回发时转换和验证视图参数。显然,这不是你期望看到的行为。为了仅在页面 URL 包含请求参数时调用转换器/验证器,你需要通过编写自定义实现来修改 UIViewParameter
类的实现。你可以尝试编写一个无状态的 UIViewParameter
类或控制转换/验证调用。当然,你必须记住,修改默认实现可能会导致更多或更不可预测的缺点。作为替代方案,你可以使用 OmniFaces 的 <o:viewParam>
标签,它解决了这些问题。相关示例可以在 showcase.omnifaces.org/components/viewParam
上看到。
因此,作为本节的最终结论,<f:viewParam>
标签用于捕获请求参数。此外,它可以与 <h:link>
和 <h:button>
标签一起使用,以发送出站请求参数,或者在非 JSF 表单中,将数据发送到使用 <f:viewParam>
标签的 JSF 页面,或者使 JSF 结果页面在 POST-redirect-GET 流中可书签。另一方面,<f:viewParam>
标签不支持 <h:form>
标签使用 GET 或通过 GET 请求提供对随机 JSF 页面的访问。
在 GET 请求上调用动作
从 JSF 2.2 开始,我们可以通过使用新的通用 视图操作 功能(在 Seam 2 和 3 中广为人知)来处理在 GET 请求上调用操作。这个新功能体现在 <f:viewAction>
标签中,它被声明为元数据面 <f:metadata>
的子标签。这允许视图操作成为 faces/non-faces 请求的生命周期的一部分。
在前面的章节中,我们看到了如何将自定义验证器附加到 <f:viewParam>
标签上以验证视图参数。当验证方法在托管 Bean 中声明而不是作为 Validator
接口的独立实现时,可以使用 <f:viewAction>
标签完成同样的事情。例如,在 index.xhtml
文件中,您可能有以下代码:
<f:metadata>
<f:viewParam id="nameId" name="playernameparam" value="#{playersBean.playerName}"/>
<f:viewParam id="surnameId" name="playersurnameparam" value="#{playersBean.playerSurname}"/>
<f:viewAction action="#{playersBean.validateData()}"/>
</f:metadata>
正如您所看到的,下面的 validateData
方法只是在 PlayersBean
中声明的一个普通方法:
public String validateData() {
//validation conditions
return "index"; //or other page
}
这个示例被封装在名为 ch2_11
的应用程序中。
注意
<f:viewAction>
标签和 preRenderView
事件监听器不是相同的!
前面的说明强调了我们的下一个讨论。您可能会认为它们是相同的,因为在先前的示例中,您可以替换 <f:viewAction>
为 preRenderView
并获得相同的效果(结果)。确实,它们在某种程度上是相同的,但以下四个要点中的一些现有差异很重要,如下所示:
-
默认情况下,
preRenderView
事件监听器在回发请求上执行,而视图操作则不会。在preRenderView
事件监听器的情况下,您需要通过以下方式测试请求类型来克服这一点:if (!FacesContext.getCurrentInstance().isPostback()) { // code that should not be executed in postback phase }
例如,以下代码将尝试使用
preRenderView
事件监听器对集合值应用一些修改:<f:metadata> <f:viewParam name="playernameparam" value="#{playersBean.playerName}"/> <f:viewParam name="playersurnameparam" value="#{playersBean.playerSurname}"/> <f:event type="preRenderView" listener="#{playersBean.init()}"/> </f:metadata>
init
方法在PlayersBean
中声明,它只是将集合值转换为大写,如下面的代码所示:public void init() { if (playerName != null) { playerName = playerName.toUpperCase(); } if (playerSurname != null) { playerSurname = playerSurname.toUpperCase(); } }
接下来,当 JSF 页面渲染时,将使用大写形式的集合值,并且可以完成进一步的请求(例如,当某个按钮被点击时,您可能想调用
#{playersBean.userAction()}
方法)。但是,每个进一步的请求都会再次调用init
方法(在userAction
方法之后),因为preRenderView
事件监听器在回发时执行。除非这是期望的功能,否则您需要通过编程测试回发以防止以下init
方法代码被执行:public void init() { if (!FacesContext.getCurrentInstance().isPostback()) { if (playerName != null) { playerName = playerName.toUpperCase(); } if (playerSurname != null) { playerSurname = playerSurname.toUpperCase(); } } }
嗯,在
<f:viewAction>
标签的情况下,情况并不相同。将preRenderView
事件监听器替换为<f:viewAction>
标签,如下面的代码所示:<f:metadata> <f:viewParam name="playernameparam" value="#{playersBean.playerName}"/> <f:viewParam name="playersurnameparam" value="#{playersBean.playerSurname}"/> <f:viewAction action="#{playersBean.init()}"/> </f:metadata>
<f:viewAction>
标签支持一个名为onPostback
的属性,默认设置为false
,这意味着在回发请求上不会调用init
方法。当然,如果你将其设置为true
,那么它将起相反的作用;但是,请注意,在preRenderView
事件监听器的情况下,init
方法是在userAction
方法之后调用的,而在<f:viewAction>
标签的情况下,init
方法是在userAction
方法之前调用的,如下面的代码行所示:<f:viewAction action="#{playersBean.init()}" onPostback="true"/>
基于
preRenderView
事件监听器的示例被封装在名为ch_12_1
的应用程序中,而对于<f:viewAction>
标签,它被命名为ch_12_2
。 -
视图操作具有导航能力,而
preRenderView
事件监听器则没有。虽然视图操作可以自然地完成导航任务,但preRenderView
事件监听器需要基于 JSF API 进行显式导航。例如,如果你将前面的
init
方法修改为返回start.xhtml
视图,那么你可能需要将其修改如下面的代码所示:public String init() { if (playerName != null) { playerName = playerName.toUpperCase(); } if (playerSurname != null) { playerSurname = playerSurname.toUpperCase(); } return "start"; }
但是,这不会与
preRenderView
事件监听器一起工作!你需要通过返回void
并替换返回"start"
代码行来添加显式导航:ConfigurableNavigationHandler handler = (ConfigurableNavigationHandler) FacesContext.getCurrentInstance().getApplication().getNavigationHandler(); handler.performNavigation("start");
如果你移除
preRenderView
事件监听器并使用<f:viewAction>
标签代替,那么前面的init
方法将正确导航到start.xhtml
,而不需要显式调用导航处理程序。基于
preRenderView
事件监听器的示例被封装在名为ch_13_1
的应用程序中,而对于<f:viewAction>
标签,它被命名为ch_13_2
。此外,
<f:viewAction>
标签支持声明性导航。因此,你可以在faces-config.xml
文件中编写一个导航规则,在页面渲染之前进行查询。例如:<navigation-rule> <from-view-id>index.xhtml</from-view-id> <navigation-case> <from-action>#{playersBean.init()}</from-action> <from-outcome>start</from-outcome> <to-view-id>rafa.xhtml</to-view-id> <redirect/> </navigation-case> </navigation-rule>
现在,将渲染
rafa.xhtml
页面而不是start.xhtml
页面。此示例被封装在名为ch2_13_3
的应用程序中。 -
默认情况下,视图操作是在调用应用程序阶段执行的。但是,通过将
immediate
属性设置为true
,它也可以在应用请求值阶段执行,如下面的代码所示:<f:viewAction action="#{playersBean.init()}" immediate="true"/>
-
此外,你可以使用
phase
属性指定执行动作的阶段,其值表示阶段名称作为预定义的常量。例如:<f:viewAction action="#{playersBean.init()}" phase="UPDATE_MODEL_VALUES"/>
支持的值有
APPLY_REQUEST_VALUES
、INVOKE_APPLICATION
、PROCESS_VALIDATIONS
和UPDATE_MODEL_VALUES
。
注意
视图操作可以放置在不含其他视图参数的视图元数据面上。
使用<f:attribute>
标签传递属性
当<f:param>
标签不能满足你的需求时,也许<f:attribute>
标签可以。这个标签允许你传递组件的属性值,或者将参数传递给组件。
例如,你可以将<h:commandButton>
标签的属性value
的值分配,如下面的代码所示:
<h:commandButton actionListener="#{playersBean.parametersAction}">
<f:attribute name="value" value="Send Rafael Nadal" />
</h:commandButton>
这将渲染一个标签为发送拉斐尔·纳达尔的按钮。其代码如下:
<h:commandButton value="Send Rafael Nadal" actionListener="#{playersBean.parametersAction}">
此外,<f:attribute>
标签可以用来向组件传递参数,如下面的代码所示:
<h:commandButton actionListener="#{playersBean.parametersAction}">
<f:attribute id="playerName" name="playerNameAttr" value="Rafael"/>
<f:attribute id="playerSurname" name="playerSurnameAttr" value="Nadal"/>
</h:commandButton>
在动作监听器方法中,你可以提取属性值,如下面的代码所示:
private final static Logger logger = Logger.getLogger(PlayersBean.class.getName());
private String playerName;
private String playerSurname;
...
//getters and setters
...
public void parametersAction(ActionEvent evt) {
playerName = (String) evt.getComponent().getAttributes().get("playerNameAttr");
playerSurname = (String) evt.getComponent().getAttributes().get("playerSurnameAttr");
logger.log(Level.INFO, "Name: {0} Surname: {1}", new Object[]{playerName, playerSurname});
}
此示例被封装在名为ch2_14
的应用程序中。
如果你喜欢 PrimeFaces(primefaces.org/
),那么你可能觉得下一个例子很有用。PrimeFaces 最伟大的内置组件之一是<p:fileUpload>
标签,它可以用来上传文件。有时,除了要上传的文件外,你还需要传递一些额外的参数,例如文件的所有者的名字和姓氏。嗯,<p:fileUpload>
标签没有提供解决方案,但<f:attribute>
标签可能会有所帮助。以下是一个经典的带有<f:attribute>
标签的<p:fileUpload>
标签的代码:
<h:form>
<p:fileUpload
fileUploadListener="#{fileUploadController.handleFileUpload}"
mode="advanced" dragDropSupport="false"
update="messages" sizeLimit="100000" fileLimit="3"
allowTypes="/(\.|\/)(gif|jpe?g|png)$/">
<f:attribute id="playerName" name="playerNameAttr" value="Rafael"/>
<f:attribute id="playerSurname" name="playerSurnameAttr" value="Nadal"/>
</p:fileUpload>
<p:growl id="messages" showDetail="true"/>
</h:form>
handleFileUpload
方法负责上传特定的步骤(以下代码中省略),但它也可以访问通过<f:attribute>
标签传递的值:
public void handleFileUpload(FileUploadEvent evt) {
//upload specific tasks, see PrimeFaces documentation
String playerName = (String) evt.getComponent().getAttributes().get("playerNameAttr");
String playerSurname = (String) evt.getComponent().getAttributes().get("playerSurnameAttr");
FacesMessage msg = new FacesMessage("Successful", evt.getFile().getFileName() + " is uploaded for " + playerName + " " + playerSurname);
FacesContext.getCurrentInstance().addMessage(null, msg);
}
如果你不是 PrimeFaces 的粉丝,那么你可能认为这个例子没有用,但你可能喜欢其他第三方库,比如 RichFaces、ICEFaces 和 MyFaces。你也可以将这项技术应用于其他组件库。
此示例被封装在名为ch2_15
的应用程序中。
<f:attribute>
标签在动态传递参数时非常有用,特别是当与绑定到管理 Bean 的 UI 组件一起使用binding
属性时。这非常有用,尤其是在 JSF 没有提供将参数传递给绑定 UI 组件的 getter/setter 方法的解决方案时,如下面的代码所示:
<h:form>
<h:inputText binding="#{playersBean.htmlInputText}" value="#{playersBean.playerNameSurname}">
<f:attribute name="playerNameAttr" value="Rafael Nadal"/>
</h:inputText>
</h:form>
注意
现在,<h:inputText>
标签的值应包含通过<f:attribute>
标签设置的值。请注意,仅使用唯一的属性名称,并且不要干扰(尝试覆盖)UI 组件的默认属性。
此外,PlayersBean
管理 Bean 的代码如下:
@Named
@RequestScoped
public class PlayersBean {
private UIInput htmlInputText= null;
public PlayersBean() {
}
public UIInput getHtmlInputText() {
return htmlInputText;
}
public void setHtmlInputText(UIInput htmlInputText) {
this.htmlInputText = htmlInputText;
}
public String getPlayerNameSurname() {
return (String) htmlInputText.getAttributes().get("playerNameAttr");
}
}
如你所见,通过这种方式传递的所有参数都可以通过父 UI 组件的getAttributes
方法访问。
此示例被封装在名为ch2_23
的应用程序中。
通过动作监听器设置属性值
<f:setPropertyActionListener>
标签使用动作监听器(由框架创建)直接将值设置到管理 Bean 的属性中;它放置在由ActionSource
类派生的组件中。target
属性指示管理 Bean 属性,而value
属性指示属性的值,如下面的代码所示:
<h:commandButton value="Send Rafael Nadal 1">
<f:setPropertyActionListener id="playerName" target="#{playersBean.playerName}" value="Rafael"/>
<f:setPropertyActionListener id="playerSurname" target="#{playersBean.playerSurname}" value="Nadal"/>
</h:commandButton>
现在,在PlayersBean
管理 Bean 中,调用 setter 方法并设置值;logger
有助于查看应用程序流程和理解监听器是如何触发的,如下面的代码所示:
private final static Logger logger =Logger.getLogger(PlayersBean.class.getName());
private String playerName;
private String playerSurname;
public void setPlayerName(String playerName) {
this.playerName = playerName;
logger.log(Level.INFO, "Player name (from setPlayerName() method: {0}", playerName);
}
public void setPlayerSurname(String playerSurname) {
this.playerSurname = playerSurname;
logger.log(Level.INFO, "Player surname (from setPlayerSurname() method: {0}", playerSurname);
}
当点击标记为发送给拉斐尔·纳达尔 1的按钮时,应用程序的输出将如下所示:
INFO: Player name (from setPlayerName() method: Rafael
INFO: Player surname (from setPlayerSurname() method: Nadal
注意
请记住,动作监听器是按照它们定义的顺序执行的,这意味着<f:setPropertyActionListener>
标签的存在可能会影响监听器被触发的顺序。
这个注释很重要!为了清楚地理解,请查看以下代码片段:
<h:commandButton value="Send Rafael Nadal 2" actionListener="#{playersBean.parametersAction}">
<f:setPropertyActionListener id="playerName" target="#{playersBean.playerName}" value="Rafael"/>
<f:setPropertyActionListener id="playerSurname" target="#{playersBean.playerSurname}" value="Nadal"/>
</h:commandButton>
以下代码是parametersAction
方法:
public void parametersAction(ActionEvent e) {
logger.log(Level.INFO, "Player name (from parametersAction(ActionEvent) method: {0}", playerName);
logger.log(Level.INFO, "Player surname (from parametersAction(ActionEvent) method: {0}", playerSurname);
}
好吧,这段代码没有按预期工作!你可能认为设置器方法首先被调用,然后是parametersAction
方法;因此,设置的值在动作方法中可用。但是,以下输出将证明相反:
INFO: Player name (from parametersAction() method: null
INFO: Player surname (from parametersAction() method: null
INFO: Player name (from setPlayerName() method: Rafael
INFO: Player surname (from setPlayerSurname() method: Nadal
因此,属性是在命令动作监听器触发后设置的!为了解决这个问题,您可以使用action
属性而不是actionListener
:
<h:commandButton value="Send Rafael Nadal 3" action="#{playersBean.parametersAction()}">
<f:setPropertyActionListener id="playerName" target="#{playersBean.playerName}" value="Rafael"/>
<f:setPropertyActionListener id="playerSurname" target="#{playersBean.playerSurname}" value="Nadal"/>
</h:commandButton>
当然,您需要根据以下代码相应地调整parametersAction
方法:
public void parametersAction() {
logger.log(Level.INFO, "Player name (from parametersAction() method: {0}", playerName);
logger.log(Level.INFO, "Player surname (from parametersAction() method: {0}", playerSurname);
}
现在,输出将反映以下期望的结果:
INFO: Player name (from setPlayerName() method: Rafael
INFO: Player surname (from setPlayerSurname() method: Nadal
INFO: Player name (from parametersAction() method: Rafael
INFO: Player surname (from parametersAction() method: Nadal
这个例子被包裹在名为ch2_16
的应用中。
使用 Flash 作用域传递参数
新的 JSF Flash 作用域是一个非常有用的工具,当你需要在用户视图之间传递参数而不需要在会话中存储它们时。如果你记住存储在 Flash 作用域中的变量将在重定向后可用,并在之后被消除,那么 Flash 作用域就很容易理解。这在实现 POST-redirect-GET 模式时非常有用。
为了更好地理解,让我们假设以下场景:
-
玩家(用户)需要在 ATP 网站上注册。在提供其他信息的同时,他需要输入自己的姓名和姓氏,然后点击注册按钮。这一步骤在
index.xhtml
页面中完成。 -
应用流程将玩家重定向到
terms.xhtml
页面。在这个页面上,用户可以看到包含他姓名和姓氏的欢迎消息,以及一些必须接受(使用接受按钮)或拒绝(使用拒绝按钮)的条款和条件。 -
如果点击了拒绝按钮,那么用户将被重定向到
index.xhtml
主页,并且表单注册字段将显示他之前提供的信息。此外,他还将看到一个生成的消息,声明条款被拒绝!玩家未注册!这是由<h:message>
标签输出的。 -
如果点击了接受按钮,那么用户将被重定向到名为
done.xhtml
的页面。在这个页面上,用户将看到一个生成的消息,声明条款已接受,玩家已注册!以及另一个消息,声明姓名 姓氏已成功注册!第一个消息由<h:message>
标签输出,而第二个消息由<h:outputText>
标签输出。
以下是对两种场景的截图:
显然,只有将提交的值存储在某个地方,你才能实现这个流程,因为这些值在重定向过程中不会存活。这意味着在请求作用域中使用管理 bean 不是一个有效的选择。但是,如果我们讨论新的 Flash 作用域,那么对于请求作用域 bean 来说,事情就会变得更加有利。
如果你快速查看以下名为PlayersBean
的请求作用域 bean 的代码,将更容易理解这个想法:
@Named
@RequestScoped
public class PlayersBean {
private final static Logger logger = Logger.getLogger(PlayersBean.class.getName());
private String playerName;
private String playerSurname;
...
public String addValuesToFlashAction() {
Flash flash = FacesContext.getCurrentInstance().getExternalContext().getFlash();
flash.put("playerName", playerName);
flash.put("playerSurname", playerSurname);
return "terms?faces-redirect=true";
}
public void pullValuesFromFlashAction(ComponentSystemEvent e) {
Flash flash = FacesContext.getCurrentInstance().getExternalContext().getFlash();
playerName = (String) flash.get("playerName");
playerSurname = (String) flash.get("playerSurname");
}
public String termsAcceptedAction() {
Flash flash = FacesContext.getCurrentInstance().getExternalContext().getFlash();
flash.setKeepMessages(true);
pullValuesFromFlashAction(null);
//do something with firstName, lastName
logger.log(Level.INFO, "First name: {0}", playerName);
logger.log(Level.INFO, "Last name: {0}", playerSurname);
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Terms accepted and player registered!"));
return "done?faces-redirect=true";
}
public String termsRejectedAction() {
Flash flash = FacesContext.getCurrentInstance().getExternalContext().getFlash();
flash.setKeepMessages(true);
pullValuesFromFlashAction(null);
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Terms rejected! Player not registered!"));
return "index?faces-redirect=true";
}
}
此外,看一下起始页面index.xhtml
。其代码如下:
<h:body>
<f:metadata>
<f:event type="preRenderView" listener="#{playersBean.pullValuesFromFlashAction}"/>
</f:metadata>
<h:messages />
<h:form>
Name: <h:inputText value="#{playersBean.playerName}"/>
Surname: <h:inputText value="#{playersBean.playerSurname}"/>
<h:commandButton value="Register" action="#{playersBean.addValuesToFlashAction()}"/>
</h:form>
</h:body>
因此,提交过程开始于用户点击标有注册的按钮。JSF 将调用addValuesToFlashAction
方法,该方法负责将提交的值放入 Flash 作用域;这将确保值在重定向到terms.xhtml
页面时仍然存活。
如果用户拒绝条款和条件,那么他会重定向到index.xhtml
页面。在这里,你需要用用户输入的值重新填充注册表单字段。为此,你可以使用preRenderView
事件,该事件在渲染响应阶段通过调用pullValuesFromFlashAction
方法从 Flash 作用域中加载值。
接下来,让我们关注terms.xhtml
页面;其代码如下:
<h:body>
<h:messages />
Hello, <h:outputText value="#{flash.keep.playerName} #{flash.keep.playerSurname}"/>
<br/><br/>Terms & Conditions ... ... ... ... ...
<h:form>
<h:commandButton value="Reject" action="#{playersBean.termsRejectedAction()}" />
<h:commandButton value="Accept" action="#{playersBean.termsAcceptedAction()}" />
</h:form>
</h:body>
首先,这个页面会显示一个欢迎信息,其中包含了输入的值。这些值是通过以下代码从 Flash 作用域中获取的:
#{flash.keep.playerName}
#{flash.keep.playerSurname}
注意到这种方法有两个功能,如下所示:
-
它从 Flash 作用域中获取值,这也可以通过以下行完成:
#{flash.playerName} #{flash.playerSurname}
-
这告诉 JSF 在下一个请求中保持 Flash 作用域中的值。这是必需的,因为放入 Flash 作用域的值在经过一次重定向后就会被删除。当我们从
index.xhtml
页面导航到terms.xhtml
页面时,我们已经触发了一个重定向。但是,当点击接受或拒绝按钮时,还会出现另一个重定向。
注意
存储在 Flash 作用域中的值在经过一次重定向后就会被删除。
此外,页面还显示了返回index.xhtml
页面的按钮和前进到done.xhtml
页面的按钮。接受按钮将调用termsAcceptedAction
方法,该方法基本上会在重定向之间保留消息(调用setKeepMessages
方法)并将流程重定向到done.xhtml
页面。同样,拒绝按钮调用termsRejectedAction
方法,保留 Flash 作用域中的消息,并将流程重定向到index.xhtml
页面。
done.xhtml
页面是通过以下代码展示的:
<h:body>
<f:metadata>
<f:event type="preRenderView" listener="#{playersBean.pullValuesFromFlashAction}"/>
</f:metadata>
<h:messages />
<h:outputText value="#{playersBean.playerName} #{playersBean.playerSurname}"/> successfully registered!
</h:body>
再次使用preRenderView
事件监听器从 Flash 作用域中获取值。
这个例子被封装在名为ch2_21
的应用程序中。
用 JSTL 的<c:set>
标签替换<f:param>
标签
有时,JSTL <c:set>
标签可以解决 JSF <f:param>
标签无法解决的问题。可能你已经知道,我们可以使用<f:param>
标签将参数传递给<ui:include>
标签,如下所示:
<ui:include src="img/rafa.xhtml">
<f:param name="rafa" value="Rafael Nadal Page"/>,
</ui:include>
嗯,这种方法会引发一个问题!现在,Rafael Nadal 页面
的值将通过 EL 在包含的页面中可用,#{rafa}
,但不会在包含页面的托管 Bean 构造函数中可用!
是时候使用<c:set>
标签保存情况了;因此,代码将更改为以下内容:
<ui:include src="img/rafa.xhtml">
<c:set var="rafa" value="Rafael Nadal Page" scope="request"/>,
</ui:include>
完成!现在,在托管 Bean 的构造函数中,值可以按照以下代码提取:
public ConstructorMethod(){
FacesContext facesContext = FacesContext.getCurrentInstance();
HttpServletRequest httpServletRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest();
String rafa = (String) request.getAttribute("rafa");
}
在第四章的配置系统事件监听器部分,你将看到如何处理针对 Flash 作用域的系统事件。
通过 cookie 发送数据
JSF 提供了一个请求 cookie 映射,可用于处理 HTTP cookies。通过 JavaScript 设置 cookie 可以轻松完成;以下是一些辅助方法:
-
设置 cookie 的 JavaScript 方法如下:
function setCookie(cookie_name, value, expiration_days) { var expiration_date = new Date(); expiration_date.setDate(expiration_date.getDate() + expiration_days); var c_value = escape(value) + ((expiration_days == null) ? "" : "; expires=" + expiration_date.toUTCString()); document.cookie = cookie_name + "=" + c_value; }
通过名称删除 cookie 的 JavaScript 方法如下:
function deleteCookie(cookie_name) { document.cookie = encodeURIComponent(cookie_name) + "=deleted; expires=" + new Date(0).toUTCString(); }
-
通过名称提取 cookie 的 JavaScript 方法如下:
function getCookie(cookie_name) { var i, part_1, part_2; var cookieslist = document.cookie.split(";"); //<![CDATA[ for (i = 0; i < cookieslist.length; i++) { part_1 = cookieslist[i].substr(0, cookieslist[i].indexOf("=")); part_2 = cookieslist[i].substr(cookieslist[i].indexOf("=") + 1); part_1 = part_1.replace(/^\s+|\s+$/g, ""); if (part_1 == cookie_name) { return unescape(part_2); } } //]]> return "nocookie"; }
假设你有两个名为name
和surname
的 cookie,如下所示:
setCookie('name', 'Rafael', 1);
setCookie('surname', 'Nadal', 1);
JSF 可以通过以下请求 cookie 映射访问这些 cookie:
Object name_cookie = FacesContext.getCurrentInstance().getExternalContext().getRequestCookieMap().get("name");
Object surname_cookie = FacesContext.getCurrentInstance().getExternalContext().getRequestCookieMap().get("surname");
//set playerName property
if (name_cookie != null) {
playerName = (((Cookie) name_cookie).getValue());
}
//set playerSurname property
if (surname_cookie != null) {
playerSurname = (((Cookie) surname_cookie).getValue());
}
JSF 还提供了几个用于处理 cookie 的获取器和设置器方法。这些方法如下表所示:
获取方法 | 设置方法 |
---|---|
String getComment() |
setComment(String arg) |
String getDomain() |
setDomain(String arg) |
String getName() |
setHttpOnly(boolean arg) |
String getPath() |
setPath(String arg) |
String getValue() |
setValue(String arg) |
int getMaxAge() |
setMaxAge(int arg) |
boolean getSecure() |
setSecure(boolean arg) |
int getVersion() |
setVersion(int arg) |
boolean isHttpOnly() |
此示例包含在名为ch2_18
的应用程序中,可以在本章的代码包中找到。
处理隐藏字段
隐藏字段有时非常有用!以微妙的方式传递数据可能是处理临时数据或用户提供的应重复使用的信息的完美选择。JSF 提供了<h:inputHidden>
标签来传递隐藏参数。以下代码将两个隐藏参数传递给托管 Bean:
<h:form id="hiddenFormId">
<h:commandButton value="Send Rafael Nadal" onclick="setHiddenValues();" action="#{playersBean.parametersAction()}"/>
<h:inputHidden id="playerName" value="#{playersBean.playerName}"/>
<h:inputHidden id="playerSurname" value="#{playersBean.playerSurname}"/>
</h:form>
通常,从 JavaScript 设置隐藏字段值是一种常见做法。当点击发送拉斐尔·纳达尔按钮时,名为setHiddenValues
的 JavaScript 函数会被调用;这发生在表单提交之前。setHiddenValues
函数的代码如下:
<script type="text/javascript">
function setHiddenValues() {
document.getElementById('hiddenFormId:playerName').value = "Rafael";
document.getElementById('hiddenFormId:playerSurname').value = "Nadal";
}
</script>
接下来,隐藏参数在指定的管理 Bean 属性中设置,并调用 parametersAction
方法——设置的值已准备好使用!
此示例被封装在名为 ch2_17
的应用程序中,并可在本章的代码包中找到。
发送密码
JSF 提供了一个名为 <h:inputSecret>
的专用标签来渲染以下众所周知的 HTML 代码:
<input type="password">
例如,你可以像以下代码所示使用它:
<h:form>
<h:inputSecret value="#{playersBean.playerPassword}"/>
<h:commandButton value="Send Password" action="#{playersBean.passwordAction()}"/>
</h:form>
此示例被封装在名为 ch2_19
的应用程序中。
通过程序访问 UI 组件属性
使用 JSF API 从管理 Bean 访问 UI 组件属性不是一种常见的方法,但有时它可能很有用。例如,假设我们有以下表单:
<h:form id="playerFormId">
<h:inputText id="playerNameId" value="#{playersBean.playerName}"/>
<h:inputText id="playerSurnameId" value="#{playersBean.playerSurname}"/>
<h:commandButton value="Process" action="#{playersBean.processAction()}"/>
</h:form>
现在,你想要在 processAction
方法中获取具有 ID 的组件 playerNameId
和 playerSurnameId
的值。此外,你想要将具有 ID 的组件 playerNameId
的值设置为 RAFAEL
。通过程序(使用 JSF API),你可以这样实现:
public void processAction() {
UIViewRoot view = FacesContext.getCurrentInstance().getViewRoot();
UIComponent uinc = view.findComponent("playerFormId:playerNameId");
Object prev = ((UIInput) uinc).getAttributes().put("value", "RAFAEL");
UIComponent uisc = view.findComponent("playerFormId:playerSurnameId");
Object current = ((UIInput) uisc).getAttributes().get("value");
}
首先,你需要获取对 UIViewRoot
的访问权限,它是顶级 UI 组件——UIComponent
树的根。然后,你可以通过 findComponent
方法在 UI 组件树中通过 ID 搜索所需的 UI 组件。每个 UI 组件都提供了 getAttributes
方法,可以通过名称访问 UI 组件属性。此时,你可以使用 get
方法提取属性值,或者使用 put
方法设置新的属性值。
此示例被封装在名为 ch2_20
的应用程序中。
通过方法表达式传递参数
使用方法表达式传递参数是将参数作为参数传递给管理 Bean 的操作方法的优雅解决方案。例如,让我们关注以下代码片段:
<h:form>
<h:commandButton value="Send Rafael Nadal" action="#{playersBean.parametersAction('Rafael','Nadal')}"/>
</h:form>
如您在以下代码中所见,action
属性指示一个接收两个参数的方法:
private String playerName;
private String playerSurname;
//getters and setters
public String parametersAction(String playerNameArg, String playerSurnameArg) {
playerName = playerNameArg;
playerSurname = playerSurnameArg;
return "result";
}
以同样的方式,你可以传递数值或对象。
此示例被封装在名为 ch2_26
的应用程序中。
通过绑定属性进行通信
JSF UI 组件支持一个名为 binding
的属性,它很少使用,有时理解得也不够好。其含义背后的故事可以扩展到几页纸,或者总结为一些黄金法则。我们将从绑定生命周期和简要概述开始,并以在生产环境中使用时应考虑的重要规则结束。
如果我们想要定位binding
属性进入战斗的时刻,我们可以参考 JSF 视图构建或恢复的时刻;构建/恢复视图的结果存在于组件树中。因此,在组件树可交付之前,JSF 需要检查所有binding
属性。对于每一个,JSF 都会检查是否存在一个预存在的(预先创建的)组件。如果找到了预存在的组件,则使用它;否则,JSF 将自动创建一个新的,并将其作为参数传递给对应于该binding
属性的 setter 方法。此外,JSF 在视图状态中添加了组件的引用。此外,回发请求(表单提交)会告诉 JSF 恢复视图,这将根据视图状态恢复组件和绑定。
现在你已经知道了binding
属性的作用,让我们列举一些使用它的重要方面:
-
在每个请求(初始或回发)之后,JSF 都会根据
binding
属性创建组件的实例。 -
在恢复视图(回发)时,组件实例创建后,JSF 根据存储的引用填充它。
-
当你将一个组件绑定到一个 bean 属性(类型为
UIComponent
)时,实际上你绑定的是整个组件。这种绑定是一个非常罕见的使用场景,当你想要工作/公开组件在视图中不可用的方法,或者你需要以编程方式更改组件的子组件时,它可能很有用。此外,你可以更改组件的属性并实例化组件,而不是让页面作者这样做。 -
由于 JSF 在每个请求中实例化组件,因此 bean 必须在请求作用域中;否则,组件可能会在不同视图之间共享。视图作用域也可能是一个解决方案。
-
binding
属性也用于将组件绑定到当前视图,而不需要 bean。这对于从另一个组件访问组件的状态非常有用。 -
如果没有 bean 属性绑定组件,则将组件放入 EL 作用域。这发生在组件树构建时;因此,EL 完全能够揭示在渲染阶段绑定的组件,这个阶段发生在组件树构建之后。
例如,一个<h:dataTable>
标签有三个有用的属性:first
、rows
和rowCount
。如果你将一个<h:dataTable>
标签绑定到当前视图,那么在这个组件外部,你可以像以下代码行所示访问这些属性:
<h:dataTable value="#{playersBean.dataArrayList}" binding="#{table}" var="t">
例如,你可以按照以下方式设置rows
属性:
#{table.rows = 3;''}
此外,按照以下方式显示rowCount
和first
属性:
<h:outputText value="#{table.rowCount}"/>
<h:outputText value="#{table.first}"/>
完整的应用程序命名为ch2_32
。
我们可以从一个豆子中完成相同的事情。首先,我们将<h:dataTable>
标签绑定到类型为HtmlDataTable
的 bean 属性,如下所示:
<h:dataTable value="#{playersBean.dataArrayList}" binding="#{playersBean.table}" var="t">
现在,在PlayersBean
中,我们添加以下代码:
private HtmlDataTable table;
...
//getter and setter
...
public void tableAction() {
logger.log(Level.INFO, "First:{0}", table.getFirst());
logger.log(Level.INFO, "Row count: {0}", table.getRowCount());
table.setRows(3);
}
完整的应用程序命名为ch2_31
。
托管 Bean 通信
到目前为止,我们特别关注了 Facelets 和托管 Bean 之间的通信。在本节中,我们将介绍 JSF 通信的另一个重要方面——托管 Bean 之间的通信。我们将讨论以下主题:
-
将托管 Bean 注入到另一个 Bean 中
-
使用应用程序/会话映射进行托管 Bean 之间的通信
-
以编程方式访问其他托管 Bean
将托管 Bean 注入到另一个 Bean 中
可以使用@ManagedProperty
将托管 Bean 注入到另一个托管 Bean 中。例如,假设你有一个会话作用域的托管 Bean,用于存储玩家名和姓氏,如下面的代码所示:
@Named
@SessionScoped
public class PlayersBean implements Serializable{
private String playerName;
private String playerSurname;
public PlayersBean() {
playerName = "Rafael";
playerSurname = "Nadal";
}
//getters and setters
}
现在,假设你想从另一个名为ProfileBean
的视图作用域 Bean 中访问这个 Bean 的属性。为此,你可以使用@ManagedProperty
,如下面的代码所示:
@ManagedBean //cannot be @Named
@ViewScoped
public class ProfileBean implements Serializable{
private final static Logger logger = Logger.getLogger(PlayersBean.class.getName());
@ManagedProperty("#{playersBean}")
private PlayersBean playersBean;
private String greetings;
public ProfileBean() {
}
public void setPlayersBean(PlayersBean playersBean) {
this.playersBean = playersBean;
}
@PostConstruct
public void init(){
greetings = "Hello, " + playersBean.getPlayerName() + " " +playersBean.getPlayerSurname() + " !";
}
public void greetingsAction(){
logger.info(greetings);
}
}
调用greetingsAction
方法的 Facelet 将在日志中绘制如下行:
INFO: Hello, Rafael Nadal !
注意
@PostConstruct
方法的存在是可选的,但了解这是注入依赖最早可用的位置是好的。
此示例被封装在名为ch2_22
的应用程序中。
如果你想要使用 CDI Bean,那么你可以像以下代码那样完成相同的事情:
@Named
@ViewScoped
public class ProfileBean implements Serializable{
@Inject
private PlayersBean playersBean;
private String greetings;
...
此示例被封装在名为ch2_30
的应用程序中。
使用应用程序/会话映射进行托管 Bean 之间的通信
根据需要,托管 Bean 之间的通信可以通过应用程序映射或会话映射来确保,无论是在多个浏览器会话中还是在单个浏览器会话中。
使用应用程序/会话映射的优点在于,多个 Bean 可以独立于它们的作用域相互通信。首先,你需要定义一个辅助类,它提供两个静态方法,一个用于将值添加到应用程序映射中,另一个用于从应用程序映射中删除值,如下面的代码所示:
public class ApplicationMapHelper {
public static Object getValueFromApplicationMap(String key) {
return FacesContext.getCurrentInstance().getExternalContext().getApplicationMap().get(key);
}
public static void setValueInApplicationMap(String key, Object value) {
FacesContext.getCurrentInstance().getExternalContext().getApplicationMap().put(key, value);
}
}
现在,你可以即兴创作一个简单的场景:在一个托管 Bean(请求作用域)中,将一些值放入应用程序映射中,在另一个托管 Bean(会话作用域)中获取这些值。因此,第一个 Bean 的代码如下:
@Named
@RequestScoped
public class PlayersBeanSet {
public void playerSetAction() {
ApplicationMapHelper.setValueInApplicationMap("PlayersBeanSet.name", "Rafael");
ApplicationMapHelper.setValueInApplicationMap("PlayersBeanSet.surname", "Nadal");
}
}
从应用程序映射中提取这些值的托管 Bean 如下所示:
@Named
@SessionScoped
public class PlayersBeanGet implements Serializable{
private final static Logger logger = Logger.getLogger(PlayersBeanGet.class.getName());
public void playerGetAction() {
String name = String.valueOf(ApplicationMapHelper.getValueFromApplicationMap("PlayersBeanSet.name"));
String surname = String.valueOf(ApplicationMapHelper.getValueFromApplicationMap("PlayersBeanSet.surname"));
logger.log(Level.INFO, "Name: {0} Surname: {1}", new Object[]{name, surname});
}
}
此示例被封装在名为ch2_24
的应用程序中。
以编程方式访问其他托管 Bean
有时,你可能需要从一个事件监听器类或另一个托管 Bean 中访问一个托管 Bean。假设我们有一个会话作用域的托管 Bean,名为PlayersBean
,还有一个请求作用域的托管 Bean,名为ProfileBean
,并且你想要在ProfileBean
中以编程方式访问PlayersBean
。假设PlayersBean
已经被创建,你可以通过以下方式完成此任务:
-
在
ProfileBean
内部使用evaluateExpressionGet
方法如下:FacesContext context = FacesContext.getCurrentInstance(); PlayersBean playersBean = (PlayersBean) context.getApplication().evaluateExpressionGet(context, "#{playersBean}", PlayersBean.class); if (playersBean != null) { //call the PlayersBean method } else { logger.info("SESSION BEAN NOT FOUND!"); }
-
在
ProfileBean
中如下使用createValueExpression
方法:FacesContext context = FacesContext.getCurrentInstance(); ELContext elcontext = context.getELContext(); PlayersBean playersBean = (PlayersBean) context.getApplication().getExpressionFactory().createValueExpression(elcontext, "#{playersBean}", PlayersBean.class).getValue(elcontext); if (playersBean != null) { //call the PlayersBean method } else { logger.info("SESSION BEAN NOT FOUND!"); }
为了使事情更简单,当你需要以编程方式创建一个值表达式时,你可以使用一个简单的辅助方法,并只传递表达式和类,如下所示:
private ValueExpression createValueExpression(String exp, Class<?> cls) { FacesContext facesContext = FacesContext.getCurrentInstance(); ELContext elContext = facesContext.getELContext(); return facesContext.getApplication().getExpressionFactory().createValueExpression(elContext, exp, cls); }
-
在
ProfileBean
中如下使用ELResolver
:FacesContext context = FacesContext.getCurrentInstance(); ELContext elcontext = context.getELContext(); PlayersBean playersBean = (PlayersBean) elcontext.getELResolver().getValue(elcontext, null, "playersBean"); if (playersBean != null) { //call the PlayersBean method } else { logger.info("SESSION BEAN NOT FOUND!"); }
注意
evaluateExpressionGet
方法是最常见的一种。
此示例被封装在名为 ch2_25
的应用程序中。
摘要
在 JSF 中,通信是其中一个最重要的方面,因为整个应用程序的流程都是围绕处理和共享 JSF 组件之间数据的能力展开的。正如你所看到的,有多种方式传递/获取参数以及从其他管理 Bean 访问管理 Bean,但选择正确的方法以获得稳健、和谐、平衡的应用程序取决于经验。本章涵盖了在 JSF 组件之间构建通信管道的广泛解决方案,但正如任何开发者都知道的,总会有需要新方法的情况!
欢迎在下一章中见到我们,我们将讨论 JSF 作用域。
第三章. JSF 作用域 – 在管理 Bean 通信中的生命周期和使用
如果编程是一种艺术,那么正确地使用作用域就是其中的一部分!
这个断言通常是正确的,不仅适用于 JSF。我现在应该使用会话作用域,还是请求作用域?我是否有太多的会话 Bean?我能将这个作用域注入到那个作用域中吗?这个会话对象是否太大?你问过自己这类问题有多少次了?我知道……很多次!也许在这一章中,你将找到一些问题的答案,并加强你对使用 JSF 作用域的知识。
我们有很多事情要做;因此,让我们简要概述一下你将在本章中看到的内容:
-
JSF 作用域与 CDI 作用域
-
请求作用域、会话作用域、视图作用域、应用作用域、会话作用域、流程作用域、无作用域、依赖作用域和自定义作用域
-
Bean 注入
JSF 作用域与 CDI 作用域
即使是 JSF 初学者也可能听说过 JSF 管理 Bean(由 JSF 管理的常规 JavaBean 类)和 CDI Bean(由 CDI 管理的常规 JavaBean 类),并且知道 JSF 支持 JSF 作用域和 CDI 作用域。从 Java EE 6 开始,CDI 被认可为管理 Bean 框架,除了 EJBs 之外。这导致程序员之间产生混淆,因为 EJBs、CDIs 和 JSF 管理 Bean 提出了一个关键问题:何时使用哪一个?
专注于 JSF,普遍的答案是 CDI Bean 比 JSF Bean 更强大。但是,当你从一开始就知道 CDI 不会成为你应用程序的一部分,或者你在一个没有默认 CDI 支持的服务器容器(如 Apache Tomcat)中运行应用程序时,那么 JSF Bean 就是正确的选择。换句话说,当你需要一个简单的方式来定义 Bean 和一个整洁的依赖注入机制时,JSF Bean 可以完成这项工作,但是当你需要重型武器,如事件、类型安全注入、自动注入、生产者方法和拦截器时,CDI 将代表完整的解决方案。
此外,NetBeans IDE 8.0 警告我们,在下一个 JSF 版本中,JSF Bean 的注解将被弃用,而 CDI Bean 被推荐使用(如以下截图所示)。这个警告和作为 CDI 依赖项引入的新 JSF 2.2 流程作用域,是 JSF 和 CDI 越来越接近的强大信号:
注意
CDI Bean 比 JSF Bean 更强大;因此,尽可能使用 CDI Bean。
因此,强有力的论据表明 CDI 通常是正确的选择,但仍然有一些情况下使用 JSF Bean 是有效的,正如你很快就会发现的。
JSF Bean 的主要注解(如@ManagedBean
和作用域注解)定义在javax.faces.bean
包中,而 CDI 的主要注解定义在javax.inject
(如@Named
)和javax.enterprise.context
(如作用域)包中。
JSF 管理 bean 使用@ManagedBean
注解,这允许我们将它注入到另一个 bean(不是 CDI bean!)中,并使用 EL 表达式从 JSF 页面访问 bean 属性和方法。CDI bean 使用@Named
注解,这为视图技术(如 JSP 或 Facelets)提供了一个 EL 名称。
通常,JSF bean 的声明如下所示:
package *package_name*;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.jsfScoped;
@ManagedBean
@*jsf*Scoped
public class JSFBeanName {
...
}
JSF bean 的@ManagedBean
支持一个可选参数name
。提供的名称可以用于以下方式从 JSF 页面引用 bean:
@ManagedBean(name="*custom name*")
CDI bean 具有相同的形状,但具有不同的注解,如下面的代码所示:
package *package_name*;
import javax.inject.Named;
import javax.enterprise.context.cdiScoped;
@Named
@*cdi*Scoped
public class CDIBeanName {
...
}
CDI bean 的@Named
注解支持一个可选参数value
。提供的名称可以用于以下方式从 JSF 页面引用 bean:
@Named(value="*custom name*")
注意
注意,CDI 注解不能与 JSF 注解在同一个 bean 中混合,只能在同一个应用程序中。例如,您不能使用@ManagedBean
和 CDI 作用域(或它们之间的任何其他组合)来定义一个 bean,但您可以在同一个应用程序中有一个管理 bean(或更多)和一个 CDI bean(或更多)。
在下面的图中,您可以看到 JSF 2.2 作用域的简要概述:
在下一节中,您将看到每个 JSF/CDI 作用域是如何工作的。
请求作用域
请求作用域绑定到 HTTP 请求-响应生命周期。
请求作用域在任何 Web 应用程序中都非常有用,定义在请求作用域中的对象通常具有较短的生存期;bean 的生存期与 HTTP 请求-响应相同。当容器从客户端接受 HTTP 请求时,指定的对象附加到请求作用域,并在容器完成向该请求发送响应后释放。每个新的 HTTP 请求都会带来一个新的请求作用域对象。简而言之,请求作用域代表用户在单个 HTTP 请求中与 Web 应用程序的交互。通常,请求作用域对于简单的 GET 请求很有用,这些请求向用户公开一些数据,而无需存储数据。
注意
请求作用域存在于 JSF 和 CDI 中,并且以相同的方式工作。它可以用于非富 AJAX 和非 AJAX 请求。对于 JSF 管理 bean(@ManagedBean
),这是默认的作用域,如果没有指定。
例如,假设我们有一个预定义的网球运动员列表,并且我们从这个列表中随机提取他们,并将他们存储在另一个列表中。当前生成的球员和提取的球员列表是管理 bean 的属性,它们的值在 JSF 页面中显示。
注意
请求作用域注解是@RequestScoped
,在 CDI 中定义在javax.enterprise.context
包中,在 JSF 中定义在javax.faces.bean
包中。
CDI bean 的代码可以写成如下:
@Named
@RequestScoped
public class PlayersBean {
final String[] players_list = {"Nadal, Rafael (ESP)","Djokovic, Novak (SRB)", "Ferrer, David (ESP)", "Murray, Andy (GBR)", "Del Potro, Juan Martin (ARG)"};
private ArrayList players = new ArrayList();
private String player;
//getters and setters
public void newPlayer() {
int nr = new Random().nextInt(4);
player = players_list[nr];
players.add(player);
}
}
JSF 页面的相关部分如下:
<h:body>
Just generated:
<h:outputText value="#{playersBean.player}"/><br/>
List of generated players:
<h:dataTable var="t" value="#{playersBean.players}">
<h:column>
<h:outputText value="#{t}"/>
</h:column>
</h:dataTable>
<h:form>
<h:commandButton value="Get Players In Same View" actionListener="#{playersBean.newPlayer()}"/>
<h:commandButton value="Get Players With Page Forward" actionListener="#{playersBean.newPlayer()}" action="index.xhtml"/>
<h:commandButton value="Get Players With Page Redirect" actionListener="#{playersBean.newPlayer()}" action="index.xhtml?faces-redirect=true;"/>
</h:form>
</h:body>
当您点击标记为使用页面前进获取玩家或获取同一视图中的玩家的按钮时,您将看到以下截图所示的内容:
由于请求作用域的生命周期与 HTTP 请求-响应的生命周期相同,而页面前进意味着一个单独的 HTTP 请求-响应,因此您将看到在当前请求中提取的播放器和提取的播放器列表,该列表将始终只包含此播放器。列表为每个请求创建,并用当前播放器填充,这使得列表变得无用。
注意
请求作用域在转发时不会丢失对象的状态,因为源页面和目标页面(转发页面)是同一请求-响应周期的一部分。这并不适用于重定向操作。
当您点击标记为使用页面重定向获取玩家的按钮时,您将看到以下截图所示的内容:
在这种情况下,当前提取的玩家和列表内容不可用,因为 JSF 重定向意味着两个请求,而不是像前进情况那样只有一个。
可以使用以下代码以编程方式访问请求映射:
FacesContext context = FacesContext.getCurrentInstance();
Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
将页面 1 中定义的表单通过豆提交到页面 2,然后您有以下情况:
-
如果使用相同的视图或前进,则数据将在页面 2 上可用以显示
-
如果使用重定向,则数据将丢失,无法在页面 2 上显示
CDI 豆的 JSF 版本如下:
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
@ManagedBean
@RequestScoped
public class PlayersBean {
...
}
它与 CDI 豆的功能相同!
注意
带有@PostConstruct
注解的方法将为每个请求调用,因为每个请求都需要请求作用域的独立实例。
CDI 豆的情况被封装在名为ch3_1_1
的应用程序中,而 JSF 豆的情况被封装在名为ch3_1_2
的应用程序中。
会话作用域
会话作用域跨越多个 HTTP 请求-响应周期(理论上无限)。
请求作用域在任何需要每个 HTTP 请求-响应周期中只有一个交互的 Web 应用程序中非常有用。然而,当您需要用户会话中属于任何 HTTP 请求-响应周期的可见对象时,则需要会话作用域;在这种情况下,该豆的生命周期与 HTTP 会话的生命周期相同。会话作用域允许您创建并将对象绑定到会话。它会在会话中涉及此豆的第一个 HTTP 请求时创建,并在 HTTP 会话无效时销毁。
注意
会话作用域存在于 JSF 和 CDI 中,并且在两者中功能相同。通常,它用于处理用户特定数据(如凭证、购物车等)的 AJAX 和非 AJAX 请求。
因此,第一个 HTTP 请求初始化会话并存储对象,而后续请求可以访问这些对象以执行进一步的任务。当浏览器关闭、超时触发、点击注销或程序性子例程强制执行时发生会话失效。通常,每次需要在整个会话(多个请求和页面)中保留数据时,会话作用域是正确的选择。
例如,你可以将会话作用域添加到本章之前的应用程序中,以存储跨多个请求随机提取的玩家列表。
注意
会话作用域注解是@SessionScoped
,在 CDI 中定义在javax.enterprise.context
包中,在 JSF 中定义在javax.faces.bean
包中。
CDI bean 修改如下:
import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
@Named
@SessionScoped
public class PlayersBean implements Serializable{
...
}
或者,JSF 版本如下:
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
@ManagedBean
@SessionScoped
public class PlayersBean implements Serializable{
...
}
注意
注意到会话作用域 bean 可能会被容器钝化,并且应该能够通过实现java.io.Serializable
接口来钝化;参考将会话数据从硬盘上持久化/恢复的能力。
会话对象在转发和重定向机制中保持有效。在下面的屏幕截图中,你可以看到当前提取的玩家和属于同一会话的几个请求后的提取玩家列表:
现在列表不再是无用的了!你可以添加操作其内容的方法,例如排序或删除。
可以按以下方式编程访问会话映射:
FacesContext context = FacesContext.getCurrentInstance();
Map<String, Object> sessionMap = context.getExternalContext().getSessionMap();
此外,你可以按以下方式使会话无效:
FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
显然,通过会话作用域提交的数据将在后续请求中可用。
注意
带有@PostConstruct
注解的方法将在会话期间仅调用一次,当会话 bean 实例化时。后续请求将使用此实例,因此它是一个添加初始化内容的好地方。
CDI bean 的情况被封装在名为ch3_2_1
的应用中,而 JSF bean 的情况被封装在名为ch3_2_2
的应用中。
视图作用域
视图作用域在你在浏览器窗口/标签中导航同一 JSF 视图时保持有效。
当你需要在不点击链接、返回不同的操作结果或任何其他导致当前视图被丢弃的交互中保留数据时,视图作用域非常有用。它在 HTTP 请求时创建,在你回发到不同的视图时销毁;只要回发到相同的视图,视图作用域就保持活跃。
注意
注意到视图作用域 bean 可能会被容器钝化,并且应该通过实现java.io.Serializable
接口来钝化。
由于视图作用域在您在相同视图中编辑某些对象时特别有用,因此它对于丰富的 AJAX 请求来说是一个完美的选择。此外,由于视图作用域绑定到当前视图,它不会反映浏览器中另一个窗口或标签页中存储的信息;这是一个特定于会话作用域的问题!
注意
为了保持视图活跃,bean 方法(动作/监听器)必须返回 null
或 void
。
在 CDI 中不可用视图作用域,但 JSF 2.2 通过新的注解 @ViewScoped
引入了它。这定义在 javax.faces.view.ViewScoped
包中,并且它与 CDI 兼容。不要将此 @ViewScoped
与定义在 javax.faces.bean
包中的那个混淆,后者是 JSF 兼容的!
注意
视图作用域注解是 @ViewScoped
,它定义在 javax.faces.view
包中用于 CDI,以及定义在 javax.faces.bean
包中用于 JSF。
您可以通过修改 PlayersBean
的作用域来查看视图作用域的实际效果,如下所示:
import java.io.Serializable;
import javax.faces.view.ViewScoped;
import javax.inject.Named;
@Named
@ViewScoped
public class PlayersBean implements Serializable{
...
}
通过点击标记为 Get Players In Same View 的按钮来触发多个 HTTP 请求将揭示如下截图所示的内容。注意,动作方法 (newPlayer
) 返回 void,并且按钮不包含 action
属性,这意味着在执行这些请求期间,您处于相同的 JSF 视图中。
其他两个按钮包含 action
属性,并指示显式导航,这意味着在每次请求时都会更改当前视图,并且数据会丢失。
您可以轻松地将 PlayersBean
(以及任何其他 bean)适配以使用 @ViewScoped
的 JSF 版本,如下所示:
import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
@ManagedBean
@ViewScoped
public class PlayersBean implements Serializable{
...
}
通过视图作用域提交的表单数据,只要您处于同一视图,就会在后续请求中可用。
注意
带有 @PostConstruct
注解的方法仅在视图作用域的 bean 实例化时被调用。后续从这个视图发出的请求将使用此实例。只要您处于同一视图,此方法就不会再次被调用;因此,它是一个添加特定于当前视图的初始化内容的良好位置。
CDI bean 的案例被封装在名为 ch3_6_1
的应用程序中,而 JSF bean 的案例被封装在名为 ch3_6_2
的应用程序中。
注意
从 JSF 2.2 版本开始,我们可以使用 UIViewRoot.restoreViewScopeState(FacesContext context, Object state)
方法来恢复不可用的视图作用域。这将在第十二章 Facelets 模板中举例说明。
应用程序作用域
应用程序作用域与 Web 应用程序的生命周期一样长。
应用范围通过所有用户与 Web 应用程序交互的共享状态扩展了会话范围;此范围与 Web 应用程序的生命周期相同。由于应用范围的 bean 在应用程序关闭(或它们被编程删除)之前一直存在,因此我们可以说这个范围存在时间最长。更精确地说,位于应用范围的对象可以从应用程序的任何页面访问(例如,JSF、JSP 和 XHTML)。
注意
应仅将应用范围用于可以安全共享的数据。由于应用范围的 bean 被所有用户共享,您需要确保该 bean 具有不可变状态,或者您需要同步访问。
通常,应用范围对象用作计数器,但它们可以用于许多其他任务,例如初始化和导航。例如,应用范围可以用来统计在线用户数量,或者将此信息与所有用户共享。实际上,它可以用于在所有会话之间共享数据,例如常量、通用设置和跟踪变量。
注意
应用范围注解是@ApplicationScoped
,在 CDI 中定义在javax.enterprise.context
包中,在 JSF 中定义在javax.faces.bean
包中。
如果将PlayersBean
托管 bean 放入应用范围,那么随机抽取的玩家列表将在所有会话中可用。您可以通过以下代码实现:
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
@Named
@ApplicationScoped
public class PlayersBean {
...
}
JSF 版本在以下代码中显示:
import javax.faces.bean.ApplicationScoped;
import javax.faces.bean.ManagedBean;
@ManagedBean
@ApplicationScoped
public class PlayersBean {
...
}
为了测试应用范围,您需要打开多个浏览器或使用多台机器。
在从应用范围的 bean 向多个会话 bean(例如,使用注入)提供数据时,请务必小心,因为所有会话共享的数据可以由每个会话单独修改。这可能导致多个用户之间的数据不一致;因此,请确保暴露的应用数据在会话中不会被修改。
注意
CDI bean 的案例被包裹在名为ch3_3_1
的应用中,而 JSF bean 的案例被包裹在名为ch3_3_2
的应用中。
可以使用以下代码以编程方式访问应用映射:
FacesContext context = FacesContext.getCurrentInstance();
Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
带有@PostConstruct
注解的方法仅在应用范围的 bean 实例化时调用。后续请求将使用此实例。通常,这发生在应用程序启动时;因此,请将此 bean 上下文中的应用特定初始化任务放在此方法内部。
会话范围
会话范围允许开发者界定会话范围的生命周期。
对话作用域致力于用户与 JSF 应用程序的交互,并代表用户视角上的工作单元;在这个作用域中的 Bean 能够跟踪与用户的对话。我们可以将对话作用域视为开发者控制的会话作用域,跨越 JSF 生命周期的多次调用;而会话作用域跨越无限请求,对话作用域仅跨越有限数量的请求。
注意
对话作用域 Bean 可能会被容器钝化,并且应该通过实现 java.io.Serializable
接口来具备钝化能力。
开发者可以显式设置对话作用域的边界,并且可以根据业务逻辑流程启动、停止或传播对话作用域。所有长时间运行的对话都作用域于特定的 HTTP 服务器会话,并且可能不会跨越会话边界。此外,对话作用域在 JSF 应用程序中保持与特定 Web 浏览器窗口/标签页相关的状态。
注意
对话作用域注解是 @ConversationScoped
,它定义在 javax.enterprise.context
包中用于 CDI。这个作用域在 JSF 中不可用!
处理对话作用域与其他作用域略有不同。首先,您使用 @ConversationScope
标记 Bean,由 javax.enterprise.context.ConversationScoped
类表示。其次,CDI 提供了一个内置的 Bean(javax.enterprise.context.Conversation
),用于控制 JSF 应用程序中对话的生命周期——其主要职责是管理对话上下文。此 Bean 可以通过注入获得,如下面的代码所示:
private @Inject Conversation conversation;
默认情况下,Conversation
对象处于短暂状态,应该通过调用 begin
方法将其转换为长时间运行的对话。您还需要通过调用 end
方法为对话的销毁做好准备。
注意
如果在对话处于活动状态时尝试调用 begin
方法,或者在对话处于非活动状态时调用 end
方法,将会抛出 IllegalStateException
。我们可以通过使用名为 isTransient
的方法来避免这种情况,该方法返回一个布尔值,用于测试 Conversation
对象的传递性状态。
现在,将 begin
、end
和 isTransient
方法一起添加到以下对话中:
-
要开始对话,代码如下:
if (conversation.isTransient()) { conversation.begin(); }
-
要停止对话,代码如下:
if (!conversation.isTransient()) { conversation.end(); }
例如,您可以在 PlayersBean
中添加对话作用域如下:
@Named
@ConversationScoped
public class PlayersBean implements Serializable {
private @Inject
Conversation conversation;
final String[] players_list = {"Nadal, Rafael (ESP)","Djokovic, Novak (SRB)", "Ferrer, David (ESP)", "Murray, Andy (GBR)", "Del Potro, Juan Martin (ARG)"};
private ArrayList players = new ArrayList();
private String player;
public PlayersBean() {
}
//getters and setters
public void newPlayer() {
int nr = new Random().nextInt(4);
player = players_list[nr];
players.add(player);
}
public void startPlayerRnd() {
if (conversation.isTransient()) {
conversation.begin();
}
}
public void stopPlayerRnd() {
if (!conversation.isTransient()) {
conversation.end();
}
}
}
除了注入内置的 CDI Bean,请注意您已经定义了一个方法(startPlayerRnd
)用于标记对话的起始点,以及另一个方法(stopPlayerRnd
)用于标记对话的结束点。在这个例子中,这两个方法都通过两个按钮暴露给用户,但您也可以通过有条件地调用它们来程序化地控制对话。
在对话中运行示例将揭示如下截图所示的内容:
随机抽取的玩家列表将保持为空或仅包含当前抽取的玩家,直到点击标记为开始对话的按钮。在此刻,列表将被存储在会话中,直到点击标记为停止对话的按钮。
注意
在对话过程中,用户可以对 bean 执行 AJAX/非 AJAX 请求或导航到其他仍引用此相同管理 bean 的页面。bean 将使用容器生成的对话标识符在用户交互中保持其状态,这就是为什么在需要实现向导时,对话范围可能是正确的选择。但是,考虑新的 JSF 2.2 流程范围也是一个好主意,因为它解决了对话范围的几个缺陷。请参阅即将到来的部分!
在此示例中,对话上下文会自动与任何 JSF faces 请求或重定向一起传播(这有助于实现常见的 POST-then-redirect 模式),但它不会自动与非 faces 请求(如链接)一起传播。在这种情况下,您需要将对话的唯一标识符作为请求参数包含在内。CDI 规范为这种用途保留了请求参数cid
。以下代码将通过链接传播对话上下文:
<h:link outcome="/link.xhtml" value="Conversation Propagation">
<f:param name="cid" value="#{conversation.id}"/>
</h:link>
注意
带有@PostConstruct
注解的方法将在每次请求中调用,只要 bean 不参与对话。当对话开始时,该方法会为该实例调用,后续请求将使用此实例,直到对话结束。因此,请小心管理此方法的内容。
此示例被封装在名为ch3_4
的应用程序中,并可在本章的代码包中找到。
流程范围
流程范围允许开发者对页面/视图进行分组,并通过入口/出口点来划分该组。
在请求范围和会话范围之间,我们有 CDI 流程范围。这个范围在 Spring Web Flow 或 ADF 流程中存在一段时间,现在在 JSF 2.2 中也可用。基本上,流程范围允许我们通过一个入口点(称为起始节点)和一个出口点(称为返回节点)来划分一组相关的页面/视图(通常,逻辑相关)。
注意
流程范围是包含向导的应用程序的好选择,例如多屏幕订阅/注册、预订和购物车。一般来说,任何具有逻辑起点和终点的应用程序块都可以封装到流程范围中。
在同一应用程序中,我们可以定义多个流,这些流可以被视为可重用且能够通信的模块。它们可以按顺序调用,可以封装成玛莉亚莎娃娃或创建任何自定义设计。此外,通过仅插入/拔出入口和出口点,就可以非常容易地将流移动、删除或添加到这样的应用程序中。
要了解使用流作用域的好处,您必须识别一些不使用它的应用程序的缺点。如下列所示:
-
每个应用程序都是一个大型流,但通常页面并不遵循任何直观的逻辑设计。显然,即使页面在逻辑上相关,如向导或购物车页面,也由无序的顺序来管理。
注意
流作用域允许我们定义逻辑工作单元。
-
重复使用页面可能是一项艰巨的任务,因为页面与 UI 组件和用户交互紧密相连。
注意
流作用域提供了可重用性。
-
CDI 提供了能够跨越多个页面的会话作用域,但流作用域更适合 JSF。
-
作为会话作用域,流作用域覆盖了一组页面/视图,但它有几个主要优点,例如它更加灵活,不需要笨拙的开始/结束操作,流作用域的 bean 在用户进入或退出流时自动创建和销毁,提供了易于使用的对入站/出站参数、预处理程序和后处理程序的支持,并且预处理器和后处理器。由于信息在页面之间通过会话作用域传输,因此普通的流不能在多个窗口/标签页中打开。
注意
流中的数据仅限于该流本身;因此,可以在多个窗口/标签页中打开流。
-
节点定义了流的入口和出口点,并且有五种类型的节点,如下列所示:
-
视图:这代表应用程序中参与流的任何 JSF 页面。它被称为流的视图节点。
-
方法调用:这表示使用 EL 调用方法。被调用的方法可能返回一个结果,指示下一个应导航的节点。
-
切换:
switch
情况语句是长if
语句的替代品。情况由 EL 表达式表示,并评估为布尔值。每个情况都伴随着一个结果,当条件评估为true
时将使用该结果。还有一个默认结果,当所有情况都评估为false
时将使用。 -
流调用:这用于在当前流中调用另一个流——这些是流之间的转换点。被调用的流(称为内部或嵌套流)嵌套在调用它的流中(称为调用流或外部流)。当嵌套流完成其任务后,它将从调用流返回一个视图节点,这意味着调用流只有在嵌套流的生存周期结束时才会获得控制权。
-
流返回:这可以用于将结果返回给调用流。
-
流可以传递参数从一个传递到另一个。一个流发送给另一个流的参数被称为出站参数,而一个流从另一个流接收到的参数被称为入站参数。
嗯,在这个时候,你应该已经拥有了足够的信息来开发一些示例。但在这样做之前,你需要注意一些标签、注解和约定。
流定义基于配置的一组约定。一个流有一个名称,一个在应用程序 Web 根目录中反映流名称的文件夹,以及一个表示起始节点且也反映流名称的视图。此文件夹将属于同一流的页面/视图分组。
为了使用一个流,你需要完成一些配置任务。这些任务可以通过配置文件或编程方式完成。如果你选择第一种方法,那么配置文件可以限制为一个流,这意味着它存储在流文件夹中,并以flowname-flow.xml
的格式命名,或者你可以使用faces-config.xml
文件将所有流放在一个地方。
由于我们的第一个示例使用配置文件,我们需要使用标签。用于配置流的标签如下:
-
< flow-definition>
:此标签包含一个id
属性,该属性唯一标识流。此 ID 的值是用于从 JSF 页面或 bean 引用流的流名称。 -
<view>
:它嵌套在<flow-definition>
标签中,表示代表流节点的 JSF 页面;它为每个页面(Facelet)路径关联一个显式 ID(进一步,你可以通过其 ID 引用每个页面)。页面路径在<view>
标签中映射,该标签嵌套在<view>
标签内。此标签的存在是可选的,但按照惯例,至少应该有一个<view>
标签表示起始节点(起始页面),特别是如果你想设置除了默认的起始节点之外的另一个起始节点,该节点由与流具有相同名称(ID)的页面表示。此外,你可以使用可选的<start-node>
ID</start-node>
标签来指示映射自定义起始页面的<view>
标签的 ID。作为替代,流起始节点可以通过将<view>
标签的id
属性值设置为流 ID,并将封装的<vdl-document>
标签的内容作为自定义起始页面的路径来指示。当你引用流 ID 时,JSF 将转到该页面并自动将你放入流中。 -
<flow-return>
:它嵌套在<flow-definition>
标签中,并将结果返回给调用流。你可以通过id
属性的值来引用它。至少有三种退出流的方法:使用<flow-return>
,使用稍后介绍的<flow-call>
,或者放弃流。
注意
我们刚才说过,一个流程通过一个 ID(一个名称)来识别。但是,当相同的流程名称在多个文档中定义(如在大型项目中使用来自不同供应商的多个打包流程),还需要一个额外的 ID。这个 ID 被称为文档 ID。因此,当你需要识别一个名称在不同文档中出现的流程时,我们需要流程 ID 和定义文档 ID。大多数情况下,文档 ID 被省略;因此,本节中没有演示。在本节中,你将看到关于它的几个提示。
为了定义最简单的流程,你需要了解以下图表:
简单的流程
使用这三个标签,<start-node>
和/或<view>
,<flow-return>
,和<from-outcome>
,你可以配置一个简单的流程,比如一个跑步注册表单。假设一个网球运动员通过由两个 JSF 页面组成的流程在线注册一个锦标赛(流程名称将为registration
):一个用于收集数据的表单页面和一个确认页面。此外,还将有两个页面在流程之外,一个用于进入流程(如网站的第一个页面),另一个在确认后调用。
在下面的图中,你可以看到我们流程的图像:
让我们看看流程之外、注册文件夹之外的第一页的代码(index.xhtml
)如下:
<h:body>
<h1><b>In flow ?
#{null != facesContext.application.flowHandler.currentFlow}
</b></h1><br/><br/>
Flow Id: #{facesContext.application.flowHandler.currentFlow.id}
REGISTER NEW PLAYER
<h:form>
<h:commandButton value="Start Registration" action="registration" immediate="true"/>
</h:form>
</h:body>
这里可以观察到两个重要的事情。首先,以下几行:
#{null != facesContext.application.flowHandler.currentFlow}
#{facesContext.application.flowHandler.currentFlow.id}
第一行返回一个布尔值,指示当前页面是否在流程中。显然,index.xhtml
页面不在流程中;因此,将返回false
。你可以用它进行测试。第二行显示当前流程的 ID。
此外,你需要查看<h:commandButton>
标签的action
属性的值。这个值是我们流程的名称(ID);在窗口上下文启用后,JSF 将搜索指定的流程并导航到流程的起始节点。默认情况下,窗口上下文是禁用的。
因此,当点击标记为开始注册的按钮时,应用程序进入注册流程并加载由registration.xhtml
页面表示的起始节点页面。此页面的代码如下:
<h:body>
<h1><b>First page in the 'registration' flow</b></h1>
<h1><b>In flow ?
#{null != facesContext.application.flowHandler.currentFlow}
</b></h1><br/><br/>
You are registered as:#{flowScope.value}
<h:form prependId="false">
Name & Surname:
<h:inputText id="nameSurnameId" value="#{flowScope.value}" />
<h:commandButton value="Register To Tournament" action="confirm"/>
<h:commandButton value="Back(exit flow)" action="taskFlowReturnIndex"/>
</h:form>
</h:body>
由于我们处于流程中,currentFlow
将返回true
。
更重要的是关注隐式对象flowScope
;然而,正如您从第一章动态访问 JSF 应用程序数据")中了解到的,通过表达式语言(EL 3.0)动态访问 JSF 应用程序数据,flowScope
隐式对象(表示当前流程)用于在整个流程中共享数据,并映射到facesContext.getApplication().getFlowHandler().getCurrentFlowScope()
。例如,<h:inputText>
标签的值可以放入flowScope
对象中,并且可以在下一页的流程作用域中读取,如下所示:
#{flowScope.value}
标有注册到锦标赛的按钮导航到流程中的第二页confirm.xhtml
;这是一个常见的导航情况,这里没有太多可说的。但另一个按钮通过指示流程返回的 ID 导航到流程外部(到index.xhtml
)。在配置文件中,此流程返回的代码如下:
<flow-return id="taskFlowReturnIndex">
<from-outcome>/index</from-outcome>
</flow-return>
confirm.xhtml
页面的代码如下:
<h:body>
<h1><b>Second page in the 'registration' flow</b></h1>
<h1><b>In flow ?
#{null != facesContext.application.flowHandler.currentFlow}
</b></h1><br/><br/>
You are registered as:#{flowScope.value}
<h:form prependId="false">
<h:commandButton value="Back (still in flow)" action="registration"/>
<h:commandButton value="Next (exit flow)" action="taskFlowReturnDone"/>
</h:form>
</h:body>
此页面显示已输入并存储在流程作用域中的数据,以及两个按钮。第一个按钮导航回registration.xhtml
页面,而另一个按钮导航到流程外部的done.xhtml
页面。流程返回通过 ID 识别,如下面的代码所示:
<flow-return id="taskFlowReturnDone">
<from-outcome>/done</from-outcome>
</flow-return>
done.xhtml
页面只是检查页面是否在流程中,并显示一条简单消息,如下面的代码所示:
<h:body>
<h1><b>In flow ?
#{null != facesContext.application.flowHandler.currentFlow}
</b></h1><br/><br/>
REGISTER NEW PLAYER ENDED
</h:body>
最后一步是在配置文件中定义流程。由于您只有一个流程,您可以在registration
文件夹中创建一个名为registration-flow.xml
的文件。以下为registration-flow.xml
文件的代码:
<faces-config version="2.2"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
<flow-definition id="registration">
<view id="registration">
<vdl-document>/registration/registration.xhtml</vdl-document>
</view>
<flow-return id="taskFlowReturnIndex">
<from-outcome>/index</from-outcome>
</flow-return>
<flow-return id="taskFlowReturnDone">
<from-outcome>/done</from-outcome>
</flow-return>
</flow-definition>
</faces-config>
您也可以将以下代码放置在faces-config.xml
文件中的<faces-flow-definition>
标签内:
<faces-flow-definition>
<flow-definition id="registration">
...
</faces-flow-definition>
此示例被封装在名为ch3_7_1
的应用程序中,该应用程序位于本章代码包中。
带有 Bean 的流程
除了页面外,一个流程还可以包含 Bean。在流程中定义的 Bean 被注解为@FlowScoped
;这是一个 CDI 注解,它允许自动激活(当作用域进入时)和钝化(当作用域退出时)。@FlowScoped
Bean 需要一个名为value
的属性,它包含流程 ID。存储在这样的 Bean 中的数据可以在属于该流程的所有页面上访问。
注意
流程作用域 Bean 可能会被容器钝化,并且应该能够通过实现java.io.Serializable
接口来支持钝化。
在注册流程中添加 Bean 可以修改初始图,如下面的图所示:
如您所见,该 Bean 会将从注册表单收集的数据存储在流程作用域中(在先前的示例中,这些数据是通过flowScope
隐式对象传递的)。标有“注册参加锦标赛”的按钮将调用registrationAction
Bean 方法,该方法将决定数据是否有效,并将流程返回到registration.xhtml
页面或confirm.xhtml
页面。
registration.xhtml
页面的代码修改如下:
<h:body>
<h1><b>First page in the 'registration' flow</b></h1>
<h1><b>In flow ?
#{null != facesContext.application.flowHandler.currentFlow}
</b></h1><br/><br/>
Your registration last credentials:
#{registrationBean.playerName} #{registrationBean.playerSurname}
<h:form prependId="false">
Name: <h:inputText value="#{registrationBean.playerName}"/>
Surname: <h:inputText value="#{registrationBean.playerSurname}"/>
<h:commandButton value="Register To Tournament" action="#{registrationBean.registrationAction()}"/>
<h:commandButton value="Back (exit flow)" action="taskFlowReturnIndex"/>
</h:form>
</h:body>
RegistrationBean
的代码如下:
@Named
@FlowScoped(value="registration")
public class RegistrationBean implements Serializable {
private String playerName;
private String playerSurname;
...
//getters and setters
...
public String getReturnValue() {
return "/done";
}
public String registrationAction(){
//simulate some registration conditions
Random r= new Random();
int nr = r.nextInt(10);
if(nr < 5){
playerName="";
playerSurname="";
FacesContext.getCurrentInstance().addMessage("password",
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Registration failed!",""));
return "registration";
} else {
return "confirm";
}
}
}
代码是自我解释的,但关于getReturnValue
方法呢?这是一个流程作用域 Bean 如何指示流程返回结果的示例。而不是使用以下代码:
<flow-return id="taskFlowReturnDone">
<from-outcome>/done</from-outcome>
</flow-return>
您可以使用以下代码:
<flow-return id="taskFlowReturnDone">
<from-outcome>#{registrationBean.returnValue}</from-outcome>
</flow-return>
此示例被封装在名为ch3_7_2
的应用程序中,该应用程序可在本章的代码包中找到。
嵌套流程
好吧,现在让我们通过在现有流程下添加另一个流程来使事情变得复杂。假设在注册后,玩家必须指出他可以参加第一场比赛的日期和时间。这可以通过一个名为schedule
的新流程来完成。registration
流程将调用schedule
流程,并向其传递一些参数。schedule
流程将返回到registration
流程,该流程将提供一个简单的按钮用于导航出registration
流程。
注意
嵌套流程只返回到调用流程。您必须在嵌套流程的<flow-return>
标签中引用调用流程的页面,包括调用流程返回的页面。
传递参数需要在配置标签中使用更多标签。因此,您需要了解以下标签:
-
<flow-call>
:在当前流程中调用另一个流程。此标签需要id
属性。此属性的值将用于引用此流程调用。 -
<flow-reference>
:这是嵌套在<flow-call>
标签中,并包含必须调用的流程的 ID。 -
<outbound-parameter>
:这是嵌套在<flow-call>
标签中,并定义了必须传递给被调用流程的参数。 -
<inbound-parameter>
:这定义了从另一个流程传递的参数。
为了看到这些标签在起作用,您需要查看应用程序流程。应用程序的图表将按以下方式更改:
我们从confirm.xhtml
页面(在registration
流程中定义)继续我们的讨论。从这个页面,我们想要导航到schedule.xhtml
页面,该页面在schedule
流程(schedule
文件夹)中可用。为此,我们可以添加一个新按钮,标有安排,如下面的代码所示:
<h:form prependId="false">
<h:commandButton value="Back (still in flow)" action="registration"/>
<h:commandButton id="Next" value="Schedule" action="callSchedule" />
<h:commandButton value="Next (exit flow)" action="taskFlowReturnDone"/>
</h:form>
按钮的action
属性值是<flow-call>
标签的 ID。当按钮被点击时,JSF 定位到相应的<flow-call>
标签,并跟随由<flow-id>
标签指示的流程,如下面的代码所示:
<flow-call id="callSchedule">
<flow-reference>
<flow-id>schedule</flow-id>
</flow-reference>
...
</flow-call>
此外,我们希望从 registration
流程传递几个参数到 schedule
流程:玩家姓名和姓氏(存储在流程作用域的 RegistrationBean
对象中)以及代表某些注册代码的常量(也可以根据某些规则生成)。这可以通过 <outbound-parameter>
标签实现,如下面的代码所示:
<flow-call id="callSchedule">
<flow-reference>
<flow-id>schedule</flow-id>
</flow-reference>
<outbound-parameter>
<name>playernameparam</name>
<value>#{registrationBean.playerName}</value>
</outbound-parameter>
<outbound-parameter>
<name>playersurnameparam</name>
<value>#{registrationBean.playerSurname}</value>
</outbound-parameter>
<outbound-parameter>
<name>playerregistrationcode</name>
<value>349CF0YO122</value>
</outbound-parameter>
</flow-call>
schedule.xhtml
页面根据接收到的参数显示一条问候信息,并允许玩家输入他可用于参加第一场比赛的日期和小时,如下面的代码所示:
<h:body>
<h1><b>First page in the 'schedule' flow</b></h1>
<h1><b>In flow ?
#{null != facesContext.application.flowHandler.currentFlow}
</b></h1><br/><br/>
Hello, #{flowScope.name} #{flowScope.surname} (#{scheduleBean.regcode})
<h:form prependId="false">
Day: <h:inputText value="#{scheduleBean.day}"/>
Starting At Hour: <h:inputText value="#{scheduleBean.hourstart}"/>
<h:commandButton value="Save" action="success"/>
</h:form>
</h:body>
注意,姓名和姓氏是通过 flowScope
对象从流程作用域中获得的,而注册代码是从流程作用域的 ScheduleBean
中获得的;这个对象存储了日期、小时(从玩家那里接收)和注册代码(从 registration
流程接收)。从注册 Bean 接收到的每条信息都使用 schedule-flow.xml
文件中的 <inbound-parameter>
标签引导到存储位置,如下面的代码所示:
<flow-definition id="schedule">
<view id="schedule">
<vdl-document>/schedule/schedule.xhtml</vdl-document>
</view>
<inbound-parameter>
<name>playernameparam</name>
<value>#{flowScope.name}</value>
</inbound-parameter>
<inbound-parameter>
<name>playersurnameparam</name>
<value>#{flowScope.surname}</value>
</inbound-parameter>
<inbound-parameter>
<name>playerregistrationcode</name>
<value>#{scheduleBean.regcode}</value>
</inbound-parameter>
</flow-definition>
在日期和小时插入后,标记为 保存 的按钮应该保存数据并导航到 success.xhtml
页面,这是一个显示玩家提供所有数据的简单页面。从该页面,我们可以通过一个标记为 退出注册 的简单按钮返回到调用流程 registration
,如下面的代码所示:
<h:body>
<h1><b>Second page in the 'schedule' flow</b></h1>
<h1><b>In flow ?
#{null != facesContext.application.flowHandler.currentFlow}
</b></h1><br/><br/>
You are registered as
#{flowScope.name} #{flowScope.surname} (#{scheduleBean.regcode})
You will play first match
#{scheduleBean.day} after #{scheduleBean.hourstart}
<h:button value="Exit Registration" outcome="taskFlowReturnThanks"/>
</h:body>
结果 taskFlowReturnThanks
在 schedule-flow.xml
文件中定义如下:
<flow-return id="taskFlowReturnThanks">
<from-outcome>/registration/thanks.xhtml</from-outcome>
</flow-return>
thanks.xhtml
页面是用户从 registration
流程退出之前的最后一步,如下面的代码所示:
<h:body>
<h1><b>Third page in the 'registration' flow</b></h1>
<h1><b>In flow ? #{null != facesContext.application.flowHandler.currentFlow}</b></h1><br/><br/>
Thanks for your patience, Mr :#{registrationBean.playerName}
#{registrationBean.playerSurname}<br/>
<b>We wish you beautiful games!</b><br/><br/>
<h:button value="Bye Bye, #{registrationBean.playerSurname}" outcome="taskFlowReturnDone"/>
</h:body>
如果你想跳过 thanks.xhtml
页面,直接从两个流程外部退出,那么你可以定义流程返回,taskFlowReturnThanks
,指向 done.xhtml
页面,这是由调用流程通过 taskFlowReturnDone
流程返回返回的。因此,我们可以使用以下代码:
<flow-return id="taskFlowReturnThanks">
<from-outcome>taskFlowReturnDone</from-outcome>
</flow-return>
这个示例被包含在名为 ch3_7_3
的应用程序中,该应用程序位于本章代码包中。
注意
流程可以使用 JSF 2.2 FlowBuilder
API 声明性或编程地配置。
编程配置流程
在所有之前的示例中,你看到了如何使用声明性方法配置流程。但是,流程也可以通过编程方式配置。编程配置流程的步骤如下:
-
创建一个类并将其命名为流程。这更像是一种约定,而不是要求!
-
在这个类中,编写一个如下所示的代码方法;
@FlowDefinition
注解是一个类级别注解,它允许使用FlowBuilder
API 定义流程定义。这个方法的名称可以是任何有效的名称,但defineFlow
似乎是一种约定。所以,名称defineFlow
不是强制的,你甚至可以在同一个类中定义更多的流程,只要正确地注解了它们。@Produces @FlowDefinition public Flow defineFlow(@FlowBuilderParameter FlowBuilder flowBuilder) { ... }
-
使用
FlowBuilder
API 配置流程。
使用 FlowBuilder
API 非常简单直观。例如,你可以按如下方式程序性地编写 registration-flow.xml
文件:
public class Registration implements Serializable {
@Produces
@FlowDefinition
public Flow defineFlow(@FlowBuilderParameter FlowBuilder flowBuilder) {
String flowId = "registration";
flowBuilder.id("", flowId);
flowBuilder.viewNode(flowId, "/" + flowId + "/" + flowId + ".xhtml").markAsStartNode();
flowBuilder.viewNode("confirm-id", "/" + flowId + "/confirm.xhtml");
flowBuilder.viewNode("thanks-id", "/" + flowId + "/thanks.xhtml");
flowBuilder.returnNode("taskFlowReturnIndex").fromOutcome("/index");
flowBuilder.returnNode("taskFlowReturnDone").fromOutcome("#{registrationBean.returnValue}");
flowBuilder.flowCallNode("callSchedule").flowReference("", "schedule").outboundParameter("playernameparam", "#{registrationBean.playerName}"). outboundParameter("playersurnameparam", "#{registrationBean.playerSurname}").outboundParameter("playerregistrationcode", "349CF0YO122");
return flowBuilder.getFlow();
}
}
如您所见,对于声明性方法中使用的每个标签,FlowBuilder
API 中都有一个对应的方法。例如,flowBuilder.id
方法接受两个参数:第一个参数表示文档 ID(通常为空格),第二个参数表示流程 ID。
schedule-flow.xml
文件可以按如下所示程序性地转换:
public class Schedule implements Serializable {
@Produces
@FlowDefinition
public Flow defineFlow(@FlowBuilderParameter FlowBuilder flowBuilder) {
String flowId = "schedule";
flowBuilder.id("", flowId);
flowBuilder.viewNode(flowId, "/" + flowId + "/" + flowId + ".xhtml").markAsStartNode();
flowBuilder.viewNode("success-id", "/" + flowId + "/success.xhtml");
flowBuilder.returnNode("taskFlowReturnThanks").fromOutcome("/registration/thanks.xhtml");
flowBuilder.inboundParameter("playernameparam", "#{flowScope.name}");
flowBuilder.inboundParameter("playersurnameparam", "#{flowScope.surname}");
flowBuilder.inboundParameter("playerregistrationcode", "#{scheduleBean.regcode}");
return flowBuilder.getFlow();
}
}
注意
被注解为 @PostConstruct
的方法将在应用程序进入当前流程并实例化流程作用域的 bean 时被调用,而后续请求将使用此实例,直到流程被丢弃。如果应用程序再次进入此流程,此操作会重复。因此,可以在此处放置针对当前流程的特定初始化。
此示例被封装在名为 ch3_7_5
的应用程序中,该应用程序位于本章的代码包中。
在同一个应用程序中可以混合声明性和程序性配置。例如,检查名为 ch3_7_4
的应用程序,它为 registration
流程使用程序性配置,为 schedule
流程使用声明性配置。
流程和导航案例
导航案例可以用于在流程内进行导航。在此刻,当您点击标记为 注册到锦标赛 的按钮时,流程将基于隐式导航进入 confirm.xhtml
页面。但我们可以通过替换 action
属性的值轻松地展示流程中的显式导航:
<h:commandButton value="Register To Tournament" action="confirm_outcome"/>
现在,confirm_outcome
不能自动获取到 confirm.xhtml
页面;因此,在 registration-flow.xml
文件中,我们可以添加一个显式的导航案例,如下所示:
<navigation-rule>
<from-view-id>/registration/registration.xhtml</from-view-id>
<navigation-case>
<from-outcome>confirm_outcome</from-outcome>
<to-view-id>/registration/confirm.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
注意
当您需要使用导航案例进入流程时,您必须在 <navigation-case>
标签内指定 <to-flow-document-id>
document_ID</to-flow-document-id>
语句。如果没有文档 ID,则使用 <to-flow-document-id/>
。此外,可以使用 <h:button>
(或 <h:link>
)进入此类流程,如下所示:
<h:button id="..." value="*enter flow*" outcome="*flow*">
<f:attribute name="to-flow-document-id" value="unique"/>
</h:button>
如果您选择编写程序性导航案例,那么 JSF 2.2 带有一个名为 getToFlowDocumentId
的方法,该方法需要被覆盖以指示文档 ID。
到目前为止,一切恢复正常。因此,我们可以使用显式导航案例在流程的页面之间进行导航。完整的应用程序命名为 ch3_11_1
。
为了以程序性的方式完成相同的事情,你需要使用 NavigationCaseBuilder
API,如下所示;这是相同的导航案例,所以我们只使用了所需的方法:
flowBuilder.navigationCase().fromViewId("/registration/registration.xhtml").fromOutcome("confirm_outcome").toViewId("/registration/confirm.xhtml").redirect();
此示例被封装在完整的应用程序 ch3_11_2
中。
此外,你甚至可以使用自定义导航处理程序。新的NavigationHandlerWrapper
类(在 JSF 2.2 中添加)提供了NavigationHandler
类的一个简单实现。因此,我们可以轻松地扩展它以使用自定义导航处理程序证明导航案例,如下面的代码所示:
public class CustomNavigationHandler extends NavigationHandlerWrapper {
private NavigationHandler configurableNavigationHandler;
public CustomNavigationHandler() {}
public CustomNavigationHandler(NavigationHandler configurableNavigationHandler){
this.configurableNavigationHandler = configurableNavigationHandler;
}
@Override
public void handleNavigation(FacesContext context, String fromAction, String outcome) {
if (outcome.equals("confirm_outcome")) {
outcome = "confirm";
}
getWrapped().handleNavigation(context, fromAction, outcome);
}
@Override
public NavigationHandler getWrapped() {
return configurableNavigationHandler;
}
}
最后,在faces-config.xml
文件中的快速配置如下:
<application>
<navigation-handler>
book.beans.CustomNavigationHandler
</navigation-handler>
</application>
注意
当流程有一个文档 ID 时,你需要重写handleNavigation(FacesContext context, String fromAction, String outcome, String toFlowDocumentId)
方法。
完整的应用程序名称为ch3_11_3
。
检查流程导航案例
无论你选择哪种方法在流程中使用导航案例,你都可以通过ConfigurableNavigationHandler.inspectFlow
方法检查它们。此方法由流程系统调用以导致流程被检查导航规则。你可以轻松地重写它以获取有关导航案例的信息,通过编写一个自定义的可配置导航处理程序。最简单的方法是扩展新的ConfigurableNavigationHandlerWrapper
类(在 JSF 2.2 中引入),它代表ConfigurableNavigationHandler
的一个简单实现。例如,以下代码段发送关于每个找到的导航案例的日志信息:
public class CustomConfigurableNavigationHandler extends ConfigurableNavigationHandlerWrapper {
private final static Logger logger = Logger.getLogger(CustomConfigurableNavigationHandler.class.getName());
private ConfigurableNavigationHandler configurableNavigationHandler;
public CustomConfigurableNavigationHandler() {}
public CustomConfigurableNavigationHandler(ConfigurableNavigationHandler configurableNavigationHandler){
this.configurableNavigationHandler = configurableNavigationHandler;
}
@Override
public void inspectFlow(FacesContext context, Flow flow) {
getWrapped().inspectFlow(context, flow);
if (flow.getNavigationCases().size() > 0) {
Map<String, Set<NavigationCase>> navigationCases = flow.getNavigationCases();
for (Map.Entry<String, Set<NavigationCase>> entry : navigationCases.entrySet()) {
logger.log(Level.INFO, "Navigation case: {0}", entry.getKey());
for (NavigationCase nc : entry.getValue()) {
logger.log(Level.INFO, "From view id: {0}", nc.getFromViewId());
logger.log(Level.INFO, "From outcome: {0}", nc.getFromOutcome());
logger.log(Level.INFO, "To view id: {0}", nc.getToViewId(context));
logger.log(Level.INFO, "Redirect: {0}", nc.isRedirect());
}
}
}
}
@Override
public ConfigurableNavigationHandler getWrapped() {
return configurableNavigationHandler;
}
}
如果你将此自定义可配置导航处理程序附加到前三个示例之一,那么你将获得有关所提供导航案例的信息。完整的示例名称为ch3_15
。
使用初始化器和终结器
通过使用FlowBuilder
API,我们可以附加回调方法,这些方法将在创建流程和销毁之前自动调用。FlowBuilder.initializer
方法具有以下签名,当创建流程时被调用:
public abstract FlowBuilder initializer(String methodExpression)
public abstract FlowBuilder initializer(javax.el.MethodExpression methodExpression)
FlowBuilder.finalizer
签名在销毁流程之前被调用,如下所示:
public abstract FlowBuilder finalizer(String methodExpression)
public abstract FlowBuilder finalizer(javax.el.MethodExpression methodExpression)
例如,可以使用initializer
方法将外部参数传递到流程中。假设在index.xhtml
页面(在流程外部),当我们点击标有开始注册的按钮时,我们希望将锦标赛名称和地点传递到流程中,如下所示:
<h:form prependId="false">
<h:inputHidden id="tournamentNameId" value="Roland Garros"/>
<h:inputHidden id="tournamentPlaceId" value="France"/>
<h:commandButton value="Start Registration" action="registration"/>
</h:form>
这两个参数必须在流程开始时可用,因为包装的信息是通过RegistrationBean
的两个属性在registration.xhml
页面(流程的起始节点)中显示的,即tournamentName
和tournamentPlace
。为此,我们需要调用RegistrationBean
中的一个方法来提取这些信息并将它们存储在这两个属性中,如下面的代码所示:
//initializer method
public void tournamentInitialize() {
tournamentName = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("tournamentNameId");
tournamentPlace = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("tournamentPlaceId");
}
现在是有趣的部分,因为我们可以使用initializer
方法将tournamentInitialize
方法指定为在创建流程时应调用的回调方法。这可以在registration-flow.xml
文件中如下完成:
<initializer>
#{registrationBean.tournamentInitialize()}
</initializer>
因此,在这个时候,我们可以从流程的开始到流程的生命周期内直接使用锦标赛的名称和地点。
进一步来说,另一个简单的场景可以是使用finalizer
方法的理由。假设我们通过一个名为PlayersCounterBean
的应用程序范围 bean 来计算注册的玩家,如下所示:
@Named
@ApplicationScoped
public class PlayersCounterBean {
private int count = 0;
public int getCount() {
return count;
}
public void addPlayer() {
count++;
}
}
当玩家退出流程并且注册成功时,count
变量应该增加;因此,我们可以在registration-flow.xml
文件中放置一个finalizer
方法,如下所示:
<finalizer>
#{registrationBean.tournamentFinalize()}
</finalizer>
tournamentFinalize
方法在RegistrationBean
中实现,如下所示:
@Named
@FlowScoped(value = "registration")
public class RegistrationBean {
@Inject
private PlayersCounterBean playersCounterBean;
...
//finalizer method
public void tournamentFinalize() {
playersCounterBean.addPlayer();
}
}
由于PlayersCounterBean
是一个应用程序 bean,我们可以在流程之外使用它的好处。完整的应用程序命名为ch3_12_1
。
同样的输出可以使用以下代码程序性地实现:
flowBuilder.initializer("#{registrationBean.tournamentInitialize(param['tournamentNameId'], param['tournamentPlaceId'])}");
flowBuilder.finalizer("#{registrationBean.tournamentFinalize()}");
为了变化,在这种情况下,我们没有使用请求参数Map
提取参数值。我们更愿意使用隐式对象param
,并将值作为tournamentInitialize
方法的参数传递,如下所示:
//initializer method
public void tournamentInitialize(String tn, String tp) {
tournamentName = tn;
tournamentPlace = tp;
}
完整的应用程序命名为ch3_12_2
。
使用流程切换
switch
情况语句是长if
语句的替代品,并且对于条件结果映射非常有用。为了看到它的工作情况,我们可以假设每个锦标赛都有一个单独的confirm.xhtml
页面。让我们有网球四大满贯及其相关的 XHTML 确认页面,如下所示:
-
罗兰·加洛斯和
confirm_rg.xhtml
-
温布尔登和
confirm_wb.xhtml
-
美国公开赛和
confirm_us.xhtml
-
澳大利亚公开赛和
confirm_ao.xhtml
锦标赛的名称和地点通过一个简单的表单(每个锦标赛一个表单)在流程中传递,如下所示(您可以从前面的章节中了解到如何在流程内部获取这些信息):
<h:form prependId="false">
<h:inputHidden id="tournamentNameId" value="Australian Open"/>
<h:inputHidden id="tournamentPlaceId" value="Australia"/>
<h:commandButton value="Start Registration (Australian Open)" action="registration"/>
</h:form>
现在,在点击标记为注册到...的按钮后,我们需要选择正确的确认页面。为此,我们可以使用程序切换,如下所示:
public class Registration implements Serializable {
@Produces
@FlowDefinition
public Flow defineFlow(@FlowBuilderParameter FlowBuilder flowBuilder) {
String flowId = "registration";
flowBuilder.id("", flowId);
flowBuilder.viewNode(flowId, "/" + flowId + "/" + flowId + ".xhtml").markAsStartNode();
flowBuilder.viewNode("no-tournament-id", "/" + flowId + "/notournament.xhtml");
flowBuilder.viewNode("confirm-rg-id", "/" + flowId + "/confirm_rg.xhtml");
flowBuilder.viewNode("confirm-wb-id", "/" + flowId + "/confirm_wb.xhtml");
flowBuilder.viewNode("confirm-us-id", "/" + flowId + "/confirm_us.xhtml");
flowBuilder.viewNode("confirm-ao-id", "/" + flowId + "/confirm_ao.xhtml");
flowBuilder.returnNode("taskFlowReturnDone").fromOutcome("#{registrationBean.returnValue}");
flowBuilder.switchNode("confirm-switch-id").defaultOutcome("no-tournament-id").switchCase().condition("#{registrationBean.tournamentName eq 'Roland Garros'}").fromOutcome("confirm-rg-id").condition("#{registrationBean.tournamentName eq 'Wimbledon'}").fromOutcome("confirm-wb-id").condition("#{registrationBean.tournamentName eq 'US Open'}").fromOutcome("confirm-us-id").condition("#{registrationBean.tournamentName eq 'Australian Open'}").fromOutcome("confirm-ao-id");
flowBuilder.initializer("#{registrationBean.tournamentInitialize(param['tournamentNameId'],param['tournamentPlaceId'])}");
flowBuilder.finalizer("#{registrationBean.tournamentFinalize()}");
return flowBuilder.getFlow();
}
}
注意,当没有条件评估为true
时,选定的节点将是notournament.xhtml
页面,它代表默认结果。这只是一个包含一些特定文本的简单 XHMTL 页面。
完整的应用程序命名为ch3_13
。在registration-flow.xml
文件中,可以通过以下代码声明性地实现这一点。您可以使用<view>
标签将结果路径隐藏在 ID 之后(将结果映射到页面),正如我们在程序示例中看到的那样:
<switch id="confirm-switch-id">
<default-outcome>
/registration/notournament.xhtml
</default-outcome>
<case>
<if>#{registrationBean.tournamentName eq 'Roland Garros'}</if>
<from-outcome>/registration/confirm_rg.xhtml</from-outcome>
</case>
<case>
<if>#{registrationBean.tournamentName eq 'Wimbledon'}</if>
<from-outcome>/registration/confirm_wb.xhtml</from-outcome>
</case>
<case>
<if>#{registrationBean.tournamentName eq 'US Open'}</if>
<from-outcome>/registration/confirm_us.xhtml</from-outcome>
</case>
<case>
<if>#{registrationBean.tournamentName eq 'Australian Open'}</if>
<from-outcome>/registration/confirm_ao.xhtml</from-outcome>
</case>
</switch>
因此,切换在您不想将每个结果映射到单个页面时非常有用。
这个例子没有被包含在一个完整的应用程序中。
打包流程
流作为工作逻辑单元;因此,它们可以在多个应用程序之间移植。这种可移植性是通过将流工件打包到 JAR 文件中获得的。此外,JAR 文件可以添加到任何应用程序的CLASSPATH
中,流就可以使用了。要打包一个流,你需要遵循一些约定,如下列所示:
-
在
faces-config.xml
文件中明确定义流。 -
在 JAR 根目录中创建一个
META-INF
文件夹。 -
将此文件夹中的
faces-config.xml
文件添加进来。 -
将此文件夹中的
beans.xml
文件添加进来。 -
在同一文件夹中,
META-INF
,创建一个名为flows
的子文件夹。 -
在
flows
文件夹中,添加流的所有节点(页面)。 -
在 JAR 根目录中,在
META-INF
文件夹外部,添加流所需的全部 Java 代码(类)。
根据前面的步骤,带有 bean 的流部分中描述的流可以被打包成一个名为registration.jar
的 JAR 文件,如下面的截图所示:
使用此 JAR 文件的应用程序完整名称为ch3_14
。
程序流作用域
从编程的角度讲,可以通过javax.faces.flow.FlowHandler
类访问流作用域。在获得FlowHandler
类的对象后,你可以轻松地访问当前流,添加一个新的流,并操纵由#{flowScope}
表示的流映射,如下所示:
FacesContext context = FacesContext.getCurrentInstance();
Application application = context.getApplication();
FlowHandler flowHandler = application.getFlowHandler();
//get current flow
Flow flow = flowHandler.getCurrentFlow();
Flow flowContext = flowHandler.getCurrentFlow(context);
//add flow
flowHandler.addFlow(context, *flow*);
//get access to the Map that backs #{flowScope}
Map<Object,Object> flowMap = flowHandler.getCurrentFlowScope();
显然,FlowHandler
类是参与运行时与面部流特征之间交互的最重要类。这是一个可以扩展以提供自定义流处理器实现的抽象类。为了做到这一点,你可以从创建一个新的FlowHandlerFactory
类开始,该类被Application
类用来创建FlowHandler
类的单例实例。这个类有一个简单的实现名为FlowHandlerFactoryWrapper
,它可以很容易地扩展以返回自定义流处理器,如下面的代码所示:
public class CustomFlowHandlerFactory extends FlowHandlerFactoryWrapper {
private FlowHandlerFactory flowHandlerFactory;
public CustomFlowHandlerFactory(){}
public CustomFlowHandlerFactory(FlowHandlerFactory flowHandlerFactory){
this.flowHandlerFactory = flowHandlerFactory;
}
@Override
public FlowHandler createFlowHandler(FacesContext context){
FlowHandler customFlowHandler = new CustomFlowHandler(getWrapped().createFlowHandler(context));
return customFlowHandler;
}
@Override
public FlowHandlerFactory getWrapped() {
return this.flowHandlerFactory;
}
}
此工厂应在faces-config.xml
文件中配置,如下面的代码所示:
<factory>
<flow-handler-factory>
book.beans.CustomFlowHandlerFactory
</flow-handler-factory>
</factory>
此外,CustomFlowHandler
类代表了对FlowHandler
类的扩展。由于FlowHandler
类是一个抽象类,你需要为它的每个方法提供一个实现,如下面的代码所示:
public class CustomFlowHandler extends FlowHandler {
private FlowHandler flowHandler;
public CustomFlowHandler() {}
public CustomFlowHandler(FlowHandler flowHandler) {
this.flowHandler = flowHandler;
}
...
//Overrided methods
...
}
例如,你可以从前面的章节中知道,registration
流向嵌套的schedule
流传递了几个出站参数。你看到了如何在registration-flow.xml
文件中声明性地完成这个操作,以及如何在Registration
类中通过FlowBuilder
API 编程式地完成,如下面的代码所示。你可以在名为transition
的方法中从自定义流处理器做同样的事情,这个方法能够在一个源流(例如,registration
)和一个目标流(例如,schedule
)之间执行转换。当registration
流调用schedule
流时,你可以编写以下代码:
@Override
public void transition(FacesContext context, Flow sourceFlow, Flow targetFlow, FlowCallNode outboundCallNode, String toViewId) {
if ((sourceFlow != null) && (targetFlow != null)) {
if ((sourceFlow.getStartNodeId().equals("registration")) &&
(targetFlow.getStartNodeId().equals("schedule"))) {
FlowCallNode flowCallNode = sourceFlow.getFlowCalls().get("callSchedule");
Map<String, Parameter> outboundParameters = flowCallNode.getOutboundParameters();
CustomParameter playernameparamO = new CustomParameter("playernameparam", "#{registrationBean.playerName}");
CustomParameter playersurnameparamO = new CustomParameter("playersurnameparam", "#{registrationBean.playerSurname}");
CustomParameter playerregistrationcodeO = new CustomParameter("playerregistrationcode","349CF0YO122");
outboundParameters.put("playernameparam", playernameparamO);
outboundParameters.put("playersurnameparam", playersurnameparamO);
outboundParameters.put("playerregistrationcode", playerregistrationcodeO);
}
}
flowHandler.transition(context, sourceFlow, targetFlow, outboundCallNode, toViewId);
}
目标入站参数可以按以下方式访问(Map
参数不能更改):
Map<String, Parameter> inboundParameters = targetFlow.getInboundParameters();
流参数由 javax.faces.flow.Parameter
抽象类表示。CustomParameter
类提供了以下实现:
public class CustomParameter extends Parameter {
private String name;
private String value;
public CustomParameter(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public String getName() {
return name;
}
@Override
public ValueExpression getValue() {
return createValueExpression(value, String.class);
}
private ValueExpression createValueExpression(String exp, Class<?> cls) {
FacesContext facesContext = FacesContext.getCurrentInstance();
ELContext elContext = facesContext.getELContext();
return facesContext.getApplication().getExpressionFactory().
createValueExpression(elContext, exp, cls);
}
}
依赖伪范围
这是 CDI Bean(@Named
)的默认范围,当没有指定任何内容时。在这种情况下,一个对象存在是为了精确地为一个 Bean 服务,并且具有与该 Bean 相同的生命周期;依赖范围 Bean 的实例不会在不同用户或不同注入点之间共享。它也可以通过使用 @Dependent
注解和导入 javax.enterprise.context.Dependent
来显式指定。这个范围仅在 CDI 中可用,并且是唯一的非上下文范围。
注意
所有 CDI 范围,除了这个之外,都被称为正常范围。关于正常范围与伪范围的更多详细信息,可以在docs.jboss.org/cdi/spec/1.0/html/contexts.html
的正常范围和伪范围部分找到。
如果你将 PlayersBean
放在依赖范围中,那么当前提取的玩家和随机提取的玩家列表(可能为空或包含此玩家)仅在 Bean 内部可用,如下面的代码所示:
import javax.enterprise.context.Dependent;
import javax.inject.Named;
@Named
@Dependent
public class PlayersBean {
...
}
注意
注有 @PostConstruct
的方法将为每个请求调用。实际上,如果 Bean 在多个 EL 表达式中使用,它可能会在同一个请求期间被多次调用。最初,有一个 Bean 的实例,如果 Bean EL 名称在 EL 表达式中多次出现,则此实例会被重用,但在另一个 EL 表达式或同一 EL 表达式的重新评估的情况下则不会重用。
这个例子被包裹在名为 ch3_5
的应用程序中,该应用程序位于本章代码包中。
无范围
无范围的 Bean 生存下来为其他 Bean 服务。
无范围似乎在 JSF 范围中是“黑羊”。即使它的名字也不让人联想到有用的东西。实际上,在这个范围内的管理 Bean 的生命周期与单个 EL 表达式评估一样长,并且在任何 JSF 页面中都是不可见的。如果应用程序范围是最长的,那么这个范围就是最短的。但是,如果你在其他管理 Bean 中注入无范围的管理 Bean,那么它们将与宿主一样长。实际上,这是它们的职责,为其他 Bean 服务。
注意
在配置文件中使用的无范围对象表示由其他管理 Bean 在应用程序中使用的管理 Bean。
因此,每次你需要一个谦逊的管理 Bean,它准备好成为请求或会话等酷范围的一部分时,你都可以使用 javax.faces.bean
包中的 @NoneScoped
注解它。此外,具有无范围的对象可以使用具有无范围的其它对象。
自定义范围
当没有前面的作用域满足你的应用程序需求时,你必须注意 JSF 2 自定义作用域。很可能你永远不会想编写自定义作用域,但如果有必要,那么在本节中,你可以看到如何完成这个任务。
注意
自定义作用域注解是@CustomScoped
,它定义在javax.faces.bean
包中。它不在 CDI 中可用!
为了实现自定义作用域,假设你想要控制几个在应用程序作用域中存在的 bean 的生命周期。通常,它们的生命周期与应用程序的生命周期相同,但你希望能够在应用程序流程的某些时刻将它们添加/从应用程序作用域中移除。当然,有许多方法可以做到这一点,但请记住,我们寻找实现自定义作用域的原因;因此,我们将尝试编写一个嵌套在应用程序作用域中的自定义作用域,这将允许我们添加/移除一批 bean。作用域的创建和销毁将反映在 bean 的创建和销毁中,这意味着你不需要引用每个 bean。
实际上,由于这只是一个演示,我们将只使用两个 bean:一个将保留在经典的应用程序作用域中(它可以用于比较应用程序和自定义作用域的生命周期),而另一个将通过自定义作用域被添加/销毁。应用程序的目的并不相关;你应该关注编写自定义作用域的技术,并填补假设和空白。多思考一下,当你真正需要实现自定义作用域时,你可以使用这些知识。
编写自定义作用域类
自定义作用域由一个扩展ConcurrentHashMap<String, Object>
类的类表示。我们需要允许对普通映射的并发访问,因为暴露的数据可能被多个浏览器并发访问。CustomScope
类的代码如下:
public class CustomScope extends ConcurrentHashMap<String, Object> {
public static final String SCOPE = "CUSTOM_SCOPE";
public CustomScope(){
super();
}
public void scopeCreated(final FacesContext ctx) {
ScopeContext context = new ScopeContext(SCOPE, this);
ctx.getApplication().publishEvent(ctx, PostConstructCustomScopeEvent.class, context);
}
public void scopeDestroyed(final FacesContext ctx) {
ScopeContext context = new ScopeContext(SCOPE,this);
ctx.getApplication().publishEvent(ctx, PreDestroyCustomScopeEvent.class, context);
}
}
当我们的作用域被创建/销毁时,其他组件将通过事件得到通知。在scopeCreated
方法中,你注册PostConstructCustomScopeEvent
,而在scopeDestroyed
方法中,你注册PreDestroyCustomScopeEvent
。
现在我们有了自定义作用域,是时候看看如何在这个作用域中声明一个 bean 了。嗯,这并不难,可以使用@CustomScoped
注解和一个 EL 表达式来完成,如下所示:
import javax.faces.bean.CustomScoped;
import javax.faces.bean.ManagedBean;
@ManagedBean
@CustomScoped("#{CUSTOM_SCOPE}")
public class SponsoredLinksBean {
...
}
解析自定义作用域 EL 表达式
在这一点上,JSF 将遍历现有解析器的链,以解析自定义作用域的 EL 表达式。显然,这次尝试将以错误结束,因为没有现有的解析器能够满足这个 EL 表达式。因此,你需要编写一个自定义解析器,就像你在第一章动态访问 JSF 应用程序数据")中看到的那样,通过表达式语言(EL 3.0)动态访问 JSF 应用程序数据。基于此,你应该获得以下代码所示的内容:
public class CustomScopeResolver extends ELResolver {
private static final Logger logger = Logger.getLogger(CustomScopeResolver.class.getName());
@Override
public Object getValue(ELContext context, Object base, Object property) {
logger.log(Level.INFO, "Get Value property : {0}", property);
if (property == null) {
String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property");
throw new PropertyNotFoundException(message);
}
FacesContext facesContext = (FacesContext) context.getContext(FacesContext.class);
if (base == null) {
Map<String, Object> applicationMap = facesContext.getExternalContext().getApplicationMap();
CustomScope scope = (CustomScope) applicationMap.get(CustomScope.SCOPE);
if (CustomScope.SCOPE.equals(property)) {
logger.log(Level.INFO, "Found request | base={0} property={1}", new Object[]{base, property});
context.setPropertyResolved(true);
return scope;
} else {
logger.log(Level.INFO, "Search request | base={0} property={1}", new Object[]{base, property});
if (scope != null) {
Object value = scope.get(property.toString());
if (value != null) {
logger.log(Level.INFO, "Found request | base={0} property={1}", new Object[]{base, property});
context.setPropertyResolved(true);
}else {
logger.log(Level.INFO, "Not found request | base={0} property={1}", new Object[]{base, property});
context.setPropertyResolved(false);
}
return value;
} else {
return null;
}
}
}
if (base instanceof CustomScope) {
CustomScope baseCustomScope = (CustomScope) base;
Object value = baseCustomScope.get(property.toString());
logger.log(Level.INFO, "Search request | base={0} property={1}", new Object[]{base, property});
if (value != null) {
logger.log(Level.INFO, "Found request | base={0} property={1}", new Object[]{base, property});
context.setPropertyResolved(true);
} else {
logger.log(Level.INFO, "Not found request | base={0} property={1}", new Object[]{base, property});
context.setPropertyResolved(false);
}
return value;
}
return null;
}
@Override
public Class<?> getType(ELContext context, Object base, Object property) {
return Object.class;
}
@Override
public void setValue(ELContext context, Object base, Object property, Object value) {
if (base != null) {
return;
}
context.setPropertyResolved(false);
if (property == null) {
String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property");
throw new PropertyNotFoundException(message);
}
if (CustomScope.SCOPE.equals(property)) {
throw new PropertyNotWritableException((String) property);
}
}
@Override
public boolean isReadOnly(ELContext context, Object base, Object property) {
return true;
}
@Override
public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
return null;
}
@Override
public Class<?> getCommonPropertyType(ELContext context, Object base) {
if (base != null) {
return null;
}
return String.class;
}
}
不要忘记通过在faces-config.xml
文件中添加它来将以下解析器放入链中:
<el-resolver>book.beans.CustomScopeResolver</el-resolver>
完成!到目前为止,你已经创建了一个自定义作用域,你将一个 Bean 放入这个作用域,并了解到这个全新的解析器提供了对这个 Bean 的访问。
自定义作用域必须存储在某个地方,所以嵌套在应用程序作用域中可以是一个选择(当然,根据你的需求,其他作用域也可以是一个选择)。当作用域被创建时,它必须放置在应用程序映射中,当它被销毁时,它必须从应用程序映射中删除。问题是何时创建它,何时销毁它?答案是,这取决于。这很可能是与应用程序流程紧密相关的决定。
使用动作监听器控制自定义作用域的生命周期
即使涉及从视图声明中进行控制,使用动作监听器也可以是一种良好的实践。假设标记为START的按钮将自定义作用域添加到应用程序映射中,如下面的代码所示:
<h:commandButton value="START">
<f:actionListener type="book.beans.CreateCustomScope" />
</h:commandButton>
以下CreateCustomScope
类是一个简单的动作监听器,因为它实现了ActionListener
接口:
public class CreateCustomScope implements ActionListener {
private static final Logger logger = Logger.getLogger(CreateCustomScope.class.getName());
@Override
public void processAction(ActionEvent event) throws AbortProcessingException {
logger.log(Level.INFO, "Creating custom scope ...");
FacesContext context = FacesContext.getCurrentInstance();
Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE);
if (customScope == null) {
customScope = new CustomScope();
applicationMap.put(CustomScope.SCOPE, customScope);
customScope.scopeCreated(context);
} else {
logger.log(Level.INFO, "Custom scope exists ...");
}
}
}
按照相同的方法,标记为STOP的按钮将按照以下方式从应用程序映射中删除自定义作用域:
<h:commandButton value="STOP">
<f:actionListener type="book.beans.DestroyCustomScope" />
</h:commandButton>
以下DestroyCustomScope
类作为动作监听器,因为它实现了ActionListener
接口:
public class DestroyCustomScope implements ActionListener {
private static final Logger logger = Logger.getLogger(DestroyCustomScope.class.getName());
@Override
public void processAction(ActionEvent event) throws AbortProcessingException {
logger.log(Level.INFO, "Destroying custom scope ...");
FacesContext context = FacesContext.getCurrentInstance();
Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE);
if (customScope != null) {
customScope.scopeDestroyed(context);
applicationMap.remove(CustomScope.SCOPE);
} else {
logger.log(Level.INFO, "Custom scope does not exists ...");
}
}
}
此示例被封装在名为ch3_8
的应用程序中,该应用程序位于本章的代码包中。只需运行并快速查看代码,就可以清楚地看到这里没有缺失的意大利面代码。
使用导航处理程序控制自定义作用域的生命周期
另一种方法是根据页面导航来控制自定义作用域的生命周期。这种解决方案更加灵活,并且对用户隐藏。你可以通过扩展NavigationHandler
来编写自定义导航处理程序。下一个实现将自定义作用域放入应用程序映射中,当导航到达名为sponsored.xhtml
的页面时,并在任何其他导航情况下将其从应用程序映射中删除。CustomScopeNavigationHandler
类的代码如下:
public class CustomScopeNavigationHandler extends NavigationHandler {
private static final Logger logger = Logger.getLogger(CustomScopeNavigationHandler.class.getName());
private final NavigationHandler navigationHandler;
public CustomScopeNavigationHandler(NavigationHandler navigationHandler) {
this.navigationHandler = navigationHandler;
}
@Override
public void handleNavigation(FacesContext context, String fromAction, String outcome) {
if (outcome != null) {
if (outcome.equals("sponsored")) {
logger.log(Level.INFO, "Creating custom scope ...");
Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE);
if (customScope == null) {
customScope = new CustomScope();
applicationMap.put(CustomScope.SCOPE, customScope);
customScope.scopeCreated(context);
} else {
logger.log(Level.INFO, "Custom scope exists ...");
}
} else {
logger.log(Level.INFO, "Destroying custom scope ...");
Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE);
if (customScope != null) {
customScope.scopeDestroyed(context);
applicationMap.remove(CustomScope.SCOPE);
} else {
logger.log(Level.INFO, "Custom scope does not exist");
}
}
}
navigationHandler.handleNavigation(context, fromAction, outcome);
}
}
不要忘记在faces-config.xml
文件中注册以下导航处理程序:
<navigation-handler>
book.beans.CustomScopeNavigationHandler
</navigation-handler>
此示例被封装在名为ch3_9
的应用程序中,该应用程序位于本章的代码包中。快速查看代码可以清楚地看到这里没有缺失的意大利面代码。
如我之前所说,JSF 2.2 提供了一个NavigationHandler
的包装类。这是一个简单的实现,可以很容易地被开发者扩展。getWrapped
方法返回被包装的类的实例。例如,你可以重写CustomScopeNavigationHandler
类,如下面的代码所示:
public class CustomScopeNavigationHandler extends NavigationHandlerWrapper {
private static final Logger logger = Logger.getLogger(CustomScopeNavigationHandler.class.getName());
private final NavigationHandler navigationHandler;
public CustomScopeNavigationHandler(NavigationHandler navigationHandler){
this.navigationHandler = navigationHandler;
}
@Override
public void handleNavigation(FacesContext context, String fromAction, String outcome) {
if (outcome != null) {
if (outcome.equals("sponsored")) {
logger.log(Level.INFO, "Creating custom scope ...");
Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE);
if (customScope == null) {
customScope = new CustomScope();
applicationMap.put(CustomScope.SCOPE, customScope);
customScope.scopeCreated(context);
} else {
logger.log(Level.INFO, "Custom scope exists ...");
}
} else {
logger.log(Level.INFO, "Destroying custom scope ...");
Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE);
if (customScope != null) {
customScope.scopeDestroyed(context);
applicationMap.remove(CustomScope.SCOPE);
} else {
logger.log(Level.INFO, "Custom scope does not exist");
}
}
}
getWrapped().handleNavigation(context, fromAction, outcome);
}
@Override
public NavigationHandler getWrapped() {
return navigationHandler;
}
}
此示例被封装在名为ch3_10
的应用程序中,该应用程序位于本章的代码包中。
管理 Bean 实例化
默认情况下,管理 Bean 在第一次引用它时(例如请求)实例化——这被称为延迟实例化。您可以通过添加eager
属性并将其值设置为true
来更改默认行为。这将使管理 Bean 在应用启动时实例化,在发出任何请求之前。但,重要的是要知道这仅适用于应用作用域的 Bean,并且延迟实例化的 Bean 被放置在应用作用域中,如下面的代码行所示:
@ManagedBean(eager=true)
@ApplicationScoped
Bean 注入
通常,解决方案取决于具体的功能需求,但找到正确的解决方案是开发者之间的区别。有时,当开发者在使用来自另一个作用域的对象的作用域中工作时,他们可能会陷入困境或犯错误。从以下图中,您可以寻求一些处理一些最常见情况的指导:
注意
如您所见,有一些限制。在 JSF 中,一般规则是不要使用生命周期比您调用的对象更短的对象。换句话说,使用生命周期与被注入对象相同或更长的对象。违反此规则将导致 JSF 异常。
该规则的逻辑可以通过两个最常见的错误来解释,如下所示:
-
在会话对象中使用请求对象:这是不好的,因为我们会有很多请求(很多实例)和只有一个会话(一个实例)。通常,请求属于所有用户,而会话是每个用户一个;因此,不清楚请求对象是如何注入的?为了更清楚,许多请求意味着许多相关的 Bean,而会话意味着一个 Bean。现在,注入一个特定的实例而忽略其他所有实例是不合逻辑的。此外,您如何以及何时检索正确的实例,因为请求对象是瞬时的,通常生命周期较短!即使您找到一个合理的用例,JSF 也不会允许您通过 JSF 管理 Bean 来实现这一点。
-
在应用对象中使用会话对象:当我们要在应用对象中使用会话对象时,相同的逻辑可以进一步应用。会话的数量与用户数量一样多,但应用只有一个;因此,您不能将所有会话注入到应用中...这是没有用的!当然,您可能想将某个会话检索到应用中,但您必须确保指定的会话存在;如果您对当前用户的会话感兴趣,这并不是问题,但如果您对其他用户的会话感兴趣,可能会出现问题。此外,如果有许多会话,您必须正确识别所需的会话。即使您找到一个合理的用例,JSF 也不会允许您通过 JSF 管理 Bean 来实现这一点。
然而,对于 CDI 来说,这些情况并不是什么大问题。当你使用一个生命周期比调用它的对象更短的对象时(例如,将请求作用域的 bean 注入到会话作用域的 bean 中),CDI 将这种情况分类为不匹配的注入,并通过 CDI 代理来解决这个问题。对于每个请求,CDI 代理都会重新建立与请求作用域 bean 的实时实例的连接。
即使遵循了书面的规则,我们仍然容易受到未书面规则的侵害。可能导致不希望的结果的未书面规则之一被称为过度使用或滥用。以下是一些需要避免的情况:
-
过度使用视图作用域的 bean 来处理请求作用域的数据可能会影响内存。
-
过度使用请求作用域的 bean 来处理视图作用域的数据可能会导致表单出现意外的行为。
-
过度使用应用作用域的 bean 来处理请求/视图/会话作用域的数据可能会导致数据在用户之间具有不希望的范围,并影响内存。
-
过度使用会话作用域的 bean 来处理请求/视图数据可能会导致在该会话的多个浏览器窗口/标签页中数据具有不希望的范围。正如你所知,视图数据是针对单个浏览器窗口/标签页的,这允许我们打开多个标签页,并在切换标签页时保持数据完整性。另一方面,如果这些数据通过会话作用域公开,那么一个窗口/标签页中的修改将在浏览器会话中反映出来;因此,在标签页之间切换将导致一种称为数据不一致性的明显奇怪的行为。在使用会话作用域处理请求/视图数据的情况下,也会影响内存,因为请求/视图作用域的预期生命周期比会话作用域短。
从 JSF 2.0 开始,可以使用@ManagedProperty
注解将管理 bean 注入到另一个管理 bean 的属性中(依赖注入)。你已经在上一章中知道了这一点,其中提供了一个示例。
注入 bean 的另一种方式是使用@Inject
注解,它是 CDI 强大注入机制的一部分。
那么,我们何时使用@ManagedProperty
,何时使用@Inject
呢?嗯,我们知道它们以不同的方式在不同的容器中做相同的事情,所以当你在一个 servlet 容器中工作或者根本不需要 CDI 时,使用@ManagedProperty
可能是个好主意。@ManagedProperty
的另一个优点是你可以用它来使用表达式语言(EL)。但是,如果你在一个合适的 CDI 环境中,可以充分利用 CDI 的好处,比如防止代理作用域泄露或更好的部署时依赖,那么就使用 CDI。
和平主义方法将在同一应用程序中将这两者结合起来。在这种情况下,你有两个选择:避免管理 bean 和 CDI bean 之间的任何交互,或者显然地,鼓励它们之间的交互以获得更好的性能。如果你选择第二个选项,那么重要的是要记住以下图示中所示的一些简单的注入规则:
摘要
在本章中,我们浏览了 JSF/CDI 作用域的概述。它从关于 JSF 作用域与 CDI 作用域的开放讨论开始,旨在提供选择任何一个(或两个)的一些优缺点。在简要概述 JSF/CDI 作用域之后,每个作用域都通过涵盖基本知识,如定义、可用性、功能、限制和示例进行了详细说明。
本章以一系列关于豆注射的思考结束。在这里,你可以找到在 JSF 应用程序中常用的一些规则、技巧和不良做法。
欢迎在下一章中相见,我们将涵盖许多种类的 JSF 工件和配置内容。
第四章. 使用 XML 文件和注解的 JSF 配置 – 第一部分
从 JSF 2.0 开始,不再需要创建配置文件 faces-config.xml
。好吧,这个断言部分是正确的,因为 JSF 注解仍然没有涵盖几个配置,例如资源包、工厂、阶段监听器等。通常,JSF 注解为我们提供了足够的应用支持;然而,正如你将在本章中看到的,仍然有许多情况下 faces-config.xml
是必需的,或者必须在 web.xml
文件中添加额外的配置。
尽管如此,JSF 2.2 提供了一种程序化方法,可以用来重新生成 faces-config.xml
,而不必使用经典方法。在本章的后面部分,你将看到如何利用这个新特性。现在,你将看到创建和配置不同类型 JSF 艺术品的混合示例。它们将被任意展示——其中一些是众所周知的,来自 JSF 1.x 和 2.0,而另一些则是新的,从 JSF 2.2 开始。由于这些配置很简单,它们可以被视为空白的文档,但将每个配置粘合到示例中更有用,并提供了一个当你需要使用它们时的良好起点。
因此,在本章中,你将了解 JSF 艺术品的配置,但你也会看到一些使用这些艺术品的示例。以下是我们将要涵盖的简要概述:
-
JSF 2.2 新命名空间
-
JSF 2.2 程序化配置
-
在 XML 中配置管理 Bean
-
使用多个配置文件
-
配置区域设置和资源包
-
配置验证器和转换器
-
配置导航
-
配置动作监听器
-
配置系统事件监听器
-
配置阶段监听器
-
使用
@ListenerFor
和@ListenersFor
显然,我们有很多工作要做,也有很多 JSF 2.2 特性需要覆盖(例如,比之前更多的 JSF 2.2 注入),所以让我们开始吧!
JSF 2.2 新命名空间
JSF 2.2 修改了现有的 JSF 命名空间,如下表所示:
命名空间 | 在 JSF 2.2 之前 | JSF 2.2 |
---|---|---|
Faces 核心库 | http://java.sun.com/jsf/core |
http://xmlns.jcp.org/jsf/core |
HTML_BASIC | http://java.sun.com/jsf/html |
http://xmlns.jcp.org/jsf/html |
Facelets 模板 | http://java.sun.com/jsf/facelets |
http://xmlns.jcp.org/jsf/facelets |
组合组件 | http://java.sun.com/jsf/composite |
http://xmlns.jcp.org/jsf/composite |
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 |
透传属性 | http://java.sun.com/jsf/passthrough |
http://xmlns.jcp.org/jsf/passthrough |
透传元素 | http://java.sun.com/jsf |
http://xmlns.jcp.org/jsf |
@FacesComponent 默认命名空间 |
http://xmlns.jcp.org/jsf/component |
JSF 2.2 程序化配置
从 JSF 2.2 版本开始,我们可以通过编程方式重现faces-config.xml
的内容和任务。起点是一个名为populateApplicationConfiguration
的回调方法,它接受一个类型为org.w3c.dom.Document
的单个参数——这个类属于 DOM API。基本上,一个Document
(树节点)是 XML 文档在内存中的表示,我们可以通过添加、删除、导入或采用节点、元素和文本来操作它。对于这些操作中的每一个,都有专门的方法。对于一些 JSF 开发者来说,这个 API 可能是一些新的东西,应该学习;因此,这可能是程序化配置的缺点。
现在,让我们从回调方法中继续讨论论文。populateApplicationConfiguration
方法由一个扩展并实现javax.faces.application
包中找到的抽象类ApplicationConfigurationPopulator
的类提供。为了告诉 JSF 这个类,您需要:
-
创建一个 JAR 包(例如,
faces-config.jar
或使用任何其他名称)。 -
在此 JAR 包中,创建一个名为
META-INF
的文件夹。 -
在
META-INF
文件夹中,创建一个名为services
的文件夹。 -
在
services
文件夹中,创建一个名为javax.faces.application.ApplicationConfigurationPopulator
的空文件。 -
在此文件中,写入扩展并实现抽象类
ApplicationConfigurationPopulator
的类的完全限定名称。 -
在 JAR 根目录下,放置扩展并实现抽象类
ApplicationConfigurationPopulator
的类。
完成!现在当您将此 JAR 包添加到项目的CLASSPATH
中时,JSF 将处理它并应用找到的配置。
假设扩展并实现抽象类ApplicationConfigurationPopulator
的类命名为faces.config.Initializer
(您可以使用任何其他名称),那么 JAR 内容将如下截图所示:
在本章中,您将看到一些程序化示例,作为经典faces-config.xml
的替代方案。当我们直接在 DOM 树节点上工作时,我们往往会犯一些愚蠢的错误,比如忘记添加元素的文本,或者将元素放置在不适当的位置,等等。为了在没有头痛的情况下消除这些错误,您可以编写一个简单的方法将 DOM 序列化到 XML 文件中,这可以很容易地通过视觉或使用专用工具进行调试。以下方法完成了这个任务,您将在本章的所有示例中找到它:
private void serializeFacesConfig(Document document,String path) {
FileOutputStream fileOutputStream = null;
OutputFormat outputFormat = new OutputFormat();
outputFormat.setIndent(5);
outputFormat.setLineWidth(150);
...
fileOutputStream = new FileOutputStream(path);
XMLSerializer xmlSerializer = new XMLSerializer();
xmlSerializer.setOutputFormat(outputFormat);
xmlSerializer.setOutputByteStream((OutputStream)
fileOutputStream);
xmlSerializer.serialize(document);
...
}
在 XML 中配置托管 Bean
JSF 管理 Bean 配置从 JSF 2.0 开始得到了本质上的改进。最常见的情况是,一个管理 Bean 被注解为@ManagedBean
,并使用另一个注解来指定 JSF 的作用域(例如,@RequestScoped
)。但是,管理 Bean 也可以在faces-config.xml
中进行配置,并且这种方法并没有被弃用或过时。最简单的配置包括管理 Bean 的名称、类和作用域:
<managed-bean>
<managed-bean-name>playersBean</managed-bean-name>
<managed-bean-class>book.beans.PlayersBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
...
</managed-bean>
注意
如果需要急切初始化的管理 Bean,可以使用<managed-bean>
标签的eager
属性:
<managed-bean eager="true">
可以使用<managed-property>
标签从faces-config.xml
中初始化管理 Bean 的属性,如下所示:
<managed-property>
<property-name>name</property-name>
<value>Nadal</value>
</managed-property>
<managed-property>
<property-name>surname</property-name>
<value>Rafael</value>
</managed-property>
注意
在<value>
标签内部,我们也可以使用 EL 表达式。例如,我们可以使用属于管理 BeanB
的属性值来初始化管理 BeanA
的属性。但是,重要的是要知道 JSF 不支持管理 Bean 引用的循环依赖——你不能从管理 BeanA
引用管理 BeanB
,反之亦然。
一个有趣的案例涉及设置一个具有上下文初始化参数值的属性。这些参数在部署描述符(web.xml
)中配置:
<context-param>
<param-name>rafakey</param-name>
<param-value>Vamos Rafa!</param-value>
</context-param>
在程序上,这些类型的参数可以通过初始化映射或通过它们的名称来提取,如下所示:
FacesContext.getCurrentInstance().getExternalContext().getInitParameterMap();
FacesContext.getCurrentInstance().getExternalContext().getInitParameter(*param_name*);
这些参数可以通过faces-config.xml
使用 EL 隐式对象initParam
来访问。JSF 提供了从管理 Bean 属性中引用 EL 隐式对象的能力,如下所示:
<managed-property>
<property-name>rafakey</property-name>
<value>#{initParam.rafakey}</value>
</managed-property>
从faces-config.xml
中,我们可以初始化更复杂的属性,例如枚举和集合。考虑以下枚举:
public enum Plays {
Left, Right
};
private Plays play;
//getters and setters
...
上述属性可以按照以下方式初始化:
<managed-property>
<property-name>play</property-name>
<value>Left</value>
</managed-property>
在集合的情况下,我们可以轻松地初始化映射和列表。一个映射(java.util.Map
)可以按照以下方式初始化:
<managed-property>
<property-name>matchfacts</property-name>
<map-entries>
<map-entry>
<key>Aces</key>
<value>12</value>
</map-entry>
<map-entry>
<key>Double Faults</key>
<value>2</value>
</map-entry>
<map-entry>
<key>1st Serve</key>
<value>70%</value>
</map-entry>
</map-entries>
</managed-property>
当一个列表java.util.List
(或数组)可以按照以下方式初始化时:
<managed-property>
<property-name>titles_2013</property-name>
<list-entries>
<value-class>java.lang.String</value-class>
<value>Sao Paulo</value>
<value>Acapulco</value>
<value>Barcelona</value>
<value>...</value>
</list-entries>
</managed-property>
注意
可以使用<null-value/>
标签来使用null
值初始化一个属性。
如果你更喜欢在 XML 描述符中配置管理 Bean(而不是使用注解),那么将它们放置在另一个描述符中而不是faces-config.xml
中是一个好的做法。保留这个描述符用于应用程序级别的配置。例如,你可以将其命名为faces-beans.xml
。当 JSF 检查应用程序描述符web.xml
时,它将知道如何使用这个文件,因为它检查以下预定义的上下文参数:
<context-param>
<param-name>javax.faces.CONFIG_FILES</param-name>
<param-value>/WEB-INF/faces-beans.xml</param-value>
</context-param>
现在,你可以保留faces-config.xml
用于其他配置。
显然,使用注解而不是标签要容易得多,但有时这种方法确实非常有用。例如,你可以有一些注解管理 Bean,你想要改变它们的行为,但由于不同的原因你不能编辑源代码。在这种情况下,你可以在一个 XML 文件中编写修改,因为在运行时,XML 文件中的配置将优先于注解。
本章代码包中提供了一个名为 ch4_12
的完整示例。
JSF 2.2 的程序化方法可以按照以下方式重现 ch4_12
应用程序的配置文件:
public class Initializer extends ApplicationConfigurationPopulator {
@Override
public void populateApplicationConfiguration (Document toPopulate) {
String ns = toPopulate.getDocumentElement().getNamespaceURI();
Element managedbeanEl = toPopulate.createElementNS(ns, "managed-bean");
Element managedbeannameEl = toPopulate.createElementNS(ns, "managed-bean-name");
managedbeannameEl.appendChild(toPopulate.createTextNode("playersBean"));
managedbeanEl.appendChild(managedbeannameEl);
Element managedbeanclassEl = toPopulate.createElementNS(ns, "managed-bean-class");
managedbeanclassEl.appendChild(toPopulate.
createTextNode("book.beans.PlayersBean"));
managedbeanEl.appendChild(managedbeanclassEl);
Element managedbeanscopeEl = toPopulate.
createElementNS(ns, "managed-bean-scope");
managedbeanscopeEl.appendChild(toPopulate.
createTextNode("request"));
managedbeanEl.appendChild(managedbeanscopeEl);
Element managedproperty0El = toPopulate.
createElementNS(ns, "managed-property");
Element propertyNameEl = toPopulate.
createElementNS(ns, "property-name");
propertyNameEl.appendChild(toPopulate.createTextNode("name"));
Element valueNameEl = toPopulate.createElementNS(ns, "value");
valueNameEl.appendChild(toPopulate.createTextNode("Nadal"));
managedproperty0El.appendChild(propertyNameEl);
managedproperty0El.appendChild(valueNameEl);
managedbeanEl.appendChild(managedproperty0El);
...
Element managedproperty5El = toPopulate.
createElementNS(ns, "managed-property");
Element propertyMatchfactsEl = toPopulate.
createElementNS(ns, "property-name");
propertyMatchfactsEl.appendChild(toPopulate.
createTextNode("matchfacts"));
Element mapEntriesEl = toPopulate.
createElementNS(ns, "map-entries");
Element mapEntry0El = toPopulate.
createElementNS(ns, "map-entry");
Element key0El = toPopulate.createElementNS(ns, "key");
key0El.appendChild(toPopulate.createTextNode("Aces"));
Element value0El = toPopulate.createElementNS(ns, "value");
value0El.appendChild(toPopulate.createTextNode("12"));
mapEntry0El.appendChild(key0El);
mapEntry0El.appendChild(value0El);
...
mapEntriesEl.appendChild(mapEntry0El);
mapEntriesEl.appendChild(mapEntry1El);
mapEntriesEl.appendChild(mapEntry2El);
managedproperty5El.appendChild(propertyMatchfactsEl);
managedproperty5El.appendChild(mapEntriesEl);
managedbeanEl.appendChild(managedproperty5El);
Element managedproperty6El = toPopulate.
createElementNS(ns, "managed-property");
Element propertyTitles_2013El = toPopulate.
createElementNS(ns, "property-name");
propertyTitles_2013El.appendChild(toPopulate.
createTextNode("titles_2013"));
Element listEntriesEl = toPopulate.
createElementNS(ns, "list-entries");
Element valueClassEl = toPopulate.
createElementNS(ns, "value-class");
valueClassEl.appendChild(toPopulate.
createTextNode("java.lang.String"));
Element value0lEl = toPopulate.createElementNS(ns, "value");
value0lEl.appendChild(toPopulate.createTextNode("Sao Paulo"));
...
listEntriesEl.appendChild(valueClassEl);
listEntriesEl.appendChild(value0lEl);
listEntriesEl.appendChild(value1lEl);
listEntriesEl.appendChild(value2lEl);
listEntriesEl.appendChild(value3lEl);
listEntriesEl.appendChild(nullValuelEl);
managedproperty6El.appendChild(propertyTitles_2013El);
managedproperty6El.appendChild(listEntriesEl);
managedbeanEl.appendChild(managedproperty6El);
toPopulate.getDocumentElement().appendChild(managedbeanEl);
//serializeFacesConfig(toPopulate, "D://faces-config.xml");
}
...
}
完整应用程序命名为 ch4_14_1
。
与多个配置文件一起工作
JSF 2.0 提供了对配置资源排序的支持。我们可以使用 部分排序(由 <ordering>
标签表示)和 绝对排序(由 <absolute-ordering>
标签表示)。
注意
每个参与订单计划的文档都通过顶级标签 <name>
进行标识。
部分排序是针对单个配置文档的。我们可以使用 <before>
和 <after>
标签来指示某个文档应该在另一个文档之前或之后处理。在 <before>
和 <after>
标签内部,我们可能有 <others/>
标签,它表示某个文档应该在所有其他排序的文档之前(或之后)处理。
下面是一个示例,其中包含文档 A
、B
、C
和别名 D
的 faces-config.xml
:
-
文档
C
需要在其他文档之前执行;因此,它将被首先执行:<name>C</name> <ordering> <before> <others/> </before> </ordering>
-
文档
B
没有指定顺序;因此,它将被执行第二个:<name>B</name>
-
文档
A
需要在文档B
之后执行;因此,它将被执行第三个:<name>A</name> <ordering> <after> <name>B</name> </after> </ordering>
-
文档
D
(faces-config.xml
) 最后执行,不需要任何排序规范。
顺序将是实现特定的配置资源,即 C
、B
、A
和 faces-config.xml
(D
)。
注意
排序过程(部分或绝对)对两个文档没有影响:相应实现(Mojarra 或 MyFaces)的默认配置资源始终首先处理,并且如果存在,faces-config.xml
始终最后处理。
可以使用几个阶段监听器和一些自定义消息来执行一个简单的测试。每个阶段监听器都在一个单独的文档中进行配置,并应用了一些部分排序方案。一个完整的示例可以在本章代码包中找到,并命名为 ch4_13_1
。控制台输出将揭示部分排序的效果。
注意
如果一个文档有排序要求但没有名称,则排序要求将被忽略。
绝对排序是通过 <absolute-ordering>
标签实现的。此标签只能出现在 faces-config.xml
中,并为我们提供了控制配置文档处理顺序的能力。例如,我们在 faces-config.xml
文档(别名文档 D
)中添加了绝对排序,如下所示:
<absolute-ordering>
<others/>
<name>C</name>
<name>B</name>
<name>A</name>
</absolute-ordering>
并且,处理顺序是:实现特定的配置资源,C
、B
、A
和 faces-config.xml
(D
)。
绝对排序的完整示例命名为 ch4_13_2
。
配置区域设置和资源包
包含消息的属性文件可以命名为 PlayerMessages.properties
。当我们有几种语言的消息时,我们可以为每种语言创建一个属性文件,并相应地命名。例如,对于英语,它将是 PlayerMessages_en.properties
,对于法语,它将是 PlayerMessages_fr.properties
。一个方便的存储位置是在应用程序源文件夹中直接或是在子文件夹中(或者在 NetBeans 中,在 Maven Web 应用程序项目的Other Sources
文件夹下)。资源包能够从这些文件中加载和显示消息。
资源包可以在本地或全局配置。本地资源包只为指定的页面加载属性文件。为此,使用以下 <f:loadBundle>
标签:
<f:loadBundle basename="players.msgs.PlayerMessages" var="msg"/>
全局资源包为所有 JSF 页面加载属性文件。在这种情况下,我们需要在 faces-config.xml
中进行声明性加载:
<application>
<resource-bundle>
<base-name>players.msgs.PlayerMessages</base-name>
<var>msg</var>
</resource-bundle>
</application>
当我们有多语言文件时,我们也必须指明区域设置。在本地,这通过在 <f:view>
标签中添加区域设置属性来完成,如下所示(这里我们指明法语):
<f:view locale="fr">
在全局上,在 faces-config.xml
中,我们通过 <default-locale>
指示默认区域设置,并通过 <supported-locale>
标签指示支持的区域设置列表:
<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>fr</supported-locale>
<supported-locale>en</supported-locale>
</locale-config>
<resource-bundle>
<base-name>players.msgs.PlayerMessages</base-name>
<var>msg</var>
</resource-bundle>
</application>
以编程方式,我们可以如下表示区域设置:
UIViewRoot viewRoot = FacesContext.getCurrentInstance().getViewRoot();
viewRoot.setLocale(new Locale("fr"));
属性文件中的简单条目如下所示:
HELLO = Hello from Rafael Nadal!
消息将使用 msg
变量(由 var
属性或 <var>
标签声明)显示:
#{msg['HELLO']}
但是,消息可以比静态文本更复杂。例如,它们可以按如下方式参数化:
HELLOPARAM = Hello from {0} {1}!
并且可以使用 <h:outputFormat>
标签替换参数:
<h:outputFormat value="#{msg['HELLOPARAM']}">
<f:param value="Roger" />
<f:param value="Federer" />
</h:outputFormat>
但是,关于以下类型的消息怎么办:
REGISTERED = You have {0} players registered!
当只有一个玩家时,消息如下所示:
You have 1 players registered!
这在语法上是错误的;因此,你需要使用类似于以下模式的模式:
REGISTERED = You have {0} {0, choice, 0#players|1#player|2#players} registered!
这将解决问题。这里使用的参数解释如下:
-
0, choice
: 取第一个参数并根据可用的格式选择输出 -
0#players
: 如果第一个参数包含 0(或以下),则应打印 "players" -
1#player
: 如果第一个参数包含 1,则应打印 "player" -
2#players
: 如果第一个参数包含 2(或以上),则应打印 "players"
你可以在本章节的代码包中找到名为 ch4_4
的完整示例。
注意
不要将 <resource-bundle>
标签与 <message-bundle>
标签混淆。前者用于注册自定义本地化静态文本,而后者用于注册自定义错误/信息/警告消息,这些消息由 <h:message>
和 <h:messages>
显示。
<message-bundle>
选项理想的使用方式如下:
<message-bundle>
players.msgs.ErrorsMessages
</message-bundle>
消息文件可以使用 <f:loadBundle>
标签加载。
配置验证器和转换器
数据验证是 JSF 应用程序(自 JSF 1.2 以来存在)的一个重要部分,因为它允许我们将业务逻辑与帮助我们从用户那里获取仅有效信息的繁琐检查分离。数据在过程验证阶段进行验证(如果将immediate
属性设置为true
,则此处理将在应用请求值阶段结束时发生)并且应在更新模型值阶段之前有效且准备好使用。
除了内置验证器之外,我们可以编写自己的自定义验证器。一个实现了Validator
接口并重写validate
方法的公共类被 JSF 识别为验证器。在 JSF 中有两种配置验证器的方式:使用@FacesValidator
注解或faces-config.xml
中的<validator>
标签。
假设我们使用@FacesValidator
配置了以下电子邮件验证器:
@FacesValidator(value = "emailValidator")
public class EmailValidator implements Validator {
@Override
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
...
}
}
}
注意
在 JSF 2.2 中,现在可以省略组件、转换器和验证器的名称,因此前面的代码将变为@FacesValidator
。在这里,我们需要注意,当省略名称时,JSF 将使用不带包名的类名,并且首字母小写。
如果你更喜欢使用faces-config.xml
,那么EmailValidator
可以配置如下:
<validator>
<validator-id>emailValidator</validator-id>
<validator-class>book.beans.EmailValidator</validator-class>
</validator>
现在,你可以轻松地将validator
链接到输入组件:
<h:inputText value="#{*bean property*}">
<f:validator validatorId="emailValidator"/>
</h:inputText>
做这件事的另一种方法是:
<h:inputText value="#{*bean property*}" validator="emailValidator"/>
EmailValidator
的完整示例可以在本章的代码包中找到,命名为ch4_3_1
。除了这个应用程序,作为一个额外的考虑,考虑两个当涉及验证器时有用的应用程序。第一个命名为ch4_2
,需要使用<f:attribute>
向验证器传递额外参数,另一个命名为ch4_11
,它是使用自定义验证器和<f:attribute>
标签验证多个字段的示例。后者也是使用PostValidateEvent
系统事件开发的——稍后在本章的配置系统事件监听器部分查看。
好吧,关于 JSF 验证器的文章有很多,但只有少数讨论了 JSF 验证器中的注入。默认情况下,JSF 2.0 不支持验证器中的注入,因为只有管理 Bean 是注入目标,但有一些技巧可以将依赖注入纳入讨论。
为了获得一个可用于注入的验证器,你需要应用以下修改,这基本上将验证器转换成一个 Bean:
-
将
@FacesValidator
注解替换为@Named
或@ManagedBean
(甚至可以使用 Spring 注解@Component
)。 -
将 Bean 放入请求作用域(使用适当的
@RequestScoped
注解)。@Named(value="emailValidator") @RequestScoped public class EmailValidator implements Validator { @Override public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { ... } }
-
使用适当的 EL 表达式引用它:
<h:inputText value="#{*bean property*}" validator="#{emailValidator.validate}" />
完成!现在,你可以在验证器中使用@Inject
。
完整示例可以在本章的代码包中找到,命名为ch4_3_2
。
一个更复杂的任务是使用@EJB
注入企业 JavaBean(EJB)会话 bean。在这种情况下,我们需要从Java 命名和目录接口(JNDI)手动查找 EJB 会话 bean。当 EJB 部署在Web 应用程序存档(WAR)中时,查找通常是以下类型:
java:app/*app-name*/*bean-name*[! *fully-qualified-interface-name*]
当 EJB 位于企业存档(EAR)中时,常见的查找类型如下:
java:global/*app-name*/*module-name*/*bean-name*[! *fully-qualified-interface-name*]
当 EJB 部署在 WAR 中时,使用以下方法:
@FacesValidator
public class EmailValidator implements Validator {
private LoginEJBBean loginEJBBean;
@Override
public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
try {
loginEJBBean = (LoginEJBBean) new InitialContext().
lookup("java:app/ch4_3_5/LoginEJBBean");
} catch (NamingException e) {
throw new ExceptionInInitializerError(e);
}
...
当 EJB 部署在 EAR 中时,使用以下方法:
@FacesValidator public class EmailValidator implements Validator {
private LoginEJBBean loginEJBBean;
@Override
public void validate(FacesContext context,
UIComponent component, Object value) throws ValidatorException {
try {
loginEJBBean = (LoginEJBBean) new InitialContext().
lookup("java:global/ch4_3_6/ch4_3_6-ejb/LoginEJBBean");
} catch (NamingException e) {
throw new ExceptionInInitializerError(e);
}
...
你可以在这个章节的代码包中找到完整的示例。部署在 WAR 中的 EJB 示例命名为ch4_3_5
,而部署在 EAR 情况下的 EJB 命名为ch4_3_6
。
这些方法只是将依赖注入引入验证器的一些附加方法,这似乎是 JSF 2.0 中唯一的解决方案。从 JSF 2.2 开始,注入可以在许多更多的地方实现,但正如规范所说,转换器和验证器仍然不是注入目标。这似乎将在 JSF 2.3 中可用。
与这种肯定相反,我尝试编写一个验证器并使用注入,就像它应该原生工作一样。我使用了@Inject
如下,其中LoginBean
是一个 CDI 应用范围 bean:
@FacesValidator
public class EmailValidator implements Validator {
@Inject
LoginBean loginBean;
@Override
public void validate(FacesContext context,
UIComponent component, Object value) throws ValidatorException {
...
此外,我还尝试使用@EJB
和@Inject
注入 EJB,其中LoginEJBBean
是一个无状态会话 bean,如下面的代码所示:
@FacesValidator
public class EmailValidator implements Validator {
@EJB
LoginEJBBean loginEJBBean;
//@Inject
//LoginEJBBean loginEJBBean;
@Override
public void validate(FacesContext context,
UIComponent component, Object value) throws ValidatorException {
...
我必须承认,我原本期待看到注入的资源为null
值,但令人惊讶的是,在所有情况下,一切工作都如预期般顺利。有传言称,最初,验证器和转换器的注入机制是在 JSF 2.2 中添加的,但在最后一刻因为一些测试失败而被移除。即使前面的示例工作得很好,这也并不意味着在生产中使用这种方法是一个好习惯。你最好等到 JSF 团队保证后再使用。
注意
如果你喜欢 OmniFaces,那么你可以使用@Inject
和@EJB
与@FacesValidator
。这个伟大的功能是从版本 1.6 开始添加的(showcase.omnifaces.org/cdi/FacesValidator
)。此外,MyFaces CODI([myfaces.apache.org/extensions/cdi/
](http://myfaces.apache.org/extensions/cdi/))也可以作为一个解决方案,但它需要一个额外的@Advanced
注解。
完整的示例可以在本章的代码包中找到,它们分别命名为ch4_3_3
(Web 应用程序)和ch4_3_4
(企业应用程序)。
当讨论转换器时,让我们记住,两个UIInput
实例之间的转换发生在处理验证阶段(默认),可以通过将immediate
属性设置为true
将其移动到应用请求值阶段。对于UIOutput
,转换发生在渲染响应阶段。
除了内置的转换器外,我们还可以编写自定义转换器。一个实现了Converter
接口并重写了getAsObject
和getAsString
方法的公共类被 JSF 识别为转换器。在 JSF 中配置转换器有两种方式:使用@FacesConverter
注解或faces-config.xml
中的<converter>
标签。
关于我们使用@FacesConverter
配置的以下转换器(请记住,JSF 2.2 不需要value
属性):
@FacesConverter(value="playerConverter")
public class PlayerConverter implements Converter{
@Override
public Object getAsObject(FacesContext context,
UIComponent component, String value) {
PlayerName playerName = new
PlayerName(value.toLowerCase(), value.toUpperCase());
return playerName;
}
@Override
public String getAsString(FacesContext context,
UIComponent component, Object value) {
PlayerName playerName = (PlayerName)value;
return "Mr. " + playerName.getUppercase();
}
}
如果你更喜欢使用faces-config.xml
,则PlayerConverter
可以配置如下:
<converter>
<converter-id>playerConverter</converter-id>
<converter-class>book.beans.PlayerConverter</converter-class>
</converter>
现在,你可以轻松地将转换器链接到输入组件如下:
<h:inputText value="#{*bean property*}">
<f:converter converterId="playerConverter"/>
</h:inputText>
做这件事的另一种方法是:
<h:inputText value="#{*bean property*}" converter="playerConverter"/>
此外,你可以这样写:
<h:inputText value="#{*bean property*}"/>
如果你使用forClass
属性配置转换器,则跳过value
属性如下:
@FacesConverter(forClass=PlayerName.class)
PlayerConverter
的完整示例可以在本章的代码包中找到,并命名为ch4_6_1
。
谈到依赖注入,将转换器作为目标与验证器的情况非常相似:
-
将
@FacesConverter
注解替换为@Named
和@ManagedBean
(对于 Spring,你也可以使用@Component
) -
将 Bean 放入请求作用域(使用适当的
@RequestScoped
注解)如下:@Named(value="playerConverter") @RequestScoped public class PlayerConverter implements Converter{ @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { ... } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { ... } }
-
使用以下适当的 EL 表达式来引用它:
<h:inputText value="#{*bean property*}" converter="#{playerConverter}"/>
完整的示例可以在本章的代码包中找到,并命名为ch4_6_2
。EJB 可以通过从 JNDI 查找 EJB 会话 Bean 来注入到转换器中。请参考示例ch4_6_5
(EAR 中的 EJB)和ch4_6_6
(WAR 中的 EJB)。
-
在
ch4_6_5
应用程序中的以下代码块;RandomEJBBean
是一个无状态会话 Bean:@FacesConverter(value = "playerConverter") public class PlayerConverter implements Converter { private static RandomEJBBean randomEJBBean; static { try { randomEJBBean = (RandomEJBBean) new InitialContext(). lookup("java:global/ch4_6_5/ch4_6_5-ejb/RandomEJBBean"); } catch (NamingException e) { throw new ExceptionInInitializerError(e); } } ...
-
在
ch4_6_6
应用程序中的以下代码块;RandomEJBBean
是一个无状态会话 Bean:@FacesConverter(value = "playerConverter") public class PlayerConverter implements Converter { private static RandomEJBBean randomEJBBean; static { try { randomEJBBean = (RandomEJBBean) new InitialContext(). lookup("java:app/ch4_6_6/RandomEJBBean"); } catch (NamingException e) { throw new ExceptionInInitializerError(e); } } ...
此外,在 GlassFish 4.0 和 Mojarra 2.2.x 中,我能够成功运行两个使用转换器注入的应用程序,没有任何复杂的解决方案。请参阅示例ch4_6_3
和ch4_6_4
。但请记住,这种方法尚未正式采用。
配置导航
从 JSF 2 开始,导航变得容易得多。可以使用以下方式完成导航:
-
隐式导航
-
条件导航
-
预先导航
-
程序化导航
我们可以谈论几个小时关于 JSF 导航,但有一些黄金法则可以帮助我们在需要选择GET
和POST
时避免最常见的错误。了解以下内容可能很有用:
-
建议使用
GET
请求进行页面间导航、搜索表单、希望可见和可书签的 URL,以及通常对于任何幂等请求。根据规范,GET
、HEAD
、PUT
、DELETE
、OPTIONS
和TRACE
是幂等的。 -
对于不应可书签的请求,重复使用相同的视图(使用转发,而不是重定向)。
-
对于不应可书签但具有可书签目标的请求,使用
POST
并重定向。
隐式导航
隐式导航将导航结果解释为目标视图 ID。最简单的隐式导航情况是在你执行操作且没有指示导航时由 JSF 本身完成的。在这种情况下,JSF 将通过HTTP POST
将表单发送回当前视图(再次渲染当前视图)。
在faces-config.xml
中没有声明性导航的情况下,我们可以轻松编写导航情况,如下所示,其中 JSF 2 知道如何将outcome
(或action
值)作为目标页面名称处理:
<h:outputLink value="success.xhtml">Success</h:outputLink>
<h:link value="Success" outcome="success"/>
<h:button value="Success" outcome="success"/>
<h:commandButton value="Success" action="success"/>
<h:commandLink value="Success" action="success"/>
注意
如果success.xhtml
页面存在,则所有给定示例都将导航到该页面。<h:outputLink>
元素将独立于 JSF 进行导航(这意味着它不与 JSF 交互)。<h:link>
和<h:button>
元素将通过可书签的GET
请求进行导航,并且不能进行表单提交(正如你将看到的,这实际上是预导航)。<h:commandButton>
和<h:commandLink>
元素是 JSF 应用内导航的主要组件。它们触发POST
请求并且能够进行表单提交。每次你想在 URL 中添加应用程序上下文路径(例如,通过<h:outputLink>
生成的 URL),你可以使用 JSF 2.2 的ExternalContext.getApplicationContextPath
方法。例如,看看以下代码:
<h:outputLink value="#{facesContext.externalContext. applicationContextPath}/next.xhtml">Next</h:outputLink>
该声明的版本如下——得益于隐式导航,此代码不是必需的:
<navigation-rule>
<from-view-id>*</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/success.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<h:link>
和<h:button>
的结果在Render Response
阶段进行评估;因此,URL 可以从相应视图的开始处直接获得。另一方面,当按钮(<h:commandButton>
)或链接(<h:commandLink>
)被点击时,JSF 将action
值success
与 XHTML 扩展名合并,并在当前页面目录中找到视图名称success.xhtml
。
注意
支持通配符("*")来指定适用于所有页面的导航规则。对于登出页面来说可能很有用。
导航情况也可以通过一个 bean 方法进行,如下所示:
<h:commandButton value="Success" action="#{playerBean.playerDone()}"/>
此外,PlayerBean
方法定义如下:
public String playerDone() {
logger.log(Level.INFO, "playerDone method called ...");
return "success";
}
在这些示例中,outcome
/action
值和目标视图 ID 相匹配。然而,outcome
/action
值和目标视图 ID 并不总是那么简单。即使它们没有相同的根,outcome
/action
值也用于确定目标视图 ID。例如,参考以下代码:
<h:commandButton value="Success" action="done"/>
上述代码指示done.xhtml
页面,但该页面不存在;因此,不会发生导航。我们需要在faces-config.xml
中添加一个声明性导航规则,以便将action
值(或通过预导航获取的outcome
值,我们很快就会看到)done
与目标视图 IDsuccess.xhtml
关联起来。此导航规则可以在以下代码中看到:
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-outcome>done</from-outcome>
<to-view-id>/success.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
如果“bean”方法返回结果done
,则导航规则将按以下方式修改:
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-action>#{playerBean.playerDone()}</from-action>
<from-outcome>done</from-outcome>
<to-view-id>/success.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
默认情况下,在前进和重定向之间,JSF 将通过前进机制(HTTP POST
)从一个页面导航到另一个页面。当 JSF 接收到用户操作时,它将转发用户到指定的目标页面,这意味着浏览器显示的 URL 不会更新以反映当前目标。保持浏览器 URL 更新意味着页面重定向机制;在这种情况下,JSF 将浏览器委托发送一个单独的GET
请求到目标页面。
你可以通过将faces-redirect=true
参数附加到结果查询字符串中来使用页面重定向机制,如下所示:
<h:commandButton value="Success" action="success?faces-redirect=true;"/>
或者,你可以在导航规则内部使用<redirect/>
标签,如下所示:
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-outcome>done</from-outcome>
<to-view-id>/success.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
注意
在前进情况下,浏览器 URL 不会更新(与导航 URL 相比落后一个步骤),但只有一个请求。在重定向情况下,浏览器 URL 是更新的,但有两次请求。由于前进只需要一个请求,所以它比页面重定向更快。速度较低,但页面重定向解决了在 Post-Redirect-Get 设计模式中发现的重复表单提交问题。当然,对于<h:link>
、<h:button>
和<h:outputLink>
来说并非如此。
这些示例被分组在本章代码包中的ch4_5_1
应用程序中。
条件导航
条件导航允许我们指定选择所需导航情况的前提条件;为了接受导航情况,必须满足前提条件。为此,我们使用<if>
标签作为<navigation-case>
标签的子标签,并使用可以评估为布尔值的 EL 表达式;在这里,true
值匹配导航情况。
让我们有一个简单的按钮,用于将用户登录到应用程序。这是通过以下代码完成的:
<h:commandButton value="Login" action="#{playerBean.playerLogin()}"/>
当登录按钮被点击时,JSF 将调用playerLogin
方法。此方法不会返回结果,实际上它返回void
。在这个例子中,我们通过随机数模拟登录过程,并相应地设置布尔值login
,如下面的代码所示:
private boolean login = false;
...
public boolean isLogin() {
return login;
}
public void setLogin(boolean login) {
this.login = login;
}
public void playerLogin() {
Random random = new Random();
int r = random.nextInt(10);
if (r <= 5) {
login = false;
} else {
login = true;
}
}
接下来,我们可以使用<if>
标签来决定是否导航到success.xhtml
页面(相当于login
等于true
)或导航到failed.xhtml
页面(相当于login
等于false
):
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-action>#{playerBean.playerLogin()}</from-action>
<if>#{playerBean.login}</if>
<to-view-id>/success.xhtml</to-view-id>
<redirect/>
</navigation-case>
<navigation-case>
<from-action>#{playerBean.playerLogin()}</from-action>
<if>#{!playerBean.login}</if>
<to-view-id>/failed.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
注意
在条件导航中,即使结果为null
或void
,也会评估导航情况。请注意,没有<else>
标签或多个条件检查;因此,在这种情况下,你必须模拟一个switch
语句。如果你想在任何情况下简单地匹配null
结果,那么你可以使用以下类型的条件:<if>#{true}</if>
。
此外,导航规则的顺序会影响导航流程;因此,优先考虑条件是一个好的做法。
你可以在本章代码包中找到完整的示例,名称为ch4_5_2
。
我们可以通过将<to-view-id>
标签的静态值替换为 EL 表达式来编写不带<if>
标签的条件导航情况。为此,我们需要按照以下方式替换:
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-action>#{playerBean.playerLogin()}</from-action>
<to-view-id>#{playerBean.navigateHelper()}</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
注意,这并不是真正的条件导航(因为缺少<if>
标签);因此,我们需要从playerLogin
方法返回一个结果:
public String playerLogin() {
Random random = new Random();
int r = random.nextInt(10);
login = r > 5;
return "done";
}
当login
属性被设置并且返回结果done
时,JSF 将遵循前面的导航情况,并调用navigateHelper
方法:
public String navigateHelper() {
if (!login) {
return "failed.xhtml";
} else {
return "success.xhtml";
}
}
在实际应用中,返回结果的方法和选择导航情况的方法可能位于不同的 bean 中。如果你考虑到你可以向决策方法传递参数,那么许多导航情况都可以解决。
你可以在本章的代码包中找到完整的示例,名称为ch4_5_3
。
预先导航
预先导航从 JSF 2.0 开始可用。导航规则更加宽容,并且它们在Render Response
阶段而不是Invoke Application
阶段进行评估。
注意
这被称为预定导航或预先导航。当前视图 ID 和指定的结果用于确定目标视图 ID。之后,目标视图 ID 被转换为可书签 URL,并用作超链接的目标。实际上,URL 是在没有用户交互的情况下准备的。
预先导航的主要用途出现在可书签组件标签<h:link>
和<h:button>
中。例如,以下两个是预先导航的经典示例:
<h:link value="Success" outcome="success"/>
<h:button value="Success" outcome="success"/>
当应用启动时,你可以检查页面的源代码,以查看在<h:link>
的情况下,相应的 URL 是如何在 HTML 标签<a>
中映射的,以及<h:button>
的情况下,HTML 标签<input type="button">
是如何映射的。即使你从未使用过这些 URL,它们也已经准备好了。
好吧,在 JSF 2.0 之前,导航规则是显式地属于POST
请求的领域(NavigationHandler.handleNavigation
在幕后执行这项脏活),但基于 GET 的导航和可书签支持将导航提升到了另一个灵活性和透明度的层次(例如,ConfigurableNavigationHandler
API)。
这里有趣的部分是如何组装 URL 的查询字符串。最简单的情况是隐式查询字符串参数,如下面的代码所示:
<h:link value="Done" outcome="done?id=done"/>
在第二章中,JSF 中的通信,你看到了如何使用<f:param>
和<f:viewParam>
构建查询字符串。
另一种方法是在导航情况中使用嵌套在<redirect>
标签中的<view-param>
标签。例如,我们可以在导航规则中向重定向 URL 添加查询字符串参数。让我们创建以下按钮:
<h:commandButton value="Success" action="#{playerBean.playerDone()}"/>
此外,还有一个名为playerDone
的愚蠢方法如下:
private String player;
public String getPlayer() {
return player;
}
public void setPlayer(String player) {
this.player = player;
}
public String playerDone() {
player = "Rafael Nadal";
return "done";
}
现在,我们可以将player
属性值(当然,你也可以添加任何其他值)作为重定向导航 URL 查询字符串中的参数:
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-action>#{playerBean.playerDone()}</from-action>
<from-outcome>done</from-outcome>
<to-view-id>/success.xhtml</to-view-id>
<redirect>
<view-param>
<name>playerparam</name>
<value>#{playerBean.player}</value>
</view-param>
</redirect>
</navigation-case>
</navigation-rule>
这样的 URL 将是以下格式(注意请求参数是如何根据导航规则附加的)http://
主机:
端口/
应用名/faces/success.xhtml?playerparam=Rafael+Nadal
。
playerparam
值将通过param
隐式对象可用:
#{param['playerparam']}
你可以在本章的代码包中找到完整的示例,名称为ch4_5_4
。
程序化导航
有时,你需要直接从应用程序控制导航。JSF 提供了NavigationHandler
和ConfigurableNavigationHandler
API,可用于访问导航案例、自定义导航处理程序、条件导航等任务。值得注意的是,从程序化的角度来看,我们可以执行以下操作:
-
使用以下代码获取对导航处理程序(
NavigationHandler
)的访问权限:FacesContext context = FacesContext.getCurrentInstance(); Application application = context.getApplication(); NavigationHandler nh = application.getNavigationHandler();
-
使用
NavigationHandler
如下调用导航案例:nh.handleNavigation(*context,fromAction,outcome*); nh.handleNavigation(*context,fromAction,outcome,toFlowDocumentId*);
-
使用以下代码访问
ConfigurableNavigationHandler
API:ConfigurableNavigationHandler cnh = (ConfigurableNavigationHandler) FacesContext.getCurrentInstance().getApplication().getNavigationHandler();
-
使用
ConfigurableNavigationHandler
如下调用导航案例:cnh.handleNavigation(*context,fromAction,outcome*); cnh.handleNavigation(*context,fromAction,outcome,toFlowDocumentId*);
-
通过以下代码根据动作表达式签名和结果检索一个
NavigationCase
对象:NavigationCase case = cnh.getNavigationCase(*context,fromAction,outcome*); NavigationCase case = cnh.getNavigationCase(*context,fromAction,outcome, toFlowDocumentId*);
-
将所有导航规则访问到
Map<String, Set<NavigationCase>>
中,其中键是<from-view-id>
的值如下:Map<String, Set<NavigationCase>> cases = cnh.getNavigationCases();
注意
从 JSF 2.2 版本开始,我们为许多类提供了包装器,这些包装器提供了基本实现并帮助开发者扩展这些类,仅覆盖必要的函数。其中,我们有一个名为NavigationHandlerWrapper
的NavigationHandler
包装类,一个名为ConfigurableNavigationHandlerWrapper
的ConfigurableNavigationHandler
包装类,以及一个名为NavigationCaseWrapper
的NavigationCase
包装类。
在第三章中,JSF 作用域 – 生命周期和在管理 Bean 通信中的应用,你在流程作用域部分看到了ConfigurableNavigationHandler
的自定义实现,在使用导航处理程序控制自定义作用域生命周期部分看到了NavigationHandler
的自定义实现。
配置动作监听器
动作监听器是 JSF 提供的一种处理动作事件的优秀设施。通常,动作监听器通过actionListener
属性附加到命令按钮(<h:commandButton>
)或命令链接(<h:commandLink>
)。
当按钮/链接被点击时,JSF 在 Invoke Application 阶段调用动作监听器。请注意,如果您使用 immediate="true"
,则动作监听器将在 Apply Request Values 阶段调用。作为监听器的指示方法应该是公开的,应该返回 void
,并接受一个 ActionEvent
对象(此对象可以用来访问调用动作的组件),它可以执行特定任务。当其执行完成后,JSF 将调用由 action
属性绑定的方法(如果存在的话!)此方法负责指示导航情况。动作监听器方法可以更改动作方法返回的响应。
注意
作为一种实践,actionListener
用于在真正的业务和导航任务之前“玩一玩”,这是 action
的责任。因此,不要滥用 actionListener
来解决业务逻辑任务!
让我们用一个简单的命令按钮的例子来说明,它使用动作监听器,如下所示:
<h:commandButton value="Player Listener 1"
actionListener="#{playerBean.playerListener}"
action="#{playerBean.playerDone()}"/>
PlayerBean
包含以下代码:
public void playerListener(ActionEvent e) {
logger.log(Level.INFO, "playerListener method called ...");
}
public String playerDone() {
logger.log(Level.INFO, "playerDone method called ...");
return "done";
}
好吧,日志消息揭示了调用顺序如下:
INFO: playerListener method called ...
INFO: playerDone method called ...
这种类型的监听器不需要任何特殊配置。
另一种类型的监听器可以通过实现 ActionListener
接口并重写 processAction
方法来编写。在这种情况下,我们需要使用 <f:actionListener>
标签将动作监听器附加到命令按钮/链接:
<h:commandButton value="Player Listener 2"
action="#{playerBean.playerDone()}">
<f:actionListener type="book.beans.PlayerListener"/>
</h:commandButton>
好吧,PlayerListener
定义如下:
public class PlayerListener implements ActionListener {
private static final Logger logger =
Logger.getLogger(PlayerListener.class.getName());
@Override
public void processAction(ActionEvent event)
throws AbortProcessingException {
logger.log(Level.INFO, "Player listener class called ...");
}
}
并且,日志消息的输出结果如下:
INFO: Player listener class called ...
INFO: playerDone method called ...
再次强调,这类监听器不需要任何特殊配置。
注意
从 JSF 2.2 开始,ActionListener
接口被封装在一个简单的实现中,命名为 ActionListenerWrapper
。您需要扩展此类并重写 getWrapped
方法以返回封装的实例。
例如,PlayerListener
可以通过以下包装器调用:
public class PlayerListenerW extends ActionListenerWrapper {
PlayerListener playerListener = new PlayerListener();
@Override
public ActionListener getWrapped() {
return playerListener;
}
}
您甚至可以将这两个监听器组合成一个单独的命令按钮,如下所示:
<h:commandButton value="Player Listener 3"
actionListener="#{playerBean.playerListener}"
action="#{playerBean.playerDone()}">
<f:actionListener type="book.beans.PlayerListener"/>
</h:commandButton>
在这种情况下,日志消息如下:
INFO: playerListener method called ...
INFO: Player listener class called ...
INFO: playerDone method called ...
注意
好吧,这个例子给我们一个重要的规则:动作监听器在 action
之前被调用,并且与它们在组件内部声明的顺序相同。
应用程序动作监听器
到目前为止一切顺利!最后一种动作监听器被称为应用程序动作监听器。它们在应用程序级别设置,并且即使对于未明确指定任何动作监听器的命令按钮/链接,JSF 也会调用它们。这样的动作监听器可能看起来像以下代码:
public class ApplicationPlayerListener implements ActionListener {
private static final Logger logger =
Logger.getLogger(PlayerListener.class.getName());
private ActionListener actionListener;
public ApplicationPlayerListener() {
}
public ApplicationPlayerListener(ActionListener actionListener) {
this.actionListener = actionListener;
}
@Override
public void processAction(ActionEvent event)
throws AbortProcessingException {
logger.log(Level.INFO, "Application player listener class called ...");
actionListener.processAction(event);
}
}
即使没有指定,此动作监听器也会为命令按钮/链接调用,如下所示:
<h:commandButton value="Player Listener 4"
action="#{playerBean.playerDone()}" />
输出结果如下:
INFO: Application player listener class called ...
INFO: playerDone method called ...
在 JSF 2.2 中,我们可以通过以下方式扩展 ActionListenerWrapper
来编写此实现:
public class ApplicationPlayerListenerW extends ActionListenerWrapper {
private ActionListener actionListener;
private static final Logger logger =
Logger.getLogger(ApplicationPlayerListenerW.class.getName());
public ApplicationPlayerListenerW(){}
public ApplicationPlayerListenerW(ActionListener actionListener){
this.actionListener = actionListener;
}
@Override
public void processAction(ActionEvent event)
throws AbortProcessingException {
logger.log(Level.INFO, "Application player listener
(wrapper) class called ...");
getWrapped().processAction(event);
}
@Override
public ActionListener getWrapped() {
return this.actionListener;
}
}
注意
应用程序动作监听器在通过 actionListener
属性或 <f:actionListener>
标签显式设置的动作监听器之后调用。
为了被调用,这些监听器必须在faces-config.xml
中进行配置。例如,前面的监听器可以配置如下:
<application>
<action-listener>book.beans.ApplicationPlayerListener</action-listener>
</application>
注意
当你使用应用程序动作监听器时,重要的是要记住以下几点:
-
应用程序动作监听器不能调用其他监听器。
-
应用程序动作监听器负责处理
action
属性。 -
应用程序动作监听器不能捕获来自其他监听器的事件。
你可能已经注意到动作监听器会抛出AbortProcessingException
异常。当这个异常出现时,JSF 将直接跳转到渲染响应并忽略后续的动作监听器。错误默认情况下会被吞没,所以不要期望看到它!你可以通过改变处理异常的默认机制来让它变得可见。
你可能认为动作监听器很酷!等你看到从 JSF 2.2 开始的功能时,你就会知道了。我们可以在动作监听器类中使用注入机制来注入 CDI 管理 Bean 和 EJB。例如,以下代码中显示的简单 Bean:
@Named
@RequestScoped
public class DemoBean {
private String demo = "TEST INJECTION VALUE ...";
public String getDemo() {
return demo;
}
public void setDemo(String demo) {
this.demo = demo;
}
}
这个 Bean 可以按照以下方式注入到我们的应用程序动作监听器中:
public class ApplicationPlayerListener implements ActionListener {
@Inject
private DemoBean demoBean;
...
显然,这个功能为应用程序的实现开辟了新的视角。而且,正如你将看到的,注入机制可用于许多其他在 JSF 2.0 中不支持它的 JSF 组件。
一个名为ch4_1
的完整示例可以在本章的代码包中找到。
配置系统事件监听器
JSF 2.0 允许我们使用系统事件。这些是在请求处理生命周期中的任意点由任意对象触发的事件。由于这些事件的数量相当大,你在这里不会看到它们全部被覆盖,但接下来的五个示例应该可以阐明系统事件的基本方面。你可以在javax.faces.event
包中找到所有这些事件。
使用<f:event>
使用系统事件监听器最简单的方法是在<f:event>
标签的listener
属性中传递管理 Bean 方法的名称。例如,PostValidateEvent
是在所有组件验证完成后触发的一个系统事件。这可以用来验证多个组件。假设,用户提交了一个包含他的名字、姓氏、银行账户以及该银行账户确认(如密码,需要输入两次以确认)的表单。为了检查是否在两个字段中输入了相同的银行账户,我们可以使用PostValidateEvent
,如下所示:
<h:body>
<h:form id="registerForm">
<f:event listener="#{playersBean.validateAccount}"
type="postValidate" />
...
<h:inputText id="bankAccountId" value="#{playersBean.bank}"
required="true" />
<h:message for="bankAccountId" style="color: red;" />
<h:inputText id="confirmBankAccountId"
value="#{playersBean.cbank}" required="true" />
<h:message for="confirmBankAccountId" style="color: red;" />
<h:commandButton action="done" value="Send" />
</h:form>
</h:body>
现在,在PlayersBean
中,我们需要按照以下方式实现validateAccount
方法:
public void validateAccount(ComponentSystemEvent event) {
UIComponent uis = event.getComponent();
//obtain bank account
String bankAccount = null;
UIInput uiBankAccount = (UIInput)
uis.findComponent("bankAccountId");
Object bankAccountObj = uiBankAccount.getLocalValue();
if (bankAccountObj != null) {
bankAccount = String.valueOf(bankAccountObj).trim();
}
//obtain bank account confirmation
String bankAccountC = null;
UIInput uiBankAccountC = (UIInput)
uis.findComponent("confirmBankAccountId");
Object bankAccountCObj = uiBankAccountC.getLocalValue();
if (bankAccountCObj != null) {
bankAccountC = String.valueOf(bankAccountCObj).trim();
}
if ((bankAccount != null) && (bankAccountC != null)) {
if (!bankAccount.equals(bankAccountC)) {
FacesContext facesContext =
FacesContext.getCurrentInstance();
FacesMessage facesMessage = new FacesMessage("Bank
account must match bank account confirmation !");
facesMessage.setSeverity(FacesMessage.SEVERITY_ERROR);
facesContext.addMessage(uiBankAccount.getClientId(),
facesMessage);
facesContext.renderResponse();
}
}
}
完成!如果你没有提供相同的银行账户,那么你会看到相应的消息。完整的应用程序名为ch4_7
。
实现系统事件监听器
处理系统事件的另一种方法基于以下步骤:
-
实现接口
SystemEventListener
。 -
覆盖
processEvent
和isListenerForSource
方法。 -
在
faces-config.xml
中配置监听器。
已注册的系统事件可以由许多种类的源(组件)触发。我们可以在isListenerForSource
方法中排序和接受某些源。当监听器应该接收作为参数传递给它的源的事件时(通常使用instanceof
运算符进行简单测试即可完成工作),返回true
。当一个源被接受时,会调用processEvent
方法,我们可以添加自定义行为。
例如,假设我们想要移除由 JSF 包含的某些资源,例如 CSS 样式表或 JS 脚本(甚至可能是第三方库添加的资源)。关于 CSS 资源,它们始终在 HTML 页面的HEAD
部分渲染。了解这一点后,我们可以配置我们的监听器,使其在事件源是UIViewRoot
实例时执行。进一步地,我们利用 JSF API 遍历 CSS 资源并移除其中的一些(或全部)。我们的监听器代码相当简单,如下所示:
public class ResourcesListener implements SystemEventListener {
@Override
public void processEvent(SystemEvent event)
throws AbortProcessingException {
FacesContext context = FacesContext.getCurrentInstance();
int i = context.getViewRoot().
getComponentResources(context, "HEAD").size() - 1;
while (i >= 0) {
UIComponent resource = context.getViewRoot().
getComponentResources(context, "HEAD").get(i);
String resourceLibrary = (String)
resource.getAttributes().get("library");
String resourceName = (String) resource.getAttributes().get("name");
if ((resourceLibrary.equals("default")) &&
(resourceName.equals("css/roger.css"))) {
context.getViewRoot().removeComponentResource
(context, resource, "HEAD");
}
i--;
}
}
@Override
public boolean isListenerForSource(Object source) {
return (source instanceof UIViewRoot);
}
}
听众应该在faces-config.xml
中进行配置,如下所示:
<system-event-listener>
<system-event-listener-class>
book.beans.ResourcesListener
</system-event-listener-class>
<system-event-class>
javax.faces.event.PreRenderViewEvent
</system-event-class>
<source-class>
javax.faces.component.UIViewRoot
</source-class>
</system-event-listener>
因此,即使最初我们写下以下代码:
<h:head>
<h:outputStylesheet library="default" name="css/rafa.css"/>
<h:outputStylesheet library="default" name="css/roger.css"/>
</h:head>
JSF 将渲染以下代码:
<head>
<title></title>
<link type="text/css" rel="stylesheet"
href="/ch4_9_1/faces/javax.faces.resource/css/rafa.css?ln=default">
</head>
注意
<source-class>
标签实际上是在覆盖isListenerForSource
方法中的条件。因此,您可以从isListenerForSource
方法中始终返回true
并使用此标签,或者反之亦然。
您可以在本章的代码包中找到完整的示例,命名为ch4_9_1
。
现在,让我们看看另一个例子。当某些表单输入字段无效时,一个常见的做法是将背景色设置为红色。在 JSF 2.0 中,我们可以使用以下代码来实现:
.ui-invalid {
background-color:red
}
...
<h:inputText value="#{...}" required="true" styleClass="#{not component.valid ? 'ui-invalid' : ''}" />
嗯,这真的很酷!但是,如果表单有几个输入字段,那么我们就必须反复重复条件,这就不那么酷了!但是,通过一点魔法,我们可以泛化这种行为。我们可以编写一个监听器,它只从UIInput
对象执行,并根据isValid
方法返回的结果修改它们的styleClass
属性:
public class InputValidationListener implements SystemEventListener {
@Override
public void processEvent(SystemEvent event)
throws AbortProcessingException {
UIInput inputSource = (UIInput) event.getSource();
if(!inputSource.isValid()) {
inputSource.getAttributes().put("styleClass", "ui-invalid");
}
}
@Override
public boolean isListenerForSource(Object source) {
return (source instanceof UIInput);
}
}
当然,这很简单,没有什么需要解释的。实际上,关键在于配置文件,因为我们必须从众多可用事件中选择正确的事件系统。由于我们需要将无效输入字段的背景色设置为红色,正确的选择应该是PostValidateEvent
,如下面的代码所示:
<system-event-listener>
<system-event-listener-class>
book.beans.InputValidationListener
</system-event-listener-class>
<system-event-class>
javax.faces.event.PostValidateEvent
</system-event-class>
<source-class>
javax.faces.component.html.HtmlInputText
</source-class>
</system-event-listener>
完成!一个功能示例可以在本章的代码包中找到,命名为ch4_9_3
。以下列出了 JSF 2.2 对该配置的程序化反射:
public class Initializer extends
ApplicationConfigurationPopulator {
@Override
public void populateApplicationConfiguration
(Document toPopulate) {
String ns = toPopulate.getDocumentElement().getNamespaceURI();
Element applicationEl = toPopulate.
createElementNS(ns, "application");
Element systemeventlistenerEl = toPopulate.
createElementNS(ns, "system-event-listener");
Element systemeventlistenerclassEl =
toPopulate.createElementNS(ns,
"system-event-listener-class");
systemeventlistenerclassEl.appendChild
(toPopulate.createTextNode
("book.beans.InputValidationListener"));
Element systemeventclassEl = toPopulate.
createElementNS(ns, "system-event-class");
systemeventclassEl.appendChild(toPopulate.
createTextNode("javax.faces.event.PostValidateEvent"));
Element sourceclassEl = toPopulate.
createElementNS(ns, "source-class");
sourceclassEl.appendChild(toPopulate.createTextNode
("javax.faces.component.html.HtmlInputText"));
systemeventlistenerEl.appendChild(systemeventlistenerclassEl);
systemeventlistenerEl.appendChild(systemeventclassEl);
systemeventlistenerEl.appendChild(sourceclassEl);
applicationEl.appendChild(systemeventlistenerEl);
toPopulate.getDocumentElement().appendChild(applicationEl);
//serializeFacesConfig(toPopulate, "D://faces-config.xml");
}
...
}
完整的应用程序命名为ch4_14_2
。
注意
从 JSF 2.2 开始,我们可以在系统事件监听器中使用依赖注入(@Inject
和@EJB
)。例如,我们可以通过 CDI 豆或 EJB 会话豆的注入来传递需要从HEAD
中移除的 CSS 资源,而不是硬编码。您可以在本章的代码包中找到一个完整的示例。这个示例命名为ch4_9_2
。
在将 CSS 类名映射到 CDI Bean(例如StyleResourcesBean
)或 EJB Bean(例如StyleResourcesEJBBean
)之后,你可以使用以下任何一种注入方式:
@Inject
StyleResourcesBean styleResourcesBean;
@Inject
StyleResourcesEJBBean styleResourcesEJBBean;
@EJB
StyleResourcesEJBBean styleResourcesEJBBean;
注意
除了注入功能外,JSF 2.2 还提供了一套针对 Flash 作用域的四个全新的系统事件。这些事件是:
-
PostKeepFlashValueEvent
: 当值保留在 Flash 中时,此事件被触发 -
PostPutFlashValueEvent
: 当值存储在 Flash 中时,此事件被触发 -
PreClearFlashEvent
: 在清除 Flash 之前,此事件被触发 -
PreRemoveFlashValueEvent
: 当从 Flash 中移除值时,此事件被触发
记得在第二章中,你看到了一个基于 Flash 作用域的应用程序。在本章中,我们将编写一个系统事件监听器来监控这两个事件,即PostKeepFlashValueEvent
和PreClearFlashEvent
。以下是相应的代码:
public class FlashListener implements SystemEventListener {
private final static Logger LOGGER =
Logger.getLogger(FlashListener.class.getName());
@Override
public void processEvent(SystemEvent event)
throws AbortProcessingException {
if (event.getSource() instanceof String) {
LOGGER.log(Level.INFO, "The following parameter was added
in flash scope: {0}", event.getSource());
} else if (event.getSource() instanceof Map) {
LOGGER.info("Preparing to clear flash scope ...");
LOGGER.info("Current content:");
Iterator iterator = ((Map) event.getSource()).entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry mapEntry = (Map.Entry) iterator.next();
LOGGER.log(Level.INFO, "{0}={1}", new
Object[]{mapEntry.getKey(), mapEntry.getValue()});
}
}
}
@Override
public boolean isListenerForSource(Object source) {
return ((source instanceof String) || (source instanceof Map));
}
}
不要忘记按照以下方式在faces-config.xml
中配置监听器:
<system-event-listener>
<system-event-listener-class>
book.beans.FlashListener
</system-event-listener-class>
<system-event-class>
javax.faces.event.PostKeepFlashValueEvent
</system-event-class>
</system-event-listener>
<system-event-listener>
<system-event-listener-class>
book.beans.FlashListener
</system-event-listener-class>
<system-event-class>
javax.faces.event.PreClearFlashEvent
</system-event-class>
</system-event-listener>
本章代码包中提供了一个功能示例,命名为ch4_9_4
。
注意
一般而言,从 JSF 2.2 版本开始,PostRestoreStateEvent
系统事件通过Application.publishEvent
发布,而不使UIComponents
成为默认监听器,但仍然执行传统的树遍历。这个事件在之前的 JSF 版本中是规则的例外!
配置阶段监听器
如其名所示,阶段监听器能够监听 JSF 六个生命周期阶段中的每个阶段的开始和结束(JSF 阶段之间如何相互作用的详细图解可在附录中找到,JSF 生命周期):
-
恢复视图阶段
-
应用请求值阶段
-
处理验证阶段
-
更新模型值阶段
-
调用应用程序阶段
-
渲染响应阶段
你可以通过以下三个步骤轻松捕获每个阶段的事件:
-
实现
PhaseListener
接口。 -
覆盖
afterPhase
、beforePhase
和getPhaseId
方法。 -
在
faces-config.xml
中配置阶段监听器。
一个好的起点是一个简单但有用的PhaseListener
,它可以用于调试阶段。如果你对查看 JSF 请求生命周期中发生的事情感到好奇,那么你可以使用这个阶段监听器,它定义如下:
public class DebugPhaseListener implements PhaseListener {
public DebugPhaseListener() {
}
@Override
public void afterPhase(PhaseEvent event) {
System.out.println("After Phase: " + event.getPhaseId());
}
@Override
public void beforePhase(PhaseEvent event) {
System.out.println("Before Phase:" + event.getPhaseId());
}
@Override
public PhaseId getPhaseId() {
return PhaseId.ANY_PHASE;
}
}
最后,按照以下方式在faces-config.xml
中配置自定义阶段监听器:
<lifecycle>
<phase-listener>book.beans.DebugPhaseListener</phase-listener>
</lifecycle>
现在,你可以通过不同的页面和组件来模拟不同的场景,以查看输出。一个简单的场景是一个隐式导航案例,正如你在本章代码包中的应用程序ch4_8_3
中看到的那样。
JSF 2.2 中此配置的程序性反射如下:
public class Initializer extends
ApplicationConfigurationPopulator {
@Override
public void populateApplicationConfiguration
(Document toPopulate) {
String ns = toPopulate.getDocumentElement().getNamespaceURI();
Element lifecycleEl = toPopulate.createElementNS(ns, "lifecycle");
Element phaselistenerEl = toPopulate.
createElementNS(ns, "phase-listener");
phaselistenerEl.appendChild(toPopulate.
createTextNode("book.beans.DebugPhaseListener"));
lifecycleEl.appendChild(phaselistenerEl);
toPopulate.getDocumentElement().appendChild(lifecycleEl);
serializeFacesConfig(toPopulate, "D://faces-config.xml");
}
...
}
完整的应用程序命名为ch4_14_3
。
注意
getPhaseId
方法用于确定通过监听器的阶段。为了捕获所有阶段事件,该方法需要返回 PhaseId.ANY_PHASE
。
阶段监听器也可以用来改变组件。例如,你可以通过拦截以下 Render Response 阶段来根据提交的值给 UIInput
的背景着色:
public class PlayerPhaseListener implements PhaseListener {
@Override
public void afterPhase(PhaseEvent event) {
}
@Override
public void beforePhase(PhaseEvent event) {
processComponents(event.getFacesContext().getViewRoot());
}
@Override
public PhaseId getPhaseId() {
return PhaseId.RENDER_RESPONSE;
}
private void processComponents(UIComponent root) {
for (UIComponent child : root.getChildren()) {
if (child.getId().equals("playerId")) {
HtmlInputText inputText = (HtmlInputText) child;
String value = (String) inputText.getValue();
if (value != null) {
if (value.equalsIgnoreCase("rafa")) {
inputText.setStyleClass("rafa-style");
} else if (value.equalsIgnoreCase("roger")) {
inputText.setStyleClass("roger-style");
}
}
}
processComponents(child);
}
}
}
完整的示例包含在本章的代码包中,名称为 ch4_8_1
。
注意
从 JSF 2.2 开始,我们可以在阶段监听器中使用依赖注入(@Inject
和 @EJB
)。例如,我们可以通过 CDI 容器或 EJB 会话容器的注入来传递 CSS 类或我们选择的 CSS 类对应的文本,而不是将它们硬编码。你可以在本章的代码包中找到一个完整的示例,名称为 ch4_8_2
。
在你将 CSS 类的名称映射到一个 CDI 容器(例如,StyleResourcesBean
)或 EJB 容器(例如,StyleResourcesEJBBean
)之后,你可以在阶段监听器中使用以下任何一种注入方式:
@Inject
StyleResourcesBean styleResourcesBean;
@Inject
StyleResourcesEJBBean styleResourcesEJBBean;
@EJB
StyleResourcesEJBBean styleResourcesEJBBean;
阶段监听器可以改变许多种 JSF 艺术品,而不仅仅是 UI 组件。例如,以下阶段监听器收集所有 FacesMessages
并修改全局的。显然,你可以选择做其他任何事情,例如通过 ID 过滤它们或将它们保存在特殊位置。
public class MsgPhaseListener implements PhaseListener {
private static final Logger logger =
Logger.getLogger(MsgPhaseListener.class.getName());
@Override
public void afterPhase(PhaseEvent event) {}
@Override
public void beforePhase(PhaseEvent event) {
FacesContext facesContext = event.getFacesContext();
Iterator<String> ids = facesContext.getClientIdsWithMessages();
while (ids.hasNext()) {
String id = ids.next();
Iterator<FacesMessage> messages = facesContext.getMessages(id);
while (messages.hasNext()) {
FacesMessage message = messages.next();
logger.log(Level.INFO, "User ID:{0} Message: {1}"
, new Object[]{id, message.getSummary()});
if(id == null){
message.setSummary(message.getSummary() +
"alerted by a phase listener!");
}
}
}
}
@Override
public PhaseId getPhaseId() {
return PhaseId.RENDER_RESPONSE;
}
}
完整的应用程序名称为 ch4_15
。
使用 @ListenerFor 和 @ListenersFor
@ListenerFor
注解是从 JSF 2.0 开始提供的一个有趣的注解。这个注解允许一个组件通过组件本身作为监听器来订阅特定的事件。为此,我们需要遵循以下步骤:
-
实现
ComponentSystemEventListener
接口(接口名称表明事件将始终与UIComponent
实例相关联)。 -
覆盖
processEvent
方法(在这里我们可以 玩弄 组件)。 -
使用
@ListenerFor
来指示 UI 组件将要订阅的事件以及 UI 组件的源类。
例如,UIInput
组件可以订阅 PostAddToViewEvent
事件来向组件添加属性,以下是一个添加一些 CSS 到每个 UIInput
组件的例子:
@ListenerFor(systemEventClass = PostAddToViewEvent.class, sourceClass = javax.faces.component.UIInput.class)
public class PlayerRenderer extends TextRenderer
implements ComponentSystemEventListener {
@Override
public void processEvent(ComponentSystemEvent event)
throws AbortProcessingException {
UIInput inputSource = (UIInput) event.getComponent();
inputSource.getAttributes().put("styleClass", "rafa-style");
}
}
完整的应用程序包含在本章的代码包中,名称为 ch4_10_1
。
@ListenersFor
注解允许一个组件订阅多个事件。在之前的示例中,我们已经为每个 UIInput
组件添加了一些 CSS。接下来,我们希望通过添加一个单独的 CSS 到无效的 UIInput
组件来扩展这个功能。为此,UIInput
组件必须订阅 PostValidateEvent
。这种方法将帮助我们区分有效的 UIInput
实例和无效的 UIInput
实例。相应的代码如下:
@ListenersFor({
@ListenerFor(systemEventClass=PostAddToViewEvent.class,
sourceClass = javax.faces.component.UIInput.class),
@ListenerFor(systemEventClass=PostValidateEvent.class,
sourceClass = javax.faces.component.UIInput.class)
})
public class PlayerRenderer extends TextRenderer
implements ComponentSystemEventListener {
@Override
public void processEvent(ComponentSystemEvent event)
throws AbortProcessingException {
UIInput inputSource = (UIInput) event.getComponent();
inputSource.getAttributes().put("styleClass", "rafa-style");
if(!inputSource.isValid()){
inputSource.getAttributes().put("styleClass", "ui-invalid");
}
}
}
完整的应用程序包含在本章的代码包中,名称为 ch4_10_2
。
注意
从 JSF 2.2 版本开始,我们可以使用依赖注入功能与@ListenerFor
/@ListenersFor
(@Inject
和@EJB
)。例如,我们不再需要像之前示例那样硬编码 CSS 类,而是可以通过注入 CDI Bean 或 EJB 会话 Bean 来传递它们。你可以在本章代码包中找到一个完整的示例,名称为ch4_10_3
。
摘要
嗯,这是一章相当深入的内容,但在这里我们触及了 JSF 中许多重要的方面。你学习了如何创建、扩展和配置几个主要的 JSF 2.x 组件,以及它们是如何在 JSF 2.2 中改进的,特别是依赖注入机制。
仍然有许多内容在这里没有讨论;然而,在下一章中,我们将继续这一旅程,并涵盖其他内容,例如渲染器、处理器和工厂。
第五章. 使用 XML 文件和注解的 JSF 配置 – 第二部分
在本章中,我们将继续探讨更多情况下faces-config.xml
文件将帮助我们完成不同的配置任务(当然,对于其中的一些,我们有注解的替代方案,而对于其他一些,我们需要切换到 XML 配置级别)。除了上一章中提供的示例之外,本章将进一步深入,涵盖以下更详细的任务列表:
-
配置资源处理器
-
配置视图处理器
-
覆盖 JSF 渲染
-
与客户端行为功能一起工作
-
配置全局异常处理器
-
配置渲染工厂
-
配置部分视图上下文
-
配置访问上下文
-
配置外部上下文
-
配置 Flash
-
JSF 2.2 Window ID API
-
配置生命周期
-
配置应用程序
-
配置 VDL
-
结合多个工厂的权力
配置资源处理器
从 JSF 2.0 开始,所有网络资源,如 CSS、JavaScript 和图像,都从名为resources
的文件夹加载,该文件夹位于您的 Web 应用程序的根目录下,或在 JAR 文件中的/META-INF/resources
下。resources
文件夹下的一个文件夹被称为library
或theme
,它类似于客户端资源的集合。我们还可以在library
文件夹下创建一个特殊文件夹,匹配正则表达式\d+(_\d+)*
,以提供版本控制。在这种情况下,默认的 JSF 资源处理器将始终检索最新版本以显示。以下图显示了可以遵循的resources
文件夹结构化方法:
在前面的图中,部分A描述了没有版本控制的resources
文件夹的常见结构,而在部分B中,你有版本控制方法。文件夹css
、js
、img
等通常表示其中文件的类型;然而,这并非强制性的。
注意
注意,库的名称不应表示内容类型。
部分C表示resources
文件夹下支持的子文件夹的完整结构。在这种情况下,我们完全利用自动本地化和版本管理,这要求我们在resources
文件夹下遵守以下结构,并被称为资源标识符(方括号[]
表示可选部分):
[localePrefix/][libraryName/][libraryVersion/]resourceName[/resourceVersion]
注意
在 JAR 文件中打包的 face flows 的情况下,打包在CLASSPATH
中的资源必须位于 JAR 条目名称META-INF/flows/resourceIdentifier
下。
我们还将讨论部分A中提到的案例,因为这是最常用的案例。但为了完整性,您可以检查名为ch5_12
的完整应用程序,它代表来自部分C(包括部分B)的实现案例。
因此,有了前面图中的结构,我们可以轻松地使用以下代码加载 CSS 文件(rafa.css
):
<h:outputStylesheet library="default" name="css/rafa.css"/>
或者,你可以使用以下代码加载一个 JavaScript 文件(rafa.js
):
<h:outputScript library="default" name="js/rafa.js"/>
或者,你可以使用以下代码加载一个图像文件(rafa.png
):
<h:graphicImage library="default" name="img/rafa.png"/>
因此,这就是 JSF 默认资源处理器处理资源的方式。但如果我们不遵守这种文件夹的僵化结构,我们该怎么办?例如,如果我们有 CSS 文件位于应用程序的 web 根目录下的 /players/css/
,或者我们想在受保护的文件夹中放置资源,例如 WEB-INF
(resources
文件夹的最大缺点可能是默认情况下它里面的所有内容都可以从外部访问)。在这种情况下,没有直接可访问的 resources
文件夹,我们也不知道库是什么。如果我们编写如下代码,它将不会工作:
<h:outputStylesheet name="rafa.css" />
在可能的解决方案中,我们有编写自定义资源处理器的功能。这比听起来要简单得多,因为 JSF 提供了几个包装器(实现 FacesWrapper
),通过仅重写我们想要影响的方法,帮助我们编写自定义处理程序和工厂。对于自定义资源处理器,我们需要执行以下步骤:
-
扩展
ResourceHandlerWrapper
类。 -
编写一个委托构造函数。JSF 会调用这个构造函数来传递标准资源处理器,我们将它包装在一个
ResourceHandler
实例中。我们也可以通过重写getWrapped
方法来获取这个实例。 -
重写
createResource
方法。在这里,我们可以对资源进行排序,并决定哪些资源应该发送到默认资源处理器,哪些资源应该发送到我们的自定义资源处理器。
以下实现基于前面的三个步骤:
public class CustomResourceHandler extends
javax.faces.application.ResourceHandlerWrapper {
private ResourceHandler wrapped;
public CustomResourceHandler(ResourceHandler wrapped) {
this.wrapped = wrapped;
}
@Override
public ResourceHandler getWrapped() {
return this.wrapped;
}
@Override
public Resource createResource(String resourceName, String libraryName){
if ((!resourceName.equals("rafa.css")) &&
(!resourceName.equals("roger.css"))) {
//in JSF 2.0 and JSF 2.2
//return super.createResource(resourceName, libraryName);
//only in JSF 2.2
return super.createResourceFromId
(libraryName+"/"+resourceName);
} else {
return new PlayerResource(resourceName);
}
}
}
PlayerResource
类是我们自定义的资源。PlayerResource
的主要目的是指明正确的路径 /players/css/
,这个路径默认情况下是不被识别的。为此,我们扩展了另一个名为 ResourceWrapper
的包装器,并重写了 getRequestPath
方法,如下所示,其中除了一个调用 getRequestPath
之外,我们将所有调用委托给 ResourceWrapper
:
public class PlayerResource extends
javax.faces.application.ResourceWrapper {
private String resourceName;
public PlayerResource(String resourceName) {
this.resourceName = resourceName;
}
@Override
public Resource getWrapped() {
return this;
}
@Override
public String getRequestPath() {
return "players/css/" + this.resourceName;
}
}
接下来,你需要在 faces-config.xml
中按照以下方式配置自定义资源处理器:
<application>
<resource-handler>book.beans.CustomResourceHandler</resource-handler>
</application>
现在,如果你尝试加载 rafa.css
(或 roger.css
)文件,你可以添加以下代码行:
<h:outputStylesheet name="rafa.css"/>
<h:outputStylesheet name="roger.css"/>
完整的应用程序名为 ch5_1_1
,并可在本章的代码包中找到。
然而,请记住我之前说的“在可能的解决方案中...”?嗯,从 JSF 2.2 版本开始,我们可以在 web.xml
描述符中通过上下文参数来指定资源文件夹,如下所示(由 ResourceHandler.WEBAPP_RESOURCES_DIRECTORY_PARAM_NAME
字段映射):
<context-param>
<param-name>javax.faces.WEBAPP_RESOURCES_DIRECTORY</param-name>
<param-value>/players/css</param-value>
</context-param>
或者,我们可以将 resources
文件夹放在 WEB-INF
下,这样 JSF 就可以从 WEB-INF
内部访问它,但永远不能从外部访问:
<context-param>
<param-name>javax.faces.WEBAPP_RESOURCES_DIRECTORY</param-name>
<param-value>/WEB-INF/resources</param-value>
</context-param>
本章代码包中提供了一个名为 ch5_1_2
的完整示例。
自定义资源处理器可以用来向链接文件(例如 CSS、JS、图片等)传递额外参数。我们可以使用这种方法来重置浏览器缓存。浏览器缓存静态资源,如 CSS、JS 和图片;因此,每次网页加载时都不会从服务器请求。我们可以通过在查询字符串中添加参数来强制这样做,表示版本号或使浏览器理解它应该从服务器而不是从缓存中加载资源。
在这种情况下,我们假设 rafa.css
文件位于 /resources/default/css/
文件夹下,并且使用以下代码加载:
<h:outputStylesheet library="default" name="css/rafa.css"/>
此时,生成的 HTML 如下所示:
<link type="text/css" rel="stylesheet" href="/ch5_1_3/faces/javax.faces.resource/css/rafa.css?ln=default" />
此外,我们希望获得以下类似代码:
<link type="text/css" rel="stylesheet" href="/ch5_1_3/faces/javax.faces.resource/css/rafa.css?ln=default&v=v4.2.1">
因此,我们需要按照以下方式覆盖 createResource
方法:
@Override
public Resource createResource(String resourceName, String libraryName) {
Resource resource = super.createResource(resourceName, libraryName);
return new PlayerResource(resource);
}
此外,PlayerResource
负责在 getRequestPath
方法中添加版本参数:
@Override
public String getRequestPath() {
String requestPath = resource.getRequestPath();
logger.log (Level.INFO, "Initial request path is: {0}", requestPath);
String new_version = "v4.2.1";
if(requestPath.contains("?"))
requestPath = requestPath + "&v=" + new_version;
else
requestPath = requestPath + "?v=" + new_version;
logger.log (Level.INFO, "New request path is: {0}", requestPath);
return requestPath;
}
完整的应用程序包含在名为 ch5_1_3
的代码包中。
当然,在实际情况下,与前面的代码不同,版本号不是硬编码的。知道 JSF 2.2 允许我们在自定义资源处理器中使用依赖注入,我们可以使用以下代码从可以充当版本跟踪系统角色的 bean 中注入参数值:
public class CustomResourceHandler extends
javax.faces.application.ResourceHandlerWrapper {
@Inject
private VersionBean versionBean;
...
@Override
public Resource createResource(String resourceName, String libraryName) {
Resource resource = super.createResource(resourceName, libraryName);
return new PlayerResource(resource, versionBean.getVersion());
}
...
完整的示例命名为 ch5_1_4
。
注意
您还可以使用 JSF 的版本控制系统来使浏览器缓存失效,但您需要在库文件夹下创建正确的文件夹。 - JSF 将自动加载最新版本。像我们之前看到的那样传递参数对于许多其他事情都很有用,例如生成定制的 JS 和 CSS 响应。服务器可以访问此类参数和 JS。
浏览器缓存也可以通过 web.xml
描述符中的两个上下文参数(特定于 Mojarra)进行控制,如下所示:
-
com.sun.faces.defaultResourceMaxAge
:此参数可用于设置以毫秒为单位的过期时间。 -
com.sun.faces.resourceUpdateCheckPeriod
:此参数指定检查包含资源的 Web 应用程序工件更改的频率(以分钟为单位)。
JSF 资源处理提供了诸如缓存和加载 JAR 内的资源以及编写包含 CSS 或 JS 的自定义 UI 组件等稳固的优势,但它也有一些缺点。例如,网页设计师使用静态方法在 CSS 中添加图片,如下所示:
background-image: url(*link_to_image*)
然而,当使用<h:outputStyleSheet>
导入 CSS 样式表时,样式表是通过/javax.faces.resource/*
文件夹由 FacesServlet 导入和处理的,这使得图片的相对路径不可用(在这种情况下,CSS 文件变成了 JSF 资源)。一种解决方案是使用 EL 中的资源映射器强制将图像 URL 转换为 JSF 资源,即#{resource}
,作为#{resource['
library:
location']}
。例如,在通过<h:outputStylesheet>
加载的rafa.css
中,我们可以使用以下代码加载rafa.png
图像:
body {
background-image: url('#{resource["default:img/rafa.png"]}')
}
基于此,<h:graphicImage>
可以按如下方式加载rafa.png
:
<h:graphicImage value="#{resource['default:img/rafa.png']}"/>
你可以在名为ch5_13
的应用程序中检查这些示例。
作为替代方案,你可以使用 OmniFaces 库的UnmappedResourceHandler
,这可以避免我们修改 CSS 文件(showcase.omnifaces.org/resourcehandlers/UnmappedResourceHandler
)。此外,另一种方法是通过编写自定义的ResourceHandler
来解决这个问题。
注意
从 JSF 2.2 版本开始,ResourceResolver
已被合并到ResourceHandler
中,并且ResourceResolver
本身已被弃用。这两个组件在第十二章中详细描述,Facelets 模板。
以编程方式添加 CSS 和 JS 资源
有时,你可能需要通过在管理 Bean 方法中指定它们来加载 CSS 和 JS 资源。例如,以下方法以编程方式加载rafa.css
和rafa.js
:
public void addResourcesAction() {
FacesContext facesContext = FacesContext.getCurrentInstance();
UIOutput rafa_css = new UIOutput();
UIOutput rafa_js = new UIOutput();
rafa_css.setRendererType("javax.faces.resource.Stylesheet");
rafa_css.getAttributes().put("library", "default");
rafa_css.getAttributes().put("name", "css/rafa.css");
rafa_js.setRendererType("javax.faces.resource.Script");
rafa_js.getAttributes().put("library", "default");
rafa_js.getAttributes().put("name", "js/rafa.js");
facesContext.getViewRoot().addComponentResource
(facesContext, rafa_css, "head");
facesContext.getViewRoot().addComponentResource
(facesContext, rafa_js, "head");
}
完整的应用程序命名为ch5_14
。
配置视图处理器
JSF 提供了一个视图处理器,可以用于处理视图。当你想要与视图交互或创建/恢复/扩展/修改视图时,它是一个非常实用的工具。在这里处理 URL 也是一个好的实践,这正是你接下来将要看到的。
注意
当你需要与组件一起工作时,视图处理器不是一个好的选择!即使这是可能的,视图处理器也不是为这些任务而创建的。
有时你可能需要将绝对 URL 转换为相对 URL。例如,如果你在一个反向代理后面运行应用程序,你可能需要提供相对 URL。默认情况下,浏览器将每个绝对 URL 附加到主机上,这显然是一个大问题。
为了将绝对 URL 转换为相对 URL,我们需要执行以下步骤:
-
通过扩展
ViewHandlerWrapper
类来创建一个新的视图处理器。扩展这个包装器允许我们仅覆盖所需的方法。 -
覆盖
getActionURL
和getResourceURL
方法。 -
在
faces-config.xml
中配置视图处理器。
虽然这可能听起来有些夸张,但以下代码是自我解释的:
public class URLHandler extends ViewHandlerWrapper {
private ViewHandler baseViewHandler;
public URLHandler(ViewHandler baseViewHandler) {
this.baseViewHandler = baseViewHandler;
}
@Override
public String getActionURL(FacesContext context, String viewId) {
return convertToRelativeURL(context,
baseViewHandler.getActionURL(context, viewId));
}
@Override
public String getResourceURL(FacesContext context, String path) {
return convertToRelativeURL(context,
baseViewHandler.getResourceURL(context, path));
}
@Override
public ViewHandler getWrapped() {
return baseViewHandler;
}
private String convertToRelativeURL(FacesContext context,
String theURL){
final HttpServletRequest request = ((HttpServletRequest)
context.getExternalContext().getRequest());
final URI uri;
String prefix = "";
String string_uri = request.getRequestURI();
try {
uri = new URI(string_uri);
} catch (URISyntaxException ex) {
Logger.getLogger(URLHandler.class.getName()).
log(Level.SEVERE, null, ex);
return "";
}
String path = uri.getPath();
String new_path = path.replace("//", "/");
if (theURL.startsWith("/")) {
int count = new_path.length() - new_path.replace("/", "").length();
for (int i = 0; i < (count - 1); i++) {
prefix = prefix + "/..";
}
if (prefix.length() > 0) {
prefix = prefix.substring(1);
}
}
return (prefix + theURL);
}
}
faces-config.xml
中所需的配置如下:
...
<application>
<view-handler>book.beans.URLHandler</view-handler>
</application>
...
完整的应用程序可在名为ch5_2_1
的代码包中找到。
如果你检查 index.xhtml
页面的源代码,你会注意到,对于 CSS 资源,不是使用绝对 URL,而是使用以下类型的相对 URL:
<link type="text/css" rel="stylesheet" href="../ch5_2_1/faces/javax.faces.resource/css/rafa.css?ln=default">
完成!现在你可以运行位于反向代理后面的应用程序。
另一个有用的视图处理器是能够“吞噬”ViewExpiredException
异常的处理器。当用户会话过期时,会抛出此异常。通过视图处理器,我们可以通过重新创建用户视图来处理此异常。将流程重定向到特殊页面(让我们称其为 expired.xhtml
)。
当用户会话过期时,应用程序的 UIViewRoot
被设置为 null
。我们可以在 restoreView
方法中使用这个检查,如下所示:
public class ExceptionHandler extends ViewHandlerWrapper {
private static final Logger logger =
Logger.getLogger(ExceptionHandler.class.getName());
private ViewHandler baseViewHandler;
public ExceptionHandler(ViewHandler baseViewHandler) {
this.baseViewHandler = baseViewHandler;
}
@Override
public UIViewRoot restoreView(FacesContext context, String viewId) {
UIViewRoot root;
root = baseViewHandler.restoreView(context, viewId);
if (root == null) {
logger.info("The session has expired ...
I will not allow ViewExpiredException ...");
root = createView(context, viewId);
//root = createView(context, "/expired.xhtml");
//context.renderResponse();
}
return root;
}
@Override
public ViewHandler getWrapped() {
return baseViewHandler;
}
}
faces-config.xml
中的配置如下:
...
<application>
<view-handler>book.beans.ExceptionHandler</view-handler>
</application>
...
完整的应用程序包含在代码包中,名称为 ch5_2_2
。
注意
从 JSF 2.2 版本开始,我们可以使用视图处理器进行依赖注入(@Inject
和 @EJB
)。
覆盖 JSF 渲染器
Renderer
的主要职责包括生成适当的客户端标记,例如 HTML、WML 和 XUL,以及将来自客户端的信息转换为组件的正确类型。
JSF 提供了一套内置的渲染器,并且具有通过自定义行为扩展它们的能力。如果你考虑一个适当的解决方案来覆盖内置的渲染器,那么请执行以下步骤:
-
扩展所需的内置渲染器(例如,
Renderer
、TextRenderer
、LabelRenderer
、MessagesRenderer
等)。 -
覆盖内置的渲染器方法。
-
在
faces-config.xml
中配置新的渲染器或使用@FacesRenderer
注解。
好吧,让我们看看一些编写自定义渲染器的示例。例如,假设我们有三个属性(player-nickname
、player-mother-name
和 player-father-name
),我们想在 <h:inputText>
标签内使用它们。如果你尝试编写以下代码:
<h:inputText value="Rafael Nadal" player-nickname="Rafa" player-mother-name="Ana Maria Parera" player-father-name="Sebastián Nadal" player-coach-name=" Toni Nadal"/>
然后,内置的渲染器将给出以下输出:
<input id="..." name="..."
value="Rafael Nadal" type="text">
显然,我们的三个属性被忽略了。我们可以通过以下方式扩展 TextRenderer
来修复这个问题:
public class PlayerInputTextRenderer extends TextRenderer {
public PlayerInputTextRenderer(){}
@Override
protected void getEndTextToRender(FacesContext context,
UIComponent component, String currentValue)
throws java.io.IOException {
String[] attributes = {"player-nickname",
"player-mother-name", "player-father-name"};
ResponseWriter writer = context.getResponseWriter();
for (String attribute : attributes) {
String value = (String) component.getAttributes().get(attribute);
if (value != null) {
writer.writeAttribute(attribute, value, attribute);
}
}
super.getEndTextToRender(context, component, currentValue);
}
}
完成!按照以下方式在 faces-config.xml
中配置新的渲染器:
<application>
<render-kit>
<renderer>
<component-family>javax.faces.Input</component-family>
<renderer-type>javax.faces.Text</renderer-type>
<renderer-class>book.beans.PlayerInputTextRenderer</renderer-class>
</renderer>
</render-kit>
</application>
现在,渲染器输入字段将如下所示:
<input id="..." name="..." player-nickname="Rafa" player-mother-name="Ana Maria Parera" player-father-name="Sebastián Nadal" value="Rafael Nadal" type="text">
注意
而不是在 faces-config.xml
中配置自定义渲染器,我们可以使用 @FacesRenderer
注解,如下所示:
@FacesRenderer(componentFamily="javax.faces.Input",rendererType="javax.faces.Text")
但,不幸的是,这不起作用。这里似乎有一个错误!
完整的示例名称为 ch5_4_1
。
让我们再看另一个示例,以加强编写自定义渲染器的知识。下一个示例将修改内置的 LabelRenderer
类,在每一个 <h:outputText>
标签前添加一个图片,如下所示:
public class RafaLabelRenderer extends LabelRenderer{
public RafaLabelRenderer(){}
@Override
public void encodeEnd(FacesContext context,
UIComponent component)throws IOException{
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.write("<img src='resources/default/img/logo.png'/>");
}
}
不要忘记按照以下方式在 faces-config.xml
中配置渲染器:
<component-family>javax.faces.Output</component-family>
<renderer-type>javax.faces.Text</renderer-type>
<renderer-class>book.beans.RafaLabelRenderer</renderer-class>
注意
从 JSF 2.2 版本开始,我们可以在渲染器中使用依赖注入(@Inject
和 @EJB
)。前面提到的渲染器的完整示例名称为 ch5_4_2
(图片名称由另一个通过注入依赖提供的 Bean 提供)。
本节接下来的示例有点棘手。
如果你使用过 PrimeFaces,特别是 <p:messages>
标签,那么你知道这个标签接受一个名为 escape
的属性。属性的值可以是 true
或 false
,它定义了 HTML 是否会被转义(默认为 true
)。
不幸的是,JSF 2.2 仍然没有为 <h:messages>
标签提供这样的属性,但至少有一个解决方案可以解决这个问题。你可以实现一个能够理解 escape
属性的自定义渲染器。
JSF 提供了一个名为 ResponseWriter
的类,在这个情况下非常有用,因为它提供了能够生成 HTML 和 XML 等标记语言元素和属性的方法。此外,JSF 还提供了一个名为 ResponseWriterWrapper
的这个类的包装器。我们可以轻松地扩展这个类,并重写 writeText
方法,这对于从对象通过转换获得的转义字符串非常有用。未转义的字符串通过 write
方法写入。
因此,根据这些信息,我们可以轻松地编写我们的响应写入器,如下所示:
public class EscapeResponseWriter extends ResponseWriterWrapper {
private ResponseWriter responseWriter;
public EscapeResponseWriter(ResponseWriter responseWriter) {
this.responseWriter = responseWriter;
}
@Override
public ResponseWriter getWrapped() {
return responseWriter;
}
@Override
public void writeText(Object text, UIComponent component,
String property) throws IOException {
String escape = (String) component.getAttributes().get("escape");
if (escape != null) {
if ("false".equals(escape)) {
super.write(String.valueOf(text));
} else {
super.writeText(String.valueOf(text), component, property);
}
}
}
}
到目前为止,一切顺利!现在我们需要编写自定义渲染器,如以下代码所示,通过扩展 MessagesRenderer
类,这是 JSF 消息的默认渲染器。我们唯一需要影响的方法是 encodeEnd
方法,通过放置我们的响应写入器代替默认的写入器。最后,我们将它恢复到默认设置。
public class EscapeMessagesRenderer extends MessagesRenderer {
public EscapeMessagesRenderer(){}
@Override
public void encodeEnd(FacesContext context,
UIComponent component) throws IOException {
ResponseWriter responseWriter = context.getResponseWriter();
context.setResponseWriter(new EscapeResponseWriter(responseWriter));
super.encodeEnd(context, component);
context.setResponseWriter(responseWriter);
}
}
最后,按照以下方式在 faces-config.xml
中配置新的渲染器:
<renderer>
<component-family>javax.faces.Messages</component-family>
<renderer-type>javax.faces.Messages</renderer-type>
<renderer-class>book.beans.EscapeMessagesRenderer</renderer-class>
</renderer>
现在,你可以通过以下方式设置 escape
属性,在你的消息中添加 HTML 内容:
<h:messages escape="false" />
完整的示例命名为 ch5_4_3
。
在前面的示例中,我们看到了扩展现有渲染器的一些用例。本节的最后一个示例将更进一步,将展示通过扩展抽象类 Renderer
来编写自定义 RenderKit
和自定义渲染器的用例。
当 Renderer
类将 UI 组件的内部表示转换为输出流时,RenderKit
代表了一组能够为特定客户端(例如,特定设备)渲染 JSF UI 组件实例的 Renderer
实例。每次 JSF 需要渲染 UI 组件时,它将调用 RenderKit.getRenderer
方法,该方法能够根据两个唯一标识它的参数返回相应渲染器的实例:组件家族和渲染器类型。
假设我们想要改变用于所有属于javax.faces.Input
家族的 UI 组件的默认渲染器的行为,通过添加一些 CSS 中的自定义样式。这可以通过编写自定义RenderKit
并重写getRenderer
方法轻松实现。从 JSF 2.2 开始,我们可以非常快速地做到这一点,因为我们可以扩展代表抽象类RenderKit
的简单实现的新的包装类。这个类命名为RenderKitWrapper
,它允许我们仅重写所需的方法。
例如,我们如下重写getRenderer
方法:
public class CustomRenderKit extends RenderKitWrapper {
private RenderKit renderKit;
public CustomRenderKit() {}
public CustomRenderKit(RenderKit renderKit) {
this.renderKit = renderKit;
}
@Override
public Renderer getRenderer(String family, String rendererType) {
if (family.equals("javax.faces.Input")) {
Renderer inputRenderer = getWrapped().
getRenderer(family, rendererType);
return new RafaRenderer(inputRenderer);
}
return getWrapped().getRenderer(family, rendererType);
}
@Override
public RenderKit getWrapped() {
return renderKit;
}
}
因此,当 JSF 需要渲染属于javax.faces.Input
家族的 UI 组件时,我们使用用于此任务的原始渲染器,并将其包装到名为RafaRenderer
的自定义渲染器中。这个自定义渲染器将扩展 JSF 2.2 的RendererWrapper
(Renderer
的一个简单实现)并重写encodeBegin
方法,如下所示:
@ResourceDependencies({
@ResourceDependency(name = "css/rafastyles.css",
library = "default", target = "head")
})
@FacesRenderer(componentFamily = "javax.faces.Rafa",
rendererType = RafaRenderer.RENDERER_TYPE)
public class RafaRenderer extends RendererWrapper {
private Renderer renderer;
public static final String RENDERER_TYPE =
"book.beans.RafaRenderer";
public RafaRenderer() {}
public RafaRenderer(Renderer renderer) {
this.renderer = renderer;
}
@Override
public void encodeBegin(FacesContext context,
UIComponent uicomponent) throws IOException {
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.writeAttribute("class", "rafastyle", "class");
getWrapped().encodeBegin(context, uicomponent);
}
@Override
public Renderer getWrapped() {
return renderer;
}
}
注意
好知道我们可以使用@ResourceDependency
和@ResourceDependecies
注解为 JSF 渲染器指定外部资源(例如 CSS 和 JS)。
最后,您需要按照如下方式在faces-config.xml
中配置自定义的RenderKit
:
<render-kit>
<render-kit-class>
book.beans.CustomRenderKit
</render-kit-class>
</render-kit>
完整的应用程序命名为ch5_15
。
与客户端行为功能一起工作
JSF 2 提供了以可重用方式为组件定义特定客户端行为的能力。客户端行为实际上是一段可以在浏览器中执行的 JavaScript 代码。
例如,当用户可以访问执行不可逆更改的按钮时;例如,删除、复制和移动,在执行操作之前通知用户后果并请求确认是一个好的做法。
为了实现客户端行为功能,我们执行以下步骤:
-
扩展
ClientBehaviorBase
类。 -
重写
getScript
方法。 -
使用
@FacesBehavior (value="
developer_id")
注解标注创建的类,其中developer_id用于引用我们的自定义客户端行为。当我们为行为定义一个标签时,这是必需的。 -
定义一个用于行为的自定义标签——在 JSF 页面中需要一个标签来指定哪些组件接收我们的客户端行为(JS 代码)。
-
在
web.xml
文件的描述符中注册自定义标签。
以下代码展示了如何编写一个客户端行为,用于在用户点击模拟删除操作的按钮时显示 JavaScript 确认对话框,这涵盖了前面提到的前三个步骤:
@FacesBehavior(value = "confirm")
public class ConfirmDeleteBehavior extends ClientBehaviorBase {
@Override
public String getScript(ClientBehaviorContext behaviorContext) {
return "return confirm('Are you sure ?');";
}
}
第四步是编写一个用于行为的自定义标签。在WEB-INF
文件夹下创建一个名为delete.taglib.xml
的文件,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib version="2.2"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-
facelettaglibrary_2_2.xsd">
<namespace>http://www.custom.tags/jsf/delete</namespace>
<tag>
<tag-name>confirmDelete</tag-name>
<behavior>
<behavior-id>confirm</behavior-id>
</behavior>
</tag>
</facelet-taglib>
注意
<behavior-id>
标签的值必须与FacesBehavior
注解的value
成员匹配(developer_id)。标签名可以自由选择。
最后一步是在web.xml
中注册标签:
<context-param>
<param-name> javax.faces.FACELETS_LIBRARIES</param-name>
<param-value>/WEB-INF/delete.taglib.xml</param-value>
</context-param>
注意
我们可以将客户端行为附加到实现ClientBehaviourHolder
接口的每个组件上。幸运的是,几乎所有组件都实现了这个接口,例如按钮、链接、输入字段等等。
完成!现在,我们可以在 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
>
<h:head>
<title></title>
</h:head>
<h:body>
<h:form>
<h:commandButton value="Delete" action="done">
<b:confirmDelete/>
</h:commandButton>
</h:form>
</h:body>
</html>
如果用户没有确认删除,则操作将被中止。
注意
从 JSF 2.2 版本开始,我们可以使用依赖注入与客户端行为(@Inject
和@EJB
)。例如,我们不是硬编码确认问题“你确定吗?”,而是可以通过 CDI 豆或 EJB 会话豆的注入来传递它。一个完整的示例可以在本章的代码包中找到。它被命名为ch5_5_1
。
注意,即使我们没有指定启动客户端行为 JS 代码的事件,示例也能正常工作。这是因为 JS 代码被附加到按钮的onclick
事件上,这是<h:commandButton>
的默认事件。现在,我们将编写另一个示例,将客户端行为同时附加到两个其他事件上。
注意
我们可以通过指定标签的event
属性来将客户端行为代码附加到其他事件上。
在下一个示例中,我们假设以下场景:一个输入字段在获得焦点时变为绿色(onfocus
JS 事件),在失去焦点时恢复为空白(onblur
JS 事件)。现在,我们必须订阅两个事件。
在前面的示例中,我们明确地将客户端行为功能链接到<confirmDelete>
标签。即使在这种情况下仍然可行,我们选择采用另一种方法。而不是直接链接,我们将使用标签处理器(TagHandler
)。
注意
自定义标签处理器允许我们操作创建的 DOM 树(从树中添加/删除节点)。
当我们编写自定义标签处理器时,我们需要关注apply
方法,特别是这个方法的第二个参数,它被命名为parent
,代表标签的父级,在我们的情况下将是<h:inputText>
。我们可以将这两个事件都添加到<h:inputText>
中,如下所示:
public class FocusBlurHandler extends TagHandler {
private FocusBlurBehavior onfocus = new FocusBlurBehavior();
private FocusBlurBehavior onblur = new FocusBlurBehavior();
public FocusBlurHandler(TagConfig tagConfig) {
super(tagConfig);
}
@Override
public void apply(FaceletContext ctx, UIComponent parent)
throws IOException {
if (parent instanceof ClientBehaviorHolder) {
ClientBehaviorHolder clientBehaviorHolder =
(ClientBehaviorHolder) parent;
clientBehaviorHolder.addClientBehavior("focus", onfocus);
clientBehaviorHolder.addClientBehavior("blur", onblur);
}
}
}
记住,在前面的章节中,我们看到了如何覆盖一些 JSF 渲染器。嗯,这里还有一个!与上一个示例中覆盖ClientBehaviorBase
的getScript
方法不同,我们将编写一个自定义渲染器,这很容易实现,因为 JSF 提供了一个专门用于客户端行为的渲染器,名为ClientBehaviorRenderer
。此渲染器包含自己的getScript
方法,如下面的代码所示:
@FacesBehaviorRenderer(rendererType = "focusblurrenderer")
@ResourceDependency(name="player.css", target="head")
public class FocusBlurRenderer extends ClientBehaviorRenderer {
private static final String FOCUS_EVENT = "focus";
private static final String BLUR_EVENT = "blur";
@Override
public String getScript(ClientBehaviorContext behaviorContext,
ClientBehavior behavior) {
if (FOCUS_EVENT.equals(behaviorContext.getEventName())) {
return "this.setAttribute('class','focus-css');";
}
if (BLUR_EVENT.equals(behaviorContext.getEventName())) {
return "this.setAttribute('class','blur-css');";
}
return null;
}
}
注意
@ResourceDependency
注解可用于在自定义UIComponent
和Renderer
组件中加载资源,如 CSS 和 JS。在 JSF 的几个版本中,@ResourceDependency
对于Renderer
没有按预期工作(似乎是一个 bug)。如果你遇到这样的问题,你必须为测试硬编码 CSS。
最后,客户端行为将如下指出上述渲染器:
@FacesBehavior(value = "focusblur")
public class FocusBlurBehavior extends ClientBehaviorBase {
@Override
public String getRendererType() {
return "focusblurrenderer";
}
}
包含 CSS 源、标签定义和特定配置的完整示例可在代码包中找到,命名为 ch5_5_2
。
JSF 工厂
以下注释是本章最后部分的良好起点,该部分专门介绍 JSF 工厂。在 JSF 中,工厂是由 FactoryFinder
初始化的,它可以识别自定义工厂是否有代理构造函数——一个用于工厂类型的单参数构造函数。
注意
当我们想要包装 JSF 的标准工厂时,这很有用,因为 FactoryFinder
会传入之前已知的工厂,通常是内置的一个。工厂实例的获取方式如下:
*XXXFactory* factory = (*XXXFactory*) FactoryFinder.getFactory(FactoryFinder.*XXX_FACTORY*);
例如,可以使用以下代码找到 RenderKitFactory
:
RenderKitFactory factory = (RenderKitFactory)FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
在 FaceletFactory
旁边,JSF 2.2 中通过 FactoryFinder
可以获取的另一个新工厂是 FlashFactory
。我们将在本书的最后一章中讨论 FaceletFactory
,第十二章,Facelets 模板。
配置全局异常处理器
在 JSF 生命周期中,我们需要在不同的应用点处理不同类型的异常。从 JSF 2 开始,我们有一个通用的 API,允许我们编写一个全局异常处理器。这可以非常方便,尤其是在我们需要避免应用程序未捕获的“静默”异常时。
为了编写一个全局异常处理器,我们需要做以下事情:
-
扩展
ExceptionHandlerFactory
,这是一个能够创建和返回新的ExceptionHandler
实例的工厂对象——处理 JSF 生命周期中抛出的意外Exception
的中心点。 -
扩展
ExceptionHandlerWrapper
,这是ExceptionHandler
的简单实现。 -
在
faces-config.xml
中配置自定义异常处理器。
因此,我们可以编写一个自定义异常处理器工厂,如下所示:
public class CustomExceptionHandlerFactory
extends ExceptionHandlerFactory {
private ExceptionHandlerFactory exceptionHandlerFactory;
public CustomExceptionHandlerFactory(){}
public CustomExceptionHandlerFactory(ExceptionHandlerFactory
exceptionHandlerFactory) {
this.exceptionHandlerFactory = exceptionHandlerFactory;
}
@Override
public ExceptionHandler getExceptionHandler() {
ExceptionHandler handler = new CustomExceptionHandler
(exceptionHandlerFactory.getExceptionHandler());
return handler;
}
}
我们处理异常的实现是发送每个错误到日志并导航到错误页面,如下所示(注意,这里也可以捕获 ViewExpiredException
):
public class CustomExceptionHandler extends ExceptionHandlerWrapper {
private static final Logger logger =
Logger.getLogger(CustomExceptionHandler.class.getName());
private ExceptionHandler exceptionHandler;
CustomExceptionHandler(ExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
@Override
public ExceptionHandler getWrapped() {
return exceptionHandler;
}
@Override
public void handle() throws FacesException {
final Iterator<ExceptionQueuedEvent> queue =
getUnhandledExceptionQueuedEvents().iterator();
while (queue.hasNext()) {
//take exceptions one by one
ExceptionQueuedEvent item = queue.next();
ExceptionQueuedEventContext exceptionQueuedEventContext =
(ExceptionQueuedEventContext) item.getSource();
try {
//log error
Throwable throwable = exceptionQueuedEventContext.getException();
logger.log(Level.SEVERE, "EXCEPTION: ", throwable.getMessage());
//redirect error page
FacesContext facesContext = FacesContext.getCurrentInstance();
Map<String, Object> requestMap =
facesContext.getExternalContext().getRequestMap();
NavigationHandler nav =
facesContext.getApplication().getNavigationHandler();
requestMap.put("errmsg", throwable.getMessage());
nav.handleNavigation(facesContext, null, "/error");
facesContext.renderResponse();
} finally {
//remove it from queue
queue.remove();
}
}
getWrapped().handle();
}
}
最后,我们需要在 faces-config.xml
中按照以下方式配置异常处理器:
<factory>
<exception-handler-factory>
book.beans.CustomExceptionHandlerFactory
</exception-handler-factory>
</factory>
完整示例命名为 ch5_3
。
注意
从 JSF 2.2 开始,我们可以使用依赖注入与异常处理器(@Inject
和 @EJB
)。
注意在处理 AJAX 异常时存在一个特殊情况。默认情况下,大多数异常对客户端是不可见的。AJAX 错误会返回给客户端,但遗憾的是 JSF AJAX 客户端并没有准备好处理任意错误消息,因此它们简单地忽略它们。但是 OmniFaces 特别创建了一个自定义异常处理器来处理这个任务(它适用于 AJAX 和非 AJAX 异常)。处理器命名为 FullAjaxExceptionHandler
,工厂命名为 FullAjaxExceptionHandlerFactory
。
一旦安装 OmniFaces,你可以在 faces-config.xml
中通过简单的配置利用 AJAX 异常处理器:
<factory>
<exception-handler-factory>
org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory
</exception-handler-factory>
</factory>
OmniFaces 异常处理器的行为在web.xml
中配置:
<error-page>
<exception-type>
java.lang.NullPointerException
</exception-type>
<location>/null.jsf</location>
</error-page>
<error-page>
<exception-type>
java.lang.Throwable
</exception-type>
<location>/ throwable.jsf</location>
</error-page>
注意
OmniFaces 异常处理器的错误页面应该是 JSF 2.0(或更高版本)页面。一个全面的演示可以在 OmniFaces 展示区showcase.omnifaces.org/exceptionhandlers/FullAjaxExceptionHandler
找到。
配置 RenderKit 工厂
在本章的早期部分,我们已经编写了一个自定义的RenderKit
,由于我们在faces-config.xml
中使用<render-kit>
标签进行了配置,因此 JSF 加载了它。但是,在幕后,JSF 使用RenderKitFactory
,它能够注册和返回RenderKit
实例。因此,我们可以编写自定义的RenderKitFactory
以返回我们的自定义RenderKit
。为了编写这样的工厂,你需要执行以下操作:
-
扩展负责注册和返回
RenderKit
实例的RenderKitFactory
类。 -
覆盖
addRenderKit
方法,使用指定的 ID 注册指定的RenderKit
实例。 -
覆盖
getRenderKit
方法,返回具有指定 ID 的RenderKit
。 -
覆盖
getRenderKitIds
方法,并返回由该工厂注册的渲染套件标识符集合的Iterator
。
根据这些步骤,我们可以如下注册我们的自定义RenderKit
:
public class CustomRenderKitFactory extends RenderKitFactory {
private RenderKitFactory renderKitFactory;
public CustomRenderKitFactory() {}
public CustomRenderKitFactory(RenderKitFactory renderKitFactory){
this.renderKitFactory = renderKitFactory;
}
@Override
public void addRenderKit(String renderKitId,
RenderKit renderKit){
renderKitFactory.addRenderKit(renderKitId, renderKit);
}
@Override
public RenderKit getRenderKit(FacesContext context,
String renderKitId) {
RenderKit renderKit = renderKitFactory.
getRenderKit(context, renderKitId);
return (HTML_BASIC_RENDER_KIT.equals(renderKitId)) ?
new CustomRenderKit(renderKit) : renderKit;
}
@Override
public Iterator<String> getRenderKitIds() {
return renderKitFactory.getRenderKitIds();
}
}
现在,我们不再使用<render-kit>
标签来配置自定义的RenderKit
,而是可以配置自定义的RenderKitFactory
,如下所示:
<factory>
<render-kit-factory>
book.beans.CustomRenderKitFactory
</render-kit-factory>
</factory>
完整的应用程序命名为ch5_16
。
配置 PartialViewContext
PartialViewContext
类负责处理部分请求并在视图中渲染部分响应。换句话说,JSF 使用PartialViewContext
来处理 AJAX 请求和响应的执行、渲染等。我们如下引用它:
FacesContext.getCurrentInstance().getPartialViewContext();
编写自定义PartialViewContext
实现意味着以下步骤:
-
扩展
PartialViewContextFactory
将产生一个能够创建和返回新的PartialViewContext
实例的工厂对象,这是处理部分请求-响应的中心点。 -
扩展
PartialViewContextWrapper
,它是PartialViewContext
的一个简单实现。 -
在
faces-config.xml
中配置自定义的PartialViewContext
实现。
现在,假设我们有多份表单通过 AJAX 提交。每个<f:ajax>
标签将包含execute
属性以及我们特别感兴趣的render
属性。此属性应包含要重新渲染的组件的客户 ID。当多个部分请求重新渲染相同的组件时,该组件的 ID 存在于每个部分请求中(每个render
属性中)。
一个常见的例子是全局的<h:messages>
标签。这个标签的 ID 应该添加到每个需要重新渲染的部分请求中。我们不必在render
属性中重新输入客户端 ID,我们可以编写一个自定义的PartialViewContext
实现来完成这个任务。首先,我们创建工厂实例,如下所示:
public class CustomPartialViewContextFactory
extends PartialViewContextFactory {
private PartialViewContextFactory partialViewContextFactory;
public CustomPartialViewContextFactory(){}
public CustomPartialViewContextFactory
(PartialViewContextFactory partialViewContextFactory) {
this.partialViewContextFactory = partialViewContextFactory;
}
@Override
public PartialViewContext getPartialViewContext(FacesContext context) {
PartialViewContext handler = new CustomPartialViewContext
(partialViewContextFactory.getPartialViewContext(context));
return handler;
}
}
接下来,我们编写我们的自定义PartialViewContext
并重写getRenderIds
方法。基本上,我们定位<h:messages>
标签的 ID,检查这个 ID 是否已经在渲染 ID 列表中,如果没有,就将其添加到列表中,如下所示:
public class CustomPartialViewContext extends PartialViewContextWrapper {
private PartialViewContext partialViewContext;
public CustomPartialViewContext(PartialViewContext partialViewContext) {
this.partialViewContext = partialViewContext;
}
@Override
public PartialViewContext getWrapped() {
return partialViewContext;
}
@Override
public Collection<String> getRenderIds() {
FacesContext facesContext = FacesContext.getCurrentInstance();
if (PhaseId.RENDER_RESPONSE == facesContext.getCurrentPhaseId()) {
UIComponent component = findComponent("msgsId",
facesContext.getViewRoot());
if (component != null && component.isRendered()) {
String componentClientId = component.getClientId(facesContext);
Collection<String> renderIds = getWrapped().getRenderIds();
if (!renderIds.contains(componentClientId)) {
renderIds.add(componentClientId);
}
}
}
return getWrapped().getRenderIds();
}
private UIComponent findComponent(String id, UIComponent root) {
if (root == null) {
return null;
} else if (root.getId().equals(id)) {
return root;
} else {
List<UIComponent> childrenList = root.getChildren();
if (childrenList == null || childrenList.isEmpty()) {
return null;
}
for (UIComponent child : childrenList) {
UIComponent result = findComponent(id, child);
if (result != null) {
return result;
}
}
}
return null;
}
}
最后,我们需要在faces-config.xml
中配置PartialViewContext
,如下所示:
<factory>
<partial-view-context-factory>
book.beans.CustomPartialViewContextFactory
</partial-view-context-factory>
</factory>
完整的示例命名为ch5_6_1
。
注意
从 JSF 2.2 开始,我们可以使用部分视图上下文的依赖注入(@Inject
和@EJB
)。一个完整的示例可以在本章的代码包中找到,命名为ch5_6_2
。
配置 visitContext
根据文档,VisitContext
是一个用于持有与执行组件树遍历相关的状态的对象。
为什么我们需要这样一个对象呢?好吧,想象一下你想以编程方式找到一个特定的组件。你可能首先会想到findComponent
或invokeOnComponent
内置方法。当你需要找到多个组件时,你可以递归地应用这个过程(就像你在前面的一些示例中看到的那样)。递归过程通过以分层的方式访问每个节点,对组件的树(或子树)进行干净的遍历。
然而,JSF 2 也提供了一个现成的名为UIComponent.visitTree
的方法来完成组件的树遍历,声明如下:
public boolean visitTree(VisitContext context,
VisitCallback callback)
第一个参数是VisitContext
的一个实例,第二个参数是VisitCallback
接口的一个实例,该接口提供了一个名为visit
的方法,该方法在访问每个节点时被调用。如果树成功遍历,则visitTree
返回true
。
基于这个知识,我们可以编写一个自定义的VisitContext
实现来重置表单的可编辑组件。这样一个组件实现了EditableValueHolder
接口,并提供了resetValue
方法。
编写自定义VisitContext
实现的步骤如下:
-
扩展
VisitContextFactory
,这是一个能够创建和返回新的VisitContext
实例的工厂对象。 -
扩展
VisitContextWrapper
,这是VisitContext
的一个简单实现。 -
在
faces-config.xml
中配置自定义的VisitContext
实现。
因此,首先我们需要扩展内置的工厂,如下所示:
public class CustomVisitContextFactory extends VisitContextFactory {
private VisitContextFactory visitContextFactory;
public CustomVisitContextFactory() {}
public CustomVisitContextFactory(VisitContextFactory
visitContextFactory){
this.visitContextFactory = visitContextFactory;
}
@Override
public VisitContext getVisitContext(FacesContext context,
Collection<String> ids, Set<VisitHint> hints) {
VisitContext handler = new CustomVisitContext(visitContextFactory.
getVisitContext(context, ids, hints));
return handler;
}
}
注意
注意,我们还可以指定要访问的客户端 ID 集合。我们还可以指定一些访问提示。当所有组件都应该使用默认的访问提示进行访问时,这些参数可以是null
。
自定义的访问上下文可以通过以下方式程序化表示——visitTree
方法通过调用invokeVisitCallback
来访问单个组件:
public class CustomVisitContext extends VisitContextWrapper {
private static final Logger logger =
Logger.getLogger(CustomVisitContext.class.getName());
private VisitContext visitContext;
public CustomVisitContext(VisitContext visitContext) {
this.visitContext = visitContext;
}
@Override
public VisitContext getWrapped() {
return visitContext;
}
@Override
public VisitResult invokeVisitCallback(UIComponent component,
VisitCallback callback) {
logger.info("Custom visit context is used!");
return getWrapped().invokeVisitCallback(component, callback);
}
}
因此,我们的自定义VisitContext
实现并没有做太多;它只是触发一些日志消息并将控制权委托给原始的VisitContext
类。我们的目标是编写一个自定义的VisitCallback
实现,用于重置表单的可编辑值,如下所示:
public class CustomVisitCallback implements VisitCallback{
@Override
public VisitResult visit(VisitContext context, UIComponent target) {
if (!target.isRendered()) {
return VisitResult.REJECT;
}
if (target instanceof EditableValueHolder) {
((EditableValueHolder)target).resetValue();
}
return VisitResult.ACCEPT;
}
}
好吧,我们几乎完成了!只需使用以下代码在faces-config.xml
中配置自定义的VisitContext
实现:
<factory>
<visit-context-factory>
book.beans.CustomVisitContextFactory
</visit-context-factory>
</factory>
让我们使用以下代码开始访问节点的过程:
FacesContext context = FacesContext.getCurrentInstance();
UIComponent component = context.getViewRoot();
CustomVisitCallback customVisitCallback = new CustomVisitCallback();
component.visitTree(VisitContext.createVisitContext
(FacesContext.getCurrentInstance()), customVisitCallback);
注意,遍历过程的起点是视图根。这并不是强制的;你可以传递任何其他的子树。
这里出现了一个明显的问题!既然这个自定义的VisitContext
并没有做重要的事情(只是触发一些日志消息),为什么我们不跳过它?
是的,确实我们可以跳过这个自定义的VisitContext
,因为我们只需要自定义的VisitCallback
实现,但这是一个很好的机会来了解它是如何实现的。也许你可以修改invokeVisitCallback
来在将动作传递到VisitCallback.visit
方法之前实现某种客户端 ID 过滤。
一个完整的示例可以在本章的代码包中找到,命名为ch5_7
。
注意
从 JSF 2.2 开始,我们可以使用依赖注入与访问上下文(@Inject
和@EJB
)。
配置外部上下文
FacesContext
和ExternalContext
对象是 JSF 中最重要的两个对象。它们各自提供了强大的功能,并且各自覆盖了 JSF(在FacesContext
的情况下)和 Servlet/Portlet(在ExternalContext
的情况下)提供的重要的工件领域。
此外,它们都可以被开发者扩展或修改。例如,在本节中,我们将编写一个用于下载文件的自定义ExternalContext
实现。有时,你可能需要通过将文件内容程序化地发送给用户来下载文件。默认的ExternalContext
可以做到这一点,如下所示——当然,你可以轻松地适应这段代码来处理其他文件:
public void readFileAction() throws IOException, URISyntaxException {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
Path path = Paths.get(((ServletContext)externalContext.getContext())
.getRealPath("/resources/rafa.txt"));
BasicFileAttributes attrs = Files.readAttributes(path,
BasicFileAttributes.class);
externalContext.responseReset();
externalContext.setResponseContentType("text/plain");
externalContext.setResponseContentLength((int) attrs.size());
externalContext.setResponseHeader("Content-Disposition",
"attachment; filename=\"" + "rafa.txt" + "\"");
int nRead;
byte[] data = new byte[128];
InputStream inStream = externalContext.
getResourceAsStream("/resources/rafa.txt");
try (OutputStream output = externalContext.getResponseOutputStream()) {
while ((nRead = inStream.read(data, 0, data.length)) != -1) {
output.write(data, 0, nRead);
}
output.flush();
}
facesContext.responseComplete();
}
通常,这种方法使用默认的响应输出流。但假设我们已经编写了我们的“虚拟”响应输出流,显然,它执行一个虚拟操作:对于每个字节数组块,将字符'a'
替换为字符'A'
,如下所示:
public class CustomResponseStream extends OutputStream {
private OutputStream responseStream;
public CustomResponseStream(OutputStream responseStream) {
this.responseStream = responseStream;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
String s = new String(b, off, len);
s = s.replace('a', 'A');
byte[] bb = s.getBytes();
responseStream.write(bb, off, len);
}
@Override
public void write(int b) throws IOException {
}
}
现在,我们想要使用这个响应输出流而不是默认的输出流,但是没有externalContext.setResponseOutputStream(OutputStream os)
方法。相反,我们可以通过以下步骤编写一个自定义的ExternalContext
:
-
扩展
ExternalContextFactory
,这是一个能够创建和返回一个新的ExternalContext
的工厂对象。 -
扩展
ExternalContextWrapper
,它是对ExternalContext
的一个简单实现。 -
在
faces-config.xml
中配置自定义的ExternalContext
实现。
自定义外部上下文工厂的代码如下:
public class CustomExternalContextFactory extends ExternalContextFactory{
private ExternalContextFactory externalContextFactory;
public CustomExternalContextFactory(){}
public CustomExternalContextFactory(ExternalContextFactory
externalContextFactory){
this.externalContextFactory = externalContextFactory;
}
@Override
public ExternalContext getExternalContext(Object context,
Object request, Object response) throws FacesException {
ExternalContext handler = new
CustomExternalContext(externalContextFactory
.getExternalContext(context, request, response));
return handler;
}
}
自定义外部上下文如下。在这里,我们覆盖了getResponseOutputStream
方法以返回我们的自定义响应输出流。
public class CustomExternalContext extends ExternalContextWrapper {
private ExternalContext externalContext;
public CustomExternalContext(ExternalContext externalContext) {
this.externalContext = externalContext;
}
@Override
public ExternalContext getWrapped() {
return externalContext;
}
@Override
public OutputStream getResponseOutputStream() throws IOException {
HttpServletResponse response =
(HttpServletResponse)externalContext.getResponse();
OutputStream responseStream = response.getOutputStream();
return new CustomResponseStream(responseStream);
}
}
最后,不要忘记在faces-config.xml
中配置自定义的外部上下文:
<factory>
<external-context-factory>
book.beans.CustomExternalContextFactory
</external-context-factory>
</factory>
完整的示例可以从本章代码包中名为ch5_8
的部分下载。
注意
从 JSF 2.2 版本开始,我们可以使用依赖注入与外部上下文和 Faces 上下文(@Inject
和@EJB
)。
JSF 还提供了工厂(FacesContextFactory
)和包装器(FacesContextWrapper
)类来扩展默认的FacesContext
类。这可以在需要将 JSF 适配到 Portlet 环境或使用 JSF 在另一个环境中运行时进行扩展。
配置 Flash
从 JSF 2.2 版本开始,我们有一个钩子可以用来覆盖和/或包装默认的 Flash 实现。通常,我们使用以下代码来引用 Flash 实例:
FacesContext.getCurrentInstance().getExternalContext().getFlash();
当需要高级主题的自定义实现时,你可以执行以下步骤:
-
扩展
FlashFactory
,这是一个能够创建和返回新的Flash
实例的工厂对象。 -
扩展
FlashWrapper
,这是一个简单的Flash
实现,允许我们选择性地覆盖方法。 -
在
faces-config.xml
中配置自定义的Flash
实现。
例如,可以使用以下代码编写自定义的 Flash 工厂:
public class CustomFlashFactory extends FlashFactory {
private FlashFactory flashFactory;
public CustomFlashFactory() {}
public CustomFlashFactory(FlashFactory flashFactory) {
this.flashFactory = flashFactory;
}
@Override
public Flash getFlash(boolean create) {
Flash handler = new CustomFlash(flashFactory.getFlash(create));
return handler;
}
}
getFlash
方法返回的CustomFlash
实例如下:
public class CustomFlash extends FlashWrapper {
private Flash flash;
public CustomFlash(Flash flash){
this.flash = flash;
}
//... override here Flash methods
@Override
public Flash getWrapped() {
return this.flash;
}
}
在CustomFlash
类中,你可以覆盖javax.faces.context.Flash
中需要自定义行为的方法。例如,你可以覆盖setKeepMessages
方法,使用以下代码输出一些日志:
@Override
public void setKeepMessages(boolean newValue){
logger.log(Level.INFO, "setKeepMessages()
was called with value: {0}", newValue);
getWrapped().setKeepMessages(newValue);
}
使用以下代码在faces-config.xml
中配置自定义的 Flash 工厂:
<factory>
<flash-factory>book.beans.CustomFlashFactory</flash-factory>
</factory>
完整的示例命名为ch5_9
。
注意
从 JSF 2.2 版本开始,我们可以使用依赖注入与 Flash(@Inject
和@EJB
)。
JSF 2.2 Window ID API
Window ID 机制的起源依赖于一个 HTML 间隙——这个协议是无状态的,这意味着它不会将客户端与请求关联起来。JSF 通过使用 cookie 来跟踪用户会话解决这个问题,但有时这还不够,需要一个更精细的跟踪机制。例如,如果用户打开几个标签页/窗口,那么所有这些都将使用相同的会话,这意味着将发送相同的 cookie 到服务器,并使用相同的登录账户(当存在登录时)。这可能会成为一个真正的问题,如果用户在这些标签页/窗口中操作修改。
为了解决这个问题提供一个解决方案,JSF 2.2 引入了 Window ID API,允许开发者识别同一会话中的不同标签页/窗口。
注意
在某些情况下,你可以使用视图作用域和 Flash 作用域跟踪用户的窗口 ID。但 Window ID 更容易使用,且专门为此目的。
开发者可以通过在 web.xml
中设置上下文参数 javax.faces.CLIENT_WINDOW_MODE
来选择用于跟踪窗口标识符的方法,如下所示——在 JSF 2.2 中,支持的值是 url
(跟踪激活)和 none
(跟踪关闭):
<context-param>
<param-name>javax.faces.CLIENT_WINDOW_MODE</param-name>
<param-value>url</param-value>
</context-param>
当指定 url
时,用户的窗口标识符通过隐藏字段或名为 jfwid
的请求参数进行跟踪。在下面的屏幕截图中,你可以看到这两个,请求参数和隐藏字段:
注意
当隐藏字段(在回发后可用)和请求参数都可用时,隐藏字段具有更高的优先级。
你可以使用以下代码轻松获取窗口 ID:
public void pullWindowIdAction() {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext=facesContext.getExternalContext();
ClientWindow clientWindow = externalContext.getClientWindow();
if (clientWindow != null) {
logger.log(Level.INFO, "The current client window id is:{0}",
clientWindow.getId());
} else {
logger.log(Level.INFO, "Client Window cannot be determined!");
}
}
注意
可以使用 ExternalContext.getClientWindow
获取 ClientWindow
实例,并将其提供为 ExternalContext.setClientWindow
。
你可以通过以下两种至少两种方式启用/禁用用户窗口跟踪:
-
在
<h:button>
和<h:link>
中,你可以使用disableClientWindow
属性,其值可以是true
或false
,如下面的代码所示:Enable/Disable client window using h:button:<br/> <h:button value="Enable Client Window" outcome="index" disableClientWindow="false"/><br/> <h:button value="Disable Client Window" outcome="index" disableClientWindow="true"/><br/> <hr/> Enable/Disable client window using h:link:<br/> <h:link value="Enable Client Window" outcome="index" disableClientWindow="false"/><br/> <h:link value="Disable Client Window" outcome="index" disableClientWindow="true"/>
-
或者,我们可以使用
disableClientWindowRenderMode
和enableClientWindowRenderMode
方法,如下面的代码所示:private FacesContext facesContext; private ExternalContext externalContext; ... ClientWindow clientWindow = externalContext.getClientWindow(); //disable clientWindow.disableClientWindowRenderMode(facesContext); //enable clientWindow.enableClientWindowRenderMode(facesContext);
一个完整的应用程序包含在本章的代码包中,其名称为 ch5_10_1
。
开发者可以通过扩展 ClientWindowWrapper
类来编写定制的 ClientWindow
实现,这是一个简单且方便的实现,允许我们仅覆盖必要的功能。一种让 JSF 使用你的自定义 ClientWindow
的方法如下:
-
扩展
ClientWindowFactory
,这是一个能够根据传入请求创建ClientWindow
实例的工厂。 -
覆盖
ClientWindowFactory.getClientWindow
方法以创建当前请求的定制ClientWindow
实例。 -
在创建定制
ClientWindow
实例之前,检查上下文参数ClientWindow.CLIENT_WINDOW_MODE_PARAM_NAME
的值。上下文参数的值应等于url
。
基于这三个步骤,我们可以使用以下代码编写一个定制的 ClientWindowFactory
实现:
public class CustomClientWindowFactory
extends ClientWindowFactory {
private ClientWindowFactory clientWindowFactory;
public CustomClientWindowFactory() {}
public CustomClientWindowFactory(ClientWindowFactory
clientWindowFactory) {
this.clientWindowFactory = clientWindowFactory;
}
@Override
public ClientWindow getClientWindow(FacesContext context) {
if (context.getExternalContext().getInitParameter
(ClientWindow.CLIENT_WINDOW_MODE_PARAM_NAME).equals("url")) {
ClientWindow defaultClientWindow =
clientWindowFactory.getClientWindow(context);
ClientWindow customClientWindow = new
CustomClientWindow(defaultClientWindow);
return customClientWindow;
}
return null;
}
@Override
public ClientWindowFactory getWrapped() {
return clientWindowFactory;
}
}
CustomClientWindow
实现是 ClientWindowWrapper
的扩展,它允许我们仅覆盖所需的方法。在我们的情况下,我们感兴趣的两个方法是名为 getId
的方法,它返回一个 String
值,该值在当前会话的作用域内唯一标识 ClientWindow
。另一个方法是名为 decode
的方法,它负责提供 getId
返回的值。为了提供此值,decode
方法应遵循以下检查:
-
请求一个名为
ResponseStateManager.CLIENT_WINDOW_PARAM
值的参数。 -
如果此检查没有返回一个令人满意的身份标识符,请查找名为
ResponseStateManager.CLIENT_WINDOW_URL_PARAM
值的请求参数。 -
如果找不到 ID 值,那么在当前会话的作用域内创建一个唯一标识此
ClientWindow
的 ID。
此外,我们可以编写一个自定义的 ClientWindow
实现,该实现将生成一个自定义 ID,类型为 CUSTOM
——当前日期的毫秒数。代码如下——请注意查看 decode
方法是如何实现的:
public class CustomClientWindow extends ClientWindowWrapper {
private ClientWindow clientWindow;
String id;
public CustomClientWindow() {}
public CustomClientWindow(ClientWindow clientWindow) {
this.clientWindow = clientWindow;
}
@Override
public void decode(FacesContext context) {
Map<String, String> requestParamMap =
context.getExternalContext().getRequestParameterMap();
if (isClientWindowRenderModeEnabled(context)) {
id = requestParamMap.
get(ResponseStateManager.CLIENT_WINDOW_URL_PARAM);
}
if (requestParamMap.containsKey
(ResponseStateManager.CLIENT_WINDOW_PARAM)) {
id = requestParamMap.get
(ResponseStateManager.CLIENT_WINDOW_PARAM);
}
if (id == null) {
long time = new Date().getTime();
id = "CUSTOM-" + time;
}
}
@Override
public String getId() {
return id;
}
@Override
public ClientWindow getWrapped() {
return this.clientWindow;
}
}
最后,使用以下代码在 faces-config.xml
中配置自定义的 ClientWindowFactory
实现:
<factory>
<client-window-factory>
book.beans.CustomClientWindowFactory
</client-window-factory>
</factory>
完成!完整的应用程序命名为 ch5_10_3
。
如果你想创建一个类型为 UUID-uuid::counter
的 ID,那么你可以编写 decode
方法,如下所示:
@Override
public void decode(FacesContext context) {
Map<String, String> requestParamMap =
context.getExternalContext().getRequestParameterMap();
if (isClientWindowRenderModeEnabled(context)) {
id = requestParamMap.get
(ResponseStateManager.CLIENT_WINDOW_URL_PARAM);
}
if (requestParamMap.
containsKey(ResponseStateManager.CLIENT_WINDOW_PARAM)) {
id = requestParamMap.get
(ResponseStateManager.CLIENT_WINDOW_PARAM);
}
if (id == null) {
synchronized (context.getExternalContext().getSession(true)) {
final String clientWindowKey = "my.custom.id";
ExternalContext externalContext =
context.getExternalContext();
Map<String, Object> sessionAttrs =
externalContext.getSessionMap();
Integer counter = (Integer) sessionAttrs.get(clientWindowKey);
if (counter == null) {
counter = 0;
}
String uuid = UUID.randomUUID().toString();
id = "UUID-" + uuid + "::" + counter;
sessionAttrs.put(clientWindowKey, ++counter);
}
}
}
在这种情况下,完整的应用程序命名为 ch5_10_4
。
当你决定使用类型为 SESSION_ID::counter
的 ID 时,使用计数器可能非常有用。由于会话 ID 将在多个窗口/标签页中保持相同,你需要计数器来区分 ID。这种类型的 ID 可以通过 JSF 2.2 的 ExternalContext.getSessionId
方法轻松获得,如下所示:
String sessionId = externalContext.getSessionId(false);
id = sessionId + "::" + counter;
配置生命周期
如你所知,JSF 生命周期包含六个阶段。为了被处理,每个 JSF 请求都将通过所有这些阶段,或者只通过其中的一部分。生命周期模型的抽象由 javax.faces.lifecycle.Lifecycle
类表示,该类负责在两个方法中执行 JSF 阶段:
-
execute
方法将执行所有阶段,除了第六阶段,即 Render Response 阶段。 -
render
方法将执行第六阶段。
可以通过以下步骤编写自定义的 Lifecycle
:
-
扩展
LifecycleFactory
,这是一个能够创建并返回一个新的Lifecycle
实例的工厂对象。 -
扩展
LifecycleWrapper
,这是一个简单的LifecycleLifecycle
实现,允许我们选择性地覆盖方法。 -
在
faces-config.xml
中配置自定义的Lifecycle
实现。 -
在
web.xml
中配置自定义的Lifecycle
实现。
让我们从扩展 LifecycleFactory
开始,创建一个通用的自定义 Lifecycle
,如下所示——注意我们如何使用唯一标识符注册自定义 Lifecycle
实现:
public class CustomLifecycleFactory extends LifecycleFactory {
public static final String CUSTOM_LIFECYCLE_ID = "CustomLifecycle";
private LifecycleFactory lifecycleFactory;
public CustomLifecycleFactory(){}
public CustomLifecycleFactory(LifecycleFactory lifecycleFactory) {
this.lifecycleFactory = lifecycleFactory;
Lifecycle defaultLifecycle = this.lifecycleFactory.
getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);
addLifecycle(CUSTOM_LIFECYCLE_ID, new
CustomLifecycle(defaultLifecycle));
}
@Override
public final void addLifecycle(String lifecycleId,Lifecycle lifecycle) {
lifecycleFactory.addLifecycle(lifecycleId, lifecycle);
}
@Override
public Lifecycle getLifecycle(String lifecycleId) {
return lifecycleFactory.getLifecycle(lifecycleId);
}
@Override
public Iterator<String> getLifecycleIds() {
return lifecycleFactory.getLifecycleIds();
}
}
此外,CustomLifecycle
扩展了 LifecycleWrapper
并覆盖了所需的方法。为了访问被包装的类的实例,我们需要如下覆盖 getWrapped
方法:
public class CustomLifecycle extends LifecycleWrapper {
private Lifecycle lifecycle;
public CustomLifecycle(Lifecycle lifecycle) {
this.lifecycle = lifecycle;
}
...
@Override
public Lifecycle getWrapped() {
return lifecycle;
}
}
接下来,我们需要在 faces-config.xml
中配置我们的自定义生命周期工厂,如下所示:
<factory>
<lifecycle-factory>book.beans.CustomLifecycleFactory</lifecycle-factory>
</factory>
最后,我们需要使用其标识符在 web.xml
中注册自定义生命周期(参见突出显示的代码):
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<init-param>
<param-name>javax.faces.LIFECYCLE_ID</param-name>
<param-value>CustomLifecycle</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
在这个时刻,我们有一个功能性的模拟自定义生命周期。接下来,我们将添加一些真实的功能,为此我们专注于Lifecycle.attachWindow
方法。这个方法是在 JSF 2.2 中引入的,用于将ClientWindow
实例附加到当前请求。在Lifecycle.attachWindow
方法中,ClientWindow
实例与传入的请求相关联。此方法将导致创建一个新的ClientWindow
实例,分配一个 ID,然后传递给ExternalContext.setClientWindow(ClientWindow)
。
在JSF 2.2 Window ID API部分,你看到了如何探索识别不同用户窗口/标签页的默认机制。基于这些知识,我们编写了一个自定义的ClientWindow
实现,为jfwid
请求参数提供自定义 ID——类型为CUSTOM—当前日期的毫秒数
——以及类型UUID::counter
。自定义客户端窗口是通过自定义的ClientWindowFactory
实现设置的。此外,我们通过覆盖以下代码中的attachWindow
方法设置了相同的自定义客户端窗口:
public class CustomLifecycle extends LifecycleWrapper {
private static final Logger logger =
Logger.getLogger(CustomLifecycle.class.getName());
private Lifecycle lifecycle;
public CustomLifecycle(Lifecycle lifecycle) {
this.lifecycle = lifecycle;
}
@Override
public void attachWindow(FacesContext context) {
if (context.getExternalContext().getInitParameter
(ClientWindow.CLIENT_WINDOW_MODE_PARAM_NAME).equals("url")) {
ExternalContext externalContext =
context.getExternalContext();
ClientWindow clientWindow = externalContext.getClientWindow();
if (clientWindow == null) {
clientWindow = createClientWindow(context);
if (clientWindow != null) {
CustomClientWindow customClientWindow = new
CustomClientWindow(clientWindow);
customClientWindow.decode(context);
externalContext.setClientWindow(customClientWindow);
}
}
}
}
private ClientWindow createClientWindow(FacesContext context) {
ClientWindowFactory clientWindowFactory = (ClientWindowFactory)
FactoryFinder.getFactory(FactoryFinder.CLIENT_WINDOW_FACTORY);
return clientWindowFactory.getClientWindow(context);
}
...
}
完成!完整的应用程序命名为ch5_10_2
。
配置应用程序
应用程序代表一个针对每个 Web 应用程序的单例对象,这是 JSF 运行时的核心。通过这个对象,我们可以完成许多任务,例如添加组件、转换器、验证器、订阅事件、设置监听器、地区和消息包。它代表许多 JSF 实体的入口点。我们使用以下代码来引用它:
FacesContext.getCurrentInstance().getApplication();
应用程序可以通过以下步骤进行扩展和自定义:
-
扩展
ApplicationFactory
,这是一个能够创建并返回新的Application
实例的工厂对象。 -
扩展
ApplicationWrapper
,这是一个简单的Application
实现,允许我们选择性地覆盖方法。 -
在
faces-config.xml
中配置自定义的Application
实现。
例如,我们可以使用自定义的Application
实现为应用程序添加一个验证器列表。我们首先编写一个自定义应用程序工厂,如下所示:
public class CustomApplicationFactory extends ApplicationFactory {
private ApplicationFactory applicationFactory;
public CustomApplicationFactory(){}
public CustomApplicationFactory(ApplicationFactory applicationFactory) {
this.applicationFactory = applicationFactory;
}
@Override
public void setApplication(Application application) {
applicationFactory.setApplication(application);
}
@Override
public Application getApplication() {
Application handler = new CustomApplication(
applicationFactory.getApplication());
return handler;
}
}
现在,工作由CustomApplication
按照以下方式完成:
public class CustomApplication extends ApplicationWrapper {
private Application application;
public CustomApplication(Application application) {
this.application = application;
}
@Override
public Application getWrapped() {
return application;
}
@Override
public void addValidator(java.lang.String validatorId,
java.lang.String validatorClass) {
boolean
flag = false;
Iterator i = getWrapped().getValidatorIds();
while (i.hasNext()) {
if (i.next().equals("emailValidator")) {
flag = true;
break;
}
}
if (flag == false) {
getWrapped().addValidator("emailValidator",
"book.beans.EmailValidator");
}
getWrapped().addValidator(validatorId, validatorClass);
}
}
最后,在faces-config.xml
中按照以下方式配置新的自定义应用程序:
<factory>
<application-factory>
book.beans.CustomApplicationFactory
</application-factory>
</factory>
注意
从 JSF 2.2 开始,我们可以使用应用程序对象进行依赖注入(@Inject
和@EJB
)。前面提供的示例,其中验证器列表由 CDI 豆作为Map
提供,可以在本章的代码包中找到,名称为ch5_11
。
配置 VDL
简称 VDL 代表视图声明语言,它表示视图声明语言必须实现以与 JSF 运行时交互的合约。ViewDeclarationLanguageFactory
类用于创建并返回ViewDeclarationLanguage
类的实例。
为了改变运行时如何将输入文件转换为组件树,你需要编写一个自定义的 ViewDeclarationLanguageFactory
实现类,这可以通过扩展原始类并重写 getViewDeclarationLanguage
方法来完成,如下面的代码所示:
public class CustomViewDeclarationLanguageFactory
extends ViewDeclarationLanguageFactory{
private ViewDeclarationLanguageFactory
viewDeclarationLanguageFactory;
public CustomViewDeclarationLanguageFactory
(ViewDeclarationLanguageFactory viewDeclarationLanguageFactory){
this.viewDeclarationLanguageFactory =
viewDeclarationLanguageFactory;
}
@Override
public ViewDeclarationLanguage
getViewDeclarationLanguage(String viewId) {
return new
CustomViewDeclarationLanguage(viewDeclarationLanguageFactory.
getViewDeclarationLanguage(viewId));
}
}
CustomViewDeclarationLanguage
的实现可以通过扩展 ViewDeclarationLanguage
并重写所有方法,或者通过扩展新的 JSF 2.2 ViewDeclarationLanguageWrapper
类并仅重写所需的方法来完成。我们的 CustomViewDeclarationLanguage
实现代表了一个基于包装类的简单骨架,如下面的代码所示:
public class CustomViewDeclarationLanguage extends
ViewDeclarationLanguageWrapper {
private ViewDeclarationLanguage viewDeclarationLanguage;
public CustomViewDeclarationLanguage
(ViewDeclarationLanguage viewDeclarationLanguage) {
this.viewDeclarationLanguage = viewDeclarationLanguage;
}
//override here the needed methods
@Override
public ViewDeclarationLanguage getWrapped() {
return viewDeclarationLanguage;
}
}
此工厂可以在 faces-config.xml
中配置如下:
<factory>
<view-declaration-language-factory>
book.beans.CustomViewDeclarationLanguageFactory
</view-declaration-language-factory>
</factory>
完成!完整的应用程序命名为 ch5_17
。
在 code.google.com/p/javavdl/
,你可以看到一个 JSF VDL 的实现,它允许使用纯 Java 编写页面或完整的 JSF 应用程序,无需任何 XML 或其他声明性标记(例如,Facelets)。
多个工厂的联合力量
在最后几个部分中,你看到了如何自定义和配置最常用的 JSF 工厂。在本章的最后部分,你将看到如何在同一应用程序中利用几个工厂。例如,一个方便的场景假设我们想要触发一个非 JSF 请求并获取一个 JSF 视图作为响应。此场景的一个方法就是编写一个 Java Servlet,它能够将非 JSF 请求转换为 JSF 视图。
为了编写这样的 Servlet,我们需要获取对 FacesContext
的访问权限。为此,我们可以结合默认的 LifecycleFactory
类的强大功能与默认的 FacesContextFactory
类的强大功能。进一步地,我们可以通过 FacesContext
访问 Application
,这意味着我们可以通过 createView
方法获取负责创建 JSF 视图的 ViewHandler
。一旦视图创建完成,我们所需做的就是设置 UIViewRoot
并告诉 Lifecycle
渲染响应(执行 Render Response 阶段)。在代码行中,Servlet 看起来如下所示:
@WebServlet(name = "JSFServlet", urlPatterns = {"/jsfServlet"})
public class JSFServlet extends HttpServlet {
...
protected void processRequest(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String page = request.getParameter("page");
LifecycleFactory lifecycleFactory = (LifecycleFactory)
FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
Lifecycle lifecycle = lifecycleFactory.getLifecycle
(LifecycleFactory.DEFAULT_LIFECYCLE);
FacesContextFactory facesContextFactory = (FacesContextFactory)
FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
FacesContext facesContext = facesContextFactory.getFacesContext
(request.getServletContext(), request, response, lifecycle);
Application application = facesContext.getApplication();
ViewHandler viewHandler = application.getViewHandler();
UIViewRoot uiViewRoot = viewHandler.
createView(facesContext, "/" + page);
facesContext.setViewRoot(uiViewRoot);
lifecycle.render(facesContext);
}
...
现在,你可以使用 <h:outputLink>
标签非常容易地进行测试,如下所示:
Navigate page-to-page via h:outputLink - WON'T WORK!
<h:outputLink value="done.xhtml">done.xhtml</h:outputLink>
Navigate page-to-page via h:outputLink, but add context path for the application to a context-relative path - WORK!
<h:outputLink value="#{facesContext.externalContext.
applicationContextPath}/faces/done.xhtml">
done.xhtml</h:outputLink>
Navigate to a JSF view via a non-JSF request using servlet - WORK!
<h:outputLink value="jsfServlet?page=done.xhtml">
done.xhml</h:outputLink>
完整的应用程序命名为 ch5_18
。
摘要
嗯,这是一个相当沉重的章节,但在这里触及了 JSF 的重要方面。你学习了如何创建、扩展和配置几个主要的 JSF 2.x 元素,以及它们是如何通过 JSF 2.2 改进的,特别是依赖注入机制。在本章中还有许多未讨论的内容,例如状态管理、Facelets 工厂等,但请继续阅读。
欢迎在下一章中见到我们,我们将讨论在 JSF 中处理表格数据。
第六章. 处理表格数据
当以电子表格(或表格结构)显示时具有意义的表格数据。在 Web 应用程序中,表格数据通常从数据库中获取,其中数据以关系表的形式本地表示。用于显示表格数据的 JSF 主要组件由<h:dataTable>
标签表示,该标签能够生成 HTML 经典表格。本章是对这个标签的致敬,因为表格数据非常常用,并且可以以多种方式操作。因此,在本章中,你将学习以下主题:
-
创建简单的 JSF 表格
-
JSF 2.2 的
CollectionDataModel
类 -
排序表格
-
删除表格行
-
编辑/更新表格行
-
添加新行
-
显示行号
-
选择单行
-
选择多行
-
嵌套表格
-
分页表格
-
使用 JSF API 生成表格
-
过滤表格
-
表格样式
注意
本章更侧重于填充来自集合(数据库)数据的表格。但是,你可以使用几乎任何 JSF UI 组件在表格中包含和操作内容。
创建简单的 JSF 表格
最常见的情况是,一切从一个 POJO 类(或 EJB 实体类)开始,如下面的代码所示——请注意,跳过了包含硬编码信息的表格:
public class Players {
private String player;
private byte age;
private String birthplace;
private String residence;
private short height;
private byte weight;
private String coach;
private Date born;
private int ranking;
public Players() {}
public Players(int ranking, String player, byte age, String birthplace, String residence, short height, byte weight, String coach, Date born) {
this.ranking = ranking;
this.player = player;
this.age = age;
this.birthplace = birthplace;
this.residence = residence;
this.height = height;
this.weight = weight;
this.coach = coach;
this.born = born;
}
...
//getters and setters
}
这个 POJO 类的每个实例实际上是用户显示的表格中的一行(这不是强制性的,但通常就是这样)。接下来,一个 JSF bean(或 CDI bean)将提供一个 POJO 实例的集合。(List
、Map
和Set
实例是最常用的。)在下面的代码中,显示了List
实例:
@Named
@ViewScoped
public class PlayersBean implements Serializable{
List<Players> data = new ArrayList<>();
final SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
public PlayersBean() {
try {
data.add(new Players(2, "NOVAK DJOKOVIC", (byte) 26, "Belgrade, Serbia", "Monte Carlo, Monaco", (short) 188, (byte) 80, "Boris Becker, Marian Vajda", sdf.parse("22.05.1987")));
data.add(new Players(1, "RAFAEL NADAL", (byte) 27, "Manacor, Mallorca, Spain", "Manacor, Mallorca, Spain", (short) 185, (byte) 85, "Toni Nadal", sdf.parse("03.06.1986")));
data.add(new Players(7, "TOMAS BERDYCH", (byte) 28, "Valasske Mezirici, Czech", "Monte Carlo, Monaco", (short) 196, (byte) 91, "Tomas Krupa", sdf.parse("17.09.1985")));
...
} catch (ParseException ex) {
Logger.getLogger(PlayersBean.class.getName()).log(Level.SEVERE, null, ex);
}
}
public List<Players> getData() {
return data;
}
public void setData(List<Players> data) {
this.data = data;
}
}
注意,通常数据是从数据库中查询的,但这里并不完全相关。
这个常见场景以一段显示屏幕上数据的代码结束。代码如下所示:
...
<h:dataTable value="#{playersBean.data}" var="t">
<h:column>
<f:facet name="header">Ranking</f:facet>
#{t.ranking}
</h:column>
<h:column>
<f:facet name="header">Name</f:facet>
#{t.player}
</h:column>
<h:column>
<f:facet name="header">Age</f:facet>
#{t.age}
</h:column>
<h:column>
<f:facet name="header">Birthplace</f:facet>
#{t.birthplace}
</h:column>
<h:column>
<f:facet name="header">Residence</f:facet>
#{t.residence}
</h:column>
<h:column>
<f:facet name="header">Height (cm)</f:facet>
#{t.height}
</h:column>
<h:column>
<f:facet name="header">Weight (kg)</f:facet>
#{t.weight}
</h:column>
<h:column>
<f:facet name="header">Coach</f:facet>
#{t.coach}
</h:column>
<h:column>
<f:facet name="header">Born</f:facet>
<h:outputText value="#{t.born}">
<f:convertDateTime pattern="dd.MM.yyyy" />
</h:outputText>
</h:column>
</h:dataTable>
...
下面的截图显示了输出:
完整示例可在本章的代码包中找到,命名为ch6_1
。
JSF 2.2 的 CollectionDataModel 类
直到 JSF 2.2 版本,<h:dataTable>
标签支持的类型包括java.util.List
、数组、java.sql.ResultSet
、javax.servlet.jsp.jstl.sql.Result
、javax.faces.model.DataModel
、null(或空列表)以及用作标量值的类型。
注意
从 JSF 2.2 版本开始,我们也可以使用java.util.Collection
。这对于 Hibernate/JPA 用户来说特别有用,因为他们通常使用Set
集合来处理实体关系。因此,没有什么可以阻止我们使用HashSet
、TreeSet
或LinkedHashSet
集合来为我们的 JSF 表格提供数据。
下一个示例类似于最常用 Java 集合的测试用例。首先,让我们声明一些Players
集合,如下所示:
-
java.util.ArrayList
:这个库实现了java.util.Collection
。java.util.ArrayList
集合的声明如下:ArrayList<Players> dataArrayList = new ArrayList<>();
-
java.util.LinkedList
:此库实现了java.util.Collection
。java.util.LinkedList
集合声明如下:LinkedList<Players> dataLinkedList = new LinkedList<>();
-
java.util.HashSet
:此库实现了java.util.Collection
。以下为java.util.HashSet
集合的代码:HashSet<Players> dataHashSet = new HashSet<>();
-
java.util.TreeSet
:此库实现了java.util.Collection
。java.util.TreeSet
集合声明如下:TreeSet<Players> dataTreeSet = new TreeSet<>();
注意
对于
TreeSet
集合,你必须使用Comparable
元素,或者提供Comparator
。否则,由于不知道如何排序元素,TreeSet
集合将无法完成其工作。这意味着Players
类应该实现Comparable<Players>
。 -
java.util.LinkedHashSet
:此库实现了java.util.Collection
。java.util.LinkedHashSet
集合声明如下:LinkedHashSet<Players> dataLinkedHashSet = new LinkedHashSet<>();
-
java.util.HashMap
:此库没有实现java.util.Collection
。java.util.HashMap
集合声明如下:HashMap<String, Players> dataHashMap = new HashMap<>();
-
java.util.TreeMap
:此库没有实现java.util.Collection
。java.util.TreeMap
集合声明如下:TreeMap<String, Players> dataTreeMap = new TreeMap<>();
-
java.util.LinkedHashMap
:此库没有实现java.util.Collection
。以下为java.util.LinkedHashMap
集合的代码:LinkedHashMap<String, Players> dataLinkedHashMap = new LinkedHashMap<>();
假设这些集合已填充且获取器可用;它们将以以下方式在表中显示其内容:
-
java.util.ArrayList
:此库实现了java.util.Collection
。以下为java.util.ArrayList
集合的代码:<h:dataTable value="#{playersBean.dataArrayList}" var="t"> <h:column> <f:facet name="header">Ranking</f:facet> #{t.ranking} </h:column> ... </h:dataTable>
注意
以同样的方式,我们可以在表中显示
LinkedList
、HashSet
、TreeSet
和LinkedHashSet
集合类。 -
java.util.LinkedList
:此库实现了java.util.Collection
。以下为java.util.LinkedList
集合的代码:<h:dataTable value="#{playersBean.dataLinkedList}" var="t"> <h:column> <f:facet name="header">Ranking</f:facet> #{t.ranking} </h:column> ... </h:dataTable>
-
java.util.HashSet
:此库实现了java.util.Collection
。以下为java.util.HashSet
集合的代码:<h:dataTable value="#{playersBean.dataHashSet}" var="t"> <h:column> <f:facet name="header">Ranking</f:facet> #{t.ranking} </h:column> ... </h:dataTable>
-
java.util.TreeSet
:此库实现了java.util.Collection
。以下为java.util.TreeSet
集合的代码:<h:dataTable value="#{playersBean.dataTreeSet}" var="t"> <h:column> <f:facet name="header">Ranking</f:facet> #{t.ranking} </h:column> ... </h:dataTable>
-
java.util.LinkedHashSet
:此库实现了java.util.Collection
。以下为java.util.LinkedHashSet
集合的代码:<h:dataTable value="#{playersBean.dataLinkedHashSet}" var="t"> <h:column> <f:facet name="header">Ranking</f:facet> #{t.ranking} </h:column> ... </h:dataTable>
注意
使用以下示例在表中显示一个
Map
集合。(HashMap
,TreeMap
, 和LinkedHashMap
以相同的方式显示。) -
java.util.HashMap
:此库没有实现java.util.Collection
。以下为java.util.HashMap
集合的代码:<h:dataTable value="#{playersBean.dataHashMap.entrySet()}" var="t"> <h:column> <f:facet name="header">Ranking</f:facet> #{t.key} </h:column> <h:column> <f:facet name="header">Name</f:facet> #{t.value.player} </h:column> ... </h:dataTable>
-
java.util.TreeMap
:此库没有实现java.util.Collection
。以下为java.util.TreeMap
集合的代码:<h:dataTable value="#{playersBean.dataTreeMap.entrySet()}" var="t"> <h:column> <f:facet name="header">Ranking</f:facet> #{t.key} </h:column> <h:column> <f:facet name="header">Name</f:facet> #{t.value.player} </h:column> ... </h:dataTable>
-
java.util.LinkedHashMap
:此库没有实现java.util.Collection
。以下为java.util.LinkedHashMap
集合的代码:<h:dataTable value="#{playersBean.dataLinkedHashMap.entrySet()}" var="t"> <h:column> <f:facet name="header">Ranking</f:facet> #{t.key} </h:column> <h:column> <f:facet name="header">Name</f:facet> #{t.value.player} </h:column> ... </h:dataTable>
对于 Map
集合,你可以有一个获取方法,如下所示:
HashMap<String, Players> dataHashMap = new HashMap<>();
public Collection<Players> getDataHashMap() {
return dataHashMap.values();
}
在这种情况下,表的代码将如下所示:
<h:dataTable value="#{playersBean.dataHashMap}" var="t">
<h:column>
<f:facet name="header">Ranking</f:facet>
#{t.ranking}
</h:column>
...
</h:dataTable>
注意
CollectionDataModel
类是DataModel
类的扩展,它包装了一个 Java 对象的Collection
类。此外,在本章中,你将看到一些示例,这些示例将改变这个新类。
完整的示例可以在本章的代码包中找到,命名为ch6_2
。
排序表格
在前面的示例中,数据是任意显示的。对数据进行排序可以提供在读取和使用信息时的更多清晰度和准确性;例如,请参阅创建简单的 JSF 表格部分的截图。你可以尝试在 ATP 排名中视觉定位数字 1,以及数字 2 和数字 3 等,但更有用的是有按排名列排序表格的选项。这是一个相对简单的实现任务,尤其是如果你熟悉 Java 的List
、Comparator
和Comparable
功能。本书的范围不涉及这些功能,但你可以通过重写compare
方法来完成大多数排序任务,该方法的流程简单明了:它比较两个参数的顺序,并返回一个负整数、零或正整数,表示第一个参数小于、等于或大于第二个参数。例如,让我们看看一些常见的排序:
-
对字符串列表进行排序,例如球员的姓名。为此排序,
compare
方法的代码如下:... String dir="asc"; //or "dsc" for descending sort Collections.sort(data, new Comparator<Players>() { @Override public int compare(Players key_1, Players key_2) { if (dir.equals("asc")) { return key_1.getPlayer().compareTo(key_2.getPlayer()); } else { return key_2.getPlayer().compareTo(key_1.getPlayer()); } } }); ...
-
对数字列表进行排序,例如球员排名。为此排序,
compare
方法的代码如下:... int dir = 1; //1 for ascending, -1 for descending Collections.sort(data, new Comparator<Players>() { @Override public int compare(Players key_1, Players key_2) { return dir * (key_1.getRanking() - key_2.getRanking()); } }); ...
-
对日期列表进行排序,例如球员的生日(这与字符串的情况相同)。为此排序,
compare
方法的代码如下:... String dir="asc"; //or "dsc" for descending sort Collections.sort(data, new Comparator<Players>() { @Override public int compare(Players key_1, Players key_2) { if (dir.equals("asc")) { return key_1.getBorn().compareTo(key_2.getBorn()); } else { return key_2.getBorn().compareTo(key_1.getBorn()); } } }); ...
注意
data
参数代表List
集合类型,因为并非所有类型的集合都可以替代这个类型。例如,List
将完美工作,而HashSet
则不会。对于不是List
集合的集合,有不同的解决方案来进行排序。你必须确保为你的情况选择正确的集合。
如果你知道如何为所选集合编写比较器,那么其他一切都很简单。你可以将这些比较器封装在托管 Bean 的方法中,并附加按钮、链接或其他调用这些方法的任何东西。例如,你可以将这些比较器添加到PlayersBean
后端 Bean 中,如下面的代码所示:
@Named
@ViewScoped
public class PlayersBean implements Serializable{
List<Players> data = new ArrayList<>();
final SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
public PlayersBean() {
try {
data.add(new Players(2, "NOVAK DJOKOVIC", (byte) 26, "Belgrade, Serbia", "Monte Carlo, Monaco", (short) 188, (byte) 80, "Boris Becker, Marian Vajda", sdf.parse("22.05.1987")));
...
} catch (ParseException ex) {
Logger.getLogger(PlayersBean.class.getName()).log(Level.SEVERE, null, ex);
}
}
public List<Players> getData() {
return data;
}
public void setData(List<Players> data) {
this.data = data;
}
public String sortDataByRanking(final int dir) {
Collections.sort(data, new Comparator<Players>() {
@Override
public int compare(Players key_1, Players key_2) {
return dir * (key_1.getRanking() - key_2.getRanking());
}
});
return null;
}
public String sortDataByName(final String dir) {
Collections.sort(data, new Comparator<Players>() {
@Override
public int compare(Players key_1, Players key_2) {
if (dir.equals("asc")) {
return key_1.getPlayer().compareTo(key_2.getPlayer());
} else {
return key_2.getPlayer().compareTo(key_1.getPlayer());
}
}
});
return null;
}
public String sortDataByDate(final String dir) {
Collections.sort(data, new Comparator<Players>() {
@Override
public int compare(Players key_1, Players key_2) {
if (dir.equals("asc")) {
return key_1.getBorn().compareTo(key_2.getBorn());
} else {
return key_2.getBorn().compareTo(key_1.getBorn());
}
}
});
return null;
}
...
接下来,你可以轻松修改index.xhtml
页面的代码,以提供访问排序功能,如下所示:
...
<h:dataTable value="#{playersBean.data}" var="t" border="1">
<h:column>
<f:facet name="header">
<h:commandLink action="#{playersBean.sortDataByRanking(1)}">
Ranking ASC
</h:commandLink>
|
<h:commandLink action="#{playersBean.sortDataByRanking(-1)}">
Ranking DSC
</h:commandLink>
</f:facet>
#{t.ranking}
</h:column>
<h:column>
<f:facet name="header">
<h:commandLink action="#{playersBean.sortDataByName('asc')}">
Name ASC
</h:commandLink>
|
<h:commandLink action="#{playersBean.sortDataByName('dsc')}">
Name DSC
</h:commandLink>
</f:facet>
#{t.player}
</h:column>
...
<h:column>
<f:facet name="header">
<h:commandLink action="#{playersBean.sortDataByDate('asc')}">
Born ASC
</h:commandLink>
|
<h:commandLink action="#{playersBean.sortDataByDate('dcs')}">
Born DSC
</h:commandLink>
</f:facet>
<h:outputText value="#{t.born}">
<f:convertDateTime pattern="dd.MM.yyyy" />
</h:outputText>
</h:column>
</h:dataTable>
...
输出显示在下述屏幕截图中:
完整的示例可以在本章的代码包中找到,命名为ch6_3_1
。
如你所见,每个排序都提供了两个链接:一个用于升序,一个用于降序。我们可以通过在我们的视图作用域 Bean 中使用一个额外的属性轻松地将这些链接粘合在一起。例如,我们可以声明一个名为sortType
的属性,如下所示:
...
private String sortType = "asc";
...
添加一个简单的条件,使其在升序和降序排序之间切换,如下面的代码所示:
...
public String sortDataByRanking() {
Collections.sort(data, new Comparator<Players>() {
@Override
public int compare(Players key_1, Players key_2) {
if(sortType.equals("asc")){
return key_1.getRanking() - key_2.getRanking();
} else {
return (-1) * (key_1.getRanking() - key_2.getRanking());
}
}
});
sortType = (sortType.equals("asc")) ? "dsc" : "asc";
return null;
}
...
现在,index.xhtml
页面包含每个排序的单个链接,如下面的代码所示:
...
<h:dataTable value="#{playersBean.data}" var="t" border="1">
<h:column>
<f:facet name="header">
<h:commandLink action="#{playersBean.sortDataByRanking()}">
Ranking
</h:commandLink>
</f:facet>
#{t.ranking}
</h:column>
...
这个技巧的输出可以在下面的屏幕截图中看到:
完整的示例可以在本章的代码包中找到,命名为ch6_3_2
。
排序和数据模型 – 集合数据模型
一个更复杂的排序示例涉及一个扩展了javax.faces.model.DataModel
类的装饰器类。即使我们没有意识到,JSF 也会使用DataModel
类,因为每个集合(List
、数组、HashMap
等等)都被 JSF 包装在一个DataModel
类中(或者,在子类中,如ArrayDataModel
、CollectionDataModel
、ListDataModel
、ResultDataModel
、ResultSetDataModel
或ScalarDataModel
)。当 JSF 渲染/解码表格数据时,它会调用DataModel
类的方法。在下面的屏幕截图中,你可以看到DataModel
类的所有直接已知的子类:
正如你将在本章中看到的,有时你需要了解DataModel
类,因为你需要改变其默认行为。(建议你快速查看该类官方文档的javaserverfaces.java.net/nonav/docs/2.2/javadocs/
部分,以获得更好的理解。)最常见的情况涉及渲染行号、排序和改变表格的行数。当你这样做时,你会暴露DataModel
类而不是底层的集合。
例如,假设我们需要使用一个集合,如HashSet
。这个集合不保证迭代顺序在时间上保持不变,如果我们想对其进行排序,这可能会成为一个问题。当然,有一些解决方案,比如将其转换为List
或使用TreeSet
代替,但我们可以改变包装HashSet
集合的DataModel
类,这是新的 JSF 2.2 类CollectionDataModel
。
我们可以通过以下步骤实现这一点,如下所示:
-
扩展
CollectionDataModel
类以覆盖其方法的默认行为,如下面的代码所示:public class SortDataModel<T> extends CollectionDataModel<T> { ...
-
提供一个构造函数并使用它来传递原始模型(在这种情况下,
CollectionDataModel
)。除了原始模型外,我们还需要一个表示行索引的整数数组(例如,rows[0]=0
、rows[1]=1
、...rows[n]= model.getRowCount()
)。实际上,排序行索引将排序HashSet
集合,如下面的代码所示:... CollectionDataModel<T> model; private Integer[] rows; public SortDataModel(CollectionDataModel<T> model) { this.model = model; initRows(); } private void initRows() { int rowCount = model.getRowCount(); if (rowCount != -1) { this.rows = new Integer[rowCount]; for (int i = 0; i < rowCount; ++i) { rows[i] = i; } } } ...
-
接下来,我们需要覆盖
setRowIndex
方法来替换默认的行索引,如下面的代码所示:@Override public void setRowIndex(int rowIndex) { if ((0 <= rowIndex) && (rowIndex < rows.length)) { model.setRowIndex(rows[rowIndex]); } else { model.setRowIndex(rowIndex); } }
-
最后,提供以下比较器:
public void sortThis(final Comparator<T> comparator) { Comparator<Integer> rowc = new Comparator<Integer>() { @Override public int compare(Integer key_1, Integer key_2) { T key_1_data = getData(key_1); T key_2_data = getData(key_2); return comparator.compare(key_1_data, key_2_data); } }; Arrays.sort(rows, rowc); } private T getData(int row) { int baseRowIndex = model.getRowIndex(); model.setRowIndex(row); T newRowData = model.getRowData(); model.setRowIndex(baseRowIndex); return newRowData; }
-
现在,我们的具有排序功能的自定义
CollectionDataModel
类已经准备好了。我们可以通过声明和填充HashSet
,将其包装在原始的CollectionDataModel
类中,并将其传递给自定义的SortDataModel
类来测试它,如下面的代码所示:private HashSet<Players> dataHashSet = new HashSet<>(); private SortDataModel<Players> sortDataModel; ... public PlayersBean() { dataHashSet.add(new Players(2, "NOVAK DJOKOVIC", (byte) 26, "Belgrade, Serbia", "Monte Carlo, Monaco", (short) 188, (byte) 80, "Boris Becker, Marian Vajda", sdf.parse("22.05.1987"))); ... sortDataModel = new SortDataModel<>(new CollectionDataModel<>(dataHashSet)); } ...
-
由于我们是调用者,我们需要提供一个比较器。完整的示例可以在本章的代码包中找到,命名为
ch6_3_3
。
删除表格行
删除表格行可以通过执行以下步骤轻松实现:
-
在管理 Bean 中定义一个方法,该方法接收有关要删除的行的信息,并将其从为表格提供数据的集合中删除。
例如,对于
Set
集合,代码将如下所示 (HashSet<Players>
):public void deleteRowHashSet(Players player) { dataHashSet.remove(player); }
对于
Map<String, Players>
,代码将如下所示:public void deleteRowHashMap(Object key) { dataHashMap.remove(String.valueOf(key)); }
-
除了包含数据的列之外,在表格中添加一个名为 删除 的新列。每一行都可以是一个指向
delete
XXX 方法的链接。例如,我们可以从
Set
(HashSet<Players>
) 中删除一个值,如下面的代码所示:<h:dataTable value="#{playersBean.dataHashSet}" var="t"> ... <h:column> <f:facet name="header">Delete</f:facet> <h:commandLink value="Delete" action="#{playersBean.deleteRowHashSet(t)}" /> </h:column> ... </h:dataTable>
并且从
Map<String, Players>
,如下所示:<h:dataTable value="#{playersBean.dataHashMap.entrySet()}" var="t"> ... <h:column> <f:facet name="header">Delete</f:facet> <h:commandLink value="Delete" action="#{playersBean.deleteRowHashMap(t.key)}" /> </h:column> ... </h:dataTable>
在下面的屏幕截图,你可以看到一个可能的输出:
完整的示例可以在本章的代码包中找到,命名为 ch6_4
。
编辑/更新表格行
编辑/更新表格行最方便的方法之一是使用一个特殊属性来跟踪行编辑状态。这个属性可以命名为 edited
,它应该是 boolean
类型(默认 false
)。在 POJO 类中定义它,如下面的代码所示:
public class Players {
...
private boolean edited;
...
public boolean isEdited() {
return edited;
}
public void setEdited(boolean edited) {
this.edited = edited;
}
}
注意
如果你的 POJO 类是一个实体类,那么使用 @Transient
注解或 transient
修饰符将这个新属性定义为 transient
。这个注解将告诉 JPA 这个属性不参与持久化,并且其值永远不会存储在数据库中。
接下来,将一个 编辑 链接分配给每一行。使用 rendered
属性,你可以通过简单的 EL 条件轻松显示/隐藏链接;最初,链接对每一行都是可见的。例如,看看以下用例:
-
对于
Set
集合,代码如下:... <h:column> <f:facet name="header">Edit</f:facet> <h:commandLink value="Edit" action="#{playersBean.editRowHashSet(t)}" rendered="#{not t.edited}" /> </h:column> ...
-
对于
Map
集合,代码如下:... <h:column> <f:facet name="header">Edit</f:facet> <h:commandLink value="Edit" action="#{playersBean.editRowHashMap(t.value)}" rendered="#{not t.value.edited}"/> </h:column> ...
当链接被点击时,edited
属性将从 false
切换到 true
,表格将按如下方式重新渲染:
-
对于
Set
集合,editRowHashSet
方法的代码如下:public void editRowHashSet(Players player) { player.setEdited(true); }
-
对于
Map
集合,editRowHashSet
方法的代码如下:public void editRowHashMap(Players player) { player.setEdited(true); }
这意味着链接不再渲染,用户应该能够编辑该表格行。你需要使用 rendered
属性在 <h:outputText>
标签(用于显示数据,当 edited
属性为 false
时可见)和 <h:inputText>
标签(用于收集数据,当 edited
属性为 true
时可见)之间切换。再次使用 rendered
属性将起到作用,如下所示:
-
对于
Set
集合,代码修改如下:... <h:column> <f:facet name="header">Name</f:facet> <h:inputText value="#{t.player}" rendered="#{t.edited}" /> <h:outputText value="#{t.player}" rendered="#{not t.edited}" /> </h:column> ...
-
对于
Map
集合,代码修改如下:... <h:column> <f:facet name="header">Name</f:facet> <h:inputText value="#{t.value.player}" rendered="#{t.value.edited}" /> <h:outputText value="#{t.value.player}" rendered="#{not t.value.edited}" /> </h:column> ...
最后,你需要一个按钮来保存更改;此按钮将edited
属性重置为false
,为更多编辑准备表格,如下所示:
-
对于
Set
集合,按钮的代码如下:<h:commandButton value="Save Hash Set Changes" action="#{playersBean.saveHashSet()}" />
-
对于
Map
集合,按钮的代码如下:<h:commandButton value="Save Hash Map Changes" action="#{playersBean.saveHashMap()}" />
这是一个直接的动作,如以下要点所示——输入文本框中插入的值会自动保存到集合中:
-
对于
Set
集合,代码如下:public void saveHashSet() { for (Players player : dataHashSet) { player.setEdited(false); } }
-
对于
Map
集合,代码如下:public void saveHashMap() { for (Map.Entry pairs : dataHashMap.entrySet()) { ((Players) pairs.getValue()).setEdited(false); } }
完成!在以下截图中,您可以看到可能的输出:
完整的示例可以在本章的代码包中找到,命名为ch6_5
。
添加新行
添加新行也是一个简单的任务。首先,你需要提供一个表行内容的表单,如下面的截图所示:
可以使用以下代码轻松实现此表单:
...
<h:inputText value="#{playersBean.player}"/>
<h:inputText value="#{playersBean.age}"/>
<h:inputText value="#{playersBean.birthplace}"/>
<h:inputText value="#{playersBean.residence}"/>
<h:inputText value="#{playersBean.height}"/>
<h:inputText value="#{playersBean.weight}"/>
<h:inputText value="#{playersBean.coach}"/>
<h:inputText value="#{playersBean.born}">
<f:convertDateTime pattern="dd.MM.yyyy" />
</h:inputText>
<h:inputText value="#{playersBean.ranking}"/>
<h:commandButton value="Add Player" action="#{playersBean.addNewPlayer()}"/>
...
标有添加玩家的按钮将调用一个托管 Bean 方法,创建一个新的Players
实例并将其添加到为表格提供数据的集合中,如下面的代码所示:
public void addNewPlayer() {
Players new_player = new Players(ranking, player, age, birthplace, residence, height, weight, coach, born);
//adding in a Set
dataHashSet.add(new_player);
//adding in a Map
dataHashMap.put(String.valueOf(ranking), new_player);
}
在以下截图中,您可以看到从前面截图中的数据中添加的新行:
完整的示例可以在本章的代码包中找到,命名为ch6_6_1
。
一种更优雅的方法是直接在表格中添加行并消除此用户表单。可以通过以下简单步骤轻松实现:
-
使用链接集合(例如,使用
LinkedHashSet
代替HashSet
或LinkedHashMap
代替HashMap
)。表格通过迭代相应的集合来填充,但某些集合,如HashSet
或HashMap
,不提供迭代顺序,这意味着迭代顺序是不可预测的。这很重要,因为我们想在表格末尾添加一行,但使用不可预测的迭代顺序很难实现。但是,链接集合可以解决这个问题,如下面的代码所示:LinkedHashSet<Players> dataHashSet = new LinkedHashSet<>(); LinkedHashMap<String, Players> dataHashMap = new LinkedHashMap<>();
-
通过在相应的集合中创建新项并使用
Set
和Map
集合激活可编辑模式来添加新行:-
以下是一个链接
Set
集合的代码:... <h:commandButton value="Add New Row" action="#{playersBean.addNewRowInSet()}" /> ... public void addNewRowInSet() { Players new_player = new Players(); new_player.setEdited(true); dataHashSet.add(new_player); }
-
以下是一个链接
Map
集合的代码:... <h:commandButton value="Add New Row" action="#{playersBean.addNewRowInMap()}" /> ... public void addNewRowInMap() { Players new_player = new Players(); new_player.setEdited(true); dataHashMap.put(String.valueOf(dataHashMap.size() + 1), new_player); }
-
查看以下截图以查看可能的输出:
完整的示例可以在本章的代码包中找到,命名为ch6_6_2
。
显示行号
默认情况下,JSF 不提供显示行号的方法。但正如你在编辑/更新表行部分的输出截图中所见,有一个名为No的列显示行号。你可以通过至少两种方式获得此列。最简单的解决方案是将表绑定到当前视图,如下面的代码所示:
<h:dataTable value="..." binding="#{table}" var="t">
<h:column>
<f:facet name="header">No</f:facet>
#{table.rowIndex+1}.
</h:column>
...
另一种方法是使用DataModel
类来获取它,该类具有getRowIndex
方法来返回当前选中的行号。为了做到这一点,你需要将集合包装在DataModel
类中。
命名为ch6_7
的示例包含了这个任务的第一个方法。
选择单行
实现此类任务的最简单方法是为表中的每一行提供一个按钮。当按钮被点击时,它可以传递所选行,如下面的代码所示:
<h:dataTable value="#{playersBean.dataHashSet}" var="t" border="1">
<h:column>
<f:facet name="header">Select</f:facet>
<h:commandButton value="#" action="#{playersBean.showSelectedPlayer(t)}"/>
</h:column>
...
由于showSelectedPlayer
方法接收所选行,它可以进一步处理它,没有其他要求。完整的示例可在本章的代码包中找到,命名为ch6_8_1
。
通常来说,从一堆项目中选择一个项目是单选按钮组的工作。在 JSF 表中,项目是行,为每一行添加一个单选按钮将导致如下截图所示的列:
然而,在<h:column>
标签中使用<h:selectOneRadio>
标签添加单选按钮并不像预期的那样表现。单选按钮的主要功能不起作用;选择一个单选按钮不会取消选择组中的其他单选按钮。现在它更像是一组复选框。你可以通过实现一个取消选择机制来修复这个问题,通过 JavaScript。此外,在这个阶段,你可以设置一个 JSF 隐藏字段,其值为所选行的值。例如,如果表是通过Map
填充的,你将使用以下代码:
<script type="text/javascript">
//<![CDATA[
function deselectRadios(id, val) {
var f = document.getElementById("hashMapFormId");
for (var i = 0; i < f.length; i++)
{
var e = f.elements[i];
var eid = e.id;
if (eid.indexOf("radiosId") !== -1) {
if (eid.indexOf(id) === -1) {
e.checked = false;
} else {
e.checked = true;
document.getElementById("hashMapFormId:selectedRowId").value = val;
}
}
}
}
//]]>
</script>
首先,你需要通过 ID 找到包含单选按钮的表单。然后,遍历表单的子元素,并通过其 ID 的固定部分识别每个单选按钮。只检查用户选择的单选按钮,并取消选择其余的单选按钮。接下来,使用所选行的值填充一个隐藏字段。所选单选按钮的 ID 和行值作为参数提供,如下(在这种情况下,表是从Map
中填充的):
<h:dataTable value="#{playersBean.dataHashMap.entrySet()}" var="t">
<h:column>
<f:facet name="header">Select</f:facet>
<h:selectOneRadio id="radiosId"
onclick="deselectRadios(this.id, '#{t.key}');">
<f:selectItem itemValue="null"/>
</h:selectOneRadio>
</h:column>
...
除了用于存储所选行信息的隐藏字段外,你还需要一个标签为显示哈希映射选择的按钮,如下面的代码所示:
<h:inputHidden id="selectedRowId" value="#{playersBean.selectedPlayerKey}"/>
<h:commandButton value="Show Hash Map Selection" action="#{playersBean.showSelectedPlayer()}" />
以下showSelectedPlayer
方法已准备好处理所选行:
public void showSelectedPlayer() {
Players player = dataHashMap.get(selectedPlayerKey);
if (player != null) {
logger.log(Level.INFO, "Selected player:{0}", player.getPlayer());
} else {
logger.log(Level.INFO, "No player selected!");
}
}
完成!完整的示例可在本章的代码包中找到,命名为ch6_8_2
。
如果你认为使用隐藏字段不是一个非常优雅的方法,那么你可以通过使用<h:selectOneRadio>
标签的valueChangeListener
属性来替换其角色。
在本章的代码包中,你可以找到一个使用名为ch6_8_3
的valueChangeListener
属性的示例。
选择多行
多选通常通过复选框组来实现。实现多选的最方便方法之一是使用一个特殊属性来跟踪行选择状态。这个属性可以命名为selected
,它应该是boolean
类型(默认false
)。你可以在 POJO 类中如下定义它:
public class Players {
...
private boolean selected;
...
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
...
注意
如果你的 POJO 类是一个实体类,那么请将这个新属性定义为 transient,使用@Transient
注解或 transient 修饰符。这个注解将告诉 JPA 这个属性不参与持久化,其值永远不会存储在数据库中。
接下来,为每一行分配一个复选框(<h:selectBooleanCheckbox>
)。使用value
属性和selected
属性,你可以轻松跟踪选择状态,如下所示:
<h:dataTable value="#{playersBean.dataHashSet}" var="t">
<h:column>
<f:facet name="header">Select</f:facet>
<h:selectBooleanCheckbox value="#{t.selected}" />
</h:column>
...
因此,<h:selectBooleanCheckbox>
标签将为我们完成艰苦的工作(我们只是利用其自然行为),因此,你所需要的只是一个标记为显示选中玩家的按钮,如下所示:
<h:commandButton value="Show Selected Players" action="#{playersBean.showSelectedPlayers()}" />
showSelectedPlayers
方法有一个简单的任务。它可以遍历集合并检查每个项目的selected
属性状态;这是一个重置选中项的好机会。例如,你可以将选中的项提取到一个单独的列表中,如下所示:
...
HashSet<Players> dataHashSet = new HashSet<>();
List<Players> selectedPlayers = new ArrayList<>();
...
public void showSelectedPlayers() {
selectedPlayers.clear();
for (Players player : dataHashSet) {
if(player.isSelected()){
logger.log(Level.INFO, "Selected player: {0}", layer.getPlayer());
selectedPlayers.add(player);
player.setSelected(false);
}
}
//the selected players were extracted in a List ...
}
完整的示例包含在本章的代码包中,命名为ch6_8_4
。
如果你不想使用额外的属性,如selected
,你可以使用一个Map <String, Boolean>
映射。代码相当简单;因此,快速查看完整的代码ch6_8_5
将立即阐明问题。
嵌套表格
很可能你不需要在表格内部显示另一个表格,但有时这个解决方案在获得清晰的数据展示方面很有用。例如,嵌套集合可以表示为嵌套表格,如下所示:
HashMap<Players, HashSet<Trophies>> dataHashMap = new HashMap<>();
在这里,玩家以HashMap
作为键存储,每个玩家都有一个奖杯集合(HashSet
)。每个HashSet
值是HashMap
中的一个值。因此,你需要显示玩家的表格;然而,你还需要显示每个玩家的奖杯。这可以通过以下代码实现:
<h:dataTable value="#{playersBean.dataHashMap.entrySet()}" var="t">
<h:column>
<f:facet name="header">Ranking</f:facet>
#{t.key.ranking}
</h:column>
<h:column>
<f:facet name="header">Name</f:facet>
#{t.key.player}
</h:column>
...
<h:column>
<f:facet name="header">Trophies 2013</f:facet>
<h:dataTable value="#{t.value}" var="q" border="1">
<h:column>
#{q.trophy}
</h:column>
</h:dataTable>
</h:column>
</h:dataTable>
上述代码的可能输出可以在以下屏幕截图中看到:
完整的应用程序命名为ch6_9
,并包含在本章的代码包中。
分页表格
当你需要显示大型表格(包含许多行)时,实现分页机制可能很有用。它有许多优点,例如外观精美、数据展示清晰、节省网页空间和延迟加载。
在此类表格的标准版本中,我们应该能够导航到第一页、最后一页、下一页、上一页,在某些表格中,还可以选择每页显示的行数。
当您将表格绑定到其后端 Bean 时,您可以访问三个方便的属性,如下所示:
-
first
:此属性表示当前表格页面中显示的第一行行号(它从默认值0
开始)。此属性的值可以使用<h:dataTable>
标签的first
属性指定。在 JSF API 中,可以通过HtmlDataTable.getFirst
和HtmlDataTable.setFirst
方法访问。 -
rows
:此属性表示从first
开始显示的单页中的行数。此属性的值可以使用<h:dataTable>
标签的rows
属性指定。在 JSF API 中,可以通过HtmlDataTable.getRows
和HtmlDataTable.setRows
方法访问。 -
rowCount
:此属性表示从所有页面中开始于行 0 的总行数。此属性没有对应的属性。在 JSF API 中,可以通过HtmlDataTable.getRowCount
方法访问。可以通过数据模型设置行数,如后文所示。默认值由 JSF 确定。
在以下截图中,可以详细看到这些属性:
前面的信息对于实现分页机制非常有用。首先,我们绑定表格,并设置第一行行号和每页行数,如下所示:
<h:dataTable value="#{playersBean.dataHashSet}" binding="#{playersBean.table}" rows="#{playersBean.rowsOnPage}" first="0" var="t">
...
基于一些算术和 EL 条件支持,我们可以得出以下结论:
-
第一行行号、每页行数和总行数可以通过以下代码访问:
<b>FIRST:</b> #{playersBean.table.first} <b>ROWS:</b> #{playersBean.table.rows} <b>ROW COUNT:</b> #{playersBean.table.rowCount}
-
使用以下代码导航到第一页:
public void goToFirstPage() { table.setFirst(0); }
可以通过 EL 条件禁用实现此导航的按钮,如下所示:
<h:commandButton value="First Page" action="#{playersBean.goToFirstPage()}" disabled="#{playersBean.table.first eq 0}" />
-
使用以下代码导航到下一页:
public void goToNextPage() { table.setFirst(table.getFirst() + table.getRows()); }
可以通过 EL 条件禁用实现此导航的按钮,如下所示:
<h:commandButton value="Next Page" action="#{playersBean.goToNextPage()}" disabled="#{playersBean.table.first + playersBean.table.rows ge playersBean.table.rowCount}" />
-
使用以下代码导航到上一页:
public void goToPreviousPage() { table.setFirst(table.getFirst() - table.getRows()); }
可以通过 EL 条件禁用实现此导航的按钮,如下所示:
<h:commandButton value="Previous Page" action="#{playersBean.goToPreviousPage()}" disabled="#{playersBean.table.first eq 0}" />
-
使用以下代码导航到最后页:
public void goToLastPage() { int totalRows = table.getRowCount(); int displayRows = table.getRows(); int full = totalRows / displayRows; int modulo = totalRows % displayRows; if (modulo > 0) { table.setFirst(full * displayRows); } else { table.setFirst((full - 1) * displayRows); } }
可以通过 EL 条件禁用实现此导航的按钮,如下所示:
<h:commandButton value="Last Page" action="#{playersBean.goToLastPage()}" disabled="#{playersBean.table.first + playersBean.table.rows ge playersBean.table.rowCount}" />
-
使用以下代码显示当前页和总页数的信息:
<h:outputText value="#{(playersBean.table.first div playersBean.table.rows) + 1}"> <f:converter converterId="javax.faces.Integer"/> </h:outputText> of <h:outputText value="#{playersBean.table.rowCount mod playersBean.table.rows eq 0 ? (playersBean.table.rowCount div playersBean.table.rows) : ((playersBean.table.rowCount div playersBean.table.rows) + 1)-(((playersBean.table.rowCount div playersBean.table.rows) + 1) mod 1)}"> <f:converter converterId="javax.faces.Integer"/> </h:outputText>
在示例应用程序(见应用程序ch6_10_1
)中将所有这些代码块合并,将得到如下截图:
这里最大的问题是,即使数据以分页的形式显示,它们仍然以批量形式加载到内存中。在这种情况下,分页只是集合的切片,它只有视觉效果。实际上,分页是延迟加载的效果,它代表了一种从数据库中查询部分数据的技术(而不是在内存中切片数据,直接从数据库中切片)。数据库中有许多种查询方式,但在 Java Web/企业应用程序中,EJB/JPA 是最常用的。EJB 和 JPA 是大型技术,这里不能全部涵盖,但通过一些假设,理解即将到来的示例将会非常容易。
注意
如果你认为 EJB/JPA 不是好的选择,你应该考虑这样一个事实,即<h:dataTable>
标签也支持java.sql.ResultSet
、javax.servlet.jsp.jstl.Result
和javax.sql.CachedRowSet
。因此,对于测试,你也可以使用普通的 JDBC。
与此同时,你将使用一个与名为PLAYERS
的表绑定的Players
JPA 实体,而不是Players
POJO 类。这个表包含应在 JSF 表格中显示的数据,它是在 Apache Derby RDBMS 的APP
数据库中创建的(如果你有 NetBeans 8.0 和 GlassFish 4.0,那么这个 RDBMS 和APP
数据库是开箱即用的)。想法是查询这个表以获取从first
到first
+ rows
的行,这正好是每页显示的行数。这可以通过 JPA 使用查询的setFirstResult
和setMaxResults
方法轻松实现(loadPlayersAction
方法定义在一个名为PlayersSessionBean
的 EJB 组件中),如下面的代码所示:
public HashSet<Players> loadPlayersAction(int first, int max) {
Query query = em.createNamedQuery("Players.findAll");
query.setFirstResult(first);
query.setMaxResults(max);
return new HashSet(query.getResultList());
}
因此,传递正确的first
和max
参数将返回所需的行!
但如果我们知道总行数,分页就会工作,因为没有这个信息,我们无法计算页数,或者最后一页,等等。在 JPA 中,我们可以通过以下代码轻松实现(countPlayersAction
方法定义在一个名为PlayersSessionBean
的 EJB 组件中):
public int countPlayersAction() {
Query query = em.createNamedQuery("Players.countAll");
return ((Long)query.getSingleResult()).intValue();
}
知道总行数(实际上并没有从数据库中提取数据)是很好的,但我们需要告诉 JSF 那个数字!由于HtmlDataTable
没有提供setRowCount
方法,我们必须考虑另一种方法。一个解决方案是扩展DataModel
类(或其子类之一)并显式提供行数;由于我们使用HashSet
,我们可以扩展 JSF 2.2 的CollectionDataModel
类,如下所示:
public class PlayersDataModel extends CollectionDataModel {
private int rowIndex = -1;
private int allRowsCount;
private int pageSize;
private HashSet hashSet;
public PlayersDataModel() {}
public PlayersDataModel(HashSet hashSet, int allRowsCount, int pageSize)
{
this.hashSet = hashSet;
this.allRowsCount = allRowsCount;
this.pageSize = pageSize;
}
@Override
public boolean isRowAvailable() {
if (hashSet == null) {
return false;
}
int c_rowIndex = getRowIndex();
if (c_rowIndex >= 0 && c_rowIndex < hashSet.size()) {
return true;
} else {
return false;
}
}
@Override
public int getRowCount() {
return allRowsCount;
}
@Override
public Object getRowData() {
if (hashSet == null) {
return null;
} else if (!isRowAvailable()) {
throw new IllegalArgumentException();
} else {
int dataIndex = getRowIndex();
Object[] arrayView = hashSet.toArray();
return arrayView[dataIndex];
}
}
@Override
public int getRowIndex() {
return (rowIndex % pageSize);
}
@Override
public void setRowIndex(int rowIndex) {
this.rowIndex = rowIndex;
}
@Override
public Object getWrappedData() {
return hashSet;
}
@Override
public void setWrappedData(Object hashSet) {
this.hashSet = (HashSet) hashSet;
}
}
因此,创建一个PlayersDataModel
类可以通过以下方式完成:
...
@Inject
private PlayersSessionBean playersSessionBean;
private int rowsOnPage;
private int allRowsCount = 0;
...
@PostConstruct
public void initHashSet() {
rowsOnPage = 4; //any constant in [1, rowCount]
allRowsCount = playersSessionBean.countPlayersAction();
lazyDataLoading(0);
}
...
private void lazyDataLoading(int first) {
HashSet<Players> dataHashSet = playersSessionBean.loadPlayersAction(first, rowsOnPage);
playersDataModel = new PlayersDataModel(dataHashSet, allRowsCount, rowsOnPage);
}
最后,每次在表格中检测到页面导航时,我们只需要调用以下方法:
lazyDataLoading(table.getFirst());
完整的示例可以在本章的代码包中找到,名称为ch6_10_2
。
使用 JSF API 生成表格
JSF 表格也可以程序化生成。JSF API 提供了全面的支持来完成此类任务。首先,你需要准备生成表格的位置,如下所示:
<h:body>
<h:form id="tableForm">
<h:panelGrid id="myTable">
</h:panelGrid>
<h:commandButton value="Add Table" action="#{playersBean.addTable()}"/>
</h:form>
</h:body>
简单来说:当点击标有添加表格的按钮时,生成的表格应该附加到 ID 为myTable
的<h:panelGrid>
标签中。
在程序化创建 JSF 表格之前,你需要知道如何创建表格、标题/页脚、列等。以下是一个简要概述——代码是自我解释的,因为 JSF 提供了非常直观的方法:
-
让我们创建最简单的表格
<h:dataTable value="..." var="t" border="1">
,如下面的代码所示:public HtmlDataTable createTable(String exp, Class<?> cls) { HtmlDataTable table = new HtmlDataTable(); table.setValueExpression("value", createValueExpression(exp, cls)); table.setVar("t"); table.setBorder(1); return table; }
-
现在,我们将创建一个带有标题、页脚和可能的转换器的列,如下所示:
public HtmlColumn createColumn(HtmlDataTable table, String header_name, String footer_name, String exp, Class<?> cls, Class<?> converter) { HtmlColumn column = new HtmlColumn(); table.getChildren().add(column); if (header_name != null) { HtmlOutputText header = new HtmlOutputText(); header.setValue(header_name); column.setHeader(header); } if (footer_name != null) { HtmlOutputText footer = new HtmlOutputText(); footer.setValue(footer_name); column.setFooter(footer); } HtmlOutputText output = new HtmlOutputText(); output.setValueExpression("value", createValueExpression(exp, cls)); column.getChildren().add(output); if (converter != null) { if (converter.getGenericInterfaces()[0].equals(Converter.class)) { if (converter.equals(DateTimeConverter.class)) { DateTimeConverter dateTimeConverter = new DateTimeConverter(); dateTimeConverter.setPattern("dd.MM.yyyy"); output.setConverter(dateTimeConverter); } //more converters ... } else { //the passed class is not a converter! } } return column; }
-
现在,使用以下代码将表格附加到 DOM 中(为了做到这一点,你需要找到所需的父组件):
public void attachTable(HtmlDataTable table, String parent_id) throws NullPointerException { UIComponent component = findComponent(parent_id); if (component != null) { component.getChildren().clear(); component.getChildren().add(table); } else { throw new NullPointerException(); } }
findComponent
方法使用 JSF 的visit
方法,这对于遍历组件树非常有用,如下面的代码所示:private UIComponent findComponent(final String id) { FacesContext context = FacesContext.getCurrentInstance(); UIViewRoot root = context.getViewRoot(); final UIComponent[] found = new UIComponent[1]; root.visitTree(new FullVisitContext(context), new VisitCallback() { @Override public VisitResult visit(VisitContext context, UIComponent component) { if (component.getId().equals(id)) { found[0] = component; return VisitResult.COMPLETE; } return VisitResult.ACCEPT; } }); return found[0]; }
注意
在 Mojarra 中,
FullVisitContext
方法来自com.sun.faces.component.visit
包。在 MyFaces 中,这个类来自org.apache.myfaces.test.mock.visit
包。这两个实现都扩展了javax.faces.component.visit.VisitContext
。 -
然后添加必要的表达式,如下面的代码所示(你曾在第二章中看到另一个例子,JSF 中的通信):
private ValueExpression createValueExpression(String exp, Class<?> cls) { FacesContext facesContext = FacesContext.getCurrentInstance(); ELContext elContext = facesContext.getELContext(); return facesContext.getApplication().getExpressionFactory().createValueExpression(elContext, exp, cls); }
-
最后,将这些方法合并到一个辅助类
TableHelper
中。记得那个标有添加表格的按钮吗?当点击该按钮时,会调用
addTable
方法。此方法利用TableHelper
类来程序化创建表格,如下面的代码所示:public void addTable() { TableHelper tableHelper = new TableHelper(); HtmlDataTable tableHashSet = tableHelper.createTable("#{playersBean.dataHashSet}", HashSet.class); tableHelper.createColumn(tableHashSet, "Ranking", null, "#{t.ranking}", Integer.class, null); tableHelper.createColumn(tableHashSet, "Name", null, "#{t.player}", String.class, null); tableHelper.createColumn(tableHashSet, "Age", null, "#{t.age}", Byte.class, null); tableHelper.createColumn(tableHashSet, "Birthplace", null, "#{t.birthplace}", String.class, null); tableHelper.createColumn(tableHashSet, "Residence", null, "#{t.residence}", String.class, null); tableHelper.createColumn(tableHashSet, "Height (cm)", null, "#{t.height}", Short.class, null); tableHelper.createColumn(tableHashSet, "Weight (kg)", null, "#{t.weight}", Byte.class, null); tableHelper.createColumn(tableHashSet, "Coach", null, "#{t.coach}", String.class, null); tableHelper.createColumn(tableHashSet, "Born", null, "#{t.born}", java.util.Date.class, DateTimeConverter.class); tableHelper.attachTable(tableHashSet, "myTable"); }
完成!完整的应用程序可在本章的代码包中找到,命名为ch6_11
。
程序化生成的表格非常适合生成具有可变列数或动态列的表格。假设我们有两个 JPA 实体,Players
和Trophies
。第一个实体应该生成一个包含九列的表格,而Trophies
应该生成一个包含三列的表格。此外,列名(标题)不同。这听起来可能很复杂,但实际上比你想象的要简单。
想象一下,每个表格都由一个 JPA 实体映射,这意味着我们可以通过指定实体名称来编写特定的查询。此外,每个实体都可以通过 Java 的反射机制来提取字段名称(我们专注于private
字段),这为我们提供了列标题。(如果你使用@Column(name="alias_name")
来更改列名,那么这个过程将稍微复杂一些,需要反射别名。)因此,我们可以使用以下代码(包名是固定的):
@Inject
//this is the EJB component that queries the database
private QueryBean queryBean;
HashSet<Object> dataHashSet = new HashSet<>();
...
public void addTable(String selectedTable) {
try {
dataHashSet.clear();
dataHashSet = queryBean.populateData(selectedTable);
String tableToQuery = "book.ejbs." + selectedTable;
Class tableClass = Class.forName(tableToQuery);
Field[] privateFields = tableClass.getDeclaredFields();
TableHelper tableHelper = new TableHelper();
HtmlDataTable tableHashSet = tableHelper.createTable("#{playersBean.dataHashSet}", HashSet.class);
for (int i = 0; i < privateFields.length; i++) {
String privateField = privateFields[i].getName();
if ((!privateField.startsWith("_")) && (!privateField.equals("serialVersionUID"))) {
tableHelper.createColumn(tableHashSet, privateField, null, "#{t."+privateField+"}",
privateFields[i].getType(), null);
}
}
tableHelper.attachTable(tableHashSet, "myTable");
} catch (ClassNotFoundException ex) {
Logger.getLogger(PlayersBean.class.getName()).log(Level.SEVERE, null, ex);
}
因此,只要我们将表名(实体名)传递给此方法,它就会返回相应的数据。对于完整的示例,请查看本章代码包中名为ch6_12
的应用程序。
过滤表格
在表格中,过滤数据是一个非常实用的功能。它允许用户只看到符合一定规则(标准)的数据集;最常见的是按列(多列)过滤。例如,用户可能需要查看所有 26 岁以下的所有球员,这是在标记为年龄的列中应用的过滤。
基本上,过滤器可以只有视觉效果,而不会修改过滤后的数据(使用一些 CSS、JS 代码,或在单独的集合中复制过滤结果并显示该集合),或者通过移除初始集合中的不必要的项目(当过滤器移除时需要恢复其内容)。
在 JSF 中,我们可以通过玩一些 CSS 代码来编写一个不错的过滤器,这些代码可以用来隐藏/显示表格的行;这并不是在生产环境中推荐的方法,因为所有数据仍然在源页面中可用,但它可能在不需要任何花哨功能时很有用。想法是隐藏所有不符合过滤器标准的表格行,为此,我们可以利用<h:dataTable>
标签的rowClasses
属性。此属性的值由逗号分隔的 CSS 类字符串表示;JSF 遍历 CSS 类,并按顺序反复应用于行。
考虑以下两个 CSS 类:
.rowshow
{
display:visible;
}
.rowhide
{
display:none;
}
现在,过滤器可以使用rowshow
CSS 类来显示包含有效数据的行,以及使用rowhide
CSS 类来隐藏其余的行。例如,遍历一个包含五个元素的集合可以揭示以下字符串形式的 CSS 类:
rowshow, rowhide, rowshow, rowhide, rowhide
因此,只有第一行和第三行将是可见的。
让我们看看编写此类过滤器所涉及的步骤:
-
为每列添加过滤器选择的一个方便方法是使用
<h:selectOneMenu>
标签。例如,我们在年龄列中添加一个过滤器选择,如下所示:... <h:column> <f:facet name="header"> Age<br/> <h:selectOneMenu value="#{playersBean.criteria}"> <f:selectItem itemValue="all" itemLabel="all" /> <f:selectItem itemValue="<26" itemLabel="<26" /> <f:selectItem itemValue=">=26" itemLabel=">=26" /> </h:selectOneMenu> <h:commandButton value="Go!" action="#{playersBean.addTableFilter()}"/> </f:facet> <h:outputText value="#{t.age}"/> </h:column> ...
-
当点击标记为Go!的按钮时,会调用
addTableFilter
方法。它检查criteria
属性的值,如果值等于<26
或>=26
,则遍历表格行并构建相应的 CSS 类字符串。否则,如果criteria
属性等于all
,则移除过滤器,如下面的代码所示:public void addTableFilter() { if (!criteria.equals("all")) { String c = ""; for (int i = 0; i < table.getRowCount(); i++) { table.setRowIndex(i); Players player = (Players) table.getRowData(); if (criteria.equals("<26")) { if (player.getAge() >= 26) { c = c + "rowhide,"; } else { c = c + "rowshow,"; } } if (criteria.equals(">=26")) { if (player.getAge() < 26) { c = c + "rowhide,"; } else { c = c + "rowshow,"; } } } String css = "rowshow"; if (!c.isEmpty()) { css = c.substring(0, c.length() - 1); } rowsOnPage = table.getRowCount(); table.setRowClasses(css); table.setFirst(0); } else { removeTableFilter(); } }
-
以下
removeTableFilter
方法将恢复 CSS 类;因此,所有数据将再次可见:public void removeTableFilter() { String css = "rowshow"; rowsOnPage = 4; //any constant in [1, rowCount] table.setRowClasses(css); table.setFirst(0); }
对于完整的示例,请查看本章代码包中名为ch6_13_1
的应用程序。
重要的是要注意,当应用过滤器时,每页的行数会发生变化。实际上,当过滤器结果显示时,每页的行数等于表行数,而当过滤器被移除时,它们可以取从 1 到行数的任何值。结论是,过滤后的数据在无分页的表格中显示。
在某些情况下,例如按年龄过滤,您可以在生成 CSS 类字符串之前应用排序。这将帮助您显示过滤结果,而不会影响数据,并且可以提供分页。一个完整的示例可以在本章代码包中找到,名称为ch6_13_2
。
您可以通过从初始集合中移除不符合过滤条件的项目来获得相同的结果。例如,请注意,在应用过滤器之前,您需要恢复集合的初始数据——initHashSet
方法可以完成这个任务:
public void addTableFilter() {
initHashSet();
Iterator<Players> i = dataHashSet.iterator();
while (i.hasNext()) {
Players player = i.next();
if (criteria.equals("<26")) {
if (player.getAge() >= 26) {
i.remove();
}
}
if (criteria.equals(">=26")) {
if (player.getAge() < 26) {
i.remove();
}
}
}
table.setFirst(0);
}
如果您想应用一系列过滤器,那么在进入链之前恢复数据。一个完整的示例可以在本章代码包中找到,名称为ch6_13_3
。
由于为表格提供数据的集合通常是从数据库中填充的,因此您可以直接在数据库上应用过滤器。一个常见的例子是具有懒加载机制的表格;由于您在内存中只有数据的一部分,您需要在数据库上应用过滤器,而不是过滤填充表格的集合。这意味着过滤过程是通过 SQL 查询完成的。例如,我们的过滤器可以通过以下步骤通过 SQL 查询建模(此示例基于本章前面介绍的懒加载应用):
-
您将过滤条件传递给 EJB 组件(
copy_criteria
充当标志——您不希望在用户使用相同的过滤器通过表格页面导航时每次都计算行数),如下面的代码所示:@Inject private PlayersSessionBean playersSessionBean; private PlayersDataModel playersDataModel; private String criteria = "all"; private String copy_criteria = "none"; private int allRowsCount = 0; ... private void lazyDataLoading(int first) { if (!copy_criteria.equals(criteria)) { allRowsCount = playersSessionBean.countPlayersAction(criteria); copy_criteria = criteria; } HashSet<Players> dataHashSet = playersSessionBean.loadPlayersAction(first, rowsOnPage, criteria); playersDataModel = new PlayersDataModel(dataHashSet, allRowsCount, rowsOnPage); }
-
按如下方式计算过滤器返回的行数:
public int countPlayersAction(String criteria) { if (criteria.equals("all")) { Query query = em.createNamedQuery("Players.countAll"); return ((Long) query.getSingleResult()).intValue(); } if (criteria.equals("<26")) { Query query = em.createQuery("SELECT COUNT(p) FROM Players p WHERE p.age < 26"); return ((Long) query.getSingleResult()).intValue(); } if (criteria.equals(">=26")) { Query query = em.createQuery("SELECT COUNT(p) FROM Players p WHERE p.age >= 26"); return ((Long) query.getSingleResult()).intValue(); } return 0; }
-
最后,通过以下方式应用过滤条件:
public HashSet<Players> loadPlayersAction(int first, int max, String criteria) { if (criteria.equals("all")) { Query query = em.createNamedQuery("Players.findAll"); query.setFirstResult(first); query.setMaxResults(max); return new HashSet(query.getResultList()); } if (criteria.equals("<26")) { Query query = em.createQuery("SELECT p FROM Players p WHERE p.age < 26"); query.setFirstResult(first); query.setMaxResults(max); return new HashSet(query.getResultList()); } if (criteria.equals(">=26")) { Query query = em.createQuery("SELECT p FROM Players p WHERE p.age >= 26"); query.setFirstResult(first); query.setMaxResults(max); return new HashSet(query.getResultList()); } return null; }
完成!完整的示例可在本章代码包中找到,名称为ch6_13_4
。
表格样式
几乎所有的 JSF UI 组件都支持style
和styleClass
属性,用于使用 CSS 创建自定义设计。但是<h:dataTable>
标签支持如captionClass
、captionStyle
、columnClasses
、rowClasses
、headerClass
和footerClass
等属性。因此,我们应该没有问题为表格的每个部分(标题、页脚、标题等)添加 CSS 样式。显然,可以构建很多示例,但让我们看看三个最令人印象深刻且常用的示例。
使用 rowclasses 属性交替行颜色
rowClasses
属性用于指示由逗号分隔的 CSS 类字符串。该字符串由 JSF 解析,并将样式依次且重复地应用到行上。例如,你可以用一种颜色为偶数行着色,用另一种颜色为奇数行着色,如下所示:
<h:dataTable value="#{playersBean.data}" rowClasses="even, odd" var="t">
...
在这里,even
和 odd
是以下 CSS 类:
.odd {
background-color: gray;
}
.even{
background-color: darkgray;
}
以下截图显示了可能的输出:
注意
你可以通过使用 columnClasses
属性而不是 rowClasses
属性来获得相同的效果。
完整示例在本书的代码包中命名为 ch6_14_1
。
鼠标悬停时高亮显示行
鼠标悬停时高亮显示行是一个可以通过一小段 JavaScript 实现的不错的效果。思路是设置 onmouseover
和 onmouseout
属性,如下面的自解释代码所示:
...
<script type="text/javascript">
//<![CDATA[
function onmouseOverOutRows() {
var rows = document.getElementById('playersTable').getElementsByTagName('tr');
for (var i = 1; i < rows.length; i++) {
rows[i].setAttribute("onmouseover", "this.bgColor='#00cc00'");
rows[i].setAttribute("onmouseout", "this.bgColor='#ffffff'");
}
}
//]]>
</script>
...
<h:body onload="onmouseOverOutRows();">
<h:dataTable id="playersTable" value="#{playersBean.data}" var="t">
...
完整示例在本书的代码包中命名为 ch6_14_2
。
另一种方法不涉及使用 JavaScript 代码。在这种情况下,你可以尝试使用 CSS 伪类,如下所示:
tbody tr:hover {
background-color: red;
}
完成!完整的应用程序在本书的代码包中命名为 ch6_14_3
。
鼠标点击时高亮显示行
使用鼠标点击来高亮显示行可以通过另一段 JavaScript 代码实现。你必须给每一行添加 onclick
属性,并在用户反复点击同一行时控制颜色交替,如下面的代码所示:
<script type="text/javascript">
//<![CDATA[
function onClickRows() {
var rows = document.getElementById('playersTable').getElementsByTagName('tr');
for (var i = 1; i < rows.length; i++) {
rows[i].setAttribute("onclick", "changeColor(this);");
}
}
function changeColor(row) {
var bgcolor = row.bgColor;
if (bgcolor === "") {
row.bgColor = "#00cc00";
} else if (bgcolor === "#00cc00") {
row.bgColor = "#ffffff";
} else if (bgcolor === "#ffffff") {
row.bgColor = "#00cc00";
}
}
//]]>
</script>
...
<h:body onload="onClickRows();">
<h:dataTable id="playersTable" value="#{playersBean.data}" var="t">
...
完整示例在本书的代码包中命名为 ch6_14_4
。
摘要
表格数据在 Web 应用程序中非常常用,本章是对强大的 JSF DataTable 组件(<h:dataTable>
)的致敬。JSF 2.2 通过允许开发者渲染比以前更多的集合,通过添加新的 CollectionDataModel
类,带来了更多的功能。本章涵盖了表格应完成的常见任务,例如排序、过滤、懒加载和 CSS 支持。请注意,PrimeFaces([primefaces.org/
](http://primefaces.org/))在 <p:dataTable>
标签(http://www.primefaces.org/showcase/ui/datatableHome.jsf)下提供了一个酷炫且全面的 <h:dataTable>
标签扩展。
在下一章中,我们将介绍 JSF 应用程序的 AJAX 技术。
第七章:JSF 和 AJAX
JSF 和 AJAX 已经是长期以来的优秀搭档。这种组合的潜力被许多 JSF 扩展(Ajax4Jsf、OmniFaces、PrimeFaces、RichFaces、ICEfaces 等)充分利用,它们提供了许多 AJAX 内置组件,扩展了 AJAX 默认功能,增加了 AJAX 的安全性和可靠性,并为需要操作 AJAX 机制“内部”的开发者提供了更多控制。
默认情况下,JSF 包含一个 JavaScript 库,该库封装了处理 AJAX 请求或响应的 AJAX 方法。此库可以通过以下两种方式加载:
-
使用
<f:ajax>
标签,隐式加载内置 AJAX 库。 -
使用
jsf.ajax.request()
,显式加载 AJAX 库,开发者可以访问 AJAX 代码。这种方法通常用于必须更改默认 AJAX 行为时。这应由具有高度专业知识的开发者执行,因为修改默认 AJAX 行为可能会导致不希望的问题和漏洞。
在本章中,您将学习以下主题:
-
JSF-AJAX 生命周期的简要概述
-
一个简单的 JSF-AJAX 示例
-
execute
、render
、listener
和event
属性如何工作 -
在客户端监控 AJAX 状态
-
在客户端监控 AJAX 错误
-
在
<f:ajax>
标签下分组组件 -
在验证错误后使用 AJAX 更新输入字段
-
混合 AJAX 和流程作用域
-
后台提交和 AJAX 如何协同工作
-
如何确定请求是 AJAX 还是非 AJAX
-
AJAX 和
<f:param>
如何工作 -
AJAX 请求的队列控制
-
如何显式加载
jsf.js
-
如何编写 AJAX 进度条 / 指示器
JSF-AJAX 生命周期的简要概述
AJAX 的请求-响应周期以 部分处理 和 部分渲染 阶段为特征;这意味着 AJAX 部分影响了当前视图。因此,请求不是典型的 JSF 请求,它们遵循由 javax.faces.context.PartialViewContext
类指定的不同生命周期。这个类的方法知道如何处理 AJAX 请求,这意味着它们负责解决组件树的部分处理和渲染。
AJAX 请求的核心由 <f:ajax>
标签的两个属性表示:execute
和 render
。execute
属性指示应在服务器上处理的组件(部分处理),而 render
属性指示应在客户端渲染(或重新渲染)的组件(部分渲染)。
在接下来的章节中,您将看到许多这些属性如何工作的示例。
一个简单的 JSF-AJAX 示例以开始学习
最简单的 JSF-AJAX 示例可以在几秒钟内编写。让我们考虑一个包含输入文本和按钮的 JSF 表单,该按钮将用户输入发送到服务器。服务器将用户输入(一个字符串)转换为大写,并在输出文本组件中显示给用户。接下来,您可以将此场景 ajax 化,如下面的示例代码所示:
<h:form>
<h:inputText id="nameInputId" value="#{ajaxBean.name}"/>
<h:commandButton value="Send" action="#{ajaxBean.ajaxAction()}">
<f:ajax/>
</h:commandButton>
<h:outputText id="nameOutputId" value="#{ajaxBean.name}"/>
</h:form>
<f:ajax>
标签的存在足以将此请求转换为 AJAX 请求。好吧,这个请求确实不是很实用,因为我们没有指定哪些组件应该执行以及哪些组件应该重新渲染。但好处是您不会收到任何错误;JSF 将使用execute
和render
属性的默认值,这将要求 JSF 处理触发请求的元素,并重新渲染无内容。
注意
当execute
或render
属性缺失时,JSF 将处理触发请求的元素,并重新渲染无内容。
添加具有inputText
ID(nameInputId
)标签值的execute
属性将告诉 JSF 将用户输入传递到服务器。这意味着用户输入将在ajaxAction
方法中可用,并将其转换为大写。您可以在应用程序服务器日志中检查此方法的效果,因为它在客户端不可见,因为render
属性仍然默认为无。因此,您需要添加render
属性并指明应重新渲染的组件 ID;在这种情况下,具有 ID nameOutputId
的输出文本:
<h:form>
<h:inputText id="nameInputId" value="#{ajaxBean.name}"/>
<h:commandButton value="Send" action="#{ajaxBean.ajaxAction()}">
<f:ajax execute ="nameInputId" render="nameOutputId"/>
</h:commandButton>
<h:outputText id="nameOutputId" value="#{ajaxBean.name}"/>
</h:form>
完成!这是一个简单且功能性的 AJAX 应用程序。您可以在本章的代码包中找到完整的代码,命名为ch7_1
。
JSF-AJAX 属性
在本节中,您将看到<f:ajax>
支持的主要属性。我们从execute
和render
开始,继续到listener
和event
,最后是onevent
和onerror
。
执行和渲染属性
在前面的示例中,execute
和render
属性影响一个由其 ID 指定的单个组件。当多个组件受到影响时,我们可以指定由空格分隔的 ID 列表,或者我们可以使用以下关键字:
-
@form
:此关键字指代包含 AJAX 组件的表单中所有组件的 ID。如果它存在于execute
属性中,则整个<h:form>
将被提交和处理。在render
属性的情况下,整个<h:form>
将被渲染。 -
@this
:此关键字指代触发请求的元素的 ID(当execute
缺失时默认)。对于execute
属性,@this
将仅提交和处理包含 AJAX 组件的组件,而对于render
属性,它将仅渲染包含 AJAX 组件的组件。 -
@none
: 没有组件将被处理/重新渲染。但对于execute
属性,JSF 仍将执行生命周期,包括其阶段监听器;而对于render
属性,JSF 将执行 Render Response 阶段,包括触发任何preRenderView
事件。这是render
属性的默认值。 -
@all
: 这个关键字代表所有组件 ID。对于execute
,页面上的所有组件都将被提交和处理——就像一个完整的页面提交。对于render
属性,JSF 将渲染页面上的所有组件;这将更新页面,但允许保留一些 JSF 外部的客户端状态。
根据应用程序的需求,这些关键字和组件 ID 可以混合使用以获得酷炫的 AJAX 请求。例如,通过以下 AJAX 请求:
-
使用以下代码处理并重新渲染当前表单:
<f:ajax execute="@form" render="@form"/>
-
处理表单,不重新渲染任何内容,按照以下方式:
<f:ajax execute="@form" render="@none"/>
-
按照以下方式处理触发请求的元素并重新渲染表单:
<f:ajax execute="@this" render="@form"/>
-
按照以下方式处理表单并重新渲染所有内容:
<f:ajax execute="@form" render="@all"/>
-
按照以下方式处理表单并重新渲染表单内具有
nameInputId phoneInputId
ID 的组件:<f:ajax execute="@form" render="nameInputId phoneInputId"/>
我们可以继续使用许多其他示例,但我认为你已经明白了这个概念。
注意
关键字(@form
、@this
、@all
和 @none
)和组件 ID 可以在 render
和 execute
属性的相同值中混合使用。别忘了用空格将它们分开。
完整的应用程序可以在本章的代码包中找到,命名为 ch7_2
。
一个特殊情况是重新渲染包含触发请求的 AJAX 元素的表单外的组件。看看以下示例:
<h:message id="msgId" showDetail="true" showSummary="true"
for="nameId" style="color: red;"/>
<h:form>
<h:inputText id="nameId" value="#{ajaxBean.name}"
validator="nameValidator"/>
<h:commandButton value="Submit">
<f:ajax execute="@form" listener="#{ajaxBean.upperCaseName()}"
render="@form :msgId :trackRequestId:trackId"/>
</h:commandButton>
</h:form>
<h:form id="trackRequestId">
Request number: <h:outputText id="trackId" value="#{ajaxBean.request}"/>
</h:form>
注意
使用 :
符号来更新表单外的组件,其中包含触发 AJAX 请求的元素。这个符号表示 UINamingContainer.getSeparatorChar
方法返回的默认分隔符。这可以通过 javax.faces.SEPARATOR_CHAR
上下文参数来指定。
完整的应用程序可以在本章的代码包中找到,命名为 ch7_3
。
监听器属性
<f:ajax>
的另一个重要属性名为 listener
。此属性指示当客户端动作触发 AJAX 请求时应执行的服务器端方法。例如,你可以使用以下代码来做这件事:
<h:form>
<h:inputText value="#{ajaxBean.name}"/>
<h:commandButton value="Send" action="#{ajaxBean.upperCaseName()}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
</h:form>
好吧,使用 listener
属性,你可以将前面的代码转换为以下代码:
<h:form>
<h:inputText value="#{ajaxBean.name}"/>
<h:commandButton value="Send">
<f:ajax listener="#{ajaxBean.upperCaseName()}"
execute="@form" render="@form"/>
</h:commandButton>
</h:form>
在这里出现了一个明显的问题。这两个之间有什么区别,为什么我应该使用 listener
而不是 action
?好吧,这两个之间有一些区别,以下是最重要的几个:
-
通过
action
属性调用的服务器端方法可以返回表示导航情况(结果)的String
,而通过listener
调用的服务器端方法不能提供导航情况。 -
如果客户端在浏览器配置中禁用了 JavaScript,
listener
属性将不再起作用——服务器端方法将不会被调用。action
属性仍然有效。 -
不支持
action
属性的组件可以使用listener
代替。 -
通过
listener
属性调用的服务器端方法接受一个类型为AjaxBehaviorEvent
的参数,它表示 AJAX 特定的组件行为。在action
属性的情况下不接受。例如,参考以下代码:<h:form> <h:inputText value="#{ajaxBean.name}"/> <h:commandButton value="Send"> <f:ajax listener="#{ajaxBean.upperCaseName}" execute="@form" render="@form"/> </h:commandButton> </h:form> ... public void upperCaseName(AjaxBehaviorEvent event){ ... }
注意
记住,客户端行为(ClientBehavior
接口)负责生成可重用的 JavaScript 代码,这些代码可以添加到 JSF 组件中。AJAX (<f:ajax>
) 是客户端行为,这意味着它始终作为行为附加到另一个 UI 组件(s)。你可以在第五章(Chapter 5)的“使用客户端行为功能”部分中找到更多关于 ClientBehavior
的信息,JSF Configurations Using XML Files and Annotations – Part 2。
完整的应用程序可以在本章的代码包中找到,名称为 ch7_4
。
事件属性
每个 AJAX 请求都是由一个表示用户或程序性操作的事件触发的。JSF 根据父组件定义默认事件;根据文档,“对于 <h:commandButton>
等ActionSource
组件,默认事件是 action
,而对于 <h:inputText>
等EditableValueHolder
组件,默认事件是 valueChange
”。大多数情况下,默认事件正是你所需要的,但如果你想要为组件显式设置一个事件,可以使用 event
属性。此属性的常见值包括 click
、focus
、blur
、keyup
和 mouseover
。
注意
不要将这些事件与带有 on
前缀的 JavaScript 事件混淆(例如 onclick
、onkeyup
、onblur
等)。JavaScript 事件位于 AJAX 事件之后;或者换句话说,AJAX 事件基于 JavaScript 事件。例如,AJAX click
事件基于 onclick
JavaScript 事件。
在以下代码中,触发 AJAX 动作的事件是 keyup
:
<h:form>
<h:inputText value="#{ajaxBean.name}">
<f:ajax event="keyup" listener="#{ajaxBean.upperCaseName()}" render="@this"/>
</h:inputText>
</h:form>
完整的应用程序可以在本章的代码包中找到,名称为 ch7_5
。
onevent 属性 - 在客户端监控 AJAX 状态
在 AJAX 请求期间,JSF 能够调用客户端定义的 JavaScript 方法,并将一个名为 data
的对象传递给它,其中包含有关请求当前状态的信息。当请求开始、完成和成功时,调用 JavaScript 函数。
data
对象封装了以下属性:
-
type
: 这个属性给出了 AJAX 调用的类型,event
-
status
: 这个属性返回begin
、complete
或success
状态(可用于实现不确定进度条)。注意
当
status
属性值为begin
时,表示 AJAX 请求尚未发送。当它等于complete
时,表示 AJAX 响应已成功到达客户端,但尚未被处理。如果接收到的响应成功处理(没有错误),则status
值变为success
。 -
source
: 这个属性返回表示 AJAX 事件源的 DOM 元素 -
responseXML
: 这是以 XML 格式表示的 AJAX 响应 -
responseText
: 这是以文本格式表示的 AJAX 响应 -
responseCode
: 这是以 AJAX 响应代码
您需要通过onevent
属性指示 JavaScript 方法的名称(在jsf.js
中,表示此属性实现的 JavaScript 方法命名为addOnEvent
(回调)):
<h:commandButton value="Submit">
<f:ajax onevent="ajaxMonitoring" execute="@form"
listener="#{ajaxBean.upperCaseName()}" render="@form"/>
</h:commandButton>
接下来,ajaxMonitoring
函数可以使用data
对象及其属性来完成不同的客户端任务。例如,以下实现向一些div
标签中填充有关 AJAX 请求的详细信息:
<script type="text/javascript">
function ajaxMonitoring(data) {
document.getElementById("statusId").innerHTML += data.status + " | ";
document.getElementById("responseCodeId").innerHTML +=
status.responseCode + "| ";
if(data.status === "complete") {
document.getElementById("typeId").innerHTML += data.type;
document.getElementById("sourceId").innerHTML += data.source;
...
</script>
在以下图中,您可以看到可能的输出:
完整的应用程序可以在本章的代码包中找到,命名为ch7_21
。
onerror 属性 - 在客户端监控 AJAX 错误
在前面的章节中,您看到了如何使用客户端定义的 JavaScript 函数和data
对象来监控 AJAX 请求的状态。基于同样的技术,我们可以获取有关在 AJAX 请求过程中可能发生的错误的信息。传递的data
对象封装了以下属性(请注意,这是前面章节中的同一个data
对象;因此您仍然可以访问这些属性):description
、errorName
和errorMessage
。
data.type
属性将为error
,而data.status
属性将为以下之一:
-
serverError
: 这包含错误的 AJAX 请求的响应 -
malformedXML
: 这是一个 XML 格式错误 -
httpError
: 这是一个有效的 HTTP 错误 -
emptyResponse
: 这是一段服务器端代码,没有提供响应
JavaScript 方法的名称通过onerror
属性指示(在jsf.js
中,表示此属性实现的 JavaScript 方法命名为addOnError
(回调))。因此,在此点,我们可以更新前面章节中的应用程序,以在客户端报告错误,如下面的代码所示(请注意,onevent
和onerror
调用同一个方法ajaxMonitoring
;然而这并非强制性的,您也可以使用单独的 JavaScript 方法):
<script type="text/javascript">
function ajaxMonitoring(data) {
document.getElementById("statusId").innerHTML += data.status + " | ";
if(data.status === "serverError" || data.status === "malformedXML" ||
data.status === "httpError" || data.status === "emptyResponse"){
document.getElementById("descriptionId").innerHTML +=
data.description;
document.getElementById("errorNameId").innerHTML += data.errorName;
document.getElementById("errorMessageId").innerHTML +=
data.errorMessage;
}
document.getElementById("responseCodeId").innerHTML +=
status.responseCode + "| ";
if (data.status === "complete") {
document.getElementById("typeId").innerHTML += data.type;
document.getElementById("sourceId").innerHTML += data.source +
"<br/><xmp>" + new XMLSerializer().serializeToString(data.source) +
"</xmp>";
document.getElementById("responseXMLId").innerHTML +=
data.responseXML + "<br/><xmp>" + new
XMLSerializer().serializeToString(data.responseXML) + "</xmp>";
document.getElementById("responseTextId").innerHTML += "<xmp>" +
data.responseText + "</xmp>";
}
}
</script>
现在,您可以通过添加一个故意的错误来测试此代码,例如调用不存在的服务器端方法,如下面的代码所示:
<h:commandButton value="Submit">
<f:ajax onevent ="ajaxMonitoring" onerror="ajaxMonitoring"
execute="@form" listener="#{ajaxBean.unexistedMethod()}"
render="@form"/>
</h:commandButton>
下面的屏幕截图显示了可能的输出:
完整的应用程序可以在本章的代码包中找到,命名为 ch7_6
。
在 <f:ajax>
标签下分组组件
有时,将多个组件分组在同一个 <f:ajax>
标签下可能很有用。例如,以下代码片段将两个 <h:inputText>
组件分组在同一个 <f:ajax>
标签下(您也可以嵌套其他组件):
<f:ajax event="click" execute="submitFormId" render="submitFormId">
<h:form id="submitFormId">
Name:
<h:inputText id="nameId" value="#{ajaxBean.name}"
validator="nameValidator"/>
<h:message id="msgNameId" showDetail="true" showSummary="true"
for="nameId" style="color: red;"/>
Surname:
<h:inputText id="surnameId" value="#{ajaxBean.surname}"
validator="nameValidator"/>
<h:message id="msgSurnameId" showDetail="true" showSummary="true"
for="surnameId" style="color: red;"/>
</h:form>
</f:ajax>
那么,它是如何工作的呢?当您点击任何一个输入组件时,会为输入组件和表单(在我们的例子中是两个请求)发起一个 AJAX 请求,并且表单中的所有组件都会重新渲染。由于 click
事件会生成 AJAX 请求/响应,除非您使用 Tab 键在每个 <h:inputText>
组件中获取焦点,否则您将无法在那些 <h:inputText>
中输入键。
注意
在 <f:ajax>
标签下分组的组件仍然可以使用内部(或本地使用)的 <f:ajax>
标签。在这种情况下,效果是累积的。当然,当您使用这种技术时,必须格外小心,因为可能会出现不期望的行为。
完整的应用程序可以在本章的代码包中找到,命名为 ch7_7
。
在验证错误后使用 AJAX 更新输入字段
在验证错误后使用 AJAX 更新输入字段是 JSF 开发者非常古老、众所周知且令人烦恼的问题。当 AJAX 请求在验证阶段失败时,没有内置的方法可以更新输入字段以包含一些有效值,因为 JSF 不允许在验证错误后访问模型值(通常,您希望清除这些字段或提供一些默认值,甚至是一些由同一用户提供的旧值)。当然,JSF 开发者找到了不同的解决方案,或者使用了其他库,如 PrimeFaces 或 OmniFaces,但需要一个 JSF 解决方案。
从 JSF 2.2 开始,如果我们将 resetValues
属性设置为 true
,则应该重新渲染的所有组件(在 render
属性中指示的组件)将被重置。理解这一点最简单的方法是通过比较测试。首先,让我们使用没有 resetValues
的 AJAX 请求:
<h:form>
<h:message id="msgId" showDetail="true" showSummary="true" for="nameId" style="color: red;"/>
<h:inputText id="nameId" value="#{ajaxBean.name}"
validator="nameValidator"/>
<h:commandButton value="Submit">
<f:ajax execute="@form" resetValues="false"
listener="#{ajaxBean.upperCaseName()}" render="nameId msgId"/>
</h:commandButton>
</h:form>
假设我们输入字段的合法值是一个字母数字字符串(与 [^a-zA-Z0-9] 模式相关)。在下面的截图中,在左侧,您可以看到插入有效值后的 AJAX 结果,而在右侧,您可以看到插入无效值后的 AJAX 结果:
如您在前面的截图中所见,在右侧,无效的值没有被重置。无效的值被保留,这非常令人烦恼。
接下来,我们继续同样的案例,但添加了 resetValues
属性:
<h:form>
<h:message id="msgId" showDetail="true" showSummary="true" for="nameId"
style="color: red;"/>
<h:inputText id="nameId" value="#{ajaxBean.name}"
validator="nameValidator"/>
<h:commandButton value="Submit">
<f:ajax execute="@form" resetValues="true"
listener="#{ajaxBean.upperCaseName()}" render="nameId msgId"/>
</h:commandButton>
</h:form>
现在,我们重复测试。在下面的截图中,左侧的提交值是有效的,而右侧的值是无效的:
现在,当提交的值无效时,输入字段会被重置(在这种情况下,会被清除)。
注意
从这个例子中,您可能会误解 resetValues
的工作原理就像是一个清除(空)字段的动作。实际上,它不是这样!当一个输入字段被重置时,替换无效值的有效值与托管 Bean(渲染器将从 Bean 中获取值)相关。如果托管 Bean 在请求作用域中,替换者(有效值)将是用于初始化相应属性(这可能是一切,而不仅仅是空字符串)的值。但如果托管 Bean 在视图作用域中,那么替换者将是相应属性的当前有效值,这可能是最初的值,或者是用户插入的先前有效值(当然,在服务器端方法中可能已更改或未更改)。
在测试本章代码包中名为 ch7_8_1
的完整应用程序时,请记住这个注意事项。默认情况下,此应用程序包含一个请求作用域的托管 Bean,但您可以轻松将其转换为视图作用域以进行更多测试。
除了用于 AJAX 请求的 resetValues
属性外,JSF 2.2 还提供了一个名为 <f:resetValues>
的标签,用于非 AJAX 请求。基本上,这是一个可以轻松附加到任何 ActionSource
实例(例如,<h:commandButton>
)的动作监听器。其效果将包括重置其 render
属性中给出的所有组件(仅使用组件 ID,而不是如 @form
、@all
等关键字):
<h:commandButton value="Submit" action="#{nonAjaxBean.upperCaseName()}">
<f:resetValues render="nameId" />
</h:commandButton>
完整的应用程序可以在本章的代码包中找到,并命名为 ch7_8_2
。此标签在所有 JSF 2.2(Mojarra 和 MyFaces)版本中可能不被识别,因此您必须进行测试以确保可以使用它。
取消和清除按钮
类型为 取消(将表单的字段重置到初始状态或最近的合法状态)和 清除(清除表单的字段)的按钮在 Web 应用程序中并不常见,但有时它们对最终用户可能很有用。在实现 取消/清除按钮时,您需要找到一种方法来跳过 过程验证 阶段(这是 提交 按钮所需的)。动机很简单:当用户取消/清除表单的值时,我们当然不需要有效的值来完成这些任务;因此,不需要验证。
在非 AJAX 请求中,一种常见的技巧是使用 immediate="true"
属性,对于命令组件(例如,<h:commandButton>
),它将在 应用请求值 阶段转移动作的调用。此属性也适用于 AJAX 请求,但 AJAX 为这些任务提供了更好的解决方案。我们不需要使用 immediate="true"
,而是可以使用 @this
关键字。此外,我们可以使用 resetValues
功能来简化并加强 取消/清除 按钮。
现在,让我们看看一些场景。我们将保持简单,因此我们需要一个带有单个输入字段和三个按钮(提交、取消和清除)的表单。验证器将只允许字母数字字符(根据[^a-zA-Z0-9]
模式)。
提交给视图作用域管理器的值
在这种情况下,运行以下代码:
<h:form>
<h:message id="msgId" showDetail="true" showSummary="true" for="nameId" style="color: red;"/>
<h:inputText id="nameId" value="#{ajaxBean.name}"
validator="nameValidator"/>
<h:commandButton value="Submit">
<f:ajax execute="@form" resetValues="true"
listener="#{ajaxBean.upperCaseName()}" render="nameId msgId"/>
</h:commandButton>
<h:commandButton value="Cancel">
<f:ajax execute="@this" render="@form"/>
</h:commandButton>
<h:commandButton value="Clear">
<f:ajax execute="@this" render="@form"
listener="#{ajaxBean.cancelName()}"/>
</h:commandButton>
</h:form>
按下提交按钮。如果值无效,你将看到一个特定的错误消息(<h:message>
),并且resetValues
将输入字段重置为初始值(空字符串或一些建议)或最近的合法值。
按下取消按钮。由于我们使用execute="@this"
,输入字段将不会在服务器上处理;因此不会发生验证。重新渲染过程对输入字段的效果与resetValues
相同,但也会清除<h:message>
标签。
按下清除按钮。此按钮也使用execute="@this"
,但它不是将输入字段重置为resetValues
,而是清除输入字段和<h:message>
。为此,在管理器中需要添加一个额外的方法,如下所示:
private String name = "RafaelNadal";
...
public void cancelName() {
name = "";
}
完整的应用程序可以在本章的代码包中找到,命名为ch7_9_1
。
作为一个小技巧,对于清除按钮,你可能想使用占位符如下:
...
<h:inputText id="nameId" value="#{ajaxBean.name}"
validator="nameValidator" f5:placeholder="Enter your name ..."/>
提交给请求作用域管理器的值
由于提交的值不会在多个 AJAX 请求之间持久化,resetValues
方法和取消按钮将输入字段重置为初始化值(空字符串或建议)。取消按钮还将重置<h:message>
标签。清除按钮将清除输入文本和<h:message>
。当然,在某些情况下(例如使用空字符串进行初始化),取消和清除按钮将执行相同的功能;因此,你可以去掉其中一个。
完整的应用程序可以在本章的代码包中看到,命名为ch7_9_2
。
注意
如何使用resetValues
和实现取消和清除按钮的更多示例可以在本书附带的源代码中找到。使用具有取消/清除功能的输入字段中的keyup
事件的一组示例包含以下应用:ch7_9_3
、ch7_9_4
、ch7_9_5
和ch7_9_6
。
似乎一切都很直接,但有一个问题我们必须修复。让我们仔细看看以下代码(其中没有复杂的地方):
<h:form>
Name:
<h:inputText id="nameId" value="#{ajaxBean.name}"
validator="nameValidator"/>
<h:message id="msgNameId" showDetail="true" showSummary="true"
for="nameId" style="color: red;"/>
Surname:
<h:inputText id="surnameId" value="#{ajaxBean.surname}"
validator="nameValidator"/>
<h:message id="msgSurnameId" showDetail="true" showSummary="true"
for="surnameId" style="color: red;"/> ..
<h:commandButton value="Submit">
<f:ajax execute="@form"
listener="#{ajaxBean.upperCaseNameAndSurname()}"
render="@form"/>
</h:commandButton>
<h:commandButton value="Cancel">
<f:ajax execute="@this" render="@form"/>
</h:commandButton>
<h:commandButton value="Clear/Reset">
<f:ajax execute="@this" render="@form"
listener="#{ajaxBean.cancelNameAndSurname()}"/>
</h:commandButton>
</h:form>
让我们关注提交过程。当我们提交一个有效的名字和姓氏时,表单会重新渲染,一切看起来都很正常,但如果一个值(或两个)是无效的,那么输入字段不会重置,并且会显示相应的错误消息。这是正常的,因为 resetValues
方法不存在;因此,第一个想法可能是将 resetValues="true"
添加到对应于 提交 按钮的 <f:ajax>
中。然而,这不会按预期工作,因为在无效值的情况下没有任何操作。虽然你可能认为输入字段会重置为无效值,但你可能会惊讶地看到在重新渲染后一切保持不变,无效值仍然存在。原因似乎是在 提交 按钮的 render
属性中存在 @form
。如果你用应该重新渲染的组件 ID(nameId
、msgNameId
、surnameId
和 msgSurnameId
)替换它,resetValues
方法将完美工作。
但是,如果你有很多输入字段,而且不想列出所有组件的 ID,或者你只想在 render
属性中使用 @form
关键字?在这种情况下,你应该知道无效的输入字段将不会自动重置(resetValues
方法无效),并且最终用户应该通过点击 取消 或 清除 按钮手动取消/清除输入字段。虽然 取消 按钮工作正常,但 清除 按钮有一个大 Oops!,因为 JSF 不会清除未执行(列在 execute
属性中)且重新渲染(列在 render
属性中)的输入字段,除非你只提交有效值。换句话说,如果名字是有效的,而姓氏不是(或任何涉及无效值的组合),那么在提交和清除后,名字的输入字段不会被清除。
解决这个问题的方法之一可以在 OmniFaces (code.google.com/p/omnifaces/
) 上找到,它提供了一个名为 org.omnifaces.eventlistener.ResetInputAjaxActionListener
的动作监听器 (showcase.omnifaces.org/eventlisteners/ResetInputAjaxActionListener
)。这个监听器能够修复 清除 按钮和其他同一类的问题:
<h:commandButton value="Clear/Reset">
<f:ajax execute="@this" render="@form"
listener="#{ajaxBean.cancelNameAndSurname()}"/>
<f:actionListener type="org.omnifaces.eventlistener.
ResetInputAjaxActionListener"/>
</h:commandButton>
完整的应用程序可以在本章的代码包中找到,该代码包命名为 ch7_9_7
。
混合 AJAX 和流程作用域
AJAX 请求通常与视图作用域中的 beans 相关(@ViewScoped
),这意味着只要当前视图没有被导航情况(或其他原因)销毁,数据就可以在多个 AJAX 请求中持久化(存储)。一个流程被定义为逻辑相关页面/视图的集合;因此,AJAX 无法在流程转换中存活。
为了更好地理解,我们将适应在第三章中开发的JSF 作用域 – 在管理 Bean 通信中的生命周期和使用(ch3_7_3
应用程序,你需要熟悉)以支持registration.xhtml
视图(流程中的第一页)中的 AJAX 请求。主要思想是编写一个视图范围内的 bean,该 bean 可能填充在流程范围内的 bean 中定义的玩家姓名和姓氏。名为ViewRegistrationBean
的视图范围内的 bean 将随机生成一个姓名-姓氏对,并将它们作为建议呈现给最终用户。用户可以提供姓名和姓氏,或者他/她可以选择使用建议的姓名和姓氏。因此,流程范围内的 bean 看起来如下所示:
import javax.faces.flow.FlowScoped;
import javax.inject.Named;
@Named
@FlowScoped(value = "registration")
public class RegistrationBean {
private String playerName ="";
private String playerSurname="";
//getters and setters
public void credentialsUpperCase(){
playerName = playerName.toUpperCase();
playerSurname = playerSurname.toUpperCase();
}
public String getReturnValue() {
return "/done";
}
public String registrationAction() {
return "confirm";
}
}
注意,getReturnValue
方法代表流程返回(退出流程),而registrationAction
方法在流程中导航到下一页。两者都将破坏当前视图。
接下来,视图范围内的 bean 是使用@PostConstruct
注解的方法,它将帮助我们查看 AJAX 是否在多个请求中使用了此 bean 的相同实例:
@Named
@ViewScoped
public class ViewRegistrationBean implements Serializable {
@Inject
RegistrationBean registrationBean;
private String playerNameView = "nothing";
private String playerSurnameView = "nothing";
private static final Map<Integer, String> myMap = new HashMap<>();
static {
myMap.put(1, "Nadal Rafael");
myMap.put(2, "Federer Roger");
...
}
@PostConstruct
public void init() {
Random r = new Random();
int key = 1 + r.nextInt(9);
String player = myMap.get(key);
String[] fullname = player.split(" ");
playerNameView = fullname[0];
playerSurnameView = fullname[1];
playerNameView = playerNameView.toUpperCase();
playerSurnameView = playerSurnameView.toUpperCase();
}
public String getPlayerNameView() {
return playerNameView;
}
public void setPlayerNameView(String playerNameView) {
this.playerNameView = playerNameView;
}
public String getPlayerSurnameView() {
return playerSurnameView;
}
public void setPlayerSurnameView(String playerSurnameView) {
this.playerSurnameView = playerSurnameView;
}
public void generateCredentials() {
registrationBean.setPlayerName(playerNameView);
registrationBean.setPlayerSurname(playerSurnameView);
}
}
我们可以通过在registration.xhtml
中使用以下代码轻松监控姓名和姓氏的值:
Your registration last credentials (in <b>flow</b>):
<h:outputText id="credentialsFlowId"
value="#{registrationBean.playerName}
#{registrationBean.playerSurname}"/>
<hr/>
Random credentials (in <b>view</b>) [as long as we are in this view this value won't change]:
<h:outputText id="credentialsViewId"
value="#{viewRegistrationBean.playerNameView}
#{viewRegistrationBean.playerSurnameView}"/>
现在,将有两个按钮触发 AJAX 请求。一个按钮将调用服务器端方法credentialsUpperCase
(来自流程范围内的 bean,RegistrationBean
),另一个按钮将调用服务器端方法generateCredentials
(来自视图范围内的 bean,ViewRegistrationBean
)。在这两种情况下,我们将按照以下方式重新渲染 bean 中的玩家姓名和姓氏:
<h:form>
Name: <h:inputText value="#{registrationBean.playerName}"/>
Surname: <h:inputText value="#{registrationBean.playerSurname}"/>
<h:commandButton value="Register To Tournament (AJAX call a method of a
flow bean)" action="#{registrationBean.credentialsUpperCase()}">
<f:ajax execute="@form"
render="@form :credentialsFlowId :credentialsViewId"/>
</h:commandButton>
<h:commandButton value="Register To Tournament (AJAX call a method of a
view bean)" action="#{viewRegistrationBean.generateCredentials()}">
<f:ajax execute="@this"
render="@form :credentialsFlowId :credentialsViewId"/>
</h:commandButton>
</h:form>
现在,最终用户可以通过两种方式注册比赛:通过手动在输入字段中插入姓名和姓氏并通过按下第一个按钮(结果将是插入的姓名和姓氏的大写形式)注册,或者他/她可以选择使用建议的姓名和姓氏,并通过按下第二个按钮(结果将是随机姓名和姓氏的大写形式)注册。
在这里可以注意到一些重要的事情,如下列所示:
-
通过按下第一个按钮触发 AJAX 请求,将提交的姓名和姓氏放入流程范围(手动输入或从随机建议导入)。
-
通过按下第二个按钮触发 AJAX 请求,将分配建议的姓名和姓氏到流程范围内的 bean 的对应部分。由于我们在多个 AJAX 请求中处于相同的视图,并且
init
方法仅在创建ViewRegistrationBean
bean 的新实例时调用,因此它不会为每个请求生成新的姓名和姓氏。 -
如果我们退出并重新进入流程,持久化的姓名和姓氏将失去它们的值。当我们退出流程时,我们达到流程作用域的边界,这意味着在再次进入流程时必须创建一个新的
RegistrationBean
实例。此外,此结果将改变当前视图;因此,还需要一个新的ViewRegistrationBean
实例。 -
当我们在流程中导航到下一页时,提交的姓名和姓氏具有相同的值,因为它们已在流程作用域中持久化;而建议的姓名和姓氏再次随机生成,结果已改变视图,即使我们处于同一流程中,如下截图所示:
现在您已经了解了 AJAX 如何与视图作用域结合使用。完整的应用程序可以在本章的代码包中找到,该代码包命名为ch7_10
。
回发和 AJAX
在整本书中,我们多次提到了回发请求。对于不熟悉它的人,或者只是需要快速提醒的人,让我们说 JSF 识别初始请求和回发请求。
初始请求(例如,HTTP GET
)是浏览器为加载页面发送的第一个请求。您可以通过在浏览器中访问应用程序 URL 或通过跟随链接(可以是应用程序任何页面的链接)来获取此类请求。此外,当page_A
包含重定向(faces-redirect=true
)到page_B
时,初始请求发生在page_B
(这不适用于转发机制)。此类请求在恢复视图阶段和渲染响应阶段进行处理。
回发请求发生在我们点击按钮/链接提交表单时。与初始请求不同,回发请求会通过所有阶段。
JSF 提供了一个名为isPostback
的方法,它返回一个布尔值:对于回发请求返回true
,对于初始请求返回false
。在代码行中,我们可以:
-
使用以下代码在管理 Bean 中检查初始/回发请求:
FacesContext facesContext = FacesContext.getCurrentInstance(); logger.log(Level.INFO, "Is postback: {0}", facesContext.isPostback());
-
使用以下代码在页面中检查初始/回发请求:
Is postback ? <h:outputText value="#{facesContext.postback}"/>
例如,您可以使用一个简单的应用程序检查 AJAX 的初始/回发请求。JSF 页面如下:
<h:form>
<h:commandButton value="Click Me!">
<f:ajax listener="#{ajaxBean.requestAction()}" render=":postbackId"/>
</h:commandButton>
</h:form>
<h:panelGrid id="postbackId" columns="1">
<h:outputText value="Is postback ?: #{facesContext.postback}"/>
<h:outputText value="REQUEST NUMBER: #{ajaxBean.request_number}"/>
</h:panelGrid>
管理 Bean 如下:
@Named
@ViewScoped
public class AjaxBean implements Serializable{
private static final Logger logger =
Logger.getLogger(AjaxBean.class.getName());
private int request_number = 1;
public int getRequest_number() {
FacesContext facesContext = FacesContext.getCurrentInstance();
logger.log(Level.INFO, "Is postback (getRequest_number method): {0}",
facesContext.isPostback());
return request_number;
}
public void setRequest_number(int request_number) {
this.request_number = request_number;
}
public void requestAction(){
FacesContext facesContext = FacesContext.getCurrentInstance();
logger.log(Level.INFO, "Is postback (requestAction method): {0}", facesContext.isPostback());
request_number ++;
}
}
代码非常简单;因此,我们可以直接跳转到检查初始/回发请求,如下所示:
-
第一次请求:通过访问应用程序 URL 来加载应用程序的第一个页面。客户端将初始请求指示如下截图左侧所示,服务器端也以相同的指示,如下截图右侧所示:
-
第二次请求:点击我!按钮第一次被点击(第二次、第三次等的结果为
true
)。客户端(在浏览器中)如以下截图左侧所示,表明这是一个回发请求,服务器端如同一截图右侧所示:
注意
了解请求是初始的还是回发可能会有用。例如,您可能希望在初始请求(例如,初始化任务)时仅完成一次任务,或者每次都完成,除了第一次(例如,显示一条消息,这在由于初始请求而显示页面时并不合适)。
回发请求的条件渲染/执行
我们可以使用初始/回发请求检测来有条件地渲染 UI 组件(当然,您也可以用于部分处理)。看看以下代码:
<h:form id="ajaxFormId">
<h:commandButton id="buttonId" value="Click Me!">
<f:ajax listener="#{ajaxBean.requestAction()}"
render="#{facesContext.postback eq true ?
':postbackId': 'ajaxFormId'}"/>
</h:commandButton>
Is postback ? <h:outputText value="#{facesContext.postback}"/>
</h:form>
<h:panelGrid id="postbackId" columns="1">
<h:outputText value="REQUEST NUMBER: #{ajaxBean.request_number}"/>
</h:panelGrid>
那么,让我们看看它是如何工作的!当页面加载时,我们有一个初始请求(#{facesContext.postback}
返回false
),这意味着服务器响应将包含如下代码片段(我们需要关注<f:ajax>
组件):
<input id="ajaxFormId:buttonId" type="submit"
name="ajaxFormId:buttonId" value="Click Me!"
onclick="mojarra.ab(this,event,'action',0,'ajaxFormId');
return false" />
在服务器端,getRequest_number
方法的日志行也会揭示一个初始请求。此外,请注意,报告的请求号是1
,这是request_number
属性的初始值。
接下来,让我们点击一次点击我!按钮。现在,AJAX 请求将看起来像以下代码行:
ajaxFormId=ajaxFormId&javax.faces.ViewState=411509096033316844%3A7611114960827713853&javax.faces.source=ajaxFormId%3AbuttonId&javax.faces.partial.event=click&javax.faces.partial.execute=ajaxFormId%3AbuttonId%20ajaxFormId%3AbuttonId&javax.faces.partial.render
=ajaxFormId&javax.faces.behavior.event=action&javax.faces.partial.ajax=true
突出的代码提供了重要信息!这是一个回发请求,但render
属性包含<h:form>
组件的 ID,而不是<h:panelGrid>
组件的 ID(正如您可能认为的那样);这是因为#{facesContext.postback}
表达式在上一个请求中评估为false
。所以,在我们的按钮第一次点击时,AJAX 不会重新渲染<h:panelGrid>
组件。同时,在服务器端,request_number
属性已成功增加到2
;然而,对于最终用户来说,它仍然显示为1
。
现在,这个 AJAX 的服务器响应将包含以下代码:
<input id="ajaxFormId:buttonId" type="submit"
name="ajaxFormId:buttonId" value="Click Me!"
onclick="mojarra.ab(this,event,'action',0,'postbackId');
return false">
注意,postbackId
,即<h:panelGrid>
的 ID,存在于响应中。按钮的第二次点击(第二次点击)将生成下一个 AJAX 请求:
ajaxFormId=ajaxFormId&javax.faces.ViewState=270275638491205347%3A7563196939691682163&javax.faces.source=ajaxFormId%3AbuttonId&javax.faces.partial.event=click&javax.faces.partial.execute=ajaxFormId%3AbuttonId%20ajaxFormId%3AbuttonId&javax.faces.partial.render=postbackId&javax.faces.behavior.event=action&javax.faces.partial.ajax=true
现在,当 AJAX 请求完成时,<h:panelGrid>
组件将被重新渲染。request_number
属性达到值3
,并将显示在客户端。进一步的 AJAX 请求将是回发请求。
在下面的屏幕截图中,您可以查看初始请求,首先点击按钮,然后从客户端和服务器端进行第二次点击:
了解 AJAX 与初始/回发请求的行为会有所帮助——这不是一个错误。当然,一旦你知道这个问题,就有很多解决方案取决于你真正想要实现的目标。
此外,您可以尝试以类似的方式测试execute
属性。
完整的应用程序可以在本章的代码包中找到,该代码包命名为ch7_11
。
这是一个非 AJAX 请求吗?
JSF 可以通过检查请求头或调用PartialViewContext.isAjaxRequest
方法来回答这个问题。提供有关请求类型信息的请求头是Faces-Request
和X-Requested-With
。对于 AJAX 请求,Faces-Request
头将具有值partial/ajax
,而X-Requested-With
请求类型将具有值XMLHttpRequest
(在 JSF 2.2 中,X-Requested-With
似乎不起作用;然而,为了完整性,您可以再次测试它们)。在下面的屏幕截图中,您可以查看典型 JSF 2.2 AJAX 请求的头部:
在一个管理 Bean 中,您可以确定请求的类型,如下面的代码所示:
public void requestTypeAction() {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
Map<String, String> headers = externalContext.getRequestHeaderMap();
logger.info(headers.toString());
//determination method 1
PartialViewContext partialViewContext =
facesContext.getPartialViewContext();
if (partialViewContext != null) {
if (partialViewContext.isAjaxRequest()) {
logger.info("THIS IS AN AJAX REQUEST (DETERMINATION 1) ...");
} else {
logger.info("THIS IS A NON-AJAX REQUEST(DETERMINATION 1)...");
}
}
//determination method 2
String request_type_header_FR = headers.get("Faces-Request");
if (request_type_header_FR != null) {
if (request_type_header_FR.equals("partial/ajax")) {
logger.info("THIS IS AN AJAX REQUEST (DETERMINATION 2) ...");
} else {
logger.info("THIS IS A NON-AJAX REQUEST(DETERMINATION 2)...");
}
}
//determination method 3
String request_type_header_XRW = headers.get("X-Requested-With");
if (request_type_header_XRW != null) {
if (request_type_header_XRW.equals("XMLHttpRequest")) {
logger.info("THIS IS AN AJAX REQUEST (DETERMINATION 3) ...");
} else {
logger.info("THIS IS A NON-AJAX REQUEST(DETERMINATION 3)...");
}
}
}
或者,在 JSF 页面上,您可以编写以下代码:
AJAX/NON-AJAX:
#{facesContext.partialViewContext.ajaxRequest ? 'Yes' : 'No'}
FACES-REQUEST HEADER: #{facesContext.externalContext.requestHeaderMap['Faces-Request']}
X-REQUESTED-WITH HEADER: #{facesContext.externalContext.requestHeaderMap['X-Requested-With']}
完整的应用程序可以在本章的代码包中找到,该代码包命名为ch7_12
。
AJAX 和<f:param>
<f:param>
标签可以用来将请求参数传递给一个管理 Bean。由于我们已经在第二章中详细讨论了这个标签,JSF 中的通信,我们可以在这里通过一个示例继续使用它,在<f:ajax>
内部使用它:
<h:form>
<h:inputText id="nameInputId" value="#{ajaxBean.name}"/>
<h:commandButton value="Send" action="#{ajaxBean.ajaxAction()}">
<f:ajax execute ="nameInputId" render="nameOutputId">
<f:param name="surnameInputId" value="Nadal"/>
</f:ajax>
</h:commandButton>
<h:outputText id="nameOutputId" value="#{ajaxBean.name}"/>
</h:form>
请记住,传递的参数在请求参数映射中可用:
FacesContext fc = FacesContext.getCurrentInstance();
Map<String, String> params =
fc.getExternalContext().getRequestParameterMap();
logger.log(Level.INFO, "Surname: {0}", params.get("surnameInputId"));
注意
请记住,<f:param>
只能与按钮和链接一起使用。尝试在输入中添加<f:param>
将不会起作用。更多详细信息请参阅第二章,JSF 中的通信。
完整的应用程序可以在本章的代码包中找到,该代码包命名为ch7_13
。
AJAX 请求排队控制
在客户端排队 AJAX 请求是一种常见的做法,旨在确保一次只处理一个请求。这种方法的目的是保护服务器免受压垮,并防止客户端浏览器阻塞或以未定义的顺序接收 AJAX 响应。虽然 AJAX 排队在 JSF 2.0 中可用,但 AJAX 排队控制从 JSF 2.2 开始提供。
为了提供 AJAX 排队控制,JSF 2.2 为<f:ajax>
标签引入了一个名为delay
的属性。此属性的值是一个表示毫秒数的字符串(默认为none
)。在此时间间隔内,只有最新的请求实际上被发送到服务器,其余的请求被忽略。换句话说,JSF 将等待n毫秒,直到最新的 AJAX 请求被执行。默认情况下,它不会等待。
下面是一个使用默认delay
属性和显式延迟 1000 毫秒的示例。为了突出延迟效果,我们构建了一个简单的应用程序,该应用程序在keyup
事件上发送 AJAX 请求(提交输入文本值),并等待服务器响应建议文本。在下面的屏幕截图中,您可以比较输入的键数,直到服务器响应第一个建议文本。在这两种情况下,这是第一个触发的 AJAX 请求。很明显,在第二种情况下,由于在 1000 毫秒范围内触发,因此没有发送七个请求(按键)。一般来说,每次输入新键时,都会删除之前的 AJAX 请求,只考虑最后一个请求。
完整的应用程序可以在本章的代码包中找到,命名为ch7_14
。您还可以查看自定义 jsf.js部分,在那里您将看到delay
属性的作用。
注意
您可以通过将其值设置为none
来禁用delay
属性的效果。这是默认值。
显式加载 jsf.js
JSF 使用的 AJAX 机制封装在一个名为jsf.js
的 JavaScript 文件中。此文件位于javax.faces
库中。当我们使用<f:ajax>
时,此文件在幕后自动加载,无需任何明确要求。
然而,可以使用以下任何一种方法显式加载jsf.js
:
-
使用如下
<h:outputScript>
组件:<h:outputScript name="jsf.js" library="javax.faces" target="head"/>
-
使用如下
@ResourceDependency
关键字:@ResourceDependency(name="jsf.js" library="javax.faces" target="head")
专注于<h:outputScript>
,您可以将 AJAX 附加到组件,如下面的示例代码所示:
<h:form prependId="false">
<h:outputScript name="jsf.js" library
="javax.faces" target="head"/>
<h:inputText id="nameInId" value="#{ajaxBean.name}"/>
<h:outputText id="nameOutId" value="#{ajaxBean.name}"/>
<h:commandButton id="submit" value="Send"
action="#{ajaxBean.upperCaseAction()}"
onclick="jsf.ajax.request(this, event, {execute:'nameInId',render:'nameOutId'});
return false;" />
</h:form>
在jsf.js
中定义的jsf.ajax.request
方法能够处理 AJAX 请求。它接受以下三个参数:
-
source
:这是触发 AJAX 请求的 DOM 元素(例如<h:commandButton>
、<h:commandLink>
等)(这是一个必填参数) -
event
:这是一个可选参数,表示触发请求的 DOM 事件 -
options
:这是一个可选参数,可以包含以下值:execute
、render
、onevent
、onerror
、delay
和params
。
显式加载jsf.js
文件的完整应用程序可在本章的代码包中找到,命名为ch7_15
。
描述 params 值
虽然execute
、render
、delay
、onevent
和onerror
值在前面章节中非常熟悉,但params
值是新的,所以让我们关注一下。params
值实际上是一个对象,允许我们向请求中添加补充参数。
例如,以下代码是将 JavaScript JSON 对象发送到管理员的优雅解决方案。代码如下:
<script type="text/javascript">
var myJSONObject =
[{
"name": "Rafael",
"surname": "Nadal",
"age": 27,
"isMarried": false,
"address": {
"city": " Mallorca",
"country": "Spain"
},
"websites": ["http://www.rafaelnadal.com",
"http://rafaelnadalfans.com/"]
},
...
}]
</script>
...
<h:form prependId="false">
<h:outputScript name="jsf.js" library="javax.faces" target="head"/>
Data type (e.g. JSON): <h:inputText id="typeInId"
value="#{ajaxBean.type}"/>
<h:commandButton id="submit" value="Send"
action="#{ajaxBean.processJSONAction()}"
onclick='jsf.ajax.request(this, event, {execute:
"typeInId", render: "typeOutId playersId", params: JSON.stringify(myJSONObject)});
return false;' />
<h:outputText id="typeOutId" value="#{ajaxBean.type}"/>
<h:dataTable id="playersId" value="#{ajaxBean.players}" var="t">
...
</h:dataTable>
</h:form>
在服务器端,params
值如下在请求参数映射中可用:
FacesContext facesContext = FacesContext.getCurrentInstance();
String json = facesContext.getExternalContext().
getRequestParameterMap().get("params");
JsonArray personArray;
try (JsonReader reader = Json.createReader(new StringReader(json))) {
personArray = reader.readArray();
}
...
完整的应用程序可以在本章的代码包中找到,名称为ch7_16
。
非 UICommand 组件和 jsf.ajax.request
<f:ajax>
标签比jsf.ajax.request
更受欢迎。这是绝对正常的,因为<f:ajax>
在上下文中更自然,且使用和理解起来更加容易。此外,<f:ajax>
支持listener
属性,允许我们在<f:ajax>
标签嵌套在其他组件而不是UICommand
时调用服务端方法。默认情况下,jsf.ajax.request
无法做到这一点!
例如,假设我们有一个表格(<h:dataTable>
),它显示包含多个网球运动员的Map
对象(Map
键是整数类型:1
、2
、3
、... n,Map
值是运动员姓名):
private Map<Integer, String> myMap = new HashMap<>();
...
myMap.put(1, "Nadal Rafael");
myMap.put(2, "Federer Roger");
...
接下来,我们想要添加一个标记为删除的列,其中包含每行的删除图标,如下面的截图所示:
我们希望捕获客户端的onclick
事件,并使用jsf.ajax.request
为每个图标触发 AJAX 请求。想法是将玩家编号(1
、2
、3
、... n)发送到名为deletePlayerAction
的服务端方法。此方法将从Map
对象中查找并删除记录,当表格重新渲染时,相应的行将消失。因此,代码可以写成如下:
<h:form prependId="false">
<h:outputScript name="jsf.js" library="javax.faces" target="head"/>
<h:dataTable id="playersTableId"
value="#{ajaxBean.myMap.entrySet()}" var="t">
<h:column>
<f:facet name="header">
Delete
</f:facet>
<h:graphicImage value="./resources/default/imgs/delete.png"
onclick="jsf.ajax.request(this, event,
{execute: '@this', render: 'playersTableId',
params: '#{t.key}'});"/>
</h:column>
...
</h:dataTable>
</h:form>
我们可以使用params
值发送玩家编号以进行删除;这将通过请求参数映射可用。但这里的大问题是,我们无法调用服务端方法deletePlayerAction
,因为我们没有UICommand
组件(如按钮)且jsf.ajax.request
没有为options
参数提供listener
值。
好吧,解决方案来自 JSF 扩展,如 PrimeFaces(检查<p:remoteCommand>
)、OmniFaces(检查<o:commandScript>
)或 RichFaces(检查<a4j:jsfFunction>
),但你也可以通过纯 JSF 解决这个问题。首先,你需要添加一个不可见的UICommand
组件,例如添加到以下代码片段中的<h:commandLink>
标签:
<h:form prependId="false">
<h:commandLink id="commandDeleteId" immediate="true"
action="#{ajaxBean.deletePlayerAction()}"
style='display: none;'/>
<h:outputScript name="jsf.js" library="javax.faces" target="head"/>
接下来,我们将 AJAX 请求绑定到这个UICommand
组件,如下面的代码片段所示:
<h:graphicImage value="./resources/default/imgs/delete.png"
onclick="jsf.ajax.request('commandDeleteId', event, {'javax.faces.behavior.event': 'action',
execute: '@this', render: 'playersTableId', params: '#{t.key}'});"/>
在此时,当我们点击删除图标时,将执行服务端方法。此方法的代码相当简单,如下所示:
public void deletePlayerAction() {
FacesContext facesContext = FacesContext.getCurrentInstance();
String nr = facesContext.getExternalContext().
getRequestParameterMap().get("params");
if(nr!=null){
myMap.remove(Integer.valueOf(nr));
}
}
完成!完整的应用程序可以在本章的代码包中找到,名称为ch7_17
。
当然,正如章节名称所暗示的,这是一个使用jsf.ajax.request
的示例,但并不是解决此场景的最佳方案。尽管如此,对此问题有简单的解决方案,例如使用与图标结合的<h:commandLink>
标签并将链接 ajax 化(由 Michael Muller 在blog.mueller-bruehl.de/tutorial-web-development/
提出),以下代码片段展示了这种方法:
<h:form id="playersFormId">
<h:dataTable id="playersTableId"
value="#{ajaxBean.myMap.entrySet()}" var="t">
<h:column>
<f:facet name="header">Delete</f:facet>
<h:commandLink id="commandDeleteId" immediate="true"
action="#{ajaxBean.deletePlayerAction(t.key)}">
<f:ajax render="playersFormId:playersTableId"/>
<h:graphicImage value=
"#{resource['default:imgs/delete.png']}"/>
</h:commandLink>
</h:column>
...
完整的示例可以在本章的代码包中找到,命名为ch7_18
。
定制 jsf.js
明确加载jsf.js
的最大优点是我们可以通过修改默认代码来自定义 AJAX 机制。首先,我们需要将默认的jsf.js
文件隔离到单独的位置——你可以在网页文件夹中的resources/default/js
文件夹中轻松保存它。之后,你可以编辑 JavaScript 文件并执行所需的修改。当然,只有在你真正了解你在做什么的情况下才修改此代码,因为你可能会引起不期望的问题!除非你真的需要,否则不建议修改代码。
例如,我们可以修改 Mojarra 的jsf.js
代码,以了解 AJAX 队列是如何工作的。更确切地说,为了了解根据delay
值,请求是如何添加到队列中和从队列中移除的,请执行以下步骤:
-
在
jsf.js
中找到enqueue
函数。这个函数被 JSF 调用,用于将 AJAX 请求添加到队列中:this.enqueue = function enqueue(element) { // Queue the request queue.push(element); };
-
修改这个函数以调用一个 JavaScript 自定义函数,并将 AJAX 队列传递给它:
this.enqueue = function enqueue(element) { // Queue the request queue.push(element); monitorQueue(queue); };
-
在
dequeue
函数中也做同样的事情。这个函数被 JSF 调用,用于从队列中移除 AJAX 请求:this.dequeue = function dequeue() { ... // return the removed element try { return element; } finally { element = null; // IE 6 leak prevention } };
-
修改这个函数以调用相同的 JavaScript 自定义函数:
this.dequeue = function dequeue() { ... monitorQueue(queue); // return the removed element try { return element; } finally { element = null; // IE 6 leak prevention } };
到目前为止,每当 AJAX 请求被添加/移除到队列中时,都会调用一个 JavaScript 自定义函数,并将当前队列传递进去。队列中的每个条目都是一个 AJAX 请求;因此,我们可以遍历队列并提取有关每个请求的信息:
<script type="text/javascript">
function monitorQueue(q) {
document.getElementById("ajaxqueueId").innerHTML = "";
if (q.length > 0)
{
//<![CDATA[
var report = "";
document.getElementById("ajaxqueueId").innerHTML =
"<b>TOTAL REQUESTS: </b>" + q.length + "<hr/>";
for (var i = 0; i < q.length; i++) {
var request = q[i];
report += (i + 1) + ".<b>Request Type:</b> " + request.xmlReq + " <b>Source Id:</b> " + request.context.sourceid + " <b>URL: </b> " + request.url + " <b>Taken Off Queue ?</b>: " + request.fromQueue + "<hr/>";
}
document.getElementById("ajaxqueueId").innerHTML += report;
//]]>
}
}
</script>
每个请求对象都有一套属性,你可以在以下代码中轻松看到(这是直接从jsf.js
源代码中提取的):
var AjaxEngine = function AjaxEngine(context) {
var req = {}; // Request Object
req.url = null; // Request URL
req.context = context; // Context of request and response
req.context.sourceid = null; // Source of this request
req.context.onerror = null; // Error handler for request
req.context.onevent = null; // Event handler for request
req.xmlReq = null; // XMLHttpRequest Object
req.async = true; // Default - Asynchronous
req.parameters = {}; // Parameters For GET or POST
req.queryString = null; // Encoded Data For GET or POST
req.method = null; // GET or POST
req.status = null; // Response Status Code From Server
req.fromQueue = false; // Indicates if the request was
taken off the queue
...
现在你需要做的就是触发一些 AJAX 请求,并监控monitorQueue
函数生成的队列报告。正如你可以在以下代码中看到的那样,每个按钮都有一个不同的delay
值:
<h:body>
<h:outputScript name="js/jsf.js" library="default" target="head"/>
<hr/>
MONITOR AJAX QUEUE
<hr/>
<h:form prependId="false">
<h:commandButton id="button_1_Id" value="Send 1 (no delay)"
action="#{ajaxBean.ajaxAction()}" onclick='jsf.ajax.request(this,
event, {execute: "@this", render: "@this"});
return false;' />
<h:commandButton id="button_2_Id" value="Send 2 (delay:600)"
action="#{ajaxBean.ajaxAction()}" onclick='jsf.ajax.request(this,
event, {delay: 600, execute: "@this", render: "@this"});
return false;' />
<h:commandButton id="button_3_Id" value="Send 3 (delay:1000)"
action="#{ajaxBean.ajaxAction()}" onclick='jsf.ajax.request(this,
event, {delay: 1000, execute: "@this", render: "@this"});
return false;' />
</h:form>
AJAX QUEUE CONTENT:
<div id="ajaxqueueId"></div>
</h:body>
如你所见,所有 AJAX 请求都引用了同一个服务器端方法ajaxAction
。这个方法可以通过为每个请求随机休眠一定数量的毫秒来轻松模拟一些业务逻辑,如下面的代码所示:
public void ajaxAction() {
Random rnd = new Random();
int sleep=1000 + rnd.nextInt(4000);
try {
//sleep between 1 and 5 seconds
Thread.sleep(sleep);
} catch (InterruptedException ex) {
Logger.getLogger(AjaxBean.class.getName()).
log(Level.SEVERE,null, ex);
};
}
一旦你知道如何监控队列内容,你可以进一步修改它,例如通过仅排队某些请求,改变它们的执行优先级,接受队列中的有限条目等。
完整的应用程序可以在本章的代码包中找到,命名为ch7_19
。
AJAX 和进度条/指示器
在本地测试时,AJAX 请求似乎非常快,但在实际的生产环境中,它们不能这么快地解决,因为许多方面都会减慢这个过程(互联网连接速度、并发用户数量等)。
一种常见的做法是使用进度条/指示器来通知用户请求正在处理中,他/她应该等待直到收到 AJAX 响应并相应地渲染。例如,PrimeFaces 为上传任务(<p:fileUpload>
)提供了一个酷炫的确定进度条,并为任何其他 AJAX 请求提供了一个不确定的进度指示器(检查<p:ajaxStatus>
)。RichFaces 也有类似的功能。
在下一章中,您将看到如何实现上传任务的进度条。无需编写自定义组件,如<p:ajaxStatus>
,我们可以通过使用onevent
属性、data
对象和一小段 CSS,轻松实现进度指示器,如下面的代码所示:
<script type='text/javascript'>
function progressIndicator(data) {
if (data.status === "begin") {
document.getElementById("progressDivId").style.display = "block";
}
if (data.status === "complete") {
document.getElementById("progressDivId").style.display = "none";
}
}
</script>
...
<h:body>
<h:panelGrid columns="2">
<h:form>
<h:inputText id="nameInputId" value="#{ajaxBean.name}"/>
<h:commandButton value="Send" action="#{ajaxBean.ajaxAction()}">
<f:ajax onevent="progressIndicator" execute ="nameInputId"
render="nameOutputId"/>
</h:commandButton>
<h:outputText id="nameOutputId" value="#{ajaxBean.name}"/>
</h:form>
<div id="progressDivId" style="display:none;">
<img src="img/ajax.gif"/>
</div>
</h:panelGrid>
</h:body>
在下面的屏幕截图中,您可以看到本章代码包中名为ch7_20
的完整应用程序的运行示例:
概述
在本章中,我们介绍了 JSF 2.2 核心的 AJAX 支持。除了使用render
、execute
、listener
和其他属性等常见任务外,您还学习了如何使用 JSF 2.2 流程作用域进行 AJAX,如何使用 JSF 2.2 的delay
属性,以及如何在验证错误后使用新的 JSF 2.2 resetValues
属性和<f:resetValues>
标签更新输入字段。此外,您还看到了如何使用 AJAX 进行回发,如何确定请求是 AJAX 还是非 AJAX,自定义 jsf.js,如何编写进度条/指示器,如何创建取消/清除按钮,如何监控 AJAX 队列等。
总结来说,JSF 框架(包括主要扩展,如 PrimeFaces、OmniFaces、RichFaces、ICEfaces 等)具有最全面且易于使用的 AJAX 功能。
欢迎您在下一章中,我们将介绍 JSF 2.2 对 HTML5 的支持以及新的上传机制。
第八章. JSF 2.2 – HTML5 和上传
本章可以分为两部分来阅读。第一部分将介绍 JSF 2.2 对 HTML5 的支持,而第二部分将讨论 JSF 2.2 的新上传组件。显然,这两部分并不相关,但正如您将看到的,JSF 2.2 的上传组件可以通过 HTML5 特性和新的透传属性来增强,这些新的透传属性对于扩展 JSF 2.2 的上传组件以支持 HTML5 上传组件功能非常有帮助。
使用 HTML5 和 JSF 2.2 一起工作
所有参与网络应用开发的人都热衷于探索和使用 HTML5,它带来了一系列新的组件和特性,如<audio>
、<video>
、<keygen>
等。从版本 2.2 开始,JSF 开发者可以使用以下方式与 HTML5 交互:
-
透传属性
-
透传元素(HTML 友好标记)
注意
虽然透传元素和透传属性受到 HTML5 的启发,但它们是 JSF 元素,也可以与其他 HTML 版本一起使用。
这些机制是编写自定义渲染套件的替代方案。这是一个很好的解决方案,因为 HTML5 处于发展阶段,这意味着编写和调整渲染套件以适应持续的 HTML5 变化可能是一个真正的挑战。
如果您想在 JSF 2.0 中使用 HTML5,那么您需要编写自定义渲染套件以支持新的组件和属性。
透传属性
从 JSF 2.2 开始,我们有在服务器端由 JSF 组件处理的属性和在运行时在客户端处理的透传属性。
一个方便的 HTML5 元素,可以用来展示透传属性的是<input>
元素。在新的支持特性中,我们有type
属性的新值(例如,email
、tel
、color
和reset
)以及新的属性placeholder
(用作空字段提示的文本)。
在纯 HTML5 中,这样的元素可以如下所示:
<input placeholder="Enter player e-mail" type="email">
可以通过五种不同的方式使用透传属性获得相同的结果:
-
将透传属性放置在新命名空间
http://xmlns.jcp.org/jsf/passthrough
中(任何 JSF 开发者都熟悉命名空间和前缀元素。使用此命名空间或前缀属性没有技巧)。让我们看看如何使用 JSF 透传属性获取前面的 HTML5 元素,如下所示:<html > ... <h:body> <h:inputText value="#{playersBean.email}" f5:type="email" f5:placeholder="Enter player e-mail"/> ...
注意
当这本书被编写时,关于此命名空间的适当前缀仍存在争议。最初选择了
p
,但这个前缀被认为是 PrimeFaces 的前缀;因此,必须使用另一个前缀。所以,当您阅读这本书时,请随意将这里使用的f5
(此处使用)替换为赢得这场辩论并变得更受欢迎的前缀。 -
使用嵌套在
<h:inputText>
中的<f:passThroughAttribute>
,如下所示:<h:inputText value="#{playersBean.email}"> <f:passThroughAttribute name="placeholder" value="Enter player e-mail" /> <f:passThroughAttribute name="type" value="email" /> </h:inputText>
-
透传属性也可能来自一个管理 Bean。将它们放在一个
Map<String, String>
中,其中映射键是属性名称,映射值是属性值,如下所示:private Map<String, String> attrs = new HashMap<>(); ... attrs.put("type", "email"); attrs.put("placeholder", "Enter player e-mail");
此外,使用以下代码中的
<f:passThroughAttributes>
标签:<h:inputText value="#{playersBean.email}"> <f:passThroughAttributes value="#{playersBean.attrs}" /> </h:inputText>
-
使用表达式语言 3(Java EE 7 的一部分),也可以直接定义多个属性,如下所示(实际上,您可以通过 EL 3 定义一个
Map<String, String>
):<h:inputText value="#{playersBean.email}"> <f:passThroughAttributes value='#{{"placeholder":"Enter player e-mail", "type":"email"}}' /> </h:inputText>
完整的示例可以在本章的代码包中找到,名称为
ch8_1
。 -
透传属性可以通过编程方式添加。例如,您可以生成一个 HTML5 输入元素并将其添加到表单中,如下所示:
<h:body> <h:form id="playerForm"> ... </h:form> </h:body> ... FacesContext facesContext = FacesContext.getCurrentInstance(); UIComponent formComponent = facesContext.getViewRoot(). findComponent("playerForm"); HtmlInputText playerInputText = new HtmlInputText(); Map passThroughAttrs = playerInputText.getPassThroughAttributes(); passThroughAttrs.put("placeholder", "Enter player email"); passThroughAttrs.put("type", "email"); formComponent.getChildren().add(playerInputText); ...
完整的示例可以在本章的代码包中找到,名称为
ch8_1_2
。
透传元素
JSF 开发者将 HTML 代码隐藏在 JSF 组件后面。对于网页设计师来说,JSF 代码可能看起来相当奇怪,但生成的 HTML 更熟悉。为了更改生成的 HTML,网页设计师必须修改 JSF 代码,这对他们来说可能很困难。但 JSF 2.2 提供了友好的 HTML5 标记,称为 透传元素。使用此功能,网页设计师可以编写纯 HTML 代码,JSF 开发者可以添加/替换必要的属性,并将 HTML 元素链接到服务器端。如果这些属性位于 xmlns.jcp.org/jsf
命名空间中,JSF 会识别这些属性。例如,我们可以编写一个没有任何 JSF 标签的 JSF 页面,如下所示:
<html
>
<head jsf:id="head">
<title></title>
</head>
<body jsf:id="body">
<form jsf:id="form">
Name:<input type="text" jsf:value="#{playersBean.playerName}"/>
Surname:<input type="text" jsf:value="#{playersBean.playerSurname}"/>
<button jsf:action="#{playersBean.playerAction()}">Show</button>
</form>
</body>
</html>
注意
JSF 会扫描 HTML 元素中命名空间 xmlns.jcp.org/jsf
的属性。对于这样的元素,JSF 将确定元素类型,并将相应的 JSF 组件添加进去(例如,对于 <head>
使用 <h:head>
,对于 <input>
使用 <h:inputText>
)。JSF 将这些组件添加到组件树中,并将它们作为 HTML 代码渲染到客户端。这个 JSF 组件将与特定元素关联,并接收作为“正常”属性或透传属性传递的属性,具体取决于它们的来源。JSF 组件与 HTML 元素之间的对应关系可以在 docs.oracle.com/javaee/7/api/javax/faces/view/facelets/TagDecorator.html
找到。对于没有直接对应元素的 HTML 元素(例如 <div>
和 <span>
),JSF 将创建一个特殊的组件,组件家族,如 javax.faces.Panel
,以及渲染类型 javax.faces.passthrough.Element
,具体请参阅 docs.oracle.com/javaee/7/javaserverfaces/2.2/vdldocs/facelets/jsf/element.html
。
完整的示例可以在本章的代码包中找到,名称为 ch8_1_3
。
由于 JSF 用 JSF 组件替换了 HTML 元素,我们可以充分利用这些组件,这意味着我们可以像在 JSF 中一样使用它们。例如,我们可以使用验证器、转换器和 <f:param>
,如下所示:
<html
>
<head jsf:id="head">
<title></title>
</head>
<body jsf:id="body">
<form jsf:id="form">
Name:
<input type="text" jsf:value="#{playersBean.playerName}">
<f:validator validatorId="playerValidator"/>
</input>
<!-- or, like this -->
<input type="text" jsf:value="#{playersBean.playerName}"
jsf:validator="playerValidator"/>
Surname:
<input type="text" jsf:value="#{playersBean.playerSurname}">
<f:validator validatorId="playerValidator"/>
</input>
<!-- or, like this -->
<input type="text" jsf:value="#{playersBean.playerSurname}"
jsf:validator="playerValidator"/>
<button jsf:action="#{playersBean.playerAction()}">Show
<f:param id="playerNumber" name="playerNumberParam" value="2014"/>
</button>
</form>
</body>
</html>
完整的示例可在本章的代码包中找到,名称为 ch8_1_4
。
JSF 2.2 – HTML5 和 Bean Validation 1.1(Java EE 7)
Bean Validation 1.1(见 docs.oracle.com/javaee/7/tutorial/doc/partbeanvalidation.htm
)可以是 JSF 2.2/HTML5 应用程序中验证用户输入的完美选择。例如,我们可以在 PlayersBean
中验证提交的名字和姓氏,如下所示——我们不接受空值、空值或小于三个字符的值:
@Named
@RequestScoped
public class PlayersBean {
private static final Logger logger = Logger.getLogger(PlayersBean.class.getName());
@NotNull(message = "null/empty values not allowed in player name")
@Size(min = 3,message = "Give at least 3 characters for player name")
private String playerName;
@NotNull(message = "null/empty values not allowed in player surname")
@Size(min = 3,message = "Give at least 3 characters for player surname")
private String playerSurname;
...
如果你将以下上下文参数在 web.xml
中设置,JSF 可以将提交的空字符串值解释为 null
:
<context-param>
<param-name>
javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL
</param-name>
<param-value>true</param-value>
</context-param>
因此,在这种情况下,没有必要使用 <f:validator>
或 validator
属性。查看完整的名为 ch8_2
的应用程序。
注意
OmniFaces 提供了一个扩展 HTML5 特定属性支持的 HTML5 渲染器。你可能想查看 showcase.omnifaces.org/
。
JSF 2.2 上传功能
JSF 开发者已经期待了很长时间一个内置的上传组件。直到 JSF 2.2,解决方案包括使用 JSF 扩展,如 PrimeFaces、RichFaces 和第三方库,如 Apache Commons FileUpload。
JSF 2.2 包含一个专门用于上传任务的输入组件(它渲染一个类型为 file
的 HTML input
元素)。这个组件由 <h:inputFile>
标签表示,它可以像任何其他 JSF 组件一样使用。支持的所有属性列表可在 docs.oracle.com/javaee/7/javaserverfaces/2.2/vdldocs/facelets/h/inputFile.html
找到,但最重要的如下:
-
value
: 这代表要上传的文件,作为一个javax.servlet.http.Part
对象。 -
required
: 这是一个布尔值。如果它是true
,则用户必须提供一个值才能提交。 -
validator
: 这表示该组件的验证器。 -
converter
: 这表示该组件的转换器。 -
valueChangeListener
: 这表示当组件的值改变时将被调用的方法。
<h:inputFile>
组件基于 Servlet 3.0,它是从 Java EE 6 版本开始成为 Java EE 的一部分。Servlet 3.0 提供了一个基于 javax.servlet.http.Part
接口和 @MultipartConfig
注解的上传机制。一个简单的 Servlet 3.0 上传文件代码如下——请记住这个 servlet,因为我们将在本章的最后部分使用它:
@WebServlet(name = "UploadServlet", urlPatterns = {"/UploadServlet"})
@MultipartConfig(location="/folder", fileSizeThreshold=1024*1024,
maxFileSize=1024*1024*3, maxRequestSize=1024*1024*3*3)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
for (Part part : request.getParts()) {
String filename = "";
for (String s: part.getHeader("content-disposition").split(";")) {
if (s.trim().startsWith("filename")) {
filename = s.split("=")[1].replaceAll("\"", "");
}
}
part.write(filename);
}
}
}
注意
如果你快速查看 JSF 2.2 FacesServlet
的源代码,你会注意到它被特别注解了 @MultipartConfig
,以处理多部分数据。
如果你不太熟悉使用 Servlet 3.0 上传文件,那么你可以尝试在docs.oracle.com/javaee/6/tutorial/doc/glrbb.html
上的教程。
在客户端,你可以使用<form>
标签和 HTML5 的file
类型输入:
<form action="UploadServlet" enctype="multipart/form-data" method="POST">
<input type="file" name="file">
<input type="Submit" value="Upload File">
</form>
基本上,JSF 2.2 上传组件只是这个例子的包装。
一个简单的 JSF 2.2 上传示例
在本节中,我们将介绍 JSF 2.2 上传应用程序的基本步骤。即使这是一个简单的例子,你也会看到后续的例子都是基于这个例子。因此,为了使用<h:inputFile>
组件,你需要关注客户端和服务器端:
在客户端,我们需要执行以下步骤:
-
首先,
<h:form>
的编码必须设置为multipart/form-data
,这将帮助浏览器相应地构建POST
请求,如下面的代码所示:<h:form id="uploadFormId" enctype="multipart/form-data">
-
第二,
<h:inputFile>
必须配置为满足你的需求,在此,我们提供了一个简单的案例,如下所示:<h:inputFile id="fileToUpload" required="true" requiredMessage="No file selected ..." value="#{uploadBean.file}"/>
-
此外,你需要一个按钮(或链接)来启动上传过程,如下所示:
<h:commandButton value="Upload" action="#{uploadBean.upload()}"/>
可选地,你可以添加一些标签来处理上传消息,如下面的代码所示:
<h:messages globalOnly="true" showDetail="false"
showSummary="true" style="color:red"/>
<h:form id="uploadFormId" enctype="multipart/form-data">
<h:inputFile id="fileToUpload" required="true"
requiredMessage="No file selected ..."
value="#{uploadBean.file}"/>
<h:commandButton value="Upload" action="#{uploadBean.upload()}"/>
<h:message showDetail="false" showSummary="true"
for="fileToUpload" style="color:red"/>
</h:form>
在服务器端,我们需要执行以下步骤:
-
通常,
<h:inputFile>
的value
属性包含一个类型为#{
upload_bean.part_object}
的 EL 表达式。如果你将upload_bean替换为uploadBean
,将part_object替换为file
,你将获得#{uploadBean.file}
。file
对象用于在UploadBean
bean 中将上传的数据存储为javax.servlet.http.Part
实例。你所要做的就是以与其他属性相同的方式定义file
属性,如下面的代码所示:import javax.servlet.http.Part; ... private Part file; ... public Part getFile() { return file; } public void setFile(Part file) { this.file = file; } ...
注意
可以通过
Part
的getInputStream
方法读取上传的数据。 -
当点击标记为上传的按钮时,会调用
upload
方法。当这个方法被调用时,file
对象已经填充了上传的字节;因此,你可以将数据作为流(使用getInputStream
方法)获取并相应地处理它。例如,你可以使用Scanner
API 将数据提取到String
中,如下所示:public void upload() { try { if (file != null) { Scanner scanner = new Scanner(file.getInputStream(), "UTF-8").useDelimiter("\\A"); fileInString = scanner.hasNext() ? scanner.next() : ""; FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload successfully ended!")); } } catch (IOException | NoSuchElementException e) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload failed!")); } }
完整的应用程序包含在本章的代码包中,命名为ch8_3
。在这种情况下,上传的数据被转换为字符串并在日志中显示;因此,尝试上传可读的信息,如纯文本文件。
使用多个<h:inputFile>
元素
如果你问自己是否可以在<h:form>
表单中使用多个<h:inputFile>
元素,答案是肯定的。为每个<h:inputFile>
元素指定一个 ID,并将其与一个唯一的Part
实例关联。为了使用两个<h:inputFile>
元素,<h:form>
表单将变为以下代码——你可以轻松地扩展这个例子以适应三个、四个或更多的<h:inputFile>
元素:
<h:form id="uploadFormId" enctype="multipart/form-data">
<h:inputFile id="fileToUpload_1" required="true"
requiredMessage="No file selected ..."
value="#{uploadBean.file1}"/>
<h:inputFile id="fileToUpload_2" required="true"
requiredMessage="No file selected ..."
value="#{uploadBean.file2}"/>
...
<h:message showDetail="false" showSummary="true"
for="fileToUpload_1" style="color:red"/>
<h:message showDetail="false" showSummary="true"
for="fileToUpload_2" style="color:red"/>
...
<h:commandButton value="Upload" action="#{uploadBean.upload()}"/>
</h:form>
现在,在服务器端,你需要两个Part
实例,定义如下:
...
private Part file1;
private Part file2;
...
//getter and setter for both, file1 and file2
...
在upload
方法中,你需要处理两个Part
实例:
...
if (file1 != null) {
Scanner scanner1 = new Scanner(file1.getInputStream(),
"UTF-8").useDelimiter("\\A");
fileInString1 = scanner1.hasNext() ? scanner1.next() : "";
FacesContext.getCurrentInstance().addMessage(null, new
FacesMessage("Upload successfully ended for file 1!"));
}
if (file2 != null) {
Scanner scanner2 = new Scanner(file2.getInputStream(),
"UTF-8").useDelimiter("\\A");
fileInString2 = scanner2.hasNext() ? scanner2.next() : "";
FacesContext.getCurrentInstance().addMessage(null, new
FacesMessage("Upload successfully ended for file 2!"));
}
...
完成!完整的应用程序包含在本章的代码包中,命名为ch8_4
。
提取要上传的文件信息
文件名、大小和内容类型是在上传文件时最常见的信息类型。在 JSF 中,这些信息在客户端和服务器端都是可用的。让我们考虑以下<h:inputFile>
元素:
<h:form id="formUploadId" enctype="multipart/form-data">
<h:inputFile id="fileToUpload" value="#{uploadBean.file}"
required="true" requiredMessage="No file selected ...">
...
</h:inputFile>
</h:form>
现在你会看到如何提取上传选择的文件的信息。
在客户端,我们需要执行以下步骤之一:
-
在客户端,可以通过 JavaScript 函数提取文件名、大小(以字节为单位)和内容类型,如下所示:
var file = document.getElementById('formUploadId:fileToUpload').files[0]; ... alert(file.name); alert(file.size); alert(file.type);
-
另一种方法是使用 JSF 页面中的 EL,如下所示(当然,文件上传后这才会工作):
// the id of the component, formUploadId:fileToUpload #{uploadBean.file.name} // the uploaded file name #{uploadBean.file.submittedFileName} // the uploaded file size #{uploadBean.file.size} // the uploaded file content type #{uploadBean.file.contentType}
在服务器端,我们需要执行以下步骤之一:
-
在服务器端,可以通过
Part
接口的几个方法提取文件名、大小(以字节为单位)和内容类型,如下所示:... private Part file; ... System.out.println("File component id: " + file.getName()); System.out.println("Content type: " + file.getContentType()); System.out.println("Submitted file name:" + file.getSubmittedFileName()); System.out.println("File size: " + file.getSize()); ...
注意
如果这个方法返回的字符串代表整个路径而不是文件名,那么你必须将文件名作为这个字符串的子字符串来隔离。
-
文件名也可以通过以下代码从
content-disposition
头中获取:private String getFileNameFromContentDisposition(Part file) { for (String content:file.getHeader("content-disposition").split(";")) { if (content.trim().startsWith("filename")) { return content.substring(content.indexOf('=') + 1).trim().replace("\"", ""); } } return null; }
以下截图显示了
content-disposition
头的示例:
如果你检查POST
请求(你可以使用 Firebug 或其他专用工具来做这件事),这将非常容易理解。在上面的截图中,你可以看到getFileNameFromContentDisposition
方法中描述的相关请求片段。
完整的应用程序包含在本章的代码包中,命名为ch8_5
。
将上传数据写入磁盘
在前面的例子中,上传的数据被转换为String
并在控制台上显示。通常,当你上传文件时,你希望将其内容保存到磁盘上的特定位置(比如说,D:\files
文件夹)。为此,你可以使用FileOutputStream
,如下所示:
try (InputStream inputStream = file.getInputStream();
FileOutputStream outputStream = new FileOutputStream("D:" +
File.separator + "files" + File.separator + getSubmittedFileName())) {
int bytesRead = 0;
final byte[] chunck = new byte[1024];
while ((bytesRead = inputStream.read(chunck)) != -1) {
outputStream.write(chunck, 0, bytesRead);
}
FacesContext.getCurrentInstance().addMessage(null, new
FacesMessage("Upload successfully ended!"));
} catch (IOException e) {
FacesContext.getCurrentInstance().addMessage(null, new
FacesMessage("Upload failed!"));
}
注意
如果你想要缓冲 I/O,那么在你的代码中添加BufferedInputStream
和BufferedOutputStream
。
完整的应用程序包含在本章的代码包中,命名为ch8_6
。如果你更喜欢从content-disposition
头中获取文件名,那么最好检查应用程序ch8_7
。
另一种方法是通过使用Part.write
方法。在这种情况下,你必须通过<multipart-config>
标签(docs.oracle.com/javaee/7/tutorial/doc/servlets011.htm
)指定文件应该保存的位置。此外,你可以设置最大文件大小、请求大小和文件大小阈值;这些配置应该添加到web.xml
中,如下所示:
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<multipart-config>
<location>D:\files</location>
<max-file-size>1310720</max-file-size>
<max-request-size>20971520</max-request-size>
<file-size-threshold>50000</file-size-threshold>
</multipart-config>
</servlet>
注意
如果你没有指定位置,将使用默认位置。默认位置是""。
上传的文件将被保存在Part.write
方法传递的名称下指定的位置,如下面的代码所示:
try {
file.write(file.getSubmittedFileName());
FacesContext.getCurrentInstance().addMessage(null, new
FacesMessage("Upload successfully ended!"));
} catch (IOException e) {
FacesContext.getCurrentInstance().addMessage(null, new
FacesMessage("Upload failed!"));
}
完整的应用程序包含在本章的代码包中,并命名为ch8_8
。
上传验证器
在大多数情况下,你需要根据某些约束来限制用户上传。通常,你会限制文件名长度、文件大小和文件内容类型。例如,你可能想拒绝以下内容:
-
文件名超过 25 个字符的文件
-
不是 PNG 或 JPG 图像的文件
-
大于 1 MB 大小的文件
为了这个,你可以编写一个 JSF 验证器,如下所示:
@FacesValidator
public class UploadValidator implements Validator {
private static final Logger logger =
Logger.getLogger(UploadValidator.class.getName());
@Override
public void validate(FacesContext context, UIComponent component,
Object value) throws ValidatorException {
Part file = (Part) value;
//VALIDATE FILE NAME LENGTH
String name = file.getSubmittedFileName();
logger.log(Level.INFO, "VALIDATING FILE NAME: {0}", name);
if (name.length() == 0) {
FacesMessage message = new FacesMessage("Upload Error: Cannot
determine the file name !");
throw new ValidatorException(message);
} else if (name.length() > 25) {
FacesMessage message = new FacesMessage("Upload Error:
The file name is to long !");
throw new ValidatorException(message);
}
//VALIDATE FILE CONTENT TYPE
if ((!"image/png".equals(file.getContentType())) &&
(!"image/jpeg".equals(file.getContentType()))) {
FacesMessage message = new FacesMessage("Upload Error: Only images can be uploaded (PNGs and JPGs) !");
throw new ValidatorException(message);
}
//VALIDATE FILE SIZE (not bigger than 1 MB)
if (file.getSize() > 1048576) {
FacesMessage message = new FacesMessage("Upload Error: Cannot
upload files larger than 1 MB !");
throw new ValidatorException(message);
}
}
}
接下来,将验证器添加到<h:inputFile>
元素中,如下所示:
<h:inputFile id="fileToUpload" required="true"
requiredMessage="No file selected ..."
value="#{uploadBean.file}">
<f:validator validatorId="uploadValidator" />
</h:inputFile>
现在,只有符合我们约束条件的文件才会被上传。对于每个被拒绝的文件,你将看到一个信息消息,它会指示文件名或其大小是否过大,或者文件是否是 PNG 或 JPG 图像。
完整的应用程序包含在本章的代码包中,并命名为ch8_9
。
AJAX 化上传
JSF 上传可以通过结合<h:inputFile>
标签与<f:ajax>
或<h:commandButton>
标签(上传初始化)与<f:ajax>
来利用 AJAX 机制。在第一种情况下,一个常见的 AJAX 化上传将类似于以下代码:
<h:form enctype="multipart/form-data">
<h:inputFile id="fileToUpload" value="#{uploadBean.file}"
required="true" requiredMessage="No file selected ...">
<!-- <f:ajax listener="#{uploadBean.upload()}"
render="@all"/> use @all in JSF 2.2.0 -->
<f:ajax listener="#{uploadBean.upload()}"
render="fileToUpload"/> <!-- works in JSF 2.2.5 -->
</h:inputFile>
<h:message showDetail="false" showSummary="true"
for="fileToUpload" style="color:red"/>
</h:form>
注意
渲染属性应该包含上传后需要重新渲染的组件的 ID。在 JSF 2.2.0 中,你需要使用@all
而不是 ID,因为后续版本中修复了一个相关的错误。例如,在 JSF 2.2.5 中,一切如预期工作。
完整的应用程序包含在本章的代码包中,并命名为ch8_10
。
在第二种情况下,将<f:ajax>
放置在<h:commandButton>
中,如下所示:
<h:form enctype="multipart/form-data">
<h:inputFile id="fileToUpload" value="#{uploadBean.file}"
required="true" requiredMessage="No file selected ..."/>
<h:commandButton value="Upload" action="#{uploadBean.upload()}">
<!-- <f:ajax execute="fileToUpload"
render="@all"/> use @all in JSF 2.2.0 -->
<f:ajax execute="fileToUpload"
render="fileToUpload"/> <!-- works in JSF 2.2.5 -->
</h:commandButton>
<h:message showDetail="false" showSummary="true"
for="fileToUpload" style="color:red"/>
</h:form>
完整的应用程序包含在本章的代码包中,并命名为ch8_11
。
带预览上传图像
上传组件的一个不错特性是它们允许我们在上传之前预览图像。在下面的屏幕截图中,你可以看到我们将要开发的内容:
因此,当用户浏览图像时,你需要进行后台自动 AJAX 上传,这样用户在选择本地机器上的图像后可以立即看到图像预览。由 AJAX 生成的 POST
请求将填充服务器端的 Part
对象(我们可以称之为 file
)。当 AJAX 完成时,你需要重新渲染一个能够显示图像的组件,例如 <h:graphicImage>
。该组件将使用 GET
请求调用一个 servlet。负责上传的管理 Bean 应该是会话作用域的;因此,servlet 能够从会话中提取 Bean 实例并使用代表图像的 file
对象。现在,servlet 可以直接将图像字节传递给响应输出流,或者创建图像的缩略图并发送少量字节。此外,当用户点击初始化上传的按钮时,你需要将文件对象写入磁盘。
这是主要思想。接下来,你将实现它,并添加一些验证功能、一个取消按钮,以及一些显示在预览旁边的图像信息。
为了实现这一点,你需要执行以下步骤:
-
根据以下内容编写基于 AJAX 的自动上传:
<h:form enctype="multipart/form-data"> <h:inputFile id="uploadFileId" value="#{uploadBean.file}" required="true" requiredMessage="No file selected ..."> <f:ajax render=":previewImgId :imgNameId :uploadMessagesId" listener="#{uploadBean.validateFile()}"/> </h:inputFile> </h:form>
-
AJAX 将调用
validateFile
方法。这个服务器端方法能够验证文件名、长度、大小和内容类型。validateFile
方法定义如下:... private Part file; ... public void validateFile() { //VALIDATE FILE NAME LENGTH String name = file.getSubmittedFileName(); if (name.length() == 0) { resetFile(); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload Error: Cannot determine the file name !")); } else if (name.length() > 25) { resetFile(); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload Error: The file name is to long !")); } else //VALIDATE FILE CONTENT TYPE if ((!"image/png".equals(file.getContentType())) && (!"image/jpeg".equals(file.getContentType()))) { resetFile(); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload Error: Only images can be uploaded (PNGs and JPGs) !")); } else //VALIDATE FILE SIZE (not bigger than 1 MB) if (file.getSize() > 1048576) { resetFile(); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload Error: Cannot upload files larger than 1 MB !")); } }
-
如果违反了约束,则调用
resetFile
方法。这是一个简单的将文件对象重置到其初始状态的方法。此外,它调用delete
方法,该方法删除文件项的底层存储(包括磁盘上的临时文件)。resetFile
方法定义如下:public void resetFile() { try { if (file != null) { file.delete(); } } catch (IOException ex) { Logger.getLogger(UploadBean.class.getName()). log(Level.SEVERE, null, ex); } file = null; }
-
当 AJAX 请求完成时,它将重新渲染具有以下 ID 的组件:
previewImgId
、imgNameId
和uploadMessagesId
。以下代码揭示了具有previewImgId
和imgNameId
ID 的组件——这里uploadMessagesId
ID 对应于一个<h:messages>
组件:... <h:panelGrid columns="2"> <h:graphicImage id="previewImgId" value="/PreviewServlet/#{header['Content-Length']}" width="#{uploadBean.file.size gt 0 ? 100 : 0}" height="#{uploadBean.file.size gt 0 ? 100 : 0}"/> <h:outputText id="imgNameId" value="#{uploadBean.file.submittedFileName} #{empty uploadBean.file.submittedFileName ? '' : ','} #{uploadBean.file.size} #{uploadBean.file.size gt 0 ? 'bytes' : ''}"/> </h:panelGrid> ...
-
<h:graphicImage>
的值访问PreviewServlet
。这个 servlet 可以通过响应输出流提供图像以供预览。为了避免缓存机制,你需要提供一个带有随机部分的 URL(请求内容长度可能是一个方便的选择)。这种技术将每次都加载正确的图像,而不是为所有请求加载相同的图像。servlet 的相关部分如下:protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //decorate with buffers if you need to OutputStream out = response.getOutputStream(); response.setHeader("Expires", "Sat, 6 May 1995 12:00:00 GMT"); response.setHeader("Cache-Control","no-store,no-cache,must-revalidate"); response.addHeader("Cache-Control", "post-check=0, pre-check=0"); response.setHeader("Pragma", "no-cache"); int nRead; try { HttpSession session = request.getSession(false); if (session.getAttribute("uploadBean") != null) { UploadBean uploadBean = (UploadBean) session.getAttribute("uploadBean"); if (uploadBean.getFile() != null) { try (InputStream inStream = uploadBean.getFile().getInputStream()) { byte[] data = new byte[1024]; while ((nRead =inStream. read(data, 0, data.length)) != -1) { out.write(data, 0, nRead); } } } } } finally { out.close(); } }
-
上述代码将发送上传图像的所有字节到响应输出流。一种常见的技术是将图像缩小以获得包含较少字节的缩略图。在 Java 中,可以通过多种方式缩放图像,但以下代码提供了一个快速的方法:
protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { OutputStream out = response.getOutputStream(); response.setHeader("Expires", "Sat, 6 May 1995 12:00:00 GMT"); response.setHeader("Cache-Control","no-store,no-cache,must-revalidate"); response.addHeader("Cache-Control", "post-check=0, pre-check=0"); response.setHeader("Pragma", "no-cache"); try { HttpSession session = request.getSession(false); if (session.getAttribute("uploadBean") != null) { UploadBean uploadBean = (UploadBean) session.getAttribute("uploadBean"); if (uploadBean.getFile() != null) { BufferedImage image = ImageIO.read(uploadBean.getFile().getInputStream()); BufferedImage resizedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); Graphics2D g = resizedImage.createGraphics(); g.drawImage(image, 0, 0, 100, 100, null); g.dispose(); ImageIO.write(resizedImage, "png", out); } } } finally { out.close(); } }
-
此外,您还需要添加两个按钮:一个标有上传的按钮,另一个标有取消的按钮。第一个按钮将初始化上传,第二个按钮将取消上传,如下面的代码所示:
<h:form> <h:commandButton value="Upload Image" action="#{uploadBean.saveFileToDisk()}"/> <h:commandButton value="Cancel" action="#{uploadBean.resetFile()}"/> </h:form>
-
当点击标有上传的按钮时,
saveFileToDisk
方法将保存上传的数据到磁盘,如下面的代码所示:public void saveFileToDisk() { if (file != null) { //decorate with buffers if you need too try (InputStream inputStream = file.getInputStream(); FileOutputStream outputStream = new FileOutputStream("D:" + File.separator + "files" + File.separator + getSubmittedFileName())) { int bytesRead; final byte[] chunck = new byte[1024]; while ((bytesRead = inputStream.read(chunck)) != -1) { outputStream.write(chunck, 0, bytesRead); } resetFile(); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload successfully ended!")); } catch (IOException e) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload failed!")); } } }
完成!完整的应用程序(无缩略图),可在本章的代码包中找到,命名为ch8_13
。带有缩略图的完整应用程序命名为ch8_12
。
验证过程可以从服务器端消除,也可以在客户端完成。这样的例子可以在本章的代码包中找到,命名为ch8_14
。JavaScript 代码相当简单,如下所示:
<script type="text/javascript">
function validateFile() {
// <![CDATA[
document.getElementById('formSaveId:uploadHiddenId').value = false;
document.getElementById('validationId').innerHTML = "";
var file= document.getElementById('formUploadId:fileToUpload').files[0];
document.getElementById('fileNameId').innerHTML =
"<b>File Name:</b> " + file.name;
if (file.size > 1048576)
fileSize = (Math.round(file.size * 100 /
(1048576)) / 100).toString() + 'MB';
else
fileSize = (Math.round(file.size * 100
/ 1024) / 100).toString() + 'KB';
document.getElementById('fileSizeId').innerHTML =
"<b>File Size:</b> " + fileSize;
document.getElementById('fileContentTypeId').innerHTML =
"<b>File Type:</b> " + file.type;
//VALIDATE FILE NAME LENGTH
if (file.name.length === 0) {
clearUploadField();
document.getElementById('validationId').innerHTML =
"<ul><li>Upload Error: Cannot determine the file name !</li></ul>";
return false;
}
if (file.name.length > 25) {
clearUploadField();
document.getElementById('validationId').innerHTML =
"<ul><li>Upload Error: The file name is to long !</li></ul>";
return false;
}
//VALIDATE FILE CONTENT TYPE
if (file.type !== "image/png" && file.type !== "image/jpeg") {
clearUploadField();
document.getElementById('validationId').innerHTML =
"<ul><li>Upload Error: Only images can be uploaded
(PNGs and JPGs) !</li></ul>";
return false;
}
//VALIDATE FILE SIZE (not bigger than 1 MB)
if (file.size > 1048576) {
clearUploadField();
document.getElementById('validationId').innerHTML =
"<ul><li>Upload Error: Cannot upload files
larger than 1 MB !</li></ul>";
return false;
}
document.getElementById('formSaveId:uploadHiddenId').value = true;
return true;
//]]>
}
function clearUploadField() {
document.getElementById('previewImgId').removeAttribute("src");
document.getElementById('imgNameId').innerHTML = "";
document.getElementById('uploadMessagesId').innerHTML = "";
var original = document.getElementById("formUploadId:fileToUpload");
var replacement = document.createElement("input");
replacement.type = "file";
replacement.id = original.id;
replacement.name = original.name;
replacement.className = original.className;
replacement.style.cssText = original.style.cssText;
replacement.onchange = original.onchange;
// ... more attributes
original.parentNode.replaceChild(replacement, original);
}
</script>
上传多个文件
默认情况下,JSF 2.2 不提供上传多个文件的支持,但通过一些调整,我们可以轻松实现这个目标。为了实现多文件上传,您需要关注两个方面,如下列所示:
-
使多文件选择成为可能
-
上传所有选定的文件
关于第一个任务,可以通过使用 HTML5 输入文件属性(multiple
)和 JSF 2.2 的透传属性功能来激活多重选择。当此属性存在且其值设置为multiple
时,文件选择器可以选择多个文件。因此,这个任务需要一些最小的调整:
<html
>
...
<h:form id="uploadFormId" enctype="multipart/form-data">
<h:inputFile id="fileToUpload" required="true" f5:multiple="multiple"
requiredMessage="No file selected ..." value="#{uploadBean.file}"/>
<h:commandButton value="Upload" action="#{uploadBean.upload()}"/>
</h:form>
第二个任务有点棘手,因为当选择多个文件时,JSF 会使用上传集中的每个文件覆盖先前的Part
实例。这是正常的,因为您使用的是Part
类型的对象,但您需要一个Part
实例的集合。解决这个问题需要我们关注文件组件的渲染器。这个渲染器被命名为FileRenderer
(TextRenderer
的扩展),而decode
方法的实现是我们问题的关键(粗体代码对我们非常重要),如下面的代码所示:
@Override
public void decode(FacesContext context, UIComponent component) {
rendererParamsNotNull(context, component);
if (!shouldDecode(component)) {
return;
}
String clientId = decodeBehaviors(context, component);
if (clientId == null) {
clientId = component.getClientId(context);
}
assert(clientId != null);
ExternalContext externalContext = context.getExternalContext();
Map<String, String> requestMap =
externalContext.getRequestParameterMap();
if (requestMap.containsKey(clientId)) {
setSubmittedValue(component, requestMap.get(clientId));
}
HttpServletRequest request = (HttpServletRequest)
externalContext.getRequest();
try {
Collection<Part> parts = request.getParts();
for (Part cur : parts) {
if (clientId.equals(cur.getName())) {
component.setTransient(true);
setSubmittedValue(component, cur);
}
}
} catch (IOException ioe) {
throw new FacesException(ioe);
} catch (ServletException se) {
throw new FacesException(se);
}
}
突出的代码会导致覆盖Part
问题,但您可以轻松地修改它,以提交一个Part
实例的列表而不是一个Part
实例,如下所示:
try {
Collection<Part> parts = request.getParts();
List<Part> multiple = new ArrayList<>();
for (Part cur : parts) {
if (clientId.equals(cur.getName())) {
component.setTransient(true);
multiple.add(cur);
}
}
this.setSubmittedValue(component, multiple);
} catch (IOException | ServletException ioe) {
throw new FacesException(ioe);
}
当然,为了修改此代码,您需要创建一个自定义文件渲染器,并在faces-config.xml
中正确配置它。
之后,您可以使用以下代码在您的 bean 中定义一个Part
实例的列表:
...
private List<Part> files;
public List<Part> getFile() {
return files;
}
public void setFile(List<Part> files) {
this.files = files;
}
...
列表中的每个条目都是一个文件;因此,您可以通过以下代码迭代列表来将它们写入磁盘:
...
for (Part file : files) {
try (InputStream inputStream = file.getInputStream(); FileOutputStream
outputStream = new FileOutputStream("D:" + File.separator + "files" + File.separator + getSubmittedFileName())) {
int bytesRead = 0;
final byte[] chunck = new byte[1024];
while ((bytesRead = inputStream.read(chunck)) != -1) {
outputStream.write(chunck, 0, bytesRead);
}
FacesContext.getCurrentInstance().addMessage(null, new
FacesMessage("Upload successfully ended: " +
file.getSubmittedFileName()));
} catch (IOException e) {
FacesContext.getCurrentInstance().addMessage(null, new
FacesMessage("Upload failed !"));
}
}
...
完整的应用程序可在本章的代码包中找到,命名为ch8_15
。
上传和不确定的进度条
当用户上传小文件时,这个过程发生得相当快;然而,当涉及到大文件时,可能需要几秒钟,甚至几分钟才能完成。在这种情况下,实现一个指示上传状态的进度条是一个好习惯。最简单的进度条被称为不确定进度条,因为它表明过程正在运行,但它不提供估计剩余时间或已处理字节数的信息。
为了实现进度条,您需要开发一个基于 AJAX 的上传。JSF AJAX 机制允许我们确定 AJAX 请求何时开始和何时完成。这可以在客户端实现;因此,可以使用以下代码轻松实现不确定进度条:
<script type="text/javascript">
function progressBar(data) {
if (data.status === "begin") {
document.getElementById("uploadMsgId").innerHTML="";
document.getElementById("progressBarId").
setAttribute("src", "./resources/progress_bar.gif");
}
if (data.status === "complete") {
document.getElementById("progressBarId").removeAttribute("src");
}
}
</script>
...
<h:body>
<h:messages id="uploadMsgId" globalOnly="true" showDetail="false"
showSummary="true" style="color:red"/>
<h:form id="uploadFormId" enctype="multipart/form-data">
<h:inputFile id="fileToUpload" required="true"
requiredMessage="No file selected ..." value="#{uploadBean.file}"/>
<h:message showDetail="false" showSummary="true"
for="fileToUpload" style="color:red"/>
<h:commandButton value="Upload" action="#{uploadBean.upload()}">
<f:ajax execute="fileToUpload" onevent="progressBar"
render=":uploadMsgId @form"/>
</h:commandButton>
</h:form>
<div>
<img id="progressBarId" width="250px;" height="23"/>
</div>
</h:body>
可能的输出如下:
完整的应用程序包含在本章的代码包中,命名为ch8_16
。
上传和确定进度条
确定进度条要复杂得多。通常,这样的进度条基于一个能够监控传输字节的监听器(如果您使用过 Apache Commons 的FileUpload
,您肯定有机会实现这样的监听器)。在 JSF 2.2 中,FacesServlet
被注解为@MultipartConfig
以处理多部分数据(上传文件),但它没有进度监听器接口。此外,FacesServlet
被声明为final
;因此,我们无法扩展它。
好吧,可能的解决方案受到这些方面的限制。为了在服务器端实现进度条,我们需要在单独的类(servlet)中实现上传组件并提供一个监听器。或者,在客户端,我们需要一个自定义的POST
请求来欺骗FacesServlet
,使其认为请求是通过jsf.js
格式化的。
在本节中,您将看到基于 HTML5 XMLHttpRequest Level 2(可以上传/下载Blob
、File
和FormData
流)的解决方案,HTML5 进度事件(对于上传,它返回已传输的总字节数和已上传的字节数),HTML5 进度条,以及自定义 Servlet 3.0。如果您不熟悉这些 HTML5 特性,那么您必须查阅一些专门的文档。
在熟悉了这些 HTML5 特性之后,理解以下客户端代码将会非常容易。首先,我们有以下 JavaScript 代码:
<script type="text/javascript">
function fileSelected() {
hideProgressBar();
updateProgress(0);
document.getElementById("uploadStatus").innerHTML = "";
var file = document.getElementById('fileToUploadForm:
fileToUpload').files[0];
if (file) {
var fileSize = 0;
if (file.size > 1048576)
fileSize = (Math.round(file.size * 100 / (1048576)) /
100).toString() + 'MB';
else
fileSize = (Math.round(file.size * 100 / 1024) /
100).toString() + 'KB';
document.getElementById('fileName').innerHTML = 'Name: ' +
file.name;
document.getElementById('fileSize').innerHTML = 'Size: ' +
fileSize;
document.getElementById('fileType').innerHTML = 'Type: ' +
file.type;
}
}
function uploadFile() {
showProgressBar();
var fd = new FormData();
fd.append("fileToUpload", document.getElementById('fileToUploadForm:
fileToUpload').files[0]);
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.addEventListener("load", uploadComplete, false);
xhr.addEventListener("error", uploadFailed, false);
xhr.addEventListener("abort", uploadCanceled, false);
xhr.open("POST", "UploadServlet");
xhr.send(fd);
}
function uploadProgress(evt) {
if (evt.lengthComputable) {
var percentComplete = Math.round(evt.loaded * 100 / evt.total);
updateProgress(percentComplete);
}
}
function uploadComplete(evt) {
document.getElementById("uploadStatus").innerHTML = "Upload
successfully completed!";
}
function uploadFailed(evt) {
hideProgressBar();
document.getElementById("uploadStatus").innerHTML = "The upload cannot be complete!";
}
function uploadCanceled(evt) {
hideProgressBar();
document.getElementById("uploadStatus").innerHTML = "The upload was
canceled!";
}
var updateProgress = function(value) {
var pBar = document.getElementById("progressBar");
document.getElementById("progressNumber").innerHTML=value+"%";
pBar.value = value;
}
function hideProgressBar() {
document.getElementById("progressBar").style.visibility = "hidden";
document.getElementById("progressNumber").style.visibility = "hidden";
}
function showProgressBar() {
document.getElementById("progressBar").style.visibility = "visible";
document.getElementById("progressNumber").style.visibility = "visible";
}
</script>
此外,我们还有使用前面 JavaScript 代码的上传组件:
<h:body>
<hr/>
<div id="fileName"></div>
<div id="fileSize"></div>
<div id="fileType"></div>
<hr/>
<h:form id="fileToUploadForm" enctype="multipart/form-data">
<h:inputFile id="fileToUpload" onchange="fileSelected();"/>
<h:commandButton type="button" onclick="uploadFile()" value="Upload" />
</h:form>
<hr/>
<div id="uploadStatus"></div>
<table>
<tr>
<td>
<progress id="progressBar" style="visibility: hidden;"
value="0" max="100"></progress>
</td>
<td>
<div id="progressNumber" style="visibility: hidden;">0 %</div>
</td>
</tr>
</table>
<hr/>
</h:body>
以下截图显示了可能的输出:
此解决方案背后的 servlet 是之前提到的UploadServlet
。完整的应用程序包含在本章的代码包中,命名为ch8_17
。
注意
对于多个文件上传和进度条,您可以扩展此示例,或者选择内置解决方案,例如 PrimeFaces Upload、RichFaces Upload 或 jQuery Upload 插件。
摘要
在本章中,您了解了如何通过 JSF 2.2 使用 pass-through 属性和 pass-through 元素技术来利用 HTML5。此外,在本章的第二部分,您还看到了如何使用新的 JSF 2.2 上传组件(简单上传、多文件上传、上传带预览的图片以及上传的不可确定/确定进度条)。
欢迎您在下一章中继续学习,我们将进一步探讨 JSF 2.2 的一个伟大特性,即无状态视图。
第九章. JSF 状态管理
通常,JSF 应用程序的性能与 CPU 内存、序列化/反序列化任务和网络带宽直接相关。当这些变量开始成为头痛的来源,或者出现ViewExpiredException
或NotSerializableException
类型的错误时,就是了解 JSF 管理视图状态功能及其如何精细调整以提高性能的时候了。因此,在本章中,我们将讨论 JSF 保存视图状态(JSF 的部分保存视图状态功能、JSF 在服务器/客户端保存视图状态、逻辑视图和物理视图等)以及 JSF 2.2 无状态视图。
JSF 保存视图状态
首先,你必须知道 JSF 使用ViewHandler
/StateManager
API 在请求之间保存和恢复视图状态。JSF 在其生命周期中这样做,视图状态在请求结束时保存在会话中(或在客户端机器上),并在请求开始时恢复。
JSF 使用这种技术是因为它需要在 HTTP 协议上保留视图状态,而 HTTP 协议是无状态的。由于 JSF 是有状态的,它需要保存视图的状态,以便在来自同一用户的多个请求上执行 JSF 生命周期。每个页面都有一个视图状态,它在客户端和服务器之间充当乒乓球。视图基本上是一个组件树,它可能在 HTTP GET 和 POST 请求期间动态更改(修改)。只有当组件树之前已保存并且完全能够提供所需信息时,每个请求才能成功通过 JSF 生命周期,也就是说,Faces Servlet 成功调用所需的视图处理实现来恢复或构建视图。因此,当组件树被程序性地更改(例如,从后端 bean 或静态组件)时,它不能从零开始成功重建(或重新构建)。唯一的解决方案是使用在渲染响应阶段保存的现有状态。尝试从头开始重建它将使程序性更改无效,因为它们将不再可用。
注意
请记住,组件树只是 UI 组件的层次结构和逻辑关系的手。视图状态维护树结构和组件状态(选中/未选中、启用/禁用等)。因此,组件树只包含通过 EL 表达式引用后端 bean 属性/操作的引用,并不存储模型值。
JSF 部分保存视图状态
从 JSF 2.0 开始,通过添加部分状态保存功能,管理状态的性能得到了显著提高。基本上,JSF 不会保存整个组件树,而只保存其中的一部分。显然,这将需要更少的内存。换句话说,这意味着现在,在恢复视图的每个请求中,JSF 将从头开始重新创建整个组件树,并从它们的标签属性初始化组件。这样,JSF 将只保存值得保存的东西。这些是容易变化的东西(例如,<h:form>
),不能从头开始重新创建,或者代表组件的内联细节。这些细节包括:动态(程序性)更改,这些更改会改变组件树;为某些组件确定的不同类型的值(通常在第一次回发时),以及已更改但尚未提交的组件的值(例如,移动滑块或勾选复选框)。另一方面,客户端无法更改的东西将不会被保存。
部分状态保存和树遍历
在 JSF 2.0 中,JSF 部分状态保存功能引发了一个问题,类似于 JSF 实现应该如何遍历组件树并询问它们的状态(部分)。JSF 2.1(以及更早的版本)的答案是针对这个特定实现的:Mojarra 使用了树遍历算法,而 MyFaces 使用了所谓的“面 + 子”遍历。但从技术上来说,这两种方法相当不同,因为 Mojarra 提供了一个可插拔的算法,而 MyFaces 则没有。此外,Mojarra 方法是在上下文中(在访问子组件之前,父组件可以选择使用上下文/作用域),而 MyFaces 方法遵循指针设计。此外,Mojarra 算法可以遍历虚拟组件。(这些组件是通过循环组件如 UIData
获得的。)另一方面,从保存状态的角度来看,使用上下文/作用域和循环虚拟组件是不理想的,即使影响遍历过程可能是主要和有用的。
为了解决这个问题,JSF 2.1 提供了一些提示,这些提示从 JSF 2.2 开始可能被认为是过时的。从 JSF 2.2 开始,树遍历完全能够实现部分状态保存;归功于 StateManagementStrategy.saveView
和 StateManagementStrategy.restoreView
方法。这两个方法旨在替换 StateManager
类中的对应方法,并且它们的实现现在是使用访问 API 的强制要求。(一个开始学习的好点可能是 UIComponent.visitTree
方法。)作为一个 JSF 开发者,你可能永远不会与这个特性交互,但为了完整性,了解它可能是个好主意。
JSF 在服务器或客户端保存视图状态
视图状态的保存可以在托管应用程序的服务器上完成,也可以在客户端机器上完成。我们可以通过向 web.xml
文件中添加名为 javax.faces.STATE_SAVING_METHOD
的上下文参数来轻松地在客户端和服务器之间进行选择。此方法的有效值可以是 server
或 client
,如下面的代码所示:
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>
从 JSF 2.2 开始,此上下文参数的值不区分大小写。
在服务器上保存状态意味着将其保存在一个具有特殊 ID 的会话中,这个 ID 被称为视图状态 ID,它引用存储在服务器内存中的状态。这作为名为 javax.faces.ViewState
的隐藏输入字段的值发送到客户端。这可以通过运行 ch9_1_1
应用程序来轻松测试,该应用程序产生包含此字段的 HTML 代码,如下面的截图所示:
如果状态保存在客户端,JSF 会将其存储为相同隐藏输入字段的值。这个值是一个表示状态序列化的 base64 加密字符串。运行 ch9_1_2
应用程序将产生以下输出:
指定视图状态将保存的位置是一件轻而易举的事情,但选择在客户端或服务器上保存视图状态可能是一个困难的抉择,因为每种方法都有其自身的优缺点。两者都有成本,每个人都希望支付更低的代价。选择客户端会增加网络流量,因为序列化的状态将为 javax.faces.ViewState
输入字段生成更大的值。此外,对视图状态进行编码/解码以及可能的越界攻击也是这种方法的重要缺点。另一方面,服务器使用更少的内存,因为会话中没有存储任何内容。此外,在客户端存储视图状态也将是一个防止服务器宕机时丢失状态以及防止会话过期或达到已打开视图的最大数量时发生的 ViewExpiredException
的好方法。在服务器上保存状态会产生相反的效果:网络流量更低,服务器使用的内存增加,服务器故障将导致状态丢失和可能的 ViewExpiredException
实例。
注意
通常,开发者更喜欢降低网络流量并在服务器上使用更多内存,因为内存容易提供给应用程序服务器。但这并不是一条规则;你只需考虑对你来说什么更便宜。一些重型基准测试也可以提供关于在客户端或服务器上存储状态的令人信服的指示。
为了做出正确的选择,不要忘记 JSF 2.0 默认带有部分状态保存,这将在javax.faces.ViewState
输入字段(客户端保存的状态)的大小减小或所需的内存减少中体现出来。你可以在web.xml
中添加以下context
参数来禁用部分状态保存:
<context-param>
<param-name>javax.faces.PARTIAL_STATE_SAVING</param-name>
<param-value>false</param-value>
</context-param>
对于一个简单的视觉测试,你可以选择在客户端保存状态并运行同一个应用两次(你可以使用名为ch9_1_2
的应用):第一次,启用部分状态保存,第二次,禁用它——下面的截图所示的结果不言自明:
此外,在同一个应用中,你可以为某些视图使用部分状态保存,而为其他视图使用完整状态保存。跳过javax.faces.PARTIAL_STATE_SAVING
上下文参数,并使用javax.faces.FULL_STATE_SAVING_VIEW_IDS
上下文参数。此上下文参数的值包含一个视图 ID 列表,对于这些视图,将禁用部分状态保存。ID 应以逗号分隔,如下面的代码所示(假设你有三个页面:index.xhtml
、done.xhtml
和error.xhtml
,仅对index.xhtml
使用部分状态保存):
<context-param>
<param-name>javax.faces.FULL_STATE_SAVING_VIEW_IDS</param-name>
<param-value>/done.xhtml,/error.xhtml</param-value>
</context-param>
通过编程方式,你可以如下检查状态是否在客户端保存:
-
在视图/页面中的代码如下:
#{facesContext.application.stateManager. isSavingStateInClient(facesContext)}
-
在后端 Bean 中,代码如下:
FacesContext facesContext = FacesContext.getCurrentInstance(); Application application = facesContext.getApplication(); StateManager stateManager = application.getStateManager(); logger.log(Level.INFO, "Is view state saved on client ? {0}", stateManager.isSavingStateInClient(facesContext));
JSF 逻辑视图和物理视图
到目前为止,一切顺利!我们知道 JSF 可以在服务器或客户端存储完整或部分视图状态,并具有一些优点和缺点。进一步来说,你必须知道 JSF 区分逻辑视图(特定于 GET 请求)和物理视图(特定于 POST 请求)。每个 GET 请求都会生成一个新的逻辑视图。默认情况下,JSF Mojarra(JSF 的参考实现)管理 15 个逻辑视图,但这个数字可以通过上下文参数com.sun.faces.numberOfLogicalViews
进行调整,如下面的代码所示:
<context-param>
<param-name>com.sun.faces.numberOfLogicalViews</param-name>
<param-value>2</param-value>
</context-param>
你可以通过启动浏览器并打开ch9_2
应用三次,在三个不同的浏览器标签页中轻松进行此设置的测试。之后,回到第一个标签页并尝试提交表单。你会看到一个ViewExpiredException
异常,因为第一个逻辑视图已被从逻辑视图映射中移除,如下面的截图所示:
如果你在一个或两个标签页中打开应用,这个错误将不会发生。
对于 POST 请求(非 AJAX),还有一个故事,因为在这种情况下,JSF(Mojarra 实现)会存储每个单独的表单,直到达到最大大小。一个 POST
请求创建一个新的物理视图(除了重复使用相同物理视图的 AJAX 请求外),JSF Mojarra 可以每个逻辑视图存储 15 个物理视图(Map<LogicalView, Map<PhysicalView, and ViewState>>
)。显然,一个物理视图可以包含多个表单。
你可以通过名为 com.sun.faces.numberOfViewsInSession
的上下文参数来控制物理视图的数量。例如,你可以将其值减少到 4
,如下面的代码所示:
<context-param>
<param-name>com.sun.faces.numberOfViewsInSession</param-name>
<param-value>4</param-value>
</context-param>
这个小值允许你进行快速测试。在浏览器中打开名为 ch9_3
的应用程序,并提交该表单四次。之后,按四次浏览器的后退按钮,返回到第一个表单并再次尝试提交。你会看到一个异常,因为此物理视图已被从物理视图的映射中移除。如果你提交表单少于四次,这种情况不会发生。
注意
如果你需要超过 15 个逻辑/物理视图,你可以增加它们的数量或选择在客户端保存状态。在客户端保存状态是推荐的,因为它将完全消除这个问题。
在页面导航的情况下,JSF 不会为 GET 请求在会话中存储任何内容,但会为 POST 请求保存表单的状态。
在数据库中保存状态——一个实验性应用程序
将客户端保存状态和复杂视图结合起来确实会加大网络带宽的压力。这种缺点的根源在于每个请求-响应周期中客户端和服务器之间应该传递的序列化状态的大小。通常,这个字符串会显著增加服务器的响应大小。一个有趣的想法是将视图状态保存在数据库中,只向客户端发送对应记录的标识符。在本节中,你将看到如何使用 MongoDB 数据库和自定义的保存客户端视图状态实现这一任务。该实现与 JSF Mojarra 紧密耦合(存在 com.sun.faces.*
特定的依赖项,需要 Mojarra)。因此,由于它没有使用标准 API 方法,这种方法在 MyFaces 中不可行。
注意
如果你不太熟悉 MongoDB(或 NoSQL 数据库系统),你可以使用 SQL RDBMS(例如,MySQL)和平凡的 JDBC。
为了将客户端视图状态传递到数据库,你必须了解 JSF 默认如何处理它,并执行相应的调整。保存状态的魔法从 ViewHandler
/StateManager
类对开始,它们指导请求之间保存/恢复视图的任务。它们都使用一个名为 ResponseStateManager
的辅助类,该类知道如何确定状态应该保存的位置(基于默认设置或 web.xml
明确设置),并将保存/恢复任务委托给两个辅助类之一,即 ClientSideStateHelper
和 ServerSideStateHelper
。
更详细地说,当视图状态应该被保存时,StateManager.writeState
方法从 ViewHandler.renderView
方法中被调用。在 StateManager.writeState
方法中,JSF 将获取一个 ResponseStateManager
实例。此对象可以检查每个渲染技术特定的请求,因为它知道使用的渲染技术。ResponseStateManager
实例来自 RenderKit
类(通过调用名为 getResponseStateManager
的 RenderKit
方法)并将写入任务委托给 ResponseStateManager.writeState
方法。在 ResponseStateManager
构造函数中,JSF 将确定视图状态应该保存的位置(在客户端或服务器上),并指示写入任务应该在两个辅助类之一中发生,这两个类负责有效地写入视图状态。
在返回过程中,在恢复视图时,ViewHandler
使用 ResponseStateManager
类来测试请求是否是初始请求或回发请求。如果是回发请求,JSF 将调用 ViewHandler.restoreView
方法。
由于我们关注在客户端保存视图状态,我们将关注定义以下重要方法的 ClientSideStateHelper
类:
-
writeState
: 此方法生成隐藏的输入字段,并使用序列化视图状态加密版本填充其值 -
getState
: 此方法检查传入的请求参数中是否有标准的状态参数名称,并解密字符串
因此,我们需要编写我们的辅助类,命名为 CustomClientSideStateHelper
。writeState
方法是一个方便的起点。想法是修改默认方法,将加密状态发送到 MongoDB 数据库,而不是发送到客户端。客户端将接收到用于在数据库中存储状态的键。以下代码中的修改被突出显示:
@Override
public void writeState(FacesContext ctx, Object state,
StringBuilder stateCapture) throws IOException {
if (stateCapture != null) {
doWriteState(ctx,state,new StringBuilderWriter(stateCapture));
} else {
ResponseWriter writer = ctx.getResponseWriter();
writer.startElement("input", null);
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("name",
ResponseStateManager.VIEW_STATE_PARAM, null);
if (webConfig.isOptionEnabled(EnableViewStateIdRendering)) {
String viewStateId = Util.getViewStateId(ctx);
writer.writeAttribute("id", viewStateId, null);
}
StringBuilder stateBuilder = new StringBuilder();
doWriteState(ctx,state,new StringBuilderWriter(stateBuilder));
WriteStateInDB writeStateInDB = new WriteStateInDB();
String client_id =
writeStateInDB.writeStateDB(stateBuilder.toString());
if (client_id != null) {
writer.writeAttribute("value", client_id, null);
} else {
writer.writeAttribute("value",
stateBuilder.toString(), null);
}
if (webConfig.isOptionEnabled(AutoCompleteOffOnViewState)) {
writer.writeAttribute("autocomplete", "off", null);
}
writer.endElement("input");
writeClientWindowField(ctx, writer);
writeRenderKitIdField(ctx, writer);
}
}
此外,后续客户端请求将传递主键到默认的 getState
方法。因此,你需要编写一个自定义的 getState
方法,通过其 ID(主键)从数据库中提取相应的状态:
@Override
public Object getState(FacesContext ctx, String viewId)
throws IOException {
String stateString = ClientSideStateHelper.getStateParamValue(ctx);
if (stateString == null) {
return null;
}
if ("stateless".equals(stateString)) {
return "stateless";
} else {
WriteStateInDB writeStateInDB = new WriteStateInDB();
stateString = writeStateInDB.readStateDB(stateString);
if (stateString == null) {
return null;
}
}
return doGetState(stateString);
}
编写自定义 ResponseStateManager
类
在这一点上,我们可以使用 MongoDB 数据库来保存/恢复客户端视图状态。展望未来,我们需要告诉 JSF 使用我们的CustomClientSideStateHelper
类而不是默认的ClientSideStateHelper
类。如果我们编写一个ResponseStateManager
类的自定义实现,这项任务可以轻松完成。这将几乎与 Mojarra 实现相同,但在构造函数中有一个小的调整(注意我们在这里巧妙地引入了CustomClientSideStateHelper
类),如下面的代码所示:
public class CustomResponseStateManager extends ResponseStateManager {
private StateHelper helper;
public CustomResponseStateManager() {
WebConfiguration webConfig = WebConfiguration.getInstance();
String stateMode =
webConfig.getOptionValue(StateSavingMethod);
helper = ((StateManager.STATE_SAVING_METHOD_CLIENT.equalsIgnoreCase(stateMode)
? new CustomClientSideStateHelper()
: new ServerSideStateHelper()));
}
...
按照同样的推理,我们需要告诉 JSF 使用我们的自定义ResponseStateManager
类。记住,JSF 通过默认的RenderKit
类获取这个类的实例;因此,我们可以轻松地编写我们的自定义RenderKit
类,并重写getResponseStateManager
方法,该方法负责创建ResponseStateManager
类的实例。为了编写一个自定义的RenderKit
类,我们将扩展包装类RenderKitWrapper
,它代表RenderKit
抽象类的一个简单实现,并省去了我们实现所有方法的麻烦,如下面的代码所示:
public class CustomRenderKit extends RenderKitWrapper {
private RenderKit renderKit;
private ResponseStateManager responseStateManager =
new CustomResponseStateManager();
public CustomRenderKit() {}
public CustomRenderKit(RenderKit renderKit) {
this.renderKit = renderKit;
}
@Override
public synchronized ResponseStateManager getResponseStateManager() {
if (responseStateManager == null) {
responseStateManager = new CustomResponseStateManager();
}
return responseStateManager;
}
@Override
public RenderKit getWrapped() {
return renderKit;
}
}
自定义的RenderKit
类必须在faces-config.xml
文件中适当配置,如下所示:
<render-kit>
<render-kit-class>
book.beans.CustomRenderKit
</render-kit-class>
</render-kit>
完成!现在,默认的StateManager
类将需要从我们的RenderKit
类中获取一个ResponseStateManager
实例,该实例将提供一个CustomResponseStateManager
类的实例。进一步,CustomResponseStateManager
类将使用CustomClientSideStateHelper
来保存/恢复客户端状态。
在等式中添加 MongoDB
前一节缺失的部分是WriteStateInDB
类。这是一个能够使用 MongoDB Java Driver(版本 2.8.0 或更高)从 MongoDB(版本 2.2.2 或更高)数据库中写入/读取数据的类,并在以下代码中列出(对于那些熟悉 MongoDB Java Driver 的人来说,这是一段非常简单的代码):
public class WriteStateInDB {
private DBCollection dbCollection;
public WriteStateInDB() throws UnknownHostException {
Mongo mongo = new Mongo("127.0.0.1", 27017);
DB db = mongo.getDB("jsf_db");
dbCollection = db.getCollection(("jsf"));
}
protected String writeStateDB(String state) {
//TTL Index
BasicDBObject index = new BasicDBObject("date", 1);
BasicDBObject options = new BasicDBObject("expireAfterSeconds",
TimeUnit.MINUTES.toSeconds(1));
dbCollection.ensureIndex(index, options);
BasicDBObject basicDBObject = new BasicDBObject();
basicDBObject.append("date", new Date());
basicDBObject.append("state", state);
dbCollection.insert(basicDBObject);
ObjectId id = (ObjectId) basicDBObject.get("_id");
return String.valueOf(id);
}
protected String readStateDB(String id) {
BasicDBObject query = new BasicDBObject("_id", new ObjectId(id));
DBObject dbObj = dbCollection.findOne(query);
if (dbObj != null) {
return dbObj.get("state").toString();
}
return null;
}
}
此外,这个类利用了 MongoDB 的一个强大功能,名为 TTL (docs.mongodb.org/manual/tutorial/expire-data/
),它能够在指定秒数或特定时钟时间后自动删除数据。这对于清理已过期的会话(孤儿会话)的数据库非常有用。在这个演示中,每个状态将在数据插入数据库后 60 秒被删除,但将时间设置为 30 分钟可能更符合实际情况。当然,即便如此,你仍然面临删除当前活跃状态的风险;因此,需要额外的检查或备选方案。不幸的是,我们无法提供更多有关 MongoDB 的详细信息,因为这超出了本书的范围。因此,你必须进行调研 (www.mongodb.org/
)。在下面的屏幕截图,你可以看到一个简单的测试,揭示了默认客户端视图状态保存(1.3 KB)和自定义客户端视图状态之间的页面大小差异。默认方法如下:
自定义方法如下:
当然,这种方法引发了一个主要缺点,即每次保存/恢复状态都需要击中数据库(缓存可以解决这个问题)。
完整的应用程序命名为 ch9_9
。为了使其工作,你需要安装 MongoDB 2.2.2(或更高版本)。应用程序附带 MongoDB Java 驱动程序版本 2.8.0,但你也可以提供更新版本的驱动程序。
作为本节的最后一点,请记住,可以通过扩展包装类 StateManagerWrapper
来编写自定义的 StateManager
类,如下面的代码所示(从 JSF 2.0 开始,我们可以使用这个包装类轻松地装饰 StateManager
类):
public class CustomStateManager extends StateManagerWrapper {
private StateManager stateManager;
public CustomStateManager() {
}
public CustomStateManager(StateManager stateManager) {
this.stateManager = stateManager;
}
@Override
// ... override here the needed methods
@Override
public StateManager getWrapped() {
return stateManager;
}
}
自定义状态管理器应在 faces-config.xml
文件中按如下方式配置:
<application>
<state-manager>
book.beans.CustomStateManager
</state-manager>
</application>
处理 ViewExpiredException
当用户会话过期(任何原因)时,将发生 ViewExpiredException
。这个异常背后的场景基于以下步骤:
-
用户视图状态保存在服务器上(
javax.faces.STATE_SAVING_METHOD
上下文参数的值是 server)。 -
用户将视图状态 ID 作为隐藏输入字段
javax.faces.ViewState
的值接收,这指向服务器上保存的视图状态。 -
用户会话过期(例如,超时会话)并且视图状态从服务器会话中删除,但用户仍然拥有视图状态 ID。
-
用户发送 POST 请求,但视图状态 ID 指示不可用的视图状态;因此,发生
ViewExpiredException
。
为了处理这个异常,你有两个选择:避免它或处理它。假设你正在查看视图 A,然后点击注销按钮,这将使会话无效并将控制权重定向到视图B(当会话无效时,状态会自动从会话中移除)。由于这是一个非 AJAX 的 POST 请求,用户可以按下浏览器后退按钮,这将再次加载视图A。现在,他可以再次点击注销按钮,但这次,他/她将看到ViewExpiredException
,因为,很可能是视图A没有再次请求服务器,而是从浏览器缓存中加载的。由于是从缓存中加载的,所以javax.faces.ViewState
视图状态 ID 与第一次注销时相同;因此,相关的状态不再可用。流程如下面的截图所示:
显然,这不是期望的行为。你必须告诉浏览器向服务器发送新的请求,而不是从缓存中加载视图A。这可以通过设置正确头部的过滤器来实现,以禁用浏览器缓存。该过滤器将应用于Faces Servlet
类,如下面的代码所示:
@WebFilter(filterName = "LogoutFilter", servletNames = {"Faces Servlet"})
public class LogoutFilter implements Filter {
...
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest requestHTTP = (HttpServletRequest) request;
HttpServletResponse responseHTTP = (HttpServletResponse) response;
try {
String resourceURI = requestHTTP.getContextPath() +
requestHTTP.getServletPath() +
ResourceHandler.RESOURCE_IDENTIFIER;
String requestURI = requestHTTP.getRequestURI();
if (!requestURI.startsWith(resourceURI)) {
responseHTTP.setHeader("Expires",
"Sat, 6 May 1995 12:00:00 GMT");
responseHTTP.setHeader("Cache-Control",
"no-store,no-cache,must-revalidate");
responseHTTP.addHeader("Cache-Control",
"post-check=0, pre-check=0");
responseHTTP.setHeader("Pragma", "no-cache");
}
chain.doFilter(request, response);
} catch (IOException | ServletException t) {
}
}
现在,重复上述场景,注意,这次不是ViewExpiredException
,视图A在javax.faces.ViewState
中接收一个新的视图状态 ID。
你可以在本章的代码包中看到两个示例。一个是名为ch9_4_1
,另一个是名为ch9_4_2
。
前面的解决方案可能对用户来说有点困惑,因为它没有提供任何关于正在发生什么的明确信息。此外,会话可能因许多其他原因而过期;因此,显示一个错误页面给用户,而不是使用过滤器来防止浏览器缓存,会更好。错误页面可以是登录页面,或者只是一个包含登录页面链接的中间页面。这可以通过在web.xml
文件中添加以下代码来实现:
<error-page>
<exception-type>
javax.faces.application.ViewExpiredException
</exception-type>
<location>/faces/expired.xhtml</location>
</error-page>
一个简单的expired.xhtml
将如下所示:
<h:body>
Your session expired ...
<h:link value="Go to Login Page ..." outcome="index" />
</h:body>
完整的示例名为ch9_5
,可在本书的代码包中找到。
至少还有另一种来自 JSF 1.2 的方法,在 JSF 2.2 中也可以使用。你可以尝试设置以下context
参数:
<context-param>
<param-name>
com.sun.faces.enableRestoreView11Compatibility
</param-name>
<param-value>true</param-value>
</context-param>
好吧,这可以解释为:当当前视图过期时,生成一个新的视图,并且不要抛出ViewExpiredException
。
完整的示例名为ch9_6
,可在本书的代码包中找到。
更多关于这个异常的详细信息(包括如何在 AJAX 环境中处理它)可以在第五章的配置视图处理器和配置全局异常处理器部分中找到,使用 XML 文件和注解配置 JSF – 第二部分。
会话中的服务器状态序列化
在服务器端,状态可以存储为浅拷贝或深拷贝。在浅拷贝中,状态不会在会话中序列化(JSF 在会话中仅存储状态指针,并且只有容器处理序列化相关事宜),这需要更少的内存,并允许你在视图作用域的 Bean 中注入 EJB(请谨慎使用此技术,因为影响一个拷贝中对象的更改将反映在其他拷贝中)。深拷贝表示在会话中完全序列化状态,这需要更多的内存,并且不允许注入 EJB。
注意
默认情况下,JSF Mojarra 使用浅拷贝,而 JSF MyFaces 使用深拷贝。无论如何,进行快速测试以确保默认值。
我们可以通过在web.xml
中显式设置javax.faces.SERIALIZE_SERVER_STATE
上下文参数来轻松更改默认行为。此上下文参数从 JSF 2.2 开始引入,代表在 Mojarra 和 MyFaces 中设置服务器状态序列化的标准上下文参数。你可以如下指示使用浅拷贝:
<context-param>
<param-name>javax.faces.SERIALIZE_SERVER_STATE</param-name>
<param-value>false</param-value>
</context-param>
注意
为了避免类型异常,例如java.io.NotSerializableException
(以及类型为Setting non-serializable attribute value ...
的警告),请记住,在会话中序列化状态意味着需要序列化的后端 Bean。(它们导入java.io.Serializable
,并且它们的属性是可序列化的。特别关注嵌套 Bean、EJB、流、JPA 实体、连接等。)当你在客户端存储视图状态时也是如此,因为整个状态应该是可序列化的。当一个 Bean 属性不应该(或不能)被序列化时,只需将其声明为transient
,并且不要忘记在反序列化时它将是null
。
除了前面的注意之外,一个常见的情况是当状态在客户端保存时,会引发java.io.NotSerializableException
。但是,当在服务器端切换状态时,这个异常在 Mojarra 中神奇地消失了,而在 MyFaces 中仍然存在。这可能令人困惑,但如果你在使用 Mojarra 实现时,这是完全正常的;在客户端保存状态时,状态应该是完全可序列化的(但实际上并不是,因为发生了这个异常),而在服务器端,Mojarra 默认不会在会话中序列化状态。另一方面,MyFaces 默认序列化状态;因此,异常仍然存在。
注意
有时,通过重新设计应用程序状态(包含视图或会话或应用程序后端豆),您可以优化内存使用并节省服务器资源,这包含视图或会话或应用程序后端豆(不要缓存可以从数据库查询的数据,并尝试减少此类豆的数量)。除了管理视图状态外,这也是一个直接反映在性能上的重要方面。当需要更多内存时,容器可能会选择序列化应用程序状态的部分,这意味着您必须付出反序列化的代价。虽然将数据保存在会话中的代价是内存,但序列化/反序列化的代价是时间和微不足道的磁盘空间(至少应该是微不足道的)。
JSF 2.2 是无状态的
无状态的概念相当令人困惑,因为每个应用程序都必须维护某种状态(例如,用于运行时变量)。一般来说,无状态应用程序将遵循每个请求一个状态的原则,这意味着状态的生命周期与请求-响应生命周期相同。这在 Web 应用程序中是一个重要的问题,因为我们需要使用会话/应用程序作用域,这显然破坏了无状态的概念。
即使如此,JSF 2.2 最受欢迎的功能之一是无状态视图(实际上从版本 2.1.19 开始就可用)。这个概念背后的想法是假设 JSF 不会在请求之间保存/恢复视图状态,而会倾向于在每个请求中从 XHTML 标签中重新创建视图状态。目标是显著提高性能:节省/恢复视图状态所用的时间,更有效地使用服务器内存,更好地支持集群环境,以及防止ViewExpiredException
。因此,JSF 开发者对无状态功能有一定的要求。
尽管如此,似乎无状态功能对保存/恢复视图状态所用的时间影响不大(这并不昂贵,尤其是当状态保存在服务器会话中且不需要序列化时)以及内存性能。另一方面,当一个应用程序部署在多台计算机上(在集群环境中)时,无状态功能可以真正提供帮助,因为我们不再需要会话复制(指在不同实例间复制会话中存储的数据)和/或粘性会话(指负载均衡器用来提高集群配置中持久会话效率的机制)。对于无状态应用程序,节点不需要共享状态,客户端回发请求可以由不同的节点解决。这是一个巨大的成就,因为为了解决许多请求,我们可以添加新的节点而不用担心共享状态。此外,防止ViewExpiredException
也是一个很大的优势。
注意
无状态视图可以用来推迟会话创建或处理大型(复杂)组件树,这可能导致不舒适的状态。
从 JSF 2.2 开始,开发者可以在同一应用程序中选择保存视图状态或创建无状态视图,这意味着应用程序可以在某些视图中使用动态表单(有状态)并在其他视图中为每个请求创建/重新创建它们(无状态)。对于无状态视图,组件树不能动态生成/更改(例如,在无状态模式下不可用 JSTL 和绑定),并且重新提交表单可能不会按预期工作。此外,一些 JSF 组件是有状态的,这将在无状态视图中导致严重问题。但是,由于它们的行为依赖于环境(上下文),因此很难指定这些组件和问题。一些特定的测试可能会有所帮助。
为了编写一个无状态的 JSF 应用程序,你必须设计一切仅与请求作用域的 bean 协同工作。在某些情况下,我们可以使用不同的技巧来完成这项任务,例如使用隐藏字段和特殊的请求参数来模拟会话。虽然会话和应用程序 bean 会破坏无状态的概念(即使使用它们是可能的),但视图 bean 将充当请求 bean。
从编程的角度来看,将视图定义为无状态是非常简单的:只需向<f:view>
标签添加名为transient
的属性并将其值设置为true
。请注意,为了有一个无状态视图,<f:view>
标签的存在是强制性的,即使它没有其他用途。应用程序中的每个无状态视图都需要这个设置,因为没有全局设置来指示应在应用程序级别应用无状态效果。
<f:view transient="true">
...
</f:view>
当视图是无状态的,javax.faces.ViewState
的值将是stateless
,如下面的屏幕截图所示:
视图作用域的 bean 和无状态特性
在无状态环境中,视图作用域的 bean 充当请求作用域的 bean。除了你不能动态创建/操作视图的事实之外,这是无状态特性带来的一个重大缺点,因为它将影响通常使用视图作用域 bean 的基于 AJAX 的应用程序。你可以通过一组具有不同作用域的 bean 轻松测试这种行为(整个应用程序命名为ch9_7
)。视图作用域 bean 可以定义如下:
@Named
@ViewScoped
public class TimestampVSBean implements Serializable{
private Timestamp timestamp;
public TimestampVSBean() {
java.util.Date date = new java.util.Date();
timestamp = new Timestamp(date.getTime());
}
public Timestamp getTimestamp() {
return timestamp;
}
public void setTimestamp(Timestamp timestamp) {
this.timestamp = timestamp;
}
}
只需将作用域更改为请求、会话和应用程序,就可以获得其他三个 bean。
接下来,我们将编写一个简单的无状态视图,如下所示:
<f:view transient="true">
<h:form>
<h:commandButton value="Generate Timestamp"/>
</h:form>
<hr/>
Request Scoped Bean:<h:outputText value="#{timestampRSBean.timestamp}"/>
<hr/>
View Scoped Bean:<h:outputText value="#{timestampVSBean.timestamp}"/>
[keep an eye on this in stateless mode]
<hr/>
Session Scoped Bean:<h:outputText value="#{timestampSSBean.timestamp}"/>
<hr/>
Application Scoped Bean:<h:outputText value="#{timestampASBean.timestamp}"/>
<hr/>
</f:view>
之后,只需多次提交此表单(点击生成时间戳按钮)并注意,视图作用域 bean 生成的时间戳在每次请求时都会改变,如下面的屏幕截图所示:
请求、会话和应用程序作用域按预期工作!
以编程方式检测无状态视图
以编程方式,您可以使用以下选项检测视图是否为无状态:
-
在视图或页面中,输入以下代码:
<f:view transient="true"> Is Stateless (using transient) ? #{facesContext.viewRoot.transient} ... </f:view>
-
在视图或页面中,输入以下代码。这仅适用于
postback
请求:Is Stateless (using stateless) ? #{facesContext.postback ? facesContext.renderKit.responseStateManager. isStateless(facesContext, null) : 'Not postback yet!'}
-
在后端 Bean 中,输入以下代码:
FacesContext facesContext = FacesContext.getCurrentInstance(); UIViewRoot uiViewRoot = facesContext.getViewRoot(); logger.log(Level.INFO, "Is stateless (using isTransient) ? {0}", uiViewRoot.isTransient()); logger.log(Level.INFO, "Is stateless (using isStateless) ? {0}", facesContext.getRenderKit().getResponseStateManager().isStateless(facesContext, null));
注意
注意,isStateless
方法只能用于postback
请求。
完整的应用程序命名为ch9_8
。
JSF 安全注意事项
关于 JSF 保存状态的论文还暗示了一些关于 JSF 安全性的方面。看起来在客户端保存 JSF 状态比在服务器上保存 JSF 状态更不安全。对于最常见的安全担忧(例如,XSS、CSRF、SQL 注入和钓鱼),JSF 提供了隐式保护。
跨站请求伪造(CSRF)
通过在服务器上保存状态,可以防止 CSRF 和钓鱼攻击。JSF 2.0 自带基于javax.faces.ViewState
隐藏字段的值的隐式保护来防止 CSRF 攻击。从 JSF 2.2 开始,通过为该字段创建一个强大且健壮的值,这种保护得到了严重加强。
跨站脚本(XSS)
通过escape
属性,JSF 隐式防止 XSS 攻击,该属性默认设置为true
(<h:outputText/>, <h:outputLabel/>
)。以下是一些示例:
<p>Hi, <h:outputText value="#{loginbean.name}" /></p>
<p>Hi, #{loginbean.name}</p>
上述示例都是 XSS 受保护的,因为它们都经过了转义。
但是,如果您编写以下示例,那么 XSS 攻击是可能的:
<p>Hi, <h:outputText value="#{loginbean.name}" escape="false" /></p>
为了允许 HTML 标签,您必须关注一个专门的工具,该工具将能够解析 HTML 代码。
注意
在无状态模式下,escape
属性应始终设置为true
,因为一个 XSS 漏洞可以提供一个方便的 CSRF 攻击方式。
SQL 注入
SQL 注入通常是一种攻击,它推测基于用户输入/选择创建的 SQL 查询。JSF 本身无法防止这类攻击,因为它不涉及生成和执行 SQL 事务。另一方面,您可以使用 JSF 来过滤/验证用户输入或选择,这可能防止此类攻击。在 JSF 之外,编写参数化查询而不是在语句中嵌入用户输入,并在过滤转义字符和类型处理上格外小心,这是一种很好的防止这些攻击的技术。
摘要
希望您认为这是一篇关于 JSF 状态的有趣论文。这是一个长期有争议的主题,从 JSF 2.2 开始,无状态视图给这个争议之火增添了更多的燃料。然而,选择正确管理状态的方式是一个影响应用程序性能的重大决策;因此,明智地选择,并尝试记录有关 JSF 状态的现有基准和解决方案。
欢迎您在下一章中,我们将讨论 JSF 中的自定义和组合组件。
第十章 JSF 自定义组件
JSF 是一个基于组件的框架,JSF 自定义组件是支持 JSF 灵活性和可扩展性的主要证据。为了编写自定义组件或扩展现有的组件,JSF 提供了一个强大的 API,允许我们开发两种类型的组件:自定义组件,以及从 JSF 2.0 开始,复合组件。自定义组件实现负责提供方面(对于非 UI 组件,如自定义验证器、转换器和渲染器,是可选的)和行为。通常,编写自定义组件的决定和实现它的技能属于高级 JSF 开发者。
在你决定编写自定义组件之前,这可能是一项耗时的工作,你必须概述以下要点(特别是第一个要点):
-
检查互联网(例如,
jsfcentral.com/
) 确保该组件尚未存在。许多 JSF 扩展,如 PrimeFaces、ICEfaces、OmniFaces 和 RichFaces,已经包含数百个自定义组件。 -
确保你需要自定义组件,而不仅仅是 Facelet 模板(参见第十二章,Facelets 模板)或对现有组件的一些自定义逻辑。
-
尝试重新设计应用程序目标以使用现有组件(有时你可以组合几个现有组件以获得所需方面和行为)。
-
仔细查看非 JSF 组件,例如 jQueryUI、ComponentJS 和 AmplifyJS(因为你在 JSF 应用程序中并不被迫只能使用 JSF 组件!)。
如果你的应用程序有一些特定的目标,这些目标无法通过前面的任何一项解决,那么是时候开始编写你自己的组件了。
在本章的第一部分,你将看到如何编写非复合自定义组件,在第二部分你将了解复合组件。非复合组件在 JSF 中已经存在很长时间了,编写此类组件的技术基于编写几个 Java 类。与复合组件一起出现的新概念从 JSF 2 开始提供,其背后的想法是用 XHTML 页面替换 Java 类。
构建非复合自定义组件
让我们直接跳到有趣的部分,并说在 JSF 2.0 中,通过在 Facelet 标签库(*taglib.xml
)中进行配置,自定义组件被提供给页面作者。
此外,当组件映射到 JAR 文件中时,需要在web.xml
中添加一个特殊的条目来指向*taglib.xml
文件。参见名为ch10_3
的应用程序。
截至 JSF 2.2,我们不再需要这些文件。一个 JSF 2.2 简单自定义组件包含一个类,它可能看起来像以下代码:
@FacesComponent(value = "components.WelcomeComponent", createTag = true)
public class WelcomeComponent extends UIComponentBase {
@Override
public String getFamily() {
return "welcome.component";
}
@Override
public void encodeBegin(FacesContext context) throws IOException {
String value = (String) getAttributes().get("value");
String to = (String) getAttributes().get("to");
if ((value != null) && (to != null)) {
ResponseWriter writer = context.getResponseWriter();
writer.writeAttribute("id", getClientId(context), null);
writer.write(value + ", " + to);
}
}
}
大部分工作都是由 @FacesComponent
注解(javax.faces.component.FacesComponent
)完成的。我们所需做的只是将 createTag
元素设置为 true
,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
>
<h:head>
<title></title>
</h:head>
<h:body>
<t:welcomeComponent value="Welcome" to="Rafael Nadal"/>
</h:body>
</html>
注意
注意,组件的默认命名空间是 http://xmlns.jcp.org/jsf/component
。这对于所有没有显式命名空间的组件都适用。
整个应用程序命名为 ch10_1
。
JSF 2.2 支持的 @FacesComponent
元素完整列表如下:
-
createTag
:这可以设置为true
或false
。当设置为true
时,JSF 将为我们生成标签(更具体地说,JSF 将在运行时创建一个扩展ComponentHandler
的 Facelet 标签处理器)。此元素只能在 JSF 2.2 中使用。 -
tagName
:这允许我们指定标签名称。当createTag
设置为true
时,JSF 将使用此名称生成标签。此元素只能在 JSF 2.2 中使用。 -
namespace
:这允许我们指定标签命名空间。当createTag
设置为true
时,JSF 将使用此命名空间生成标签。当未指定命名空间时,JSF 将使用http://xmlns.jcp.org/jsf/component
命名空间。此元素只能在 JSF 2.2 中使用。 -
value
:此元素来自 JSF 2.0,表示 组件类型。组件类型可以用作Application.createComponent(java.lang.String)
方法的参数,以创建Component
类的实例。截至 JSF 2.2,如果value
元素缺失或为null
,JSF 将通过在@FacesComponent
附着的类上调用getSimpleName
方法并小写第一个字符来获取它。
通过组件类型,我们理解一小块数据,它针对每个 UIComponent
子类,可以与 Application
实例结合使用,以编程方式获取这些子类的实例。此外,每个 UIComponent
子类属于一个组件家族(例如 javax.faces.Input
)。当我们编写自定义组件并在某个家族下声明它时,这很重要,因为我们可以利用该家族组件特有的渲染器。在组件家族旁边,我们可以使用 渲染器类型 属性从 RenderKit
集合中选择一个 Renderer
实例(例如,输入字段属于 javax.faces.Input
家族和 javax.faces.Text
渲染器类型)。
每个自定义组件必须扩展 UIComponent
或其子类型之一,例如 UIComponentBase
,它实际上是 UIComponent
所有抽象方法的默认实现。无论如何,有一个例外是 getFamily
方法,即使扩展 UIComponentBase
也必须重写它。作为常见做法,当自定义组件需要接受最终用户输入时,它将扩展 UIInput
,而当它需要作为命令执行时,它将扩展 UICommand
。
此外,让我们修改我们的应用程序如下,以指示自定义命名空间和标签名称:
@FacesComponent(value = "components.WelcomeComponent", createTag = true, namespace = "http://atp.welcome.org/welcome", tagName = "welcome")
public class WelcomeComponent extends UIComponentBase {
...
}
接下来,组件将被如下使用:
<html
>
...
<t:welcome value="Welcome" to="Rafael Nadal"/>
完整的应用程序命名为 ch10_2
。此外,此应用程序的 JSF 2.0 版本(包含 *taglib.xml
描述符和 web.xml
中的特定条目)命名为 ch10_3
。
编写自定义标签处理器
注意,在某些情况下仍然需要 *taglib.xml
。例如,如果你决定为你组件编写自定义标签处理器,那么你仍然需要此文件来配置处理器类。在这种情况下,你将扩展 ComponentHandler
类并覆盖所需的方法。大多数开发者利用 onComponentCreated
和 onComponentPopulated
方法。第一个方法在组件创建后但尚未用子组件填充之前被调用,第二个方法在组件用子组件填充之后被调用。截至 JSF 2.2,为希望接管 UIComponent
实例化任务的开发者添加了一个新方法。此方法名为 createComponent
。如果它返回 null
,则此方法将需要通过 TagHandlerDelegate
创建组件。由于这是一个相当罕见的情况,我们不坚持这一点,只是提供了一个简单的 ComponentHandler
模板:
public class WelcomeComponentHandler extends ComponentHandler {
private static final Logger logger =
Logger.getLogger(WelcomeComponentHandler.class.getName());
public WelcomeComponentHandler(ComponentConfig config) {
super(config);
}
@Override
public UIComponent createComponent(FaceletContext ctx) {
logger.info("Inside 'createComponent' method");
return null;
}
@Override
public void onComponentCreated(FaceletContext ctx,
UIComponent c, UIComponent parent) {
logger.info("Inside 'onComponentCreated' method");
super.onComponentCreated(ctx, c, parent);
}
@Override
public void onComponentPopulated(FaceletContext ctx,
UIComponent c, UIComponent parent) {
logger.info("Inside 'onComponentPopulated' method");
super.onComponentPopulated(ctx, c, parent);
}
}
为了指示我们的类处理器应该被使用,我们需要在 *taglib.xml
文件中对其进行配置,如下面的代码所示:
<handler-class>book.beans.WelcomeComponentHandler</handler-class>
完整示例命名为 ch10_24_1
。另一个可以作为起点使用的模板可以在 ch10_24_2
中找到。后者定义了自定义 ComponentHandler
、自定义 TagHandlerDelegateFactory
和自定义 TagHandlerDelegate
的最小实现。
分析自定义组件
到目前为止,你可以看到我们的组件类覆盖了 encodeBegin
方法。此方法属于一组四个用于渲染组件的方法,其中每个组件都可以自行渲染(setRendererType
方法的参数为 null
)或将渲染过程委托给 Renderer
类(内置或用户定义)。这些方法如下:
-
decode
:为了解析输入值并将它们保存到组件中,每个请求都会通过decode
方法。通常,当此方法被覆盖时,开发者会从请求映射中(或使用UIComponent.getAttributes
方法从Map
属性中)提取所需值,并通过调用setSubmittedValue(
value)
方法将这些值设置到组件中。 -
encodeBegin
:此方法开始自定义组件的渲染过程。它通过FacesContext.getResponseWriter
方法获取的响应流写入。当我们需要编码子组件但希望在编码之前向用户输出响应时,会覆盖此方法。注意
ResponseWriter
对象(FacesContext.getResponseWriter
)包含用于生成标记的特殊方法,例如startElement
、writeAttribute
、writeText
和endElement
。 -
encodeChildren
:此方法渲染自定义组件的子组件。它很少被重写;然而,如果您想改变编码组件子组件的默认递归过程,那么请继续重写它。 -
encodeEnd
:这可能是被重写最多的方法。正如其名称所暗示的,此方法在结束时被调用。在这里,我们将自定义标记写入响应流。当自定义组件接受最终用户输入时,encodeEnd
比encodeBegin
更受欢迎,因为在encodeBegin
的情况下,输入可能尚未通过潜在的附加转换器传递。
注意
我们刚才讨论的四个方法对所有自定义组件和所有渲染器都可用。在两种情况下,它们都有相同的名称,它们之间的区别在于一个参数。当它们在自定义组件类中被重写时,它们获得一个表示 FacesContext
的单个参数。另一方面,当它们在自定义渲染器中被重写时,它们获得 FacesContext
实例和相应的自定义组件(UIComponent
)作为参数。
因此,我们可以得出结论,自定义组件是基于 UIComponent
的子类,并且它可以自行渲染或委托给一个能够渲染 UIComponent
实例并解码获取用户输入的 POST 请求的 Renderer
类。
自定义组件的一个重要方面是管理它们的状态。您应该已经熟悉从 第九章 JSF 状态管理 中了解的状态概念。因此,我们可以说 JSF 2.0 带来了 StateHelper
接口,它基本上允许我们在多个请求(回发)之间存储、读取和删除数据。这意味着我们可以用它来保留组件的状态。
理解如何结合使用 StateHelper
方法与自定义组件可能有点棘手,但一个常见的例子可以帮助澄清问题。让我们考虑以下自定义组件的使用示例:
<t:temperature unitto="Celsius" temp="100" />
在自定义组件类中,我们可以轻松地将这些属性名称和默认值进行映射,如下面的代码所示:
// Attribute name constant for unitto
private static final String ATTR_UNITTO = "unitto";
// Default value for the unitto attribute
private static final String ATTR_UNITTO_DEFAULT = "Fahrenheit";
// Attribute name constant for temp
private static final String ATTR_TEMP = "temp";
// Default value for the temp attribute
private static final Float ATTR_TEMP_DEFAULT = 0f;
接下来,我们希望在常量 ATTR_UNITTO
下保留 unitto
的值(对于 temp
来说,这完全相同)。为此,我们使用 StateHelper.put
方法,如下面的代码所示:
public void setUnitto(String unitto) {
getStateHelper().put(ATTR_UNITTO, unitto);
}
这些示例使用 Object put(Serializable key, Object value)
方法,但 StateHelper
还有一个名为 Object put(Serializable key, String mapKey, Object value)
的方法,可以用来存储那些本应存储在 Map
实例变量中的值。此外,StateHelper
还有一个名为 void add(Serializable key, Object value)
的方法,可以用来保留那些本应存储在 List
实例变量中的值。
接下来,你可以检索存储在 ATTR_UNITTO
常量下的值,如下面的代码所示:
public String getUnitto() {
return (String) getStateHelper().eval(ATTR_UNITTO, ATTR_UNITTO_DEFAULT);
}
Object eval(Serializable key, Object defaultValue)
方法将搜索 ATTR_UNITTO
常量。如果找不到,则返回默认值(ATTR_UNITTO_DEFAULT
)。这是一个非常有用的方法,因为它可以避免我们执行 null
值检查。除了这个方法之外,StateHelper
还具有 Object eval(Serializable key)
和 Object get(Serializable key)
方法。
为了从 StateHelper
中删除条目,我们可以调用 Object remove(Serializable key)
或 Object remove(Serializable key, Object valueOrKey)
。
在这个时候,我们有很多信息可以转化为代码,所以让我们编写一个自定义组件来举例说明上述知识。让我们称它为温度自定义组件。基本上,下一个自定义组件将公开一个作为 JSF 组件的公共网络服务。这个网络服务能够将摄氏度转换为华氏度,反之亦然,我们需要传递温度值和转换单位作为参数。基于这两个参数,我们可以直观地推断出相应的 JSF 标签将类似于以下代码:
<t:temperature unitto="celsius/fahrenheit" temp="*number_of_degrees*" />
我们可以先实现一个辅助类来处理通信任务背后的网络服务。这个类的名字是 TempConvertClient
,可以在名为 ch10_4
的完整应用程序中看到。它相关部分是以下方法的声明:
public String callTempConvertService(String unitto, Float temp) {
...
}
自定义组件实现
现在,我们可以专注于对我们来说重要的部分,即自定义组件实现。为此,我们可以遵循以下步骤:
-
编写一个带有
@FacesComponent
注解的类。 -
使用
StateHelper
在多个请求中保留组件的属性值。 -
调用
callTempConvertService
方法。 -
渲染结果。
前三个步骤可以编码如下:
@FacesComponent(value = TempConvertComponent.COMPONENT_TYPE, createTag = true, namespace = "http://temp.converter/", tagName = "temperature")
public class TempConvertComponent extends UIComponentBase {
public TempConvertComponent() {
setRendererType(TempConvertRenderer.RENDERER_TYPE);
}
public static final String COMPONENT_FAMILY =
"components.TempConvertComponent";
public static final String COMPONENT_TYPE =
"book.beans.TempConvertComponent";
private static final String ATTR_UNITTO = "unitto";
private static final String ATTR_UNITTO_DEFAULT = "fahrenheit";
private static final String ATTR_TEMP = "temp";
private static final Float ATTR_TEMP_DEFAULT = 0f;
public String getUnitto() {
return (String) getStateHelper().
eval(ATTR_UNITTO, ATTR_UNITTO_DEFAULT);
}
public void setUnitto(String unitto) {
getStateHelper().put(ATTR_UNITTO, unitto);
}
public Float getTemp() {
return (Float) getStateHelper().eval(ATTR_TEMP, ATTR_TEMP_DEFAULT);
}
public void setTemp(Float temp) {
getStateHelper().put(ATTR_TEMP, temp);
}
public String getTempConvert() {
TempConvertClient tempConvertClient = new TempConvertClient();
return String.format("%.1f", Float.valueOf(tempConvertClient.
callTempConvertService(getUnitto(), getTemp())));
}
@Override
public String getFamily() {
return TempConvertComponent.COMPONENT_FAMILY;
}
}
在第四步中,前一段代码中有一个提示。如果你仔细查看类构造函数,你会看到渲染任务被委托给一个外部类(renderer)。这个类将渲染一个包含网络服务响应的简单样式化 HTML div
,如下所示:
@ResourceDependencies({
@ResourceDependency(name="css/temp.css",library="default",target="head")
})
@FacesRenderer(componentFamily = TempConvertComponent.COMPONENT_FAMILY, rendererType = TempConvertRenderer.RENDERER_TYPE)
public class TempConvertRenderer extends Renderer {
public static final String RENDERER_TYPE =
"book.beans.TempConvertRenderer";
public TempConvertRenderer() {
}
@Override
public void encodeEnd(FacesContext context, UIComponent uicomponent)
throws IOException {
ResponseWriter responseWriter = context.getResponseWriter();
TempConvertComponent component = (TempConvertComponent) uicomponent;
String unit = component.getUnitto();
responseWriter.startElement("div", component);
responseWriter.writeAttribute("class", "tempClass", null);
responseWriter.writeAttribute("id", component.getClientId(), "id");
responseWriter.write("°");
if (unit.equals("fahrenheit")) {
responseWriter.write("F ");
} else {
responseWriter.write("C ");
}
responseWriter.write(component.getTempConvert());
responseWriter.endElement("div");
}
}
注意
@ResourceDependency
和 @ResourceDependencies
注解用于自定义组件和渲染器中的链接外部资源(例如,JavaScript 和 CSS)。
为了将此类注册为Renderer
类,您需要使用@FacesRenderer
注解它或在faces-config.xml
中进行配置,如下面的代码所示:
<application>
<render-kit>
<renderer>
<component-family>
components.TempConvertComponent
</component-family>
<renderer-type>book.beans.TempConvertRenderer</renderer-type>
<renderer-class>book.beans.TempConvertRenderer</renderer-class>
</renderer>
</render-kit>
</application>
Renderer
类的一个重要特性是它必须定义一个无参数的公共构造函数。
注意,<renderer-type>
标签对应于renderedType
元素,而<component-family>
标签对应于componentFamily
元素。此外,componentFamily
的值与组件的getFamily
方法返回的值相同。RenderKit
可以根据这些信息提供一个Renderer
实例。
当然,在这个例子中,您也可以在自定义组件类中实现渲染过程,因为没有真正的理由要编写一个单独的类。通常,当您需要支持多个客户端设备并且需要通过RenderKit
集合注册特殊渲染器时,您会想要编写一个单独的渲染器类。
下面的代码示例展示了如何使用我们的自定义组件(代码本身具有自解释性):
<html
>
...
<h:form>
Convert to:
<h:selectOneMenu value="#{tempBean.unitto}">
<f:selectItem itemValue="fahrenheit" itemLabel="fahrenheit" />
<f:selectItem itemValue="celsius" itemLabel="celsius" />
</h:selectOneMenu>
Insert value:
<h:inputText value="#{tempBean.temp}"/>
<h:commandButton value="Convert">
<f:ajax execute="@form" render=":tempId" />
</h:commandButton>
</h:form>
<t:temperature id="tempId"
unitto="#{tempBean.unitto}" temp="#{tempBean.temp}" />
或者,我们可以将转换单位和温度作为常量提供(如果其中一个或两个属性缺失,则将使用默认值):
<t:temperature id="tempId" unitto="celsius" temp="10" />
TempBean
只是一个简单的后端 Bean,如下面的代码所示:
@Named
@SessionScoped
public class TempBean implements Serializable {
private String unitto = "fahrenheit";
private Float temp = 0f;
...
//getters and setters
}
完整的应用程序位于本章的代码包中,名称为ch10_4
。在下面的屏幕截图中,您可以看到运行此应用程序的结果:
因此,我们的自定义组件只是渲染一个包含温度转换结果的div
块。进一步地,我们希望编写一个自定义组件,除了这个div
之外,它还将渲染用于收集数据(转换单位和温度)的用户界面,并通过 AJAX 提交。换句话说,前面表单的内容将是自定义组件的一部分。
这次我们需要直接在自定义组件中处理用户输入,这意味着我们的自定义组件可以扩展UIInput
而不是UIComponentBase
。这个主要变化将给我们带来UIInput
组件的优势。我们可以在解码过程中使用setSubmittedValue
方法提交自定义组件的值,并在编码(渲染)过程中使用getValue
方法获取结果值。
大问题在于我们的自定义组件值由两个值组成:转换单位和温度值。有一些解决方案可以解决这个问题。在这种情况下,我们可以简单地将这些值连接成一个,例如以下示例(conversion_unit/
temperature):
<t:temperature value="celsius/1" />
现在我们可以编写自定义组件类,如下面的代码所示:
@FacesComponent(createTag = true, namespace = "http://temp.converter/", tagName = "temperature")
public class TempConvertComponent extends UIInput
implements NamingContainer {
public TempConvertComponent() {
setRendererType(TempConvertRenderer.RENDERER_TYPE);
}
public String getTempConvert(String unitto, float temp) {
TempConvertClient tempConvertClient = new TempConvertClient();
return String.format("%.1f",
tempConvertClient.callTempConvertService(unitto, temp));
}
}
注意,我们不再需要指定组件家族,getFamily
方法是从UIInput
类继承的。进一步来说,我们需要编写渲染器类。
提示
如果你想要允许组件自行渲染,使用setRendererType(null)
并覆盖组件类中的相应方法。
我们需要渲染四个 HTML 标签(下拉列表、输入字段、提交按钮和结果div
)。为此,我们可以覆盖encodeEnd
方法,如下面的代码所示:
@Override
public void encodeEnd(FacesContext context, UIComponent uicomponent)
throws IOException {
TempConvertComponent component = (TempConvertComponent) uicomponent;
String clientId = component.getClientId(context);
char separator = UINamingContainer.getSeparatorChar(context);
encodeSelectOneMenu(context,
component, clientId + separator + "selectonemenu");
encodeInput(context, component, clientId + separator + "inputfield");
encodeButton(context, component, clientId + separator + "button");
encodeResult(context, component, clientId + separator + "div");
}
每个组件的标识符是从主组件的客户端 ID(使用getClientId
方法)拼接上命名容器分隔符和一个表示组件类型的字符串获得的。
注意
在这个例子中,查询NamingContainer
接口(由UINamingContainer
实现)以获取用于分隔客户端 ID 段分隔符,但其主要目的是确保其内部声明的组件的唯一性。
接下来,渲染下拉组件的方法如下:
private void encodeSelectOneMenu(FacesContext context, TempConvertComponent component, String clientId) throws IOException {
String cv = String.valueOf(component.getValue());
String unitto = cv.substring(0, cv.indexOf("/"));
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.startElement("span", component);
responseWriter.write("Convert to:");
responseWriter.endElement("span");
responseWriter.startElement("select", component);
responseWriter.writeAttribute("name", clientId, "clientId");
responseWriter.writeAttribute("size", 1, "size");
responseWriter.startElement("option", component);
responseWriter.writeAttribute("value", "fahrenheit", "value");
if (unitto.equals("fahrenheit")) {
responseWriter.writeAttribute("selected", "selected", "selected");
}
responseWriter.writeText("fahrenheit", "fahrenheit");
responseWriter.endElement("option");
responseWriter.startElement("option", component);
responseWriter.writeAttribute("value", "celsius", "value");
if (unitto.equals("celsius")) {
responseWriter.writeAttribute("selected", "selected", "selected");
}
responseWriter.writeText("celsius", "celsius");
responseWriter.endElement("option");
responseWriter.endElement("select");
}
这段代码的前两行很重要,其中我们提取了组件值中的转换单位部分,并在下拉组件中选择了相应的项。
接下来,我们渲染输入字段,如下面的代码所示:
private void encodeInput(FacesContext context, TempConvertComponent component, String clientId) throws IOException {
String cv = String.valueOf(component.getValue());
String temp = cv.substring(cv.indexOf("/") + 1);
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.startElement("span", component);
responseWriter.write("Insert value:");
responseWriter.endElement("span");
responseWriter.startElement("input", component);
responseWriter.writeAttribute("name", clientId, "clientId");
responseWriter.writeAttribute("value", temp, "value");
responseWriter.writeAttribute("type", "text", "type");
responseWriter.endElement("input");
}
现在我们将从组件值中提取温度值。为了完成这个任务,我们渲染了一个标记为转换的按钮,该按钮负责通过 AJAX 提交用户输入,如下所示:
private void encodeButton(FacesContext context, TempConvertComponent component, String clientId) throws IOException {
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.startElement("input", component);
responseWriter.writeAttribute("type", "Submit", null);
responseWriter.writeAttribute("name", clientId, "clientId");
responseWriter.writeAttribute("value", "Convert", null);
responseWriter.writeAttribute("onclick",
"jsf.ajax.request(this,event,{execute:'" + "@form" + "',"
+ "render:'" + "@form" + "'," + "});"
+ "return false;", null);
responseWriter.endElement("input");
}
在用户输入提交后,我们需要渲染从网络服务获得的结果:
private void encodeResult(FacesContext context, TempConvertComponent component, String clientId) throws IOException {
String cv = String.valueOf(component.getValue());
String unitto = cv.substring(0, cv.indexOf("/"));
String temp = cv.substring(cv.indexOf("/") + 1);
String result = component.getTempConvert(unitto, Float.valueOf(temp));
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.startElement("div", component);
responseWriter.writeAttribute("class", "tempClass", null);
responseWriter.writeAttribute("name", clientId, "clientId");
responseWriter.write("°");
if (unitto.equals("fahrenheit")) {
responseWriter.write("F ");
} else {
responseWriter.write("C ");
}
responseWriter.write(result);
responseWriter.endElement("div");
}
后端 Bean,TempBean
,相当简单,如下面的代码所示:
@Named
@SessionScoped
public class TempBean implements Serializable {
private String value = "celsius/0";
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
最后一步涉及解码用户输入并将其提交给组件,如下面的代码所示:
@Override
public void decode(FacesContext context, UIComponent uicomponent) {
TempConvertComponent component = (TempConvertComponent) uicomponent;
Map requestMap = context.getExternalContext().getRequestParameterMap();
String clientId = component.getClientId(context);
char separator = UINamingContainer.getSeparatorChar(context);
String temp = ((String)
requestMap.get(clientId+ separator + "inputfield"));
String unitto = ((String)
requestMap.get(clientId + separator + "selectonemenu"));
component.setSubmittedValue(unitto+"/"+temp);
}
完成!现在你可以看到两个测试,如下面的代码所示:
Specified unit and temperature from bean:
<h:form id="tempForm1">
<t:temperature id="temp1" value="#{tempBean.value}" />
<h:message for="temp1"/>
</h:form>
<hr/>
Specified unit and temperature as constants:
<h:form id="tempForm2">
<t:temperature id="temp2" value="celsius/1" />
<h:message for="temp2"/>
</h:form>
完整的应用程序包含在本章的代码包中,名称为ch10_5
。
构建组合组件
可能组合组件背后的想法源于 JSF 页面作者和 JSF 组件作者对组件的不同看法。虽然 JSF 页面作者将组件视为可以在 XHTML 页面中使用的标签,但 JSF 组件作者将组件视为UIComponent
、UIComponentBase
、NamingContainer
、Renderer
、Validator
和Converter
元素的混合体——这些元素构成了 JSF 组件。基于此,似乎只有 JSF 组件作者才能编写自定义组件,因为他们对这些 JSF 元素和 Java 语言有了解。然而,从 JSF 2 开始,这一事实已经开始改变,组合组件实际上是在 XHTML 页面中使用标记标签编写的自定义组件。这意味着 JSF 页面作者可以开始编写他们的组件,而无需具备与专门的 JSF 组件作者相同水平的知识和技能——至少,简单的组合组件。
例如,JSF 2.2 组合组件的结构如下所示:
<?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
>
<!-- INTERFACE -->
<cc:interface>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
</cc:implementation>
</html>
结构非常简单!正如你所见,有两个主要标签属于http://xmlns.jcp.org/jsf/composite
库。第一个标签定义了接口部分,表示组件使用合同。在这里,我们可以定义可能被最终用户更改的组件属性(原则上,任何页面作者可以使用的属性)。第二个标签标记了实现部分,其中包含组件本身。这将渲染给最终用户。此外,在这个部分中,我们根据接口部分中定义的属性(使用合同实现)定义组件的行为。
注意
复合组件基本上是存储在resources
文件夹下库中的 XHTML 页面(放置在 Web 应用程序根目录下的顶级文件夹或 JAR 中的META-INF
文件夹下)。记住,库只是resources
文件夹的子文件夹。基于此,复合组件路径的类型为http://xmlns.jcp.org/jsf/composite/
library_name。
那么,让我们来做一个快速测试。记住,在本章中首先开发的自定义组件WelcomeComponent
是从一个带有@FacesComponent
注解的类构建的。在这个类中,我们重写了encodeBegin
方法以渲染组件。好的,现在让我们看看同一个组件,但这次是一个复合组件。我们将这个页面存储在resources/customs/welcome.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
>
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="value"/>
<cc:attribute name="to"/>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<p>#{cc.attrs.value}, #{cc.attrs.to}</p>
</cc:implementation>
</html>
将自定义组件与其复合版本进行类比非常简单。这里重要的是要注意属性是如何使用<cc:attribute>
标签声明的。除了名称外,属性可以有类型,可以是必需的或非必需的,可以有默认值,可以针对组件,等等(在本章中,你将有机会探索不同类型的属性)。一般来说,JSF 确定属性是MethodExpression
(或者它有如actionListener
、valueChangeListener
、action
等特殊名称)还是ValueExpression
。
第一个案例有点棘手;JSF 会尝试根据targets
属性中定义的 ID 列表(ID 列表由空格分隔,相对于顶级组件)将属性与实现中的组件进行匹配。如果不存在targets
属性,那么 JSF 将name
属性的值作为客户端 ID(相对于顶级组件)并尝试在实现部分中找到相应的组件。嗯,在简单的情况下,属性是一个ValueExpression
,JSF 将只将属性存储在可通过UIComponent.getAttributes
访问的属性映射中。
在实现部分,通过#{cc}
隐含对象使用属性。
注意
有必要知道,JSF 会隐式地为所有构成复合组件的组件创建一个顶级组件。这个组件是页面中所有组件的父组件,命名为UINamingContainer
(通过UIComponent.getNamingContainer
方法可用)。现在,#{cc}
隐式对象实际上指的是这个顶级组件,可以用来获取各种信息,但它特别用于获取客户端 ID(#{cc.clientId}
)和访问复合组件属性(#{cc.attrs}
)。
现在是时候测试我们的复合组件了。这非常简单——只需导入复合命名空间,设置一个前缀,然后开始使用它,如下面的代码所示:
<?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
>
<h:head>
<title></title>
</h:head>
<h:body>
**<t:welcome value="Welcome" to="Rafael Nadal"/>**
</h:body>
</html>
通过大量实践学习编写复合组件的技术。这就是为什么在接下来的章节中,您将看到几种不同类型的复合组件,它们探索了不同的实现方式。
完整的应用程序命名为ch10_6
。
开发温度复合组件
主演:后端组件
还记得我们在上一节中实现的温度自定义组件吗?好吧,我们确信您记得。那么,让我们看看如何开发一个看起来和表现相同的复合组件。复合组件页面可以命名为temperature.xhtml
,我们可以将其存储在resources
文件夹下的temperature
文件夹中。首先,让我们在下面的代码中看看它;然后我们可以分析它:
<?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
>
<!-- INTERFACE -->
<cc:interface componentType="book.beans.TempConvertComponent">
<cc:attribute name="value" type="java.lang.String"
default="celsius/0"/>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:outputStylesheet name="temperature/temp.css" />
<div id="#{cc.clientId}">
<h:outputLabel for="selectonemenu" value="Convert to:"/>
<h:selectOneMenu id="selectonemenu" binding="#{cc.unittoI}">
<f:selectItem itemValue="fahrenheit" itemLabel="fahrenheit" />
<f:selectItem itemValue="celsius" itemLabel="celsius" />
</h:selectOneMenu>
<h:outputLabel for="inputfield" value="Insert value:"/>
<h:inputText id="inputfield" binding="#{cc.temptI}"/>
<h:commandButton id="button" value="Convert">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
<h:panelGroup id="div" layout="block" class="tempClass">
<h:outputText value="° #{cc.unittoI.value eq 'fahrenheit' ? 'F ': 'C ' } #{cc.getTempConvert()}"/>
</h:panelGroup>
</div>
</cc:implementation>
</html>
在接口部分,我们定义了一个名为value
的属性,这是针对UIInput
组件的。进一步地,我们指出接受值为String
类型,当属性缺失时,默认值为celsius/0
。通常,type
属性用于将元素(们)链接到 bean 的属性(对于其值,使用完全限定名,如前述代码所示)。
实现部分更有趣,因为在这里我们需要定义我们组件的子组件:下拉菜单、输入字段、提交按钮和结果 div(注意 JSF 从<h:panelGroup layout="block"/>
生成一个 HTML <div>
)。当您的复合组件包含多个组件时,将它们放置在 ID 设置为#{cc.clientId}
的<div>
或<span>
标签内是一个好习惯。这个 ID 是复合组件本身的客户端标识符,当页面作者需要通过一个简单的 ID 引用整个复合组件时非常有用。
外部资源,如 CSS 和 JS,不需要任何特殊处理。您可以将它们放置在与复合组件相同的库中,或者放置在任何其他库中,并且可以使用<h:outputScript>
和<h:outputStylesheet>
来加载它们。
快速查看后,一个明显的问题出现了:getTempConvert
方法的实现和用于根据binding
属性链接这些组件的后端 bean 属性在哪里?好吧,所有这些都在一个 Java 类中,称为后端组件(不要与后端 bean 混淆!)。是的,我知道我之前说过复合组件不需要 Java 代码,但有时它们确实需要,就像在这个案例中,我们需要编写调用 Web 服务的代码!为了编写后端组件,您需要记住以下步骤:
-
使用
@FacesComponent
注解标注后端组件 -
扩展
UINamingContainer
或实现NamingContainer
并覆盖getFamily
方法如下:@Override public String getFamily() { return UINamingContainer.COMPONENT_FAMILY; } ```**
-
通过向
<cc:interface>
标签添加componentType
属性将复合组件与后端组件链接。此属性的值是组件类型(这告诉 JSF 创建此处指示的类的实例)@FacesComponent(value = "*component type value*") ... <cc:interface componentType="*component type value*"> ```**
注意
后端组件可以定义 getter 来通过#{cc}
隐式对象(#{cc}
可以访问操作方法)公开其属性。另一方面,<cc:attribute>
属性可以通过UIComponent.getAttributes
方法在后端组件中访问。
考虑到这些,我们的复合组件的后端组件如下:
@FacesComponent(value = "book.beans.TempConvertComponent",
createTag = false)
public class TempConvertComponent extends UIInput implements NamingContainer {
private UIInput unittoI;
private UIInput temptI;
public TempConvertComponent() {
}
public UIInput getUnittoI() {
return unittoI;
}
public void setUnittoI(UIInput unittoI) {
this.unittoI = unittoI;
}
public UIInput getTemptI() {
return temptI;
}
public void setTemptI(UIInput temptI) {
this.temptI = temptI;
}
public String getTempConvert() {
TempConvertClient tempConvertClient = new TempConvertClient();
return String.format("%.1f",
tempConvertClient.callTempConvertService(String.valueOf(unittoI.getValue()), Float.valueOf(String.valueOf(temptI.getValue()))));
}
@Override
public void decode(FacesContext context) {
this.setSubmittedValue(temptI.getSubmittedValue() + "/" +
unittoI.getSubmittedValue());
}
/*
* you can override getSubmittedValue instead of decode
@Override
public Object getSubmittedValue() {
return temptI.getSubmittedValue() + "/" + unittoI.getSubmittedValue();
}
*/
@Override
public void encodeBegin(FacesContext context) throws IOException {
if (getValue() != null) {
String cv = String.valueOf(getValue());
String unitto = cv.substring(0, cv.indexOf("/"));
String temp = cv.substring(cv.indexOf("/") + 1);
if (temptI.getValue() == null) {
temptI.setValue(temp);
}
if (unittoI.getValue() == null) {
unittoI.setValue(unitto);
}
}
super.encodeBegin(context);
}
@Override
public String getFamily() {
return UINamingContainer.COMPONENT_FAMILY;
}
}
我们的后端组件的故事相当清晰。在encodeBegin
方法中,我们确保组件值被解析,并且每个子组件(下拉列表和输入字段)都收到了正确的值部分。当用户提交数据时,我们在encode
方法中处理它,其中我们获取下拉列表和输入字段的值,并构建一个类型为conversion_unit``/``temperature
的字符串。这成为提交的值。
现在是指出 JSF 如何选择顶级组件的好时机。JSF 试图做以下事情:
-
在
<cc:interface>
标签中定位componentType
属性。如果存在,则后端组件将被实例化并用作顶级组件。这就是温度复合组件的情况。 -
定位一个与复合组件页面相同名称和位置的
UIComponent
实现。这可以是一个与复合组件页面同名同地的 Groovy 脚本(当然,带有.groovy
扩展名)。 -
定位一个名为
component_library_name.composite_component_page_name
的 Java 类,并将其实例化为顶级组件。这种方法使我们能够使用@FacesComponent
。 -
生成一个类型为
javax.faces.NamingContainer
的组件。
完整的应用程序命名为ch10_8
。基于通过这个教学示例引入的知识,您可以查看另一个名为时区的示例,如下截图所示。完整的应用程序命名为ch10_25
。
将 jQuery 组件转换为复合组件
亮点:JavaScript 闭包
jQuery UI 是在 jQuery JavaScript 库之上构建的出色的用户界面交互、效果、小部件和主题集合。在本节中,你将了解如何将 jQuery 组件公开为 JSF 复合组件。更确切地说,我们将转换 jQuery 范围滑块(jqueryui.com/slider/#range
),如下面的截图所示:
此组件背后的主要代码如下所示:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<title>Range Slider with jQuery UI</title>
<link rel="stylesheet" type="text/css" media="all"
href="css/styles.css">
<link rel="stylesheet" type="text/css" media="all" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/base/jquery-ui.css">
<script type="text/javascript" src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/jquery-ui.min.js"></script>
</head>
<body>
<div id="w">
<div id="content">
<h2>Ranged Slider</h2>
<div id="rangedval">
Range Value: <span id="rangeval">90 - 290</span>
</div>
<div id="rangeslider"></div>
</div>
</div>
<script type="text/javascript">
$(function(){
$('#rangeslider').slider({
range: true,
min: 0,
max: 100,
values: [ 5, 20 ],
slide: function( event, ui ) {
$('#rangeval').html(ui.values[0]+" - "+ui.values[1]);
}
});
});
</script>
</body>
</html>
JSF 复合组件应如下所示(重要部分已突出显示):
<h:form id="sliderFormId">
<h:panelGrid columns="1">
**<t:range-slider id="sliderId" min="#{sliderBean.min}"**
**max="#{sliderBean.max}" leftside="#{sliderBean.leftside}"**
**rightside="#{sliderBean.rightside}"/>**
<h:commandButton value="Send" action="#{sliderBean.sliderListener()}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
</h:panelGrid>
</h:form>
我们可以从定义复合组件属性开始。这些属性将允许最终用户设置最小值(min
属性)、最大值(max
属性)和初始范围(leftside
和rightside
属性)。这些属性将在接口部分声明,如下面的代码所示:
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="min" type="java.lang.Integer"
default="0" required="true"/>
<cc:attribute name="max" type="java.lang.Integer"
default="1000" required="true"/>
<cc:attribute name="leftside" type="java.lang.Integer"
default="450" required="true"/>
<cc:attribute name="rightside" type="java.lang.Integer"
default="550" required="true"/>
</cc:interface>
实现部分可以分为三个逻辑部分。在第一部分中,我们定义外部资源(CSS 和 JS 文件)。请注意,<h:outputScript>
和<h:outputStylesheet>
不能为绝对 URL(http://...
)加载此类资源,因此你需要将这些资源放在你的本地机器上:
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:outputStylesheet name="range-slider/css/styles.css"/>
<h:outputStylesheet name="range-slider/css/jquery-ui.css"/>
<h:outputScript target="head" name="range-slider/js/jquery.min.js"/>
<h:outputScript target="head" name="range-slider/js/jquery-ui.min.js"/>
...
在第二部分,我们渲染显示范围滑块的 div。为此,我们遵循原始组件的精确模型,但添加了我们的属性leftside
和rightside
,如下面的代码所示:
<div id="#{cc.clientId}:w" class="w">
<div id="#{cc.clientId}:content" class="content">
<div id="#{cc.clientId}:rangedval" class="rangedval">
Range Value: <span id="#{cc.clientId}:rangeval">
#{cc.attrs.leftside} - #{cc.attrs.rightside}</span>
</div>
<div id="#{cc.clientId}:slider">
</div>
</div>
<h:inputHidden id="leftsideId" value="#{cc.attrs.leftside}"/>
<h:inputHidden id="rightsideId" value="#{cc.attrs.rightside}"/>
</div>
虽然min
和max
属性可以设置为特定值,但我们特别关注leftside
和rightside
属性,它们应被视为最终用户的输入。为此,我们添加了两个隐藏字段(一个用于leftside
,一个用于rightside
),可以轻松地将此信息传输到服务器。
在第三部分,我们需要调整代表组件引擎的 JavaScript 代码。当在同一页面上添加多个范围滑块时,此代码必须正确生成,因此我们需要按以下方式修改它以适应正确的 ID 和属性值:
<script type="text/javascript">
$(function() {
var rangeval = "${cc.clientId}:rangeval".replace(/:/g, "\\:");
var slider = "${cc.clientId}:slider".replace(/:/g, "\\:");
$('#' + slider).slider({
range: true,
min: #{cc.attrs.min},
max: #{cc.attrs.max},
values: [#{cc.attrs.leftside}, #{cc.attrs.rightside}],
slide: function(event, ui) {
$('#' + rangeval).html(ui.values[0] + " - " + ui.values[1]);
$("#${cc.clientId}:leftsideId".
replace(/:/g, "\\:")).val(ui.values[0]);
$("#${cc.clientId}:rightsideId".
replace(/:/g, "\\:")).val(ui.values[1]);
}
});
});
</script>
</cc:implementation>
注意
将 JSF 和 jQuery 结合使用时,一个常见问题涉及使用冒号(:
)。虽然 JSF 将其用作 ID 段分隔符,但 jQuery 选择器有其他用途。为了在 jQuery 中工作,我们需要转义冒号。如果你使用 PrimeFaces 方法,这可以很容易地完成,如下面的代码所示:
escadpeClientId:function(a){return"#"+a.replace(/:/g,"\\:")}
完成!现在你可以在你的页面上测试复合组件。完整的应用程序命名为ch10_11
。
好吧,如果您在页面中添加多个范围滑块,您将看到前面的 JavaScript 代码每次都会生成并添加。这段代码的大小微不足道,您在同一页面上需要多个范围滑块的可能性很小,所以这不会是一个大问题。但是,当出现这样的问题时,您需要知道有一些解决方案。
例如,我们可以将 JavaScript 从复合组件中提取出来并放置到页面中,或者作为一个应该自包含的组件,最好是将代码放置到一个单独的 JavaScript 文件中,并在复合组件中使用 <h:outputScript>
引用它。之后,我们使用期望的属性参数化 JavaScript 代码,并从复合组件中调用它。因此,参数化版本可能看起来像以下代码(将此代码放入名为 slider.js
的文件中):
var rangeslider = {
init: function(clientId, min, max, leftside, rightside) {
var rangeval = (clientId + ":rangeval").replace(/:/g, "\\:");
var slider = (clientId + ":slider").replace(/:/g, "\\:");
$('#' + slider).slider({
range: true,
**min: min,**
**max: max,**
**values: [parseInt(leftside), parseInt(rightside)],**
slide: function(event, ui) {
$('#' + rangeval).html(ui.values[0] + " - " + ui.values[1]);
$("#" + (clientId + ":leftsideId").
replace(/:/g, "\\:")).val(ui.values[0]);
$("#" + (clientId + ":rightsideId").
replace(/:/g, "\\:")).val(ui.values[1]);
}
});
}
};
进一步,我们调整了复合组件实现部分以调用以下可重用 JavaScript 代码:
<cc:implementation>
...
<h:outputScript target="head" name="range-slider/js/slider.js"/>
...
<script>
rangeslider.init('#{cc.clientId}', '#{cc.attrs.min}',
'#{cc.attrs.max}', '#{cc.attrs.leftside}', '#{cc.attrs.rightside}');
</script>
</cc:implementation>
可能您已经看到了 JavaScript 闭包的技术。这个想法是推测 JavaScript 是一种动态语言,它允许我们在运行时修改 DOM。使用 JSF 客户端标识符和这种 JavaScript 功能可以帮助我们解决为多个组件重复代码的问题。有时,一个好的做法是将整个复合组件放在一个 ID 为 JSF 客户端标识符的 div
元素中。此外,您可以直接从 JavaScript 中识别和管理每个 div
的内容。
本例的完整应用程序命名为 ch10_9
。如果您想直接将 JavaScript 代码放入页面中,请检查名为 ch10_26
的应用程序。除了这个应用程序之外,另一个使用 JavaScript 闭包的完整示例应用程序命名为 ch10_7
。在这个例子中,一个复合组件封装了一个 HTML5 SSE(服务器端事件)示例。对于那些不熟悉 SSE 的人来说,一个好的起点是 www.html5rocks.com/en/tutorials/eventsource/basics/
上的教程。
将 HTML5 日期选择器编写为复合组件
主演:<cc:clientBehavior>
和 <cc:insertChildren />
在本节中,您将了解如何将 HTML5 日期选择器组件转换为复合组件。有一些属性允许我们自定义原生日期选择器组件。以下是一个包含三个示例的列表:
-
最简单情况的代码如下:
<input id="exId" type="date" value="" /> ```**
-
约束日期选择器的代码如下:
<input id="exId" type="date" value="2015-01-05" min="2015-01-01" max="2015-01-31" /> ```**
-
具有数据列表的日期选择器的代码如下:
<input id="exId" type="date" value="" list="listId" /> <datalist id="listId"> <option label="Day 1" value="2015-01-01"/> <option label="Day 2" value="2015-01-02" /> <option label="Day 3" value="2015-01-03" /> </datalist> ```**
我们的复合组件应该反映这些形式,所以它可能看起来像以下代码:
Date-time without data-list:
<h:form>
<t:datetime value="#{dateTimeBean.date}"
min="#{dateTimeBean.min}" max="#{dateTimeBean.max}">
<f:ajax event="change" execute="@form"
listener="#{dateTimeBean.selectedDate()}"/>
</t:datetime>
</h:form>
或者,使用数据列表,如下面的代码所示:
Date-time with data-list:
<h:form>
<t:datetime list="listId" value="#{dateTimeBean.date}">
<f:ajax event="change" execute="@form"
listener="#{dateTimeBean.selectedDate()}"/>
</t:datetime>
<t:datalist id="listId">
<t:option label="Day 1" value="2015-01-01"/>
<t:option label="Day 2" value="2015-01-02"/>
<t:option label="Day 3" value="2015-01-03"/>
</t:datalist>
那么,让我们专注于接口定义。首先,我们有一组很容易定义的属性,例如value
、list
、step
、required
和readonly
:
<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="required" type="java.lang.String"
default="false" />
<cc:attribute name="readonly" type="java.lang.String"
default="false" />
这很简单!现在我们需要更仔细地看看min
和max
属性,它们定义了选择范围。实际上,它们只是一些日期,但不是java.util.Date
的实例,因为它们的格式是 HTML 5 特有的(y-m-d),而不是 Java。这意味着我们需要一些 Java 代码来完成从 Java 日期格式到 HTML5 日期格式的转换。我们需要一个后端组件来完成这个任务(注意,我们在这里不能使用任何转换器):
@FacesComponent(value = "datetime")
public class DateTimeComponent extends UINamingContainer {
private static final DateTimeFormatter HTML5_FORMAT =
DateTimeFormat.forPattern("yyyy-MM-dd");
private String minHTML5Date = "";
private String maxHTML5Date = "";
public String getMinHTML5Date() {
return minHTML5Date;
}
public String getMaxHTML5Date() {
return maxHTML5Date;
}
@Override
public void encodeBegin(FacesContext context) throws IOException {
Date min = (Date) getAttributes().get("min");
if (min != null) {
DateTime minDateTime = new DateTime(min);
minHTML5Date = HTML5_FORMAT.print(minDateTime);
}
Date max = (Date) getAttributes().get("max");
if (max != null) {
DateTime maxDateTime = new DateTime(max);
maxHTML5Date = HTML5_FORMAT.print(maxDateTime);
}
super.encodeBegin(context);
}
}
当然,我们不会忘记在接口和限制选择属性中指明后端组件,现在这些属性可以声明为java.util.Date
,如下面的代码所示:
<cc:interface componentType="datetime">
<cc:attribute name="min" type="java.util.Date" />
<cc:attribute name="max" type="java.util.Date" />
...
在接口中我们还需要做一件事。当最终用户选择一个日期时,我们希望它通过 AJAX 提交给后端 bean。为此,我们需要允许他/她附加客户端行为(我们在本书中多次提到了客户端行为,但一个完美的教程可以在 DZone 找到,java.dzone.com/articles/jsf-2-client-behaviors
),为此我们需要使用<cc:clientBehavior>
标签,如下面的代码所示。name
属性包含将监听的事件名称(例如,这里的change
)和targets
属性指示实现中的组件(哪些组件)将通过event
属性支持声明的 JavaScript 事件(JavaScript 事件不要使用on
前缀)。
<cc:clientBehavior name="change" targets="#{cc.id}" event="change" />
到目前为止,接口已经准备好了!进一步来说,我们需要一个实现。这很简单,基于 JSF 2.2 的透传元素,如下面的代码所示:
<cc:implementation>
<div id="#{cc.clientId}:dt">
<input jsf:id="#{cc.id}" type="date" jsf:value="#{cc.attrs.value}"
jsf:readonly="#{cc.attrs.readonly != 'false' ? 'true': 'false'}"
min="#{cc.minHTML5Date}" max="#{cc.maxHTML5Date}"
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>
注意
为了方便引用<datalist>
,我们使用了#{cc.id}
。如果组件在页面中被多次使用,那么你必须为每次使用指定一个唯一的 ID。不过,如果你需要一个干净的解决方案,避免在渲染的 XHTML 文档中出现非唯一 ID,你可能需要做一些额外的 ID 解析(例如使用 JavaScript 或在后端组件中完成)。
在这个时候,我们可以使用我们的复合组件,除了数据列表(参见前面的 HTML5 <datalist>
)。为此,我们需要编写两个额外的复合组件。正如你所看到的,数据列表只是一组选项(项目),每个选项有两个属性,分别命名为label
和value
。因此,我们可以很容易地将一个选项封装在一个复合组件中,如下面的代码所示:
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="label" type="java.lang.String" default="" />
<cc:attribute name="value" type="java.lang.String" default="" />
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<option value="#{cc.attrs.value}" label="#{cc.attrs.label}" />
</cc:implementation>
现在我们需要在 <datalist>
中嵌套几个选项,但在这里这不合适,因为选项的数量是不确定的。幸运的是,对于这类情况,JSF 提供了 <cc:insertChildren>
标签,它用于在父组件(们)内插入子组件(子组件将由 JSF 自动重新设置父组件)。了解这一点后,我们可以为 <datalist>
编写以下复合组件:
<!-- INTERFACE -->
<cc:interface>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<datalist id="#{cc.id}">
<cc:insertChildren />
</datalist>
</cc:implementation>
注意
仅在 <cc:implementation>
中使用 <datalist>
标签,并小心避免重复 ID 错误。为了避免这种情况,建议您只使用此标签一次。要找出子组件的数量,请使用 #{cc.childCount}
。
完成!现在你可以尝试测试名为 ch10_12
的完整应用程序了。
注意
为了完整性,你可以处理浏览器不支持 HTML5 的情况,通过回到这个组件的 jQuery UI 版本。这可以通过 Modernizr 库(modernizr.com/
)来完成,该库能够检测这类问题。从我们的观点来看,这类浏览器在未来将会过时,所以我们认为添加这个检查和回退的工作是不必要的。
使用动作装饰图像
主演:<cc:actionSource>
,Java enum
类型
复合组件非常神奇,因为它们可以将简单的事物转换成真正的强大组件。例如,在本节中,我们将通过添加动作和动作监听器支持,将一个简单的图像装饰成一个具有 AJAX 和动作能力的复合组件。此外,我们将强制页面作者只使用一定范围内的值来设置某个属性。
首先,我们处理接口部分,我们可以从声明一个表示图像位置的属性开始,如下面的代码所示:
<cc:attribute name="src" required="true"/>
在这短暂的预热之后,我们可以声明 action
属性。页面作者将使用此属性来指示后端 Bean 的 action
方法。请注意,action
方法签名必须在这里声明,如下面的代码所示:
<cc:attribute name="action" method-signature="void action()"/>
当你编写方法签名时,你需要指明返回类型(在这种情况下为 void
),方法名和参数类型。例如,一个返回 String
并接受两个 Integer
参数的 action
方法将被声明如下:
method-signature="java.lang.String
myMethod(java.lang.Integer, java.lang.Integer)"
此外,我们添加了对 <f:actionListener>
标签的支持。为此,我们使用 <cc:actionSource>
标签如下:
<cc:actionSource name="imgActionListener" targets="#{cc.clientId}:imgForm:imgAction"/>
name
属性的值将被页面作者用作 <f:actionListener>
的 for
属性的值。targets
属性指向实现部分中的组件(们),这些组件接收这个能力。
注意
<cc:actionSource>
标签指定了 ActionSource2
的实现。
接下来,我们使用 <cc:clientBehavior>
标签声明一个客户端行为,如下所示:
<cc:attribute name="item" targets="#{cc.clientId}:
imgForm:imgAction" required="true"/>
作为最后的润色,我们添加了一个只能接受一系列值的属性,如下面的代码所示:
<cc:attribute name="item" targets="#{cc.clientId}:
imgForm:imgAction" required="true"/>
接口部分已经准备好了,让我们专注于实现部分。为了更好地理解,让我们如下查看:
<cc:implementation>
<h:outputStylesheet library="default" name="css/styles.css" />
<ui:param name="range" value="#{cc.attrs.item}" /> <!-- or c:set -->
<ui:fragment rendered="#{range == 'item_1' or
range == 'item_2' or range == 'item_3'}">
<div id="#{cc.clientId}:img">
<h:form id="imgForm">
<h:commandLink id="imgAction" immediate="true"
action="#{cc.attrs.action}" styleClass="linkopacity">
<h:inputHidden id="itemId" value="#{cc.attrs.item}"/>
<h:graphicImage value="#{cc.attrs.src}"/>
</h:commandLink>
</h:form>
</div>
</ui:fragment>
</cc:implementation>
这里有几个有趣的地方!让我们以下面的几个点从内到外剖析代码:
-
首先,图像通过 JSF 经典样式中的
<h:graphicImage>
标签加载。这里没有太多花哨的东西! -
<h:graphicImage>
标签嵌套在<h:commandLink>
标签中,该标签支持动作和动作监听器功能。请注意,这个组件是从接口部分定位的。此外,我们在其中嵌套了一个隐藏字段(<h:inputHidden>
),它与我们的图像关联(持有)一个值。这个值通过item
属性从一系列允许的值中获取。 -
<h:commandLink>
标签嵌套在<h:form>
标签中,所有内容都添加到<div>
标签中。请注意,通常在组合组件中添加<h:form>
不是一个好习惯,因为页面作者可能希望在他的/她的<h:form>
中使用组合组件。这将导致嵌套表单,从而导致无效的 HTML 代码。 -
为了将属性值限制在一系列值范围内,你可能想到使用 Java 枚举类型。问题是,你无法在接口部分这样做,但你可以在实现部分添加检查。例如,我们选择在
item
属性的值不是item_1
、item_2
和item_3
时,不渲染组合组件。
组合组件已经准备好测试。一个完美的例子可以在本章的代码包中看到,名称为ch10_18
。基于同样的原则,我们编写了另一个名为ch10_13
的例子。
与组合面交互
主演:<cc:facet>
、<cc:renderFacet>
和<cc:insertFacet>
组合组件在接口部分包含面定义。为此,我们需要使用<cc:facet>
标签,并通过name
属性指定至少一个面名,如下面的代码所示:
<cc:facet name="name" />
<cc:facet name="surname" />
一旦在接口部分声明了面,它们可以通过<cc:renderFacet>
标签在实现部分使用。对于这个标签,我们需要指定要渲染哪个面,通过设置name
属性的值与接口部分中定义的相应面相一致,如下面的代码所示:
<cc:renderFacet name="name" required="true"/>
<cc:renderFacet name="surname" required="true"/>
这就结束了!你可以在本章的代码包中看到完整的示例,名称为ch10_14
。
除了<cc:renderFacet>
之外,组合组件还支持<cc:insertFacet>
标签。现在事情变得更有趣了,因为一个常见的问题是,它们之间有什么区别?最好的答案将来自一个例子。让我们看看以下代码中一个简单的使用<cc:renderFacet>
的组合组件:
<!-- INTERFACE -->
<cc:interface>
<cc:facet name="header" />
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<cc:renderFacet name="header"/>
<!-- this will not render -->
<!-- <cc:insertFacet name="header"/> -->
</cc:implementation>
以下用法可以正确渲染:
...
...
<q:renderfacet>
<f:facet name="header">
Render Facet
</f:facet>
</q:renderfacet>
然而,将 <cc:renderFacet>
替换为 <cc:insertFacet>
也不会起作用。将不会渲染任何内容。
现在让我们看看以下使用 <cc:insertFacet>
的复合组件:
<!-- INTERFACE -->
<cc:interface>
<cc:facet name="header" />
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:dataTable border="1">
<cc:insertFacet name="header"/>
<!-- this will not render -->
<!-- <cc:renderFacet name="header"/> -->
</h:dataTable>
</cc:implementation>
以下代码片段将渲染所需的结果:
<t:insertfacet>
<f:facet name="header">
Insert Facet
</f:facet>
</t:insertfacet>
但是,再次强调,将 <cc:insertFacet>
替换为 <cc:renderFacet>
不会起作用。将不会渲染任何内容。
因此,我们可以得出结论,<cc:renderFacet>
对于将方面作为复合组件的子组件进行渲染非常有用。这意味着 <cc:renderFacet>
允许我们在父组件不支持方面时渲染方面;方面名称可以是任何接受的字符串。另一方面,<cc:insertFacet>
允许我们仅在支持方面的组件中渲染方面。在这里,方面名称必须存在于顶级组件的方面映射中。方面作为嵌套此元素的组件的方面子组件被插入。
完整的应用程序命名为 ch10_17
。
在复合组件内验证/转换输入
重点:<cc:editableValueHolder>
让我们快速查看以下代码中的简单复合组件,特别是突出显示的部分:
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="name" type="java.lang.String" required="true"/>
<cc:attribute name="surname" type="java.lang.String"
required="true" />
**<cc:editableValueHolder name="playerId" targets="nameId surnameId"/>**
<cc:attribute name="action"
method-signature="void action()" required="true" />
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:messages/>
<h:outputLabel for="**nameId**" value="Player Name:"/>
<h:inputText id="nameId" value="#{cc.attrs.name}" />
<h:outputLabel for="**surnameId**" value="Player Surname:"/>
<h:inputText id="surnameId" value="#{cc.attrs.surname}" />
<h:commandButton id="button" value="Submit" action="#{cc.attrs.action}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
</cc:implementation>
现在我们编写一个使用此组件并附加自定义转换器和自定义验证器的页面,如下所示:
<h:form id="playerFormId">
<t:player name="#{playerBean.name}" surname="#{playerBean.surname}"
action="#{playerBean.playerAction()}">
<f:converter converterId="playerConverter" **for="playerId"**/>
<f:validator validatorId="playerValidator" **for="playerId"**/>
</t:player>
</h:form>
由于 <cc:editableValueHolder>
的存在,一切工作都如预期般进行。在这种情况下,此标签告诉 JSF,任何具有 for
属性值等于 playerId
的转换器/验证器都应该应用于目标组件,即 nameId
和 surnameId
。一般来说,< cc:editableValueHolder>
指示实现 EditableValueHolder
的组件,因此任何适合实现 EditableValueHolder
的对象都可以附加到复合组件上。
完整的应用程序命名为 ch10_10
。
正如你所知,EditableValueHolder
是 ValueHolder
的扩展。除了 <cc:editableValueHolder>
之外,JSF 定义了一个名为 <cc:valueHolder>
的标签,它指示实现 ValueHolder
的组件。
检查属性的存在
有时,只有当作者页面中存在某个属性时,才需要渲染复合组件。例如,以下复合组件检查 mandatory
属性的存在:
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="value" required="true"/>
<cc:attribute name="mandatory" type="java.lang.Boolean"/>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:panelGroup rendered="#{not empty cc.attrs.mandatory}">
<h:inputText value="#{cc.attrs.value}"
required="#{cc.attrs.mandatory}"/>
</h:panelGroup>
</cc:implementation>
现在,只有当 mandatory
属性存在时,复合组件才会被渲染,如下面的代码所示:
<t:attrcheck value="*some_text*" mandatory="false"/>
完整的应用程序命名为 ch10_22
。
复合组件的陷阱
在本章的下一部分,我们将讨论复合组件的一些陷阱,例如:复合组件属性中的 null
值、复合组件中的隐藏透传属性、复合组件的子组件数量以及 <h:panelGrid>
中的渲染顶级组件。
复合组件属性中的 null
值
截至版本 2.2,JSF 可以在值是null
的情况下确定组合组件属性的适当类型。这是 2.1 版本中的一个问题。
让我们看看以下代码中的简单组合组件:
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="value"/>
<cc:editableValueHolder name="test"/>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:inputText id="test" value="#{cc.attrs.value}"/>
</cc:implementation>
此外,一个使用此组合组件的简单页面如下所示:
<h:form>
<t:nullTest value="#{dummyBean.dummy}">
<f:validator for="test" binding="#{dummyBean.dummyValidator}"/>
</t:nullTest>
<h:commandButton value="Send"/>
</h:form>
现在,如果你从这个组件提供一个null
值,它将在 JSF 2.2 中正确工作,但在 JSF 2.1 中则不会工作。完整的示例命名为ch10_19
。
在组合组件中隐藏透传属性
组合组件可以隐藏透传属性。例如,让我们考虑以下简单的组合组件:
<!-- INTERFACE -->
<cc:interface componentType="book.beans.PtaComponent">
<cc:attribute name="value"/>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:inputText value="#{cc.attrs.value}"/>
</cc:implementation>
接下来,让我们通过向其中添加两个透传属性,在页面中使用这个组合组件,如下所示:
<h:form>
<t:pta value="leoprivacy@yahoo.com">
<f:passThroughAttribute name="placeholder"
value="Type an e-mail address" />
<f:passThroughAttribute name="type" value="email" />
</t:pta>
<h:commandButton value="Submit"/>
</h:form>
此刻,如果你检查属性列表(使用UIComponent.getAttributes
方法)和透传属性列表(使用UIComponent.getPassThroughAttributes
方法),你会注意到placeholder
和type
属性在透传属性列表中。我们可以通过将它们封装到组合组件中,轻松地将它们移动到属性列表中,如下面的代码所示:
<!-- INTERFACE -->
<cc:interface componentType="book.beans.PtaComponent">
<cc:attribute name="value"/>
<cc:attribute name="placeholder"/>
<cc:attribute name="type"/>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:inputText value="#{cc.attrs.value}">
<f:passThroughAttribute name="placeholder"
value="#{cc.attrs.placeholder}" />
<f:passThroughAttribute name="type" value="#{cc.attrs.type}" />
</h:inputText>
</cc:implementation>
此外,你可以在页面中使用组合组件,如下所示:
<h:form>
<t:pta value="leoprivacy@yahoo.com"
placeholder="Type an e-mail address" type="email" />
<h:commandButton value="Submit"/>
</h:form>
完成!现在placeholder
和type
属性不再出现在透传属性列表中。它们被添加到了getAttributes
方法返回的属性列表中。
完整的应用程序命名为ch10_16
。
计算组合组件的子组件数量
假设你有一个通过<cc:insertChildren/>
标签接受子组件的组合组件。有时你可能需要在子组件列表为空时渲染特定的消息,为此你可能考虑编写一个组合组件实现,如下面的代码所示:
<!-- IMPLEMENTATION -->
<cc:implementation>
<div id="#{cc.clientId}">
<ul>
<cc:insertChildren/>
<h:panelGroup rendered="#{cc.childCount == 0}">
The list of names is empty!
</h:panelGroup>
</ul>
</div>
</cc:implementation>
现在,如果组合组件按如下方式使用,你可能认为将渲染消息“名称列表为空!”**
<t:iccc/>
嗯,你说得对!但是,当组件按如下方式使用时,将渲染与列表内容并排的消息:
<t:iccc>
<li>Mike</li>
<li>Andrew</li>
</t:iccc>
为了解决这个问题,你可以使用以下代码:
<cc:implementation>
<div id="#{cc.clientId}">
<ul>
<cc:insertChildren/>
**<c:if test="#{cc.childCount == 0}">**
**The list of names is empty!**
**</c:if>**
</ul>
</div>
</cc:implementation>
完成!完整的应用程序命名为ch10_20
。
顶级组件的陷阱
记住,我们在这章前面提到,每个组合组件都接收UINamingContainer
作为顶级组件。当定义组合组件时,这一点很重要,如下面的代码所示:
<!-- INTERFACE -->
<cc:interface>
<cc:attribute name="labelvalue"/>
<cc:attribute name="imgvalue"/>
</cc:interface>
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:outputLabel for="img" value="#{cc.attrs.labelvalue}"/>
<h:graphicImage id="img" value="#{cc.attrs.imgvalue}"/>
</cc:implementation>
然后,你尝试使用它,如下面的代码所示:
<h:panelGrid columns="2" border="1">
<t:pg labelvalue="SMILEY"
imgvalue="#{resource['default/images:smiley.gif']}"/>
<t:pg labelvalue="SAD SMILE"
imgvalue="#{resource['default/images:sad_smile.gif']}"/>
</h:panelGrid>
如果你忘记了顶级组件,你可能期望看到如下截图的左侧部分,而实际上,你将看到如下截图的右侧部分:
这是正常的,因为 <h:panelGrid>
将复合组件视为一个整体。定义复合组件的所有组件都是顶级组件的子组件,并且对 <h:panelGrid>
不可见。
完整的示例命名为 ch10_15
。
在 JSF 2.2 中将复合组件作为 JAR 分发
截至 JSF 2,我们可以在自定义标签库(taglibs)中添加复合组件。在将复合组件工件放置在正确的文件夹后,我们需要编写一个类型为(此文件名应以 taglib.xml
结尾)的文件,如下面的代码所示:
<facelet-taglib version="2.0">
<namespace>**http://***some***/***namespace*</namespace>
<composite-library-name>
*library_name*
</composite-library-name>
</facelet-taglib>
根据此文件的内容,更确切地说,根据 <composite-library-name>
,JSF 2 检测属于此库的复合组件。这意味着在此 JAR 中映射的复合组件必须仅来自此库。
截至 JSF 2.2,这个限制不再存在,我们可以在同一个 JAR 中添加来自不同库的复合组件。
让我们看一个例子!假设我们想在同一个 JAR 中添加温度组件(在应用程序 ch10_8
中开发)和范围滑块组件(在应用程序 ch10_11
中开发)。该 JAR 将命名为 jsfcc.jar
。完成此操作的步骤如下:
-
创建一个名为
jsfcc.jar
的空 JAR。 -
在
jsfcc.jar
中创建文件夹META-INF/resources
。 -
复制包含复合组件的库到
META-INF/resources
(从应用程序ch10_8
复制resources/temperature
文件夹,并从应用程序ch10_11
复制resources/range-slider
)。 -
对于温度复合组件,将类
book.beans.TempConvertClient.class
和book.beans.TempConvertComponent.class
复制到 JAR 根目录下。 -
创建一个空的
faces-config.xml
文件并将其放置在META-INF
文件夹下,如下所示:<?xml version="1.0"?> <faces-config xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd" version="2.2"> </faces-config> ```**
-
创建以下
cc.taglib.xml
文件并将其放置在META-INF
文件夹下。请注意,我们不需要<composite-library-name>
标签,并且我们已经将两个复合组件配置在同一个命名空间下,jsf/cc/packt/taglib
。使用此示例,定义更多组件非常容易,如下所示:<facelet-taglib version="2.2" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-facelettaglibrary_2_2.xsd" <namespace>http://jsf/cc/packt/taglib</namespace> <tag> <tag-name>range-slider</tag-name> <component> <resource-id> resources/range-slider/range-slider.xhtml </resource-id> </component> </tag> <tag> <tag-name>temperature</tag-name> <component> <resource-id> resources/temperature/temperature.xhtml </resource-id> </component> </tag> </facelet-taglib> ```**
-
在下面的屏幕截图中,你可以看到 JAR 结构应该如何看起来:
特别为测试 jsfcc.jar
,你可以运行应用程序 ch10_21
。请注意,即使 NetBeans 不识别复合组件的标签,它们也能正常工作。
以编程方式添加复合组件
本章的最后部分讨论了以编程方式添加复合组件。在 JSF 2.2 之前,没有官方 API 通过用户代码将复合组件作为 Java 实例实例化。但是,至少有两种简单的方法可以非官方地完成此操作:
-
从 JSF 1.5 版本开始,可以使用 OmniFaces 中可用的
Components.includeCompositeComponent
方法(code.google.com/p/omnifaces/
). -
以
Components.includeCompositeComponent
源代码作为编写你自己的实现的灵感。这种实现列在完整的应用程序ch10_23
中。在那个应用程序中,你可以看到如何通过程序化在页面中添加 Welcome 和 Temperature 组合组件(你需要传递给addCompositeComponent
方法以下数据:组合组件父级、库名称和路径,以及一个唯一的 ID)。
截至 JSF 2.2,我们可以使用显式 API 来程序化实例化组合组件。这个 API 的核心是基于在ViewDeclarationLanguage
类中添加的新createComponent
方法。这个方法的签名如下:
public UIComponent createComponent(FacesContext context, String taglibURI, String tagName, Map<String,Object> attributes)
除了FacesContext
外,您还需要传递标签库 URI、标签名称和标签的属性,如果没有属性则传递null
。例如,可以通过此 API 添加 Welcome 组件,如下所示(我们将 Welcome 组件添加到具有welcomeId
ID 的<h:panelGroup>
中):
public void addWelcomeCompositeComponent() {
FacesContext context = FacesContext.getCurrentInstance();
ViewDeclarationLanguage vdl = context.getApplication().getViewHandler().getViewDeclarationLanguage(context,
context.getViewRoot().getViewId());
Map<String, Object> attributes = new HashMap<>();
attributes.put("value", createValueExpression("#{welcomeBean.value}",
java.lang.String.class).getExpressionString());
attributes.put("to", createValueExpression("#{welcomeBean.to}",
java.lang.String.class).getExpressionString());
UINamingContainer welcomeComponent = (UINamingContainer)
vdl.createComponent(context, "http://xmlns.jcp.org/jsf/composite/customs", "welcome", attributes);
UIComponent parent = context.getViewRoot().findComponent("welcomeId");
welcomeComponent.setId(parent.getClientId(context) + "_" + "welcomeMsgId");
parent.getChildren().add(welcomeComponent);
}
完整的应用程序命名为ch10_27_1
。
此 API 的一个副作用是它还允许我们添加常规组件。例如,你可以将UIOutput
组件添加到具有myPlayerId
ID 的<h:panelGroup>
中,如下所示:
public void addComponent() {
FacesContext context = FacesContext.getCurrentInstance();
ViewDeclarationLanguage vdl = context.getApplication().getViewHandler().getViewDeclarationLanguage(context,
context.getViewRoot().getViewId());
Map<String, Object> attributes = new HashMap<>();
attributes.put("value", createValueExpression("#{playersBean.player}", java.lang.String.class).getExpressionString());
UIOutput outputTextComponent = (UIOutput) vdl.createComponent(context,
"http://java.sun.com/jsf/html", "outputText", attributes);
UIComponent parent = context.getViewRoot().findComponent("myPlayerId");
outputTextComponent.setId(parent.getClientId(context) +
"_" + "nameId_"+ new Date().getTime());
parent.getChildren().clear();
parent.getChildren().add(outputTextComponent);
}
完整的应用程序命名为ch10_27_2
。在第十二章 Facelets 模板中,你可以看到使用此 API 添加<ui:include>
的示例。
# 摘要
在本章中,你看到了 JSF 最伟大的功能之一。自定义和组合组件功能代表了 JSF 对开发者的尊重。编写自定义/组合组件无疑是每个 JSF 开发者必须通过的强制性测试,因为普通组件和非凡组件之间的区别在于其技能。我希望,在许多关于 JSF 自定义/组合组件的书籍和教程中,你也能找到本章关于这个广泛主题的有趣论文。
作为本章的最后一句话,我们必须向所有感到被忽视的 JSP 粉丝道歉。因为我们没有提到任何关于编写与 JSP 兼容的自定义/组合组件的内容。正如你所知,这些组件可以通过标签类(而不是标签处理器)与 JSP 兼容,但自 JSF 2 以来,JSP 已被弃用。我认为这是不涵盖甚至提及 JSP 的合理借口。
欢迎你回到下一章,我们将探索新的 JSF 2.2 主题!**
第十一章. JSF 2.2 资源库合约 – 主题
从版本 2.0 开始,JSF 开发者利用 Facelets 作为默认的视图声明语言(VDL)。Facelets 提供了许多优势,但我们特别感兴趣的是使用Facelet 模板,它代表了一种 XHTML 和其他资源(如 CSS、JS 和图像)的混合。Facelet 模板充当应用程序页面的基础(或模型)。实际上,它代表了一段可重用的代码,为应用程序页面提供了一致和标准的视觉和感觉。在本书的最后一章中,我们将更深入地探讨 Facelets 和模板的细节,而本章我们将重点介绍新的 JSF 2.2 特性,称为资源库合约。
这个新特性通过允许我们以可重用和灵活的方式轻松装饰和使用整个应用程序中的 Facelet 模板,从而加强了并简化了主题(如 PrimeFaces 或 RichFaces)的实现。
在本章中,您将看到如何执行以下操作:
-
与合约一起工作
-
使用合约样式 JSF 表格和 UI 组件
-
不同设备间的样式合约
-
为组合组件编写合约
-
编写主题切换器
-
在 XML 中配置合约
-
将合约打包到 JAR 中
注意
此外,请注意正确理解当前上下文中的合约一词。它可以用来定义诸如contracts
文件夹、contracts
属性或<contracts>
标签等概念。有时,它可能会令人困惑。
与合约一起工作
合约由模板和 CSS 文件组成,这些文件组成了contracts
文件夹。为了定义合约,我们需要在 Web 应用的根目录下遵守一些约定。最重要的约定(例如,名称、结构和内容)涉及参与定义合约的文件夹。所有合约都存储在一个特殊的文件夹中——命名为contracts
——直接位于应用的 Web 根目录下,或者位于 JAR 文件中的META-INF
文件夹下。
注意
我们可以通过WEBAPP_CONTRACTS_DIRECTORY_PARAM_NAME
上下文参数来更改此文件夹的位置和名称。尽管这个上下文参数可以包含斜杠(/
),但它的值不能以斜杠开头。运行时将此值解释为相对于应用 Web 根目录的路径。
通常,在contracts
文件夹下,我们为每个合约定义一个子文件夹(子文件夹的名称代表合约的名称),其中包含合约的工件,如 CSS、JS、图像和 XHTML 模板(您可以通过将它们添加到代表子文件夹中,将 CSS、JS 和图像等资源与 XHTML 模板分开)。
在下面的屏幕截图中,您可以看到同一应用程序中两个合约(rafa1
和rafa2
)的文件夹结构,命名为ch11_1
:
在我们的示例中,rafa1/template.xhtml
和 rafa2/template.xhtml
的源代码是相同的(当然,这并非强制要求);然而,它们只是使用了不同的 CSS 文件。这些 XHTML 文件作为应用页面的模板。以下是 rafa1/template.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
>
<h:head>
<title></title>
</h:head>
<h:body>
<h:outputStylesheet name="styles.css"/>
<div class="header">
<ui:insert name="header"/>
</div>
<div class="content">
<ui:insert name="content"/>
</div>
<div class="footer">
<ui:insert name="footer"/>
</div>
</h:body>
</html>
此外,你可以直接在应用网页中使用合约,归功于 <f:view>
标签的新 JSF 2.2 属性 contracts
(这必须放在模板客户端)。此属性的值应该是你想要使用的合约名称。例如,如果你想使用名为 rafa2
的合约,你可以在 index.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
>
<h:head>
<title></title>
</h:head>
<h:body>
<f:view contracts="rafa2"><!-- switch to rafa1 to see first theme -->
<ui:composition template="/template.xhtml">
<ui:define name="header">
<p>Rafael Nadal photos - header</p>
</ui:define>
<ui:define name="content">
<h:graphicImage
value="#{resource['default/images:RafaelNadal.jpg']}"/>
</ui:define>
<ui:define name="footer">
<p>Rafael Nadal photos - footer</p>
</ui:define>
</ui:composition>
</f:view>
</h:body>
</html>
为了使用名为 rafa1
的合约,你只需将此名称指定为 contracts
属性的值。
完整的应用程序名为 ch11_1
。
使用合约来设置表格样式
现在你已经知道了如何编写和使用合约,你可以尝试玩转这个伟大的功能,为你的页面创建不同种类的样式/主题。大多数时候,创建酷炫的主题涉及两个因素:拥有一个酷炫且灵活的模板机制,以及拥有扎实的 CSS 和 JS 知识。
例如,我们可以尝试为 JSF 表格编写两个酷炫的主题。首先,我们将定义两个名为 tableBlue
和 tableGreen
的合约。在两种情况下,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
>
<h:head>
<title></title>
</h:head>
<h:body>
<h:outputStylesheet name="styles.css"/>
<div class="content">
<ui:insert name="content"/>
</div>
</h:body>
</html>
现在,你可以使用 tableBlue
或 tableGreen
合约,如下所示:
...
<h:body>
<f:view contracts="tableBlue">
<ui:composition template="/template.xhtml">
<ui:define name="content">
<h:dataTable value="#{playersBean.data}" var="t" border="1">
<h:column>
<f:facet name="header">
Ranking
</f:facet>
#{t.ranking}
</h:column>
...
</h:dataTable>
</ui:define>
</ui:composition>
</f:view>
</h:body>
...
结果将如以下截图所示:
如你所见,没有必要为 <h:dataTable>
指定一个类或样式属性。这个想法很简单;JSF 使用 HTML 标签如 <table>
、<tr>
、<td>
、<tbody>
、<thead>
和 <tfoot>
来渲染 <h:dataTable>
。因此,如果我们编写一个自定义这些 HTML 标签外观的 CSS 样式表,那么我们将获得期望的结果。对于 <h:dataTable>
,基本的 CSS 可能包含以下类(content
与 <ui:define>
组件的 name
属性值匹配):
.content {}
.content table {}
.content table td,.content table th {}
.content table thead th {}
.content table thead th:first-child {}
.content table tbody td {}
.content table tbody .alt td {}
.content table tbody td:first-child {}
.content table tbody tr:last-child td {}
有时,你可能需要给你的表格添加分页。JSF 不提供用于此任务的属性(与 PrimeFaces 中的 <p:dataTable>
标签不同)。但是,作为一个例子,如果你编写一个类似于以下代码片段的页脚,你可能可以解决这个问题——当然,<div>
内容应该是动态生成并控制的(更多详情请见第六章,处理表格数据):
...
<f:facet name="footer">
<div id="paging">
<ul>
<li>
<a href="#">
<span>Previous</span>
</a>
</li>
...
</ul>
</div>
</f:facet>
</h:dataTable>
现在,你需要添加一些 CSS 类来控制分页方面,如下所示:
.content table tfoot td div {}
.content table tfoot td {}
.content table tfoot td ul {}
.content table tfoot li {}
.content table tfoot li a {}
.content table tfoot ul.active,.content table tfoot ul a:hover {}
结果如下截图所示:
特别感谢 Eli Geske,他是《学习 DHTMLX Suite UI》一书的作者(www.packtpub.com/learning-dhtmlx-suite-ui/book
)。他的免费在线 CSS3 表格生成器(你可以在 tablestyler.com/
找到 HTML 表格样式生成器)在本节的成果中非常有用。
完整的应用程序命名为 ch11_3
。
使用合约样式化 UI 组件
基于前面的示例,我们可以为所有的 JSF UI 组件编写样式/主题。在本节中,你可以看到一个专注于通常出现在表单中的 JSF UI 组件的示例,例如 <h:inputText>
、<h:inputTextarea>
、<h:selectOneMenu>
、<h:selectManyCheckbox>
等。实际上,我们希望得到以下截图(这只是一个示例表单):
我们首先定义一个新的合约名为 jsfui
。模板相当简单,如下所示:
<?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
>
<h:head>
<title></title>
</h:head>
<h:body>
<h:outputStylesheet name="styles.css"/>
<div class="content">
<ui:insert name="content"/>
</div>
</h:body>
</html>
现在,我们只需要编写与 JSF 渲染的 HTML 元素对应的 CSS 类,如下所示:
.content input[type=text] {} /* <h:inputText> */
.content input[type=submit] {} /* <h:commandButton> */
.content textarea {} /* <h:inputTextarea> */
.content label {} /* <h:outputLabel> */
.content select {} /* <h:selectOneMenu>,
<h:selectOneListbox>,
<h:selectManyMenu>,
<h:selectManyListbox> */
.content input[type=radio] {} /* <h:selectOneRadio> */
.content input[type=checkbox] {} /* <h:selectManyCheckbox> */
你可以轻松地为其他 UI 组件添加 CSS 类。此外,你只需指定主题名称作为 contracts
属性的值,就可以编写具有自定义主题的 JSF 表单:
...
<f:view contracts="jsfui">
<ui:composition template="/template.xhtml">
<ui:define name="content">
...
完整的应用程序命名为 ch11_2
。
在不同设备上样式化合约
在前面的示例中,我们看到了如何编写 JSF 合约以及如何通过在 <f:view>
标签的 contracts
属性中显式设置它们来使用它们。有时,你可能需要动态设置一个合约(主题);例如,你可能需要根据应该显示应用程序的设备类型(PC、平板电脑、智能手机、手机等)来选择正确的合约。在这种情况下,你需要从管理 Bean 中提供 contracts
属性值。
提供用于检测设备类型、分辨率等的有力代码(或算法)超出了本书的范围。在最小程度地参与移动领域的情况下,我们将尝试编写一个 JSF 应用程序,使其能够根据设备类型选择正确的合约。实际上,我们将定义以下四个合约(不要将以下分辨率与设备之间的关联视为认证或授权的决定):
-
contracts/browserpc
:此合约适用于 PC(它将是默认的) -
contracts/Device640
:此合约适用于平板电脑(我们假设对于任何类型的平板电脑,640 像素的宽度是一个合理的选择) -
contracts/Device480
:此合约适用于智能手机(我们假设对于任何类型的智能手机,480 像素的宽度是一个合理的选择) -
contracts/Device320
:此合约适用于普通手机(我们假设对于任何类型的手机,320 像素的宽度是一个合理的选择)
现在,我们将编写一个简单的托管 Bean,它将根据名为UAgentInfo
的辅助类(访问blog.mobileesp.com/
)检测设备类型。基本上,这个类根据 HTTP 请求头部的User-Agent
和Accept
检测不同类型的设备。基于这种检测,我们可以设置一个名为正确合约的托管 Bean 属性。托管 Bean 的代码如下:
@Named
@SessionScoped
public class ThemeBean implements Serializable {
private String theme = "browserpc";
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
}
publicThemeBean() {
Map<String, String>getRequestMap = FacesContext.getCurrentInstance().getExternalContext().getRequestHeaderMap();
String userAgent = getRequestMap.get("User-Agent");
String httpAccept = getRequestMap.get("Accept");
UAgentInfo detector = new UAgentInfo(userAgent, httpAccept);
if (detector.isMobilePhone) {
if ((detector.detectSmartphone())) {
System.out.println("SMARTPHONE THEME!");
theme = "Device480";
} else {
System.out.println("SIMPLE MOBILE THEME!");
theme = "Device320";
}
} else {
if (detector.detectTierTablet()) {
System.out.println("TABLET THEME!");
theme = "Device640";
} else {
System.out.println("BROWSER THEME!");
theme = "browserpc";
}
}
}
}
每个合约都包含一个 XHTML 模板和一个名为styles.css
的 CSS 文件。每个 CSS 文件包含用于为分辨率类型设置样式的类。模板对所有合约都是相同的,相当简单,如下所示:
<?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
>
<h:head>
<title></title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta name="HandheldFriendly" content="true"/>
<meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no"/>
</h:head>
<h:body>
<h:outputStylesheet name="styles.css"/>
<div class="content">
<ui:insert name="content"/>
</div>
</h:body>
</html>
让我们看看以下屏幕截图中的简单页面。(JSF 代码很简单,你可以在名为ch11_4
的完整应用程序中看到它。)这个视图是为桌面浏览器设计的。
此页面的相关 JSF 代码包括添加正确的合约:
<h:body>
<f:view contracts="#{themeBean.theme}">
<ui:composition template="/template.xhtml">
<ui:define name="content">
...
完成!现在,你可以轻松地使用移动模拟器进行一些测试,例如 Opera Mobile Operator。在以下屏幕截图中,你可以看到与三星 Galaxy Tab 上相同的页面,分辨率为 1024x600(PPI:240):
此外,相同的页面还可以为手机设备渲染:左侧是分辨率为 540x960(PPI:267)的摩托罗拉 Atrix4G,右侧是分辨率为 320x480(PPI:252)的诺基亚 N9 手机:
注意,我们可以通过使用响应式 CSS 将前面的示例简化为一个合约,并且不需要托管 Bean。而不是使用四个合约(browserpc
、Device640
、Device480
和Device320
),我们可以使用一个单一的合约;让我们称它为alldevices
。我们在alldevices
合约下放置两个 CSS 文件:一个通用 CSS 文件(styles.css
)和一个响应式 CSS 文件(responsive.css
)。此外,我们修改template.xhtml
文件,使用以下代码加载这两个 CSS 文件:
<h:body>
<h:outputStylesheet name="styles.css"/>
<h:outputStylesheet name="responsive.css"/>
...
</h:body>
在最后一步,我们在应用程序的 JSF 页面上设置此合约,如下所示:
<f:view contracts="alldevices">
<ui:composition template="/template.xhtml">
<ui:define name="content">
...
完成!完整的应用程序名为ch11_5
。
另一种方法包括编写一个自定义的RenderKitFactory
类、一个自定义的RenderKit
类以及一组自定义的Renderers
类——每个设备一个。例如,使用这些工具,名为ch11_15
的应用程序展示了如何为不同设备渲染在第十章(第十章,JSF 自定义组件)中开发的温度自定义组件。
编写复合组件的合约
在本节中,你将了解如何编写复合组件的合约。为此,我们将使用在第十章(第十章,JSF 自定义组件)中开发的温度复合组件。代码中的实现部分如下所示:
<cc:implementation>
<div id="#{cc.clientId}:tempconv_main">
<h:outputLabel id="tempconv_smlabel" for="tempconv_selectonemenu" value="Convert to:"/>
<h:selectOneMenu id="tempconv_selectonemenu" binding="#{cc.unittoI}">
<f:selectItem itemValue="fahrenheit" itemLabel="fahrenheit" />
<f:selectItem itemValue="celsius" itemLabel="celsius" />
</h:selectOneMenu>
<h:outputLabel id="tempconv_iflabel" for="tempconv_inputfield" value="Insert value:"/>
<h:inputText id="tempconv_inputfield" binding="#{cc.temptI}"/>
<h:commandButton id="tempconv_button" value="Convert">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
<h:panelGroup id="tempconv_result" layout="block">
<h:outputText value="° #{cc.unittoI.valueeq 'fahrenheit' ? 'F ': 'C ' } #{cc.getTempConvert()}"/>
</h:panelGroup>
</div>
</cc:implementation>
子组件的 ID 用于定义用于样式化复合组件的 CSS 文件。因此,我们需要编写以下 CSS 类。注意我们如何利用 CSS 通配符来查找子组件。
.content {}
.content *[id*='tempconv_main'] {}
.content *[id*='tempconv_result'] {}
.content *[id*='tempconv_inputfield'] {}
.content *[id*='tempconv_button'] {}
.content *[id*='tempconv_inputfield']:hover {}
.content *[id*='tempconv_inputfield']:active {}
.content *[id*='tempconv_smlabel'] {}
.content *[id*='tempconv_iflabel'] {}
.content *[id*='tempconv_selectonemenu'] {}
此外,我们将此 CSS 文件放置在与以下 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
>
<h:head>
<title></title>
</h:head>
<h:body>
<h:outputStylesheet name="styles.css"/>
<div class="content">
<ui:insert name="content"/>
</div>
</h:body>
</html>
最后,如下所示使用复合组件:
...
<f:view contracts="tempStyleGray">
<ui:composition template="/template.xhtml">
<ui:define name="content">
<h3>Composite component with contract:</h3>
<h:form id="tempForm">
<t:temperature id="temp" value="#{tempBean.value}" />
</h:form>
</ui:define>
</ui:composition>
</f:view>
...
注意我们已经定义了两个合同:tempStyleGray
(以下截图中的第一个条形图)和 tempStyleGreen
(以下截图中的第二个条形图):
完整的应用程序命名为 ch11_6
。
编写主题切换器
如果你是 PrimeFaces 的粉丝,那么我确信你已经看到了 PrimeFaces 主题切换器。基本上,主题切换器由一个包含主题名称和缩略图的下拉菜单表示。最终用户只需从列表中选择即可在应用程序的主题之间切换。
在本节中,您将看到如何使用 JSF 2.2 合同开发主题切换器。目标是获得一个主题切换器,以便:
-
它可以作为 JAR 添加到任何 JSF 2.2 应用程序中
-
它可以自动检测并列出应用程序的主题
-
它可以提供很好的外观和感觉,如下面的截图所示(左侧显示的是 PrimeFaces 主题切换器,右侧显示的是我们的主题切换器)
显然,这种下拉菜单不能使用内置的 <h:selectOneMenu>
标签生成。为了自定义包含图像和描述的下拉菜单,我们可以编写一个专门的 Renderer
,或者尝试使用一个能够像前面截图那样渲染它的 JavaScript 插件。好吧,第二个选项更容易实现,而且不需要我们重新发明轮子。实际上,我们可以使用一个免费且酷的 jQuery 插件 ddSlick (designwithpc.com/Plugins/ddSlick
),这是一个免费轻量级的 jQuery 插件,允许您创建一个包含图像和描述的自定义下拉菜单。还有许多其他这样的插件可以做到同样的事情。
基本上,这个插件可以将一个简单的下拉菜单(使用 <select>
和 <option>
定义)转换成一个包含图像和描述的精美下拉菜单。为此,我们从以下代码片段中的纯 HTML5 <select>
标签开始:
<select id="demo-htmlselect">
<option value="0" data-imagesrc="img/..."
data-description="Description ...">text</option>
<option value="1" data-imagesrc="img/..."
data-description="Description ...">text</option>
...
</select>
当这个 <select>
标签通过 ddSlick 处理时,将生成所需的下拉菜单。基本上,ddSlick 将 <select>
标签渲染为 <ul>
标签,每个 <option>
标签作为 <li>
。图像和描述使用 <img>
和 <small>
渲染,而选项文本使用 <label>
渲染。此外,将为每个 <option>
值生成一个隐藏的输入。HTML5 属性 data-imagesrc
和 data-description
用于告诉 ddSlick 为每个 <option>
使用哪些图像和描述。
理解 ddSlick 的工作原理非常重要,因为我们将将其封装成一个名为ThemeSwitcher
的复合组件。接口部分非常简单,包含一个名为theme
的单个属性。这个属性代表所选的主题,如下所示:
<!-- INTERFACE -->
<cc:interfacecomponentType="book.beans.ThemeSwitcherComponent">
<cc:attribute name="theme" default="" type="java.lang.String" required="true"/>
</cc:interface>
在实现部分,我们完成了几个任务。首先,我们加载组件所需的 JavaScript 库:
<!-- IMPLEMENTATION -->
<cc:implementation>
<h:outputScript library="themeswitcher" name="js/jquery.min.js"/>
<h:outputScript library="themeswitcher" name="js/modernizr-2.0.6-development-only.js"/>
<h:outputScript library="themeswitcher" name="js/jquery-ui.min.js"/>
<h:outputScript library="themeswitcher" name="js/prettify.js"/>
<h:outputScript library="themeswitcher" name="js/ddslick.js"/>
...
此外,我们定义了 HTML 中的<select>
组件,它被封装在<h:form>
中(理想情况下,这个组件不应该与<h:form>
中的其他组件一起使用;因此,我们不必担心嵌套表单):
<div id="#{cc.clientId}:themeswitcher">
<h:form id="themeswitcherForm">
<!--<h:outputScript name="jsf.js" library="javax.faces" target="head"/> -->
<select id="#{cc.clientId}:themeswitcherForm:themeswitcher_content">
<ui:repeat value="#{cc.contracts}" var="t">
<option value="#{t}" data-imagesrc="img/#{t}.png?con=#{t}" data-description="Description: #{t} theme">#{t}</option>
</ui:repeat>
<option selected="true" style="display:none;" data-description="Current theme: #{cc.attrs.theme}">Select theme ...</option>
</select>
<h:inputHidden id="selectedTheme" value="#{cc.attrs.theme}"/>
</h:form>
</div>
合约会自动检测并添加为<option>
,使用<ui:repeat>
组件。所选的主题(<option>
)通过隐藏字段<h:inputHidden>
提交给管理 Bean。提交后(通过 AJAX 或非 AJAX),整个页面将被加载,contracts
属性(<f:view>
)将接收并应用所选的主题。为此,我们需要一点 JavaScript 代码。首先,我们调用ddslick
方法,它将把无聊的下拉菜单变成酷炫的。进一步,我们指示一个 JavaScript 回调方法,当选择主题时将自动调用。在这个方法中,我们刷新隐藏字段的值,并提交表单(通过 AJAX 或非 AJAX):
<cc:implementation>
...
<script type="text/javascript">
$(document).ready(function() {
var themeForm = ("#{cc.clientId}:themeswitcherForm").replace(/:/g, "\\:");
var themeSelectElem = ("#{cc.clientId}:themeswitcherForm:themeswitcher_content").replace(/:/g, "\\:");
var themeHiddenElem = ("#{cc.clientId}:themeswitcherForm:selectedTheme").replace(/:/g, "\\:");
$('#' + themeSelectElem).ddslick({
onSelected: function(data) {
if (data.selectedData.text !== "Select theme ...") {
setTheme(data);
}
}
});
// callback function
functionsetTheme(data) {
$('#' + themeHiddenElem).val(data.selectedData.text);
//jsf.ajax.request(this, null, {execute: '#{cc.clientId}:themeswitcherForm:selectedTheme', render: "@all"});
$('#' + themeForm).submit(); // without AJAX
}
});
</script>
</cc:implementation>
使用这个回调方法提交所选主题非常方便,因为 ddSlick 提供了这个功能。还有许多其他可能性,例如编写值变化监听器、触发自定义事件等。
我相信你已经注意到我们的复合组件指示了存在一个后端组件。这个组件负责检测应用程序的合约并将它们的名称添加到List
中。这个列表通过<ui:repeat>
转换为<option>
。其代码相当简单,如下所示:
@FacesComponent(value = "book.beans.ThemeSwitcherComponent", createTag = false)
public class ThemeSwitcherComponent extends UIComponentBase implements NamingContainer {
private List<String> contracts = new ArrayList<>();
public List<String>getContracts() {
return contracts;
}
publicThemeSwitcherComponent() throws IOException {
FacesContextfacesContext = FacesContext.getCurrentInstance();
ExternalContextexternalContext = facesContext.getExternalContext();
Path path = Paths.get(((ServletContext) externalContext.getContext()).getRealPath("/contracts"));
try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) {
for (Path file : ds) {
if (Files.readAttributes(file, BasicFileAttributes.class).isDirectory()) {
contracts.add(file.getFileName().toString());
}
}
} catch (IOException e) {
throw e;
}
}
@Override
public String getFamily() {
returnUINamingContainer.COMPONENT_FAMILY;
}
}
想要使用这个ThemeSwitcher
组件的开发者必须在每个合约中添加一个与合约同名的 PNG 图像(推荐大小为 40 x 40 像素)。按照惯例,对于每个合约,ThemeSwitcher
组件将寻找这样的图像,并在主题名称和描述旁边显示它。你可以改进这个后端组件以确保这些图像存在。此外,你可以扩展其功能,以便允许组件用户提供自定义描述。
完成!完整的应用程序命名为ch11_10
。
ThemeSwitcher
复合组件被打包成 JAR 文件,并在ch11_7
应用程序中作为示例使用,如下所示:
<html ...
>
...
<h:body>
<f:view contracts="#{themeSwitcherBean.theme}">
<t:themeswitcher theme="#{themeSwitcherBean.theme}"/>
...
ThemeSwitcherBean
的源代码非常简单,如下所示:
@Named
@RequestScoped
public class ThemeSwitcherBean {
private String theme = "tableBlue";
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
}
}
ch11_7
应用程序的输出在以下屏幕截图中显示:
如果你决定通过编程方式更改 <f:view>
的 contracts
属性值,你不再需要这个 Bean。此外,如果你认为加载这个 jQuery 插件有缺点,你可以编写纯 JavaScript 代码。或者,如果你想有 JavaScript 代码,自定义渲染器可能是一个不错的选择。
一个基于纯 JavaScript 的 ThemeSwitcher
组件示例在名为 ch11_11
的应用程序中开发,并在名为 ch11_12
的应用程序中以 JAR 文件的形式展示。该示例修改了名为 iconselect.js
的免费 JavaScript UI 库([bug7a.github.io/iconselect.js/
](http://bug7a.github.io/iconselect.js/))和完全重写的 iScroll 4 库(http://cubiq.org/iscroll-4)。这两个库都是纯 JavaScript;它们不使用如 jQuery 这样的附加库。此外,它们非常小,可以免费复制、修改、分发、改编和商业使用。
包裹这些库的复合组件可以像以下代码所示那样使用。请注意,你可以自定义方面(即网格),并且你可以选择指定要忽略哪些合同(不在主题切换器中列出)。
<t:themeswitcher theme="#{themeSwitcherBean.theme}" ignore="default" columns="1" rows="1"/>
输出如下所示:
如果你不想有任何 JavaScript 代码,你可以编写自定义的 Renderer
代码或扩展现有的 MenuRenderer
代码(Mojarra 或 MyFaces 实现)或编写一个使用 JSF UI 组件创建良好主题切换器的复合组件。编写自定义 Renderer
代码(或扩展 MenuRenderer
代码)似乎不是一件容易的事情,我不知道它是否值得付出努力。但是,基于 JSF UI 组件编写复合组件相当简单。你可以在名为 ch11_13
的应用程序中看到这样的实现,并在名为 ch11_14
的应用程序中以 JAR 文件的形式展示。在这个例子中,主题列在 <h:dataTable>
组件中,如下截图所示:
在 XML 中配置合同
合同可以与 JSF 页面相关联,正如你在前面的章节中看到的。作为替代,我们可以在 faces-config.xml
文件中配置合同来完成相同的事情。例如,假设我们有三个合同:default
、tableGreen
和 tableBlue
。它们与不同页面的关联如下:
-
default
合同与tables/defaultTablePage.xhtml
页面相关联 -
tableGreen
合同与greenTablePage.xhtml
页面相关联 -
tableBlue
合同与blueTablePage.xhtml
页面相关联
在 faces-config.xml
中,我们可以使用几个标签来完成这些关联——以下示例代码自说自话:
<application>
<resource-library-contracts>
<contract-mapping>
<url-pattern>/blueTablePage.xhtml</url-pattern>
<contracts>tableBlue</contracts>
</contract-mapping>
<contract-mapping>
<url-pattern>/greenTablePage.xhtml</url-pattern>
<contracts>tableGreen</contracts>
</contract-mapping>
<contract-mapping>
<url-pattern>/tables/*</url-pattern>
<contracts>default</contracts>
</contract-mapping>
</resource-library-contracts>
</application>
注意
作为备注,快速看一下第三个关联。注意您如何可以使用 *
通配符将合同与文件夹中的所有 XHTML 页面关联起来。不要尝试在 <contracts>
中使用 EL。这是不会工作的!
完整的应用程序命名为 ch11_8
。
将合同打包到 JAR 文件中
为了分发合同,您可以将它们放入一个 JAR 文件中。这是一个非常简单的任务,只需三个步骤即可完成,具体如下:
-
考虑一个空的 JAR 文件。
-
在 JAR 中创建一个名为
META-INF
的文件夹。 -
将您的应用程序中的
contracts
文件夹复制到META-INF
。
例如,一个包含 default
、tableGreen
和 tableBlue
合同文件夹的 JAR 文件,其结构如下所示(请参考以下截图):
使用此 JAR 文件的一个完整示例命名为 ch11_9
。
摘要
希望您喜欢这个倒数第二章。
JSF 2.2 资源库合同是其中一个重要特性。长期以来,JSF 开发者一直要求有一个机制,允许在 JSF 中编写和使用主题,就像在其他系统中一样。正如您刚才看到的,JSF 2.2 合同在这一方向上打开了一扇门,并鼓励开发者编写和使用主题。当然,还有很多其他事情应该添加,例如主题仓库、主题管理控制台、动态切换主题等等。但,这是一个良好的开端!
欢迎大家在最后一章中,我们将讨论关于 Facelets 的内容。
第十二章。Facelets 模板化
在本章中,我们将涵盖 Facelets 模板化的几个方面以及一些相关内容。
JSF 被定义为基于组件的应用程序开发框架。当我们提到 Facelets 时,我们指的是友好的页面开发、代码的重用性、模板化、组件组合、自定义逻辑标签、表达式函数、高性能渲染、优化编译时间等。但 Facelets 实际上是什么?嗯,Facelets 代表了一种VDL(视图声明语言),最初它是作为 JSP 的替代品而创建的。在 JSF 1.1 和 1.2 期间,这个视图处理器只能在单独下载和配置之后使用,而 JSP 是默认的视图处理器。随着 JSF 2.0 的推出,JSF 和 JSP 之间的不匹配使得 Facelets 成为了标准和默认的 VDL,而 JSP 被弃用。从 JSF 2.2 开始,这个概念得到了加强,Facelets 通过新的功能和能力得到了提升。
Facelets 标签的简要概述
模板化是一个基于代码重用性的概念。模板,或称为砖块,代表了可以拼凑在一起以获得 JSF 页面的可重用代码部分。为了实现这一点,我们利用了来自xmlns.jcp.org/jsf/facelets
命名空间的一批标签。
通常,这些标签以ui
为前缀,列表如下:
-
<ui:composition>
标签(TagHandler
):这个标签定义了一个可以使用模板的页面组合(此标签之外的所有内容都将被忽略)。template
属性是可选的,用于指示应将封装内容应用于哪个模板。多个组合可以使用相同的模板,从而封装和重用布局。Facelets 会将封装的内容粘贴到组件的层次结构中,通常在UIViewRoot
之下。《ui:composition>`标签的使用方式如下:<ui:composition template="*template_path*">
-
<ui:define>
标签(TagHandler
):这个标签定义了模板插入到页面中的内容。它可能出现在<ui:composition>
、<ui:component>
、<ui:decorate>
和<ui:fragment>
标签中,并且有一个匹配的<ui:insert>
标签,可以将定义的内容插入到页面中。最常见的是,它出现在<ui:composition>
标签中。<ui:define>
标签的使用方式如下:<ui:define name="*ui_insert_name*">
-
<ui:insert>
标签(TagHandler
):这个标签将内容插入到模板中。通常,这个内容是由<ui:define>
标签在<ui:composition>
、<ui:component>
、<ui:decorate>
或<ui:fragment>
标签中定义的。这个标签指示了内容将被插入的确切位置。当name
属性缺失时,Facelets 会将此标签的正文内容添加到视图中。《ui:insert>`标签的使用方式如下:<ui:insert name="*ui_insert_name*">
-
<ui:include>
标签(TagHandler
):这个标签用于封装和重用来自多个页面的内容。包含的内容可以是纯 XHTML 和具有<ui:composition>
标签或<ui:component>
标签的 XHTML 页面。这个标签可以很容易地与<ui:param>
结合使用,为包含的页面提供参数,但它也可以与<ui:fragment>
、<ui:decorate>
和<ui:insert>
标签结合使用。这是最常用的标签之一,因为它支持重用模板代码的理念。《ui:include>`标签的使用方式如下:<ui:include src="img/em>">
从 JSF 2.2 版本开始,新增了
UIViewRoot.restoreViewScopeState(FacesContext context, Object state)
方法,允许在构建组件树模板中使用视图作用域的 bean 进行 EL 表达式。这意味着以下代码是有用的:<ui:include src="img/em>}"/>
-
<ui:param>
标签(TagHandler
):这个标签用于将参数传递给包含的文件或模板。它用于<ui:include>
、<ui:composition>
或<ui:decorate>
标签中。参数由一个名称-值对组成——两者都可以是字符串字面量或 EL 表达式。在包含的文件或模板中,参数可以通过 EL 访问。<ui:param>
标签的使用方式如下:<ui:param name="*param_name*" value="*param_value*">
-
<ui:repeat>
标签(ComponentHandler
):这个标签用作循环标签(如<c:forEach>
和<h:dataTable>
)的替代品。由于<ui:repeat>
是一个组件处理器,而<c:forEach>
是一个标签处理器,所以在选择它们时需要特别注意!<ui:repeat>
标签的使用方式如下:<ui:repeat value="*some_collection*" var="*var_name*">
-
<ui:debug>
标签(ComponentHandler
):这个标签在组件树中定义了一个能够捕获调试信息(如组件树、作用域变量和视图状态)的调试组件。默认情况下,当您按下Ctrl + Shift + D(在 Windows 操作系统上)时,这些信息会显示在调试弹出窗口中。您可以通过使用可选的hotkey
属性显式设置另一个键盘来更改D键。<ui:debug>
标签的使用方式如下:<ui:debug hotkey="*key*" />
-
<ui:component>
标签(ComponentHandler
):这与<ui:composition>
类似,只是它直接在组件树中定义了一个组件,而没有关联的模板。《ui:component>`标签的使用方式如下:<ui:component>
-
<ui:fragment>
标签(ComponentHandler
):再次强调,这与<ui:component>
标签类似,但不会忽略此标签之外的内容。其主要技能包括rendered
属性,这对于决定封装内容是否显示非常有用。此标签不会产生客户端效果,这使得它成为<h:panelGroup>
的绝佳替代品,后者会产生<span>
或<div>
标签的客户端效果。如果您想在不会产生<span>
或<div>
标签的情况下使用<h:panelGroup>
,则无需显式添加 ID。一个<h:panelGroup>
标签如果具有显式 ID,则会产生一个<span>
标签;如果具有显式 ID 并且layout
属性的值设置为block
,则会产生一个<div>
标签。《ui:fragment>` 标签的使用方法如下:<ui:fragment>
-
<ui:decorate>
标签(TagHandler
):这与<ui:composition>
标签类似,但不会忽略此标签之外的内容。这是一个很好的特性,因为它允许我们将页面上的任何元素应用于模板。template
属性是必需的。《ui:decorate>` 标签的使用方法如下:<ui:decorate template="*template_path*">
-
<ui:remove>
标签:从页面中删除内容。《ui:remove>` 标签的使用方法如下:<ui:remove>
您可以在 docs.oracle.com/javaee/7/javaserverfaces/2.2/vdldocs/facelets/ui/tld-summary.html
了解这些标签的更多详细信息。
创建简单模板 – 页面布局
当这十一个标签结合其技能时,我们可以创建令人惊叹的模板。例如,假设我们想从以下图表创建模板,并将其命名为 PageLayout
:
备注
注意,只需几点击,NetBeans 就可以生成 Facelets 几个模板背后的代码,包括前面的抽象化。但这次我们将手动编写它,以便举例说明 Facelets 标签。虽然 NetBeans 提供了一个基于单个 XHTML 页面的紧凑代码,但我们将使用六个 XHTML 页面编写一个扩展方法。这样,你将有两种编写此类模板的方式。
如您所见,有五个独特的部分:标题、页脚、左侧、中心和右侧。对于这些部分中的每一个,我们将编写一个单独的 XHTML 页面。标题是在 topDefault.xhtml
页面生成的,该页面简单地使用 <ui:composition>
标签提供以下默认内容:
<?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
>
<h:body>
<ui:composition>
<h1>This is default header</h1>
</ui:composition>
</h:body>
</html>
同样的方法也可以用于剩余的四个部分。只需替换默认的标题文本,并创建以下 XHTML 文件:bottomDefault.xhtml
用于页脚,contentDefault.xhtml
用于中心,leftDefault.xhtml
用于左侧,rightDefault.xhtml
用于右侧。
这五个 XHTML 文件就像拼图的五个部分,它们作为模板的默认内容。现在,我们可以通过编写一个使用 <ui:insert>
和 <ui:include>
标签的 XHTML 页面来组合拼图(这被称为模板文件或简单地称为模板),如下面的代码所示——这是 layout.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
>
<h:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<h:outputStylesheet name="css/default.css"/>
<h:outputStylesheet name="css/cssLayout.css"/>
<title>My Template</title>
</h:head>
<h:body>
<div id="top">
<ui:insert name="top">
<ui:include src="img/topDefault.xhtml" />
</ui:insert>
</div>
<div>
<div id="left">
<ui:insert name="left">
<ui:include src="img/leftDefault.xhtml" />
</ui:insert>
</div>
<div id="right">
<ui:insert name="right">
<ui:include src="img/rightDefault.xhtml" />
</ui:insert>
</div>
<div id="content">
<ui:insert name="content">
<ui:include src="img/contentDefault.xhtml" />
</ui:insert>
</div>
</div>
<div id="bottom">
<ui:insert name="bottom">
<ui:include src="img/bottomDefault.xhtml" />
</ui:insert>
</div>
</h:body>
</html>
每个部分都由一个 <ui:insert>
标签表示,默认内容是通过 <ui:include>
包含的。在主模板文件中,一些 <div>
标签和 CSS 被用来排列和样式化拼图的各个部分。
现在,如下面的代码所示,模板已经准备好在 index.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
>
<h:head>
<title></title>
</h:head>
<h:body>
<ui:composition template="template/layout.xhtml" />
</h:body>
</html>
在此刻,我们可以使用 <ui:define>
标签来更改模板的默认内容,例如,如果我们想替换中心部分显示的文本。这是默认内容,包含有 拉斐尔·纳达尔主页 文本,如下面的代码所示:
...
<ui:composition template="template/layout.xhtml">
<ui:define name="content">
Rafael Nadal Home Page
</ui:define>
</ui:composition>
...
同样地,你可以为剩余的四个部分重新定义内容。完整的应用程序被命名为 ch12_1
。
这是一个相当简单的模板,它可以作为我们展示其他 Facelets 标签的支持。我们将继续使用几个具体的例子来展示 <ui:param>
、<ui:decorate>
、<ui:fragment>
、<ui:repeat>
等标签。
通过 ui:param 传递参数
<ui:param>
标签是一个能够向包含文件或模板发送参数的标签处理器。一个简单的例子会让你明白 <ui:param>
的工作原理。在下面的代码中,我们处于模板客户端 index.xhtml
,并向模板文件 layout.xhtml
发送一个参数:
<ui:composition template="template/layout.xhtml">
<ui:param name="playername" value="Rafael " />
</ui:composition>
我们也可以在 layout.xhtml
中使用表达式语言(EL)通过其名称访问此参数。我们可以在模板文件的任何地方这样做;例如,我们可以使用此参数来创建一个新的参数,将其发送到包含的文件,在这种情况下,是 contentDefault.xhtml
文件,如下面的代码所示:
<div id="content">
<ui:insert name="content">
<ui:include src="img/contentDefault.xhtml">
<ui:param name="playernamesurname" value="#{playername} Nadal" />
</ui:include>
</ui:insert>
</div>
从同一个模板,我们可以向不同的包含页面发送参数。除了 playernamesurname
参数外,让我们使用以下代码向 topDefault.xhtml
页面发送一个参数:
<ui:insert name="top">
<ui:include src="img/topDefault.xhtml">
<ui:param name="headertext" value="This is default header (passed through ui:param)" />
</ui:include>
</ui:insert>
接下来,使用以下代码向 bottomDefault.xhtml
页面发送一个参数:
<ui:insert name="bottom">
<ui:include src="img/bottomDefault.xhtml">
<ui:param name="footertext" value="This is default footer (passed through ui:param)" />
</ui:include>
</ui:insert>
现在,playernamesurname
参数在 contentDefault.xhtml
页面中可通过表达式语言(EL)访问,如下面的代码所示:
<ui:composition>
#{playernamesurname} (passed through ui:param)
</ui:composition>
此外,headertext
和 footertext
在 topDefault.xhtml
和 bottomDefault.xhtml
页面中可通过表达式语言(EL)访问。现在,使用 <ui:param>
的结果如下面的截图所示:
完整的应用程序被称作 ch12_11
。
通过 ui:param 传递属性和动作方法
在前面的例子中,你看到了如何利用 <ui:param>
将文本字符串发送到模板或包含页面,但 <ui:param>
可以用于更多的情况。假设我们有以下 TemplatesBean
的代码:
@Named
@ViewScoped
public class TemplatesBean implements Serializable {
private String msgTopDefault="";
private String msgBottomDefault="";
private String msgCenterDefault="No center content ...press the below button!";
//getters and setters
public void topAction(String msg){
this.msgTopDefault = msg;
}
public void bottomAction(String msg){
this.msgBottomDefault = msg;
}
public void centerAction(){
this.msgCenterDefault="This is default content";
}
}
此外,我们希望在contentDefault.xhtml
中显示msgCenterDefault
属性的值。当然,这使用以下代码行很容易实现:
<h:outputText value=#{templatesBean.msgCenterDefault} />
但我们希望通过<ui:param>
传递 bean 的名称和属性的名称。这可以通过以下代码实现:
<ui:insert name="content">
<ui:include src="img/contentDefault.xhtml">
<ui:param name="templatesBeanName" value="#{templatesBean}"/>
<ui:param name="contentPropertyName" value="msgCenterDefault"/>
</ui:include>
</ui:insert>
接下来,在contentDefault.xhtml
中,你可以显示msgCenterDefault
属性的值,如下面的代码行所示:
<h:outputText value="#{templatesBeanName[contentPropertyName]}"/>
好吧,这很简单!但是,如何调用修改msgCenterDefault
属性值的centerAction
方法呢?为此,我们在方括号内添加方法名,并用一对括号表示没有参数的方法,如下面的代码所示:
<h:form>
<h:commandButton value="Center Button" action="#{templatesBeanName['centerAction']()}"/>
</h:form>
最后,我们希望调用topAction
(或bottomAction
)方法。这次,我们希望通过<ui:param>
传递 bean 名称、action 方法名称和参数值。为此,我们将编写以下代码:
<ui:insert name="top">
<ui:include src="img/topDefault.xhtml">
<ui:param name="templatesBeanName" value="#{templatesBean}"/>
<ui:param name="topActionName" value="topAction"/>
<ui:param name="arg" value="Hello from topDefault.xhtml .."/>
</ui:include>
</ui:insert>
在topDefault.xhtml
中,我们可以利用通过这三个参数传递的信息,如下面的代码所示:
<h:form>
<h:commandButton value="Top Button" action="#{templatesBeanNametopActionName}"/>
</h:form>
在下面的屏幕截图中,你可以看到一切按预期工作:
完整的应用程序命名为ch12_13
。
可以从管理 Bean 中访问<ui:param>
的值,如下面的代码所示:
FaceletContext faceletContext = (FaceletContext) FacesContext.getCurrentInstance().getAttributes().
get(FaceletContext.FACELET_CONTEXT_KEY);
String paramValue = (String) faceletContext.getAttribute("param");
利用ui:decorate和ui:fragment标签
首先,让我们谈谈<ui:decorate>
标签。正如其名称所暗示的,这个标签用于装饰页面的一部分。与<ui:composition>
不同,这个标签不会忽略其外部的内容,这有时可能是一个额外的优势。好吧,一个简单的示例如下面的代码所示(template
属性是必需的):
<?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
>
<h:head>
<title></title>
</h:head>
<h:body>
<h:outputText value="You can see this header text thanks to ui:decorate!"/>
<ui:decorate template="template/layout.xhtml">
<ui:define name="content">
Rafael Nadal Website
</ui:define>
</ui:decorate>
<h:outputText value="You can see this footer text thanks to ui:decorate!"/>
</h:body>
</html>
前面的代码片段产生了以下屏幕截图:
完整的示例命名为ch12_10
。基本上,这个示例使用模板装饰了一个页面,并证明了<ui:decorate>
标签相对于<ui:composition>
标签的效果。然而,让我们看看装饰页面一部分的更好示例。
在这个示例中,我们将使用<ul>
列表装饰一个<div>
元素。列表的<li>
项来自两个独立的页面,实现这一技术的关键是嵌套的<ui:decorate>
标签。模板客户端index.xhtml
使用<ui:decorate>
装饰<div>
元素,并包含<ul>
列表,如下面的代码所示:
<h:body>
<div style="border:2px solid; border-radius:25px;width:180px;">
<ui:decorate template="/files/ul.xhtml"/>
</div>
</h:body>
此外,ul.xhtml
模板提供了<ul>
列表和部分<li>
项,还使用<ui:decorate>
标签装饰<ul>
列表,以包含剩余的<li>
项,这些项可通过li.xhtml
模板获得,如下面的代码所示:
<ul>
<li style="color: red;">Andy Murray</li>
<ui:decorate template="/files/li.xhtml"/>
<li style="color: red;">Stanislas Wawrinka</li>
</ul>
li.xhtml
模板使用<ui:fragment>
标签来提供<li>
列表的其余部分。但由于<ui:fragment>
不会阻止未关闭的内容,我们也可以将一些<li>
项目放在它外面,如下面的代码所示:
<li style="color: green;">John Isner</li>
<ui:fragment>
<li>Rafael Nadal</li>
<li>Roger Federer</li>
<li>Novak Djokovic</li>
</ui:fragment>
<li style="color: green;">Fabio Fognini</li>
完成!我们特别使用了不同的颜色来表示<li>
项目。这非常有用,因为它确实有助于我们理解页面是如何使用<ui:decorate>
标签组成的。请参考以下截图:
完整应用程序命名为ch12_24
。
可以使用不同的方法获得相同的结果。但是,另一种可以作为模板技术使用的方法是将ul.xhtml
模板中的<ui:decorate>
标签替换为<ui:insert>
、<ui:define>
和<ui:include>
标签的组合。为了做到这一点,我们使用以下代码更改ul.xhtml
模板:
<ul>
<li style="color: red;">Andy Murray</li>
<ui:insert name="content"/>
<li style="color: red;">Stanislas Wawrinka</li>
</ul>
这将更改模板客户端的代码如下:
<h:body>
<div style="border:2px solid; border-radius:25px;width:180px;">
<ui:decorate template="/files/ul.xhtml">
<ui:define name="content">
<ui:include src="img/li.xhtml"/>
</ui:define>
</ui:decorate>
</div>
</h:body>
完整应用程序命名为ch12_23
。
使用ui:repeat进行迭代
<ui:repeat>
标签是一个能够遍历集合的组件处理器,在每次迭代时,它将子元素的一个副本添加到组件树中。我们可以这样说,<ui:repeat>
充当一个不渲染 HTML 表的<h:dataTable>
标签。当然,你可以通过将其机制包裹在<table>
、<tr>
和<td>
套件中来实现这一点(你将在下一节中看到一个示例,使用 jsfc 属性)。
它包含一组非常实用的属性,在举例之前值得提一下。除了众所周知的属性外,还有value
(表示迭代集合的java.lang.Object
)和var
(表示迭代器为java.lang.Object
),我们还有以下可选属性:
-
step
:此属性允许我们以int
值的形式指示每次迭代要跳过的项目数。默认情况下,<ui:repeat>
标签遍历集合中的每个项目,这表示step
属性等于 1,并指示过程从第一个项目开始。 -
size
:这是要迭代的集合的大小;它必须评估为int
值。 -
offset
:默认情况下,<ui:repeat>
从集合的第一个项目开始迭代过程。此属性允许我们通过告诉 Facelets 从某个偏移量开始迭代过程来跳过一定数量的项目。此偏移量在迭代过程开始之前确定;它必须评估为int
值。 -
varStatus
:此属性通过 POJO 对象揭示当前项的状态。稍后将提供一个使用它的明确示例,但现在让我们看几个迭代不同类型 Java 集合的示例。
迭代ArrayList
集合的一个简单例子如下代码(相同的方法可以应用于任何java.util.List
包):
<ui:repeat value="#{myBean.dataArrayList}" var="t">
<h:outputText value="#{t}" />
</ui:repeat>
然而,<ui:repeat>
也可以使用 toArray
方法遍历 HashSet
集合,如下所示(相同的方法也可以应用于 TreeSet
和 LinkedHashSet
):
<ui:repeat value="#{myBean.dataHashSet.toArray()}" var="t">
<h:outputText value="#{t}" />
</ui:repeat>
或者,更进一步,<ui:repeat>
也可以使用以下方法遍历 Map
集合(HashMap
、TreeMap
和 LinkedHashMap
):
-
以下为第一种方法的代码:
<ui:repeat value="#{myBean.dataHashMap.entrySet().toArray()}" var="t"> <h:outputText value="key:#{t.key} value:#{t.value}" /> </ui:repeat>
-
以下为第二种方法的代码:
<ui:repeat value="#{myBean.dataHashMap.keySet().toArray()}" var="t"> <h:outputText value="key:#{t} value:#{myBean.dataHashMap.get(t)}" /> </ui:repeat>
-
以下为第三种方法的代码:
<ui:repeat value="#{myBean.dataHashMap.values().toArray()}" var="t"> <h:outputText value="#{t}" /> </ui:repeat>
-
以下为第四种方法的代码:
<ui:repeat value="#{myBean.dataHashMap.entrySet()}" var="t"> <ui:repeat value="#{t.toArray()}" var="q"> <h:outputText value="key:#{q.key} value:#{q.value}" /> </ui:repeat> </ui:repeat>
上述示例遍历了整个集合。但如果你只想遍历偶数位置的项,那么我们可以引入 step
属性,如下所示:
<ui:repeat value="#{myBean.dataArrayList}" var="t" step="2">
<h:outputText value="#{t}"/>
</ui:repeat>
对于奇数项,你可能想结合使用 step
和 offset
属性,以下代码展示了如何操作:
<ui:repeat value="#{myBean.dataArrayList}" var="t" step="2" offset="1">
<h:outputText value="#{t}"/>
</ui:repeat>
显示偶数/奇数项的另一种常见方法是通过使用 varStatus
属性。代表此属性值的 POJO 对象包含几个只读的 JavaBeans 属性。在这些属性之间,我们有偶数和奇数属性,可以很容易地与 <ui:fragment>
结合使用,如下所示:
-
对于偶数属性,代码如下所示:
<ui:repeat value="#{myBean.dataArrayList}" var="t" varStatus="vs"> <ui:fragment rendered="#{vs.even}"> <h:outputText value="#{vs.index}. #{t.player}"/> </ui:fragment> </ui:repeat>
-
对于奇数属性,代码如下所示:
<ui:repeat value="#{myBean.dataArrayList}" var="t" varStatus="vs"> <ui:fragment rendered="#{vs.odd}"> <h:outputText value="#{vs.index}. #{t.player}"/> </ui:fragment> </ui:repeat>
整套属性在以下代码片段中暴露:
<ui:repeat value="#{myBean.dataArrayList}" var="t" varStatus="vs">
Index: #{vs.index}
First: #{vs.first}
Last: #{vs.last}
Begin: #{vs.begin}
End: #{vs.end}
Step: #{vs.step}
Current: #{vs.current}
Even: #{vs.even}
Odd: #{vs.odd}
</ui:repeat>
所有的先前示例都统一在名为 ch12_6
的完整应用程序下。
使用 ui:include 和 <f:viewParam>
你可能会认为将 <ui:include>
与 <f:viewParam>
结合使用是一种奇怪的组合,也许确实如此。但是,正如你所知,<ui:include>
能够封装和重用来自多个页面的内容,而 <f:viewParam>
可以在链接中添加视图参数(使用 GET 查询字符串)很有用。这意味着我们可以通过 <f:viewParam>
在 <ui:include>
中使用当前页面传递的参数。
例如,在当前页面中,我们可以包含一个随机页面,或者一个名称被硬编码为视图参数值的页面。我们还可以使用 includeViewParams
属性告诉其他页面包含与当前页面相同的内容。这三个例子只是更多场景的入门。以下示例不言自明:
<h:head>
<title></title>
<f:metadata>
<f:viewParam name="in" value="#{randomInBean.in}"/>
</f:metadata>
</h:head>
<h:body>
<ui:include src="img/#{randomInBean.in}"/>
<h:button value="Tell mypage.xhtml To Include The Same Page As You Did" outcome="mypage.xhtml" includeViewParams="true"/>
<h:button value="Random Page" outcome="index.xhtml"
includeViewParams="false"/>
<h:button value="Include in_index_A.xhtml Page" outcome="index.xhtml?in=in_index_A.xhtml"/>
<h:button value="Include in_index_B.xhtml Page" outcome="index.xhtml?in=in_index_B.xhtml"/>
<h:button value="Include in_index_C.xhtml Page" outcome="index.xhtml?in=in_index_C.xhtml"/>
</h:body>
RandomInBean
的代码如下:
@Named
@RequestScoped
public class RandomInBean {
private String in = "";
public RandomInBean() {
int in_rnd = new Random().nextInt(3);
if (in_rnd == 0) {
in = "in_index_A.xhtml";
} else if (in_rnd == 1) {
in = "in_index_B.xhtml";
} else if (in_rnd == 2) {
in = "in_index_C.xhtml";
}
}
public String getIn() {
return in;
}
public void setIn(String in) {
this.in = in;
}
}
因此,我们有几个按钮来证明 <ui:include>
和 <f:viewParam>
标签之间的共生关系。首先,我们有三个按钮,分别标记为 包含 in_index_A.xhtml 页面、包含 in_index_B.xhtml 页面 和 包含 in_index_C.xhtml 页面。这三个按钮的作用相同;它们传递一个名为 in
的视图参数。视图参数的值是一个字符串字面量,表示应该包含的页面。这将生成以下类型的 URL:
http://localhost:8080/ch12_12/faces/index.xhtml?in=in_index_B.xhtml
因此,根据此 URL,<ui:include>
标签将包含 in_index_B.xhtml
页面。
此外,我们还有一个标签为 随机页面 的按钮。此按钮将在三个页面之间随机选择。为了实现这一点,我们需要添加 includeViewParams="false"
,如下面的代码所示:
<h:button value="Random Page" outcome="index.xhtml" includeViewParams="false"/>
最后,我们可以告诉其他页面包含与当前页面相同的内容。当您点击标签为 告诉 mypage.xhtml 包含与您相同的页面 的按钮时,mypage.xhtml
页面将包含与当前页面相同的内容。为此,我们需要添加 includeViewParams="true"
。
完整的应用程序命名为 ch12_12
。
使用 ui:include 和 ui:param
<ui:include>
和 <ui:param>
标签是两种可以用于完成许多任务的标签处理器;只要我们记住标签处理器只有在视图树构建时才是高效的,我们就可以利用它们来为我们谋福利。例如,我们可以使用它们来生成如下截图所示的树节点结构:
为了完成这个任务,我们将使用一点 JSTL(<c:if>
和 <c:forEach>
标签处理器)和递归性来增强 <ui:include>
和 <ui:param>
标签。
首先,我们需要一个代表树节点概念的抽象化类。基本上,树节点表示是一个可以递归遍历的标签分层结构。基于此,我们可以编写一个通用的树节点类,如下面的代码所示:
public class GenericTreeNode {
private final List<GenericTreeNode> descendants;
private final String label;
public GenericTreeNode(String label, GenericTreeNode... descendants) {
this.label = label;
this.descendants = Arrays.asList(descendants);
}
public boolean isHasDescendants() {
return !descendants.isEmpty();
}
public List<GenericTreeNode> getDescendants() {
return descendants;
}
public String getLabel() {
return label;
}
}
此类可以作为一个能够定义特定树节点的 bean。如下所示:
@Named
@RequestScoped
public class TreeNodeBean {
private GenericTreeNode root = new GenericTreeNode("Players",new GenericTreeNode("Rafael Nadal",new GenericTreeNode("2013", new GenericTreeNode("Roland Garros", new GenericTreeNode("Winner")), new GenericTreeNode("Wimbledon", new GenericTreeNode("First round"))),new GenericTreeNode("2014", new GenericTreeNode("..."))),new GenericTreeNode("Roger Federer",new GenericTreeNode("2013"), new GenericTreeNode("...")));
public GenericTreeNode getRoot() {
return root;
}
}
有趣的部分是如何将这个结构显示为树节点。HTML 提供了 <ul>
和 <li>
标签,能够将数据表示为列表。此外,嵌套的 <ul>
标签输出一个分层结构,这对于找到自定义表示非常有用。为了反映 TreeNodeBean
中定义的树节点,我们编写了一个名为 node.xhtml
的页面,该页面能够使用 <ui:include>
标签在迭代-递归过程中自动包含,如下所示:
<h:body>
<ui:composition>
<li>#{node.label}
<c:if test="#{node.hasDescendants}">
<ul>
<c:forEach items="#{node.descendants}" var="node">
<ui:include src="img/node.xhtml" />
</c:forEach>
</ul>
</c:if>
</li>
</ui:composition>
</h:body>
node
参数通过 <ui:param>
从名为 index.xhtml
的主页面传递。从主页面,我们传递树节点根。此外,在 node.xhtml
中,我们以递归方式遍历根节点的后代,并显示每个节点,如下面的代码所示:
<h:body>
<ul>
<ui:include src="img/node.xhtml">
<ui:param name="node" value="#{treeNodeBean.root}" />
</ui:include>
</ul>
</h:body>
如果您觉得这个例子没有用,至少要记住 <ui:include>
可以用于递归过程。完整的应用程序命名为 ch12_14
。
使用 ui:debug 调试
<ui:debug>
标签(ComponentHandler
)在组件树中定义了一个能够捕获调试信息(如组件树、作用域变量和视图状态)的调试组件。例如,您可以使用以下代码将 <ui:debug>
标签添加到模板中:
<ui:debug hotkey="q" rendered="true"/>
现在,当您按下 Ctrl + Shift + Q 时,您将看到如下截图所示的内容:
完整的应用程序命名为ch12_9
。<ui:debug>
标签添加到了layout.xhtml
中。
使用ui:remove移除内容
<ui:remove>
标签用于移除内容。这个标签很少使用,但移除类型为<!-- -->
的注释是一个完美的例子。你可能想到了以下这样的行:
<!-- <h:outputText value="I am a comment!"/> -->
这对渲染的 HTML 代码没有副作用。嗯,这并不完全正确,因为在 HTML 源代码中,你会看到类似以下截图的内容:
但如果我们用<ui:remove>
将其封装起来,那么前面的客户端效果将不再产生,以下代码就是证明。
<ui:remove>
<!-- <h:outputText value="I am a comment!"/> -->
</ui:remove>
以下代码将产生相同的效果:
<ui:remove>
<h:outputText value="I am a comment!"/>
</ui:remove>
为了从生成的 HTML 代码中移除注释,你需要在web.xml
中添加context
参数,如下所示:
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
或者,为了与现有的 Facelets 标签库保持向后兼容性,代码如下所示:
<context-param>
<param-name>facelets.SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
完整的应用程序命名为ch12_8
。
使用 jsfc 属性
Facelets 包含一个名为jsfc
的属性。其主要目标是将 HTML 元素转换为 JSF 组件(JSF 页面中的 HTML 原型)。例如,在以下代码中,我们将一个 HTML 表单转换成了一个 JSF 表单:
<form jsfc="h:form">
<input type="text" jsfc="h:inputText" value="#{nameBean.name}" />
<input type="submit" jsfc="h:commandButton" value="Send"/>
</form>
此属性代表快速原型设计,并且易于使用。以下是一个另一个例子——这次jsfc
属性与<ui:repeat>
结合用于生成<table>
标签:
<table>
<thead>
<tr>
<th>Ranking</th>
<th>Player</th>
<th>Age</th>
<th>Coach</th>
</tr>
</thead>
<tbody>
<tr jsfc="ui:repeat" value="#{playersBean.dataArrayList}" var="t">
<td>#{t.ranking}</td>
<td>#{t.player}</td>
<td>#{t.age}</td>
<td>#{t.coach}</td>
</tr>
</tbody>
</table>
第一个例子命名为ch12_7
,第二个例子命名为ch12_25
。
扩展 PageLayout 模板
记得本章开头开发的PageLayout
模板吗?嗯,这是一个不错的模板,但让我们扩展它,使其更加现实。通常,一个网页模板除了我们使用的五个部分之外,还包含标题、登录、搜索、标志、页眉、菜单、左侧、中间、右侧和页脚。也很有必要有一个模板,允许我们做以下事情:
-
无副作用且无需手动移除孤儿 CSS 代码地移除部分(通常,你可以通过编写一个空的
<ui:define>
标签来移除一个部分,但这不会移除该部分的相应 CSS 代码)。此外,一个空的<ui:define>
标签仍然会有副作用,即产生空的<div>
标签或空的<span>
或<td>
标签。这是因为,通常<ui:define>
被包裹在一个<div>
、<span>
或<td>
标签中。 -
设置模板的宽度,即左侧和右侧面板,而不改变 CSS。这些是常见的调整;因此,我们可以通过
<ui:param>
暴露它们,并允许页面作者在 CSS 文件中滚动。 -
添加一个菜单部分。我们可以通过
<ui:include>
作为一个单独的文件来提供支持,或者有一个约定机制,允许页面作者更容易地添加它。
最后,模板将看起来像以下截图:
没有秘密,大多数网站都是将内容放置在多个列中,这些列是通过<div>
或<table>
元素创建的。之后,使用 CSS 将这些元素定位在页面上。基本上,这是大多数模板背后的主要思想,这个也不例外。在下面的图中,你可以看到我们的模板布局,它基于<div>
元素(在图中,你可以看到每个<div>
的 ID):
好吧,由于每个部分都被包裹在一个<div>
元素中,我们可以轻松地使用<ui:fragment>
标签及其rendered
属性来删除它。我们可以将每个部分包裹在一个<ui:fragment>
标签中,并通过<ui:param>
标签设置rendered
属性的值为false
来删除它。这将删除部分而不会产生任何副作用。当删除部分时,我们需要跳过加载相应的 CSS 代码。为此,我们可以将 CSS 文件分为以下三类:
-
一个包含模板通用样式的 CSS 文件(通常这是一个小文件)
-
一个包含为页面上的每个部分定位样式的 CSS 文件(通常这是一个小文件)
-
每个部分的 CSS 文件,包含针对每个部分特定的样式(这些文件可能相当大)
有这样的结构,我们可以轻松地决定不加载已删除部分的 CSS 代码。这可以通过<h:outputStylesheet>
标签中的简单条件实现,该条件基于用于删除部分的相同参数。当删除部分时,我们为它加载一个名为dummy.css
的空 CSS 文件。
因此,模板文件(layout.xhtml
)可能需要更改为以下内容:
<h:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<h:outputStylesheet name="css/default.css"/>
<h:outputStylesheet name="css/cssLayout.css"/>
<h:outputStylesheet name="#{title eq false ? 'css/dummy.css' : 'css/titleStyle.css'}"/>
<h:outputStylesheet name="#{loginsearch eq false ? 'css/dummy.css' : 'css/login_and_searchStyle.css'}"/>
<h:outputStylesheet name="#{top eq false ? 'css/dummy.css' : 'css/topStyle.css'}"/>
...
</h:head>
<h:body>
<div id="wrapper" style="width: #{empty wrapperWidth ? '100%' : wrapperWidth}">
<ui:fragment rendered="#{empty title ? true : title}">
<div id="title">
<ui:insert name="title">
<ui:include src="img/titleDefault.xhtml"/>
</ui:insert>
</div>
</ui:fragment>
...
<ui:fragment rendered="#{empty bottom ? true : bottom}">
<div id="bottom">
<ui:insert name="bottom">
<ui:include src="img/bottomDefault.xhtml"/>
</ui:insert>
</div>
</ui:fragment>
</div>
</h:body>
因此,在模板客户端中,我们可以轻松地删除一个部分(例如,标题部分),使用以下代码行:
<ui:param name="title" value="false"/>
在此时刻,在模板客户端中,我们可以轻松地使用<ui:define>
来提供我们的内容给模板,以及使用<ui:param>
进行以下设置:
-
删除标题部分:这会将
title
参数设置为false
-
删除登录和搜索部分:这会将
loginsearch
参数设置为false
-
仅删除登录部分:这会将
login
参数设置为false
-
仅删除搜索部分:这会将
search
参数设置为false
-
删除标志部分:这会将
logo
参数设置为false
-
删除顶部部分:这会将
top
参数设置为false
-
删除菜单部分:这会将
menu
参数设置为false
-
删除左侧部分:这会将
left
参数设置为false
-
删除右侧部分:这会将
right
参数设置为false
-
删除底部部分:这会将
bottom
参数设置为false
-
设置模板固定宽度:这会将
wrapperWidth
参数设置为宽度 px -
设置左侧面板固定宽度:这会将
leftWidth
参数设置为宽度 px(默认 150px) -
设置右侧固定宽度:这会将
rightWidth
参数设置为宽度 px(默认 150px)
现在,让我们专注于添加菜单。模板用户可以在一个单独的文件中定义菜单,只要它遵循以下简单的编写约定:
<h:body>
<ui:composition>
<ul>
<li>
<h:link value="..." outcome="..."/>
</li>
<li>
<h:link value="..." outcome="..."/>
</li>
...
</ul>
</ui:composition>
</h:body>
该文件可以如下包含:
<ui:define name="menu">
<ui:include src="img/em>.xhtml"/>
</ui:define>
另一种方法是通过<ui:param>
传递菜单项,如下面的代码所示:
<ui:param name="items" value="*item*#*outcome*,*item*#*outcome*,..."/>
这将起作用,因为menuDefault.xhtml
页面提供了一个默认实现,如下面的代码所示:
<ui:composition>
<c:if test="${not empty items}">
<ul>
<ui:repeat value="${fn:split(items, ',')}" var="t">
<li>
<h:link value="${fn:substringBefore(t, '#')}" outcome="${fn:substringAfter(t, '#')}"/>
</li>
</ui:repeat>
</ul>
</c:if>
</ui:composition>
完整的应用程序命名为ch12_18
。在应用程序ch12_19
中,你可以看到这个模板的使用示例,它看起来类似于以下截图:
注意,我们已经删除了搜索和右侧面板部分。
Facelets 的程序化方面
在本章的第二部分,我们将更关注 Facelets 的几个程序化方面。我们将从 JSF 2.2 关于FaceletFactory
的新特性开始,它产生与底层实现上下文相关的 Facelets。
FaceletFactory 的考虑
在 JSF 2.0 中,FaceletFactory
类不能通过访问工厂的标准 APIFactoryFinder
访问。这意味着以下这样的行不起作用:
FaceletFactory faceletFactory = (FaceletFactory)
FactoryFinder.getFactory(javax.faces.view.facelets.FaceletFactory);
但从 JSF 2.2 开始,前面的代码片段应该可以工作。至少这是 JSF 2.2 特性列表所说的。不幸的是,它不起作用,因为规范中没有名为javax.faces.view.facelets.FaceletFactory
的类。在 Mojarra 2.2.6 实现中,FaceletFactory
类甚至不存在;有一个名为com.sun.faces.facelets.impl.DefaultFaceletFactory
的公开类。另一方面,在 MyFaces 2.2.2 中,我们有抽象类org.apache.myfaces.view.facelets.FaceletFactory
。所以,当你决定使用、装饰或编写一个新的FaceletFactory
类时,请记住这些方面。
在不久的将来,我们可能能够通过程序创建一个 Facelet 并调用apply
方法来构建组件树。
与 FaceletCache 一起工作
从 JSF 2.1 开始,Facelets 通过FaceletCache
API 创建和缓存。缓存处理两种不同的 Facelets:视图 Facelets和视图元数据 Facelets。对于每种类型,FaceletCache
API 提供了一个基于传递的 URL(getFacelet
/getViewMetadataFacelet
)返回/创建缓存实例的方法,以及一个能够确定给定 URL 是否存在缓存 Facelet 实例的方法(isFaceletCached
/isViewMetadataFaceletCached
)。
注意
视图元数据 Facelets是一种特殊的 Facelet,对应于ViewDeclarationLanguage.getViewMetadata(javax.faces.context.FacesContext, java.lang.String)
。
Facelets 实例是在getFacelet
/getViewMetadataFacelet
方法中使用公共静态接口FaceletCache.MemberFactory
创建的;该接口负责使用名为newInstance(URL key)
的方法创建 Facelet 或视图元数据 Facelet 实例。getFacelet
方法通过受保护的方法getMemberFactory
访问FaceletCache.MemberFactory
。getViewMetadataFacelet
方法通过受保护的方法getMetadataMemberFactory
访问相同的接口。
FaceletCache
API 的实例是从FaceletCacheFactory
获得的。这是一个工厂类,它提供了两个方法:getFaceletCache
和getWrapped
。第一个返回一个FaceletCache
实例,后者返回被包装的类的实例。
为了返回一个自定义的FaceletCache
实例,我们可以从以下代码所示的自定义FaceletCacheFactory
实现开始:
public class CustomFaceletCacheFactory extends FaceletCacheFactory {
private FaceletCacheFactory faceletCacheFactory;
public CustomFaceletCacheFactory() {}
public CustomFaceletCacheFactory(FaceletCacheFactory faceletCacheFactory) {
this.faceletCacheFactory = faceletCacheFactory;
}
@Override
public FaceletCache getFaceletCache() {
return new CustomFaceletCache();
}
@Override
public FaceletCacheFactory getWrapped() {
return this.faceletCacheFactory;
}
}
此工厂必须使用以下代码在faces-config.xml
中进行配置:
<factory>
<facelet-cache-factory>
book.beans.CustomFaceletCacheFactory
</facelet-cache-factory>
</factory>
现在,我们的CustomFaceletCache
类将覆盖getFacelet
和getViewMetadataFacelet
方法以禁用缓存机制;我们的实现将不会缓存 Facelets。CustomFaceletCache
类的代码如下:
public class CustomFaceletCache extends FaceletCache<Facelet> {
public CustomFaceletCache() {}
@Override
public Facelet getFacelet(URL url) throws IOException {
MemberFactory<Facelet> memberFactory = getMemberFactory();
Facelet facelet = memberFactory.newInstance(url);
return facelet;
}
@Override
public boolean isFaceletCached(URL url) {
return false;
}
@Override
public Facelet getViewMetadataFacelet(URL url) throws IOException {
MemberFactory<Facelet> metadataMemberFactory = getMetadataMemberFactory();
Facelet facelet = metadataMemberFactory.newInstance(url);
return facelet;
}
@Override
public boolean isViewMetadataFaceletCached(URL url) {
return false;
}
public FaceletCache<Facelet> getWrapped() {
return this;
}
}
完整的应用程序命名为ch12_15
。
为了更新缓存,JSF 定期检查 Facelets 视图的变化。在开发阶段,你可能需要比在生产环境中更频繁地执行此检查。为此,你可以设置javax.faces.FACELETS_REFRESH_PERIOD
上下文参数,如下面的示例所示(值表示两次连续检查之间的秒数):
<context-param>
<param-name> javax.faces.FACELETS_REFRESH_PERIOD</param-name>
<param-value>2</param-value>
</context-param>
或者,为了与现有的 Facelets 标签库保持向后兼容性,以下代码如下:
<context-param>
<param-name>facelets.REFRESH_PERIOD</param-name>
<param-value>2</param-value>
</context-param>
如果你想要禁用这些检查,则将javax.faces.FACELETS_REFRESH_PERIOD
(或facelets.REFRESH_PERIOD
)参数设置为-1
。
被 ResourceHandler 吞没的 ResourceResolver
JSF 2.0 将ResourceResolver
类推广为从应用程序 Web 根目录以外的其他位置(如允许我们改变 Facelets 加载模板文件方式的钩子)加载 Facelets 视图的自定义方法。自定义位置代表我们可以编写 URL 的任何位置。
例如,假设我们的PageLayout
模板的 Facelets 视图存储在本地机器上,在D:
的facelets
文件夹中。一个自定义的ResourceResolver
类可以从此位置加载 Facelets 视图——只需覆盖resolveUrl
方法,如下面的代码所示:
public class CustomResourceResolver extends ResourceResolver {
private ResourceResolver resourceResolver;
public CustomResourceResolver(){}
public CustomResourceResolver(ResourceResolver resourceResolver){
this.resourceResolver = resourceResolver;
}
@Override
public URL resolveUrl(String path) {
URL result = null;
if (path.startsWith("/template")) {
try {
result = new URL("file:///D:/facelets/" + path);
} catch (MalformedURLException ex) {
Logger.getLogger(CustomResourceResolver.class.getName()).log(Level.SEVERE, null, ex);
}
} else {
result = resourceResolver.resolveUrl(path);
}
return result;
}
}
如果我们在web.xml
文件中正确配置,JSF 将识别自定义的ResourceResolver
类,如下面的代码所示:
<context-param>
<param-name>javax.faces.FACELETS_RESOURCE_RESOLVER</param-name>
<param-value>book.beans.CustomResourceResolver</param-value>
</context-param>
然而,从 JSF 2.2 开始,我们可以跳过此配置并使用@FaceletsResourceResolver
注解,如下所示:
@FaceletsResourceResolver
public class CustomResourceResolver extends ResourceResolver {
...
使用web.xml
配置的完整应用程序命名为ch12_2
。使用@FaceletsResourceResolver
注解的相同应用程序命名为ch12_5
。
另一方面,建议使用ResourceHandler
类向客户端提供不同类型的资源,例如 CSS、JS 和图像;请参阅第五章中的配置资源处理器部分,使用 XML 文件和注解配置 JSF – 第二部分。默认情况下,ResourceHandler
的首选位置是/resources
文件夹(或在CLASSPATH
上的META-INF/resources
)。如果我们正确地在faces-config.xml
文件中配置它,JSF 就能识别自定义的ResourceHandler
类,如下所示:
<application>
<resource-handler>*fully_qualified_class_name*</resource-handler>
</application>
由于这是一个相当尴尬的方法,JSF 2.2 将这些类统一为一个。更确切地说,ResourceResolver
类的功能已被合并到ResourceHandler
类中,并且ResourceResolver
类本身已被弃用。这一行动的主要结果是ResourceHandler
类中新增了一个名为createViewResource
的方法。这个方法的目的就是替换resolveUrl
方法。因此,我们不再通过ResourceResolver
从自定义位置加载 Facelets 视图,而是可以使用自定义的ResourceHandler
类和createViewResource
方法,如下面的代码所示:
public class CustomResourceHandler extends ResourceHandlerWrapper {
private ResourceHandler resourceHandler;
public CustomResourceHandler() {}
public CustomResourceHandler(ResourceHandler resourceHandler) {
this.resourceHandler = resourceHandler;
}
@Override
public Resource createResource(String resourceName, String libraryOrContractName) {
//other kinds of resources, such as scripts and stylesheets
return getWrapped().createResource(resourceName, libraryOrContractName);
}
@Override
public ViewResource createViewResource(FacesContext context, String resourceName) {
ViewResource viewResource;
if (resourceName.startsWith("/template")) {
viewResource = new CustomViewResource(resourceName);
} else {
viewResource = getWrapped().
createViewResource(context, resourceName);
}
return viewResource;
}
@Override
public ResourceHandler getWrapped() {
return this.resourceHandler;
}
}
当ResourceResolver
类被弃用时,现有的类型javax.faces.application.Resource
类已被赋予一个名为javax.faces.application.ViewResource
的基类。这个类包含一个名为getURL
的单个方法。因此,当需要从自定义位置加载 Facelets 视图时,我们告诉 JSF 使用我们的CustomViewResource
类,如下所示:
public class CustomViewResource extends ViewResource {
private String resourceName;
public CustomViewResource(String resourceName) {
this.resourceName = resourceName;
}
@Override
public URL getURL() {
URL url = null;
try {
url = new URL("file:///D:/facelets/" + resourceName);
} catch (MalformedURLException ex) {
Logger.getLogger(CustomViewResource.class.getName()).log(Level.SEVERE, null, ex);
}
return url;
}
}
注意
createViewResource
方法提供了几个优点,因为它适用于通用视图资源,并且默认情况下与现有的 createResource
方法功能等效。除了更加一致之外,这意味着现在也可以从 JAR 文件中加载 Facelets,而无需提供自定义解析器*。
完整的应用程序命名为ch12_3
。
为了保持向后兼容性,JSF 将允许默认解析器调用新的createViewResource
方法,如下面的代码所示:
public class CustomResourceResolver extends ResourceResolver {
...
@Override
public URL resolveUrl(String path) {
URL result;
if (path.startsWith("/template")) {
ViewResource viewResource = new CustomViewResource(path);
result = viewResource.getURL();
} else {
FacesContext facesContext = FacesContext.getCurrentInstance();
ResourceHandler resourceHandler = facesContext.getApplication().getResourceHandler();
result = resourceHandler.createViewResource(facesContext, path).getURL();
}
return result;
}
}
完整的应用程序命名为ch12_4
。
程序化地包含 Facelets
你已经知道如何使用<ui:include>
标签包含 Facelets。但有时你可能需要程序化地重现如下代码:
<ui:include src="img/fileA.xhtml">
<ui:param name="bparam" value="string_literal"/>
</ui:include>
<ui:include src="img/fileB.xhtml">
<ui:param name="cparam" value="#{managed_bean_property}"/>
</ui:include>
<ui:include src="img/fileC.xhtml"/>
从编程的角度讲,如果你知道如何获取对FaceletContext
的访问权限,如何使用FaceletContext.includeFacelet
方法,以及如何使用FaceletContext.setAttribute
设置属性,同样可以达到相同的效果。例如,上述代码片段的编程版本如下:
public void addFaceletAction() throws IOException {
FacesContext context = FacesContext.getCurrentInstance();
FaceletContext faceletContext = (FaceletContext) context.getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
faceletContext.includeFacelet(context.getViewRoot(), "/files/fileA.xhtml");
faceletContext.setAttribute("bparam", "file B - text as ui:param via string literal...");
faceletContext.includeFacelet(context.getViewRoot(), "/files/fileB.xhtml");
faceletContext.setAttribute("cparam", cfiletext);
faceletContext.includeFacelet(context.getViewRoot(), "/files/fileC.xhtml");
}
完整的应用程序命名为ch12_22
。
创建 TagHandler 类
您已经知道几个 Facelets 标签是标签处理器,而其他的是组件处理器——在第十章中,您看到了如何为自定义组件编写ComponentHandler
类。在本节中,您将了解如何编写TagHandler
类。
注意
标签处理器只有在视图树构建时才有效。
为了编写TagHandler
类,您需要执行以下步骤:
-
扩展
TagHandler
类并重写apply
方法;此方法处理特定UIComponent
类上的更改。通过getAttribute
和getRequiredAttribute
方法访问标签属性,这些方法返回一个TagAttribute
实例,该实例公开属性值、命名空间、本地名称、标签(后者是 JSF 2.2 中的新功能,请参阅getTag
/setTag
文档),等等。此外,使用tag
和tagId
字段来引用与该TagHandler
实例对应的Tag
实例。使用nextHandler
字段将控制权委托给下一个标签处理器。 -
编写一个
*taglib.xml
文件以配置标签命名空间、名称和处理程序类。 -
使用
web.xml
文件中的javax.faces.FACELETS_LIBRARIES
上下文参数来指示*taglib.xml
文件的位置。
例如,假设我们需要以下功能:我们提供一个文本片段,指定它应该显示的次数,以及是否可以大写显示。我们可能会将标签考虑如下:
<t:textrepeat text="Vamos Rafa!" repeat="10" uppercase="yes"/>
TagHandler
类可以满足我们的需求。首先,我们扩展TagHandler
类,如下面的代码所示:
public class CustomTagHandler extends TagHandler {
protected final TagAttribute text;
protected final TagAttribute repeat;
protected final TagAttribute uppercase;
public CustomTagHandler(TagConfig config) {
super(config);
this.text = this.getRequiredAttribute("text");
this.repeat = this.getRequiredAttribute("repeat");
this.uppercase = this.getAttribute("uppercase");
}
@Override
public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
String s = "";
UIOutput child = new HtmlOutputText();
for (int i = 0; i < Integer.valueOf(repeat.getValue()); i++) {
s = s + text.getValue() + " ";
}
if (uppercase != null) {
if (uppercase.getValue().equals("yes")) {
s = s.toUpperCase();
} else {
s = s.toLowerCase();
}
}
child.setValue(s);
parent.getChildren().add(child);
nextHandler.apply(ctx, parent);
}
}
此外,您还需要编写*taglib.xml
文件并在web.xml
文件中配置它。完整的应用程序命名为ch12_17
。
编写自定义 Facelets 标签库函数
当您需要直接在 EL 中评估值时,Facelets 标签库函数(或表达式函数)是一个很好的解决方案。例如,假设我们想要加密/解密文本,并将结果直接放入 EL 表达式中。为了做到这一点,您需要执行以下一般步骤来编写函数:
-
编写一个
public final
Java 类。 -
在这个类中,使用
public static
方法实现所需的功能。 -
编写一个
*taglib.xml
文件以将public static
方法(函数)与 JSF 页面链接。对于每个static
方法,您需要指定名称(<function-name>
)、包含static
方法的完全限定类名(<function-class>
)以及static
方法的声明(<function-signature>
)。 -
使用
web.xml
文件中的javax.faces.FACELETS_LIBRARIES
上下文参数来指示*taglib.xml
文件的位置。
因此,基于这些步骤,我们可以编写一个包含两个函数的类,一个用于加密,一个用于解密,如下面的代码所示:
public final class DESFunction {
...
public static String encrypt(String str) {
...
}
public static String decrypt(String str) {
...
}
}
*taglib.xml
文件非常简单,如下面的代码片段所示:
<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"version="2.0">
<namespace>http://packt.com/encrypts</namespace>
<function>
<function-name>encrypt</function-name>
<function-class>book.beans.DESFunction</function-class>
<function-signature>String encrypt(java.lang.String)
</function-signature>
</function>
<function>
<function-name>decrypt</function-name>
<function-class>book.beans.DESFunction</function-class>
<function-signature>String decrypt(java.lang.String)
</function-signature>
</function>
</facelet-taglib>
在你配置了 web.xml
中的前面 *taglib.xml
文件后,你可以尝试调用加密/解密函数,如下所示:
<h:outputText value="#{des:encrypt('Rafael Nadal')}"/>
<h:outputText value="#{des:decrypt('9QnQL04/hGbJj/PqukPb9A==')}"/>
完整的应用程序命名为 ch12_16
。
Facelets 陷阱
这是一个众所周知的事实,JSF 陷阱不易理解和修复。这主要是因为它们的根源在于:JSF 生命周期、监听器和事件的使用不良习惯、对 EL 处理和评估的误解、标签处理器与组件的冲突组合等等。
在本节中,我们将重点关注三个常见的 Facelets 陷阱。
AJAX 和 <ui:repeat>
使用 AJAX 重新渲染 <ui:repeat>
标签的内容是一个常见的场景。编写如下代码是完全直观的:
<h:form>
<ui:repeat id="playersId" value="#{playersBean.dataArrayList}" var="t">
<h:outputText value="#{t.player}" />
</ui:repeat>
<h:commandButton value="Half It" action="#{playersBean.halfAction()}">
<f:ajax execute="@form" render="playersId" />
</h:commandButton>
</h:form>
因此,最初有一个包含 n 个玩家的列表,当我们点击标记为 Half It 的按钮时,我们希望删除一半的玩家并重新渲染列表。问题是前面的代码片段不会按预期工作,因为 <ui:repeat>
标签不会渲染 HTML 代码;因此,将不会有一个具有 ID playersId
的 HTML 元素。而不是看到一个只有五个玩家的列表,我们将得到一个 malformedXML
错误。
注意
这更多的是使用 JSF AJAX 与未按预期渲染的组件时的一个陷阱。
一个简单的解决方案是将 <ui:repeat>
标签包裹在一个 <div>
标签内,如下面的代码所示:
<h:form>
<h:panelGroup id="playersId" layout="block">
<ui:repeat value="#{playersBean.dataArrayList}" var="t">
<h:outputText value="#{t.player}" />
</ui:repeat>
</h:panelGroup>
<h:commandButton value="Half It" action="#{playersBean.halfAction()}">
<f:ajax execute="@form" render="playersId" />
</h:commandButton>
</h:form>
完整的应用程序命名为 ch12_26
。
举例说明 <c:if>
与 <ui:fragment>
的区别
另一个常见的场景是根据 <c:if>
条件渲染表格数据,如下所示:
<h:dataTable value="#{playersBean.dataArrayList}" var="t">
<h:column>
<c:if test="#{t.age gt 26}">
<h:outputText value="#{t.player}, #{t.age}"/>
</c:if>
</h:column>
</h:dataTable>
再次,结果将不会如预期。问题是 <c:if>
是一个标签处理器;因此,它在树构建时有效地反映了。一个完美的解决方案是将 <c:if>
替换为 <ui:fragment>
标签,这是一个组件处理器。<ui:fragment>
的 rendered
属性可以用以下代码成功替换 <c:if>
测试:
<h:dataTable value="#{playersBean.dataArrayList}" var="t">
<h:column>
<ui:fragment rendered="#{t.age gt 26}">
<h:outputText value="#{t.player}, #{t.age}"/>
</ui:fragment>
</h:column>
</h:dataTable>
或者,以更简单的方式,使用 <h:outputText>
的 rendered
属性;这种方法仅适用于此示例:
<h:dataTable value="#{playersBean.dataArrayList}" var="t">
<h:column>
<h:outputText value="#{t.player}, #{t.age}" rendered="#{t.age gt 26}"/>
</h:column>
</h:dataTable>
相反,更酷的是,使用 lambda 表达式(EL 3.0),你可以编写以下代码:
<h:dataTable value="#{(playersBean.dataArrayList.stream().filter((p)->p.age gt 26 )).toList()}" var="t">
<h:column>
<h:outputText value="#{t.player}, #{t.age}"/>
</h:column>
</h:dataTable>
完整的应用程序命名为 ch12_20
。
举例说明 <c:forEach>
与 <ui:repeat>
的区别
显然,你可能认为 <ui:repeat>
/<ui:include>
对是使用以下代码包含 Facelets 页面列表的完美选择:
<ui:repeat value="#{filesBean.filesList}" var="t">
<ui:include src="img/#{t}"/>
</ui:repeat>
嗯,<ui:include>
标签是一个标签处理器;因此,当视图构建时它将可用,而 <ui:repeat>
标签是一个在渲染过程中可用的组件处理器。换句话说,当 <ui:include>
需要变量 t
时,<ui:repeat>
不可用。因此,<ui:repeat>
应该被替换为一个标签处理器,例如 <c:forEach>
,如下面的代码所示:
<c:forEach items="#{filesBean.filesList}" var="t">
<ui:include src="img/#{t}"/>
</c:forEach>
完整的应用程序命名为 ch12_21
。
摘要
Facelets 是一个涵盖众多有趣方面的大型主题,这些方面在书的几章中很难全面覆盖。正如你所知,有专门介绍 Facelets 的书籍,但我希望在我写的最后三章中,我成功地涵盖了 JSF 2.2 默认 VDL 的大部分内容。可能,Facelets 最常用的部分是模板化;因此,我尝试介绍了一些编写灵活且酷炫模板的实用技巧。当然,除了技能和技术,编写模板也是对想象力的考验。一旦我们掌握了 Facelets 标签并选择了正确的技巧,我们就可以开始编写模板了。如果我们还选择了一些命名约定,那么我们可以轻松地将我们的模板与 JSF 世界分享,就像 Mamadou Lamine Ba 在weblogs.java.net/blog/lamineba/archive/2011/10/03/conventional-ui-design-facelets-and-jsf-22
的 Java.Net 项目中尝试的那样。此外,如果我们用一些 Facelets 编程技巧来丰富我们的模板文件,那么我们真的可以在 JSF 模板的世界中掀起波澜!
附录 A. JSF 生命周期
在 JSF 中,初始请求和回发请求会经过一个 JSF 生命周期。当处理初始请求时,它只执行 恢复视图 和 渲染响应 阶段,因为没有用户输入或动作需要处理。另一方面,当生命周期处理回发请求时,它会执行所有阶段。
此外,JSF 支持 AJAX 请求。一个 AJAX 请求由两部分组成:部分处理(execute
属性)和部分渲染(render
属性)。
在下面的图中,您可以查看 JSF 生命周期的不同阶段:
前一个图中的符号 I、P、E 和 R 分别代表:
-
我:这是为初始请求执行的阶段
-
P:这是为回发请求执行的阶段
-
E:这是在部分处理时执行的阶段
-
R:这是在部分渲染时执行的阶段