JavaEE7-数字-Web-应用开发-全-
JavaEE7 数字 Web 应用开发(全)
原文:
zh.annas-archive.org/md5/93f0d8847a906bcc2ba377d2ffdb9fc2译者:飞龙
前言
这是一本关于 Java EE 7 平台和 Web 数字开发的书籍,它是第一本书《Java EE 7 开发者手册》的延续。这本书的整个焦点是前端技术和业务逻辑层之间的空间和软件架构。虽然在我的第一本书中,这个主题在印刷空间和生活与时间平衡方面有所欠缺,但在本书《数字 Java EE 7》中,我们投入了大量的努力和决心,专门来写关于 Java 表现层的内容。这本书是为那些希望成为在 JVM 上使用标准 Java EE 7 平台构建 Web 应用的优秀和熟练的开发者而写的。
这本书主要从 Java 标准的视角来介绍表现层。因此,有整整几章是专门介绍 JavaServer Faces 的,因为它是 Java EE 7 平台上最重要且最古老的专用 Web 框架。尽管这项技术自 2004 年以来就已经存在,但全球有商业组织和公司依赖 JSF。这些组织从蓝筹公司到备受尊敬的投资银行都有。然而,随着 Java EE 7 的发布,JSF 2.2 有几个关键特性,Web 开发者会非常喜欢并发现它们非常有帮助,例如 HTML5 友好的标记支持以及 Faces Flow。
作为读者,我希望能让你在构建能够让你攀登当代 Java Web 技术高峰的软件的道路上得到启发,并且你能在心中获得一位熟练的大师(或女大师)的资格。
因此,从 JSF 开始,我们将通过对其概念的全面介绍来学习这个框架。我们将继续构建 JSF 输入表单,并学习如何以多种方式验证它们的输入。对于 JSF Web 应用开发来说,最重要的任务是开发创建、检索、更新和删除(CRUD)功能,我们将直接解决这个问题。之后,我们将为 JSF 应用增添更多风格和精致。在这个过程中,我们将编写使用 AJAX 进行验证的应用程序,以实现即时效果。我们将继续我们的冒险,进入优雅的对话作用域后端控制器世界。我们会发现这些小巧的实用工具可以一起映射,并捕捉我们的利益相关者的客户旅程。最后,我们将学习关于 Faces Flows 的内容,这是 JSF 2.2 中的一个亮点。
没有一本 Java 网络技术书籍会不提及 JavaScript 编程语言和新兴技术。许多资深 Java 工程师会同意,Java 在 Web 上在一定程度上已经向 JavaScript 客户端框架让出了表现层。构建 REST/UI 前端应用程序现在如此普遍,以至于所谓的数字 Java 工程师很难忽视 jQuery、RequireJS 等的影响。在野外有几种已知的 JavaScript 框架。在这本书中,我们将介绍 AngularJS。我们将进入 Java、JVM 和 JavaScript 这两个主要景观之间的狂风暴雨的桥梁中间。我不能保证这不会让你感到害怕,但你可能会发现自己能够舒适地站在那里,在 JAX-RS 服务和 AngularJS 控制器之间谈判过梁和扶手。
在本书的远端,我们为您准备了一个特别的即时发布。我们专门用一整章的篇幅来介绍即将到来的 Java EE 8 模型-视图-控制器(Model-View-Controller),这可能会成为我们在构建未来 REST/UI 应用程序方式中的一个备选的炽热翡翠。在本书的终点线之外,我们还汇集了三个重要的附录,希望它们能作为优秀的参考资料。
在每一章的结尾,我们都专门设置了一个特殊部分用于教育练习,希望您觉得这些练习相关且合适,并在学习的过程中享受乐趣,同时您的思维过程也能得到方便的拓展。这是为您写的,为了创新而奋斗的 Java 网络开发者,祝您享受阅读!
您可以在www.xenonique.co.uk/blog/找到我的博客。您可以在 Twitter 上关注我,账号为@peter_pilgrim。
本书源代码可在 GitHub 上找到,链接为github.com/peterpilgrim/digital-javaee7。
本书涵盖内容
第一章,数字 Java EE 7,从网络技术的角度介绍了企业 Java 平台。我们将看到一个简短的 JSF 示例,研究 JavaScript 模块模式,并检查 Java EE 现代网络架构。
第二章,JavaServer Faces 生命周期,从 JSF 框架的基本元素开始。我们将了解 JSF 阶段和生命周期、自定义标签、常见属性和表达式语言。
第三章,构建 JSF 表单,将引导我们了解如何构建 JSF 的创建-更新-检索-删除应用程序表单。我们将使用 Bootstrap HTML5 框架和 JSF 自定义标签,以现代网络方法构建这些表单。
第四章,JSF 验证和 AJAX,深入探讨了从输入表单验证客户数据。我们将研究从后端和持久层到前端使用客户端 AJAX 检查数据的各种方法。
第五章,对话与旅行,将我们的 JSF 知识扩展到对话作用域的 Bean 中。我们将学习如何将数字客户的旅程映射到控制器,并将其他 CDI 作用域应用于我们的工作。
第六章,优雅的 JSF 流程,涵盖了 JSF 2.2 版本的关键亮点——流程作用域 Bean。我们将掌握 Faces 流程和对话作用域 Bean 之间的区别,并在过程中为我们的应用程序添加用户友好的功能。
第七章,渐进式 JavaScript 框架和模块,从 Java 工程师的角度提供了一个现代 JavaScript 编程的快速概述。我们将熟悉 jQuery 和其他相关框架,如 RequireJS 和 UnderscoreJS。
第八章,AngularJS 和 Java RESTful 服务,基于我们新的 JavaScript 知识。我们将使用流行的 AngularJS 框架来编写单页架构应用程序。我们还将获得编写 JAX-RS 服务端点的经验。
第九章,Java EE MVC 框架,探讨了即将推出的 Java EE 8 模型-视图-控制器框架的内部结构。我们将利用 Handlebars 模板框架在 Java 中的移植。
附录 A,JSF 与 HTML5、资源与 JSF 流程,提供了使用 HTML5 支持在 JSF、资源库合约和程序化 JSF 流程中的参考。它还包括有关使用消息和资源包进行国际化的重要信息。
附录 B,从请求到响应,提供了关于现代 Java 企业应用程序架构的密集参考材料。它回答了当收到 Web 请求时会发生什么,以及最终将响应发送回客户端的问题。
附录 C,敏捷性能 – 数字团队内部工作,涵盖了现代数字和敏捷软件开发团队中各种性格和角色的范围。
附录 D,精选参考文献,是一组特别精选的参考文献、资源和链接,用于进一步学习。
你需要这本书的什么
对于这本书,你需要在笔记本电脑或台式电脑上安装以下软件列表:
-
Java SE 8 (Java 开发工具包)
java.oracle.com/ -
GlassFish 4.1 (
glassfish.java.net/) -
一个不错的 Java 编辑器或 IDE 用于编码,例如 IntelliJ 14 或更高版本 (
www.jetbrains.com/idea/)、Eclipse Kepler 或更高版本 (www.eclipse.org/kepler/) 或 NetBeans 8.1 或更高版本 (netbeans.org/)) -
用于构建软件的 Gradle 2.6 或更高版本,这是本书的一部分 (
gradle.org/) -
带有开发者工具的 Chrome 网络浏览器 (
developer.chrome.com/devtools) -
Firefox 开发者工具 (
developer.mozilla.org/en/docs/Tools) -
Chris Pederick 的 Web 开发者和使用代理切换器扩展 (
chrispederick.com/work/web-developer/)
这本书面向的对象
你应该是一位熟练掌握编程语言的 Java 开发者。你应该已经了解类、继承和 Java 集合。因此,这本书是为中级 Java 开发者准备的。你可能拥有 1-2 年的 Java SE 核心开发经验。你应该对核心 Java EE 平台有所了解,尽管深入的知识不是严格必需的。你应该熟悉 Java 持久化、Java 服务器端组件和将 WAR 文件部署到应用服务器,如 GlassFish 或 WildFly 或等效服务器。
这本书的目标读者是那些想学习 JavaServer Faces 或更新现有知识的人。你可能或可能没有 JavaScript 编程经验;然而,本书中有一个专门的入门主题。这主要是一本 Java EE 网络开发书,但要涵盖 AngularJS,你需要学习或重新应用 JavaScript 编码技能。
无论你是来自数字环境,如代理机构或软件公司,还是刚刚开始考虑以网络开发为职业的专业工作,如果你需要在团队中与其他成员合作,你会发现这本书非常有帮助。你将看到行业术语,但我已经将它们的提及保持在最低限度,以便你可以专注于手头的科技并实现你的学习目标。然而,专家可能会在每一章末尾的问题中识别出某些行业想法的渗透。
术语约定
在这本书中,你会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:“我们可以通过使用include指令来包含其他上下文。”
代码块将如下设置:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出将如下所示:
# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
/etc/asterisk/cdr_mysql.conf
新术语和重要词汇将以粗体显示。您在屏幕上看到的,例如在菜单或对话框中的单词,在文本中显示如下:“点击下一个按钮将您带到下一屏幕”。
注意
警告或重要注意事项将以如下所示的框显示。
小贴士
小技巧和技巧将如下所示。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书籍标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误,请通过访问www.packtpub.com/submit-errata来报告它们,选择您的书籍,点击错误提交表单链接,并输入您的错误详情。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站,或添加到该标题的“错误”部分下的现有错误列表中。任何现有错误都可以通过从www.packtpub.com/support选择您的标题来查看。
侵权
在互联网上,版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过链接版权问题联系我们涉嫌盗版的内容。
我们感谢您在保护我们作者和提供有价值内容方面的帮助。
问题
如果你在本书的任何方面遇到问题,可以通过链接问题咨询我们,我们将尽力解决。
第一章. 数字 Java EE 7
| "没有人比我更生气,因为网站不工作" | ||
|---|---|---|
| --美国总统巴拉克·奥巴马,2013 年 10 月 21 日在玫瑰园的演讲 |
数字适应是当代网络设计涉及的软件开发者的时代标志。数字转型这个短语是商业高管们又一种迎合的时髦词汇。企业 Java 开发者不必害怕这个新的数字世界,因为我们参与构建这个星球上最激动人心的软件。我们正在为用户、客户和人们构建软件。将数字这个词替换为用户体验,你将立刻明白所有这些喧嚣的原因。
因此,让我们彻底摆脱营销术语。数字化转型将非在线业务流程转化为等效的在线版本。当然,一个笨重的丑陋毛毛虫不会一夜之间突然变成美丽的红雀蝶,没有生活经验和遗传因素。这需要开发者、设计师和建筑师相当大的技能来适应、转换并将业务需求应用于技术。近年来,软件行业已经认识到用户及其经验的合理性。本质上,我们已经成熟了。
这本书是关于那些能够成熟并且渴望成熟的开发者。这些开发者能够拥抱 Java 技术,并对相关网络技术表示同情。
在本章中,我们将从网络开发者的要求开始,也就是所谓的“前端”工程师,以及数字和创意行业。我们将调查企业 Java 平台,并问这样一个问题:Java 在哪里?我们将研究 JavaScript 的增长。我们将了解 Java EE 7 的现代网络架构。最后,我们将通过一个简单的 JavaServer Faces 示例结束本章。
在数字领域工作
在数字领域工作要求企业超越传统和制度化的思维。仅仅堆砌一些 HTML、几个指向新闻发布的链接和一些白皮书,打包一些写得不好的 JavaScript 代码,然后称之为网站,这种策略曾经是合适的。如今,私营和公共公司,甚至政府,通过关注高可用性和内容来为商业的长尾效应规划网络技术(en.wikipedia.org/wiki/Long_tail)。如果你的网络技术难以使用,那么你将不会赚到任何钱,也没有任何公民会使用你的在线服务。
数字 Java 开发者要求
作为数字开发者,你肯定需要强大的开发机器,能够同时运行多个应用程序。你需要坚强和自信,并坚持你的经验是最好的。你对自己的学习负责。数字开发者不应该被适合销售和营销部门的笔记本电脑所束缚。
你的工作机器必须能够物理上处理以下列表中每个工具的需求:
| 项目 | 说明 |
|---|---|
| Java SE 8(或至少 Java SE 7) | Java SE 8 于 2014 年 3 月 18 日发布,提供了 Lambda 表达式,其中函数是一等公民。Java 7 是短期内的可接受替代品,对于谨慎的商业 IT 总监来说也是如此。 |
| Java EE 7 | GlassFish 4.1 应用程序服务器是 Java EE 7 的参考实现,但缺乏专业支持。作为替代,IT 总监可以考虑拥有服务级别协议的 Red Hat JBoss WildFly 9。 |
| 集成开发环境 | IDE 如 IntelliJ IDEA 14、Eclipse Kepler 4.3 或 NetBeans 8 |
| Adobe 创意套件 | Adobe Photoshop CC(有时还有 Adobe Illustrator)是创意数字媒体行业内的实际图形工作。 |
| Cucumber 或 Selenium | Cucumber 是一种行为驱动开发,用于测试 Web 应用程序的功能。它是针对 Ruby 编程语言编写的,因此需要该环境和工具链。 |
| 一套现代网络浏览器 | Mozilla Firefox、Google Chrome 和 Internet Explorer 10 以及支持 HTML5 和最新 W3C 标准的实际网络浏览器。最近推出的 Windows 10 需要 Edge。 |
| 网络浏览器开发者插件 | 在你的工具集中拥有 JavaScript 调试器、HTML 和 CSS 元素检查器非常有帮助。如 Chrome 开发者工具之类的插件可以简化数字工程。 |
| 文本编辑器 | 轻量级文本编辑器常用于处理小型工作,对于编辑 JavaScript、Puppet(或 Chef)脚本,以及 HTML 和 CSS 文件非常有用。 |
只需查看这个软件表,就不难理解为什么平均的商业提供的公司笔记本电脑在处理这种开发时如此不配备。
小贴士
数字工程师是聪明、专业的工程师
我个人有一台 2012 年的 MacBook Pro Retina 版,16 GB 的 RAM,512 GB 的静态硬盘驱动器作为我的主要机器。一些客户给了我配置不当的机器。一个特定的金融客户给了我一台只有 4 GB RAM 的 Dell Latitude,运行 Windows 7 专业版。这台开发者机器性能如此之差,以至于我不得不多次投诉。通知你业务中的决策者,数字工作者需要适合工程和设计优秀用户体验的开发机器。
让我们从创造力和设计转向 Java 平台。
图中的 Java
Java 平台目前被广泛使用。它是最先商业化的语言,具备 JVM 和具有垃圾回收、沙箱安全和网络功能的字节码,被企业所采用。Java 的最大优势是,企业信任这个平台来驱动服务器端计算中的企业级应用。自 1995 年以来,这种深度优势已经发展到平台被视为非常成熟和主流的程度。作为主流群体的一个部分,其劣势是创新需要一段时间才能发生;作为平台的管理者,早期的 Sun Microsystems 和现在的 Oracle Corporation,始终保证向后兼容性和通过 Java Community Process 维护标准。
JVM 是平台上的皇冠上的宝石。Java 是在 JVM 上运行的母编程语言。其他如 Scala、Groovy 和 Clojure 等语言也运行在 JVM 上。这些替代 JVM 语言很受欢迎,因为它们向主流开发者引入了许多函数式编程思想。函数式编程原语,如闭包和推导式,以及如 Scala 这样的语言展示了纯面向对象模型和混入。这些语言受益于一个名为 REPL 的易于交互的工具。
小贴士
事实上,Java SE 9 很可能将拥有 读取-评估-打印-循环(REPL)。请密切关注官方 OpenJDK 项目 Kulla 的进展,该项目网址为 openjdk.java.net/projects/kulla/。
2014 年发布的 Java SE 8 特性包括函数式接口,也称为 Lambda,它将闭包和函数式块的好处带到了平台上的主要 JVM 语言。
无论你选择哪种编程语言来开发你的下一个企业级应用,Java、Scala 或其他,我认为你可以赌 JVM 会长期存在,至少在未来十年左右。PermGen 问题终于在 Java SE 8 中得到了解决,因为其中没有永久生成。在 Java SE 8 之前,PermGen 是许多内存泄漏(缓慢且稳定的内存消耗者)的来源。这也是 JVM 将类加载到内存片段(如 Java 运行时,例如java.lang.String、java.lang.System或java.util.concurrent.ConcurrentHashMap)的专用空间。然而,类很少被卸载或压缩大小,尤其是在 JVM 非常长的执行过程中。如果你在连续几天甚至几周的时间内运行网站,并且有一定程度的用户交互,那么你的应用程序(及其应用服务器)可能会耗尽内存(java.lang.OutOfMemoryError: PermGen space)。永久生成是 JDK 8 之前为 Java 类的内部表示保留的存储区域。对于长时间运行的应用服务器和 JVM 进程,即使 WAR/EAR 应用程序未部署和卸载,元数据和类的引用也可能仍然保留在永久生成内存中。在 Java SE 8 中,为 Java 类保留的内存分配是自适应的。现在,JVM 可以优雅地管理自己的内存分配,与之前的版本相比,至少提高了 10%的效率。
在 Java SE 8 中,我们有一个名为 G1 的垃圾回收器,它是一个并行收集器。Java SE 8 还包括新的字节码,以提高诸如 JRuby 和 Clojure 这样的动态语言的效率。从 JDK 7 继承的 InvokeDynamic 字节码和 Method Handle API 特别为 Nashorn(JavaScript(ECMAScript Edition 6)的实现)进行了优化。
提示
截至 2015 年 4 月,Oracle 停止向其公共下载站点发布 Java SE 7 的更新。请将此信息转达给你的 CTO!
毫无疑问,Java 平台将继续作为后端技术为数字工程师提供服务。企业甚至可能会利用 Java SE 8 平台提供的客户端技术。JavaFX 是一个有趣的解决方案,但超出了本书的范围。
我们现在应该引入一些代码。以下是一个 Java SE 8 的 Lambda 函数:
public interface PaymentIssuer {
public void allocate( int id );
}
@ApplicationScoped
public class CreditCardTicketTracker() {
// Rely on CDI product factory to inject the correct type
@Inject PaymentIssuer issuer;
public void processTickets( List<Ticket> ticketBatch ) {
final LocalDate dt = LocalDate.now().plusDays(2)
ticketBatch.stream()
.filter(
t -> t.isAvailable() &&
t -> t.paymentType == PaymentType.CreditCard &&
dt.isBefore(DateUtils.asLocalDate(
t.getConcertDate())))
.map(t -> t.getAllocation().allocateToTicket(t))
.forEach(allocation -> issuer.allocate(allocation));
}
}
如果这段代码看起来非常奇怪,那么你可能还不熟悉 Lambda 表达式。我们有一个上下文和依赖注入(CDI)bean,它是应用范围的,称为CreditCardTicketTracker。它有一个名为processTickets()的方法,该方法接受一个包含 Ticket 对象的列表集合。Ticket 的确切类型并不重要。然而,重要的是 CDI 注入到普通 Java 对象(POJO)中的PaymentIssuer类型。processTickets()方法调用了 Java SE 8 集合的流 API。本质上,调用parallelStream()方法会导致在 Java 集合的每个元素上以多线程的方式执行处理操作。Lambda 表达式位于更新后的 Collection API 的filter()、map()和forEach()方法上。
此外,代码的阅读效果接近于书面英语。现在让我来解释一下processTickets()方法。外部组件正在将一批音乐会门票发送到我们的组件CreditCardTicketTracker进行处理。对于批次中的每一张门票,我们只过滤那些标记为可用、已经使用信用卡支付并且音乐会日期在当前日期两天或两天以上的门票。顺便说一句,我们利用了 Java SE 8 中新出现的java.time.LocalDate。
因此,非常简短地说,Lambda 表达式是一个匿名方法,其语法遵循以下格式:
( [[Type1] param1 [, [Type2] param2 ....]] ) -> {
/*
* Do something here and return a result type
* including void
*/
}
Lambda 表达式可以是参数化的;因此,Java 编译器可以推断出可以用表达式替换java.lang.Runnable类型。如果有参数,编译器可以在提供足够信息的情况下推断出参数的类型。因此,Type1 和 Type2 声明是可选的。Lambda 必须返回一个类型的单个实例,或者可能是 void,这意味着可以省略花括号。
Lambda 表达式简洁、节省时间,并允许将函数传递到库中。有关更多信息,请参阅 Oracle 网站上的优秀 Java SE 8 教程(docs.oracle.com/javase/tutorial/java/index.html)。正如我们在前面的示例中所看到的,你的应用程序可以使用集合中的并行流功能来实现并发。
Lambda 表达式的一个直接用途是替换调用管理线程服务的内部类,即 Java EE 7 中的javax.enterprise.concurrent.ManagedExecutorService。我们知道 Java 支持多线程、网络和安全。让我们把注意力转向客户端。
JavaScript 令人印象深刻的增长
在过去十年中,数字工程师对 JavaScript 作为编程语言的健康尊重。尽管它有许多缺点,但开发者已经学会了编写模块化应用程序,真正利用了这种在浏览器和服务器上普遍执行的编程语言的功能。最终改变游戏规则的框架是被称为 jQuery 的东西。它是由 John Resig 编写的,目的是简化 HTML 的客户端脚本。2006 年发布,它是当今 JavaScript 库中最受欢迎的框架。
jQuery 最大的创新是名为 Sizzle 的选择器引擎,它允许 JavaScript 编程通过声明过滤 DOM 元素,允许遍历,并通过集合理解(collection comprehension)类型执行算法。它为 JavaScript 开发开辟了新的方向。
不仅 jQuery 在推动 JavaScript 的发展。实际上,这种进步可以追溯到 AJAX 技术的重新发现以及几个竞争框架的出现,例如 Script.aculo.us 和 Prototype。
以下是一个使用 jQuery 的 JavaScript 代码示例:
var xenonique = xenonique || {}
xenonique.Main = function($) {
// Specifies the page marker ID
var siteNavigationMarker = '#navigationMarker';
var init = function() {
$(document).ready( function() {
$('#scrollToTheTopArrow').click( function() {
$('html, body').animate({
scrollTop: 0
}, 750);
})
})
}
var oPublic = {
init: init,
siteNavigationMarker: siteNavigationMarker,
};
return oPublic;
}(jQuery);
如果你在理解前面的代码上遇到困难,请不要担心,当然也不要避开这段 JavaScript 代码。这段代码展示了在现代以良好的、可靠的和可维护的方式开发 JavaScript 的现代习语,而不使用全局变量。它完美地说明了将 JavaScript 变量和函数方法保持在封装作用域内的模块技术。作用域是理解 JavaScript 编程中最重要的项目。
前面的 JavaScript 代码创建了一个名为 xenonique 的命名空间,它存在于自己的作用域中。我们利用 Module Pattern 创建了一个名为 Main 的模块,该模块依赖于 jQuery。定义了一个名为 init() 的方法,它使用匿名函数执行 jQuery 选择器。当用户点击具有 ID #scrollToTheArrow 的 HTML 元素时,网页会在 750 毫秒内自动滚动到顶部。
JavaScript 模块模式
在此代码中,Douglas Crockford 在他的经典著作 JavaScript: The Good Parts 中详细阐述的关键技术是创建一个类似单例对象(singleton object)的模块。由于声明末尾的参数语句,该模块被解释器立即调用,这依赖于 jQuery 实例。
让我们简化模块以产生效果:
var xenonique = xenonique || {}
xenonique.Main = function($) {
/* ...a */
return oPublic;
}(jQuery);
上一段代码中的模块 xenonique.Main 实际上是一个 JavaScript 闭包,它有自己的作用域。因此,模块模式模拟了私有和公共成员和函数。闭包的返回值是一个对象,它定义了公开可访问的属性和方法。在模块中,init() 方法和 siteNavigationMarker 属性对其他 JavaScript 变量是公开可访问的。闭包与 JavaScript 执行上下文一起保留在返回对象中,因此所有私有和公共方法将在整个应用程序的生命周期中存在。
JavaScript 高级库
对于一些工程师来说,即使是围绕 jQuery 选择器的自定义 JavaScript 编写,也过于详细和底层。AngularJS 是一个将客户端编程进化推向更高阶段的 JavaScript 框架的例子。AngularJS 特别具有将 DOM 元素声明性地双向绑定到彼此或 JavaScript 模块代码的功能。AngularJS 的创造者旨在将 模型-视图-控制器(MVC)设计模式和关注点分离引入到网络应用开发中,并通过内置的测试框架激发行为驱动设计。
AngularJS (angularjs.org/) 是 JavaScript 新现代运动中的亮点之一。尽管似乎每周都有新的 JavaScript 库被发明,但在专业开发领域脱颖而出的还包括 GruntJS、Node.js、RequireJS 和 UnderscoreJS。
GruntJS (gruntjs.com/) 特别有趣,因为它在 C 或 C++ 中的 Make 或者在 Java 空间中的 Maven 或 Gradle 的工作方式相似。Grunt 是一个 JavaScript 任务管理系统,它可以构建应用程序、执行单元测试、编译 Sass 和 LESS 文件到 CSS,并使用资源执行其他任务。它还可以调用通过称为精简的过程压缩 JavaScript 的实用程序,并将它们优化成丑陋(难以逆向工程)的文件,以实现速度和一定程度的安全性。
注意
Sass (sass-lang.com/) 和 LESS (lesscss.org/) 是设计师和开发者使用的 CSS 预处理器。这些工具将可重用的通用样式配置转换为特定设备和网站的样式表。
对于一位新数字工程师,你可能觉得这个讨论令人压倒。所以我会以下表的形式总结:
| JavaScript 项目 | 描述 |
|---|---|
| jQuery | 用于操作 DOM 和通过 ID 和名称选择元素的最重要的开源 JavaScript 库。它有一个非常流行的插件架构,提供了许多产品。jquery.org/ |
| jQuery UI | 这是一个流行的插件,它扩展了标准的 jQuery,并添加了额外的动画、可定制的主题以及包括日期日历选择器在内的 UI 组件。jqueryui.org/ |
| RequireJS | 一个 JavaScript 文件和模块的依赖管理框架及模块加载器。这个框架具有优化大型应用程序模块包的能力,特别是通过异步模块定义 API。requirejs.org/ |
| Nashorn | 由 Oracle 构建,并随 Java SE 8 标准提供的 JavaScript 运行时引擎。Nashorn 在 JVM 上运行,是开源的,也是 OpenJDK 项目的一部分。openjdk.java.net/projects/nashorn/ |
| Dojo Toolkit 和微内核架构 | 一个经过重构的 JavaScript 模块化框架和工具包,包含丰富的组件。它利用 AMD(异步模块定义)实现快速下载速度和模块的高效性,仅加载客户端应用程序所需的必要内容。Dojo Toolkit 包含实用的图表和可视化组件。dojotoolkit.org/ |
| Ember JS | Ember 是一个用于构建客户端 Web 应用程序的框架。它使用 JavaScript 调用模板以生成页面内容。Ember 致力于希望与原生应用程序竞争的移动开发者。该框架利用了 Handlebars 模板库。emberjs.com/ |
| Handlebars JS | Handlebars 是一个用于客户端 Web 应用程序的 JavaScript 模板库。在初步观察中,模板类似于 HTML,增加了表达式标记。熟悉 AngularJS 的人会发现,这些表达式的语法非常相似。www.handlebarsjs.com/ |
| Underscore JS | 这是一个 JavaScript 开发者库,通过 API 将函数式编程的思想和结构引入语言。它包含超过 80 个库辅助方法,包括 select、map、flatMap、filter、reduce、forEach 和 invoke 等方法。underscorejs.org/ |
| Backbone JS | 一个为客户端应用程序添加建模功能的 JavaScript 框架。它为模型提供与 DOM 的键值绑定和自定义应用程序事件。模型和集合可以保存到服务器。Backbone 还提供了具有声明性数据绑定的视图。在许多方面,这个框架被视为 AngularJS 的可行竞争对手。backbonejs.org/ |
| Angular JS | AngularJS 是一个提供 DOM 元素和自定义 JavaScript 模块代码之间双向数据绑定的 JavaScript 框架。它具有模型-视图-控制器架构,并通过一个名为指令的功能提供对自定义 HTML 组件的支持。AngularJS 也得到目前在工作于 Google 的开发者的强烈支持,因此它是一个著名的 JavaScript 框架。它的优势在于单页网络应用程序和声明式编程。angularjs.org/ |
正如你所见,如果你恰好与精通上述许多技术的前端(界面开发者)工程师合作,你会面临许多挑战。一个企业级 Java 或后端工程师必须了解其他人的技能集。(参见附录 C,敏捷性能 – 数字团队内部工作)。
信息架构和用户体验
当前的数字工作者面临着多种输入和客户设计要求。其中之一就是所谓的信息架构(IA)。这本质上是一个网站的静态结构,并描述了为了获得最佳的客户用户体验而进行的流程。信息架构模拟了客户可以在网页、应用程序和环境中所看到的共享视觉和上下文数据。
大多数 Java 工程师可能都见过在业务团队讨论中,设计师和业务分析师之间传递的信息架构图。简单地忽略或忽视这些讨论是一个错误,这就是为什么数字开发者应该对信息架构如何、为什么以及在哪里应用有一定的了解。它看起来有点像网站地图的视觉化。以下是一个电子商务应用程序的信息架构图示例:

时尚商店网站的基本信息架构
上述图表描述了一个潜在时尚商店网络应用程序的信息架构。对于本书来说,它可能显得过于简单。然而,这个图表是为了提案、为了赢得开发并构建网络应用程序的合同而进行的销售会议的一部分。架构基于三个对客户至关重要的组件:初始欢迎页面、访问目录和产品,以及关于公司的内容。对于这个特定的客户,图表反映了他们对特色时尚商品、品牌和促销的关注。信息架构会随着与客户进一步互动而演变。如果我们赢得了开发时尚商店应用程序的提案,那么可能需要更深入地研究网站的搜索功能。
信息架构有助于设计师、开发者和业务利益相关者通过一个共享的语言来理解网站的结构,这个语言巩固了领域的知识和目的。网站所有者和业务可以视信息架构为内容的分解。他们可以理解网站是如何构建的。
信息架构也可以是关于对内容的感觉反应(某人内心的感受)。在未来,这对于可穿戴计算机来说将非常重要,因为用户可能不会通过屏幕来感受和接收通知。交互可能通过声音,甚至通过气味或味道。这些建模技术和能够以具有情感影响的方式写作的能力被纳入一个新的、最近的工作标题:内容策略师。
编写和构建专业网站或企业应用程序已经从其婴儿期成长起来。现在的开发者必须可接近、可适应和复杂。可接近意味着能够与他人和谐共事并作为一个团队。可适应意味着面对持续的挑战和变化时无所畏惧;复杂意味着能够应对压力并优雅地处理它。
让我们继续了解企业 Java 平台的技术方面。
Java EE 7 架构
Java EE 是一个开放标准,一个企业平台,以及一个适用于在服务器上执行的应用程序的规范。对于数字工作者,Java EE 提供了许多构建 Web 应用程序的服务,包括 Servlets、JavaServer Faces、Facelets、上下文和依赖注入、Java 消息服务、WebSocket 以及至关重要的用于 RESTful 服务的 Java。
Java EE 7 于 2013 年 6 月宣布并发布,其主题是更好的 HTML5 支持和提高生产力。目前,看起来未来的 Java EE 8 规范可能会通过声明性注解(JSR 220 的扩展)增加对服务和管理方面的声明性配置支持。
标准平台组件和 API
Java EE 架构是一个基于容器和层的架构。设计的核心是一个应用服务器和日益基于云的解决方案,尽管这尚未在规范中标准化。
在非云基础的 Java EE 中,我们可以将 Java EE 视为四个独立的容器:第一个是用于企业 Java Bean 生命周期管理的 EJB 容器,第二个容器是用于 Java Servlets 和托管 Bean 生命周期管理的 Web 容器。第三个容器被称为应用程序客户端容器,它管理客户端组件的生命周期。最后,第四个容器是为 Java Applets 及其生命周期预留的。
大多数时候,数字工程师关注的是 EJB、Web 和托管 CDI Bean 容器。
注意
如果您对架构的完整描述感兴趣,请参阅我的第一本书,由 Packt Publishing 出版的 《Java EE 7 开发者手册》。您可以将其视为这本书的姊妹篇。
根据 Java EE 7 规范,有两个官方实现配置文件:完整配置文件和 Web 配置文件。完全符合 Java EE 规范的产品,如 Glassfish 或 JBoss WildFly 应用服务器,实现了完整配置文件,这意味着它拥有所有容器:EJB、CDI 和 Web。像 Apache Tomcat 这样的服务器,它基于 Java EE 7 Web 配置文件构建,仅实现 Web 容器。像 Tom EE 这样的服务器,它扩展了 Apache Tomcat,实现了 Web 容器,并可能添加额外的设施,如 CDI、EJB,甚至是 JMS 和 JAX-RS。
以下图表说明了作为企业解决方案的完整配置文件 Java EE 7 架构。Java EE 平台是硬件、磁盘存储、网络和机器代码的抽象。Java EE 依赖于 Java 虚拟机的存在以进行操作。有 JVM 版本已经移植到英特尔、ARM、AMD、Sparc、FreeScale 等硬件芯片,以及包括 Windows、Mac OS、Linux、Solaris,甚至树莓派在内的操作系统。
因此,Java 和其他替代语言可以在这个芯片架构上无缝执行,这适用于企业应用程序。Java EE 为标准核心 Java SE 提供了额外的标准 API。让我们简要地看看一些 Java EE 功能。
Java EE 7 利用 Java SE 版本的 新输入输出(NIO)功能,允许 Java Servlet 3.1 处理异步通信。
JavaServer Faces 2.2 现在增强了与 CDI 的更紧密集成、改进的生命周期事件以及 AJAX 请求的新队列控制。对于数字工程师来说,有合理的 HTML5 支持、资源库合约、faces flows 和无状态视图。

Java EE 7 完整平台和 JSR 规范的示意图
表达式语言 3.0 并非一个全新的规范,但它是从 Servlets、JavaServer Pages 和 JavaServer Faces 中分离出来的规范。开发者可以访问表达式评估器,并对自定义表达式进行调用处理,例如,他们的自定义标签库或服务器端业务逻辑。
也许,Java EE 7 中最重要的变化是加强 CDI(控制反转),以提高类型安全性和简化 CDI 扩展的开发。CDI、拦截器和通用注解提高了 CDI 容器内类型安全的依赖注入和生命周期事件的观察。这三个规范共同确保可以编写解决横切关注点且可应用于任何组件的扩展。开发者现在可以编写可移植的 CDI 扩展,以标准方式扩展平台。
RESTful 服务(JAX-RS)有三个关键增强:添加客户端 API 以调用 REST 端点、客户端和服务器端点的异步 I/O 支持以及超媒体链接。
Bean 验证是针对领域和值对象的约束验证解决方案。现在它支持方法级别的验证,并且与 Java EE 平台的其他部分有更好的集成。
WebSocket API 1.0 是添加到 Java EE 7 的新规范。它允许 Java 应用程序与 HTML5 WebSocket 客户端进行通信。
Java EE 7 继续了平台早期版本中开始的主题:提高开发便利性,并允许开发者编写 POJOs。
Xentracker JavaServer Faces
现在让我们进入开发模式。我们将查看我为之前书籍创建的 JSF 示例。项目被称为XenTracker。代码可在github.com/peterpilgrim/javaee7-developer-handbook找到。以下是一个 JSF 视图(xentracker-basic/src/main/webapp/views/index.xhtml):
<!DOCTYPE html>
<html
>
<ui:composition template="/views/template.xhtml">
<f:metadata>
<f:viewParam name="id" value="#{taskListViewController.id}" />
<f:event type="preRenderView" listener="#{taskListViewController.findProjectById}"/>
</f:metadata>
<ui:define name="content">
<div class="entry-form-area">
<h1>Task List for Project</h1>
<p>
<h:outputText value="Task list for this project:"/>
</p>
<h:link styleClass="btn btn-primary" outcome= "createTask.xhtml?id=#{taskListViewController.id}">
<f:param name="id" value="#{taskListViewController.id}" />
Create New Task
</h:link>
<table class="table table-striped table-bordered" >
<tr>
<td>Title:</td>
<td><strong>
 #{taskListViewController.project.name}
</strong></td>
</tr>
<tr>
<td>Headline:</td>
<td> 
#{taskListViewController.project.headline}</td>
</tr>
<tr>
<td>Description:</td>
<td> 
#{taskListViewController.project.description}</td>
</tr>
</table>
<!-- form table grid see below -->
</div>
</ui:define>
</ui:composition>
</html>
初看之下,这看起来像是标准的 HTML5;具体的 JSF 标签和表达式语言语法并不那么明显。项目中的文件名为projectTaskList.xhtml,这为该文件代表的视图类型提供了重要线索。实际上,这个视图是一个 JSF Facelet 模板。文件类型指的是 HTML 4.01 标准创建后,由万维网联盟(W3C)批准的较老 XHTML 标准。XHTML 与 HTML4 相同,但受 XML 模式限制,因此真正是一个 XML 文档。
为了从 JSF 生成任何输出,规范规定了提供一种页面描述语言(PDL)。可能存在多种类型的 PDL。标准 PDL 是一个称为 Facelet 视图模板的视图。Facelet 框架最初是一个独立于 JCP 的开源 API,但自从 JSF 2.0 以来,它已经被纳入其中。Facelet 模板被设计成轻量级,并且与 JSF 框架原生工作。Facelets 对 JSF 生命周期有隐式了解,可以通过表达式语言访问 UI 组件,并为标准 HTML 元素提供装饰器。Facelets 模板解决方案在 servlet 引擎中表现非常出色。它最终可能会在 Java EE 8 的规范中拥有自己的特性。
上述示例中的模板通过特定的 Facelet 标签进行说明,即<ui:define>和<ui:composition>。简而言之,<ui:composition>标签指的是一个模板视图,它定义了页面的布局。可以将其视为主视图。<ui:define>标签定义了实际内容,这些内容将被插入到模板中,形成最终的页面输出。我们将在本书的后续章节中全面探讨 JSF 和 Facelets。
通过检查视图定义顶部的打开 XML 定义,我们可以看到一些命名空间声明。xmlns:ui 命名空间,正如你已经看到的,指的是 Facelet 扩展。xmlns:f 命名空间指的是核心 JSF 标签,而 xmlns:h 命名空间指的是渲染 HTML 元素的 JSF 组件。警告你,不要期望完全的一对一匹配,因为你将在后面理解。例如,<h:outputText> 标签只是打印内容;你几乎可以将其视为 PHP 中的 echo 函数。
在你们当中真正细心的观察者会看到,使用 JSF 建立现代网站是完全可能的。标记中有一个 <div> 元素,是的,正如你可能正确猜到的,Bootstrap CSS 确实被使用了。重要的是要强调 JSF 是一种服务器端模板解决方案和视图技术。
重新查看以下部分:
<h:link styleClass="btn btn-primary" outcome="createTask.xhtml?id=#{taskListViewController.id}">
这大约等同于以下直接的 HTML5:
<a style="btn btn-primary" href="JSF_munged_createTask.xhtml?id=jsf_fizz_12345">
在 JSF 中,语法分隔符表示 表达式:#{...}。表达式语言在 JSF 运行时在生命周期渲染阶段被解析。我们将在下一章讨论生命周期。
之前的视图是不完整的,因为我们缺少表格视图组件。尽管由于网页内容的布局,HTML 表格不被看好,但表格对于其原始目的仍然极其重要,即显示表格数据。
以下是在正确位置插入的缺失的表格视图:
<h:form>
<h:dataTable id="projects" value="#{taskListViewController.project.tasks}" styleClass="table table-bordered" var="task">
<h:column>
<f:facet name="header">
<h:outputText value="Task Name" />
</f:facet>
<h:outputText value="#{task.name}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Target Date" />
</f:facet>
<h:outputText value="#{task.targetDate}">
<f:convertDateTime pattern="dd-MMM-yyyy" />
</h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Completed" />
</f:facet>
<h:outputText value="#{task.completed}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Action" />
</f:facet>
<h:link styleClass="btn" outcome="editTask.xhtml?taskId=#{task.id}">
<f:param name="taskId" value="#{task.id}" />
<i class="icon-edit"></i>
</h:link>
<h:link styleClass="btn" outcome="removeTask.xhtml?taskId=#{task.id}">
<f:param name="taskId" value="#{task.id}" />
<i class="icon-trash"></i>
</h:link>
</h:column>
</h:dataTable>
<hr/>
<h:commandLink styleClass="btn btn-primary btn-info" immediate="true" action="#{taskListViewController.returnToProjects}">
Return to Projects</h:commandLink>
</h:form>
之前的代码示例展示了 JSF 风格的内容。<h:form> 标签对应于 HTML 表单元素。<h:dataTable> 标签表示渲染来自管理 JSF 实例的数据的表格组件网格。值属性表示从名为 taskListViewController 的服务器组件检索的数据。该控制器使用表达式语言访问任务对象的列表集合,并将其转换为 taskListViewController.getProjects().getTasks() 的 Java 反射调用。再次值得注意的是属性 styleClass="table table-bordered" 中的 Bootstrap CSS。
<h:dataTable> JSF 组件本质上遍历 Java 集合、数组、迭代器或枚举器,设置由属性 var 定义的当前元素,并在其体中处理内容。它构建一个 HTML 表格。<h:column> 标签声明行中的每一列的内容,而 <f:facet> 标签声明特定于放入表格表头行的内容。
<h:outputText> 标签也足够灵活,可以接受另一个常用标签 <f:convertDateTime>,它将特定数据值格式化为日期时间格式。
最后,我们有h:commandLink标签,它渲染一个 HTML 锚点标签,其行为类似于表单提交按钮。h:commandLink标签可以与后端 bean 关联,在我们的例子中是taskListViewController。JSF HTML 标签的某些组件,如h:dataTable和h:commandLink,包含在h:form标签中,以便正确处理。
小贴士
Bootstrap CSS (getbootstrap.com) 是一个非常流行的 CSS、组件和前端框架,用于开发响应式网站。它默认适用于移动项目,因为它基于一个灵活且流动的网格系统。CSS 和 JavaScript 可以轻松添加到 Web 应用程序中;Bootstrap 确实是许多项目的启动器。
应用程序服务器
在撰写本文时,有几个流行的应用程序服务器被认证为 Java EE 7 兼容:GlassFish、WildFly、Payara、Cosminexus 和 TMax Jeus 8。整个伞形规范参考实现是 GlassFish 4.1 (glassfish.java.net/)。在 2013 年,我专门为 GlassFish 撰写了姐妹书籍和源代码示例,即《Java EE 7 开发者手册》,因为它是当时唯一可用的服务器。GlassFish 基于开源,有一个公共问题跟踪器,许多关于 Java EE 主题的论坛,并且由于 Oracle 支持主机和源代码仓库,它可以直接使用。
为了成为 Java EE 7 应用程序服务器,供应商或开源提供者必须通过测试兼容性工具包(Test Compatibility Kit),这保证了符合性认证列表(www.oracle.com/technetwork/java/javaee/overview/compatibility-jsp-136984.html)。仅针对 Java EE 7 标准 API 编写的代码必须能够在符合性服务器上运行,否则“标准”这个词就失去了意义。Java 的基本原则:一次编写,到处运行,应该是可以实现的。问题在于当代码依赖于标准之外的供应商特定功能时。也值得指出的是,TCK 并非免费。事实上,我知道一个非常好的信息来源,它提到成本至少为 250 K 美元。因此,这个入门障碍超出了大多数开源项目或中小企业(SME)的能力范围,除非有天使投资者或 Kick-starter 基金的重大投资。
2014 年初,Oracle 宣布将停止对 GlassFish 服务器的商业支持。这一消息让 Java EE 社区对应用服务器的未来感到不安。Oracle 后来澄清说,GlassFish 5 有一个路线图,它仍然是 Java EE 8 的参考实现计划之一。数据库供应商和 Java 管理员建议升级到 Oracle WebLogic 以用于生产。2014 年,Oracle 发布了带有选定的 Java EE 7 兼容组件的 WebLogic 12.1.3。
WildFly 9 (wildfly.org/) 是 Red Hat 公司下一代的应用服务器。该服务器采用基于新类加载器基础设施的模块化架构,旨在避免第三方 JAR 与服务器内部基础设施之间的依赖冲突问题。WildFly 有两个关键优势:新的高性能 HTTP 服务器,称为 Undertow (undertow.io/),以及管理端口的减少。端口 8080 用于 Web 流量、Servlets、REST 和 WebSocket 终端,9990 用于服务器管理。使用 WildFly,可以通过事实上的 HTTP 端口 8080 调用 EJB 远程方法,这为企业管理应用增加了许多有趣的可能性。
WildFly 9 中的模块化方法似乎适合那些希望对其企业架构部署有严格控制的终端站点。WildFly 有一个名为 核心 的下载选项,允许开发者在应用服务器运行时配置他们所需的模块。WildFly 的最终优势是它是第一个与 Java SE 8 兼容的 Java EE 7 服务器(Lambdas 和默认接口)。只有 GlassFish 4.1 版本与 Java SE 8 兼容。
在 GlassFish 专业支持失败之后,另一家公司进入了这个领域。C2B2 咨询公司提供了一个名为 Payara Server (payara.co.uk) 的开源 GlassFish 版本,提供 24/7 的生产支持。
我应该快速提及其他一个正在服务器端 Java 社区中崭露头角的服务器,这也是未来值得关注的。Apache 软件基金会有一个名为 Tom EE (tomee.apache.org/) 的开源项目。Tom EE(发音为 Tommy)本质上是一个带有额外扩展的 Apache Tomcat 7,这些扩展已经配置好以支持 JSF、JPA JAX-RS、CDI 和 EJB。David Blevins,一位受欢迎的演讲者和 ASF 提交者,是 Tom EE 项目的创始人。在撰写本文时,Tom EE 仅针对 Java EE 6 Web Profile 进行认证;然而,有计划添加对 Java EE 7 的支持。企业利益相关者可以通过像 Tomi Tribe (www.tomitribe.com/) 这样的供应商获得 Tom EE 的商业和生产支持。
由于 GlassFish、WildFly 和 Payara 是当时唯一获得 Java EE 7 兼容性认证的应用服务器,因此我们将在这本书的剩余部分只关注它们。源代码示例与这两个服务器都兼容。在必要时,我们将指出差异并适当地解释功能。现在让我们继续我们的 Java EE 7 数字网络之旅。
摘要
在本章中,我们讨论了数字工人的角色,也就是你,作为工程师,以及你如何适应作为创意人员的新的市场营销角色。我们探讨了在 2015 年和 2016 年预期所需的技能和工具链集合。我们介绍了 Java 平台和 JVM 如何融入这幅画面。
成为数字 Java EE 7 工人不仅仅是开发服务器端 Java 代码;你被期望在基础层面上理解 JavaScript 编程。你们中的一些人可能已经具备基本的 JavaScript 知识,而其他人可能对客户端空间的编程了解得更多。尽管 JavaScript 存在许多缺陷和失误,但它是一种值得尊重的专业语言,我们介绍了一些你们预期应该了解的框架。虽然这本书并不教授 JavaScript,而是针对 Java EE 开发,但我建议你们复习一下关于模块模式和高级库应用方面的技能。
在本章中,我们探讨了 Java EE 7 架构和平台的一部分规范。最后,我们仔细审查了一个简单的 JavaServer Faces 示例的代码。特别是,我们检查了 Facelet 视图代码。我们注意到视图的大部分内容与标准 HTML 相似。
在接下来的章节中,我们将深入探讨 JSF,并构建一个简单的创建、检索、更新、删除(CRUD)示例。我们将以几种不同的方式生成这个示例。正如俗话所说,我们必须先爬行,然后才能走路,然后才能奔跑。我们的爬行已经结束,现在让我们开始走路。
练习
为了帮助教育领域的人士:学生、教师和讲师,我们在本书每一章的末尾都提供了问题。
-
拿一张纸;概述核心 Java EE 7 规范,包括 Servlets CDI、EJB、JPA、JMS、JAX-RS 和 JSF。在 1-10(1 为新手,10 为专家)的尺度上,问问自己你诚实地知道多少?
-
你上次查看 Java EE 是什么时候?如果你仍然认为企业开发就是 J2EE 这个术语,那么你绝对需要看看这本书,《Java EE 开发者手册》。记下你不太熟悉的规范,并计划学习它们。
-
通过将规范与您最近参与的一个 Web 应用程序相匹配来测试你对 Java EE 平台的了解。描述每个规范如何提供好处,包括生产力的提升。
-
现在转到另一边,反对 Java EE。社区中有一些声音支持标准化,而另一些则坚决反对。批评者说,标准化过程对于需要和创新的世界来说太慢了。你认为依赖 Java EE 平台可能存在哪些潜在的风险?考虑软件开发以外的领域,如教育、培训、招聘和更广泛的社区。Java 的理想状态是什么,你可以做你喜欢的事情,而不必承担责任?
-
你可能已经有一个喜欢的网站,你经常访问,也许每天都会访问。为它绘制或概述其基本(高级)信息架构。你的最爱网站可能拥有大量丰富的内容,并且存在了很长时间。你注意到了信息架构的哪些变化,基于你现在的了解?
-
你的 JavaScript 知识水平如何?在 1(初学者)到 10(专家)的评分尺度上,你如何评估你的技能?你的 JavaScript 与你的 Java 编程相比如何?
-
你知道你可以使用现代网络浏览器的开发者工具(Chrome 开发者工具
developer.chrome.com/devtools或 Christopher Pederick 的 Web 开发者工具chrispederick.com/work/web-developer/或类似工具)动态地检查 HTML、CSS 和 JavaScript 吗?你是否曾通过这些工具学习过调试 JavaScript?为什么不学习简单地给代码添加断点呢?使用检查器检查计算出的 CSS 怎么样? -
使用分布式版本控制系统 Git,从 GitHub(
github.com/peterpilgrim/digitaljavaee7)克隆本书的源代码,并检查本章给出的简单 JSF 示例周围的代码。下载并设置 GlassFish 4.1(glassfish.java.net/)或 WildFly 9(wildfly.org/)应用服务器,并运行第一个示例。 -
在网页设计中(使用商业应用程序 Adobe Photoshop 或 Firework 或 Xara Edit)你的图像编辑技能如何?你在工作中或在家里参与这项活动吗,还是将这项工作委托给另一个人,比如创意设计师?在这个领域拥有更好的知识是否会对你的更广泛的职业规划有所帮助?问问自己,这会让你成为一个更好的数字工作者吗?
-
实践敏捷软件开发数字团队的往往与利益相关者合作。如果他们很幸运,他们能与利益相关者直接接触。利益相关者是客户,是业务终端用户的代表,这些团队正是向他们交付软件的。您是否曾直接与利益相关者(们)进行过对话?这些讨论的结果如何?它们是如何进行的?您是否曾希望更加参与其中?您是否曾感到想要逃避?回顾过去,您在这些谈话中的努力如何才能做得更好?试着站在利益相关者的角度,理解他如何看待您。
第二章. JavaServer Faces 生命周期
| "世界上没有两个人是完全相同的,在音乐中也是如此,如果不是这样,那就不是音乐" | ||
|---|---|---|
| --比莉·假日 |
Java 在服务器端已经取得了长期的成功:自 2000 年以来。企业一直信任 JVM、Java 编程语言以及丰富的框架作为他们选择的企业软件平台。那么,我们继续信任 JVM 作为数字 Web 工程师是否正确?我认为这个问题的答案,以及因为你正在阅读这本书,答案是肯定的!
本章是对 JavaServer Faces(JSF)概念的全面概述。我们将从 JSF 的历史和目的开始,以及它如何与基本设计模式:模型-视图-控制器(MVC)相关联。我们将探讨 JSF 中的生命周期概念,这是将其与其他 Java Web 应用程序框架区分开来的关键概念之一。此外,我们将检查一些 JSF 代码,包括管理 Bean 的邪恶概念。我们还将介绍 JSF 应用程序如何在 POJO 和页面之间导航。为了使内容更加丰富,我们将探讨页面作者强大的表达式语言。当我们完成本章时,我们将建立起坚实的知识基础。
JSF 简介
JSF 是一个用于从组件模型构建 Web 用户界面的规范。它包含 MVC 和模板框架。JSF 是 Java EE 平台的标准库。Java 社区进程(JCP)控制规范,当前版本是 JSF 2.2,由 Java 规范请求(JSR)334 定义(www.jcp.org/en/jsr/detail?id=344)。
最初,JSF 背后的承诺是将快速用户界面开发带到服务器端 Java。当 JSF 首次构想时,这个说法是正确的;但当然,如果你不想写很多 JavaScript 代码和手工制作的样板代码来处理 HTTP 请求到 Java 调用以及返回页面的响应,这个说法仍然是有用的。自 2004 年 JSF 1.0 被构想以来,Web 技术和,特别是数字开发已经从网页上飞跃。当时,JavaScript 并没有被当作一种编程语言那样认真对待;没有响应式 Web 设计,对移动 Web 编程的需求也肯定较少。如今,看到“移动优先”或“默认为数字”这样的术语是很常见的。这意味着网站会考虑各种屏幕尺寸和设备,并认识到人们可以在智能手机或平板电脑上查看内容。有些人(你的目标客户)无法访问台式电脑或笔记本电脑。
小贴士
请参阅 Cameron Moll 的开创性——但现在已经有些过时的——关于移动网页设计的电子书(mobilewebbook.com/)。英国政府非常重视“数字优先”(www.gov.uk/service-manual/digital-by-default)这一术语,而美国数字服务则提出了“默认为开放”的广泛概念(playbook.cio.gov/)。
JSF 被构想为一种用户界面技术,它甚至可以让 Java 工程师以与 JavaFX(或 Swing)应用相同的方式构建前端。这个想法是让开发者(而不是设计师)使用自定义编辑器组装 HTML 页面。JSF 应用程序被设计为可主题化的。框架的意图是允许渲染工具产生不同形式的输出。一个渲染工具可能生成 PDF 输出,另一种类型可能生成 HTML 输出,还有一种类型会以 无线应用协议(WAP)的形式生成特定的移动内容(WAP 是在 2007 年苹果公司推出第一代 iPhone 之前受到广泛关注的技术)。技术已经取得了飞跃性的进步!
虽然对 JSF 作为产生严肃应用程序的 Web 技术有很多批评,但它得到了 Java EE 平台的支持。Facelets 是一个有用的模板框架,用于构建可共享的组件和网页的部分内容。JSF 具有生命周期模型,可以集成到 POJO 中,这意味着它与 Context 和依赖注入(CDI)豆无缝工作。此外,JSF 还与数字领域的变化保持同步。JSF 2.2 支持 HTML5 友好的标记。它支持 AJAX 事件,并允许事件排队。它允许为 HTML5 内容的所有元素使用 W3C 授权的 ID 属性。JSF 2.2 引入了 Faces Flow,它增加了引导用户通过一系列屏幕、工作流程和向导的能力。最重要的是,JSF 2.2(JSR 334)代表了持续支持 Java EE 平台标准组件化框架的承诺。
小贴士
Mojarra 2.2
为了使 JSF 成为 Java EE 平台的标准,它需要一个 JSR 和参考实现。对于 JSF,参考实现项目被称为 Mojarra。该软件是开源的,并由甲骨文公司(javaserverfaces.java.net/)支持。参考实现是 GlassFish 4 应用服务器的一部分。
JSF 1.0 和 2.0 历史简介
JSF 的概念最早在 2001 年左右被讨论。它是 Sun Microsystems 项目 Project Rave 的一部分,后来宣布为 JSR 127。尽管这项技术在当时基于动作请求的框架(如 Apache Struts)之上有所改进,但在 2003 年和 2004 年却遭到了冷淡的欢迎。2004 年发布了一个维护版本 1.1,但直到 2006 年,JSF 1.2 规范才成为 Java EE 5 的官方规范的一部分。
然而,到了这个时候,开发者的市场份额已经演变成了 AJAX 技术、部分应用程序以及 Ruby 和 Ruby on Rails 等非 JVM 软件等。JSF 1.2 被平台默认的模板技术 JavaServer Pages 所拖累。JSP 证明不适合 JSF,因为请求拦截和响应生成的生命周期本质上是不兼容的。寻找替代方案导致了 Facelets 的创建,它被设计成与 JSF 显式协同工作。
在 2009 年,Facelets 成为 JSF 2.0(JSR 314)的默认模板解决方案,JSF 2.0 也是 Java EE 6 的一部分。JSF 2.0 添加了验证和转换的注解。JSF 2.0 定义了标准的 AJAX 组件生命周期,并增加了对图形编辑器的改进。JSF 2.0 引入了一个用于网络内容的资源处理程序,包括图像、JavaScript 和 CSS 文件。
Key JSF 2.2 features
JSF 2.2 规范的主要特性如下:
-
它提供了对 HTML5 友好标记的支持,这对网页设计师和界面开发者来说是一个福音。
-
资源库合同是 JSF 中的一个新系统,通过捆绑 Facelet 视图、组件、样式表和其他资源(包括国际化)来构建可重用的主题。
-
它提供了与 Java EE 7 规范一致的新的 URI 定位器。Oracle 公司在 2010 年收购了 Sun Microsystems,因此,旧形式的 URI
java.sun.com/jsf/core被转换为xmlns.jcp.org/jsf/core,反映了 JCP 网络域的命名空间。 -
Faces Flows 允许将应用程序建模为页面和视图的定向图。使用 Faces Flows,我们可以从用户界面的角度构建工作流应用程序的基础。作为数字工程师,我们可以将应用程序的子集组装成更大的整体。这类工作流程非常适合 CDI 容器的对话范围。你将在第六章 JSF Flows and Finesse 中详细了解流程。
-
无状态视图允许开发者构建没有服务器端状态的组件。通常,JSF 组件会在服务器或客户端上保存用户界面组件的状态,但有时视图不需要这种额外的资源,因此,拥有无状态视图可以提供在服务器上扩展 Web 应用的可伸缩性。
-
它提供了从客户端窗口(标签、浏览器窗口、弹出对话框或模态对话框)正确处理浏览器内容的能力。
JSF 2.2 与 Faces 2.1 和 2.0 兼容。针对 Faces 2.0 或 2.1 构建的应用程序不需要更改即可在 Faces 2.2 中运行;然而,在相反方向使用特定的 2.2 功能在这些较旧的环境中无法运行。
JSF 基于以下 Java API 规范:
-
JavaServer Pages 2.2 和 JavaServer Pages 标签库 (JSTL) 1.2
-
Java Servlet 3.0
-
Java SE 7
-
Java EE 7
-
Java Beans 1.01
为什么选择 JSF 而不是其他替代品?
到目前为止,JSF 是唯一一个被认证为 JCP 标准的 Java Web 应用框架。当然,有替代品;实际上,可能有 100 多种不同的 Java Web 框架,其中大多数将是开源的。然而,它们将根据愿景、实现、代码库的年龄以及谁实际上维护它作为存储库而有所不同。如果你的应用程序所依赖的 Web 框架是用昨天的技术构建的,因为 Web 正在不断发展,这对业务来说是没有好处的。同样,Web 框架必须与时俱进,否则最终会变得无关紧要。企业相信 JSF 是一个有保证的标准,即这项技术将得到长期支持。
小贴士
事实上,MVC (JSR 371) 将成为 Java EE 8 的另一个标准 Web 应用框架。你将在第九章Java EE MVC 框架中学习 MVC。
对于一个应用架构师可能希望根据业务需求选择 JSF 以外的 Web 框架,这是完全可以理解的。Apache Struts 2、Spring MVC 和 Apache Wicket 是我顺便提到的几个。Apache Struts 2 和 Spring MVC 通常被认为是面向请求的框架。Apache Wicket 是一个面向组件的框架,并且是 JSF 的直接竞争对手。Apache Struts 是 2000 年代初最著名的 Web 应用框架之一,并且是第一个打破传统框架的。
Web 框架的世界并不局限于 Java。大多数开发者都会听说过 Ruby on Rails,这是一种非 JVM 技术。一些工程师会了解 Play 框架,它适用于 Java 和 Scala 开发者,然后还有基于 Groovy 语言的解决方案,如 Grails。
无论你选择哪个框架来构建 Web 应用,本质上都会决定你的开发者的 Java 前端架构。无论你做什么,我强烈建议不要发明自己的 Web 应用框架。开源的优势在于拥有来自数千家不同公司、项目和文化的开发者社区。
如果你选择 JSF,那么作为客户,你很可能希望维护你在 Java 平台上的投资。你的 JSF 企业应用的核心优势是丰富的组件,你依赖模型来添加默认的好处,例如更简单的验证、类型转换和 HTTP 请求参数到 Bean 属性的映射。许多经验丰富的 Java EE 工程师都将有在 JSF 框架中的经验,所以你将身处一群人中。
MVC 设计模式
MVC 设计描述了一组旨在将用户界面与应用逻辑(它们在语义上绑定)的关注点分离的设计模式。模型描述业务逻辑。视图表示展示——用户感知和与之交互的抽象表面。控制器表示处理模型和视图之间交互的组件。MVC 的原始想法源于 Trygve Reenskaug,他在 20 世纪 70 年代在 Smalltalk 编程语言中引入了这个概念。该模式随后在 Smalltalk-80 中实现并普及,之后被更广泛的软件工程社区采用。MVC 因其关于组件劳动分工和责任分离的想法而闻名。
我们称之为 MVC 模式,因为复数术语描述了一组与经典模式相关的相关衍生模式,作为组模式。
MVC 模式随后演变,产生了如分层模型-视图-控制器(HMVC)、模型-视图-表示器(MVP)、模型视图 ViewModel(MVVM)等变体,它们将 MVC 适应到不同的上下文中。
JSF 中的 MVC
MVC 如何映射到 JSF?以下是一些回答:
-
模型: 在 JSF 和 Java EE 中,模型是处理业务数据和逻辑的组件或组件集。模型可以是 CDI Bean、EJB 或与 Web 容器和 JSF 框架的生命周期兼容的其他组件。
-
控制器: 在经典设计模式中,控制器逻辑的大部分责任由框架承担。在 JSF 中,可以将控制器的开始视为 FacesServlet,它负责将传入的 HTTP 请求分发到正确的管理 Bean。
-
视图: 在 JSF 中,视图是包含 UI 组件及其相应 Bean 的渲染组。通常,视图是用页面描述语言描述的,对于 JSF 2.0 来说,就是 Facelets。JSF 的渲染套件将 UI 组件和 Bean 组合成整个页面。
以下图示从 JSF 框架的角度说明了 MVC 模式:

在 JSF 框架中,模型视图控制器模式被称为
Facelets
JSF 规范定义了一种视图声明语言(VDL)来渲染页面的输出。在 JSF 1.0 中,这是 JavaServer Pages;但在 JSF 2.0 中,VDL 默认改为 Facelets。Facelets 是 JSF 2.0 的默认视图处理器,定义为 XHTML 文件。
Facelets 可以在模板化场景中使用。一个 Facelets 文件可以作为组合引用主模板,视图可以提供看起来像模具提供给模板的内容。利用引用模板的 Facelet 被称为模板客户端。模板客户端中的占位符内容将覆盖主模板中的默认内容。这样,Facelets 可以重用以共享内容。模板客户端可能成为主模板,从而派生出视图的层次结构。
Facelets 还通过自定义标签提供重用。工程师可以通过 XHTML 文件和元数据编写自己的自定义标签。设计师和开发者将通过标签库描述文件提供内容。
Facelets 提供的最后一个模板化选项是复合组件组合。这种机制允许组合在其他 Facelet 视图中重用,使它们看起来像一等组件。然而,模板文件必须创建在一个特殊目录中,以便允许内部组合处理器成功。
请求处理生命周期
JSF 有一个基于 HTTP 协议的请求-响应处理生命周期。JSF 建立在 Java Servlet 规范之上,负责将请求用户代理(在大多数情况下是网络浏览器)转换为已知的端点。对于 JSF 来说,第一个端口是javax.faces.webapp.FacesServlet。这个 servlet 将简单地转发传入的请求到控制器,并且这个组件可以选择生成响应或将输出委托给内部 JSF 控制器实现。
在请求处理生命周期中,JSF 有三种情况。第一种是带有 Faces 请求调用 JSF 控制器,这最终生成一个 Faces 响应。
第二种请求是检索资源,如 CSS 或 JavaScript 文件、图片或其他媒体文件。然而,不需要执行逻辑的 Faces 资源请求,会导致 JSF 框架提供输出作为 Faces 资源响应。
最后一个是页面请求,用于检索与 JSF 无关的内容,这被称为非 Faces 请求,随后产生非 Faces 响应。向 JAX-RS 服务端点发出的 HTTP 请求就是一个非 Faces 请求和响应的例子。让我们看一下以下图示:

JSF 请求和响应处理
JSF 框架首先确定传入的请求是否为资源。如果是,则框架提供资源并发送字节、内容类型和数据到用户代理。
当传入的请求被处理为 Face 请求时,有趣的工作发生了;JSF 框架以线性工作流程处理此处理。这个过程被称为执行和渲染生命周期。
执行和渲染生命周期
以下图表显示了 JSF 生命周期以处理 Faces 请求:

JSF 框架内部的执行和渲染生命周期阶段
标准请求处理生命周期从 Faces 请求刺激到恢复视图阶段开始。JSF 维护一个javax.faces.context.FacesContext实例以处理生命周期。此对象实例包含与单个 Faces 请求相关联的所有信息。FacesContext 被传递到各个阶段。
恢复视图
恢复视图是生命周期的一个阶段,其中 JSF 框架确保组件树及其状态与视图最初在响应中生成时匹配。换句话说,JSF 必须在开始插入更改并应用从 Faces 请求中获取的表单属性值之前,准确重建视图。此阶段存在的原因是整体输入的状态可以在请求之间动态更改。以下是对技术深度的描述。
恢复视图根据算法确定请求是回发还是初始请求。JSF 中的每个视图都有一个唯一的标识符,即viewId,这通常在框架的实现内部存储在映射集合中。框架在关联的视图的javax.faces.application.ViewHandler实例上调用initView()方法,以便构建或检索视图以显示给用户代理。
如果视图已经存在,则请求是回发。JSF 将使用之前保存的状态恢复带有viewId的视图。状态可以存储在服务器或客户端。此行为是从应用程序的 Web XML 部署行为中配置的。
对于全新的视图,JSF 创建一个javax.faces.component.UIViewRoot类型的新实例,该实例最初为空,并设置其关联的属性,如区域设置和字符集。然后 JSF 以树形数据结构填充视图中的 UI 组件。
应用请求值
在组件树恢复后,JSF 将请求信息参数映射到组件属性。框架遍历树中的组件对象。每个组件从请求对象中检索数据,这些数据通常是请求参数,但也可以是 cookie、会话属性,甚至是头部参数。因此,新值被存储在 UI 组件本地。值从请求信息中提取,在这个阶段,值仍然是字符串。这个阶段被称为应用请求值阶段。
在这个阶段,JSF 将尝试在适当的位置对组件属性进行转换。如果转换或验证失败,则错误信息将被排队在 FacesContext 中。
应用请求值阶段在命令按钮或链接被点击时向内部 JSF 事件队列添加事件。JSF 存在某些特殊条件,在这些条件下,事件处理器允许打破处理流程的线性流程并跳转到最终阶段:渲染响应。
处理验证
处理验证阶段是提交的字符串值(这些值与组件一起存储)被转换为本地值的阶段。这些本地值可以是任何类型的 Java 对象。在这个阶段,与组件关联的验证器可以验证本地值的值。如果验证通过并且所有必需的验证器都成功调用,则 JSF 生命周期继续到下一个阶段。如果验证失败或前一个生命周期阶段(应用请求值阶段)中存在转换错误,则 JSF 框架直接跳转到渲染响应阶段。然后,网络用户有机会在 HTML 输入表单中输入正确数据。
作为 JSF 开发者,将验证器附加到具有输入的 UI 组件是你的责任,这些输入非常重要。
更新模型值
在转换和验证阶段之后,JSF 进入更新模型值阶段。在这个时候,本地值被认为是安全的,以便更新模型。记住,在 JSF 与 MVC 术语中,模型很可能是你的管理后端 bean、CDI bean 或 EJB 或聚合对象。JSF 更新由组件引用的 bean。
调用应用
在生命周期中,我们到达了模型已更新,转换和验证已应用的阶段。JSF 称这个阶段为调用应用阶段,在这里,最终调用业务逻辑。JSF 调用由命令按钮或链接组件的动作方法命名的函数。调用应用阶段是用户提交 HTML 表单或调用导航锚链接的结果,因此 JSF 框架执行后端 bean 的相应方法。
该方法可以选择返回一个简单的结果字符串。自从 JSF 2.0 以来,方法被允许返回一个简单的字符串,该字符串通过名称引用视图。或者,方法可以使用 FacesContext 实例程序化构建自己的响应,或者返回传递给导航处理器的导航视图 ID,该处理器随后查找下一页。
渲染响应
生命周期中的最后一个阶段是渲染响应阶段。这一阶段需要编码 Faces 响应,JSF 框架将此输出发送到请求的用户代理,这通常是网络浏览器。一旦数据通过网络发送到客户端,请求和响应的生命周期就结束了。在下一个请求上,一个新的生命周期开始。
事件处理
在某些阶段之间,你可能已经注意到了进程事件阶段。JSF 允许监听器注册到框架中,以便观察事件。这些被称为阶段监听器。它们之所以特殊,是因为它们可以在行为中保持活跃,导致生命周期跳过,或者它们可以保持被动,仅监控对应用程序有趣的用户界面的某些方面。这些小型扩展点对于应用程序构建者非常有用且强大,因此是 JSF 与其他 Web 框架之间的重要区别。
一个基本的 JSF 示例
我们已经对 JSF 框架的理论进行了足够的介绍。我认为是时候让我的读者看看一些代码了。第一段代码是用于在网站上显示基本网页的 XHTML 文件。源代码可在作者公共 GitHub 账户的书籍网站上找到,网址为 github.com/peter_pilgrim/digital_javaee7。
这里是初始 Facelets 视图的 XHTML 源代码,文件名为 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>Digital Java EE 7 - Sample JSF</title>
</h:head>
<h:body>
This is the simplest JSF example possible.
<h:form>
<h:commandButton action="#{basicFlow.serveResponse}" value="Invoke Action" />
</h:form>
<h:link outcome="composition1.xhtml">
Composition 1
</h:link>
</h:body>
</html>
值得提醒的是,此文件不是 HTML5 文档;尽管 JSF 2.2 可以处理文档语法,但我们必须先学会走路,然后才能奔跑。XHTML 是一种使用 XML 架构命名空间添加额外标签的 HTML 格式。因此,存在针对 HTML、UI 和 F 的 JSF 特定命名空间。请参阅以下内容,了解这些命名空间的描述。
<h:head>、<h:body> 和 <h:form> 自定义标签类似于大家在网上开发中都知道的标准 HTML 元素标签。这是因为它们被设计成故意反映这一目的。实际上,这些是添加功能并在最后渲染等效 HTML 元素输出的 JSF 自定义标签。
你可能想知道 <h:link> 元素是什么。这仅仅是 JSF 渲染 HTML 锚点标签的方式。结果标签属性直接引用另一个 XHTML,而在 JSF 2.0 之后,开发人员被允许在代码中这样写。
<h:commandButton> 标签是 JSF 表单按钮的一个示例,它最终渲染一个 HTML 提交元素标签。此标签接受一个 action 属性,该属性引用一个特殊字符串。该字符串是表达式语言的示例;它引用了一个实例的方法名。
这是 JSF 管理实例 BasicFlow 的代码:
package uk.co.xenonique.digital.javaee.jsfcomps;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named
@RequestScoped
public class BasicFlow {
public String serveResponse() {
return "endState.xhtml";
}
}
BasicFlow 是一个具有请求作用域生命周期的 CDI 实例,由 @javax.enterprise.context.RequestScoped 注解声明。该实例在 servlet 请求生命周期开始时由 CDI 框架创建,一旦 servlet 响应完成,则完成并留给垃圾回收。
在 JSF 2.2 中,我们将使用 @javax.inject.Named 注解来指定对 JSF 框架可用的实例。我们可以明确写出注解为 @Named("basicFlow"),但默认情况下是简单类名的驼峰标识符。我们建议数字开发者不要使用旧的 @javax.faces.bean.ManagedBean 注解,因为它现在在未来的 JSF 规范中已被标记为弃用。
小贴士
确保您的 POJO 实际上是 CDI 实例。如果您为 JSF 使用了错误的导入,可能会出现混淆。在部署时,您将无法在 #{basicFlow.serveResponse} 这样的表达式注入或找到后端实例。请检查您是否导入了 javax.enterprise.context.RequestScoped 而不是过时的 javax.faces.bean.RequestScoped 注解。
#{basicFlow.serveResponse} 字符串是 表达式语言(EL)的一个示例,这是一种页面内容与后端实例通信的机制,同时保持关注点的分离。第一个 BasicFlow 元素引用后端实例,第二个 serverResponse 元素引用 serveResponse() 方法。因此,这是一个引用后端实例方法的 EL 表达式。我们将在本章后面学习更多关于表达式语言的内容。
您可以看到响应是一个简单的字符串,这是下一个 VDL 文件:endstate.xhtml。严格来说,可以省略后缀,JSF 框架将确定正确的视图。
endState.xhtml Facelet 视图文件如下所示:
<html
>
<h:head>
<title>Digital Java EE 7 - End State</title>
</h:head>
<h:body>
<p>
This is the <strong>end state</strong>.
</p>
<h:link outcome="index.xhtml">
Go back to the start
</h:link>
</h:body>
</html>
这是一个 JSF 视图,允许用户通过 <h:link> 元素返回起始视图。
网络部署描述符
为了充分利用 JSF 框架,我们建议配置网络应用程序部署描述符。此文件是一个特殊的 XML 文档,声明性地描述了入口 servlet 端点、servlet 映射和其他环境资源。XML 文件的代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1" metadata-complete="false">
<display-name>
jsf-compositions-1.0-SNAPSHOT
</display-name>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>
javax.faces.webapp.FacesServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<welcome-file-list>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
前面的文件具有 WEB-INF/web.xml 路径。为了激活 JSF 框架,部署描述符声明了具有完全限定类名的 servlet;javax.faces.webapp.FacesServlet。请注意,servlet 被映射以服务 *.xhtml 文件。
我们将使用上下文参数 javax.faces.PROJECT_STAGE 和一个适当的值来定义当前项目的活动阶段。在前面的例子中,阶段设置为 Development,但在应用程序上线后,我们可能希望将值切换到 Production。切换到 Production 可以提高性能并禁用一些调试输出。
你可以在本书的源代码中找到部署描述符,它是项目 ch02/jsf-compositions 的一部分。一旦将项目添加到 IDE 中——比如说,IntelliJ、Eclipse 或 NetBeans——你就可以在 URL http://localhost:8080/jsf-compositions-1.0-SNAPSHOT/ 上查看应用程序服务器的输出。
JSF XML 命名空间
下面是一个描述常见 JSF 及相关命名空间的表格:
| 命名空间 | 描述 |
|---|---|
h |
xmlns.jcp.org/jsf/html 这定义了标准的 JSF 标签库,用于 HTML 渲染器和组件,如 h:link、h:commandButton 等。 |
f |
xmlns.jcp.org/jsf/core 这定义了标准的 JSF 标签库,用于独立于任何渲染套件的核心理念。这个库包括处理验证和转换的标签。 |
ui |
xmlns.jcp.org/jsf/facelet 这定义了标准的 JSF 标签库,用于模板化支持,包括视图的组合。 |
cc |
xmlns.jcp.org/jsf/composite 这定义了用于构建组合组件的标准 tag 库。 |
jsf |
xmlns.jcp.org/jsf 这定义了支持 HTML5 友好输出的标签。 |
p |
xmlns.jcp.org/jsf/passthrough 这定义了支持通过 tag 属性输出 HTML5 友好输出的标签。 |
c |
xmlns.jcp.org/jsp/jstl/core 这定义了用于 JSP 核心行为的 JSTL 1.2 标签库。这些标签包括 <c:forEach>, <c:if>, <c:choose>, 和 <c:catch>。 |
fn |
xmlns.jcp.org/jsp/jstl/ficmtion 这定义了用于 JSP 函数的 JSTL 1.2 标签库。这些标签包括 <fn:upperCase>, <fn:length>, 和 <fn:contains>。 |
必须将一个缩写名称,如 fn,添加到根 XML 文档元素中,在大多数情况下这是一个 HTML 元素。
一个组合示例
在我们结束这一章之前,让我们深入一些代码来演示 JSF 组合。我们将从一个简单的 JSF 模板开始,该模板在两个区域中布局一个网页:一个顶部区域和主要区域。
这个 template-top.xhtml 文件是执行以下操作的 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>Digital Java EE 7 - Sample JSF</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="XeNoNiQUE"/>
<meta name="author" content="Peter Pilgrim"/>
<h:outputStylesheet library="styles" name="main.css" rel="stylesheet"/>
<h:outputStylesheet name="/styles/top.css" rel="stylesheet"/>
</h:head>
<h:body>
<div id="content">
<div id="top" class="topContent">
<ui:insert name="top">
Reserved for Top Content
</ui:insert>
</div>
<div id="main" class="mainContent">
<ui:insert name="content">
Reserved for Main Content
</ui:insert>
</div>
</div>
</h:body>
</html>
上述代码是模板母版。到目前为止一切顺利。这类似于一个标准的网页,带有 HTML 元素,我们可以看到页面使用了嵌套的 DIV 元素来结构化内容。我将把你的注意力引向<h:outputStylesheet>标签,它表示我们应该包含几个层叠样式表作为资源。
ui:insert标签是表示模板中将被客户端模板中的占位符替换的区域的组合 JSF 标签。插入占位符必须有一个名称,在这个例子中我们有两个,即 top 和 content。请注意,ui:insert标签被插入到 HTML div元素的 body 内容中。
这里是客户端模板的代码,作为composition1.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
>
<ui:composition template="/template-top.xhtml">
<ui:define name="top">
<h1>layout composition 1</h1>
</ui:define>
<ui:define name="content">
The is the main content
<h:form>
<h:commandButton action="#{basicFlow.serveResponse}" value="Invoke Action" />
</h:form>
</ui:define>
</ui:composition>
</html>
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
这个文件中的关键注释是<ui:composition> JSF 自定义标签,它引用了正在使用的母版模板。模板属性指向文件的路径。
两个<ui:define>标签定义了具有替换母版模板中默认内容的名称占位符。在这个例子中,占位符是 top 和 content。
这里是这个过程的截图。第一张截图是初始的 Facelets 视图,index.xhtml:

第二个是第二个 Facelets 视图,endState.xhtml:

JSF 服务资源
JSF 期望我们的 Web 资源默认放置在resources文件夹中。快速查看以下文件目录将有助于你理解:
jsf-composition
jsf-composition/src/main/webapp
jsf-composition/src/main/webapp/resources
jsf-composition/src/main/webapp/resources/images
jsf-composition/src/main/webapp/resources/javascripts
jsf-composition/src/main/webapp/resources/styles
jsf-composition/src/main/webapp/WEB-INF
jsf-composition/src/main/webapp/WEB-INF/classes
jsf-composition/src/main/webapp/WEB-INF/lib
在这个 Gradle 项目的简化视图中,我们可以看到放置在resource文件夹下的文件夹、图像、JavaScript 和 CSS 文件。让我们再次提醒自己 JSF 视图代码,如下所示:
<h:outputStylesheet library="styles" name="main.css" rel="stylesheet"/>
<h:outputStylesheet name="/styles/top.css" rel="stylesheet"/>
这些标签本质上指的是两个文件:resources/style/top.css 和 resources/style/main.css。
为了使这些标签正常工作,资源必须放置在resources文件夹下,或者它可以放置在随 Web 应用程序部署的 Web 应用程序 JAR 文件的META-INF/resources文件夹中。规范列出了以下两个选项:
<ROOT>/resources/<RES-ID>
否则,你可以使用这个:
<ROOT>/WEB-INF/lib/<DEPENDANT-JAR>/META-INF/resources/<RES-ID>
这里,<ROOT>是项目 Web 根目录,<DEPENDANT-JAR>是第三方依赖 JAR,<RES-ID>是资源标识符。
<RES-ID>可以进一步细分为正式部分,如下所示:
<RES-ID> ::=
[ <LOCALE-PREFIX> / ] +
[ <LIBRARY-NAME> / ] [ <LIBRARY-VERSION> / ] +
<RESOURCE-NAME> [ / <RESOURCE-VERSION> ]
[]内的术语部分是可选的,除了资源名称。因此,可以有一个完全国际化、版本化和模块化的库中的资源标识符。也许你的项目可能会使用以下资源:
/en_us/sportsBulletin/v1_2_3/top_masters_golfer/2015_may.png
如果你想逆势而行,更改 JSF 2.2 中资源的默认位置,该怎么办?可以在web.xml部署描述符文件中配置一个替代文件夹。你可以设置一个上下文参数变量:javax.faces.WEBAPP_RESOURCES_DIRECTORY。
这里是定义资源文件夹为资产的描述符摘录:
<context-param>
<param-name>
javax.faces.WEBAPP_RESOURCES_DIRECTORY</param-name>
<param-value>assets</param-value>
</context-param>
我们将涵盖 JSF 自定义标签的全面内容以及如何在第三章构建 JSF 表单和第四章JSF 验证和 AJAX中提交 HTML 表单。
表达式语言
<h:inputText id="firstName" value="${employee.firstName}"/>
这将作为一个只读值工作。然而,由于表达式在任何生命周期状态都没有被评估,因此无法使用 JSF 将值应用到 employee bean 上。
如果我们将表达式从立即形式更改为延迟形式,那么我们将看到以下行为:
<h:inputText id="firstName" value="#{employee.firstName}"/>
随着这一变化,在生命周期的渲染响应阶段,EL(表达式语言)立即被评估。JSF 实现执行评估并从名为 employee 的 bean 中检索firstName属性的值。
当表单作为 Faces 请求发送回服务器时,顺便说一下,这也被称为postback事件,JSF 实现有机会在延迟时间检索值。在这些后续的生命周期状态——应用请求值、处理验证和更新模型中,值表达式被评估,并且从 Faces 请求中的值被注入到目标 bean 属性中。
值表达式
值表达式是返回单个结果的表达式。值从管理 Java 实例集合的实现中的对象图中检索。对于 Java EE,这可以是 JSF 或 JSP 提供者,对于应用程序服务器,它是一个上下文和依赖注入提供者。CDI 内部维护一组@javax.inject.Named bean 的集合。(请等待对命名 bean 的解释,或者直接查看第三章构建 JSF 表单。)特别是,JSF 传统上保留有注解了@javax.faces.bean.ManagedBean的托管 bean 的记录。
JSP 将在页面作用域、请求作用域、会话作用域以及最后在 servlet 容器的应用程序作用域中搜索命名对象。在幕后,有一个抽象类 javax.el.ELResolver 的子类,它负责评估。这个类有有用的方法,例如 getValue()、setValue()、isReadOnly() 和 invoke(),开发者可以使用这些方法以编程方式将评估添加到自己的应用程序中。
在任何情况下,值表达式的首要目标是具有识别名称的对象实例。这被称为初始项。之后,评估逻辑可以通过使用点符号(.)通过命名属性遍历对象图。评估将继续通过表达式中的后续项。让我们暂时坚持 JSF,并考虑 #{employee.firstName} 表达式将评估在作用域中延迟搜索名为 employee 的对象。然后,EL 解析器将在名为 firstName 的 bean 中查找属性,这反过来将调用 getFirstName() 方法。任务将完成,EL 解析器返回属性的值。
EL 也可以与 Java 集合一起使用。特别是,java.util.Map 集合被特别处理。标准的 EL 假设集合有一个字符串类型的键,我们可以将其视为 Map<String,Object>。Map 中的条目可以使用点符号或方括号符号 [] 访问。
下面的值表达式表将使更复杂的表达式方案更加清晰:
| 表达式 | 含义 |
|---|---|
Employee |
这将找到与名称 employee 关联的初始项 |
employee.firstName |
这解析了命名实例并调用 getFirstName() |
employee.department.name |
这解析了对象,调用 getFirstName(),检索下一个对象,并在该对象上调用 getName() |
employee["firstName"] |
这与点符号 employee.firstName 等价 |
employee['firstName'] |
这与点符号 employee.firstName 等价 |
capitalCities['Brazil'] |
这将找到名称实例,并且假设 capitalCities 是 java.util.Map 类型,通过键 Brazil 获取值 |
capitalCities["Brazil"] |
这与前面的映射表达式等价 |
方括号符号 [] 在包含破折号和/或点字符的字符串中非常有用。这种符号在您想从资源包中提取消息以进行国际化时很有帮助。您可以编写一个值表达式,如下所示:appMessages["registeredTraveller.applicant.firstName.required"]。
方括号符号允许我们编写特殊表达式。我们可以编写以下值表达式:
${employee['class'].simpleName}
这翻译成以下等效的 Java 代码:
employee.getClass().getSimpleName()
映射表达式
EL 使用方括号符号 [] 无缝地处理 Map 对象。如果表达式评估为一个引用,该引用访问或读取右侧 Map 键(一个 rvalue)关联的值,那么 EL 解析器将其转换为 Map.get("key") 调用。以下为读取值的表达式:
${capitalCitiesMap['France']}
// translates to capitalCitiesMap.get("France")
#{capitialCitesMap['France']} // ditto, but deferred
如果表达式绑定到左侧(一个 lvalue),那么 EL 解析器将其转换为 Map.put("key", newValue)。
列表表达式
EL 可以使用方括号符号从索引数组中检索对象。它的工作方式与 Map 表达式完全相同,只是键必须评估为一个字面量整数。在 EL 中,数组索引数从零开始,这是预期的。
因此,以下值表达式在 departmentList 是 java.util.List 类型且 departmentArray 是原始数组时是有效的:
${departmentList[0].name}
${departmentArray[0].name}
这些是等效的伪 Java 语句:
List<Department> department = resolve(...)
departmentList.get(0).getName()
Department departmentArray[] = resolve(...)
departmentArray[0].getName()
解析初始项
EL 依赖于从 servlet 容器、JSF 管理 Bean 列表和 CDI 作用域中查找初始项的能力。本质上,你可以给 JSF Bean 任何你想要的名字,但你应该避免预定义对象。初始项是表达式的一部分。
在 servlet 容器中,你可以引用几个预定义对象。例如,requestScope 是页面上的所有请求作用域属性的 Map 集合。请求也是 EL 中的一个预定义对象,它代表传递给 JSF 视图的 javax.servlet.http.HttpServletRequest 实例。我们可以使用它在一个 lvalue 表达式中检索 Web 应用程序上下文路径,如下所示:
<link href="#{request.contextPath}/resources/styles/main.css" rel="stylesheet"/>
上述代码对于确保在 JSF 应用程序中找到资源非常有用。它用于创建可靠的相对 URL。我们将在 第四章 中进一步解释,JSF 验证和 AJAX。
初始项的解析从检查表达式中的初始项是否为预定义对象开始。如果是预定义对象,则解析器继续此对象。如果不是,则 JSF 实现将按照以下顺序在 servlet 容器作用域中搜索对象名称:requestScope、sessionScope 或 applicationScope。
如果按名称找不到对象,JSF 2.2 框架将委托给 ELResolver,该解析器将搜索实例的等效 CDI 作用域,然后查看已注册或注解的管理 Bean。
以下表格列出了表达式语言中的预定义对象实例:
| 预定义名称 | 描述 |
|---|---|
applicationScope |
这是一个包含应用程序作用域属性(javax.servlet.ServletContext.getAttributes())的 Map 集合 |
application |
这指的是 ServletContext 实例 |
cookie |
这是一个包含 cookie 名称和值的 Map 集合 |
facesContext |
这是页面的 javax.faces.context.FacesContext 实例及其生命周期 |
header |
这是一个 Map 集合,包含 HTTP 标头参数,只产生多个值中的第一个元素 |
headerValues |
这是一个 Map 集合,包含 HTTP 标头参数,并产生一个 String[] 值数组 |
initParam |
这是 Web 应用程序初始化参数的 Map 集合 |
param |
这是一个 Map 集合,包含 HTTP 请求参数,只包含任何值数组中的第一个元素 |
paramValue |
这是一个 Map 集合,包含 HTTP 请求参数,并产生一个 String[] 值数组 |
requestScope |
这是一个 Map 集合,包含请求作用域属性(HttpServletRequest.getAttributes()) |
request |
这指的是 HttpServletRequest 实例 |
sessionScope |
这是一个 Map 集合,包含会话作用域属性(HttpSession.getAttributes()) |
session |
这指的是 HttpSession 实例 |
View |
这是页面的 javax.faces.component.UIViewRoot 实例 |
让我们继续到方法表达式。
方法表达式
EL 还允许将方法绑定关联到对象实例上的方法。这种类型的引用称为方法绑定表达式。JSF 框架允许方法表达式引用动作方法、验证器、转换器和阶段监听器。方法表达式在命名对象实例上调用方法,并返回结果(如果有)。
一个方法表达式的良好例子是在一个托管 Bean 上的动作处理器,这在本章的基本 JSF 示例中你已经见识过了。
<h:commandButton action="#{basicFlow.serveResponse}" value="Invoke Action" />
#{basicFlow.serverResponse} 表达式是一个方法绑定,它指向控制器、名为 BasicFlow 的 CDI 实例以及 serveResponse() 方法。
参数化方法表达式
EL 还支持带有参数的方法调用。参数可以是字面常量,也可以是页面作用域中术语的名称。这提供了一种非常强大的方式来构建使用列表集合和其他复杂数据结构的应用程序。
下面是一个使用方法参数代码的表达式示例:
<h:inputText action="#{complexFlow.process('SALE',productLine)}" value="Purchase Products/>
process() 方法在通过 complexFlow 初始术语解析的对象实例上调用。第一个参数是一个字面字符串。第二个参数是 subterm,productLine 的值,我们假设它对 EL 解析器是可用的。
由于这是一个无参数调用,因此也可以通过定义来获取集合的大小。此表达式看起来像 #{genericSearchResult.size()},假设初始术语引用的是 java.util.Collection 或 java.util.Map 类型。
算术表达式
我们可以在表达式中使用算术运算符进行计算。表达式还可以包含关系和逻辑运算符。
在 EL 中,以下是一些保留运算符:
-
算术操作符:
+-*/div%mod -
关系操作符:
==或eq,!=或ne,<或lt,>或gt,<=或le,>=或ge -
逻辑操作符:
&&和,||或,!not -
空操作符:空
这里是一些这些算术表达式在应用中的示例:
<p> The expression \#{1+5*2-3/4} evaluates to: #{1+5*2-3/4} </p>
<p> The expression \#{(2014 div 4) mod 3} evaluates to: #{(2014 div 4) mod 3} </p>
<p> The expression \#{2018 lt 2022} evaluates to: #{2018 lt 2022} </p>
<p> The expression \#{0.75 == 3/4} evaluates to: #{0.75 == 3/4} </p>
注意到使用了转义字符——反斜杠(\),它可以防止 JSF 视图解释表达式。我们也可以在页面上直接渲染表达式,而不需要 <h:outputText/> 自定义标签。这对页面作者来说是个不错的待遇。
小贴士
保留 MVC 模型
将业务逻辑放在控制器 bean 中,而不是用复杂条件填充页面会更好。
页面导航
在 JSF 2 之后,在控制器中提供导航变得非常容易。在早期的 JSF 示例中的 BasicFlow 控制器中,我们依赖于隐式页面导航。开发者可以通过简单地返回一个字符串来指定要渲染的下一页。
这里再次展示了控制器类:
@Named @RequestScoped
public class BasicFlow {
public String serveResponse() {
return "endState.xhtml";
}
}
在 JSF 1 中,页面导航在 Faces 配置 XML 文件中明确确定:/WEB-INF/faces-config.xml,这使得开发变得更难,因为强制认知间接性。faces-config.xml 的目的是定义 JSF 网络应用程序的配置。开发者可以定义导航规则,注入 bean 属性,定义属性文件,并声明资源包和区域设置。他们可以注册转换器、验证器和渲染组件。
显式页面导航对于定义的信息架构路径很有用。编写页面导航在团队中更容易分享。为业务利益相关者制作原型可以非常快。然而,如果你的控制器和渲染页面直接映射为一对一关系,显式导航可能就是多余的。
导航规则
JSF 还支持在 Faces 配置 XML 文件中显式导航规则。我应该提醒你,这是 JSF 1.x 中的旧式方法,用于明确描述页面之间的导航。在 JSF 2.x 中,导航规则不再需要,如果你想更好地描述页面导航,记得学习关于 Faces Flows(见第六章 JSF Flows and Finesse)。然而,在你的专业工作中,你很可能遇到旧的 JSF 应用程序,因此,你需要学习 JSF 导航规则是如何设计的。
考虑到这一点,以下是导航如何显式工作的示例。假设我们有一个简单的页面,我们可以从一组页面中选择水果和蔬菜的集合。我们还可以选择取消选择。
这里是这些规则在标准 Faces 配置文件 faces-config.xml 中的表示。这个文件通常位于 Maven 和 Gradle 项目的 src/main/webapp/WEB-INF 目录下:
<?xml version="1.0" encoding="UTF-8"?>
<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">
<navigaton-rule>
<from-view-id>/order-start.xhtml</from-view-id>
<navigation-case>
<from-outcome>cancel</from-outcome>
<to-view-id>/cancel.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>fruit</from-outcome>
<to-view-id>/fruit.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>vegetable</from-outcome>
<to-view-id>/vegetable.xhtml</to-view-id>
</navigation-case>
</navigation-case>
</faces-config>
显式导航由 JSF 框架应用的一组规则确定。开发者将在 <navigation-rule> 标签中编写一系列复合元素。规则的上下文由 <from-view-id> 元素确定,它引用一个特定的视图页面,/order-start.xhtml,或者它可以是应用于多个导航情况的通配符规则(星号 *)。每个导航规则都有一个 <navigation-case> 元素的集合。每个情况都需要一个 <from-outcome> 和 <to-view-id> 元素。结果标识了由控制器方法返回的文本字符串,视图 ID 是目标视图。因此,在取消情况下,由取消标识的结果字符串将导航到 /cancel.xhtml 视图。
将结果映射到视图页面的间接映射的优势是显而易见的。控制器中的结果代码保持不变,但目标视图可以更改。
我们可以编写一个处理这些导航规则的 JSF 控制器。这是 ProductTypeController 类的一个摘录:
@Named @ViewScoped
public class ProductTypeController {
public String productType;
/* ... getter and setter omitted */
public String cancel() { return "cancel";}
public String navigate() {
if ("fruit".equalsIgnoreCase(productType)) {
return "fruit";
}
else {
return "vegetable";
}
}
}
cancel() 方法简单地返回取消结果,JSF 将其映射到 /cancel.xhtml 页面,因为导航情况与结果匹配。navigate() 方法根据 productType 属性设置结果。在 fruit 和 vegetable 方法中只能有两个结果,导航情况确保 fruit.xhtml 和 vegetable.xhtml 页面分别渲染。
通配符
导航规则也可以有通配符视图。通配符导航规则出现在 <from-view-id> 元素中的 URI 路径前缀为星号 (*) 的情况下。
假设我们有一个网站,其中受保护的页面除非用户以注册用户登录,否则不应显示。我们可以编写一个适用于受保护区域下所有页面的共享导航规则。让我们说我们想要保护任何位于 URI /secured 下的 Web 页面:
<faces-config ...>
<navigaton-rule>
<from-view-id>/secured/*</from-view-id>
<navigation-case>
<from-outcome>login</from-outcome>
<to-view-id>/login.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>register</from-outcome>
<to-view-id>/register.xhtml</to-view-id>
</navigation-case>
</navigation-case>
... more rules ...
</faces-config>
通配符 from-view-id /secured/* 识别所有以 /secured/ 前缀开始的页面。在 URI 路径中,你只能有一个通配符。
在结果中使用通配符引发优先级问题。何时通配符 视图 ID 覆盖直接结果?以下是设置源页面视图的 Faces 配置 XML 中的导航情况摘录:
<from-view-id>stocks.xhtml</from-view-id>
// Has higher precedure than
<from-view-id>*</from-view-id>
直接 视图 ID 总是比等效的通配符视图有更高的优先级。JSF 选择直接视图的导航情况,stocks.xhtml,而不是通配符视图,如下所示:
<from-view-id>/secured/portfolio/*</from-view-id>
// Has higher precedure than
<from-view-id>/secured/*</from-view-id>
如果存在多个通配符视图在竞争匹配,那么将选择最长的匹配。JSF 在最长匹配的视图中选择导航情况,如图所示 /secured/portfolio/*。
条件导航
JSF 显式页面导航也支持在 Faces 配置文件中实现条件导航的概念。这允许开发者根据应用程序的动态状态声明式地设置导航规则。条件导航是通过使用<if>元素标签实现的,如下一个示例所示:
<faces-config>
<navigaton-rule>
<from-view-id>/shopping-cart.xhtml</from-view-id>
<navigation-case>
<from-outcome>nextPage</from-outcome>
<if>#{user.registered}</if>
<to-view-id>/existing-customer.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>nextPage</from-outcome>
<if>#{!user.registered}</if>
<to-view-id>/new-customer.xhtml</to-view-id>
</navigation-case>
</navigation-case>
</faces-config>
<if>标签的正文内容是一个延迟值表达式,它在 JSF 生命周期中评估,并应返回一个布尔值。在代码中,#{user.registered}表达式被评估为当前登录用户配置文件 bean 及其名为 registered 的属性。#{!user.registered}表达式评估否定——注意运算符使用了感叹号。
静态导航
为了完成关于页面导航的数字开发者故事,现在是时候看看 JSF 的静态导航了。静态导航允许我们从一个 JSF 页面导航到另一个页面,而不需要调用管理 bean 控制器。这对于不需要服务器端 Java 代码或没有 HTML 输入元素的页面视图非常有用。静态导航是通过 Facelets 视图上的标记组合和显式导航规则实现的。
在早期的Basic JSF示例中,我们有一个带有<h:link>的页面视图。让我们将其更改为<h:commandButton>:
<h:form>
...
<h:commandButton action="your-next-view">
Composition 1
</h:commandButton>
</h:form>
动作属性指定要导航的结果名称。我们将用<h:commandButton> JSF 标签替换旧元素。动作属性指定值表达式。JSF 将在几个上下文中查找初始项,但它也会搜索 Faces 配置以匹配导航规则。为了使此遍历工作,我们还需要在 Faces 配置中有一个导航规则:
<navigation-case>
<from-outcome>your-next-view</from-outcome>
<to-view-id>/shopping-cart.xhtml</to-view-id>
</navigation-case>
导航规则与 Facelets 视图中的your-next-view结果匹配,因此 JSF 可以导航到目标页面。
我认为关于页面导航的话题我们就到这里吧。我们将继续我们的开发者数字之旅,在第四章中继续页面导航,JSF 验证和 AJAX。
摘要
本章在 JSF 的世界里是一次稳健的冒险。你应该能够理解框架是如何构建的理论。我们涵盖了 JSF 的关键特性,如 HTML5 友好的标记和模板引擎。JSF 是 Java EE 平台的一部分,可在许多应用程序服务器和 servlet 容器上使用。我们学习了 JSF 框架如何与模型-视图-控制器设计模式相关联。
你应该能够理解 JSF 在执行和渲染生命周期中的请求和响应处理生命周期以及阶段变化状态模型。
在中间章节,我们检查了 JSF 基本页面、自定义标签库、Facelets 视图和简单的后端 bean。我们还观察了包含主模板和客户端模板的复合布局。
我们还详细介绍了强大的 EL 框架,它是 Java EE 7 和 JSF 2.2 的一部分。EL 是服务器端 Java 应用程序的一个重要设施,特别是如果它们针对 Faces 构建。为了完成这次旅程,我们探讨了隐式和显式页面导航。
我们现在有足够的知识来构成 JSF 基础。在下一章中,我们将通过 HTML 表单增强我们的 JSF 知识,并开始验证。在随后的章节中,我们肯定会放弃 XHTML 文档并添加 HTML5,以便我们可以开发更现代的网站。
练习
这里是本章的问题:
-
在计算机科学的哪个其他领域可以找到模型-视图-控制器设计模式?
-
你认为为什么热衷于计算机科学的科学家和建筑师想要将业务逻辑与展示视图代码分离?
-
考虑一种情况,你被市政政府委托负责一个地方区域。
-
你被要求编写一个用于选民注册的选举名单网络应用程序,以取代传统的纸质记录。而不是向公民发送官方信件并等待收到填写好的表格,公民将能够在线注册选举名单。在这个应用程序中,模型-视图-控制器(Model-View-Controller)由什么构成?
-
JSF 生命周期的哪些部分映射到模型-视图-控制器模式?
-
描述框架何时何地会遇到恢复视图阶段。
-
描述 HTML 表单提交的过程。在 JSF 中,将 HTML 表单的内容传输到 Java POJOS 时会发生什么?
-
当客户在表单中输入无效值时,描述 JSF 生命周期中处理 Faces 请求的阶段。你认为在 Faces 响应中添加了什么?为什么?
-
JSF 规范编写者为什么明确设计了特殊的渲染响应阶段?
-
JSF 在后端 Bean(或动作控制器)中明确地将评估与业务逻辑的调用分离。其他 Web 框架在后端 Bean 中有验证代码。概述这两种方法的优缺点。
-
下载第二章的代码,研究 Web 应用程序的布局。如果你感到勇敢,可以修改项目示例之一。向后端 Bean 添加另一个字符串属性,然后添加一个 JSF 表单文本字段(提示:
<f:inputText>)。会发生什么?如果你的更改出错,你总是可以撤销更改。
第三章。构建 JSF 表单
| *"这是全部。某物实际上在许多不同层面上是如何工作的。最终,当然,设计定义了我们经历的大部分内容。" | ||
|---|---|---|
| --苹果美国设计高级副总裁乔尼·艾夫 |
JavaServer Faces 是一个面向组件的 Web 应用程序框架的例子,与 Java EE 8 MVC(见第九章,Java EE MVC 框架)、WebWork 或 Apache Struts 相对,后者被称为请求导向的 Web 应用程序框架。
请求导向的框架是指信息流从网络请求到响应。这样的框架为你提供了在javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse对象之上的能力和结构,但没有特殊用户界面组件。因此,应用程序用户必须编写将参数和属性映射到数据实体模型的程序。因此,开发者必须编写解析逻辑。
理解这一点很重要,即面向组件的框架,如 JSF,也有其批评者。代码的快速检查类似于在 Java Swing 或 JavaFX 等独立客户端中找到的组件,但同样的HttpServletRequest和HttpServletResponse隐藏在幕后。因此,一个合格的 JSF 开发者必须了解 Servlet API 和底层的 servlet 作用域。这在 2004 年是一个有效的批评,在数字营销时代,数字开发者不仅要了解 Servlet,我们还可以假设他们愿意学习其他技术,如 JavaScript。基于从第二章中获得的知识,我们将学习如何构建 JSF 表单。
创建、检索、更新和删除
在本章中,我们将使用 JSF 解决一个日常问题。Java EE 框架和企业应用程序主要是为了解决数据录入问题。与使用不同架构和非功能性需求(如可伸缩性、性能、无状态和最终一致性)构建的社交网络软件不同,Java EE 应用程序是为有状态的工作流程设计的,如下面的截图所示:

页面视图截图以创建联系详情
上述截图是 JSF 应用程序jsf-crud,显示了创建联系详情表单。
作为提醒,你可以通过本书的源代码找到这个应用程序的完整代码。
通常,企业应用程序从网络用户那里捕获信息,将其存储在数据存储中,并允许检索和编辑这些信息。通常有一个选项可以删除用户信息。在软件工程中,我们称这种习语为创建、检索、更新和删除(CRUD)。
注意
构成实际删除用户和客户数据的是影响业务所有者的一个最终问题,他们面临着遵守定义隐私和数据保护的本地和国际法律的压力。
基本的创建实体 JSF 表单
让我们创建一个基本的表单,用于捕获用户的姓名、电子邮件地址和出生日期。我们将使用 HTML5 编写此代码,并利用 Bootstrap 的现代 CSS 和 JavaScript。请参阅 getbootstrap.com/getting-started/。
以下是 JSF Facelet 视图,createContact.xhtml:
<!DOCTYPE html>
<html
>
<h:head>
<meta charset="utf-8"/>
<title>Demonstration Application </title>
<link href="#{request.contextPath}/resources/styles/bootstrap.css" rel="stylesheet"/>
<link href="#{request.contextPath}/resources/styles/main.css" rel="stylesheet"/>
</h:head>
<h:body>
<div class="main-container">
<div class="header-content">
<div class="navbar navbar-inverse"
role="navigation">
...
</div>
</div><!-- headerContent -->
<div class="mainContent">
<h1> Enter New Contact Details </h1>
<h:form id="createContactDetail"
styleClass="form-horizontal"
p:role="form">
...
</h:form>
</div><!-- main-content -->
<div class="footer-content">
</div> <!-- footer-content -->
</div> <!-- main-container -->
</h:body>
<script src="img/jquery-1.11.0.js"></script>
<script src="img/bootstrap.js"></script>
<script src="img/main.js">
</script>
</html>
你应该已经认识 <h:head> 和 <h:body> JSF 自定义标签了。由于类型是 Facelet 视图(*.xhtml),文档实际上必须像 XML 文档一样具有良好的格式。你应该已经注意到某些 HTML5 元素标签,如 <meta>,已经被关闭并完成;XHTML 文档在 JSF 中必须具有良好的格式。
小贴士
始终关闭 XHTML 元素
典型的电子商务应用程序具有带有标准 HTML 的网页,包括 <meta>、<link> 和 <br> 标签。在 XHTML 和 Facelet 视图中,这些标签,通常是网页设计师留下开放和悬空的,必须关闭。可扩展标记语言(XML)不太宽容,而由 XML 衍生的 XHTML 必须具有良好的格式。
新的 <h:form> 标签是 JSF 自定义标签,对应于 HTML 表单元素。JSF 表单元素与 HTML 对应元素共享许多属性。你可以看到 id 属性是一样的。然而,在 JSF 中,我们使用 styleClass 属性而不是 class 属性,因为在 Java 中,java.lang.Object.getClass() 方法是保留的,因此不能被重写。
小贴士
JSF 请求上下文路径表达式是什么?
围绕样式表、JavaScript 和其他资源的链接周围的标记是表达式语言:#{request.contextPath}。表达式引用确保将 Web 应用程序路径添加到 JSF 资源的 URL 中。Bootstrap CSS 本身依赖于特定文件夹中的字体图标。JSF 图像、JavaScript 模块文件和 CSS 文件应放置在 Web 根目录的资源文件夹中。
p:role 属性是 JSF passthrough 属性的一个例子,它通知 JSF 渲染器将键和值发送到渲染输出。passthrough 属性是 JSF 2.2 的一个重要新增功能,它是 Java EE 7 的一部分。它们允许 JSF 与最近的 HTML5 框架(如 Bootstrap 和 Foundation)良好地协同工作(foundation.zurb.com/)。
下面是渲染的 HTML 源代码的摘录:
<h1> Enter New Contact Details </h1>
<form id="createContactDetail" name="createContactDetail" method="post" action="/jsf-crud-1.0-SNAPSHOT/createContactDetail.xhtml" class="form-horizontal" enctype="application/x-www-form-urlencoded" role="form">
<input type="hidden" name="createContactDetail" value="createContactDetail" />
JSF 是在 Twitter 创建 Bootstrap 之前实现的。JSF 设计者如何将框架改造以兼容最近的 HTML5、CSS3 和 JavaScript 创新?这就是passthrough属性发挥作用的地方。通过在 XHTML 中声明 XML 命名空间 URI xmlns.jcp.org/jsf/passthrough,我们可以为页面视图启用该功能。正如你所见,属性名称和值role="form"简单地传递到输出中。passthrough属性允许 JSF 轻松处理 HTML5 功能,如文本输入字段中的占位符,我们将从现在开始利用这些功能。
小贴士
如果你刚开始接触 Web 开发,你可能会对看起来过于复杂的标记感到害怕。有很多很多由页面设计师和界面开发者创建的 DIV HTML 元素。这是历史效应,也是 HTML 和 Web 随时间演变的方式。2002 年的做法与 2016 年无关。我建议你阅读附录 C,敏捷性能 – 在数字团队中工作。
让我们更深入地看看<h:form>并填补缺失的细节。以下是提取的代码:
<h:form id="createContactDetail"
styleClass="form-horizontal"
p:role="form">
<div class="form-group">
<h:outputLabel for="title" class="col-sm-3 control-label">
Title</h:outputLabel>
<div class="col-sm-9">
<h:selectOneMenu class="form-control"
id="title"
value="#{contactDetailController.contactDetail.title}">
<f:selectItem itemLabel="--" itemValue="" />
<f:selectItem itemValue="Mr" />
<f:selectItem itemValue="Mrs" />
<f:selectItem itemValue="Miss" />
<f:selectItem itemValue="Ms" />
<f:selectItem itemValue="Dr" />
</h:selectOneMenu>
</div>
</div>
<div class="form-group">
<h:outputLabel for="firstName" class="col-sm-3 control-label">
First name</h:outputLabel>
<div class="col-sm-9">
<h:inputText class="form-control"
value="#{contactDetailController.contactDetail.firstName}"
id="firstName" placeholder="First name"/>
</div>
</div>
... Rinse and Repeat for middleName and lastName ...
<div class="form-group">
<h:outputLabel for="email" class="col-sm-3 control-label">
Email address </h:outputLabel>
<div class="col-sm-9">
<h:inputText type="email"
class="form-control" id="email"
value="#{contactDetailController.contactDetail.email}"
placeholder="Enter email"/>
</div>
</div>
<div class="form-group">
<h:outputLabel class="col-sm-3 control-label">
Newsletter
</h:outputLabel>
<div class="col-sm-9 checkbox">
<h:selectBooleanCheckbox id="allowEmails"
value="#{contactDetailController.contactDetail.allowEmails}">
Send me email promotions
</h:selectBooleanCheckbox>
</div>
</div>
<h:commandButton styleClass="btn btn-primary"
action="#{contactDetailController.createContact()}"
value="Submit" />
</h:form>
此表单使用 Bootstrap CSS 样式构建,但我们将忽略无关的细节,纯粹关注 JSF 自定义标签。
<h:selectOneMenu>标签是 JSF 自定义标签,对应于 HTML 表单选择元素。《f:selectItem>标签对应于 HTML 表单选择选项元素。《h:inputText>标签对应于 HTML 表单输入元素。《h:selectBooleanCheckbox>标签是一个特殊自定义标签,用于表示只有一个复选框元素的 HTML 选择。《h:commandButton>代表 HTML 表单提交元素。
JSF HTML 输出标签
<h:outputLabel>标签以以下方式渲染 HTML 表单标签元素:
<h:outputLabel for="firstName" class="col-sm-3 control-label"> First name</h:outputLabel>
开发者应该优先选择此标签与其他相关 JSF 表单输入标签一起使用,因为特殊的for属性针对的是元素的正确糖化标识符。
这是渲染的输出:
<label for="createContactDetail:firstName" class="col-sm-3 control-label"> First name</label>
我们可以使用 value 属性来编写标签,如下所示:
<h:outputLabel for="firstName" class="col-sm-3 control-label" value="firstName" />
在这一点上,也可以利用国际化;所以为了说明,我们可以将页面内容重写如下:
<h:outputLabel for="firstName" class="col-sm-3 control-label" value="${myapplication.contactForm.firstName}" />
关于 JSF 中国际化和资源包的更多信息,请参阅附录 A,JSF 与 HTML5、资源和 Faces 流。让我们继续到输入字段。
JSF HTML 输入文本
<h:inputText>标签允许以文本等形式在表单中输入数据,如下面的代码所示:
<h:inputText class="form-control" value="#{contactDetailController.contactDetail.firstName}" id="firstName" placeholder="First name"/>
值属性代表一个 JSF 表达式语言,线索是评估字符串以哈希字符开头。表达式语言值引用具有 contactDetailController 名称的具有作用域的后端 Bean ContactDetailController.java。在 JSF 2.2 中,现在有便利属性来支持 HTML5 支持,以便标准的 id、class 和 placeholder 属性按预期工作。
渲染输出如下所示:
<input id="createContactDetail:firstName" type="text" name="createContactDetail:firstName" class="form-control" />
注意,简化的 createContactDetails:firstName 标识符与 <h:outputLabel> 标签的输出相匹配。
JSF HTML 选择单菜单
<h:selectOneMenu> 标签生成一个单选下拉列表。实际上,它是选择类型自定义标签家族的一部分。参见下一节中的 JSF HTML 选择布尔复选框。
在代码中,我们有以下代码:
<h:selectOneMenu class="form-control"
id="title"
value="#{contactDetailController.contactDetail.title}">
<f:selectItem itemLabel="--" itemValue="" />
<f:selectItem itemValue="Mr" />
<f:selectItem itemValue="Mrs" />
<f:selectItem itemValue="Miss" />
<f:selectItem itemValue="Ms" />
<f:selectItem itemValue="Dr" />
</h:selectOneMenu>
<h:selectOneMenu> 标签对应于 HTML 表单选择标签。value 属性再次是一个 JSF 表达式语言字符串。
在 JSF 中,我们可以使用另一个新的自定义标签 <f:selectItem>,它将一个 javax.faces.component.UISelectItem 子组件添加到最近的父 UI 组件。<f:selectItem> 标签接受 itemLabel 和 itemValue 属性。如果你设置了 itemValue 而没有指定 itemLabel,则值成为标签。因此,对于第一个选项,选项设置为 --,但提交给表单的值是一个空字符串,因为我们想提示用户应该选择一个值。
渲染的 HTML 输出如下所示:
<select id="createContactDetail:title" size="1"
name="createContactDetail:title" class="form-control">
<option value="" selected="selected">--</option>
<option value="Mr">Mr</option>
<option value="Mrs">Mrs</option>
<option value="Miss">Miss</option>
<option value="Ms">Ms</option>
<option value="Dr">Dr</option>
</select>
JSF HTML 选择布尔复选框
<h:selectBooleanCheckbox> 自定义标签是选择中的一个特殊情况,用户只能选择一个项目。通常,在 Web 应用程序中,你会在最终条款和条件表单或通常在电子商务应用程序的市场营销电子邮件部分找到这样的元素。
在目标管理 Bean 中,唯一的值必须是布尔类型,如下所示:
<h:selectBooleanCheckbox for="allowEmails" value="#{contactDetailController.contactDetail.allowEmails}"> Send me email promotions
</h:selectBooleanCheckbox>
此自定义标签的渲染输出如下所示:
<input id="createContactDetail:allowEmails" type="checkbox" name="createContactDetail:allowEmails" />
JSF HTML 命令按钮
<h:commandButton> 自定义标签对应于 HTML 表单提交元素。它们接受 JSF 中的 action 属性,该属性指向后端 Bean 中的方法。语法再次使用 JSF 表达式语言:
<h:commandButton styleClass="btn btn-primary" action="#{contactDetailController.createContact()}" value="Submit" />
当用户按下此 提交 按钮时,JSF 框架将找到对应于 contactDetailController 的命名管理 Bean,然后调用无参数方法:createContact()。
注意
在表达式语言中,重要的是要注意不需要括号,因为解释器或 Facelet 会自动检查其含义是动作(MethodExpression)还是值定义(ValueExpression)。请注意,现实世界中的大多数示例都不添加括号作为快捷方式。
value 属性表示表单 提交 按钮的文本。我们可以以另一种方式编写标签并达到相同的结果,如下所示:
<h:commandButton styleClass="btn btn-primary"
action="#{contactDetailController.createContact()}" >
Submit
</h:commandButton>
值是从自定义标签的正文内容中获取的。标签的渲染输出如下所示:
<input type="submit" name="createContactDetail:j_idt45" value="Submit" class="btn btn-primary" />
<input type="hidden" name="javax.faces.ViewState" id="j_id1:javax.faces.ViewState:0" value="-3512045671223885154:3950316419280637340" autocomplete="off" />
以下代码展示了 Mojarra 实现(javaserverfaces.java.net/)中 JSF 渲染器的输出,Mojarra 是参考实现。你可以清楚地看到渲染器在输出中写入了一个 HTML 提交和隐藏元素。隐藏元素捕获有关视图状态的信息,这些信息被发送回 JSF 框架(postback),从而允许它恢复视图。
最后,这是联系方式表单的截图:

带有额外出生日期字段(DOB)的联系方式输入 JSF 表单
还有更多 JSF 自定义标签需要考虑,你将在本章后面找到所有标签的完整表格列表。现在,让我们检查后端 Bean,它也被称为控制器。
后端 Bean 控制器
对于我们的简单 POJO 表单,我们需要一个后端 Bean,或者用现代 JSF 开发者的术语来说,就是一个管理 Bean 控制器。
以下是对 ContactDetailController 的完整代码:
package uk.co.xenonique.digital;
import javax.ejb.EJB;
import javax.inject.Named;
import javax.faces.view.ViewScoped;
import java.util.List;
@Named("contactDetailController")
@ViewScoped
public class ContactDetailController {
@EJB ContactDetailService contactDetailService;
private ContactDetail contactDetail =
new ContactDetail();
public ContactDetail getContactDetail() {
return contactDetail;
}
public void setContactDetail(
ContactDetail contactDetail) {
this.contactDetail = contactDetail;
}
public String createContact() {
contactDetailService.add(contactDetail);
contactDetail = new ContactDetail();
return "index.xhtml";
}
public List<ContactDetail> retrieveAllContacts() {
return contactDetailService.findAll();
}
}
对于这个管理 Bean,让我们介绍几个新的注解。第一个注解是 @javax.inject.Named,它声明这个 POJO 是一个 CDI 管理的 Bean,同时也声明了一个 JSF 控制器。在这里,我们将显式声明管理 Bean 的名称值为 contactDetailController。这实际上是管理 Bean 的默认名称,因此我们可以省略它。
我们也可以写一个替代名称,如下所示:
@Named("wizard")
@ViewScoped
public class ContactDetailController { /* .. . */ }
然后,JSF 会给我们一个名为 wizard 的 Bean。管理 Bean 的名称有助于表达式语言语法。
提示
当我们谈论 JSF 时,我们可以自由地互换使用“后端 Bean”和“管理 Bean”这两个术语。许多专业的 Java 网络开发者都明白这两个术语意味着同一件事!
@javax.faces.view.ViewScoped 注解表示控制器已经将视图的生命周期进行了范围限定。范围限定的视图是为了满足一种情况,即应用程序数据仅保留在一个页面中,直到用户导航到另一个页面。一旦用户导航到另一个页面,JSF 就会销毁该 Bean。JSF 会从其内部数据结构中移除对视图范围 Bean 的引用,该对象随后留给垃圾回收器处理。
@ViewScoped 注解是 Java EE 7 和 JSF 2.2 中新引入的,它修复了 Faces 和 CDI 规范之间的一个错误。这是因为 CDI 和 JSF 是独立开发的。通过查看 Javadoc,你会找到一个旧的注解:@javax.faces.bean.ViewScoped,它来自 JSF 2.0,并且不是 CDI 规范的一部分。
目前,如果你选择编写带有 @ViewScoped 注解的控制器,你可能应该使用 @ManagedBean。我们将在本章后面解释 @ViewScoped Bean。
ContactDetailController还依赖于一个企业 JavaBean(EJB)服务端点:ContactDetailService,并且最重要的是,有一个名为ContactDetail的 bean 属性。注意getter和setter方法,我们还将确保在构造时实例化该属性。
我们现在将注意力转向方法,如下所示:
public String createContact() {
contactDetailService.add(contactDetail);
contactDetail = new ContactDetail();
return "index.xhtml";
}
public List<ContactDetail> retrieveAllContacts() {
return contactDetailService.findAll();
}
createContact()方法使用 EJB 创建一个新的联系详情。它返回一个字符串,这是下一个 Facelet 视图:index.xhtml。此方法由<h:commandButton>引用。
retrieveAllContacts()方法调用数据服务以获取实体的列表集合。此方法将被另一页引用。
数据服务
控制器依赖于一个实体 bean:ContactDetail。以下是此 bean 的代码,它已经被简化:
package uk.co.xenonique.digital;
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name="CONTACT")
@NamedQueries({
@NamedQuery(name="ContactDetail.findAll",
query = "select c from ContactDetail c " +
"order by c.lastName, c.middleName, c.firstName")
})
public class ContactDetail {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="CONTACT_ID", nullable = false,
insertable = true, updatable = true,
table = "CONTACT")
private long id;
private String title="";
private String firstName;
private String middleName;
private String lastName;
private String email;
@Temporal(TemporalType.DATE)
private Date dob;
private Boolean allowEmails;
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
// Other getters and setters omitted
// equals, hashCode, toString omitted
}
它使用Java 持久化 API(JPA)注解将 Java 属性映射到关系数据库。
有一组注解是针对实体本身声明的。@Entity注解将此 POJO 标记为持久化能力对象。@Table注解覆盖了实体的默认数据库表名——而不是CONTACT_DETAIL,它变成了CONTACT。@NameQueries和@NameQuery注解定义了Java 持久化查询语言(JPQL)中的名称查询。
剩余的注解声明与数据库表列关联的元数据。@Id注解指定将成为主键的属性,即id字段。@GenerationValue注解声明主键是自动生成的。如果存在0或 null 值,JPA 提供商会生成一个唯一值。此属性上的其他注解@Column将默认数据库表列名从ID重命名为CONTACT_ID并设置某些约束。
最后,JPA 必须使用@Temporal注解指定字段的日期时间类型。注解值可以是Temporal.DATE、Temporal.TIME或Temporal.TIMESTAMP。
你可以在我的书中了解 JPA,即《Java EE 7 开发者手册》,其中有几个关于该主题的简洁和专门的章节。然而,这本书关注的是 Web 应用程序开发。
现在应该很明显,实体 bean 可以直接在 JSF 表单中使用。你还记得表单属性的 JSF 表达式语言吗?看看以下的名字字段:
<h:inputText class="form-control" value="#{contactDetailController.contactDetail.firstName}" id="firstName" placeholder="First name"/>
由于 JSF 框架通过名称知道contactDetailController,其类类型为ContactDetailController,它可以遍历对象图并确定属性。控制器有一个名为contactDetail的属性,其类型为ContactDetail,它有一个字符串类型的firstName属性。
控制器的关键要求是在表单提交时以及从表单检索remembered值时,实体应该被实例化。让我们看看以下代码:
private ContactDetail contactDetail = new ContactDetail();
开发者可以针对大型对象层次结构使用多种可能性。延迟加载和延迟创建数据结构可以帮助在这些情况下。
让我们来看看企业服务豆ContactDataService:
package uk.co.xenonique.digital;
import javax.ejb.Stateful;
import javax.persistence.*;
import java.util.List;
@Stateful
public class ContactDetailService {
@PersistenceContext(unitName = "applicationDB",
type = PersistenceContextType.EXTENDED)
private EntityManager entityManager;
public void add(ContactDetail contactDetail) {
entityManager.persist(contactDetail);
}
public void update(ContactDetail contactDetail) {
ContactDetail contactDetailUpdated
= entityManager.merge(contactDetail);
entityManager.persist(contactDetailUpdated);
}
public void delete(ContactDetail contactDetail) {
entityManager.remove(contactDetail);
}
public List<ContactDetail> findAll() {
Query query = entityManager.createNamedQuery(
"ContactDetail.findAll");
return query.getResultList();
}
}
这个类是一个有状态会话 EJB 的例子,它本质上是一个具有会话状态的应用服务器中的可池化远程服务端点。有状态会话豆与调用客户端相关联。
ContactDetailService依赖于一个 JPA 提供者,正如我们通过@PersistenceContext注解注入实体管理器所看到的那样。请注意,我们使用持久化上下文的扩展版本,因为会话可以持续多个请求-响应周期。
在非扩展持久化会话中,EntityManager 将仅存在于 JTA 事务期间。一旦 Java EE 模型中的事务完成,所有持久化对象都会从EntityManager中分离,并变为未管理状态。
扩展持久化会话是指 EntityManager 可以超出Java 事务 API(JTA)事务的作用域。实际上,它可以在多个事务中存活。在这种情况下,持久化对象不会从 EntityManager 中分离;数据仅在显式刷新或通过应用服务器为有状态会话豆提供的特殊状态标记时保存到数据库中。因此,扩展持久化上下文只能用于有状态会话豆。
有关权限和有状态会话豆的更多信息,请参阅我的姐妹书籍《Java EE 7 开发者手册》。
目前,我们只需关注ContactDataService中的方法。add()方法在数据库中插入一条新记录。update()方法修改现有记录,delete()方法删除记录。findAll()方法从底层数据库检索所有ContactDetail记录。它使用命名的 JPQL 查询:Contact.findAll。
你可能想知道用户界面中的 JSF 字段在哪里设置Date of Birth (DOB)属性,就像在ContactDetail实体豆中看到的那样。我们稍后会添加这些字段。
JSF 自定义标签
正如你所见,JSF 自带丰富的自定义标签库。为了充分利用框架,数字开发者应该了解它们及其功能。标签可以按照我们之前看到的命名空间进行划分。
HTML 渲染工具自定义标签
JSF 2.2 中的第一组标签与 HTML 元素的渲染相关。它们位于命名空间:xmlns.jcp.org/jsf/html。JSF 框架中渲染工具的默认实现包含javax.faces.component.UIComponent的组件标签。
这里是 HTML 渲染工具标签的表格:
| JSF 自定义标签 | 描述 |
|---|---|
<h:column> |
这将渲染一个javax.faces.component.UIColumn实例,代表父 UIData 组件中的单个数据列。此自定义标签用于<h:dataTable>中。 |
<h:commandButton> |
这将渲染一个具有提交或重置类型的 HTML 输入元素。 |
<h:commandLink> |
这将渲染一个类似于提交按钮的 HTML 锚元素,因此,该标签必须添加在<h:form>标签中。 |
<h:dataTable> |
这将渲染一个具有行和列的 HTML 表格,包括表头和表列单元格。 |
<h:form> |
这将渲染一个 HTML 表单元素。 |
<h:graphicImage> |
这将渲染一个 HTML 图像元素。 |
<h:inputFile> |
这将渲染一个具有文件类型的 HTML 表单输入元素,并允许应用程序从客户端操作系统上传文件。 |
<h:inputHidden> |
这将渲染一个具有隐藏类型的 HTML 表单输入元素。 |
<h:inputSecret> |
这将渲染一个具有密码类型的 HTML 表单输入元素。 |
<h:inputText> |
这将渲染一个具有文本类型的 HTML 表单输入元素。 |
<h:inputTextarea> |
这将渲染一个 HTML 表单文本区域元素。 |
<h:link> |
这将渲染一个 HTML 锚元素,执行对应用的 HTTP GET 请求。 |
<h:outputFormat> |
此标签以格式化参数渲染参数化文本。 |
<h:outputLabel> |
这将渲染一个 HTML 标签元素。 |
<h:outputLink> |
这将渲染一个 HTML 锚元素,通常用于非 JSF 应用的链接。 |
<h:outputText> |
此标签将输出渲染到视图中。 |
<h:message> |
这将为特定组件向页面渲染一条消息。该标签通过资源包允许国际化。 |
<h:messages> |
这将从 Faces 上下文向页面渲染一系列消息。 |
<h:panelGrid> |
此自定义标签将组件渲染到网格中。默认的 JSF 实现使用 HTML 表格元素。 |
<h:panelGroup> |
此自定义标签将嵌套的 JSF 标签组织到定义的组中,其中布局生成并生成单个实体。 |
<h:selectBooleanCheckbox> |
这将渲染一个具有复选框类型的 HTML 输入元素,并设计用于布尔属性。 |
<h:selectManyCheckbox> |
这将渲染具有复选框类型的 HTML 输入元素列表。 |
<h:selectManyListbox> |
这将渲染 HTML 选择选项元素的列表。 |
<h:selectManyMenu> |
这将渲染 HTML 选择选项元素的列表。 |
<h:selectOneListbox> |
这将渲染 HTML 选择选项元素的列表。 |
<h:selectOneMenu> |
这将渲染 HTML 选择选项元素的列表。 |
<h:selectOneRadio> |
这将渲染一个具有单选类型的 HTML 输入元素列表。 |
JSF HTML 标签被分为不同的类型,例如命令、输入、输出和类型,以便处理项目的选择。还有额外的标签来处理特殊案例,例如<h:graphicImage>用于渲染<img>标签和<h:dataTable>用于渲染<table>信息。
核心 JSF 自定义标签
核心 JSF 自定义标签添加了独立于 HTML 渲染标签的功能。这些标签的命名空间是xmlns.jcp.org/jsf/core。JSF 框架是可扩展的。如果您想使用替代渲染套件,那么您只需添加它。核心 JSF 自定义标签仍然会工作。
这里是 JSF 核心标签的表格:
| JSF 自定义标签 | 描述 |
|---|---|
<f:actionListener> |
这注册一个ActionListener实例。 |
<f:attribute> |
这向UIComponent添加一个属性,并使用最近的父UIComponent执行操作。 |
<f:convertDateTime> |
这将DateTimeConverter注册到UIComponent。 |
<f:convertNumber> |
这将NumberConverter注册到UIComponent。 |
<f:converter> |
这渲染一个类似于提交按钮的 HTML 锚点元素,因此,该标签必须添加到<h:form>中。 |
<f:facet> |
这向组件添加一个面。 |
<f:loadBundle> |
这加载一个为当前视图的 Locale 本地化的资源包,并将属性存储为java.util.Map。 |
<f:metadata> |
这声明此视图的元数据面。 |
<f:param> |
这向UIComponent添加一个参数。 |
<f:phaseListener> |
这将PhaseListener实例注册到页面中。 |
<f:selectItem> |
这指定了一个单选或多选组件的项目。 |
<f:selectItems> |
这指定单选或多选组件的项目。 |
<f:setProperty-ActionListener> |
这将ActionListener注册到组件的特定属性。 |
<f:subview> |
这创建另一个 JSF 命名上下文(参见<f:view>)。 |
<f:validateDoubleRange> |
这将DoubleRangeValidator注册到组件中。 |
<f:validateLength> |
这将LengthValidator注册到组件中。 |
<f:validateLongRange> |
这将LongRangeValidator注册到组件中。 |
<f:validateRegex> |
这将正则表达式验证器注册到组件中。如果整个模式匹配,则它是有效的。 |
<f:validateRequired> |
这确保在表单提交时组件中的值存在。 |
<f:validator> |
这将命名的 Validator 实例注册到组件中。 |
<f:valueChangeListener> |
这将ValueChangeListener注册到组件中。 |
<f:verbatim> |
这向 JSF 页面添加标记,并允许主体内容直接传递到渲染输出。 |
<f:view> |
这为页面的 JSF 当前命名上下文设置参数。使用此标签可以覆盖区域设置、编码或内容类型。 |
<f:viewParam> |
这将一个视图参数添加到分面的元数据中,以便页面可以访问在 GET 请求中查询参数。此标签只能在<f:metadata>中使用。 |
许多核心 JSF 标签的目的是增强和配置一个UIComponent实例。您已经在之前的代码示例createContact.xhtml中的<h:selectOneMenu>的<f:selectItem>标签中看到了这个示例的使用。(见基本 JSF 表单部分)。
在大多数情况下,开发者可以使用核心 JSF 标签向组件添加属性、监听器、转换器、分面、参数和选择。
模板组合自定义标签
模板 JSF 自定义标签库为您提供了使用其他页面内容组合页面的能力。模板允许内容在整个 JSF 应用程序中重用和共享。最好的是,可以通过指定参数来适应模板,从而在混合中具有适应性和灵活性。这些标签的命名空间是xmlns.jcp.org/jsf/facelets,这强调了 Facelet 视图背后的技术。
这里是 JSF 2.2 中的模板标签列表:
| JSF 自定义标签 | 描述 |
|---|---|
<ui:component> |
这定义了一个模板组件并指定了组件的文件名。 |
<ui:composition> |
这定义了一个页面组合,它封装了可选使用模板的 JSF 内容。 |
<ui:debug> |
这将在当前页面创建并添加一个特殊组件,允许显示调试输出。 |
<ui:define> |
这定义了由组合模板插入到页面中的 JSF 内容。 |
<ui:decorate> |
这定义了装饰 JSF 页面特定区域的内联内容。 |
<ui:fragment> |
以类似于<ui:composition>标签的方式定义模板片段,不同之处在于此标签保留正文外的内容,而不是丢弃它。 |
<ui:include> |
这将另一个 JSF 页面插入到当前页面中。 |
<ui:insert> |
这将一个命名的内联定义插入到当前页面中。此标签与<ui:define>一起使用。 |
<ui:param> |
这将参数传递给由<ui:include>或模板引用(如<ui:composition>或<ui:include>)指定的包含文件。 |
<ui:repeat> |
这遍历来自 bean 属性或方法的列表集合。此标签是循环遍历集合的替代方法,类似于<h:dataTable>或<c:forEach>。 |
<ui:remove> |
这从页面中删除特定的标记内容。 |
我们已经在第二章中看到了<ui:composition>、<ui:define>和<ui:insert>的操作,JavaServer Faces 生命周期。我们肯定会使用模板 JSF 标签来处理本书中关于 JSF 的剩余部分。
常见属性
JSF 标准标签共享许多共同属性。以下表格是一个参考,其中一些属性适用于大多数 HTML 渲染标签:
| 属性名称 | 描述 |
|---|---|
id |
这指定了 HTML 元素标识符。JSF 开发者应该每次都使用此属性。 |
binding |
这将一个标签绑定到一个管理 Bean 中的组件实例。JSF 框架将组件树中的组件引用绑定到一个作用域变量。 |
Immediate |
这指定了一个布尔值,如果设置为true,则导致 JSF 框架在 JSF 生命周期中的“应用请求值”阶段之后跳过验证、转换和事件的处理。 |
rendered |
这指定了一个布尔值,通常默认为 true,表示组件是否应该被渲染。 |
required |
这指定了该输入元素是否为输入验证所必需的布尔值。 |
styleClass |
这指定了渲染标签的 HTML 类属性。 |
stylestyle |
这指定了渲染标签的 HTML 样式属性。 |
valuevalue |
这指定了一个字符串值或表达式语言引用。 |
现在我们已经看到了 JSF 标签,我们将回到我们的 CRUD 示例。
显示对象列表集合
对于 CRUD 示例,我们经常面临在应用程序中以用户可理解的有意义的方式显示数据的实际问题。最容易的方法之一就是简单地打印出项目的列表,对于相对简单的数据。另一种方法是显示数据的表格视图。如果你的数据是树结构或图,还有其他值得考虑的解决方案。
对于我们的情况,我们将选择第二条路径,并在表格中显示联系详情列表。在 JSF 中,我们可以使用<h:dataTable>HTML 组件。这个自定义标签遍历列表中的每个对象并显示指定的值。《h:dataTable》组件是一个非常强大且灵活的标签,因为 Java 网络工程师可以配置它以在多种布局中渲染自定义样式。
让我们来看看jsf-crud项目中的另一个 JSF Facelet 视图,index.html。作为提醒,我们正在使用 Bootstrap CSS 进行样式设计。现在,以下是提取的代码,如下所示:
<div class="main-content">
...
<h2> List of Contact Details </h2>
<h:dataTable id="contactTable"
value="#{contactDetailController.retrieveAllContacts()}"
styleClass="table-striped table-bordered user-table"
var="contact">
<h:column>
<f:facet name="header">
<h:outputText value="Title" />
</f:facet>
<h:outputText value="#{contact.title}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="First name" />
</f:facet>
<h:outputText value="#{contact.firstName}"/>
</h:column>
... (repeat for Middle name and Last Name) ...
<h:column>
<f:facet name="header">
<h:outputText value="Email" />
</f:facet>
<h:outputText value="#{contact.email}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="D.O.B" />
</f:facet>
<h:outputText value="#{contact.dob}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Allow emails?" />
</f:facet>
<h:outputText value="#{contact.allowEmails}"/>
</h:column>
</h:dataTable>
<hr class="subfeaturette-divider" />
</div><!-- main-content -->
你首先会注意到<h:dataTable>标签接受一个值属性,这是 JSF 表达式语言对控制器retrieveAllContacts()方法的引用。ContactDetailController将此请求委派给我们在前面看到的ContactDetailService,即有状态的会话 EJB。
var属性指定了 JSF 作用域变量的名称,这是每次组件遍历列表集合时创建的元素。视图中的元素类型是实体 bean:ContactDetail。
styleClass属性添加了来自 Bootstrap 框架的特定 CSS 样式,当然,每个组件都可以有一个id属性。
<h:dataTable>组件需要嵌套的描述表格列数据的<h:column>标签。
如果您想为表格添加标题行,那么您必须在<h:column>标签中放置并添加一个核心 JSF 标签<f:facet>。此标签名必须有一个特殊的名称属性,其值为header。如果您问我:为什么我必须使用不同的 XML 命名空间来编写标签?那么我的回答将是,这是 JSF 设计者预见核心标签可以重复用于其他渲染套件的方式。因此,标签名是<f:facet>而不是类似<h:headerColumn>的东西。
为了向用户显示每行的信息,我们使用<h:outputText>元素。此标签接受另一个表达式语言语句,即实体 Bean 中属性的引用,例如#{contact.firstName}。
这里是index.html列表视图的截图:

CRUD 应用程序的列表视图截图
增强的日期时间输入
如果您注意到,我们忽略了添加 JSF 控件,以便用户可以将他的或她的出生日期添加到联系详细信息表单中。让我们假设我们敏捷团队中的 UX 人员有一个指令,并且输入必须以两个下拉列表的形式出现。业务希望有两个下拉元素,分别用于月份的天数和年份的月份。他们还希望有一个文本输入用于年份。
在我们的 JSF 之旅中,我们已经介绍了一些 HTML 选择自定义标签,例如<h:selectOneMenu>和<h:selectBooleanCheckbox>。现在,我们将学习如何从我们的管理 Bean 中程序化地生成这些标签的数据。如果我们能帮助的话——我们当然可以——我们真的不想在 JSF 视图中重复编写<f:selectItem> 31 次。
我们需要在ContactDetailController中添加额外的逻辑。这些增强是为了 JSF 管理 Bean,它提供了通过表达式语言可访问的方法,如下所述:
@ManagedBean(name = "contactDetailController")
@ViewScoped
public class ContactDetailController {
// ... same as before
public String createContact() {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.DAY_OF_MONTH, dobDay);
cal.set(Calendar.MONTH, dobMonth-1 );
int year = Integer.parseInt(dobYear);
cal.set(Calendar.YEAR, year);
contactDetail.setDob(cal.getTime());
contactDetailService.add(contactDetail);
contactDetail = new ContactDetail();
return "index.xhtml";
}
// ...
private int dobDay;
private int dobMonth;
private String dobYear;
public int getDobDay() { return dobDay; }
public void setDobDay(int dobDay) {
this.dobDay = dobDay; }
// ... getter and setter for dobMonth and dobYear
private static List<Integer> daysOfTheMonth
= new ArrayList<>();
private static Map<String,Integer> monthsOfTheYear
= new LinkedHashMap<>();
static {
for (int d=1; d<=31; ++d) {
daysOfTheMonth.add(d);
}
DateFormatSymbols symbols =
new DateFormatSymbols(Locale.getDefault());
for (int m=1; m<=12; ++m) {
monthsOfTheYear.put(symbols.getMonths()[m-1], m );
}
}
public List<Integer> getDaysOfTheMonth() {
return daysOfTheMonth;
}
public Map<String,Integer> getMonthsOfTheYear() {
return monthsOfTheYear;
}
}
我们将在控制器中添加三个新的 Bean 属性:dobDay、dobMonth和dobYear。请注意,dobYear是一个字符串,而其他两个是整数,因为年份字段是一个文本字段。当使用整数时,前端显示的默认值是0,这会减损并混淆用户。我们希望用户看到一个空白的文本字段。这些新属性有 getter 和 setter。
我们增强了createContact()方法,以便考虑来自三个单独字段的出生日期,并使用java.util.Calendar实例将它们转换为 DOB 值。在将实体 Bean 保存到数据库之前,我们将设置一个具有java.util.Date类型计算值的属性。
有两个属性方法,getDaysOfTheMonth() 和 getMonthsOfTheYear(),它们将返回由类静态初始化器构建的静态集合。daysOfTheMonth 字段是一个从 1 到 31 的整数列表集合,而 monthsOfTheYear 字段是一个与整数关联的条目和字符串的映射集合,代表一年的月份。
我们使用 JDK 的 DateFormatSymbols 类来检索设置为应用程序默认区域设置的月份的长名称。
通过这些后端更改,我们可以调整 JSF 视图,以便添加设置申请人的出生日期的能力。
这里是 JSF 视图 createContactDetails.xhtml 的更新更改:
<label class="control-label"> Your Date of Birth</label>
<div class="row my-group-border">
<div class="col-sm-3">
<label class="control-label" for="dobDay">Day</label>
<div class="controls">
<h:selectOneMenu id="dobDay"
value="#{contactDetailController.dobDay}"
label="Registration Day">
<f:selectItem itemLabel="----" itemValue=""/>
<f:selectItems
value="#{contactDetailController.daysOfTheMonth}"
var="day"
itemLabel="#{day}" itemValue="#{day}" />
<f:validateRequired/>
</h:selectOneMenu>
<h:message for="dobDay" styleClass="form-error"/>
</div>
</div>
<div class="col-sm-3">
<label class="control-label" for="dobMonth">Month</label>
<div class="controls">
<h:selectOneMenu id="dobMonth"
value="#{contactDetailController.dobMonth}"
label="Registration Month">
<f:selectItem itemLabel="----" itemValue=""/>
<f:selectItems
value="#{contactDetailController.monthsOfTheYear}" />
<f:validateRequired/>
</h:selectOneMenu>
<h:message for="dobMonth" styleClass="form-error"/>
</div>
</div>
<div class="col-sm-3">
<label class="control-label" for="dobYear">Year</label>
<div class="controls">
<h:inputText id="dobYear"
value="#{contactDetailController.dobYear}"
label="Registration Year">
<f:validateRequired/>
</h:inputText>
<h:message for="dobYear" styleClass="form-error"/>
</div>
</div>
</div>
<h:commandButton styleClass="btn btn-primary" action="#{contactDetailController.createContact()}" value="Submit" />
嗯,希望我没有吓到你,让你跑上坡路!我们在这里使用 Bootstrap CSS v3.11,这就是为什么你在 HTML 中看到很多带有特定命名的 CSS 选择器的 <div> 元素,如 control-label、col-sm-6 和 row。Bootstrap 是一个流行的 HTML5、CSS 和 JavaScript 框架,它帮助设计师和开发者构建响应式网站。
作为组件框架,JSF 提供了封装 <div> 层、CSS 和 JavaScript 的基础。有一些方法可以帮助实现这一点。首先,团队可以开发自己的自定义组件;其次,他们可以利用具有所需功能和定制的第三方组件系统;最后,团队可以作为库编写者,因此创建他们自己的定制 HTML 渲染工具包。自定义组件编程起来要容易得多,我们将在第五章 Conversations and Journeys 中讨论,对话和旅程。
如果您的团队对组件库感兴趣,那么您可能想查看供应商解决方案,例如 Rich Faces (richfaces.jboss.org/) 和特别是 Prime Faces (primefaces.org/)。
让我们集中讨论 <h:selectOneMenu> 标签。这个来自 JSF 命名空间的 HTML 自定义标签指定了一个下拉选择列表,用户只能选择一个项目。value 属性引用控制器 bean 中的一个属性。因此,第一个字段的表达式语言是 #{contactDetailController.dobDay}。
在父标签中,您可以看到 <f:selectItem> 和 <f:selectItems> 自定义标签。<f:selectItem> 标签定义了一个菜单项。它接受 itemLabel 和 itemValue 属性。我们可以用它来定义一个默认的空选项。
<f:selectItems> 标签定义了许多菜单项,并接受另一个值属性,即表达式语言 #{contactDetailController.daysOfTheMonth}。这个表达式引用控制器获取方法 getDaysOfTheMonth(),它返回 List<Integer>。我们将使用 var、itemLabel 和 itemValue 来配置如何渲染每个菜单选项,如下所示。
<f:selectItems value="#{contactDetailController.daysOfTheMonth}" var="day" itemLabel="#{day}" itemValue="#{day}" />
正如 <h:dataTable> 标签一样,我们可以使用 var 属性定义 JSF 范围变量,并有效地遍历集合。
在 <f:selectMenu> 中,年份下拉菜单的标记略有不同。由于 getMonthsOfTheYear() 已经返回了一个 Map<String,Integer> 集合,因此不需要提供标签和值的配置。自定义标签已经知道它必须渲染地图集合。
DOB 年份的最后字段是 <h:inputText>,到现在为止,你已经知道这些标签是如何工作的。你可能已经注意到了一些惊喜。
<f:validateRequired> 标签是一个验证自定义标签,它指定了在表单提交时必须定义 bean 属性。<h:message> 标签指定了一个区域,在 HTML 中我们希望特定的验证错误显示如下:
<h:message for="dobYear" styleClass="form-error"/>
<h:message> 标签接受一个必填的 for 属性,该属性引用 JSF HTML 表单属性。我们可以使用 styleClass 属性设置 CSS 样式,这是一个来自 Bootstrap 的表单错误。在下一章中,我们将详细探讨验证。
这里是新的表单的截图:

联系数据应用程序创建页面视图的截图
编辑数据
现在,让我们再添加一个 JSF index.xhtml,以便用户可以编辑和删除联系详情。在我们能够编辑联系详情之前,我们必须向列表视图添加一些 JSF 链接,以便用户可以导航到编辑和删除页面。
让我们修改 index.xhtml 视图中的 <h:dataTable> 部分,并添加一个额外的列。代码如下:
<h:dataTable id="contactTable"
... other columns as before ...
<h:column>
<f:facet name="header">
<h:outputText value="Action" />
</f:facet>
<h:link styleClass="btn"
outcome="editContactDetail.xhtml?id=#{contact.id}">
<f:param name="id" value="#{contact.id}" />
<span class="glyphicon glyphicon-edit"></span>
</h:link>
<h:link styleClass="btn"
outcome="removeContactDetail.xhtml?id=#{contact.id}">
<f:param name="id" value="#{contact.id}" />
<span class="glyphicon glyphicon-trash"></span>
</h:link>
</h:column>
</h:dataTable>
我们有两个 <h:link> 标签,它们生成两个 HTML 锚点元素链接到两个新页面:editContactDetail.xhtml 和 removeContactDetail.xhtml。
<h:link> 自定义标签有一个 outcome 属性,用于使用 JSF 导航规则生成 URL。value 属性指定链接上的文本或你可以指定正文文本。这个标签足够聪明,如果链接不存在,它将生成一个 <span> 元素。这是一个有用的功能,用于原型设计。
这里是 <h:link> 的渲染输出的一部分:
<td>
<a href="/jsf-crud-1.0-SNAPSHOT/editContactDetail.xhtml?id=5" class="btn">
<span class="glyphicon glyphicon-edit"></span></a>
<a href="/jsf-crud-1.0-SNAPSHOT/deleteContactDetail.xhtml?id=5" class="btn">
<span class="glyphicon glyphicon-trash"></span></a>
</td>
glyphicon、glyphicon-edit 和 glyph-trash 类是 Bootstrap 的标记,用于显示图标按钮。
在设置好链接之后,我们现在必须在服务器端允许编辑合同详情。我们将通过添加新的属性和方法来适配 ContactDetailController。我们将引入的第一个属性是 id,这样我们就可以跟踪数据库中联系 ID 的主键。我们还将需要为 JSF 框架提供 getter 和 setter。
重新考虑一下,允许用户取消任务会更好。因此,我们将在控制器中引入一个 cancel() 方法。我们还将添加几个方法:findByContactId() 和 editContact()。
这是 ContactDetailController 的以下代码,现在如下所示:
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
public class ContactDetailController {
// ... as before ...
private int id;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String cancel() {
return "index.xhtml";
}
public void findContactById() {
if (id <= 0) {
String message =
"Bad request. Please use a link from within the system.";
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(
FacesMessage.SEVERITY_ERROR, message, null));
return;
}
ContactDetail item = contactDetailService.findById(id).get(0);
if (item == null) {
String message =
"Bad request. Unknown contact detail id.";
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(
FacesMessage.SEVERITY_ERROR, message, null));
}
contactDetail = item;
Calendar cal = Calendar.getInstance();
cal.setTime(contactDetail.getDob());
dobDay = cal.get(Calendar.DAY_OF_MONTH);
dobMonth = cal.get(Calendar.MONTH)+1;
dobYear = Integer.toString(cal.get(Calendar.YEAR));
}
public String editContact() {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.DAY_OF_MONTH, dobDay);
cal.set(Calendar.MONTH, dobMonth-1);
int year = Integer.parseInt(dobYear);
cal.set(Calendar.YEAR, year);
contactDetail.setDob(cal.getTime());
contactDetail.setId(id)
contactDetailService.update(contactDetail);
contactDetail = new ContactDetail();
return "index.xhtml";
}
// ...
}
cancel() 方法简单地返回下一个视图:index.xhtml。它什么都不做,这并不是代码中的错误,而实际上是目的:返回到开始。
findContactById() 方法使用 id 属性通过 ContactDataService EJB 查找联系详细信息。此方法使用 Calendar 实例从 ContactDetail 实体中提取 dob 属性,并将其分解为 dobDay、dobMonth 和 dobYear 属性。
javax.faces.context.FacesContext 类型是一个聚合对象,用于存储当前请求和响应信息。FacesContext 只能通过工厂方法获取。在示例中,我们将向 Faces 响应添加错误消息,该消息可以在视图中显示。javax.faces.application.FacesMessage 类型是错误验证的表示,或者可以定义为来自外部资源包的消息资源。参见 附录 A,JSF with HTML5, Resources, and Faces Flows。
editContact() 方法几乎与 createContect() 相同,因为它在实体中重建了 dob 属性。不同之处在于实体中的 id 属性是从控制器属性 id 中设置的:设置正确的主键至关重要,因为用户不希望看到重复条目。现在 editContect() 方法使用 update() 而不是 create() 调用数据库。
我们现在将使用新的命名查询来适配 ContactDetail 实体。以下是修改内容:
@Entity
@Table(name="CONTACT")
@NamedQueries({
@NamedQuery(name="ContactDetail.findAll", query = "select c from ContactDetail c " + "order by c.lastName, c.middleName, c.firstName"), @NamedQuery(name="ContactDetail.findById", query = "select c from ContactDetail c where c.id = :id"),
})
public class ContactDetail { /* ... as before ... */ }
命名的 ContactDetail.findById 查询使用带有键参数的 JPQL 语句,该参数在字符串中表示为 :id。我们现在将向 EJB 添加一个额外的方法。
这里是附加的 ContactDetailService 方法,以下是其代码:
@Stateful
public class ContactDetailService {
// ... as before ...
public List<ContactDetail> findById(Integer id) {
Query query = entityManager.createNamedQuery("ContactDetail.findById").setParameter("id", id);
return query.getResultList();
}
}
findById() 方法使用命名查询并调用 JPA 查询以检索 ContactDetail 元素的列表集合。根据定义,集合中应该只有一个元素,因为我们是通过主键进行查询的。
在后端进行这些更改后,我们只需要在页面视图中进行一些更改,这几乎与 createContactDetail.xhtml 相同。
这里是 Facelet 视图的摘录,editContactDetail.xhtml:
<h:body>
<f:metadata>
<f:viewParam name="id" value="#{contactDetailController.id}" />
<f:event type="preRenderView" listener="#{contactDetailController.findContactById()}"/>
</f:metadata>
...
<div class="main-content">
<h1> Edit Contact Details </h1>
<h:form id="editContactDetail"
styleClass="form-horizontal"
p:role="form">
<h:inputHidden value="${contactDetailController.id}" />
<div class="form-group">
...
</div>
<h:commandButton styleClass="btn btn-primary"
action="#{contactDetailController.editContact()}"
value="Submit" />
 
 
<h:commandButton styleClass="btn btn-default"
action="#{contactDetailController.cancel()}"
immediate="true" value="Cancel"/>
</h:form>
<hr class="subfeaturette-divider" />
</div><!-- "main-content" -->
...
</h:body>
这里使用了 JSF 自定义标签。<f:metadata> 标签是一个容器标签,用于声明当前页面的元数据面。
<f:viewParam> 标签将页面作为当前视图的元数据附加一个 GET 请求参数。我们将使用它将查询参数附加到控制器属性。name 属性指定查询参数名称。value 属性指定 JSF 表达式语言引用。提供一个 URL 请求,如 /jsf-crud-1.0-SNAPSHOT/editContactDetail.xhtml?id=4,将导致框架在 ContactDetailController 中的 id 属性中填充 4 的值。这个调用发生在 JSF 生命周期的 Restore View 阶段。
小贴士
由于 <f:metadata> 标签声明了单个页面视图的元数据,它必须放置在页面根元素视图附近。如果 <f:metadata> 标签用于 JSF 模板组合,则必须放置在 <ui:define> 中。在示例中,该标签位于 <h:body> 之后。
<f:event> 自定义标签将 JSF Faces 事件与组件关联。官方文档描述此标签时说,它在页面上的目标组件上安装了一个 ComponentSystemEventListener 实例。在这里,我们可以简单地说,该标签将预渲染事件与控制器中的 findByContactId() 方法关联。换句话说,<f:event> 预填充表单以包含底层数据库中的数据。
在 <h:form> 内容中,我们将使用 <h:hidden> 自定义标签来存储联系详情的当前 ID。值属性是一个表达式引用。这样,当用户提交表单时,标识符就会传播回控制器。
最后,有两个 <h:submit> 按钮,它们分别引用控制器中的 editContact() 和 cancel() 方法。第二个 <h:submit> 按钮中的中间属性指定 JSF 生命周期应跳过 Process Validation 状态。因此,当表单提交时,JSF 不应用验证。相反,生命周期从 Apply Request Values 直接移动到 Render Response 状态。
小贴士
在 XHTML 中添加 HTML 实体字符
Facelets 只支持五个预定义的 XML 实体字符:<、>、&、" 和 &apos。添加 HTML 元素的唯一方法是通过十六进制或八进制表示法。  实体表示 Unicode 字符  (空格)。
这里是 editContactDetail.xhtml 视图的截图:

联系详情应用程序编辑页面视图的截图
删除数据
我们的用户能够创建联系详情,现在她可以更新条目。为了完成我们客户的旅程,我们应该允许她作为良好的网络公民删除条目。为什么现在有那么多公司想要通过设置危险或额外的麻烦来阻止用户删除数据的访问,使得这样一个简单的任务变得如此困难,这让我感到困惑!然而,我们可以为我们联系详情应用程序做这件事,现在它变得非常直接,因为我们已经有了构建块。
我们将在 ContactDetailController 中添加一个 removeDetail() 方法。以下是额外的方法:
public class ContactDetailController {
// ... as before ...
public String removeContact() {
contactDetail = contactDetailService.findById(id).get(0);
contactDetailService.delete(contactDetail);
contactDetail = new ContactDetail();
return "index.xhtml";
}
}
此方法通过新的 id 搜索 contactDetail。id 字段是控制器的属性,它在隐藏的表单字段中设置。通过在表单提交时调用数据服务的 findById() 方法,我们将确保从持久化上下文中检索最新信息。也许用户去吃午饭然后回来提交了表单。找到实体后,我们可以调用数据服务来删除它。
这是 removeContactDetail.xhtml 视图的摘录:
<div class="main-content">
<h1> Delete Contact Details </h1>
<table class="table table-striped table-bordered">
<tr>
<th> Item</th> <th> Value</th>
</tr>
<tr>
<td> Title </td>
<td>
#{contactDetailController.contactDetail.title} </td>
</tr>
<tr>
<td> First Name </td>
<td>
#{contactDetailController.contactDetail.firstName} </td>
</tr>
<tr>
<td> Middle Name </td>
<td>
#{contactDetailController.contactDetail.middleName} </td>
</tr>
<tr>
<td> Last Name </td>
<td>
#{contactDetailController.contactDetail.lastName} </td>
</tr>
<tr>
<td> Allows Email? </td>
<td>
#{contactDetailController.contactDetail.allowEmails} </td>
</tr>
<tr>
<td> Email </td>
<td> #{contactDetailController.contactDetail.email} </td>
</tr>
<tr>
<td> Date of Birth </td>
<td>
<h:outputText
value="#{contactDetailController.contactDetail.dob}" >
<f:convertDateTime type="date" pattern="dd-MMM-yyyy"/>
</h:outputText>
</td>
</tr>
</table>
<h:form id="editContactDetail"
styleClass="form-horizontal"
p:role="form">
<h:inputHidden value="${contactDetailController.id}" />
<h:commandButton styleClass="btn btn-primary"
action="#{contactDetailController.removeContact()}"
value="Submit" />
 
 
<h:commandButton styleClass="btn btn-default"
action="#{contactDetailController.cancel()}"
immediate="true" value="Cancel"/>
</h:form>
</div>
如果你仔细观察,你会看到显示 ContactDetail 实体属性的 <table> 元素;但是等等,那些 <h:outputText> 元素去哪了?嗯,在 JSF 2 中,你不再需要写 <h:outputText>,只需为 JSF 管理的 Bean 输出内容,你就可以立即直接写入表达式。
因此,人们只需这样写:
<td> #{contactDetailController.contactDetail.title} </td>
而不是:
<td>
<h:outputText
value="#{contactDetailController.contactDetail.title}"/>
</td>
你更喜欢使用以下哪种 preceding authoring contents 进行编程?
然而,日期出生(DOB)是我们将使用 <h:outputText> 元素的领域。<f:convertDateTime> 标签将 java.util.Date 类型格式化为可读格式。模式属性指定日期格式模式。此标签依赖于 java.text.SimpleDateFormat 类。
<h:form> 标签仍然需要,以便允许用户提交表单。它包围了两个 <h:commandButton> 标签。当表单提交时,JSF 将在控制器中调用 removeContact() 方法。
最后,页面还需要之前在章节的 编辑数据 部分提到的 <f:metadata> 标签,以便在页面渲染之前获取联系详情。
使用这个基本的默认为数字的 JSF 示例,我们已经完成了客户的旅程。我们可以使用网页表单创建、检索、更新和删除数据库中的联系详情。这真的很简单。我们还利用了 HTML5 框架,如 Bootstrap,因此我们可以快速将我们的应用程序适应响应式网页设计。
这是 deleteContent.xhtml 视图的截图:

联系详情应用的删除页面视图
在我们关闭这一章之前,我们将简要介绍 JSF 和 CDI 范围。
JSF 和 CDI 范围
在 Java EE 7 之前,关于哪些注解是声明管理 Bean 的正确注解存在一些混淆。问题是 JavaServer Faces 规范早于 CDI 的后续标准,以及范围重叠的事实。范围的历史来自原始设计和对 servlet 容器的定义,以及为应用程序开发者提供便利。范围简单地说是一个名称/值对的映射集合。有助于将它们视为 java.util.Map 类型的哈希映射集合。范围在它们的生存周期上有所不同。
对于 CDI,包名是 javax.enterprise.context,对于 JSF 管理豆子,包名是 javax.faces.bean。
豆子作用域
@RequestScoped 注解表示一个控制器,其生命周期与 Faces 请求和响应的持续时间相同。请求作用域是短暂的。它从网络客户端提交 HTTP 请求开始,然后由 Servlet 容器处理。作用域在将响应发送回客户端时结束。
@SessionScoped 注解表示许多请求和响应的生命周期。会话作用域是为了绕过 HTTP 的无状态协议而设计的。Servlet 容器通过能够存储和检索比一个请求和响应周期更长时间的对象来增强 HTTP 协议。因此,会话作用域是长期存在的。会话作用域可以在超时后过期,或者如果服务重启,可能会变得无效。
@ApplicationScoped 注解表示一个生命周期,只要 web 应用程序运行并可用,它就存在。更重要的,应用程序作用域是跨所有请求、会话、会话和自定义作用域共享的。这个作用域从 web 应用程序启动时开始。它结束于 web 应用程序关闭时。
请求、会话和应用程序作用域是作用域模型的经典版本。JSF 和 CDI 还具有额外的作用域。
@javax.enterprise.context.ConversationScoped 注解表示一个生命周期,其持续时间大于一个或多个请求和响应周期,但短于会话作用域。CDI 定义了一个称为会话作用域的范围。它是请求和会话作用域之间的范围,同时也与它所封装的豆子具有上下文关联。我们将在后面的章节中讨论会话作用域。
JSF 2.0 定义了一个名为 @javax.faces.bean.ViewScoped 的作用域,它与会话作用域相似,因为它也有比请求作用域更长的生命周期。视图作用域从客户端提交 HTTP 请求时开始。它持续存在,直到用户导航到另一个页面。这使得 @ViewScoped 豆子比 @RequestScoped 类型更适合作为管理豆子控制器。@ViewScoped 注解适合管理一个用户故事,正如我们在 CRUD 示例中所看到的。
小贴士
@ViewScoped 注解对于 CDI 豆子不可用。如果你使用的是 JSF 2.2 之前的版本和 Java EE 7,那么这个注解将无法与 @javax.inject.Named 注解的豆子一起使用。你必须使用 @javax.faces.bean.ManagedBean 代替。
在 JSF 2.2 中,有一个@javax.faces.flow.FlowScoped注解,这是一个 CDI 授权的扩展。流程作用域也与会话作用域类似,其生命周期大于请求作用域但小于会话作用域;然而,它是为工作流管理操作设计的。流程作用域允许开发者创建一组具有明确入口和出口点的页面。可以将这个作用域视为适合向导数据输入应用程序。
最后,让我们了解剩下的两个作用域。有一个 POJO 注解,@javax.faces.beanCustomScoped,它允许管理 Bean 在运行时评估值。对于自定义作用域,JSF 实现将委托给实现,因此,任何 EL 表达式都可以根据基于代码的值进行自定义。@javax.faces.bean.NoneScoped注解是一个特殊的作用域,意味着管理 Bean 没有任何作用域。每次引用这些类型的无作用域管理 Bean 时,JSF 都会实例化它们。你可能想知道为什么 JSF 应该允许这种类型的 Bean?在安全上下文或你不想让 Bean 保持状态的情况下,无作用域 Bean 可能很有用。
小贴士
其他 HTML 标签在哪里?
互联网上有许多关于 JSF 旧版本的例子。你可能想知道为什么我们没有看到像h:panel和h:panelGrid这样的标签。这些标签用于布局内容,特别是在 JSF 1.x版本中。这些标签的默认 HTML 实现使用 HTML 表格元素生成内容。现代数字工程师知道使用无处不在的表格元素来构建网站是不推荐的。因此,我选择不使用这些标签来构建我的示例。
摘要
在本章关于 JSF 表单的讨论中,我们探讨了 HTML 和核心 JSF 自定义标签,以构建互联网上最被寻求的答案之一:作为一个数字开发者,我究竟如何编写一个 CRUD 应用程序?这个简单想法被认为编程起来很困难。
我们构建了一个数字 JSF 表单,最初创建了一个联系详情。我们看到了 Facelet 视图、管理 Bean 控制器、有状态的会话 EJB 和实体。我们之所以现代,是因为我们利用了最近的 HTML5 进步,如 Bootstrap CSS 框架。我们使用<h:dataTable>标签显示对象列表集合,这是一个强大且灵活的组件。然后我们添加了编辑和从应用程序中删除联系详情的能力。
在下一章中,我们将详细探讨表单验证,并在 JSF 中混合使用 AJAX 通信。我们已经——某种程度上——在验证领域进行了探索,使用了<f:validateRequired>、<h:messages>和<h:message>。
练习
这些是第三章的问题:
-
HTML5 渲染套件和 JSF 核心自定义标签之间有什么区别?
-
JSF 自定义标签之间有哪些常见的属性是共享的?
-
商业网络应用通常有两种类型:数据提交和案例处理。数据提交只是捕获数据并有一些有限的验证。另一种模式提供你完全控制以输入新记录、修改它们以及通常删除数据。你认为这两种类型的原因是什么?
-
对于面向电子商务应用的商业来说,创建、读取、更新、删除(CRUD)是必不可少的。你在哪里遇到过这些应用?这些应用是否仅限于网络?如果给你第二次机会,你将如何改进这些应用的现状?更好的数字化转型如何帮助这些企业和更重要的是,他们的客户?
-
编写一个简单的 JSF 应用,基本使用 HTML 表单元素
<h:form>和一个命令按钮<h:commandButton>。你的任务是编写一个为热衷于小说的本地爱好读书俱乐部的注册应用。你的参与者必须在线注册后才能参加。从后端 bean(管理控制器)开始。考虑你需要记录的属性。(你的Registration.javaPOJO 将需要联系详情,如姓名、年龄和电子邮件。)在这个阶段,你不需要将任何信息持久化到数据库中,但如果你创建一个图书数据记录(Book.java),其中包含属性title(字符串)、author(字符串)、genre(字符串)、publisher(字符串)和publication year(整数)将会很有帮助。使用 MVC 设计模式编写一个设计规范。 -
在与一个假想的利益相关者的第一次迭代中,你只需要编写一个简单的 JSF 表单。创建一个后端 bean 来捕获书名和作者。你需要
<h:outputLabel>和<h:inputText>。在书的源网站上,你会找到一个空的项目目录,其中包含空的 JSF 占位符页面和 Bootstrap CSS 和 JavaScript 库,如 jQuery 已经设置好。你可以复制并重命名这个文件夹以更快地开始。 -
为了在 JSF 中使用 Bootstrap CSS,我们可以将几乎所有的 JSF HTML 组件应用到
styleClass属性上。其他常见的属性有哪些? -
将爱好读书俱乐部应用添加一些其他组件,例如下拉列表
<h:selectManyMenu>。你需要在后端 bean 中添加属性。(这可能包括书籍的类型,如犯罪、科幻、惊悚或浪漫。)你需要一个 POJO 作为注册者的数据记录(可能Registrant.java类名将很好地为我们服务)。 -
如果你发现一本难以归类于某一类别的罕见图书,会发生什么?你将如何建模这个属性 bean,以及你会使用哪些 JSF HTML 自定义标签?
-
修改你的爱好应用程序,以便使用 JSF HTML 自定义标签的其他元素,例如
<h:selectBooleanCheckbox>。你可能会给一个属性添加一个布尔值,以捕获当小组中的某个人已经审阅了书籍时的状态。 -
<h:selectOneMenu>和<h:selectManyCheckbox>之间有什么区别?解释一下当客户面对<h:selectOneListbox>和<h:selectManyListbox>时会看到什么? -
在现代数字网页设计中,为什么我们应该避免使用
<h:panelGroup>元素来构建网页用户界面? -
为了完成爱好书籍应用程序,我们可能允许注册用户向他们的申请表添加评论。他们想要表达他们的特定专长是什么,这可能是从未来的赛博朋克《博士谁》到古希腊和罗马的历史海军战争。
<h:inputText>和<h:inputTextArea>之间有什么区别?你能用现代 CSS 框架优化这个控件吗? -
当两个客户想要在网页数据库中编辑相同的联系详情记录时会发生什么?你认为应用程序应该如何表现?你会添加哪些功能?你认为客户会对你的想法有何感受?
第四章. JSF 验证和 AJAX
| *"直到完成,这似乎总是不可能的。" | ||
|---|---|---|
| --纳尔逊·曼德拉 |
到目前为止,我们已经创建了一个数字客户旅程,实现了常见的创建、检索、更新和删除,即著名的 CRUD 需求。这些结果对利益相关者和产品所有者都有吸引力,但我们的团队成员的用户对表单不满意,因为它缺乏验证公众成员数据输入的能力。
当我们思考时,验证对用户很重要,因为是他们正在输入网络应用程序中的数据。它节省了用户的时间和挫败感,因为他们知道他们在输入数据时输入是错误的。它避免了数据库管理员因错误提交数据而产生的成本。验证提高了在互联网上 24/7 工作的网络应用程序的效率。随着我们日常生活中的更多活动依赖于传统服务的数字化,电子商务现在成为了一种必需品;我们有必要在正确的时间向公众提供正确的信息,即在销售点或捕获点。
验证方法
在本章中,以基本的 JSF 表单为基础,我们将学习如何在服务器端和客户端应用验证。这两种策略都有一定的优势;我们将了解这两种方法的优缺点。
服务器端验证
在 Java EE 应用程序中,可以在运行在应用服务器或 servlet 容器上的服务器端实现表单验证。信息以正常 HTTP 表单提交的形式从 Web 浏览器发送到 Web 应用程序。在这种模式下,表单作为传统的 HTML 表单元素提交。在这种情况下,Java EE 框架验证输入并向客户端发送响应。如果表单验证失败,包含 Web 表单的页面将被重新显示,并显示错误消息。
服务器端快速验证在安全方面是可靠的,因为它即使在 Web 浏览器中禁用或不可用时也会保护数据库。另一方面,这种验证需要客户端到服务器端的往返。用户在提交表单之前不会得到关于表单数据的反馈。
规则似乎总有例外。如果使用 AJAX 提交服务器端表单验证,那么我们可以绕过缓慢的响应。AJAX 验证是一个很好的折衷方案,因为表单可以在用户在表单上输入数据时进行验证。另一方面,AJAX 需要在 Web 浏览器中启用 JavaScript。
客户端验证
我们团队中的用户体验人员真的很喜欢客户端验证,但这种验证类型需要在浏览器(或等效的动态脚本技术)中存在 JavaScript。客户端验证提供了更响应和丰富的用户与表单的交互。
客户端验证确保在用户被允许提交表单之前,表单始终是正确的。由于 JavaScript 是一种渐进式语言,有许多方法可以通知用户如何更好地与表单提交过程交互。例如 jQuery 这样的技术允许程序员在用户输入数据时实时添加提示和验证错误消息。
在某些情况下,JavaScript 在浏览器中可能被禁用或不可用。我可以想到政府安全部门或专业中心,这些地方沙盒是严格控制的。当用户或设备管理员关闭 JavaScript 时,客户端验证肯定会失败,用户能够绕过验证。
小贴士
结合客户端和服务器端验证
在面向企业的专业应用中,我强烈建议您结合两种验证方法,以获得两者的最佳效果。客户端验证提供了更快、更丰富的体验,而服务器端验证则保护您的数据和数据库免受不良数据和黑客攻击。
在我们讨论验证的技术主题之前,我们必须了解消息在 JSF 中的表示方式。
Faces 消息
JSF 提供了两个自定义标签来显示错误消息。<h:message>标签显示绑定到特定组件的消息。<h:messages>标签显示未绑定到特定组件的消息。
我们在第三章中看到了<h:message>的第一个使用,构建 JSF 表单。该标签通常与表单控件相关联。我们可以使用以下方式向我们的 JSF 页面添加消息:
<h:messages globalOnly="false" styleClass="alert alert-danger" />
标签被添加到内容顶部。属性globalStyle是一个布尔值,它指定标签是否应显示与组件无关的消息。在这里,我们再次使用 Bootstrap CSS 选择器。
以下是一个表格,列出了 JSF 标签<h:messages>和<h:message>之间共享的属性:
| 属性 | 描述 |
|---|---|
Id |
指定唯一标识符 |
errorClass |
指定错误消息的 CSS 类选择器 |
errorStyle |
指定错误消息的样式 |
infoClass |
指定信息消息的 CSS 类选择器 |
infoStyle |
指定信息消息的 CSS 样式 |
for |
指定与消息关联的组件 |
rendered |
设置一个布尔值以指定标签是否渲染到页面 |
style |
定义所有消息类型的 CSS 选择器 |
styleClass |
定义所有消息类型的 CSS 样式 |
在幕后,这些标签分别渲染了javax.faces(HtmlMessages)和javax.faces(HtmlMessages)组件的内容,而这些组件又依赖于javax.faces.application.FacesMessage元素的列表集合。作为一个 JSF 数字开发者,我们不必太担心日常的HtmlMessage和HtmlMessages组件,因为它们位于引擎盖下。如果我们从事编写新的 JSF 渲染器或扩展的工作,那么我们就必须查看 Javadoc 和 JSF 规范。
在第三章中,构建 JSF 表单,你被介绍到FacesMessage应用程序,用于创建 JSF CRUD 风格表单。在控制器中,我们可以创建一个与表单中任何UIComponent无关的验证错误消息。因此,这个验证错误只能通过全局错误消息访问。以下是一个生成此类验证错误的代码示例:
public void findContactById() {
if (id <= 0) {
String message = "Bad request. Please use a link from within the system.";
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null));
return;
}
/* ... */
}
FacesMessage对象代表一个具有严重级别的验证消息。我们将其添加到FacesContext对象中。FacesMessage构造函数的形式如下:
public FacesMessage(Severity severity, String summary, String detail)
严重性可以是FaceMessages类中定义的四个静态常量之一,这些常量分别是SEVERITY_INFO、SEVERITY_WARNING、SEVERITY_ERROR和SEVERITY_FATAL。这些值实际上是私有内部类Severity的实例化,不幸的是,这个类在封装类外部不可访问,因此我们可以发明我们自己的严重性级别。
Faces 消息还需要消息摘要,以及可选的无效消息的详细信息。
javax.faces.context.FacesContext是当前传入请求和潜在响应的聚合持有者。对象实例在初始 JSF 传入请求(Faces 请求)时实例化,并且它将一直存活,直到后续的 JSF release()方法被触发,这通常在框架的深处。FacesContext是添加FacesMessage的地方,也是检索消息列表集合的地方。
FacesContext有几个有趣的方法,包括isValidationFailed(),这个方法在 JSF 生命周期早期检测任何验证失败非常有用。我们将在稍后的 Bean Validation 示例中看到这个调用的例子。还有其他方法,例如使用getViewRoot()获取视图根,getCurrentPhaseId()获取 JSF 生命周期中的当前阶段,以及getRenderKit()检索渲染套件。使用isPostback()方法,我们可以找出请求是否是 HTML 表单,以及 JSF 框架是否即将将数据发送回同一表单。上下文对象还有很多其他功能。
将 faces 消息添加到上下文的方法如下:
public abstract void addMessage(String clientId, FacesMessage message);
如果clientId属性为 null,则消息是全局可用的消息,并且与任何视图组件无关。
现在我们已经了解了如何生成 JSF 特定的消息,让我们深入了解 JSF 应用程序的验证。
验证
在服务器端实现验证主要有两种方式。一种途径是遵循 Java EE 7 规范中 Bean Validation 1.1 版本的使用,另一种传统途径是通过 JSF 验证。
使用 Bean Validation 限制表单内容
Bean Validation 是一种规范,允许开发者对 POJOs 和实体豆进行注解,然后调用自定义验证器实例来验证属性。验证框架与 Java 注解一起工作,因此数字工程师可以明确地说一个属性或甚至一个方法是如何被验证的。
我在 Java EE 7 开发者手册 中专门用了一章来介绍 Bean Validation;尽管如此,我仍将在本数字网络应用程序书中与您一起简要介绍基础知识。Bean Validation 1.1 标准中有几个注解您可以直接使用。然而,如果您的平台允许或您决定添加 Hibernate Validator,那么还有更多有用的验证注解可供使用。开发者还可以创建自定义验证。
让我们再次使用 ContactDetail 实体,但这次我们在属性中添加了 Bean Validation 注解,如下所示:
package uk.co.xenonique.digital;
import javax.persistence.*;
import javax.validation.constraints.*;
import java.util.Date;
@Entity @Table(name="CONTACT")
/* ... */
public class ContactDetail {
@Id /* ... */ private long id;
@NotNull(message = "{contactDetail.title.notNull}")
private String title;
@Size(min = 1, max = 64,
message = "{contactDetail.firstName.size}")
private String firstName;
private String middleName;
@Size(min = 1, max = 64,
message = "{contactDetail.lastName.size}")
private String lastName;
@Pattern(regexp =
"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$",
message = "{contactDetail.email.pattern}")
private String email;
@NotNull( message = "{contactDetail.dob.notNull}")
@Temporal(TemporalType.DATE)
@Past( message="{contactDetail.dob.past}" )
private Date dob;
/* ... as before ... */
}
我们向 ContactDetail 实体的属性中添加了 @Pattern、@Past、@NotNull 和 @Size 注解。这些注解可以在 Java 包 javax.validation.constraints 中找到,为 Bean Validation 保留。
以下是一个重要的 Bean Validation 注解表:
| 约束名称 | 描述 | 允许类型 |
|---|---|---|
@Null |
指定元素必须是一个 null 引用指针。 | 任何 |
@NotNull |
指定元素必须不是一个 null 引用指针。 | 任何 |
@Min |
指定元素必须是一个数值,该数值大于或等于提供的最小值。由于浮点数四舍五入错误,不支持 float 和 double。 | BigDecimal, BigInteger, byte, short, int, 和 long |
@Max |
指定元素必须是一个数值,该数值小于或等于提供的最小值。由于浮点数四舍五入错误,不支持 float 和 double。 | BigDecimal, BigInteger, byte, short, int, 和 long |
@DecimalMin |
与 @Min 类似,但增加了将值设置为字符串参数的能力。数值必须大于或等于提供的值。FP 限制也适用于此处。 |
BigDecimal, BigInteger, CharSequence, byte, short, int, 和 long |
@DecimalMax |
与 @Max 类似,但增加了将值设置为字符串参数的能力。数值必须小于或等于提供的值。FP 限制也适用于此处。 |
BigDecimal, BigInteger, CharSequence, byte, short, int, 和 long |
@Size |
元素的大小必须在提供的包含边界限制内。 | CharSequence, Collection, Map 和原始数组 |
@Past |
元素必须是 Java 虚拟机当前时间的过去日期。 | java.util.Date 和 java.util.Calendar |
@Future |
元素必须是 Java 虚拟机当前时间的未来日期。 | java.util.Date 和 java.util.Calendar |
@Pattern |
元素必须匹配一个符合 Java 习惯的提供的正则表达式模式。 | CharSequence |
Bean Validation 注解通常接受一个消息属性,这是用户的验证消息,或者它可以是括号中的值,这是验证框架从 java.util.ResourceBundle 中搜索消息的触发器。某些注解如 @Min、@Max、@DecimalMin 和 @DecimalMax 有额外的属性如 min 和 max 来指定明显的边界。
我们可以在具有验证消息的属性上定义一个 @NotNull 约束,如下所示:
@NotNull( message = "The office reference must not be null")
private String officeReference;
这是一种好的方法,可能适用于原型设计一个网站;但正如我们从软件考古学知识中了解的那样,这可能会成为一个维护噩梦,因为我们正在将数字副本直接写入 Java 代码中。将文本副本写入属性文件中,该文件可以被标准 ResourceBundle 捕获,这对于 Bean Validation 来说是更好的选择。我们的驻场数字策略师和文案专家珍妮会感谢我们发送给她属性文件而不是 Java 源代码。
因此,让我们将此属性上的约束重写如下:
@NotNull( message = "{mydataform.officeReference.notNull}")
private String officeReference;
通过将消息放置在特定位置,Bean Validation 可以与 JSF 集成。程序员只需在 WEB-INF/classes 文件夹中创建一个 ValidationMessages.properties 文件。
以下是为 ContactDetail 实体消息属性文件的摘录:
contactDetail.title.notNull = Title cannot be blank or empty
contactDetail.firstName.size = First name must be between {min} and {max} characters
contactDetail.middleName.size = Middle name must be between {min} and {max} characters
contactDetail.lastName.size = Last name must be between {min} and {max} characters
contactDetail.email.pattern = You must supply a valid email address
contactDetail.dob.past=Your Date of Birth must be in the past
使用 Bean Validation,我们可以添加用花括号表示的占位符,以丰富用户将看到的消息。占位符是特定的,如 {min} 和 {max}。属性文件的另一个优点是 JDK 中的 ResourceBundle 已经处理了不同区域设置的国际化难题。
仅依赖 JSF 的 Bean Validation 存在很大的缺点。它对于保护数据库免受错误输入数据的影响非常出色,并且由于数字开发者几乎免费获得的 Java EE 7 应用服务器,验证在每条记录之前都会添加或修改到数据库中。然而,Bean Validation 与 JSF 前端没有任何关联。框架与页面作者的內容没有任何关联。在当今的软件工程中,我们也不希望表现层和模型层之间存在这种依赖关系。面向对象编程的最佳实践之一是 SOLID 原则。我们当然希望各层只为一个目的负责;易于扩展,但封闭于修改,最重要的是,防止随着时间的推移导致软件技术债务的抽象泄漏。
仅依赖 Bean Validation 的另一个缺点是,数据的验证完全依赖于 Java 数字工程师的技能。这意味着页面作者或设计师无法创新、编辑或删除验证,以实现更好的以用户为中心的体验。
Bean Validation 对于在应用程序逻辑中添加验证非常出色。如果您有业务需求,可以确保联系详情的标题永远不会为空。可以实现对属性进行复杂和分组验证。有关更多详细信息,请参阅我的书籍《Java EE 7 开发者手册》。
以下截图显示了来自书籍源代码的 bean-validation/createContactDetail.xhtml 的实际应用。截图显示了当用户仅提交未填写的表单时会发生什么:

联系详情应用程序上的 Bean Validation 捕获图
具有设置 globalStyle=true 的 <h:messages> 标签显示了框架在 ContactDetail 实体中发现的验证消息输出。
使用 JSF 验证用户输入
JSF 自 2004 年推出以来就始终拥有一个验证框架。这是将 JSF 与当时没有内置验证支持的 Apache Struts 实际上使用的 Web 框架区分开来的特性。
记住,转换和验证发生在 JavaServer Faces 生命周期的不同阶段(请参阅第二章中的图表,“JavaServer Faces 生命周期”,“执行和渲染生命周期”部分)。作为提醒,JSF 将在“应用请求值”阶段设置组件中的值,然后根据需要使用各种转换将输入字符串值转换为目标对象。验证发生在“处理验证”阶段,并且这个生命周期是按设计顺序进行的。为了将 HTML 请求中的输入数据转换为输入数据,JSF 尝试并检查是否可以在后端 bean 中设置参数。更新模型值阶段跟在早期阶段之后。如果在生命周期中发生验证或转换错误,则它实际上会被缩短。JSF 直接进入“渲染响应”阶段,并将后端 bean 中的属性转换为字符串,以便 Web 客户端可以显示它们。
JSF 提供了一组预构建的验证器标签,您可以将它们应用于标记页面,第三章中给出了核心 JSF 自定义标签表,构建 JSF 表单。以下是一些示例:<f:validateDoubleRange>,<f:validateLength>,<f:validateLongRange>,<f:validateRegex> 和 <f:validateRequired>。
我们可以将这些标签应用于联系详情 CRUD 示例。因此,让我们从 createContact.xhtml 页面开始。以下是页面上的一个简短摘录:
<h:form id="createContactDetail"
styleClass="form-horizontal" p:role="form">
<div class="form-group">
<h:outputLabel for="title" class="col-sm-3 control-label">
Title</h:outputLabel>
<div class="col-sm-9">
<h:selectOneMenu class="form-control"
label="Title" id="title"
value="#{contactDetailControllerBV.contactDetail.title}">
<f:selectItem itemLabel="-" itemValue="" />
<f:selectItem itemValue="Mr" />
<f:selectItem itemValue="Mrs" />
<f:selectItem itemValue="Miss" />
<f:selectItem itemValue="Ms" />
<f:selectItem itemValue="Dr" />
<f:validateRequired/>
</h:selectOneMenu>
<h:message for="title" styleClass="alert validation-error"/>
</div>
</div>
<div class="form-group">
<h:outputLabel for="firstName" class="col-sm-3 control-label">
First name</h:outputLabel>
<div class="col-sm-9">
<h:inputText class="form-control" label="First name"
value="#{contactDetailControllerBV.contactDetail.firstName}"
id="firstName" placeholder="First name">
<f:validateRequired/>
<f:validateLength maximum="64" />
</h:inputText>
<h:message for="firstName" styleClass="alert validation-error"/>
</div>
</div>
<!-- . . . -->
<div class="form-group">
<h:outputLabel for="email" class="col-sm-3 control-label">Email address
</h:outputLabel>
<div class="col-sm-9">
<h:inputText type="email"
label="Email" class="form-control" id="email"
value="#{contactDetailControllerBV.contactDetail.email}"
placeholder="Enter email">
<f:validateRequired/>
<f:validateLength maximum="64" />
</h:inputText>
<h:message for="email" styleClass="alert validation-error"/>
</div>
</div>
<!-- . . . -->
<label class="control-label"> Your Date of Birth</label>
<!-- . . . -->
<div class="row my-group-border">
<div class="col-sm-12">
<h:message for="dobDay" styleClass="alert validation-error"/>
</div>
<div class="col-sm-12">
<h:message for="dobMonth" styleClass="alert validation-error"/>
</div>
<div class="col-sm-12">
<h:message for="dobYear" styleClass="alert validation-error"/>
</div>
</div>
</h:form>
我们将 <f:validateRequired>,<f:validateLength> 和 <f:validateLongRange> 标签放置在 JSF HTML 渲染标签(如 <h:inputText> 和 <h:selectOneMenu>)的正文内容中。validateLength 标签验证字符串属性的长度。该标签接受一个最大参数,但也可以接受一个最小属性。
我们还在其相应的 HTML 输入字段附近添加了 <h:message> 标签。styleClass 属性指定了一个自定义 CSS 选择器,该选择器将验证消息强制显示在单独的新行上。此 CSS 看起来如下所示:
.validation-error {
display: block;
margin: 5px 15px 5px 15px;
padding: 8px 15px 8px 15px;
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
为了避免将 JSF 糖化名称(如 <jsf_form>:<form_property_name>)添加到验证错误消息中,这会导致结果如“contactDetail:title -验证错误:值是必需的”,我们为每个 HTML 渲染标签指定了标签属性。标题输入字段有一个设置 label="Title" 的属性。
<f:validateLongRange> 标签检查字符串的实际内容是否在最小和最大属性之间是一个数值。我们在 出生日期 字段中使用此标签。
以下是从 DOB 组中天字段的编写摘录:
<h:selectOneMenu id="dobDay"
value="#{contactDetailControllerBV.dobDay}"
label="Registration Day">
<f:selectItem itemLabel="----" itemValue=""/>
<f:selectItems
value="#{contactDetailControllerBV.daysOfTheMonth}"
var="day"
itemLabel="#{day}" itemValue="#{day}" />
<f:validateRequired/>
<f:validateLongRange minimum="1" maximum="31" />
</h:selectOneMenu>
前面的代码演示了 <f:validateLongRange> 标签如何强制在表单中执行月份字段。我们对其他出生日期字段重复此操作。
<f:validateRegex> 标签将输入属性字符串与正则表达式匹配。我们使用这个标签来验证 email 属性。以下是对这个验证检查的代码:
<div class="col-sm-9">
<h:inputText type="email"
label="Email" class="form-control" id="email"
value="#{contactDetailControllerBV.contactDetail.email}"
placeholder="Enter email">
<f:validateRequired/>
<f:validateLength maximum="64" />
<f:validateRegex pattern="^[_A-Za-z0-9-\+]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$" />
</h:inputText>
<h:message for="email" styleClass="alert validation-error"/>
</div>
值得注意的是,强制模式属性值,正则表达式,几乎与 Bean Validation @Pattern中的完全相同。我们不得不将双反斜杠字符转换为单反斜杠,因为在正常的正则表达式中我们不需要转义字面量,在 Java 代码中也没有设置。
以下是对页面 jsf-validation/createContactDetail.xhtml 的截图:

展示 JSF 内置验证规则的截图
自定义 JSF 验证
如果你玩过源代码并运行了示例,我敢打赌你会注意到 JSF 验证中的一些明显问题。例如,当 email 字段有一个不是有效电子邮件地址的值时,你会看到一个像这样的验证消息:
Regex pattern of '^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$' not matched
显然,这个非常详细的应用程序消息违反了以用户为中心的设计和清晰的语言。我们能用 JSF 做些什么来避免这些消息?
可以应用于输入字段(如 <h:inputText> 和 <h:selectOneMenu>)的 HTML 渲染标签有三个属性。以下表格显示了可以帮助自定义 JSF 验证消息的属性:
| 属性 | 描述 |
|---|---|
requiredMessage |
定义了一个基于值的表达式,如果字段是必需的,它将被用作消息文本。 |
validatorMessage |
定义了一个基于值的表达式,如果字段和属性验证失败,它将被用作验证文本。 |
conversionMessage |
定义了一个基于值的表达式,如果字段可以转换为目标类型,它将被用作消息。 |
根据这些信息,我们可以通过将 requiredMessage 属性应用于我们的字段来轻松解决消息问题:
<h:inputText type="email"
label="Email" class="form-control" id="email"
value="#{contactDetailControllerBV.contactDetail.email}"
validatorMessage="Value must be in the format of an email address"
converterMessage="Value should be in the format in an email address"
placeholder="Enter email">
<f:validateRequired/>
<f:validateLength maximum="64" />
<f:validateRegex pattern=". . ." />
</h:inputText>
requiredMessage、validatorMessage 和 conversionMessage 会覆盖服务器端 JSF 验证器设置的任何消息。请注意,这些属性可以接受值表达式。这对页面作者来说是一个很好的方法来指定方法。然而,在另一边,我们的电子邮件地址字段有两个验证约束,一个是正则表达式检查,另一个是字段长度约束。对于 validateLength,这个消息并不合适。所以如果我们使用这种方法来使用多个类型的验证器,我们就会遇到问题。
我们还可以采取另一种方法。在 JSF 框架中全局覆盖验证消息怎么样?我们可以配置我们自己的 JSF 验证器消息版本。为了实现这个目标,首先,我们使用有关加载这些消息的位置的信息来配置框架。我们在 WEB-INF/faces-config.xml 文件中设置了一个消息包,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<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">
<application>
<message-bundle>
uk.co.xenonique.digital.JSFVMessages
</message-bundle>
</application>
</faces-config>
此配置文件定义了应用程序的 Faces 资源,在这里我们可以配置一个引用属性文件的消息包。属性文件的路径实际上是 uk/co/xenonique/digital/JSFVMessages.properties,您可以在项目资源 ch04/jsf-crud-validation/src/main/resources 中找到。
其次,我们提供自己的消息包文件。此属性文件 JSFVMessages.properties 的内容仅仅是以下属性定义:
javax.faces.validator.RegexValidator.NOT_MATCHED = Input value does not conform to according expected format.
javax.faces.validator.RegexValidator.PATTERN_NOT_SET = A pattern must be set for validate.
javax.faces.validator.RegexValidator.MATCH_EXCEPTION = The pattern is not a valid regular expression.
javax.faces.validator.LengthValidator.MAXIMUM = {1}: This field can accept up to ''{0}'' characters long.
javax.faces.validator.LengthValidator.MINIMUM = {1}: This field must have at least ''{0}'' characters long.
javax.faces.validator.LongRangeValidator.MAXIMUM = {1}: This value must be less than or equal to ''{0}''
javax.faces.validator.LongRangeValidator.MINIMUM = {1}: This value must be greater than or equal to ''{0}''
javax.faces.validator.LongRangeValidator.NOT_IN_RANGE = {2}: This value must be between of {0} and {1} inclusive.
javax.faces.validator.LongRangeValidator.TYPE = {0}: Unable to convert this value to a decimal number.
javax.faces.component.UIInput.REQUIRED = {0}: This value is required
如您所见,我们已覆盖属性 RegexValidator.NOT_MATCHED 以提供新的消息。原始定义位于应用程序服务器或捆绑包中的 JAR 文件中,作为您的 servlet 容器中的第三方 JAR。这些定义可以在 JAR 包(jsf-api-2.2.jar)的 javax/faces/Messages.properties 文件中找到。
正则表达式验证器的原始定义如下所示:
javax.faces.validator.RegexValidator.NOT_MATCHED = {1}: Validation Error: Value not according to pattern ''{0}''
javax.faces.validator.RegexValidator.PATTERN_NOT_SET = A pattern must be set for validate.
javax.faces.validator.RegexValidator.MATCH_EXCEPTION = The pattern is not a valid regular expression.
javax.faces.validator.LengthValidator.MAXIMUM = {1}: Validation Error: Length is greater than allowable maximum of ''{0}''
javax.faces.validator.LengthValidator.MINIMUM = {1}: Validation Error: Length is less than allowable minimum of ''{0}''
javax.faces.validator.LongRangeValidator.MAXIMUM = {1}: Validation Error: Value is greater than allowable maximum of ''{0}''
javax.faces.validator.LongRangeValidator.MINIMUM = {1}: Validation Error: Value is less than allowable minimum of ''{0}''
javax.faces.validator.LongRangeValidator.NOT_IN_RANGE = {2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}.
javax.faces.validator.LongRangeValidator.TYPE = {0}: Validation Error: Value is not of the correct type.
您可以在源代码中检查此文件,地址为 svn.apache.org/repos/asf/myfaces/core/branches/2.0.x/api/src/main/resources/javax/faces/Messages.properties。如您所见,它们相当技术性且对用户不友好。消息包中的许多属性定义都接受参数的占位符。NOT_MATCHED 接受两个参数:第一个参数 {0} 是模式,第二个参数 {1} 是输入字段的标签。
注意
在 Java EE 7 中,JSF 验证中参数化的占位符与 Bean Validation 框架中的不同。JSF 使用整数索引,而 Bean Validation 可以使用命名占位符。
在编写本文时,JSF 验证器的参考实现中存在一个错误,这阻止了开发人员在使用消息属性中的某些占位符时使用。我们本希望有一个这样的属性定义:
javax.faces.validator.RegexValidator.NOT_MATCHED = Input value {1} does not conform to according expected format.
可惜,当前 Mojarra 中的错误阻止我们将此代码作为生产代码编写。
有一种替代策略可以自定义 JSF 验证。我们可以定义自己的验证器来扩展框架的功能。
自定义验证方法
JSF 允许数字工程师在管理器控制器中配置一个方法,该方法将在验证字段时被调用。将属性验证器添加到 HTML 渲染标签中实现了这一策略,并且它是一个值表达式。
以下是将自定义验证方法添加到联系详情表单的 emailAddress 属性的方法:
<h:inputText type="email"
label="Email" class="form-control" id="email"
value="#{contactDetailControllerBV.contactDetail.email}"
validator="#{contactDetailControllerJV.validateEmailAddress}"
placeholder="Enter email">
<f:validateRequired/>
</h:inputText>
属性验证器引用修改后的 ContactDetailControllerJV 实例中的 validateEmailAddress() 方法。此方法如下所示:
public void validateEmailAddress(
FacesContext context, UIComponent component, Object value) {
String text = value.toString();
if ( text.length() > 64 ) {
throw new ValidatorException(
new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"The value must be less than 64 chars long.", null));
}
final String REGEX =
"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
Pattern pattern = Pattern.compile(REGEX);
Matcher matcher = pattern.matcher(text);
if ( !matcher.matches() ) {
throw new ValidatorException(
new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"The value must be a valid email address.", null));
}
}
在前面的方法 validateEmailAddress() 中,传入的参数是 FacesContext,正在验证的组件类型为 UIComponent,待检查的待定值类型为 Object。此方法验证两个约束:检查字段长度不要太长,并且该字段是电子邮件地址。我们使用 JDK 标准库 javax.regex 包来完成此操作。为了断言验证错误(如果有),我们创建 FacesMessage 对象并将它们添加到当前的 FacesContext 实例中。
定义自定义验证器
在控制器或 CDI 命名 bean 中编写验证器是一种有用的策略。然而,缺点是您始终需要在您的应用程序中有一个间接的 POJO。还有一种策略,JSF 允许我们定义集成在框架内的自定义验证器。开发者可以选择编写一个使用 javax.faces.validator.FacesValidator 注解声明的 POJO。该 POJO 必须实现 javax.faces.validator.Validator 接口。
让我们把电子邮件地址检查代码移入一个自定义验证器。FacesEmailAddressValidator 的代码如下:
package uk.co.xenonique.digital;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.*;
import java.util.regex.*;
@FacesValidator("emailValidator")
public class FacesEmailAddressValidator implements Validator {
public static final String EMAIL_REGEX =
"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
@Override
public void validate(FacesContext context,
UIComponent component, Object value)
throws ValidatorException
{
String text = value.toString();
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher matcher = pattern.matcher(text);
if ( !matcher.matches() ) {
throw new ValidatorException(
new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"The value must be a valid email address.", null));
}
}
}
此类使用 @FacesValidator 注解,单个参数标识页面视图中验证器的名称。validate() 方法实现了验证器接口中的设计约束。JSF 传入 FacesContext、与输入值关联的组件以及值本身。
我们将输入值作为文本字符串检索。验证电子邮件地址的正则表达式代码几乎与之前相同,只是消息键不同。错误键是 {application.emailAddress.pattern}。
在我们的 POJO 自定义验证器就绪后,我们可以重写页面视图上的 HTML 以使用它。以下是从 login.xhtml 的提取视图:
<div class="form-group">
<h:outputLabel for="email" class="col-sm-3 control-label">
Email</h:outputLabel>
<div class="col-sm-6">
<h:inputText class="form-control" label="Email"
value="#{loginControllerJV.email}"
id="email" placeholder="Password"
validator="emailValidator">
<f:validateRequired/>
</h:inputText>
<h:message for="email" styleClass="alert validation-error"/>
</div>
</div>
唯一的区别是 <h:inputText> 元素中的验证器属性。此属性通过名称指定自定义验证器为 emailValidator。正如我们所见,我们可以将自定义验证器与默认标准验证器结合使用。仍然有一个 <f:validateRequired> 元素。
以下截图显示了 LoginControllerJV 的渲染输出:

前端页面视图演示两因素安全登录和验证
验证属性组
现在我们已经了解了 JSF 自定义验证器,我们可以编写一个自定义验证器来验证日期出生输入字段组。我们可以实现这个目标,因为 FacesContext 已经传入。可以单独查找 UI 组件,与上下文分开。
我们将使用页面视图中的 JSF 绑定技术。绑定有效地在 JSF 值中发布了javax.faces.component.UIInput的实例,并在页面的其他地方使其可用。HTML 渲染 JSF 标签上的属性绑定将组件树中的组件引用绑定到作用域变量。以下是与jsf-validation/createContact.xhtml代码提取中 JSF 隐藏输入元素相关的 JSF 代码。特别是,请注意代码提取开始处的 JSF 隐藏输入元素。
<label class="control-label"> Your Date of Birth</label>
<h:inputHidden id="aggregateDobHidden"
label="hiddenField1" value="true">
<f:validator validatorId="dateOfBirthValidator" />
<f:attribute name="dob_dotm" value="#{dob_dotm}" />
<f:attribute name="dob_moty" value="#{dob_moty}" />
<f:attribute name="dob_year" value="#{dob_year}" />
</h:inputHidden>
<div class="row my-group-border">
<div class="col-sm-3">
<label class="control-label"
for="dobDay">Day</label>
<div class="controls">
<h:selectOneMenu id="dobDay"
value="#{contactDetailControllerJV.dobDay}"
binding="#{dob_dotm}"
label="Registration Day">
<f:selectItem itemLabel="----" itemValue=""/>
<f:selectItems
value="#{contactDetailControllerJV.daysOfTheMonth}" var="day"
itemLabel="#{day}" itemValue="#{day}" />
<f:validateRequired/>
<f:validateLongRange
minimum="1" maximum="31" />
</h:selectOneMenu>
</div>
</div>
<div class="col-sm-3">
...
<h:selectOneMenu id="dobMonth"
value="#{contactDetailControllerJV.dobMonth}"
binding="#{dob_moty}"
label="Registration Month">
...
</h:selectOneMenu>
</div>
<div class="col-sm-3">
...
<h:inputText id="dobYear"
value="#{contactDetailControllerJV.dobYear}"
binding="#{dob_year}"
label="Registration Year">
...
</h:inputText>
</div>
</div>
...
<div class="col-sm-12">
<h:message for="aggregateDobHidden"
styleClass="alert validation-error"/>
</div>
</div>
我们利用一个标识为aggregateDobHidden的隐藏字段,具有一个虚拟的表单参数名称hiddenField1。它始终发送一个 true 值。<f:attribute>元素向这个 UI 组件附加额外的绑定信息。我们需要三个具有名称dob_dotm(月份中的日)、dob_moty(年份中的月)和dob_year的属性。这些属性是相应命名的页面作用域变量#{dob_dotm}、#{dob_moty}和#{dob_year}的值表达式。
我们为每个 JSF 选择组件添加一个绑定属性。再次看看以下第一个字段:
<h:selectOneMenu id="dobDay"
value="#{contactDetailControllerJV.dobDay}"
binding="#{dob_dotm}"
属性绑定将组件与视图关联,并使其在由字面字符串定义的页面作用域变量#{dob_dotm}中可用。这是一个javax.faces.component.UIInput类的实例,它有一个getSubmittedValue()方法来获取提交的值。我们重复添加其他两个属性的绑定。在表单提交期间,hiddenField1记录了每个单独属性的绑定值。这个属性与单独的日、月和年属性不同。
这个绑定技巧允许我们将属性组合在一起进行表单验证。以下源代码显示了服务器端的验证:
package uk.co.xenonique.digital;
import javax.faces.component.*;
import javax.faces.context.FacesContext;
import javax.faces.validator.*;
import java.util.*;
@FacesValidator("dateOfBirthValidator")
public class FacesDateOfBirthValidator implements Validator {
@Override
public void validate(FacesContext context,
UIComponent component, Object value)
throws ValidatorException {
UIInput dayComp = (UIInput)
component.getAttributes().get("dob_dotm");
UIInput monthComp = (UIInput)
component.getAttributes().get("dob_moty");
UIInput yearComp = (UIInput)
component.getAttributes().get("dob_year");
List<FacesMessage> errors = new ArrayList<>();
int day = parsePositiveInteger(
dayComp.getSubmittedValue());
if ( day < 1 || day > 31 ) {
errors.add(new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"DOB day must be in the range of 1 to 31 ", null));
}
int month = parsePositiveInteger(
monthComp.getSubmittedValue());
if ( month < 1 || month > 12 ) {
errors.add(new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"DOB month must be in the range of 1 to 12 ", null));
}
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.YEAR, -18);
Date eighteenBirthday = cal.getTime();
cal.setTime(new Date());
cal.add(Calendar.YEAR, -100);
Date hundredthBirthday = cal.getTime();
int year = parsePositiveInteger(
yearComp.getSubmittedValue());
cal.set(year,month,day);
Date targetDate = cal.getTime();
if (targetDate.after(eighteenBirthday) ) {
errors.add(new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"DOB year: you must be 18 years old.", null));
}
if ( targetDate.before(hundredthBirthday)) {
errors.add(new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"DOB: you must be younger than 100 years old.", null ));
}
if ( !errors.isEmpty()) {
throw new ValidatorException(errors);
}
}
public int parsePositiveInteger( Object value ) { /*...*/ }
}
POJO FacesDateOfBirthValidator 验证三个出生日期属性。它使用 JSF 页面视图中的一个技术,称为绑定,我们将在下一分钟看到。绑定允许 JSF 输入属性传播到另一个命名作用域变量,可以在页面的其他地方使用。至于验证器,我们使用 HTML 隐藏元素作为检索这些绑定值的载体。这就是将组件转换为javax.faces.component.UIInput并提取值的目的。
UIInput dayComp = (UIInput)
component.getAttributes().get("dob_dotm");
int day = parsePositiveInteger(
dayComp.getSubmittedValue());
我们有一个辅助方法parsePositiveInteger(),用于将文本值转换为整数。在此之前,我们创建一个列表集合来存储任何错误消息。然后我们验证月份的边界从 1 到 31。对于月份属性,逻辑几乎相同。
对于年份属性,我们采取不同的步骤。使用 JDK Calendar 和 Date 类,我们构建两个 Date 实例:一个代表 100 年前确切的当前日期,另一个代表 18 年前的当前日期。然后我们可以比较用户的输入日期是否落在这两个生日限制之间。
如果在validate()方法的末尾有任何错误,则它将引发一个带有错误集合的ValidatorException异常。请注意,我们选择使用替代构造函数。
为了完成验证器,辅助方法parsePositiveInteger()可以编写如下:
public int parsePositiveInteger( Object value ) {
if ( value == null ) return -1;
try {
return Integer.parseInt( value.toString().trim());
}
catch (NumberFormatException nfe) {
return -1;
}
}
以下是一个截图,展示了正在使用的群组验证器的联系详细信息:

生日验证器的截图
转换器
标准的 JSF 验证器允许数字开发者实现很多功能。在某些情况下,需求会超出默认行为。转换器是 JSF 类,用于在字符串和对象之间进行转换。类似于注解定义自定义验证器的方式,JSF 允许注册自定义转换器。转换器与一个 JSF 组件相关联。
注解@java.faces.convert.FacesConverter表示一个 POJO 是一个自定义 JSF 转换器。此类型必须实现javax.faces.convert.Converter接口,该接口具有以下方法:
public Object getAsObject( FacesContext context,
UIComponent component, String newValue);
public String getAsString( FacesContext context,
UIComponent component, Object value);
getAsObject()方法将客户端的字符串表示转换为目标对象。另一个方法getAsString()将对象转换为字符串表示,该表示在客户端浏览器中渲染。
我们将用一个将字符串转换为扑克牌花色的自定义 JSF 转换器来举例。我们可以使用简单的 Java 枚举类来编写这个转换器:
public enum FrenchSuit {
HEARTS, DIAMONDS, CLUBS, SPADES
}
以下是为自定义转换器FrenchSuitConverter类提供的完整列表:
package uk.co.xenonique.digital;
import javax.faces.application.FacesMessage;
import javax.faces.component.*;
import javax.faces.context.FacesContext;
import javax.faces.convert.*;
import static uk.co.xenonique.digital.FrenchSuit.*;
@FacesConverter("frenchSuitConverter")
public class FrenchSuitConverter implements Converter {
@Override
public Object getAsObject(FacesContext context,
UIComponent component, String value) {
String text = value.trim();
if ( text.length() == 0 ) {
text = ((UIInput)component).getSubmittedValue().toString();
}
text = text.toUpperCase();
switch (text) {
case "HEARTS": return HEARTS;
case "DIAMONDS": return DIAMONDS;
case "CLUBS": return CLUBS;
case "SPADES": return SPADES;
default:
throw new ConverterException(
new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"Unable to convert object to string", null));
}
}
@Override
public String getAsString(FacesContext context,
UIComponent component, Object value) {
if ( value instanceof String ) {
return value.toString();
}
else if ( !(value instanceof FrenchSuit)) {
throw new ConverterException(
new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"Unable to convert object to string", null));
}
switch ((FrenchSuit)value) {
case HEARTS: return "Hearts";
case DIAMONDS: return "Diamonds";
case CLUBS: return "Clubs";
case SPADES: return "Spades";
}
throw new IllegalStateException(
"PC should never reach here!");
}
}
POJO 是注解了@FacesConverter的,其值成为页面视图中的标识符。
JSF 使用文本表示调用getAsObject()方法,该文本表示被修剪并转换为大写,以便于比较。在方法开始时,新值可能是空字符串。如果是这样,那么我们从已提交的值中检索文本表示。对于这个特定的转换器,空值的用例是可能的,因此我们添加了保护措施。如果过程中出现任何问题,该方法将引发异常,javax.faces.convert.ConverterException。
JSF 调用getAsString()方法将对象表示转换为字符串。根据对象类型,该方法针对不同类型的输入进行防御。输入值可能只是一个字符串,或者它可能是FrenchSuit枚举的一个实例。如果输入值不是这些之一,该方法将引发ConverterException。
在现实世界中,我们知道一副扑克牌中始终有四种花色,因此我们可以相当有信心地保证枚举的可维护性。作为一个数字开发者,你可能不会有这样的奢侈,因此,在转换器和验证器中应用防御性编程原则可以大大有助于追踪错误。
以下是从页面/jsf-validation/french-suit.xhtml中摘录的代码,它练习了自定义转换器:
<h:form id="cardForm"
styleClass="form-horizontal"
p:role="form">
...
<div class="form-group">
<h:outputLabel for="suit" class="col-sm-3 control-label">
Card Suit</h:outputLabel>
<div class="col-sm-9">
<h:selectOneMenu class="form-control"
label="Suit" id="suit"
value="#{frenchSuitController.suit}" >
<f:converter converterId="frenchSuitConverter" />
<f:selectItem itemLabel="-" itemValue="" />
<f:selectItem itemValue="#{frenchSuitController.suitEnumValue('HEARTS')}" />
<f:selectItem itemValue="#{frenchSuitController.suitEnumValue('CLUBS')}" />
<f:selectItem itemValue="#{frenchSuitController.suitEnumValue('DIAMONDS')}" />
<f:selectItem itemValue="#{frenchSuitController.suitEnumValue('SPADES')}" />
<f:validateRequired/>
</h:selectOneMenu>
<h:message for="suit" styleClass="alert validation-error"/>
</div>
</div>
<h:commandButton styleClass="btn btn-primary"
action="#{frenchSuitController.doAction()}"
value="Submit" />
...
</h:form>
在前面的视图中,我们使用了一个下拉菜单<h:selectOneMenu>,允许用户选择一张牌的花色。现在代码应该对你来说非常熟悉了。区别在于每个牌的花色的值表达式,它们都是带有字符串字面量参数的方法调用。表达式语言允许你带有参数调用方法。因此,表达式#{frenchSuitController.suitEnumValue('HEARTS')}等价于对控制器的一个方法调用。
在<h:selectOneMenu>的体内容中,我们通过一个标识符显式引用自定义转换器,并通过以下方式将其与 UI 组件关联:
<f:converter converterId="frenchSuitConverter" />
然后,JSF 调用自定义转换器,将页面视图中的单个FrenchSuit枚举转换为字符串。这听起来像是一种绕弯子展示值列表的方法,但这个例子展示了FrenchSuitConverter中的getAsString()方法被调用。此外,它还说明了如何在页面视图和控制器中以健壮的方式引用 Java 枚举。
现在我们来检查控制器:
package uk.co.xenonique.digital;
import javax.faces.context.Flash;
import javax.faces.context.FacesContext;
import javax.faces.view.ViewScoped;
import javax.inject.Named;
@Named("frenchSuitController")
@ViewScoped
public class FrenchSuitController {
private String firstName;
private String lastName;
private FrenchSuit suit;
public String doAction() {
Flash flash = FacesContext.getCurrentInstance().
getExternalContext().getFlash();
flash.put("firstName",firstName);
flash.put("lastName",lastName);
flash.put("suit", suit);
return "/jsf-validation/french-suit-complete?redirect=true";
}
public String cancel() {
return "/index.xhtml?redirect=true";
}
public FrenchSuit suitEnumValue( String name ) {
return FrenchSuit.valueOf(name);
}
// Getters and setters omitted
}
在这段代码中,我们对FrenchSuitController进行了稍微的跳进。首先,让我将你的注意力引到suitEnumValue()方法上,它将一个字符串字面量转换为枚举类型FrenchSuit。这是一个在页面视图中获取枚举的好用技巧,因为表达式语言不允许直接访问 Java 枚举。它特别适用于随不同项目版本更新而改变的枚举。
doAction()和cancel()方法返回带有特殊查询参数redirect=true的 URI。这是对 JSF 返回可书签 URL 的指令;我们将在本章的后面部分详细讨论这个主题。
在doAction()方法中,我们首次使用了 JSF 中的 Flash 作用域。Flash 作用域是一个临时上下文,允许控制器将数据传递给下一个导航视图。请记住,视图作用域仅对导航到同一页面视图的当前控制器有效。当FacesContext移动到下一个页面视图时,@ViewScoped管理 Bean 将超出作用域。这些方法在javax.faces.context.Flash实例中设置键值关联。
最后一个拼图展示了我们如何在页面视图中使用 Flash 作用域。这段代码可以在文件/jsf-validation/french-suit-complete.xhtml中找到。以下代码是同一文件的摘录:
<ui:define name="mainContent">
<h1> House of Card with JSF Validation</h1>
...
<div class="jumbotron">
<h1> Complete </h1>
<p>
Terrific! You completed the French suit action.
Your first name is <b>#{flash['firstName']}</b> and
your last name is <b>#{flash['lastName']}</b> and
you chose <b>#{flash['suit']}</b> as the playcard suit.
</p>
</div>
...
</ui:define>
在此页面视图中,我们使用映射表达式从 Flash 作用域中检索值。表达式#{flash['suit']}是用户所选的牌型。读者还被指引查看默认 JSF 转换器javax.faces.convert.EnumConverter的文档。在同一个包中,还有其他标准转换器,例如BigDecimalConverter、BigIntegerConverter、DateTimeConverter、ByteConverter、LongConverter和DoubleConverter。
我将把french-suit.xhtml表单视图的截图留给你:

JSF 转换器的卡片套件截图
以下是一个截图,显示了french-suit-complete.xhtml的最终状态。标记显示从 Bootstrap 的 CSS jumbotron 风格中获得的良好视觉效果。

卡片屋示例提交和验证后的第二屏
我们已经介绍了在服务器端发生的许多验证。现在让我们继续讨论 AJAX 验证。
使用 AJAX 立即验证
异步 JavaScript 和 XML(AJAX)是一组技术,它们共同解决了检索网页部分更新的限制,并提供了一个丰富的交互式用户体验。AJAX 的关键是术语异步,它基于一个万维网联盟(W3C)标准,即XmlHttpRequest对象。它在 2006 年的 Internet Explorer 中引入,现在所有现代网络浏览器都支持这个对象。异步模式允许浏览器在单独的连接上向服务器发送数据传输请求;企业后端响应以数据结果响应,通常是 JSON 或 XML。与每次重新加载整个页面相比,这些 AJAX 数据传输通常要小得多。
JSF 内置了对 AJAX 请求和响应的支持;开发者不需要了解XmlHttpRequest和 JavaScript 编程的细节,就可以获得即时响应的好处。数字开发者可以从执行 AJAX 交互的默认 JavaScript 库开始。
使用<f:ajax>标签在 JSF 中开始 AJAX 非常简单。这个核心 JSF 标签将 AJAX 行为与 UI 组件注册,并用于对字段进行验证。开发者只需将标签放置在代表需要验证的组件的 HTML JSF 标签的正文内容中即可。
以下代码展示了如何在与联系详情应用程序一起使用此标签:
<f:ajax event="blur" render="firstNameError"/>
tag 属性的事件确定框架何时调用 AJAX 验证。blur 值表示当用户从该组件字段移动到下一个输入字段时发生。因此,当用户在台式计算机上按下 Tab 键或在手机或平板电脑上导航 UI 时,验证会立即发生,因为 JavaScript 会向服务器发送 AJAX 请求。第二个属性,render,通知框架如果有的话,将错误消息渲染到特定的 UI 组件中。JSF 接收到 AJAX 响应,如果有错误,它知道要更新验证消息的 HTML 组件 ID。
让我们看看项目 ch04/jsf-crud-ajax-validation,这是页面视图 jsf-validation/createContactDetail.xhtml 的完整提取:
<h:form id="createContactDetail"
styleClass="form-horizontal"
p:role="form">
<div class="form-group">
<h:outputLabel for="title" class="col-sm-3 control-label">
Title</h:outputLabel>
<div class="col-sm-9">
<h:selectOneMenu class="form-control"
label="Title" id="title"
value="#{contactDetailControllerJV.contactDetail.title}">
<f:selectItem itemLabel="-" itemValue="" />
<f:selectItem itemValue="Mr" />
<f:selectItem itemValue="Mrs" />
<f:selectItem itemValue="Miss" />
<f:selectItem itemValue="Ms" />
<f:selectItem itemValue="Dr" />
<f:validateRequired/>
<f:ajax event="blur" render="titleError"/>
</h:selectOneMenu>
<h:message id="titleError"
for="title" styleClass="alert validation-error"/>
</div>
</div>
<div class="form-group">
<h:outputLabel for="firstName" class="col-sm-3 control-label">
First name</h:outputLabel>
<div class="col-sm-9">
<h:inputText class="form-control" label="First name"
value="#{contactDetailControllerJV.contactDetail.firstName}"
id="firstName" placeholder="First name">
<f:validateRequired/>
<f:validateLength maximum="64" />
<f:ajax event="blur" render="firstNameError"/>
</h:inputText>
<h:message id="firstNameError"
for="firstName" styleClass="alert validation-error"/>
</div>
</div>
<div class="form-group">
<h:outputLabel for="middleName" class="col-sm-3 control-label">
Middle name</h:outputLabel>
...
</div>
<div class="form-group">
<h:outputLabel for="lastName" class="col-sm-3 control-label">
Last name</h:outputLabel>
<div class="col-sm-9">
<h:inputText class="form-control"
value="#{contactDetailControllerJV.contactDetail.lastName}"
label="Last name"
id="lastName" placeholder="Last name">
<f:validateRequired/>
<f:validateLength maximum="64" />
<f:ajax event="blur" render="lastNameError"/>
</h:inputText>
<h:message id="lastNameError"
for="lastName" styleClass="alert validation-error"/>
</div>
</div>
...
</h:form>
此页面视图表明在 JSF 中向页面添加 AJAX 验证非常简单。<f:ajax> 核心 JSF 标签嵌入到相应的 HTML JSF 标签中,如您在姓名和姓氏字段中看到的那样。非 AJAX 和 AJAX 页面之间的另一个区别是为 <h:message> 标签添加了标识符,例如 firstNameError 和 lastNameError。我们需要添加 HTML 标识符元素,以便 JavaScript 可以从浏览器中的 文档对象模型 (DOM) 通过 ID 查找 HTML 元素。
页面除了中间名和新闻通讯 HTML 复选框字段外,所有属性都添加了 AJAX 验证。AJAX 验证也适用于自定义验证器和转换器。
以下截图展示了单属性 AJAX 验证的示例:

展示每个输入字段单独验证的截图
验证输入字段组
到目前为止,我们已经看到了 JSF AJAX 验证在单个输入字段实例上的应用。<f:ajax> 标签也可以与组件组的验证一起使用。我们可以将标签包围在一个或多个 JSF 输入字段周围,然后 <f:ajax> 标签成为 UI 组件的父级。这导致 JSF 将 AJAX 验证应用于多个组件。
让我们在联系详情表单的出生日期字段中添加以下页面视图的组验证:
<h:inputHidden id="aggregateDobHidden"
label="hiddenField1" value="true">
<f:validator validatorId="dateOfBirthValidator" />
<f:attribute name="dob_dotm" value="#{dob_dotm}" />
<f:attribute name="dob_moty" value="#{dob_moty}" />
<f:attribute name="dob_year" value="#{dob_year}" />
</h:inputHidden>
<f:ajax event="blur" render="dobDayError dobMonthError dobYearError">
<div class="row my-group-border">
<div class="col-sm-3">
<label class="control-label" for="dobDay">Day</label>
<div class="controls">
<h:selectOneMenu id="dobDay" value="#{contactDetailControllerJV.dobDay}"
binding="#{dob_dotm}"
label="Registration Day">
...
</h:selectOneMenu>
</div>
</div>
<div class="col-sm-3">
<label class="control-label" for="dobMonth">Month</label>
<div class="controls">
<h:selectOneMenu id="dobMonth" value="#{contactDetailControllerJV.dobMonth}"
binding="#{dob_moty}"
label="Registration Month">
...
</h:selectOneMenu>
</div>
</div>
<div class="col-sm-3">
<label class="control-label" for="dobYear">Year</label>
<div class="controls">
...
</div>
</div>
<div class="col-sm-12">
<h:message id="dobDayError"
for="dobDay" styleClass="alert validation-error"/>
</div>
<div class="col-sm-12">
<h:message id="dobMonthError"
for="dobMonth" styleClass="alert validation-error"/>
</div>
<div class="col-sm-12">
<h:message id="dobYearError"
for="dobYear" styleClass="alert validation-error"/>
</div>
<div class="col-sm-12">
<h:messages for="aggregateDobHidden"
styleClass="alert validation-error"/>
</div>
</div>
</f:ajax>
如您所见,我们用包含的 <f:ajax> 标签包围了 DOB 输入字段。事件属性仍然设置为 blur。渲染属性设置为特定验证消息的 HTML 元素 ID 列表,即 dobDayError、dobMonthError 和 dobYearError。
aggregationDobHidden HTML 隐藏元素与非 AJAX 示例中保持一致,以便说明验证不会干扰自定义验证。
总结一下,使用 <f:ajaxTag> 并将其嵌入任何 JSF 组件内部。要验证多个组件组,请将组件用 <f:ajaxTag> 包围起来。
以下截图展示了围绕出生日期字段的多个组件 AJAX 验证。年份组件最后获得了浏览器的焦点,因此相应的验证消息描述了 onblur DOM JavaScript 事件。同样,在一系列字段之间按 Tab 键会逐个显示错误消息。

以下是出生日期输入字段的分组验证截图
AJAX 自定义标签深入探讨
了解可以应用于此 Core JSF 自定义标签的属性很有用。以下表格描述了 <f:ajax> 的属性。
| 属性 | 描述 |
|---|---|
delay |
指定在向服务器发送多个 AJAX 请求之间的延迟(以毫秒为单位)。请求由 JSF 实现排队。将值设置为 none 禁用此功能。 |
disabled |
指定一个布尔值以指示标签状态。如果设置为 true,则 AJAX 行为不会渲染。默认值是 false。 |
event |
定义一个表示 AJAX 动作事件类型的字符串枚举。默认情况下,JSF 确定组件的事件名称。 |
execute |
列出代表在服务器上执行的组件的空格分隔名称集合。值可以是字符串或值表达式。默认值是 @this,表示 AJAX 行为的父组件。 |
immediate |
声明一个布尔值,指示输入值是否在 JSF 生命周期早期处理。 |
listener |
指示在广播事件期间将被调用的监听器方法的名称,即 AjaxBehaviorEvent。 |
onerror |
指定一个 JavaScript 函数的名称,该函数将接受错误。 |
onevent |
指定一个 JavaScript 函数的名称,该函数将处理 UI 事件。 |
render |
列出在 AJAX 行为完成后将在客户端渲染的 UI 组件集合。此值可以是组件标识符的空格分隔集合,也可以是值表达式。默认值是 @none,表示不渲染任何组件。 |
从前面的表中,您会注意到执行和渲染属性可能指示额外的有意义的值。执行属性规定要在服务器上执行的组件。渲染属性确定在 AJAX 行为完成后受影响的 UI 组件。以下表格列出了属性值:
| 值 | 描述 |
|---|---|
@all |
指定在视图中执行或渲染所有组件。 |
@form |
指定仅执行或渲染表单的子组件。 |
@none |
指定不执行或渲染任何组件。 |
@this |
指定仅执行或渲染触发 AJAX 请求的当前组件。 |
组件标识列表 |
列出了显式执行或作为 AJAX 请求渲染的 UI 组件的标识符。 |
表达式语言 |
指定一个值表达式,该表达式最终返回一个字符串集合,表示执行或作为 AJAX 请求-响应渲染的 UI 组件。 |
一个部分 JSF 生命周期
JSF 生命周期实际上适用于所有 Faces 请求和响应,包括来自启用 AJAX 的组件产生的请求和响应。在幕后,JSF 为 AJAX 请求和响应实例化一个特殊对象,javax.faces.context.PartialViewContext,并将其加入到处理生命周期中。此上下文对象包含允许 JSF 在服务器端更新组件模型的信息。基于部分上下文,JSF 决定是否完成所选 UI 组件的部分处理和/或 UI 组件的部分渲染。部分处理对应于生命周期的 Apply-Requests-Values、Process-Validations 和 Update-Model-Values 阶段。部分渲染指的是 Render-Response 阶段。

部分请求-响应生命周期用于 AJAX 提交
上述图表封装了我们对于 JSF 生命周期中 AJAX 请求和响应的部分上下文的理解。
处理视图
在本章中,我们主要检查了使用 JSF 验证用户输入。我们简要地提到了一些关于导航的杂项概念。现在让我们来谈谈处理视图和导航。
调用控制器方法
有几种方法可以从页面视图调用控制器并传递参数。在许多数字电子商务应用的情况中,开发者需要检索特定的数据记录、触发服务器端操作或在后端从客户端保存某种状态。
参数化方法调用
JSF 允许开发者使用表达式语言将参数传递到页面视图中的方法。在 第三章 中给出的第一个特性,构建 JSF 表单 被称为方法表达式调用,它在 JSF 2.0 中引入。
下面的内容是从页面视图 /jsf-miscellany/examplar-methods.xhtml 的摘录:
<h:form id="methodExampler"
styleClass="form-horizontal"
p:role="form">
...
<div class="form-group">
<div class="col-sm-9">
<p>
Invoke JSF controller with 3 literal arguments
</p>
<p class="monospace">
\#{examplarController.methodThreeArgs(
'Obiwan','Ben','Kenobi')}
</p>
<h:commandButton styleClass="btn btn-primary"
action="#{examplarController.methodThreeArgs(
'Obiwan','Ben','Kenobi')}"
value="Invoke" />
</div>
</div>
...
</h:form>
上述代码描述了具有操作值表达式的 <h:commandButton> 标签,该表达式是 #{examplarController.methodThreeArgs('Obiwan','Ben','Kenobi')}。这是一个带有三个字面字符串参数的方法调用。
参数也可以是其他 JSF 范围实例的引用。以下是一个只有两个参数的另一个调用示例,展示了这一点:
<h:commandButton
styleClass="btn btn-primary"
action="#{examplarController.methodTwoArgs(
examplarController.city, examplarController.country)}"
value="Invoke" />
参数是从控制器 bean 属性动态设置的。现在让我们看看控制器 ExamplarController:
@Named("examplarController") @ViewScoped
public class ExamplarController {
private String city = "London";
private String country="United Kingdom";
public String methodOneArg( String alpha ) {
Flash flash = FacesContext.getCurrentInstance().
getExternalContext().getFlash();
flash.put("result",
String.format("executed methodOneArg(\"%s\")",
alpha ));
return "examplar-methods-complete?redirect=true";
}
public String methodTwoArgs(
String alpha, String beta ) {
Flash flash = FacesContext.getCurrentInstance().
getExternalContext().getFlash();
flash.put("result",
String.format("executed methodTwoArgs(\"%s\", \"%s\")",
alpha, beta ));
return "examplar-methods-complete?redirect=true";
}
public String methodThreeArgs(
String alpha, String beta, String gamma ) {
Flash flash = FacesContext.getCurrentInstance().
getExternalContext().getFlash();
flash.put("result",
String.format("executed methodThreeArgs(\"%s\", \"%s\", \"%s\")",
alpha, beta, gamma ));
return "examplar-methods-complete?redirect=true";
}
...
// Getters and setters omitted
}
有三种方法被调用,分别是methodOneArg()、methodTwoArgs()和methodThreeArgs()。这些名称对于可以传递的参数数量是自解释的;每个方法在移动到下一个页面视图/jsf-miscellany/examplar-methods-complete.xhtml之前,都会在 JSF Flash 作用域中保存一个输出结果。
以下是从最终状态 Facelet 视图中提取的内容,exemplar-methods-complete.xhtml:
<ui:composition template="/basic_layout.xhtml">
<ui:define name="title">
<title> JSF Method Invocation Example </title>
</ui:define>
<ui:define name="mainContent">
<h1> Method Invocations Complete</h1>
<h:messages globalOnly="true"
styleClass="alert alert-danger" />
<div class="jumbotron">
<h1> Complete </h1>
<p>
Terrific! You completed the action.
The result message was <b>#{flash['result']}</b>.
</p>
</div>
...
</ui:define> <!--name="mainContent" -->
</ui:composition>
向控制器传递参数
在 JSF 2.0 规范创建之前,可以通过在<h:commandLink>、<h:commandButton>或<h:link>标签的体内容中使用<f:param>标签将参数发送到后端 bean 控制器。尽管这种技术在 JSF 2.0 中被方法调用表达式所取代,但它仍然是一种向控制器发送超出范围通信的有用技术。
以下代码显示了配方,我们在<h:commandButton>自定义标签中嵌入两个<f:param>元素:
<div class="form-group">
<div class="col-sm-9">
<p>
Invoke JSF controller method with parameters
</p>
<p class="monospace">
\#{examplarController.methodPassingAttribute()
<br/>
name="callToActionText" value="FindNearestDealer"
<br/>
name="customerType" value="Motorbikes"
</p>
<h:commandButton styleClass="btn btn-primary"
action="#{examplarController.methodPassingParameters()}"
value="Invoke" >
<f:param name="callToActionText"
value="FindNearestDealer"/>
<f:param name="customerType"
value="Motorbikes"/>
</h:commandButton>
</div>
</div>
此页面视图上的编写调用控制器的不带参数方法methodPassingParameters()。JSF 通过带有键名callToActionText和customerType的 Faces 请求将两个参数传递给目标方法。
让我们看看处理此调用的控制器方法:
public String methodPassingParameters() {
Map<String,String> params = FacesContext.getCurrentInstance()
.getExternalContext().getRequestParameterMap();
String ctaText = params.get("callToActionText");
String custType = params.get("customerType");
Flash flash = FacesContext.getCurrentInstance().
getExternalContext().getFlash();
flash.put("result",
String.format("executed methodPassingParameters() " +
"ctaText=\"%s\", custType=%s", ctaText, custType ));
return "examplar-methods-complete?redirect=true";
}
在methodPassingParameters()方法内部,我们通过嵌套调用getRequestParameterMap()从FacesContext实例中检索参数。然后,从类型为Map<String,String>的映射集合中访问参数变得非常直接。值得注意的是,参数只能是字符串,并且这种技术可以与 JSF 2.0 及以后版本中的方法参数调用相结合。
以下截图显示了页面,以演示本节中描述的方法调用技术:

方法调用 JSF 示例的截图
调用动作事件监听器
处理视图的最终技术是在控制器中调用动作监听器。任何接受单个javax.faces.event.ActionEvent参数的实例方法都可以是动作事件监听器。动作监听器与页面标记中的 UI 组件相关联。JSF 在调用动作之前调用动作监听器,因此这种技术有助于挂钩业务逻辑并为动作调用设置数据。
以下是从实现此技术的调用方法页面中提取的内容。在此代码中,我们将省略 Bootstrap CSS 标记:
<h:commandButton styleClass="btn btn-primary"
action="#{examplarController.performAction}"
actionListener="#{examplarController.attributeListener}"
value="Invoke">
<f:attribute name="contactName" value="Roy Keane" />
</h:commandButton>
<h:commandButton>标签有一个额外的actionListener属性,设置为引用动作监听器方法的表达式,即attributeListener()。该标签还嵌入了一个<f:attribute>来定义传递的属性。动作属性引用performAction()方法。
让我们检查我们的ExamplarController后端 bean 以查看代码:
private String contactName;
public void attributeListener(ActionEvent event){
contactName = (String) event.getComponent()
.getAttributes().get("contactName");
}
public String performAction() {
Flash flash = FacesContext.getCurrentInstance()
.getExternalContext().getFlash();
flash.put("result",
String.format("executed performAction()
contactName=\"%s\" ", contactName ));
return "examplar-methods-complete?redirect=true";
}
在提交命令按钮后,JSF 首先使用一个ActionEvent实例调用attributeListener()方法。我们可以找到负责调用的组件,并检索其上存储的属性。在这种情况下,我们检索键入为contactName的属性值。这个值存储在控制器的实例变量中。(如果我们把后端 bean 的作用域设置为@RequestScope或@ViewScope以外的范围,我们必须小心使用这种技术,因为实例变量将在多个请求之间共享!)
动作监听器返回后,JSF 最终会调用performAction()动作方法。实例变量contactName可用,并具有页面上的当前值。该方法继续到下一页视图。
重定向页面
如果你一直在跟随本章中的示例,你一定注意到了页面视图后面都附加了一个查询参数redirect=true(或根据官方 JSF 规范为faces-redirect=true)。这是对 JSF 的一个指令,要求它发送 HTTP 响应回网络客户端以重定向到 URL。为什么需要这个后缀?它允许用户将页面视图添加到书签,因为 JSF 框架通过仅渲染输出实际上有效地隐藏了当前页面视图。主要问题是内部页面转发,这使得使用数字应用程序的客户难以记住或添加书签。如果客户有一个深层嵌套的信息架构网站,提供页面重定向的能力是关键。次要问题是,如果你的网络应用程序以线性方式执行流程,那么网络浏览器的 URL 会更新,但总是显示流程中的上一页。
重定向在控制器方法examplar-methods-complete?redirect=true中起作用,这会导致 JSF 向浏览器发送 HTTP 响应重定向。网络浏览器将重定向解释为对 URL(如http://localhost:8080/jsf-crud-ajax-validation-1.0-SNAPSHOT/jsf-miscellany/examplar-methods.xhtml)的另一个 HTTP GET 请求。重定向的结果是,每个页面导航或动作至少发生两个请求-响应事件。如果你还记得,@ViewScoped或@RequestScopedbean 的作用域仅可用很短的时间。当 JSF 处理重定向指令的 HTTP GET 时,原始 bean 已经消失。这就是为什么示例使用 Flow 作用域的原因;该作用域保证了控制器业务逻辑中的数据能够持续到下一个页面视图显示。
指定页面重定向的另一种方式是通过faces-config.xml针对特定的导航情况。我们可以定义一个案例如下:
<navigation-rule>
<from-view-id>epayment.xhtml</from-view-id>
<navigation-case>
<from-outcome>payment-delivery</from-outcome>
<to-view-id>payment-deliver.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<from-outcome>payment-creditcard</from-outcome>
<to-view-id>payment-credit.xhtml</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
这种配置风格在设置第三方 JSF 包时可能很有用。当然,它也为库编写者提供了灵活性,并且不会污染 Java 管理的 bean 重定向字符串。我想这是一个因材施教的情况,因此取决于项目的目的。
最后,开发者可以通过提交链接和按钮直接将重定向设置到页面视图。以下代码展示了这种技术:
<h:commandButton style="btn bth-primary"
action="epayments.xhtml?faces-redirect=true"
value="Proceed to Paymemt" />
调试 JSF 内容
也许我应该早点介绍 JSF 的这个特性,因为对于初学者来说,学习使用 JSF 进行开发可能会感到困惑。如果你在模板视图之一中包含<ui:debug/>自定义标签元素,就可以在 JSF 应用程序中获得可调试的输出。实际上,框架的 Facelet 视图渲染器负责这个特性。
在<ui:insert>标签内嵌入单个<ui:debug>会导致 JSF 向 UI 层次结构树添加一个特殊的 UI 组件。这个调试组件捕获 Facelet 视图信息和 UI 层次结构的状态,包括应用程序中的任何作用域变量。信息是在渲染时捕获的。如果用户按下Ctrl + Shift + D键,JSF 将打开一个单独的浏览器窗口,显示可调试的信息,这在困难情况下非常有用。应用程序的主要模板是添加<ui:debug>标签的最佳位置。
<ui:debug>标签接受以下属性:
| 名称 | 类型 | 描述 |
|---|---|---|
hotkey |
String |
定义了触发可调试窗口打开的热键的单个字符。默认为d。 |
rendered |
ValueExpression |
指定调试组件是否渲染。它必须是一个值表达式或一个评估为true或false的字符串字面量。 |
以下截图显示了示例方法调用:

点击加号(+)符号可以展开内容,以便开发者动态地看到更多信息。
摘要
本章重点介绍了 JSF 验证的不同形式,因为对于用户来说,知道数据是否正确输入非常重要。我们研究了两种验证方法:客户端和服务器端。我们查看了FacesMessage实例,并学习了如何创建它们。之后,我们继续介绍从服务器端进行的验证,特别是 Java EE 7 中的 Bean Validation 框架。然后,我们进行了 JSF 验证的深入开发者之旅。我们学习了如何创建自定义验证器和转换器。我们还学习了如何使用 AJAX 进行即时模式验证,并理解了部分上下文生命周期。最后,我们花了大量时间处理视图,并将信息从页面视图传递到控制器。在这个过程中,我们解决了 JSF 流程作用域和页面重定向的问题。
在下一章中,我们将把注意力转向对话作用域,并开始组装有用的流程应用程序。在这个时候,我们给我们的新兴数字 JSF 应用程序增添优雅和复杂性。我会让你看到这一点。
练习
-
<h:outputLink>和<h:commandButton>元素之间的基本区别是什么?你如何使用像 Bootstrap 这样的 CSS 框架来适当地设置控件样式? -
在上一章中,有一些围绕为注册新成员到当地爱好读书俱乐部开发网络应用程序的练习。你是否偶然在单独的页面上编写了内容而没有重用?
-
将 UI 模板组合应用于你的爱好读书俱乐部项目。将其版本称为二,并将第一个版本保存为参考。在这个阶段,仅使用
<ui:define>、<ui:composition>和<ui:insert>模板组合标签。 -
向模板页面添加
<ui:debug>自定义标签以掌握它。这个特殊标签对开发者有什么作用? -
一位烦恼的业务利益相关者来到你的办公室,告诉你他们与伪造数据有关的问题。似乎有一些不怀好意的人在互联网上伪造数据输入,这给案件工作人员带来了更多负担。作为一名 JSF 咨询师,解释你是如何通过验证来保护后端数据库中的数据的。只有服务器端验证有效吗?只有客户端验证有效吗?
-
参考先前的爱好读书俱乐部应用程序,现在让我们向你创建的 JSF 表单元素添加验证。
-
将 Bean Validation 添加到注册者类(
Registrant.java——你可能在你的项目中将此类命名为不同的名称)。你的用户会对验证输出满意吗? -
当你只向应用程序添加服务器端验证时,会发生什么?
-
Bean Validation 和 JSF 验证之间有什么区别?
-
Bean Validation 和 JSF 验证之间有哪些相似之处?
-
根据用户,Bean Validation 和 JSF 验证的错误消息有多合适?
-
从创建页面开始。对注册者的姓名进行验证。你可以在页面视图中直接使用
<f:validateRequired>和<f:validateLength>进行验证。向页面视图添加适当的<h:messages>。 -
一些注册者使用 Facebook、Twitter 和 Instagram 等社交网络。向注册者 POJO 添加一些属性。添加一个 URL 验证器来验证社交网络属性是否正确。使用正则表达式验证器来验证 Twitter 账户语法,或者也许你可以编写自己的自定义验证器。
-
下载本书的源代码示例并运行第四章的示例代码。研究从服务器端发生验证的方式。
-
由于你使用服务器端验证开发了你的项目,你必须将 Hobby Book Club 网络应用程序提升到一个新的水平。为控制元素添加带有 AJAX 的客户端验证。你需要在你的 JSF 表单控制元素中添加适当的
<f:ajax>元素。别忘了每个控制都需要一个区域来渲染特定的错误消息;因此,你不会在页面上添加一个相应的<h:message>元素,并且与控制元素紧密相邻。 -
下载 Chrome 开发者网络工具或类似的网页检查开发工具,并检查 JSF 应用的 HTML 内容。你观察和注意到各种 HTML 元素的命名情况如何,尤其是表单的命名?
-
休息一下,并为 Hobby Book Club 应用程序添加现代 CSS 样式。请同事或朋友评估你应用程序的用户体验并收集反馈。根据反馈采取行动;更改内容。
-
在你的 CRUD 应用程序中添加取消操作;你需要确保 JSF 不验证输入?
第五章. 对话和旅程
| *"成功就是喜欢自己,喜欢你所做的事情,以及喜欢你做事的方式。" | |
|---|---|
| --玛雅·安吉洛 |
在本章中,我们专注于 JSF 对话作用域。此作用域定义了跨越请求和会话作用域的管理后端 Bean 的生命周期。这使得表单中的数据能够在请求作用域和会话作用域之间的生命周期中存活。对话作用域也被认为是上下文相关的。这个术语是从上下文和依赖注入(CDI)规范中借用的,它的意思是被标记为对话作用域的 Bean 的生命周期被视为上下文的一部分。你可以将这视为 CDI 容器在对象实例周围绘制的点状标记,以将它们定义为私有组,这表示一个生命周期。CDI 容器通过将一个对象 Bean 与对另一个 Bean 的依赖关联起来来完成这项将对象实例聚集在一起的工作。
在 CDI 中,上下文表示 CDI 容器将一组具有状态的组件对象实例绑定到定义良好且可扩展的生命周期中的能力。
在 CDI 中,依赖注入表示 CDI 容器将组件注入到应用程序中的能力,同时考虑到类型安全。CDI 容器在运行时选择要注入的 Java 接口的实现。
JavaServer Faces 集成到标准 CDI 作用域中,包括对话作用域。对话的例子包括多种当代数字客户旅程。你可能自己在在线申请新工作时见过,或者在电子商务网站的发货和配送流程中,或者在设置政府资源或功能(如税务评估或退税)时见过。在本章中,我们将探讨一个客户旅程的例子,其中开发人员或用户正在申请即时安全贷款。你可能已经见过这些,或者实际上已经幸运地或不幸地浏览过现金贷款设施。
对话作用域与客户端保持状态。标记为对话作用域的控制器或 POJO 及其组件实例成为其状态的一部分。
下图概述了我们将在本章中研究的受管理 Bean 控制器周围的对话作用域:

几个具有不同 CDI 作用域的 Bean 实例的插图
上一张图展示了两个不同的客户,他们已经登录到企业应用程序中。从左到右,我们有UserProfile实例,它们捕获客户的登录信息,并存储在 CDI 会话作用域中。这些豆子只与特定的客户共享,该客户与javax.servlet.http.HttpSession对象相关联。
向右移动,我们有一个豆实例的对象图,LendingController、ContactDetail和BankDetails,它们存储在对话范围内。
在图例底部,在应用范围内,我们有豆实例,Utility和DataHelperController。所有 Web 应用用户共享这些豆。对话豆能够访问当前会话范围和应用程序范围内的共享信息。
小贴士
想了解更多关于 CDI 的信息,请阅读姊妹书籍,Java EE 7 开发者手册,Packt Publishing。
数字电子商务应用
Java EE 应用非常适合维护状态的数字站点。如果网站在用户的客户旅程中维护任何与用户相关的状态,那么用户通常都会参与对话。用户体验测试表明,对于许多发生大量交互的企业网站,存在多个对话。换句话说,Java Champion,Java EE 7 专家组成员 Antonio Gonclaves 曾经说过,如果你的目的是构建数字 Web 应用,那么它必须能够处理复杂的流程管理。
瞬时贷款并不完全属于快速启动公司和企业家为解决全球经济信用危机提供的最终解决方案的产品行列。随着这些新敏捷初创企业的竞争加剧,许多发达国家的国内家庭银行不得不迅速组建一个瞬时贷款设施产品。在本章中,我们将开发一个瞬时安全贷款设施。我们的产品不是一个完整的解决方案,但它展示了为数字客户交付初始原型的途径。我们不会与金融服务集成,而商业解决方案则需要管理信息报告以及与商业银行基础设施的集成。
让我们更深入地探讨对话范围。
对话范围
对话范围由跨越多个对服务器 HTTP 请求的生命周期定义。开发者决定范围何时开始和结束,最重要的是,它与用户相关联。关键注解由一个名为@javax.enterprise.context.ConversationScoped的 CDI 规范定义。当你将此注解应用于控制器或 POJO 时,请记住确保你实现了标记接口,java.io.Serializable。
CDI 还定义了一个接口,javax.enterprise.context.Conversation,它代表对话接口。对话可以存在两种不同的状态:短暂的和长期运行的。短暂状态意味着对话是一个临时状态。当你用@ConversationScoped注解一个豆时,它将默认处于短暂状态。
开发者控制会话何时从瞬态状态切换到长时间运行状态。然后,会话变得活跃,并保持 HTTP 用户连接的持有状态,这通常与特定的网络浏览器标签相关联。本质上,会话是一个工作单元。会话被启动,最终结束。
以下是对 javax.enterprise.context.Conversation 接口的定义:
public interface Conversation {
void begin();
void begin(String id);
void end();
String getId();
long getTimeout();
void setTimeout(long milliseconds);
boolean isTransient();
}
方法 begin() 启动一个会话。CDI 容器通过标记会话作用域 POJO 以便进行长时间运行存储。会话有一个标识符;另一个方法 begin(String id) 允许开发者提供一个显式的标识符。
方法 end() 终止会话,CDI 容器有效地丢弃与 POJO 相关的上下文信息,状态返回瞬态。为了找出会话是否为瞬态,使用 isTransient() 调用。
以下图表展示了 CDI 会话作用域 Bean 的生命周期:

会话超时和序列化
如我们之前讨论的,会话作用域的生命周期超出了请求作用域,但不能在会话作用域之外存活。CDI 容器可以超时会话作用域并终止上下文信息,以保留或恢复资源。这也是为什么带有 @ConversationScoped 注解的 Bean 必须是 Serializable 的部分原因。一个智能的 CDI 容器和 Servlet 容器可能会将一个会话转移到磁盘,甚至转移到另一个正在运行的 JVM 实例,但如果没有序列化,它们永远不会尝试这样做。
应用程序开发者可以通过 getTimeout() 和 setTimeout() 方法检索和设置超时时间。
因此,我们现在知道了 @ConversationScoped 和 Conversation 是什么。让我们在我们的即时安全贷款应用程序中好好利用它们。
会话作用域控制器
我们数字客户旅程的核心是一个名为 LendingController 的管理 Bean。随着我们通过本章的进展,我们将将其分解成更易于理解的几个部分。
初始实现看起来是这样的:
package uk.co.xenonique.digital.instant.control;
import uk.co.xenonique.digital.instant.boundary.ApplicantService;
import uk.co.xenonique.digital.instant.entity.Address;
import uk.co.xenonique.digital.instant.entity.Applicant;
import uk.co.xenonique.digital.instant.entity.ContactDetail;
import uk.co.xenonique.digital.instant.util.Utility;
// imports elided
@Named("lendingController")
@ConversationScoped
public class LendingController implements Serializable {
@EJB ApplicantService applicantService;
@Inject Conversation conversation;
@Inject Utility utility;
public final static int DEFAULT_LOAN_TERM = 24;
public final static BigDecimal DEFAULT_LOAN_AMOUNT = new BigDecimal("7000");
public final static BigDecimal DEFAULT_LOAN_RATE = new BigDecimal("5.50");
private int dobDay;
private int dobMonth;
private String dobYear;
private BigDecimal minimumLoanAmount = new BigDecimal("3000");
private BigDecimal maximumLoanAmount = new BigDecimal("25000");
private BigDecimal minimumLoanRate = new BigDecimal("3.0");
private BigDecimal maximumLoanRate = new BigDecimal("12.0");
private String currencySymbol = "£";
private BigDecimal paymentMonthlyAmount = BigDecimal.ZERO;
private BigDecimal totalPayable = BigDecimal.ZERO;
private Applicant applicant;
public LendingController() {
applicant = new Applicant();
applicant.setLoanAmount( DEFAULT_LOAN_AMOUNT);
applicant.setLoanRate( DEFAULT_LOAN_RATE );
applicant.setLoanTermMonths( DEFAULT_LOAN_TERM );
applicant.setAddress(new Address());
applicant.setContactDetail(new ContactDetail());
}
public void checkAndStart() {
if ( conversation.isTransient()) {
conversation.begin();
}
recalculatePMT();
}
public void checkAndEnd() {
if (!conversation.isTransient()) {
conversation.end();
}
}
/* ... */
}
初看这可能是一个复杂的控制器,然而,这里有两个重要的事项。首先,我们使用 @ConversationScoped 注解 LendingController,其次,我们要求 CDI 将 Conversation 实例注入到这个 Bean 中。我们还实现了 Serializable 标记接口,以便让 Servlet 容器有自由选择在需要时以及在实现支持此功能的情况下,即时持久化和重新加载 Bean。
特别注意辅助方法 checkAndStart() 和 checkAndEnd()。checkAndStart() 方法在当前状态为瞬时时启动一个新的长时间运行会话。checkAndEnd() 方法在当前会话实例处于运行状态时终止长时间运行会话。
你可以看到,前一个联系详情应用程序的一些元素已经进入了我们的即时贷款应用程序。这是经过深思熟虑的设计。
LendingController豆包含一个Applicant实例成员,这是领域主详细记录。它是一个 JPA 实体豆,用于存储申请人的数据。你已经看到了出生日期字段。控制器还包括与月供金额和贷款总额相关的成员。它还包含贷款金额和利率的下限和上限,这些作为 getter 和 setter 公开。
最后,CDI 将一个实用实例注入到LendingController中。这是一个应用程序范围的 POJO,它让我们可以避免编写静态单例。我们将在稍后看到实用类的细节,但首先我们必须绕到一个设计模式。
实体-控制-边界设计模式
这个即时贷款应用程序利用了一种特定的设计模式,称为实体-控制-边界。这是一个将应用程序中一组对象的责任和关注点分离的模式。线索在于导入的LendingController包名。
简单来说,实体的概念代表软件应用中的数据模型。控制元素是管理信息流的软件应用组件。边界元素属于应用程序系统,但位于系统的外围。
你在假设这个模式类似于模型-视图-控制器时是正确的,除了 ECB 适用于整个软件系统,控制元素比控制器和用户界面更有责任。
在这个应用程序中,我将LendingController放在控制包中,因为源代码显示它包含大部分业务逻辑。也许,对于一个合适的生产应用程序,我们可以将我们的逻辑委托给另一个 CDI 豆或 EJB。正如一位顾问曾经对他的客户说的那样,这取决于情况。
在实体包内部,没有争议;我添加了Applicant、ContactDetail和Address类。这些是持久化对象。你已经看到了在第四章中的ContactDetail实体豆,JSF 验证和 AJAX。
我将ApplicantService EJB 放在边界包中,因为它位于外围,并且负责数据访问。
客户旅程
让我们再次深入探讨LendingController并揭示我们的客户旅程。我们假设我们已经与创意设计师和用户体验团队会面,并提出了一个设计方案。该应用程序基于一系列线性网页,组织成一个向导。为了这个基本示例,我们只允许消费者在成功输入当前页面的有效信息后,才能进入下一页。
以下是为每个页面设定的标题:
| 步骤 | 页面 | 描述 |
|---|---|---|
| 1 | 开始 | 向客户提供关于资格标准的详细信息 |
| 2 | 您的详细信息 | 客户输入他们的个人联系详情和出生日期 |
| 3 | 您的利率 | 客户选择贷款金额和期限 |
| 4 | 您的地址 | 客户输入他们的完整家庭地址和电话号码 |
| 5 | 确认 | 消费者同意服务条款并查看摘要 |
| 6 | 完成 | 消费者看到他们申请表提交的确认 |
这现在在控制器中实现起来相当简单,以下是一个示例:
@Named("lendingController")
@ConversationScoped
public class LendingController implements Serializable {
/* ... */
public String cancel() {
checkAndEnd();
return "index?faces-redirect=true";
}
public String jumpGettingStarted() {
return "getting-started?faces-redirect=true";
}
public String doGettingStarted() {
checkAndStart();
return "your-details?faces-redirect=true";
}
public String doYourDetails() {
checkAndStart();
Calendar cal = Calendar.getInstance();
cal.set(Calendar.DAY_OF_MONTH, dobDay);
cal.set(Calendar.MONTH, dobMonth-1);
int year = Integer.parseInt(dobYear);
cal.set(Calendar.YEAR, year);
applicant.getContactDetail().setDob(cal.getTime());
return "your-rate?faces-redirect=true";
}
public String doYourRate() {
checkAndStart();
return "your-address?faces-redirect=true";
}
public String doYourAddress() {
checkAndStart();
return "confirm?faces-redirect=true";
}
public String doConfirm() {
/* ... */
return "completion?faces-redirect=true";
}
public String doCompletion() {
/* ... */
return "index?faces-redirect=true";
}
/* ... */
}
LendingController 面豆有多个与用户需求相对应的操作方法,即 doGettingStarted()、doYourDetails()、doYourRate()、doYourAddress()、doConfirm() 和 doCompletion()。这些操作方法通过简单地返回名称来推进客户到下一个页面视图。对于这些方法中的大多数,除了 doCompletion() 之外,我们通过调用 checkAndStart() 确保会话处于长时间运行状态。在 doCompletion() 和 cancel() 方法中,我们调用 checkAndEnd() 确保会话回到短暂状态。doCompletion() 方法利用 ApplicationService 将数据、Applicant 实例保存到底层数据库。
小贴士
在示例代码中,我们在每个操作方法的开头应用 checkAndStart() 是一种欺骗行为。对于生产代码,我们通常应该确保如果用户跳转到应该有会话的标记 URL,那么它将是一个错误或重定向。
让我们检查实体并填写更多的空白。
实体类
实体 Applicant 是一个主从记录。这被称为核心域对象。它存储了客户对即时保障贷款的申请数据。我们捕获客户的贷款信息,例如联系详情(ContactDetail)、地址(Address)、电话号码(家庭、工作和手机),最重要的是,财务详情。
Applicant 实体如下所示:
package uk.co.xenonique.digital.instant.entity;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;
@Entity
@Table(name="APPLICANT")
@NamedQueries({
@NamedQuery(name="Applicant.findAll",
query = "select a from Applicant a " +
"order by a.submitDate"),
@NamedQuery(name="Applicant.findById",
query = "select a from Applicant a where a.id = :id"),
})
public class Applicant {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@OneToOne(cascade = CascadeType.ALL)
private ContactDetail contactDetail;
@OneToOne(cascade = CascadeType.ALL)
private Address address;
private String workPhone;
private String homePhone;
private String mobileNumber;
private BigDecimal loanAmount;
private BigDecimal loanRate;
private int loanTermMonths;
private boolean termsAgreed;
@Temporal(TemporalType.TIMESTAMP)
private Date submitDate;
public Applicant() { }
// Getters and setters omitted ...
// hashCode(), equals(), toString() elided
}
Applicant 实体存储贷款金额、利率、期限,以及提交日期。它还包含家庭、工作和手机电话号码。申请人与 ContactDetail 和 Address 实体都存在一对一的单向关系。
对于像 loanRate 和 loanAmount 这样的财务属性,请注意,我们更喜欢在计算时使用 BigDecimal 而不是原始的浮点类型,以确保货币计算的准确性。
向利益相关者解释领域对象的方式是:客户有一个贷款利率,一个贷款期限,并且必须电子方式同意法律条件。有了这些信息,系统可以计算贷款以及客户每月需要偿还的金额,并在申请贷款时显示出来。
您已经看到了ContactDetail实体。它与之前完全相同,只是包名已被重构为实体。以下是Address实体 bean 的提取代码:
package uk.co.xenonique.digital.instant.entity;
import javax.persistence.*;
@Entity
@Table(name="ADDRESS")
@NamedQueries({
@NamedQuery(name="Address.findAll",
query = "select a from Address a "),
@NamedQuery(name="Address.findById",
query = "select a from Address a where a.id = :id"),
})
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="ADDRESS", nullable = false,
insertable = true, updatable = true,
table = "ADDRESS")
private long id;
String houseOrFlatNumber;
String street1;
String street2;
String townOrCity;
String region;
String areaCode;
String country;
// toString(), hashCode(), equalsTo() elided
/* ... */
}
实体Address代表申请人的通信和法律个人地址。这里没有什么特别之处。它是一个标准的实体 bean,你将在电子商务应用程序中看到。
请允许我提醒您,示例的源代码是在线的,并且是本书的一部分,供您参考。
数据服务
我们如何将客户的输入保存到持久存储中?我们的应用程序使用有状态的会话 EJB,并提供保存和检索Applicant实体记录的方法。
ApplicantService类如下:
package uk.co.xenonique.digital.instant.boundary;
import uk.co.xenonique.digital.instant.entity.Applicant;
import javax.ejb.Stateful;
import javax.persistence.*;
import java.util.List;
@Stateful
public class ApplicantService {
@PersistenceContext(unitName = "instantLendingDB",
type = PersistenceContextType.EXTENDED)
private EntityManager entityManager;
public void add(Applicant applicant) {
entityManager.persist(applicant);
}
/* ... */
public List<Applicant> findAll() {
Query query = entityManager.createNamedQuery(
"Applicant.findAll");
return query.getResultList();
}
public List<Applicant> findById(Integer id) {
Query query = entityManager.createNamedQuery(
"Applicant.findById").setParameter("id", id);
return query.getResultList();
}
}
add()方法将新申请人插入到数据库中。findAll()和findById()在即时贷款示例中未使用。这些查询方法仅用于说明目的。假设,在完整应用程序的另一个部分需要访问申请人数据。
我们已经涵盖了应用程序的实体、控制和边界。现在是时候检查页面视图了。
页面视图
视图的控件流程由客户旅程定义。每个页面视图代表业务利益相关者希望看到的特定要求。索引页面视图是一个要求,因为贷款人希望客户看到着陆页。它也是国家政府当局要求的合规法律义务。您还会注意到客户旅程映射为线性流程,但并非所有旅程都是如此。
注意
短期贷款方案必须遵循合规要求。请参阅英国金融服务管理局(goo.gl/NfbFbK)和美国消费者金融保护局(goo.gl/3V9fxk)的网站。
以下表格概述了控制器操作与视图页面之间的关系:
| 查看源代码 | 查看目标 | 操作方法 |
|---|---|---|
index |
getting-started |
jumpGettingStarted() |
getting-started |
your-detail |
doGettingStarted() |
your-details |
your-rate |
doYourDetails() |
your-rate |
your-address |
doYourRate() |
your-address |
confirm |
doYourAddress() |
confirm |
completion |
doConfirm() |
completion |
index |
doCompletion() |
在前面的表中,所有视图页面都带有扩展名 xthml。很明显,在对话中正在发生线性工作流程。对话范围理想地开始于客户通过 jumpGettingStarted() 动作方法进入开始视图时。
一个初始页面视图
让我们看看初始的 index.xhtml 页面视图。这是贷款申请的着陆页。以下是我们贷款申请和着陆页的截图:

这个页面的视图 index.xhtml 非常简单直接。它包含一个基本的链接按钮组件,并使用 Bootstrap 滚动图:
<!DOCTYPE html>
<html ...>
<ui:composition template="/basic_layout.xhtml">
...
<ui:define name="mainContent">
<h1> JSF Instant Secure Lending</h1>
<p>
Welcome to Duke Penny Loan where developers,
designers and architect can secure
an instant loan. <em>You move. We move.</em>
</p>
<div class="content-wrapper center-block">
<div id="carousel-example-generic" class="carousel slide"
data-ride="carousel" data-interval="10000">
<!-- Indicators -->
<ol class="carousel-indicators">
<li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
<li data-target="#carousel-example-generic" data-slide-to="1"></li>
<li data-target="#carousel-example-generic" data-slide-to="2"></li>
<li data-target="#carousel-example-generic" data-slide-to="3"></li>
</ol>
...
</div>
</div><!-- content-wrapper -->
<div class="content-wrapper">
<h:link styleClass="btn btn-primary btn-lg"
outcome="#{lendingController.jumpGettingStarted()}">
Apply Now!
</h:link>
</div>
...
</ui:define> <!--name="mainContent" -->
</ui:composition>
</html>
<h:link> 元素是这个视图最重要的功能。这个自定义标签的输出引用了控制器中的 jumpGettingStarted() 方法,这开始了一个长时间运行的对话。
即使在这个阶段,在对话开始之前,我们也可以向客户传递信息。因此,在页面视图的进一步部分,我们使用表达式语言告诉客户最低和最高贷款金额以及利率。
以下代码,也是页面视图 index.xhtml 的一部分:
<div class="content-wrapper">
<p>
Apply for a Dukes Dollar loan. You borrow from
<b>
<h:outputText value="#{lendingController.minimumLoanAmount}" >
<f:convertNumber currencyCode="GBP" type="currency" />
</h:outputText>
</b>
to
<b>
<h:outputText value="#{lendingController.maximumLoanAmount}" >
<f:convertNumber currencyCode="GBP" type="currency" />
</h:outputText>
</b>
on a rate from
<b>
<h:outputText value="#{lendingController.minimumLoanRate}" >
<f:convertNumber pattern="0.00" />
</h:outputText>%
</b>
to
<b>
<h:outputText value="#{lendingController.maximumLoanRate}" >
<f:convertNumber pattern="0.00" />
</h:outputText>%
</b>.
</p>
</div>
这个页面使用了 JSF 核心标签 <f:convertNumber> 将浮点数格式化为货币格式。HTML 实体字符 % 代表百分号字符 (%)。记住,视图技术是严格使用 Facelets 而不是 HTML5。
开始页面视图
开始视图甚至更简单。我们向客户展示他们申请贷款的资格信息。客户必须年满 18 岁;我们重复说明他们可以借多少钱以及借款期限。
这个视图被命名为 getting-started.xhtml,如下截图所示:

有一个单独的 JSF 表单,有一个按钮可以将客户移动到下一个页面视图 your-details.xhtml。没有必要查看这个视图的完整源代码,因为它主要是标记 HTML。然而,我们还有一个命令链接:
<h:link styleClass="btn btn-primary btn-lg" outcome="#{lendingController.doGettingStarted()}">
Next</h:link>
联系详情页面视图
下一个视图是熟悉的联系详情屏幕。我们将它从前面的章节中纳入即时安全贷款示例。我们还重新使用了 JSF 表达式语言来引用控制器和嵌套属性。
首名字段页面编写代码如下:
<h:inputText class="form-control" label="First name"
value="#{lendingController.applicant.contactDetail.firstName}"
id="firstName" placeholder="First name">
<f:validateRequired/>
<f:validateLength maximum="64" />
<f:ajax event="blur" render="firstNameError"/>
</h:inputText>
EL #{lendingController.applicant.contactDetail.firstName} 引用了相关的嵌套实体属性。我们还保留了来自 第四章 的 AJAX JSF 验证功能,JSF 验证和 AJAX,以提供丰富的客户旅程。
对于这个视图,我们使用 JSF 命令按钮提交表单:
<h:commandButton styleClass="btn btn-primary"
action="#{lendingController.doYourDetails()}"
value="Submit" />
 
 
<h:commandButton styleClass="btn btn-default"
action="#{lendingController.cancel()}"
immediate="true" value="Cancel"/>
我们还提供了必要的取消操作,以防客户当天不再想申请贷款。
以下是对 your-details.xhtml 视图的截图,允许客户输入他们的联系信息:

现在是时候做一些新的事情了。何不向老牌的 JavaServer Faces 中添加一些 HTML5 的好处?
您的评分页面视图
贷款金额和利率页面视图依赖于 HTML5 范围控件元素,在大多数符合标准浏览器中,它被渲染为一个水平滑块。JSF 没有内置对范围控件的支持;因此,对于此视图,我们利用了 JSF HTML5 友好支持功能。JSF 规范允许我们编写看起来像标准 HTML 组件的标记,但如果我们提供一个特殊属性,JSF 就将其视为 UI 组件。透传功能仅适用于类似于现有 JSF 核心控件标记。
一图胜千言,让我们看看以下 your-rate.xhtml 的截图:

该视图使用 AJAX 部分更新和 HTML5 友好的标记功能。让我向您展示表单的代码:
<h:form id="yourRateForm"
styleClass="form-horizontal"
p:role="form">
<div class="form-group">
<h:outputLabel for="loanAmount"
class="col-sm-3 control-label">
Loan Amount</h:outputLabel>
<div class="col-sm-9">
<input class="form-control" jsf:label="Loan Amount"
jsf:value="#{lendingController.applicant.loanAmount}"
type="range"
min="#{lendingController.minimumLoanAmount}"
max="#{lendingController.maximumLoanAmount}"
step="250"
id="loanAmount" >
<f:validateRequired/>
<f:ajax event="blur" render="loanAmountError"/>
<f:ajax event="valueChange"
listener="#{lendingController.recalculatePMT()}"
render="paymentMonthlyOutput loanRateOutput totalPayableOutput" />
</input>
<h:message id="loanAmountError"
for="loanAmount" styleClass="alert validation-error"/>
</div>
</div>
与所有 JSF 表单一样,我们首先声明一个名为 yourRateForm 的表单,并使用 Bootstrap CSS 进行样式设计。专注于控件元素,您会注意到它被编写为 <input> 而不是 <h:inputText>。这是因为 JSF 的 <h:inputText> 不支持新的 HTML5 范围元素。通常,无法访问更丰富的 UI 组件会对即时安全贷款造成问题。
HTML5 范围输入元素接受最小值、最大值和当前值。它还接受步长大小。
HTML5 友好支持
JSF 2.2 允许使用新的标签库 URI 和 XML 命名空间 ``. 通过属性 jsf:id、jsf:label 和 jsf:attribute,HTML5 标签在 JSF 框架中具有可见性。`
```your-rate.xhtml 的完整 XML 命名空间如下所示:
<!DOCTYPE html>
<html
>
我们将在本章后面讨论组合组件。HTML5 友好的标签库向 JSF 生命周期公开了标准的 HTML 输入组件。对于不熟悉 JSF 或 Java 的创意人员来说,理解页面视图也更简单。我们不再需要担心 JSF 对视图 ID 应用特殊名称混淆的问题;这意味着组件 ID 对 HTML 和 JavaScript 都是有用的。
使用 AJAX 进行部分更新
``在 第四章 中,我们学习了如何使用 Ajax 验证表单属性。JSF 允许开发者使用 <f:ajax> 自定义标签执行部分页面更新。```
为了启用丰富的用户示例,每当客户更改贷款金额滑块时,我们都会调用服务器端来重新计算月付款金额。我们通过附加一个事件监听器来跟踪值的变化来实现这一点。相应的代码如下:
<f:ajax event="valueChange"
listener="#{lendingController.recalculatePMT()}"
render="paymentMonthlyOutput loanRateOutput
totalPayableOutput" />
代码的新增功能是渲染属性,它指定了在 AJAX 响应中将被重新渲染的 JSF UI 组件的唯一 ID。换句话说,我们声明性地指定了在 AJAX 行为完成时 JSF 上要重新渲染的组件,从而获得部分更新。
绑定组件
让我们看看 HTML5 Range 元素(在本例中为贷款金额)与之绑定的其他组件。
看看下面的代码:
<c:set var="loanAmountWidth" value="#{100.0 * (lendingController.applicant.loanAmount - lendingController.minimumLoanAmount) / (lendingController.maximumLoanAmount - lendingController.minimumLoanAmount)}" />
<div class="progress">
<div id="loanAmountProgress" class="progress-bar progress-bar-success progress-bar-striped"
role="progressbar" aria-valuenow="#{lendingController.applicant.loanAmount}"
aria-valuemin="#{lendingController.minimumLoanAmount}"
aria-valuemax="#{lendingController.maximumLoanAmount}"
style="width: ${loanAmountWidth}%;">
#{lendingController.applicant.loanAmount}
</div>
</div>
<div class="content-wrapper">
<p id="loanAmountText" class="monetary-text">
You would like to borrow
<b> #{lendingController.currencySymbol}
<h:outputText value="#{lendingController.applicant.loanAmount}" >
<f:convertNumber pattern="#0,000" />
</h:outputText> </b>
</p>
</div>
进度标记直接复制自 Bootstrap CSS 组件示例。我们插入了值表达式,从LendingController和Applicant实例中提取信息。
在前面代码摘录的顶部,我们使用 JSTL 核心标签<c:set>设置了进度条的初始值。
<c:set var="loanAmountWidth" value="#{100.0 * (lendingController.applicant.loanAmount - lendingController.minimumLoanAmount) / (lendingController.maximumLoanAmount - lendingController.minimumLoanAmount)}" />
这表明 EL 3.0 中的统一表达式语言具有检索 JSF 中后期绑定的值以计算结果的能力。结果被设置在名为loanAmountWide的页面作用域变量中。稍后,使用$(loanAmountWidth)访问该变量,并设置 Bootstrap CSS 进度条组件的初始位置值。
HTML5 标准没有内置支持来显示 HTML5 Range 元素在所有顶级浏览器中的值。在撰写本文时,这个特性缺失,W3C 或 WHATWG 可能在不久的将来加强 HTML5 规范中的这一弱点。在此之前,我们将使用 jQuery 和 JavaScript 来填补这一空白。
如果你注意到了,前面的代码中,文本被标识为loanAmountText,进度组件被表示为loanAmountProgress。将 HTML5 Range 元素绑定到这些字段上的 jQuery 代码是微不足道的。
我们需要一个 JavaScript 模块来实现绑定。完整的代码位于/resources/app/main.js中,如下所示:
var instantLending = instantLending || {};
instantLending.Main = function()
{
var init = function()
{
$(document).ready( function() {
associateRangeToText(
'#loanAmount', '#loanAmountProgress', '#loanAmountText',
3000.0, 25000.0,
function(value) {
var valueNumber = parseFloat(value);
return "You would like to borrow <b>£" +
valueNumber.formatMoney(2, '.', ',') + "</b>";
});
});
};
var associateRangeToText = function( rangeElementId,
rangeProgressId, rangeTextId, minimumValue,
maximumValue, convertor) {
var valueElem = $(rangeElementId);
var progressElem = $(rangeProgressId);
var textElem = $(rangeTextId);
valueElem.change( function() {
var value = valueElem.val();
progressElem.html(value);
progressElem.attr("aria-valuenow", value);
var percentage = 100.0 * ( value - minimumValue) /
( maximumValue - minimumValue );
progressElem.css("width", percentage+"%");
var monetaryText = convertor( value )
textElem.html( monetaryText );
});
}
var oPublic =
{
init: init,
associateRangeToText: associateRangeToText
};
return oPublic;
}(jQuery);
instantLending.Main.init();
模块instantLending.Main定义了一个 HTML Range 元素与两个其他组件的绑定:一个进度条和一个标签文本区域。为了快速复习 JavaScript 模块,请参阅第一章,*数字 Java EE 7*。
该模块有一个init()函数,它使用 jQuery 文档加载机制设置绑定。它调用一个名为associateRangeToText()的函数,该函数计算进度条的百分比并将其值写入文本元素区域。该函数接受相关组件的文档 ID:范围、进度和文本标签组件。它将一个匿名函数附加到范围元素上;当用户更改组件时,它更新相关组件。
模块main.js还定义了一个添加到 JavaScript 数字类型中的有用原型方法。以下代码显示了它是如何工作的:
// See http://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-money-in-javascript
Number.prototype.formatMoney = function(c, d, t){
var n = this,
c = isNaN(c = Math.abs(c)) ? 2 : c,
d = d == undefined ? "." : d,
t = t == undefined ? "," : t,
s = n < 0 ? "-" : "",
i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "",
j = (j = i.length) > 3 ? j % 3 : 0;
return s + (j ? i.substr(0, j) + t : "") +
i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
(c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
};
```formatMoney()方法将浮点数值类型格式化为货币输出作为字符串。此代码由 Patrick Desjardins 贡献给 Stack Overflow。以下代码说明了如何调用此函数:
var p = 128500.99
console.log(p.formatMoney(2, '.', ',') ) // 128,500.99
第一个参数是固定分数大小,第二个参数确定小数符号,第三个指定千位字符。
使用此模块,我们将 HTML5 Range 元素绑定到页面上的其他元素,从而展示了 JSF 对 HTML5 的友好支持。
使用 AJAX 局部更新更新区域
``JSF 如何使用 AJAX 响应更新页面上的区域?开发者指定了由<f:ajax>标签的 render 属性更新的 UI 组件。在现代网页设计中,哪个组件可以被视为标准 JSF 渲染套件内的 HTML 层元素<div>?这个答案是通过使用<h:panelGroup> JSF 自定义标签。我们可以为这个 UI 组件提供一个唯一标识符,当 AJAX 行为完成时,JSF 渲染此组件。`
``以下是为即时贷款利率的代码摘录,其中 div 元素通过loanRateOutput标识:`
<c:set var="loanRateWidth" value="#{100.0 * (lendingController.applicant.loanRate - lendingController.minimumLoanRate) / (lendingController.maximumLoanRate - lendingController.minimumLoanRate)}" />
<h:panelGroup layout="block" id="loanRateOutput">
<div class="progress">
<div id="loanRateProgress" class="progress-bar progress-bar-info progress-bar-striped"
role="progressbar" aria-valuenow="#{lendingController.recalculateLoanRate()}"
aria-valuemin="#{lendingController.minimumLoanRate}"
aria-valuemax="#{lendingController.maximumLoanRate}"
style="width: ${loanRateWidth}%;">
#{lendingController.applicant.loanRate}
</div>
</div>
<div class="content-wrapper">
<p id="loanRateText" class="monetary-text">
The tax rate will be
<b> <h:outputText value="#{lendingController.applicant.loanRate}" >
<f:convertNumber pattern="0.000" />
</h:outputText>%</b>
</p>
</div>
</h:panelGroup>
```<h:panelGroup>默认渲染一个 div 层,因此包含进度条组件和文本输出内容。该 div 在LendingController中调用recalculatePMT()方法之后渲染。请参考前面的章节以提醒此代码。
``函数recalclulatePMT()和recalculateLoanRate()如下所示:`
public BigDecimal recalculatePMT() {
recalculateLoanRate();
paymentMonthlyAmount = new BigDecimal(utility.calculateMonthlyPayment(
applicant.getLoanAmount().doubleValue(),
applicant.getLoanRate().doubleValue(),
applicant.getLoanTermMonths()));
totalPayable = paymentMonthlyAmount.multiply(
new BigDecimal( applicant.getLoanTermMonths()));
return paymentMonthlyAmount;
}
public BigDecimal recalculateLoanRate() {
applicant.setLoanRate(
utility.getTaxRate(applicant.getLoanAmount()));
return applicant.getLoanRate();
}
``函数recalculatePMT()使用经典的数学公式根据本金金额、期限长度以及当然的利率来评估贷款的月付款金额。`
``函数recalculateLoanRate()使用一个实用工具,一个应用范围的 CDI 豆,根据贷款账户的利率限制表来计算利率。`
``让我们回顾一下。JavaScript 模块instantLending::Main在客户端更新。当客户更改贷款金额时,此模块会更改进度条组件和文本内容。同时,JSF 向服务器端发起 AJAX 请求并调用动作事件监听器recalculatePMT()。框架最终接收到 AJAX 响应,然后重新渲染贷款利率、期限控件和摘要区域。`
``为了完成 XHTML,让我们检查这个页面视图上剩余的内容,你的rate.xhtml。以下是为贷款期限的内容,它是一个下拉组件:`
<div class="form-group">
<h:outputLabel for="loanTerm" class="col-sm-3 control-label">
Loan Term (Months)</h:outputLabel>
<div class="col-sm-9">
<h:selectOneMenu class="form-control"
label="Title" id="loanTerm"
value="#{lendingController.applicant.loanTermMonths}">
<f:selectItem itemLabel="12 months" itemValue="12" />
<f:selectItem itemLabel="24 months" itemValue="24" />
<f:selectItem itemLabel="36 months" itemValue="36" />
<f:selectItem itemLabel="48 months" itemValue="48" />
<f:selectItem itemLabel="60 months" itemValue="60" />
<f:validateRequired/>
<f:ajax event="blur" render="loanTermError"/>
<f:ajax event="valueChange"
listener="#{lendingController.recalculatePMT()}"
render="paymentMonthlyOutput loanRateOutput
monthTermsOutput totalPayableOutput" />
</h:selectOneMenu>
<h:message id="loanTermError"
for="loanTerm" styleClass="alert validation-error"/>
</div>
</div>
``该组件还包含一个<f:ajax>自定义标签,用于调用重新计算事件监听器。因此,如果客户选择不同的贷款期限,由于 AJAX 局部更新,loanRateOutput和paymentMonthlyOutput也会随着摘要一起改变。`
最后,让我们看看摘要区域的内容:
<div class="content-wrapper" >
<div class="row">
<div class="col-md-12">
<p class="monetary-text-large">
Your monthly payment is <b>
#{lendingController.currencySymbol}<h:outputText
id="paymentMonthlyOutput"
value="#{lendingController.recalculatePMT()}">
<f:convertNumber pattern="#0.00" />
</h:outputText></b>
</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<p class="monetary-text">
Loan term
<h:outputText id="monthTermsOutput"
value="#{lendingController.applicant.loanTermMonths}"/>
months
</p>
</div>
<div class="col-md-6">
<p class="monetary-text">
Total payable
#{lendingController.currencySymbol}<h:outputText
id="totalPayableOutput"
value="#{lendingController.totalPayable}">
<f:convertNumber pattern="#0,000" />
</h:outputText>
</p>
</div>
</div>
</div>
在先前的代码摘录中,我们使用`<h:outputText>`而不是`<h:panelGroup>`来仅使用部分 AJAX 更新更新内容的一部分。JSF 输出文本元素是一个 JSF UI 组件,它通过请求 AJAX 行为重新渲染来工作。
地址页面视图
地址页面视图捕获客户的住宅主要地址。这个页面还具备客户端的 AJAX 验证。
这段代码与联系详情表单如此相似,所以我们在这里省略代码摘录和树形结构。我将在下面的代码中只展示第一个`houseOrFlatNumber`字段:
<h:form id="yourAddressForm"
styleClass="form-horizontal"
p:role="form">
<div class="form-group">
<h:outputLabel for="houseOrFlatNumber"
class="col-sm-3 control-label">
House number</h:outputLabel>
<div class="col-sm-9">
<h:inputText class="form-control"
label="House or Flat Number"
value="#{lendingController.applicant.address.houseOrFlatNumber}"
id="houseOrFlatNumber" placeholder="First name">
<f:validateLength maximum="16" />
<f:ajax event="blur" render="houseOrFlatNumberError"/>
</h:inputText>
<h:message id="houseOrFlatNumberError"
for="houseOrFlatNumber"
styleClass="alert validation-error"/>
</div>
</div>
...
</h:form>
以下是对`your-address.xhtml`页面视图的截图。

确认页面视图
确认页面视图是客户看到他们即时贷款所有详情的地方。在这个视图中,他们有机会阅读合同的条款和条件。客户必须选择复选框以接受协议,或者他们可以点击取消按钮来终止对话。取消按钮会在`LendingController`中调用`cancel()`方法,然后反过来调用`checkAndEnd()`。
这里唯一相关的代码是关于协议复选框的。代码摘录如下:
<h:form id="yourConfirmForm"
styleClass="form-horizontal" p:role="form"> ...
<div class="form-group">
<h:outputLabel for="tocAgreed" class="col-sm-6 control-label">
Do you agree with the <em>Terms of Conditions</em>?
</h:outputLabel>
<div class="col-sm-6">
<h:selectBooleanCheckbox class="form-control"
label="TOC Agreement" id="tocAgreed"
value="#{lendingController.applicant.termsAgreed}"
validator="#{lendingController.validateTermsOrConditions}" >
<f:ajax event="blur" render="tocAgreedError"/>
</h:selectBooleanCheckbox>
<h:message id="tocAgreedError"
for="tocAgreed" styleClass="alert validation-error"/>
</div>
</div>
...
</h:form>
我们使用`<h:selectBooleanCheckBox>`在 blur 事件上执行即时 AJAX 验证。这确保了布尔属性在服务器端被设置为 true。然而,我们仍然必须在表单提交时进行验证,正如我们在动作控制器方法中看到的那样:
public String doConfirm() {
if ( applicant.isTermsAgreed()) {
throw new IllegalStateException(
"terms of agreements not set to true");
}
recalculatePMT();
applicant.setSubmitDate(new Date());
applicantService.add(applicant);
return "completion?faces-redirect=true";
}
在`doConfirm()`方法内部,我们重新计算月供期限以确保无误。我们检查申请人的数据值是否没有改变,设置提交日期,然后调用`ApplicationService`将一条新记录插入到数据库中。在这个方法之后,客户被认为是成功申请了。
我们在`isTermsAgreed()`上包含了一个手动检查,因为这是合同中客户接受条款和条件的法律要求。在这里引发一个应用程序错误`IllegalStateException`可能是有争议的。更有可能的是,开发者会向错误日志打印一条消息,并引发异常。Servlet 规范允许捕获不同的异常并将它们发送到特定的错误页面。因此,如果我们创建了一个自定义运行时异常,例如`LegalTermsAgreementException`,我们就可以负责任地处理这些情况。
在生产系统中,这个序列的结束可能会触发一个额外的业务流程。例如,工作消息可能会通过消息总线 JMS 发送到另一个工作区域。在现代数字应用中,客户应该期待收到带有确认和贷款合同详情的电子邮件。当然,这是读者需要额外完成的任务。
以下是对确认视图的截图,`confirm.xhtml`:

让我们转到完成的最终页面视图。''
完成页面视图
``完成阶段很简单。客户已提交申请,所以我们只需通知他或她这一点,然后对话结束。以下是LendingController中doCompletion()方法的完整代码:`''
public String doCompletion() {
checkAndEnd();
return "index?faces-redirect=true";
}
``此方法仅结束对话范围,因为到那时用户的数字客户旅程已经完成。`''
``现在我们有一个完整的流程,一个数字客户旅程。还缺少什么?我们应该添加接受有效银行账户、银行分类代码、IBAN 号码以及与国家银行基础设施集成的步骤!当然,我们还需要一定水平的金融资本,足够的资金来满足监管机构;在英国,这将是由金融行为管理局(www.fca.org.uk/)。`''
``本页面的截图,completion.xhtml,如下所示:`''

实用类
``通常在应用程序中,我们将常见的方法和属性重构到单独的实用类中,这些功能如此普遍,以至于它们在任何特定的包域中都没有意义。我们通常将这些概念放在单例的静态方法中。使用 Java EE,我们可以做得更好。由于 CDI 支持应用范围,我们可以简单地将我们的公共方法移动到一个 POJO 中,并让 CDI 将 Bean 注入到依赖对象中。这是处理数据、时间和月付款期限计算的LendingController示例中的智能方式。`''
``应用范围 Bean DateTimeController充当页面作者视图的辅助工具:`''
package uk.co.xenonique.digital.instant.control;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import java.io.Serializable;
import java.text.DateFormatSymbols;
import java.util.*;
@Named("dateHelperController")
@ApplicationScoped
public class DateHelperController implements Serializable {
private List<Integer> daysOfTheMonth = new ArrayList<>();
private Map<String,Integer> monthsOfTheYear
= new LinkedHashMap<>();
@PostConstruct
public void init() {
for (int d=1; d<=31; ++d) { daysOfTheMonth.add(d); }
DateFormatSymbols symbols = new DateFormatSymbols(Locale.getDefault());
for (int m=1; m<=12; ++m) {
monthsOfTheYear.put(symbols.getMonths()[m-1], m );
}
}
public List<Integer> getDaysOfTheMonth() {
return daysOfTheMonth;
}
public Map<String,Integer> getMonthsOfTheYear() {
return monthsOfTheYear;
}
}
```DateHelperController方法用于your-details.view,并为出生日期字段的下拉日和月生成数据。此代码最初是第四章中ContactDetailsController方法的组成部分,*JSF 验证和 AJAX*。它已被重构以供重用。''
存在另一个具有应用范围的 POJO,它被称为 Utility。''
package uk.co.xenonique.digital.instant.util;
import javax.enterprise.context.ApplicationScoped;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.*;
@ApplicationScoped
public class Utility implements Serializable {
protected List<LoanRateBounds> bounds = Arrays.asList(
new LoanRateBounds("0.0", "4500.0", "22.50"),
new LoanRateBounds("4500.0", "6000.0", "9.79"),
new LoanRateBounds("6000.0", "9000.0", "7.49"),
new LoanRateBounds("9000.0", "11500.0", "4.49"),
new LoanRateBounds("11500.0", "15000.0", "4.29"),
new LoanRateBounds("15000.0", "20000.0", "5.79"),
new LoanRateBounds("20000.0", "25000.0", "6.29"),
new LoanRateBounds("30000.0", "50000.0", "6.99")
);
public BigDecimal getTaxRate( BigDecimal amount ) {
for ( LoanRateBounds bound : bounds ) {
if ( bound.getLower().compareTo(amount) <= 0 &&
bound.getUpper().compareTo(amount) > 0 ) {
return bound.getRate();
}
}
throw new IllegalArgumentException("no tax rate found in bounds");
}
public double calculateMonthlyPayment( double pv, double apr, int np ) {
double ir = apr / 100 / 12;
return (pv * ir) / (1 - Math.pow(1+ir, -np));
}
}
``在上述代码中,方法calculateMonthlyPayment()计算月付款金额。参数是pv(指定本金值)、apr(指定年百分比率)和np,代表以月为单位的提前通知期。`''
``方法getTaxRate()根据本金值查找适当的税率,即客户想要的贷款金额。LoanRateBounds类是一个简单的 POJO,如下面的代码所示:`''
package uk.co.xenonique.digital.instant.util;
import java.math.BigDecimal;
public class LoanRateBounds {
private final BigDecimal lower;
private final BigDecimal upper;
private final BigDecimal rate;
public LoanRateBounds(String lower, String upper, String rate) {
this(new BigDecimal(lower), new BigDecimal(upper),
new BigDecimal(rate));
}
public LoanRateBounds(final BigDecimal lower,
final BigDecimal upper, final BigDecimal rate) {
this.lower = lower;
this.upper = upper;
this.rate = rate;
}
// toString(), hashCode(), equals() and getters omitted
}
``这个LoanRateBounds POJO 是一个不可变对象,并且是线程安全的。''
java`# Composite custom components JSF also features custom components that you, the developer, can write. In fact, the instant secure lending example uses one: the top header of each page view in the conversation. It is a hint that informs the customer where he or she is in the flow. I've called it the `WorkerBannerComponent`. In JSF, a custom component describes a reusable piece of page content that may insert into a Facelet view many times over. A custom component may or may not have a backing bean, and it may or may not group together a set of properties into a form. As mentioned in Chapter 2, *JavaServer Faces Lifecycle*, we can use custom components to build repeated page content that takes advantage of the latest HTML frameworks such as Bootstrap and that abstracts away the deeper details. Businesses can use custom components to establish a common structure for the page content and markup. ## Components with XHTML The `WorkerBannerComponent` is a backing bean for the logic of the display header, which identifies the section of the flow that the customer is active in. The code for the custom component is as follows: 包含 uk.co.xenonique.digital.instant.control; 导入 javax.faces.component.*; 导入 javax.faces.context.FacesContext; 导入 java.io.IOException; @FacesComponent("workerBannerComponent") public class WorkerBannerComponent extends UINamingContainer { private String gettingStartedActive; private String yourDetailsActive; private String yourRateActive; private String yourAddressActive; private String confirmActive; private String completedActive; @Override public void encodeAll(FacesContext context) throws IOException { if (context == null) { throw new NullPointerException("no faces context supplied"); } String sectionName = (String)getAttributes().get("sectionName"); gettingStartedActive = yourDetailsActive = yourRateActive = yourAddressActive = confirmActive = completedActive = ""; if ( "gettingStarted".equalsIgnoreCase(sectionName)) { gettingStartedActive = "active"; } else if ( "yourDetails".equalsIgnoreCase(sectionName)) { yourDetailsActive = "active"; } else if ( "yourRate".equalsIgnoreCase(sectionName)) { yourRateActive = "active"; } else if ( "yourAddress".equalsIgnoreCase(sectionName)) { yourAddressActive = "active"; } else if ( "confirm".equalsIgnoreCase(sectionName)) { confirmActive = "active"; } else if ( "completed".equalsIgnoreCase(sectionName)) { completedActive = "active"; } super.encodeAll(context); } // Getters and setters omitted } java We apply the annotation `@javax.faces.component.FacesComponent` to the POJO `WorkerBannerComponent`. This annotation declares to the JSF that we have a custom component with the name `workerBannerComponent`. The `@FacesComponent` is expanded in JSF 2.2, so that we can write all the code for generating the output HTML in Java. Fortunately, we do not require the ability to create a custom component that also registers its own custom tag, because it is quite handy to control the markup in a lightweight editor like Sublime or VIM. Our `WorkerBannerComponent` extends `javax.faces.component.UINamingContainer`, which is a custom component supplied by JSF with the ability to add a unique identifier. In the JSF parlance, a naming container is a bucket for a component that has a unique name and which can also store the child components with the same characteristics. The overridden method `encodeAll()` is usually the place to render the output of a custom tag that provides its own markup. Here, we second the intent with the logic that decides which worker tab is active and which is not. Similar to the custom event handling in the previous chapter (Chapter 4, *JSF Validation and AJAX*, *Invoking an action event listener* section), we can interrogate the attributes in order to retrieve the parameters that are passed to our component from the page content. Let's examine the page content for this component. The name of the file is `worker-banner.xhtml`, and the extracted page content looks like the following: <cc:interface componentType="workerBannerComponent"> <cc:attribute name="sectionName" required="true"/> </cc:interface> cc:implementation
java In JSF, the custom composite component content must be placed into the special directory, `/resources`. The full path for this content is `/resources/components/workflow-banner.xhtml`. The composite components are registered under an XML namespace, namely [`xmlns.jcp.org/jsf/composite`](http://xmlns.jcp.org/jsf/composite). A custom component requires an interface and an implementation. Facelets define two tags `<cc:interfaces>` and `<cc:implementation>`. The library tag `<cc:interface>` declares a composite component, and the attribute `componentType` references the name with the Java component. Here, it refers to the `WorkerBannerComponent`. The outer tag also encompasses a set of `<cc:attribute>` tags that declare the attributes that a component accepts. In our component, we only accept a `sectionName` attribute, which allows the page author to state where the customer is in their journey. The tag `<cc:implementation>` declares the actual implementation, which is the rendered output. The tag also places a specially named variable called `cc`, which stands for composite component, into the JSF page scope. We can use it to access the properties in the custom composite component, and this special variable is only accessible inside the body content of the `<cc:implementation>` tag. Therefore, the value expression `#{cc.gettingStartedActive}` accesses the property called `gettingStartedActive` in `WorkerBannerComponent`. The logic ensures that only the named section will be highlighted as active through CSS. The logic is placed in a bean, because we need it to execute in the Render-Response phase of the JSF lifecycle rather than in the build time. JSF also adds another special variable into the page scope called component. This variable refers to the actual component being processed during the rendering phase. ### Tip **Why does JSTL not work?** You might have thought that we could have dispensed with the server-side component and solved our banner problem with the good old **JavaServer Pages Tag Library** (**JSTL**). Unfortunately, this will fail to work, because JSF operates with lifecycles and therefore, the later binding of the frameworks makes working with the core JSTL tags such as `<c:if>`, `<c:choose>`, and `<c:set>` unworkable. Besides, good software engineers know that best practice means separating the presentation mark-up from business logic. Although not shown in this section, it is possible to access the supplied attributes on the custom component inside the `<cc:implementation>` body content. The value expression `cc.attrs` provides such access. So if we wanted to write a component to access the input attributes, then we could have retrieved the section name in the markup using `#{cc.attrs.sectionName}`. That is all there is to writing a composite component. In order to use it, we need to add an XML namespace, which is associated with the tag to the page that uses it. Let's see how to use it from the page content for `your-rate.xhtml` from our instant secure loan application, as shown in the following code: <ui:composition template="/basic_layout.xhtml"> <ui:define name="mainContent"> <xen:workflow-banner sectionName="yourRate"/> ... java The namespace is [`xmlns.jcp.org/jsf/composite/components`](http://xmlns.jcp.org/jsf/composite/components), and it is identified by the name **xen**. The special directory `/resources/components` is significant, because JSF searches for the custom components at this location by default. Now we can directly use the component by the element name `<xen:workflow-banner>`. Under the counter, JSF knows that it has to look up a custom component definition called `workflow-banner.xhtml`; it then associates the component type, `WorkerBannerComponent`. To recap, a composite component allows the JSF developers to create reusable dynamic content. A custom component uses an XHTML Facelet view and, usually, a backing bean or another action controller. A composite component may include other template views. There are no restrictions on the sort of markup as long as it is well formed and valid XHTML Facelets. The page author can use this composite component in many pages. Best of all, these composite components have the full support of the JSF action listeners, validators, and convertors. ## Composite components and custom components As mentioned in the preceding section, the JSF custom components make a distinction between the composite parent and the component in the render phase. Inside `<cc:implementation>`, one can access the composite parent with the variable `cc` and the actual processed component with the variable component. An example of the distinction will make it abundantly clear. Let's create a custom composite component with just an XHTML markup. The path of the file, under the web root, is `/resources/components/component-report.xhtml`. cc:interface/
cc:implementation <h:outputText value="Own ID: #{component.id}, parent composite ID: #{cc.id}" />
<h:outputText value="Own ID: #{component.id}, parent composite ID: #{cc.id}" />
java By default, JSF references the XHTML with the basename, component-report. This component just dumps the component and the composite ids to the page . The component is one of the three `<h:outputText>` tags. The parent of these is the composite component itself. In fact, the component can be derived programmatically by invoking the static helper method, `getCompositeComponentParent()`, on the abstract class, `javax.faces.component.UIComponent`. Let's inspect the page view that uses the composite component, `/composite-demo.xhtml`: <ui:composition template="/basic_layout.xhtml"> <ui:define name="mainContent"> Custom Composite Demonstations
<xen:workflow-banner sectionName="gettingStarted"/> <pro:infoSec message="The definition of digital transformation" /> xen:component-report/ Home </ui:define> </ui:composition>java The XHTML element `<xen:component-report>` represents the custom component. It is defined with the namespace [`xmlns.jcp.org/jsf/composite/components`](http://xmlns.jcp.org/jsf/composite/components), the same one as before. The following is a screenshot of the page view illustrating the identifiers for the composite and component tags:  You can see the component ID changes per processing. ## Composite component with self-generating tag In JSF 2.2, `@FacesComponent` has the ability to generate the custom tag without specifying any XML declaration. This feature is in contrast to the previous versions of the specification, where it was a lot harder to write maintainable and reusable custom components. JSF 2.2 adds three additional attributes, including the attribute `createTag`. The following table outlines the attributes for the `@FacesComponent` annotation: | Attribute | Type | Description | | --- | --- | --- | | `value` | `String` | The value of this expression is the name of the custom component. By default, it is the camel case of the POJO class with the first character in lowercase, like any Java identifier. | | `createTag` | `Boolean` | If true, then JSF creates a custom tag for this component. | | `tagName` | `String` | Specifies the tag name for the custom component. | | `namespace` | `String` | Specifies the namespace for the custom component. If none is given, the default is [`xmlns.jcp.org/jsf/component`](http://xmlns.jcp.org/jsf/component). | By way of example, we will write a basic security information custom tag, which is a custom component. The following is the complete code for a custom component called **InfoSecurityComponent**: 包含 uk.co.xenonique.digital.instant.control; 导入 javax.faces.component.FacesComponent; 导入 javax.faces.component.UINamingContainer; 导入 javax.faces.context.FacesContext; 导入 javax.faces.context.ResponseWriter; 导入 java.io.IOException; 导入 java.security.Principal; @FacesComponent( value="informationSecurity", namespace = "http:/www.xenonique.co.uk/jsf/instant/lending", tagName = "infoSec", createTag = true) public class InfoSecurityComponent extends UINamingContainer { private String message; @Override public String getFamily() { return "instant.lending.custom.component"; } @Override public Object saveState(FacesContext context) { Object values[] = new Object[2]; values[0] = super.saveState(context); values[1] = message; return ((Object) (values)); } @Override public void restoreState(FacesContext context, Object state) { Object values[] = (Object[]) state; super.restoreState(context, values[0]); message = (String) values[1]; } public void encodeBegin(FacesContext context) throws IOException { ResponseWriter writer = context.getResponseWriter(); writer.startElement("div", this); writer.writeAttribute("role", "alert", null ); Principal principal = FacesContext.getCurrentInstance() .getExternalContext().getUserPrincipal(); String name; if ( principal !=null ) { writer.writeAttribute("class","alert alert-success",null); name = principal.getName(); } else { writer.writeAttribute("class","alert alert-danger",null); name = "unknown"; } writer.write( String.format("[USER: %s] - %s", name, message)); } public void encodeEnd(FacesContext context) throws IOException { ResponseWriter writer = context.getResponseWriter(); writer.endElement("div"); writer.flush(); } // Getter and setter omitted } java Once again, our `InfoSecurityComponent` component extends the class `UINamingContainer`, because this handles many useful JSF interfaces such as `NamingContainer`, `UniqueIdVendor`, `StateHolder`, and `FacesListener`. We annotate with `@FacesComponent`, and this time, we supply the namespace, `createTag`, and value tag. The `getFamily()` method specifies the collection that this component belongs to. It is helpful if you are creating a reusable library of components for distribution, and effectively aids the third-party programming tools. The `saveState()` and `restoreState()` methods demonstrate how we persist the state of a component over multiple HTTP requests. The reason for the existence of `StateHelpert` is the impedance between the JSF lifecycle and the stateless nature of HTTP. As you already know by now, JSF builds a dynamic graph of the component tree for a page. The state of this tree changes during the transition between the JSF phases. Saving the states allows JSF to preserve the information from the web form, when the user submitted the page. If there is a failure during conversion or validation, JSF can restore the state of the view. In the `saveState()` method, we create an `Object[]` array of the necessary size and fill it with values. The first element of the array must be the saved context. On the other hand, JSF invokes the `loadState()` method with the object state, which is also an `Object[]` array. We ignore the first element, because this is an irrelevant and, probably, a stale context from the previous request. We reassign the properties from the remaining elements of the array. The methods `encodeBegin()` and `encodeEnd()` are where the real fun happens. These methods are designed for rendering the markup of the custom component, the tag's output. Because the custom component may embed other components, it is a good idea to split the rendered output. Here, we are using `javax.faces.context.ResponseWriter` to build up the HTML output. The abstract class has methods called `startElement()` and `endElement()` to render the content at the beginning and at the end of the markup element respectively. The method `writeAttribute()` handles the markup attributes of the element. So `InfoSecurityComponent` renders a div layer element with the Bootstrap CSS alert classes. It attempts to retrieve the name of the current Java EE security principal, if one has been defined, and displays that information to the customer. When given the XHTML page view: <html ... > ... <pro:infoSec message="Hello world component" /> java The output HTML should look like this: java Note that the namespace in the XHTML matches the custom tag's annotation. Take a look at the following screenshot of the view of `your-rate.xhtml` running on an iOS Simulator. It demonstrates the responsive web design features of the application:  # Summary We made tremendous strides in this chapter towards a working application by examining a popular and contemporary business model. We looked at how conversational scope could help drive an instant secure lending application. Conversation scope allows us to easily write the customer journey and the wizard form that takes the user gradually through a process. Conversation scope ensures that data is stored over a lifecycle between the request and the session scopes. We talked very briefly about a useful design pattern called Entity-Control-Boundary. It was revealed how this pattern is similar to the MVC pattern. Along the way, we saw a JavaScript module that linked an HTML5 range component together with a Bootstrap CSS Progress element. We studied how JSF provides AJAX with partial updates of a view. We also learnt that we could replace static singleton classes with the CDI application-scoped POJOs. Finally, we took a deep dive into custom composite components. We now know how to write a section banner component and even provide information about the Java EE security principal to a page view. JSF 2.2 is definitely a fun standard to play with. I think by now you agree that it fits the modern web architecture very well. In the next chapter, we will look at Faces Flow. # Exercises 1. Describe, in simple steps, the digital customer journey for change of address for your local bank. You might start with the identification step first. Don't be tempted to drill down deep into banking security; instead remain at the altitude of 30,000 features, and list or tabulate steps on what you might expect to see. 2. At this stage of the study, you need to know how to persist data to a backing store. If you haven't done so already, revise your favorite persistence layer, be it JPA, Hibernate, Mongo DB, or something else. Do you know how to retrieve, save, amend, and remove entities from your persistence store? 3. Copy and rename the blank application, and write a simple conversational scope bean that captures web comments like a guest book. 4. Ensure that your backing bean uses `@ConversationScoped`. What happens to the guest book before a conversation starts? Is the information retained? (A guest book in the early digital stage was a very simple web application that allowed an Internet user to write a comment on a web page. The list of comments would grow and grow until the machine restarted or crashed. Nowadays nobody would write such a professional application and deploy it on a website.) 5. Open another browser and point it to the same guest book application. Do you see the same guest entries as before or afterwards? 6. Let's return to the Hobby Book Club application that you have been building through the previous chapters. We will add to it now by allowing books to be reviewed as a conversation. We'll keep it simple with user stories: * As a reviewer, I want to be able to add my book reviews to the club's website * As a reviewer, I want to see other people's reviews of the books including my own * As a reviewer, I want to edit any reviews * As a reviewer, I want to delete any reviews 7. Write a `@ConversationScoped` backing bean that handles the customer journey of adding a book review, amending, and removing it. To break this task down to easier milestones, you might prefer to first store the data records using the basic Java collection, without persistence into memory. After building the functionality, you can use a real database. 8. The Conversation scope is ideal for data-capture applications, especially where the user is entering information across several complex sections. Consider a business website that captures résumés (curriculum vitaes): CV Entry Application. Write the output of the customer journey, given the following sections: * Personal information (full name, qualification) * Address (home address, e-mail, and phone numbers) * Skills matrix (professional industry skills) * Work experience (places of employment) * Achievements (awards) * Education (education) 9. Map out the customer journey for the CV Entry Application. Build a website project using `@ConversationScoped` that captures this information. Apply the KISS (Keep It Simple Stupid) principle. You only need to demonstrate the conversation state across multiple pages and not build a complete professional application. 10. In the CV Entry Application, have you taken care of tracking the user journey in the page views? How does the user know where he or she is in the process? Write a custom component that enables this UX feature. 11. What is the difference between a custom component and a composite component? Do any of these component types require backing beans? 12. In the CV Entry Application, there are probably other areas where content reuse can be applied. Write a composite component that captures the skill set entry. You probably require a collection: `Collection<SkillSet>`, where the SkillSet entity has the properties: `title (String)`, `description (String)`, and `years or months of experience (Integer)`. How did you organize the data structure so that the order of the skills presented remain exactly the same as the user entered them? Is this an embellishment for an advanced skill-set a CRUD by itself?
第六章. JSF 流程与优雅
| "我有机缘驾驶过很多不同的飞机,但那次航天飞机的体验完全不同。" | ||
|---|---|---|
| --指挥官克里斯·哈德菲尔德 |
本章介绍 JSF 2.2 中的新特性 Faces Flow。流程的概念源于工作流程和业务流程管理的概念。工作流程通常是一系列有序且可重复的业务活动,旨在高效地完成一个可实现的单位工作。这项工作可能涉及状态转换、数据处理以及/或提供服务或信息。
在许多网络电子商务应用中,结账流程是工作流程的一个很好的例子,它对用户来说是可见的。当你从亚马逊购买产品时,网站会带你到一个网站上的单独区域输入详细信息。在幕后,亚马逊会优雅地将你从负责处理电子和摄影部分产品的微服务,移动到结账工作流程的第一步微服务。你登录你的账户或创建一个新的账户,然后你决定送货地址。接下来,你用信用卡或借记卡支付,亚马逊会要求你提供一个发票地址。最后,你可以选择你希望产品如何交付。你可以选择将项目分组,也可以选择快递或普通配送。亚马逊的复杂工作流程难以复制;然而,JSF 允许数字开发者从基本的简单流程构建起来。
工作流程也出现在面向丰富用户客户端的应用程序中,尤其是政府及金融服务行业。你可能见证过类似工作流程的应用程序,它们是案例工作系统、交易系统和仓库系统。其基本思想是相同的,即引导员工从业务流程的开始到结束,通过不同的步骤。
在 JSF 2.2 中,Faces Flow 提供了创建类似于通用应用程序工作流程的行为和用户体验的基本编程 API。Apache MyFaces CODI(编排模块)、Spring Web Flow 和专有的 Oracle 应用程序开发框架(ADF)等开源框架启发了 Faces Flow 的设计。
什么是 Faces Flow?
Faces Flow 是将具有特殊作用域的后台 bean 及其相关页面封装到一个模块中。一个 Faces Flow 是一个具有单个、明确定义的入口点和一到多个出口点的模块。应用程序开发者决定 Faces Flow 的组成方式和其功能。换句话说,Faces Flow 是一个低级 API,而其他框架,特别是具有 BPM 的框架,具有高级配置和宏观流程功能。
-
JSF Faces Flow 在执行上是模块化的;一个流程可以以嵌套的方式调用另一个流程。
-
Faces Flow 可以将参数传递给另一个嵌套流程,嵌套流程也可以通过一个称为 Flow Scope 的特殊映射属性返回数据。
-
应用程序开发者可以将流程与相应的页面打包到一个模块中,该模块可以分发给第三方开发者。
-
有一个全新的作用域称为
FlowScoped,它表示一个 POJO 是否是 flow-scoped 实例。这个注解是@javax.faces.flow.FlowScoped。flow-scoped 实例与 CDI 兼容;因此,你可以使用熟悉的 Java EE 注解,并按顺序注入其他实例和 EJB 元素。 -
你可以像使用
@RequestScoped、@ConversationScoped、@SessionScoped和@ApplicationScoped实例一样,编写 action-controller 方法并在 flow-scoped 实例中处理逻辑。
流程定义和生命周期
Faces Flows 使用 @FlowScoped 实例,用户可以进入一个单页,这被称为起始页。进入流程后,用户可以导航与流程关联的页面。用户可以在预定义的点上退出流程。一个流程可以调用嵌套流程。
@FlowScoped 实例的生命周期长于 @ViewScoped 实例,但短于 @SessionScoped 实例。因此,我们可以将 flow-scoped 实例与它们的对话兄弟进行比较。一个 @ConversationalScoped 实例维护浏览器中所有视图和网页标签的状态。像它们的对话伙伴一样,@FlowScoped 实例可以存活多个请求;实际上,它们甚至更好,因为它们在会话中有多个窗口的不同实例。flow-scoped 实例不会在浏览器标签之间共享。
当用户进入和离开应用程序中的流程时,Faces Flows 有一个专门的 CDI 作用域,JSF 框架实现使用它来激活和钝化实份数据。
当用户离开一个流程时,该实例就会受到 JVM 的垃圾回收。因此,与 @SessionScoped 和 @ConversationalScoped 实例相比,流程通常有较低的内存需求。
下图说明了 Faces Flow 的作用域。

简单隐式 Faces Flow
使用文件夹名称、一个空的 XML 配置和一些 Facelet 页面创建隐式 Faces Flow 是相对直接的。flow 是你 web 应用程序中的一个文件夹名称,最好在根目录下。我们从同一名称的目录中的基本流程 digitalFlow 开始。你的流程必须与文件夹名称匹配。
为了定义隐式流程,我们创建一个具有通用基本名称和后缀的空 XML 文件:digitalFlow/digitalFlow-flow.xhtml。
我们现在在具有通用基本名称的文件夹中创建一个起始页。这个文件是一个 Facelet 视图页面,称为 digitalFlow/digitalFlow.xhtml。
我们可以在文件夹内创建其他页面,并且它们可以有任何我们喜欢的名称。我们可能有 digitalFlow/digitalFlow1.xhtml、digitalFlow/checkout.xhtml 或 digitalFlow/song.xhtml。只有定义的流程 digitalFlow 可以访问这些页面。如果外部调用尝试访问这些页面中的任何一个,JSF 实现将报告错误。
为了退出隐式流程,我们必须在 Web 应用的根目录中提供一个特殊的页面 /digitalFlow-return.xhtml,这意味着该文件位于文件夹之外。
隐式导航
让我们将这些知识运用到我们的第一个 Faces Flow 导航示例中。在源代码中,项目被命名为 jsf-implicit-simple-flow。检查这个项目的文件布局是有帮助的,布局如下:
src/main/java
src/main/webapp
src/main/webapp/WEB-INF/classes/META-INF
src/main/webapp/index.xhtml
src/main/webapp/assets/
src/main/webapp/resources/
src/main/webapp/basic-layout.xhtml
src/main/webapp/view-expired.xhtml
src/main/webapp/digitalFlow/
src/main/webapp/digitalFlow/digitalFlow.xml
src/main/webapp/digitalFlow/digitalFlow.xhtml
src/main/webapp/digitalFlow/digitalFlow-p2.xthml
src/main/webapp/digitalFlow/digitalFlow-p2.xthml
src/main/webapp/digitalFlow/digitalFlow-p4.xthml
src/main/webapp/digitalFlow-return.xhtml
在研究前面的布局时,你会注意到项目有一个标准的首页 index.xhtml,正如我们所期望的那样。它有一个 digitalFlow 文件夹,这是网站专门为这个 Faces Flow 设定的特殊区域。在这个目录中,有一系列 Facelet 文件和一个配置。起始页面被称为 digitalFlow.xhtml,还有一个空的 XML 文件用于流程定义,digitalFlow.xml。
到现在为止,你已经知道了资产和资源文件夹的目的,但我们将很快回到 view-expired.xhtml 文件。我们如何确保我们的文件夹结构被当作 Faces Flow 处理?
流程作用域的 bean
使用注解 @javax.faces.flow.FlowScoped,我们将一个 POJO 定义为一个流程作用域的 bean。以下是我们第一个 Faces Flow 的代码,它是一个后端 bean:
package uk.co.xenonique.digital.flows.control;
import javax.faces.flow.FlowScoped;
import javax.inject.Named;
import java.io.Serializable;
@Named
@FlowScoped("digitalFlow")
public class DigitalFlow implements Serializable {
public String debugClassName() {
return this.getClass().getSimpleName();
}
public String gotoPage1() {
return "digitalFlow.xhtml";
}
public String gotoPage2() {
return "digitalFlow-p2.xhtml";
}
public String gotoPage3() {
return "digitalFlow-p3.xhtml";
}
public String gotoPage4() {
return "digitalFlow-p4.xhtml";
}
public String gotoEndFlow() {
return "/digitalFlow-return.xhtml";
}
}
简单控制器类 DigitalFlow 有 gotoPage1() 和 gotoPage2() 等动作方法,用于将用户移动到流程中的适当页面。gotoEndFlow() 方法导航到 JSF 检测到的返回 Facelet 视图,以便退出流程。
@FlowScoped 注解需要一个单个的 String 值参数,在这个情况下与文件夹名称 digitalFlow 匹配。我们现在将转到视图。
Facelet 视图
我们在 Facelet 视图中使用流程作用域的 bean,就像我们使用任何其他 CDI 作用域的 bean 一样。以下是从首页 index.xhtml 的摘录:
<!DOCTYPE html>
<html
>
<ui:composition template="/basic_layout.xhtml">
<ui:define name="mainContent">
<h1> JSF Implicit Simple Flow</h1>
<p>
Welcome to a simple Faces Flow...
</p>
<div class="content-wrapper">
<h:form>
<h:commandButton styleClass="btn btn-primary btn-lg"
action="digitalFlow" value="Enter Digital Flow" />
</h:form>
</div>
<!-- ... -->
</ui:define> <!--name="mainContent" -->
</ui:composition>
</html>
在前面的视图中,<h:commandButton>定义了一个名为流程的名称的动作,digitalFlow。调用此动作会导致 JSF 进入 Faces Flow,与后端 bean 的相应注解名称匹配。
JSF 认识到流程具有隐式导航,因为 XML 流程定义文件digitalFlow.xml为空。此文件必须存在;否则,实现会报告错误。文件内也不需要开始或结束标签。
当用户调用按钮时,JSF 在将其转发到起始页面之前实例化一个流程作用域 bean。以下是从流程digitalFlow.xhtml起始页面的摘录:
<html ...>
<ui:composition template="/basic_layout.xhtml>
<ui:define name="mainContent">
<!-- ... -->
<div class="content-wrapper">
<h1>Page <code>digitalFlow.xhtml</code></h1>
<p>View is part of a flow scope? <code>
#{null != facesContext.application.flowHandler.currentFlow}
</code>.</p>
<table class="table table-bordered table-striped">
<tr>
<th>Expression</th>
<th>Value</th>
</tr>
<tr>
<td>digitalFlow.debugClassName()</td>
<td>#{digitalFlow.debugClassName()}</td>
</tr>
</table>
<h:form prependId="false">
<h:commandButton id="nextBtn1"
styleClass="btn btn-primary btn-lg"
value="Next Direct" action="digitalFlow-2" />
 
<h:commandButton id="nextBtn2"
styleClass="btn btn-primary btn-lg"
value="Next Via Bean" action="#{digitalFlow.gotoPage2()}" />
 
<h:commandButton id="exitFlowBtn1"
styleClass="btn btn-primary btn-lg"
value="Exit Direct" action="/digitalFlow-return" />
 
<h:commandButton id="exitFlowBtn2"
styleClass="btn btn-primary btn-lg"
value="Exit Via Bean" action="#{digitalFlow.gotoEndFlow()}" />
 
</h:form>
</div>
</ui:define> <!--name="mainContent" -->
</ui:composition>
</html>
该视图展示了使用熟悉的表达式语言调用流程作用域 bean 以及直接页面间导航的调用。为了允许用户移动到流程中的第二页,我们有两个命令按钮。具有属性 action 和值digitalFlow-2的命令按钮是页面导航直接,无需验证任何表单输入。具有属性 action 和表达式语言值#{digitalFlow.gotoPage2()}的命令按钮是对流程作用域 bean 方法的调用,这意味着执行整个 JSF 生命周期。
提示
如果您忘记了生命周期中的不同阶段,请参阅第二章,JavaServer Faces 生命周期。
在此视图中,我们还生成了输出#{digitalFlow.debugClassName()},以说明我们可以在流程作用域 bean 中调用任意方法。
让我也提醒您注意以下实际内容中建立视图是否为流程一部分的表达式语言:
#{null != facesContext.application.flowHandler.currentFlow}
这在功能上等同于以下 Java 语句:
null == FacesContext.getCurrentInstance().getApplication()
.getFlowHandler().getCurrentFlow()
其他页面,digitalFlow-p2.xhtml和digitalFlow-p3.xhtml,非常相似,因为它们只是添加了命令按钮以导航回前一个视图。您可以在书籍源代码分布中看到完整的代码。
提示
在数字工作中,我们经常为某些 HTML 元素提供众所周知的标识符(ID),尤其是表单控件。当我们编写用于网络自动化测试的测试脚本时,这非常有帮助,尤其是在使用 Selenium Web Driver 框架(www.seleniumhq.org/)时。
我们将跳转到最终视图,digitalFlow-p4.xhtml,并仅提取表单元素进行研究:
<h:form prependId="false">
<h:commandButton id="prevBtn1"
styleClass="btn btn-primary btn-lg"
value="Prev Direct" action="digitalFlow-p3" />
 
<h:commandButton id="prevBtn2"
styleClass="btn btn-primary btn-lg"
value="Prev Via Bean" action="#{digitalFlow.gotoPage3()}" />
 
<h:commandButton id="exitFlowBtn1"
styleClass="btn btn-primary btn-lg"
value="Exit Direct" action="/digitalFlow-return" />
 
<h:commandButton id="exitFlowBtn2"
styleClass="btn btn-primary btn-lg"
value="Exit Via Bean" action="#{digitalFlow.gotoEndFlow()}" />
 
</h:form>
如我们所见,前面的内容说明了如何直接通过DigitalFlow后端 bean 进行导航。为了在隐式导航中退出流程,用户必须触发一个事件,将 JSF 框架带到/digitalFlow-return.xhtml视图,这将导致流程结束。直接导航到返回模式会跳过验证,并且表单输入元素中的任何数据都会丢失。如果我们想将表单输入元素作为表单请求进行验证,我们必须在动作控制器、后端 bean 中调用一个方法。
这就是 JSF Faces Flow 中的隐式导航。
真的是这么简单,所以让我们看看这个简单流程的截图,从主页/index.html开始:

DigitalFlow的起始页面digital-flow.xhtml看起来如下截图所示:

退出流程后,用户看到视图/digitalFlow-return.xhtml。以下是这个视图的截图:

如果我们在进入流程之前尝试通过已知的 URI /digitalFlow/digitalFlow.xhtml 直接访问 Facelet 视图,视图将类似于以下截图:

使用 GlassFish 4.1,我们收到一个 500 的 HTTP 响应码,这是一个内部服务器错误。此外,CDI 容器抛出一个异常,表示没有活动的流程作用域。
处理视图过期
我承诺过,我们会给我们的数字应用增加一些精致。如果你已经使用 JSF 了一段时间,你可能有一份相当多的带有javax.faces.application.ViewExpiredException作为根本原因的堆栈跟踪。这是最臭名昭著的异常之一。你可以增加 HTTP 会话的生存时间来补偿过时的请求,但一个普通人离开电脑有多长时间呢?同时,对象会持续存在于内存中。有一种更好的方法,那就是使用 Web XML 部署描述符。
在应用的web.xml文件中,我们需要触发一个重定向到更令人愉悦的错误页面。以下是从 XML 文件中提取的内容:
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
<!-- ... -->
<error-page>
<exception-type>
javax.faces.application.ViewExpiredException
</exception-type>
<location>/view-expired.xhtml</location>
</error-page>
<!-- ... -->
</web-app>
<error-page>元素指定了异常类型和页面视图之间的关联。每当 JSF 遇到异常类型ViewExpiredException时,它将响应动作推进到页面视图/view-expired.xhtml。此行为的情况在以下截图中有说明:

我相信你们会同意,我们的公开客户会喜欢这个改进的专用页面视图,而不是对堆栈跟踪感到困惑。
提示
流程范围内的 Bean 依赖于 CDI,因此它们需要一个 Java EE 7 环境,这决定了 CDI 1.1 容器,例如 JBoss Weld。您还必须使用@Named而不是较旧的@ManagedBean注解。如果您不使用 WildFly 8 或 GlassFish 4,那么在深入代码之前,请检查您容器的实现是否支持最新的 JSF 和 CDI 规范。
与对话范围 Bean 的比较
如果您还记得,在使用@ConversationScoped Bean 时,我们必须显式地标记对话的状态。我们注入了Conversation实例,并在数字客户旅程的特定点上调用begin()和end()方法。使用@FlowScoped CDI Bean,作用域将在定义的点自动开始和结束。
捕获流程范围 Bean 的生命周期
由于 CDI 容器管理流程范围 Bean,它们可以正常参与上下文生命周期。我们可以使用@PostConstruct注解来初始化 Bean,获取数据库资源或计算可缓存的缓存数据。同样,当流程超出作用域时,我们可以使用@PreDestroy注解方法。
声明性和嵌套流程
到目前为止,我们已经看到了隐式流程的实际应用。对于最简单的流程,隐式流程非常直接,它就像一个基本的网页向导,用户可以线性地导航,向前和向后移动。它还可以使用随机访问来导航到页面。
如果我们想要进一步扩展 Faces Flow,那么我们必须深入研究 XML 流程定义,但首先让我们定义一些术语。
流程节点术语
受工作流和 BPM 启发的根本技术,Faces Flow 规范声明了以下表格中给出的不同类型的节点:
| 节点类型名称 | 描述 |
|---|---|
| 视图 | 表示任何类型的 JSF 应用视图 |
| 方法调用 | 表示通过表达式语言(EL)在流程图中进行方法调用 |
| 流程调用 | 表示调用另一个流程,带有出站(调用)和入站(返回)参数 |
| 流程返回 | 表示返回到调用流程 |
| 切换 | 表示通过 EL(表达式语言)确定的逻辑进行导航选择 |
以下是一个说明两个流程的示例,这两个流程代表了一个购物车业务流程。外部流程调用嵌套流程,该流程处理配送方式。

XML 流程定义描述文件
由于 <<Flowname>> 是 web 应用程序中的一个文件夹名称,流程描述文件名与该模式匹配,即 <<Flowname>>/<<Flowname>>.xml。此描述文件的内容声明了 JSF 流的某些特性。它可以定义一个替代起始页,定义一组返回结果,并将某些结果映射到特定的页面。描述符还可以定义将结果映射到特定页面的条件布尔逻辑。
以下是一个 XML 流定义文件的示例:
<?xml version='1.0' encoding='UTF-8'?>
<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="flow-id">
<start-node>startPage</start-node>
<view id="startPage">
<vdl-document>/flowname/start.xhtml</vdl-document>
</view>
<view id="inside-flow-id-1"> <vdl-document>
/flowname/inside-flow-id-1.xhtml </vdl-document>
</view>
<view id="inside-flow-id-2"> <vdl-document>
/flowname/inside-flow-id-2.xhtml </vdl-document>
</view>
<flow-return id="return-from-flow-id-1">
<from-outcome>/outside-page-1</from-outcome>
</flow-return>
<flow-return id="return-from-flow-id-2">
<from-outcome>/outside-page-21</from-outcome>
</flow-return>
</flow-definition>
</faces-config>
根元素必须是带有适当 XML 命名空间的 <faces-config> 标签。您可能会对这种根元素的选择感到惊讶。这是因为流程定义可以在整个应用程序范围内全局定义,通过在 /WEB-INF/faces-config.xml 文件中设置流程定义来实现。然而,这种做法是一个高级用例,不建议用于模块化开发。
流定义标签
<flow-definition> 元素通过定义的流程 ID 建立 Faces 流。标识符的值必须与 @FlowScoped 实例的值匹配。此元素包含一系列标签,用于建立起始页、视图、流程返回或条件切换语句。
必要的流程返回标签
Faces 流必须至少有一个返回结果。<flow-return> 元素通过 ID 建立一个流程返回节点。它必须包含一个 <flow-outcome> 元素,其主体内容指定了 Facelet 视图。
视图页面标签
流可以可选地定义一组视图节点(页面),这些节点对于在范围内从一视图直接导航到另一视图是有用的。<view> 标签通过 <vdl-document> 元素建立视图描述语言节点。视图需要一个标识符。此标签的主体内容简单地引用了一个 Facelet 视图。因此,视图 ID 不一定必须与 Facelet 视图同名。
可选的起始页标签
开发者可以覆盖默认起始页的名称并提供一个替代视图。元素 <start-node> 的主体内容指定了视图 ID,因此引用了一个适当的流程,即视图节点。
切换、条件和案例标签
流定义通过一个名为 <switch> 的元素标签为开发者提供了定义条件逻辑的能力。这是 XML 中低级功能中的最高级。<switch> 标签指定了一个切换节点,这与在 /WEB-INF/faces-config.xml 文件中的 <navigation-case> 内使用 <if> 标签非常相似,显然是为了非流程导航。切换允许在流程中通过评估带有条件逻辑的 EL 表达式来映射多个 Facelet 视图。
以下是一个扩展的 XML 流定义示例文件:
<faces-config version="2.2" ...>
<flow-definition id="flow-id">
...
<switch id="customerPaymentTab">
<case>
<if>
#{controller.paymentType == 'CreditCard'}
</if>
<from-outcome>creditcard</from-outcome>
</case>
<case>
<if>
#{controller.paymentType == 'DebitCard'}
</if>
<from-outcome>debitcard</from-outcome>
</case>
<case>
<if>
#{controller.paymentType == 'PayPal'}
</if>
<from-outcome>PayPal</from-outcome>
</case>
<default-outcome>bacs-direct</default-outcome>
</switch>
...
</flow-definition>
</faces-config>
<switch>标签包含多个<case>元素和一个单独的<default-outcome>元素。每个<case>元素包含一个<if>和<from-outcome>元素。<if>的正文内容定义了一个条件逻辑 EL。<from-outcome>映射到最终的 Facelet 视图或引用视图节点的标识符。为 Switch 节点设置一个默认结果是很好的主意。当所有 case 条件都不为真时,<default-outcome>的正文内容确定这个结果。
在这个例子中,我们有一个在电子商务网站结账过程中使用的虚拟支付控制器。当流程遇到customerPaymentTab结果,这是 Switch 节点的标识符时,JSF 按顺序处理每个 case 条件逻辑。如果这些条件测试中的任何一个评估为真,那么 JSF 就选择该结果作为 switch 的结果。比如说#{controller.paymentType == 'DebitCard' }为真,那么借记卡就是选择视图。如果没有测试评估为真,那么结果视图是bacs-direct。
提示
但是,所有的逻辑不都应该在控制器中定义而不是在 Switch 节点中吗?无论哪种方式,答案都是具有争议性的,并且取决于具体情况。如果你作为一个库开发者构建一个具有复杂 Faces Flow 图的复杂应用程序,那么可以争论这种灵活性对于外部配置器是有益的。如果你在构建简单应用程序,那么遵循 YAGNI(You Aren't Going to Need it)的实践可能有助于最小可行产品。
这涵盖了我们需要了解的所有关于基本流程定义文件的内容。现在让我们继续看嵌套声明性流程。
这是一个嵌套流程的例子
是时候看看嵌套声明性流程定义的 Faces Flow 的实际例子了。我们的应用程序很简单。它允许客户记录碳足迹数据。所以首先让我们定义一个数据记录:
package uk.co.xenonique.digital.flows.entity;
import javax.persistence.*;
import java.io.Serializable;
@Entity
@Table(name = "CARBON_FOOTPRINT")
@NamedQueries({
@NamedQuery(name="CarbonFootprint.findAll",
query = "select c from CarbonFootprint c "),
@NamedQuery(name="CarbonFootprint.findById",
query = "select c from CarbonFootprint c where c.id = :id"),
})
public class CarbonFootprint implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String applicationId;
private String industryOrSector;
// KWh (main source)
private double electricity;
// KWh (main source)
private double naturalGas;
// Litres (travel commute costs)
private double diesel;
// Litres (travel commute costs)
private double petrol;
public CarbonFootprint() { }
// hashCode(), equals() and toString()
// Getters and setters omited
}
CarbonFootprint是一个 JPA 实体 bean,它声明了一组属性来存储客户的碳足迹数据。用户可以提供他们的行业或部门,以及他们在一段时间内消耗的电力、天然气、柴油和汽油的数量。记录还有一个applicationId值,我们将利用它。
让我们查看这个项目的文件布局:
src/main/java
src/main/webapp
src/main/webapp/WEB-INF/classes/META-INF
src/main/webapp/index.xhtml
src/main/webapp/assets/
src/main/webapp/resources/
src/main/webapp/basic-layout.xhtml
src/main/webapp/view-expired.xhtml
src/main/webapp/section-flow/
src/main/webapp/section-flow/section-flow.xml
src/main/webapp/section-flow/section-flow.xhtml
src/main/webapp/section-flow/section-flow-1a.xthml
src/main/webapp/section-flow/section-flow-1b.xthml
src/main/webapp/section-flow/section-flow-1c.xthml
src/main/webapp/footprint-flow/
src/main/webapp/footprint-flow/footprint-flow.xml
src/main/webapp/footprint-flow/footprint-flow.xhtml
src/main/webapp/footprint-flow/footprint-flow-1a.xml
src/main/webapp/endflow.xhtml
该项目名为 jsf-declarative-flows,并作为书籍源代码的一部分提供。该项目包含两个流程:section-flow 和 digital-flow。章节流程捕捉了这个虚构示例中的行业信息。足迹流程捕捉能源消耗数据。这两个流程共享一个客户详细信息记录,即 CarbonFootprint 实体对象,您将在后面的 backing beans 中看到。
XML 流程定义
以下是对应于行业流程的 XML 流程定义,sector-flow/sector-flow.xhtml:
<faces-config version="2.2" ...>
<flow-definition id="sector-flow">
<flow-return id="goHome">
<from-outcome>/index</from-outcome>
</flow-return>
<flow-return id="endFlow">
<from-outcome>#{sectorFlow.gotoEndFlow()}</from-outcome>
</flow-return>
<flow-call id="callFootprintFlow">
<flow-reference>
<flow-id>footprint-flow</flow-id>
</flow-reference>
<outbound-parameter>
<name>param1FromSectorFlow</name>
<value>param1 sectorFlow value</value>
</outbound-parameter>
<outbound-parameter>
<name>param2FromSectorFlow</name>
<value>param2 sectorFlow value</value>
</outbound-parameter>
<outbound-parameter>
<name>param3FromSectorFlow</name>
<value>#{sectorFlow.footprint}</value>
</outbound-parameter>
<outbound-parameter>
<name>param4FromSectorFlow</name>
<value>#{sectorFlow.footprint.applicationId}</value>
</outbound-parameter>
</flow-call>
</flow-definition>
</faces-config>
此流程的标识符为 sector-flow,与文件夹名称匹配。它还建立了 backing bean、动作控制器上的 @FlowScoped 值,正如我们稍后将要看到的。
有两个流程返回节点,即 goHome 和 endFlow。goHome 具有直接导航到主页的功能,而 endFlow 通过 EL 值 #{sectorFlow.gotoEndFlow()} 在 bean 上调用操作。这种技术特别有用,可以确保在允许客户完成他们的数字旅程之前,他们已经输入了正确并经过验证的数据。
新节点 callFootprintFlow 代表嵌套流程调用。元素 <flow-call> 定义了一个流程调用节点。它必须有一个 <flow-reference> 标签元素,其中嵌套 <flow-id> 标签。后者的主体内容定义了目标流程的标识符。
<outbound-parameter> 元素指定了如何将参数和值对传递到目标流程。每个参数都需要一个 <name> 和一个 <value> 元素。调用流程中传出参数的名称必须与被调用流程中传入参数的名称匹配。
param1FromSectorFlow 和 param2FromSectorFlow 展示了如何从一个流程传递字面字符串值到另一个流程。如果您传递的是数值,那么您必须在目标流程中自行编码和解码这些值。参数 param3FromSectorFlow 和 param4FromSectorFlow 也说明了如何使用 EL。注意我们如何轻松地将实体记录 #{sectorFlow.footprint} 从行业流程传递到足迹流程。我们还可以像在最后一个参数中那样传递单个属性:#{sectorFlow.footprint.applicationId}。
以下是对应于足迹流程的 XML 流程定义,footprint-flow/footprint-flow.xml:
<faces-config version="2.2" ...>
<flow-definition id="footprint-flow">
<flow-return id="goHome">
<from-outcome>/index</from-outcome>
</flow-return>
<flow-return id="exitFromFootprintFlow">
<from-outcome>#{footprintFlow.exitFromFootprintFlow}</from-outcome>
</flow-return>
<flow-return id="exitToSectionFlow">
<from-outcome>/section-flow</from-outcome>
</flow-return>
<inbound-parameter>
<name>param1FromSectorFlow</name>
<value>#{flowScope.param1Value}</value>
</inbound-parameter>
<inbound-parameter>
<name>param2FromSectorFlow</name>
<value>#{flowScope.param2Value}</value>
</inbound-parameter>
<inbound-parameter>
<name>param3FromSectorFlow</name>
<value>#{flowScope.param3Value}</value>
</inbound-parameter>
<inbound-parameter>
<name>param4FromSectorFlow</name>
<value>#{flowScope.param4Value}</value>
</inbound-parameter>
</flow-definition>
</faces-config>
此流程通过名称 footprint-flow.xml 进行标识。它有一组返回流程节点。goHome 节点实际上会退出流程到调用主页,尽管视图文档值为 /index。您可能会认为这种行为很奇怪。然而,JSF 是正确的,因为当前的流程足迹是一个嵌套流程,它将流程指针驱动到调用流程 sector 的状态。节点 exitFromFootprintFlow 和 exitToSectionFlow 代表不同的导航策略,分别是间接和直接。
<inbound-parameter> 元素的集合指定了流向流程的传入参数。参数的名称非常重要,因为它们必须与调用流程中相应的 <outbound-parameter> 元素中的名称匹配。值定义了 EL 对象引用,它说明了这些值被写入的位置。换句话说,在 Faces Flow 中的参数传递行为类似于映射属性名称传输。
让我们分析 sector-flow.xml 的第三个参数。它将实体记录实例 CarbonFootprint 的值发送到嵌套的 Footprint 流程:
<outbound-parameter>
<name>param3FromSectorFlow</name>
<value>#{sectorFlow.footprint}</value>
</outbound-parameter>
在 footprint-flow.xml 中,传入参数名称与传入参数名称匹配:
<inbound-parameter>
<name>param3FromSectorFlow</name>
<value>#{flowScope.param3Value}</value>
</inbound-parameter>
EL 指定了 JSF 设置参数值到对象属性的位置。在这种情况下,flowScope 映射集合有一个键和一个值设置。我们可以使用任何具有生命周期大于或等于流程生命周期的现有对象。我们倾向于使用 flowScope,因为它旨在在 Face Flows 之间传递参数。因为我们可以在对象中引用属性,所以我们也有从嵌套流程返回信息的能力。调用流程可以在嵌套流程结束后检索值。
现在我们已经从 XML 视角理解了嵌套流程,我们可以查看 Java 源代码。
流程 Bean
sector-flow 后备 Bean 的外观如下:
package uk.co.xenonique.digital.flows.control;
import uk.co.xenonique.digital.flows.boundary.*;
import uk.co.xenonique.digital.flows.entity.*;
import uk.co.xenonique.digital.flows.utils.UtilityHelper;
// ...
@Named
@FlowScoped("sector-flow")
public class SectorFlow implements Serializable {
@Inject UtilityHelper utilityHelper;
@Inject CarbonFootprintService service;
private CarbonFootprint footprint
= new CarbonFootprint();
public SectorFlow() {}
@PostConstruct
public void initialize() {
footprint.setApplicationId(
utilityHelper.getNextApplicationId());
}
public String gotoEndFlow() {
return "/endflow.xhtml";
}
public String debugClassName() {
return this.getClass().getSimpleName();
}
public String saveFootprintRecord() {
service.add(footprint);
return "sector-flow-1c.xhtml";
}
// Getters and setters ...
}
SectorFlow 类用 @FlowScoped 注解,其值与流程定义 XML 文件匹配。我们用 @PostConstruct 注解一个 initialize() 方法,以便在实体记录中设置随机应用程序 ID。请注意,我们无法在这里将此逻辑放入正常的 Java 构造函数中,因为我们的 Bean 是通过 CDI 容器赋予生命的。
我们向 SectorFlow 注入几个实例。UtilityHelper 是一个 @ApplicationScoped CDI POJO 类,用于生成随机应用程序标识符。有一个有状态的 EJB CarbonFootprintService 负责处理 JPA 持久化。
gotoEndflow() 方法是一个导航结束,用于退出流程。该方法 saveFootprintRecord() 使用数据服务将 CarbonFootprint 实体存储到数据库中。
这就完成了内部流程,即 SectorFlow;嵌套后备 Bean FootprintFlow 的代码如下:
@Named
@FlowScoped("footprint-flow")
public class FootprintFlow implements Serializable {
private CarbonFootprint footprint;
public FootprintFlow() { }
@PostConstruct
public void initialize() {}
Map<Object,Object> flowMap =
FacesContext.getCurrentInstance()
.getApplication().getFlowHandler()
.getCurrentFlowScope();
footprint = (CarbonFootprint) flowMap.get("param3Value");
}
public String exitFromFootprintFlow() {
return "/endflow.xhtml";
}
public String gotoPage1() {
return "footprint-flow";
}
public String gotoPage2() {
return "footprint-flow-1a";
}
public String debugClassName() {
return this.getClass().getSimpleName();
}
// Getters and setters ...
}
我们重复之前的技巧;我们用 @PostConstruct 注解 FootprintFlow 类。在 initialize() 方法中,我们通过 FacesContext 实例程序性地从流程作用域中检索对象。请注意,参数名称 param3Value 必须与 XML 定义中的值一致。流程作用域中的值恰好是 CarbonFootprint 实体。
你可能想知道为什么我们要费心从一个作用域中检索一个实体并将其设置为 bean 属性?这只是一个例子,允许页面内容设计者在页面中使用一致的标记。EL #{footprintFlow.footprint.diesel} 比起 #{flowScope.param3Value.diesel} 更易于理解。
我们现在将转向标记。
页面视图
我们现在已经熟悉了页面视图的内容标记。让我们研究一下 sector-flow/sector-flow.xhtml:
<h1>Page <code>sector-flow.xhtml</code></h1>
...
<table class="table table-bordered table-striped">
<tr>
<th>Expression</th>
<th>Value</th>
</tr>
...
<tr>
<td>sectorFlow.footprint.applicationId</td>
<td>#{sectorFlow.footprint.applicationId}</td>
</tr>
<tr>
<td>sectorFlow.footprint</td>
<td>#{sectorFlow.footprint}</td>
</tr>
</table>
<h:form prependId="false">
<div class="form-group">
<label jsf:for="exampleInputEmail1">
Email address</label>
<input type="email" class="form-control"
jsf:id="exampleInputEmail1" placeholder="Enter email"
jsf:value="#{flowScope.email}"/>
</div>
<div class="form-group">
<h:outputLabel for="industryOrSector">Industry or Sector</h:outputLabel>
<h:inputText type="text" class="form-control" id="industryOrSector"
placeholder="Your industry sector"
value="#{sectorFlow.footprint.industryOrSector}"/>
</div>
<h:commandButton id="nextBtn" styleClass="btn btn-primary btn-lg" value="Next" action="sector-flow-1a" />
 
<h:commandButton id="exitFlowBtn" styleClass="btn btn-primary btn-lg" value="Exit Flow" action="endFlow" />
 
<h:commandButton id="homeBtn" styleClass="btn btn-primary btn-lg" value="Home" action="goHome" />
 
<h:commandButton id="callFootPrintFlowBtn" styleClass="btn btn-primary btn-lg" value="Call Footprint" action="callFootprintFlow" />
 
<h:commandButton id="saveBtn" styleClass="btn btn-primary btn-lg" value="Save Record" action="#{sectorFlow.saveFootprintRecord()}" />
 
</h:form>
视图使用 HTML5 友好的标记和标准 JSF 标签的混合。输入文本字段 exampleInputEmail1 及其关联的标签提醒我们关于 HTML5 友好的标记。此输入控件与流程作用域映射集合相关联,即 #{flowScope.email}。答案是肯定的,我们被允许这样做,但我们最好在应用程序的某个地方存储数据值!
输入元素 industryOrSector 显示了 JSF 标准标签,并且它直接关联到 CarbonFootprint 实体记录。
让我提醒大家注意那个调用后端 bean 中 saveFootprintRecord() 动作方法的命令按钮 saveBtn。最后,有一个专门的命令按钮,其标识为 callFootPrintFlowBtn,它调用嵌套流程。callFootprintFlow 动作与 sector-flow.xml 流定义文件中的节点完全匹配。
有一个 nextBtn 命令按钮,可以直接导航到流程中的下一个视图。homeBtn 命令按钮退出流程并返回主页。
视图的其余部分以 HTML Bootstrap 风格的表格显示可调试的输出。JSF <h:form> 元素上的 prependId=false 通知 JSF 避免对控件属性 HTML 标识符进行糖化。
注意
JSF 表单 prependId 指定在 clientId 生成过程中 <h:form> 是否应该将其 ID 预先附加到其子代 ID 上。当在外部表单中导入或插入复合组件(这些组件是表单)时,该标志变得相关。此值默认为 true。
以下是一个起始页面 sector-flow/sector-flow.xhtml 的截图:

在源代码中,我们使用 EL 输出了一系列可调试的值。在本书的源代码中,项目被命名为 jsf-declarative-form。你将找到一个没有调试输出的专业版本,被称为 jsf-declarative-form-pro。
以下是一个嵌套流程的截图,footprint-flow-1a.xhtml。在导航到起始页面后,我们点击了 下一步 按钮,在表单中输入了一些数据,并将其保存。

回到 SectorFlow,当我们调用 SaveBtn 命令按钮时,JSF 会调用 saveFootprintRecord() 方法。记录将被保存到数据库中,并且会显示视图 sector-flow-1c.xthml,如下截图所示:

剩下的只有注入的 POJO 的源代码UtilityHelper和 EJBCarbonFootprintService。不幸的是,我们无法在这个书中展示所有列表。
一个现实世界的例子
为了总结 Faces Flows,让我们看看如何向我们的应用程序添加功能。我们希望我们的 JSF 应用程序具有质量和优雅。在本书的源代码中,你可以在jsf-product-flow和jsf-product-flow-s2项目下找到这些示例。第一个项目演示了概念的原型设计。第二个项目展示了改进和清理后的数字设计,具有足够的质量向业务利益相关者展示。
确保应用程序填充数据库
通常,我们开发的应用程序针对 UAT 数据库进行测试。我们编写代码将测试信息填充到数据库中,这些信息不会进入生产环境。在许多情况下,我们只想启动应用程序以检查是否已引入了正确的模式。
我们的第一想法是创建一个带有@PostConstruct注解的@ApplicationScoped POJO,这将解决我们的启动问题。我们可以编写一个DataPopulator类,其唯一目的是在开发应用程序中创建数据。尽管我们有全公司的应用程序实例,但我们不能保证我们的 bean 在 Web 应用程序启动后被调用。
使用 Java EE,我们可以使用@javax.ejb.Startup和@javax.ejb.Singleton来启动 EJB。@Startup注解确保在应用程序部署后,EJB 容器初始化 bean。@Singleton注解表示会话 bean,它保证在应用程序中最多只有一个实例。
以下是为DataPopulator对象编写的代码:
package uk.co.xenonique.digital.product.utils;
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.inject.Inject;
@Singleton
@Startup
public class DataPopulator {
@Inject ExtendedPersistenceLoaderBean loaderBean;
@PostConstruct
public void populate() {
loaderBean.loadData();
}
}
因此,为了用数据填充应用程序,我们委托另一个 bean。ExtendedPersistenceLoaderBean是一个具有新 CDI 1.1 事务范围的 CDI bean。以下是这个委托的代码:
package uk.co.xenonique.digital.product.utils;
import uk.co.xenonique.digital.product.boundary.*;
import uk.co.xenonique.digital.product.entity.*;
import uk.co.xenonique.digital.product.entity.*;
import javax.annotation.*;
import javax.ejb.*;
import javax.inject.Inject;
import java.io.Serializable;
import java.util.*;
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
@javax.transaction.TransactionScoped
public class ExtendedPersistenceLoaderBean implements Serializable {
public static final String DEFAULT = "digital";
@Inject
UserProfileService service;
@PostConstruct
public void init() { /* ... */ }
@PreDestroy
public void destroy() { /* ... */ }
public void loadData() {
UserRole userRole = new UserRole("user");
UserRole managerRole = new UserRole("manager");
List<UserProfile> users = Arrays.asList(
new UserProfile("user@products.com", DEFAULT, userRole),
new UserProfile("test@products.com", DEFAULT, userRole),
new UserProfile("admin@products.com", DEFAULT, managerRole),
);
for (UserProfile user: users) {
service.add(user);
}
}
}
我们使用@TransactionScoped注解来标注DataPopulator对象。每当我们在@TransactionScoped对象上调用方法时,方法、CDI 容器将激活一个事务或创建一个事务。单个事务将在同一对象上的多个方法之间共享,或者在不同可能被调用的@TransactionScoped对象之间共享。换句话说,事务上下文在参与组件之间传递,而不需要开发者添加显式的方法参数来传递javax.transaction.TransactionContext实例。
回顾一下,我们在DataPopulator对象上添加了另一个特殊注解。
一旦在这个 Bean 上调用任何方法,线程上下文就会与一个新的事务关联,因为我们用@TransactionAttribute注解了这个类,并传递了一个属性值TranscationAttributeType.NEW。这强制容器创建一个新的事务,无论是否已经存在一个。事务作用域的生命周期是调用loadData()的持续时间。该方法只是在数据库中创建几个UserRole实体,然后为使用UserProfile实体进行登录创建用户账户。
最后,init()和destroy()方法只是将调试信息打印到控制台,详细内容在摘录中未显示。
提示
尽管 CDI 1.1 为 CDI Bean 定义了@javax.inject.Singleton注解,但这个 CDI Bean 的确切启动在规范中并没有定义,因为@javax.inject.Startup中缺少这个规范。因此,我们必须依赖于 EJB 单例启动 Bean。我们可能不得不等到 CDI 2.0 和 Java EE 8 才能看到这个注解。
现在我们有了用户配置文件和角色,我们如何确保 JSF 应用程序的安全性?
保护页面视图和流程
如果你想要继续使用标准的 Java EE 库,那么规范中就有容器管理认证功能。为了利用这个功能,你需要扩展 web 部署描述符文件web.xml,并为你的应用程序添加一个安全约束。
以下是一个web.xml文件中安全约束的示例:
<security-constraint>
<web-resource-collection>
<web-resource-name>public</web-resource-name>
<url-pattern>/products/*</url-pattern>
<url-pattern>/cart/*</url-pattern>
<url-pattern>/checkout/*</url-pattern>
<url-pattern>/promotions/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>admin</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
第一个安全约束将这个网站的产品、购物车、结账和促销页面限制为任何用户。注意给出的通配符作为角色名称。第二个安全约束将这个网站的 admin 页面仅限制为具有管理员角色的用户。
<user-data-constraint>元素声明页面视图是否可以通过 HTTP 或 HTTPS 访问。它指定了所需的安全级别。可接受的值是NONE、INTEGRAL和CONFIDENTIAL。将传输保证设置为CONFIDENTIAL通知应用程序服务器,这些页面和资源只能通过 SSL 访问。INTEGRAL值在客户端或服务器发送的数据不应以任何方式更改的通信中很重要。
提示
提示:Java EE 8 安全性——关于未来的 TODO(供我检查更新和进展的笔记。见javaee8.zeef.com/arjan.tijms)。对于标准的 Java EE 安全性来说,替代方案并不多。其他选择有 Apache Shiro (shiro.apache.org/) 或 Spring Security(以前称为 Acegi)。希望 Java EE 8 将包括一个全新的概念,也许是一个单独的规范。
虽然标准机制快速且易于添加,但它特定于应用服务器。该机制仅适用于粗粒度资源,并且没有可以应用于 CDI Bean 的注解。Java EE 安全需要配置安全领域,它定义了用户组的角色。为了使用细粒度权限来保护网站,我们必须添加多个角色,这可能导致高度复杂性。
我们可以为 JSF 和 Web 应用程序定义自己的自定义安全。这种方法的优点是我们有细粒度控制,并且它可以在容器之间工作。另一方面,如果我们忽略了 Java EE 安全标准特性,那么任何自制的安全实现可能都没有在野外得到充分的验证,以确保其安全性。这样的组件将无法通过基本的渗透测试。在最理想的情况下,如果要求简单且对复杂权限的要求不多,自定义安全才能正常工作。
为了创建自定义安全,我们应定义一个独特的javax.servlet.ServletFilter,以保护我们网站某些区域的访问。LoginAuthenticationFilter定义如下:
package uk.co.xenonique.digital.product.security;
import uk.co.xenonique.digital.product.control.LoginController;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.*;
import java.io.IOException;
@WebFilter(urlPatterns={"/protected/*", "/simple/*"})
public class LoginAuthenticationFilter implements Filter {
private FilterConfig config;
public void doFilter(ServletRequest req,
ServletResponse resp, FilterChain chain)
throws IOException, ServletException
{
final HttpServletRequest request =
(HttpServletRequest)req;
final HttpServletResponse response =
(HttpServletResponse)resp;
if (request.getSession().getAttribute(
LoginController.LOGIN_KEY) == null) {
response.sendRedirect(
request.getContextPath()+LoginController.LOGIN_VIEW);
} else {
chain.doFilter(req, resp);
}
}
public void init(FilterConfig config)
throws ServletException {
this.config = config;
}
public void destroy() {
config = null;
}
}
我们使用@WebFilter注解来标记我们想要保护的 URL 资源。如果用户尝试访问/protected/*和/simple/*文件夹下的页面,那么LoginAuthenticationFilter过滤器将被触发。Servlet 容器调用doFilter()方法,我们检查是否存在 HTTP 会话属性。如果存在键LoginController.LOGIN_KEY,则用户已登录到网站,否则,用户将被重定向到登录页面视图。
让我们转到后台 Bean LoginController,它允许用户登录到网站:
package uk.co.xenonique.digital.product.control;
// imports elided...
@Named("loginController") @RequestScoped
public class LoginController {
public final static String LOGIN_KEY="LOGIN_USERNAME";
public final static String LOGIN_VIEW="/login.xhtml";
private String username;
private String password;
@Inject UserProfileService userProfileService;
public boolean isLoggedIn() {
return FacesContext.getCurrentInstance()
.getExternalContext().getSessionMap()
.get(LOGIN_KEY) != null;
}
public String login() {
List<UserProfile> users =
userProfileService.findById(username);
if ( users.isEmpty()) {
throw new IllegalArgumentException("unknown user");
}
if ( !users.get(0).getPassword().equals(password)) {
throw new IllegalArgumentException("invalid password");
}
FacesContext.getCurrentInstance().getExternalContext()
.getSessionMap().put(LOGIN_KEY, username);
return "/protected/index?faces-redirect=true";
}
public String logout() {
FacesContext.getCurrentInstance().getExternalContext()
.getSessionMap().remove(LOGIN_KEY);
return "/index?faces-redirect=true";
}
// Getters and setter omitted ...
}
LoginController后台 Bean 接受两个基于表单的参数:用户名和密码。它依赖于注入的UserProfileService通过用户名查找UserProfile记录。在login()方法内部,如果密码参数与实体记录匹配,则允许用户登录。该方法将用户名添加到 HTTP 会话中,键为LOGIN_KEY。
有几个有用的方法。logout()方法从 HTTP 会话中移除登录键。isLoggedIn()方法检查用户是否已登录。
Servlet 过滤器仅处理直接导航资源、servlet、过滤器和路径。我们需要另一个保护器来保护 JSF 视图,因为LoginAuthenticationFilter不足以做到这一点。
以下是一个名为LoginViewAuthenticator的后台 Bean 控制器的代码:
package uk.co.xenonique.digital.product.security;
import javax.faces.application.NavigationHandler;
// other imports elided...
@Named("loginViewAuthenticator") @ApplicationScoped
public class LoginViewAuthenticator {
// ...
public void check() {
FacesContext facesContext = FacesContext.getCurrentInstance();
HttpSession session = (HttpSession)
facesContext.getExternalContext().getSession(true);
String currentUser = (String)session.getAttribute(
LoginController.LOGIN_KEY);
if (currentUser == null || currentUser.length() == 0) {
NavigationHandler navigationHandler =
facesContext.getApplication().getNavigationHandler();
navigationHandler.handleNavigation(
facesContext, null, LoginController.LOGIN_VIEW);
}
}
}
LoginViewAuthenticator 类的 check() 方法执行检查。我们从 HTTP 会话中检索已知的密钥 LOGIN_KEY。请注意,我们可以通过在 FacesContext 上调用 getExternalContext() 方法的链式调用,访问 Java Servlet API 的一部分。我们检索 HttpSession 实例或创建一个,然后检查关联的值。如果用户未登录,则我们更改当前 NavigationHandler 的目标。JSF 中的导航处理程序是一个实现定义的类型,它在 Faces 请求-响应交互期间携带目标结果字符串。
我们在页面视图中使用 LoginViewAuthenticator 来限制访问:
<ui:composition template="/basic_layout.xhtml">
<ui:define name="mainContent">
<f:metadata>
<f:event type="preRenderView"
listener="#{loginViewAuthenticator.check}" />
</f:metadata>
<div class="login-username-box pull-right">
<b>#{sessionScope['LOGIN_USERNAME']}</b>
</div>
<h1> JSF Protected View </h1>
<!-- ... -->
</ui:composition>
对于页面 view /protected/index.html,我们在 <f:metadata> 部分插入了一个预渲染视图事件。<f:event> 元素调用了 LoginViewAuthenticator 实例的 check() 方法。
在项目中,我们还通过向页面视图 /simple.xhtml 添加相同的 stanza 来保护 Faces 流。这个视图是起始页面,因此在这里添加预渲染视图事件实际上限制了访问流。LoginViewAuthenticator 实例确保未知网站用户被重定向到 /login.xhtml 视图。
资源库合约
JSF 2.2 作为 Java EE 7 的一部分,引入了在称为资源库合约的设施下主题化和样式化网站的能力。合约的想法是关于在运行时动态重用 Facelets。现在,通过合约可以在不重新部署应用程序的情况下在资源之间切换。合约也可以为匹配 URL 模式的页面静态声明。
规范保留了一个名为 /contracts 的特殊命名文件夹作为资源库合约的父文件夹。这个文件夹是默认的。如果你已经有一个名为此视图的文件夹,那么你将不得不按名称重构,遗憾的是。
在类路径上还有一个默认位置,META-INF/contracts,用于 JAR 文件。这个位置允许资源库合约被打包成 JAR 文件,以便分发给第三方客户。
在 /contracts 文件夹内,开发者可以定义命名的合约(或主题)。你只能在位置文件夹 /contract 或 (/META-INF/contracts) 内创建文件夹,并且每个文件夹代表一个命名的合约。在规范中,合约有一个声明的模板。每个合约都可以定义资源,如图像、CSS、JavaScript 文件和其他内容文件。
在书籍的源分发中有一个名为 jsf-resource-library-contracts 的项目,在那里你可以看到以下文件布局:
/src/main/webapp/contracts/
/src/main/webapp/contracts/default/
/src/main/webapp/contracts/default/template.xhtml
/src/main/webapp/contracts/default/styles/app.css
/src/main/webapp/contracts/default/images/
/src/main/webapp/contracts/victoria/
/src/main/webapp/contracts/victoria/template.xhtml
/src/main/webapp/contracts/victoria/styles/app.css
/src/main/webapp/contracts/victoria/images/
有两个资源库合约:default 和 victoria。这两个文件夹共享相同的资源,尽管它们不必这样做。两个 template.xhtml 文件是 UI 组成文件,用于布局页面视图。两个 app.css 文件是 CSS。
资源合同必须至少有一个 UI 组合模板,在规范中称为声明模板。在每个合同文件夹中,文件 template.xhtml 是一个声明模板。在规范提到的每个模板文件中,任何 <ui:insert> 标签都称为声明插入点。声明资源是指图像、CSS、JavaScript 以及其他资源的集合。
在 default/template.xhtml 文件内部,我们有一个重要的链接到参考样式表:
<h:head>
<!-- ...-->
<link href="#{request.contextPath}/contracts/default/
styles/app.css" rel="stylesheet"/>
</h:head>
同样地,在 victoria/template.xhtml 中,我们有一个链接到备选样式表:
<link href="#{request.contextPath}/contracts/victoria/
styles/app.css" rel="stylesheet"/>
在每个资源合同中,我们可以通过 CSS 文件中的共享 CSS 选择器的属性来变化,以产生不同的主题。以下是一个 default/styles/app.css 的摘录:
.fashion-headline {
color: #ff4227;
font-family: Consolas;
font-style: italic;
font-size: 22pt;
margin: 30px 10px;
padding: 15px 25px;
border: 2px solid #8b200c;
border-radius: 15px;
}
这与 victoria/styles/app.css 类似:
.fashion-headline {
color: #22ff1e;
font-family: Verdana, sans-serif;
font-weight: bold;
font-size: 20pt;
margin: 30px 10px;
padding: 15px 25px;
border: 2px solid #31238b;
border-radius: 15px;
}
存在着颜色、字体家族、大小和样式的差异。
为了从匹配的 URL 模式配置静态使用的资源合同,我们在 Faces 配置文件 faces-config.xml 中声明标题。JSF 2.2 引入了一个新的 <resource-library-contracts> 元素。每个合同都与一个名称和一个或多个 URL 模式相关联。
静态资源库合同引用
在我们的示例项目中,我们应该有一个包含以下代码的 Faces 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config ... version="2.2">
<application>
<resource-library-contracts>
<contract-mapping>
<url-pattern>/corporate/*</url-pattern>
<contracts>victoria</contracts>
</contract-mapping>
<contract-mapping>
<url-pattern>*</url-pattern>
<contracts>default</contracts>
</contract-mapping>
</resource-library-contracts>
</application>
</faces-config>
<contract-mapping> 元素定义了两个合同:default 和 victoria。合同的顺序对于处理很重要。对于整个网站,default 合同是激活的,而 victoria 合同仅在 /corporate/ URL 下的页面视图中激活。合同映射可以包含多个 URL 模式。
我们可以编写一个页面视图来触发这个静态合同。以下是一个页面视图的摘录,/corporate/index.xhtml:
<!DOCTYPE html>
<html... >
<f:view >
<ui:composition template="/template.xhtml">
<ui:define name="mainContent">
<!-- ... -->
<p>This is <code>/corporate/index.xhtml</code></p>
<p class="fashion-headline">
This is a fashion statement!</p>
<a class="btn btn-info"
href="#{request.contextPath}/index.xhtml">Go Home</a>
<!-- ... -->
</ui:define> <!--name="mainContent" -->
</ui:composition>
</f:view>
</html>
根据 faces-config.xml 中的先前资源库合同定义,具有 CSS 类 fashion-headline 的段落应该是绿色的。注意 JSF 如何搜索并找到位于企业文件夹中的 /template.xhtml 引用。因此,定义可以静态切换的资源库合同是一个很好的特性,但如果我们想动态更改合同呢?我们可以实现这个目标,我们将在下一节中学习如何做到这一点。
动态资源库合同引用
如果你用 f:view 元素将 UI 组合静态引用包围起来,就像我们在页面视图中做的那样,那么我们可以添加另一个新属性,称为 contracts。该属性接受一个字符串表达式,通过名称引用资源合同。
以下是这个主页视图 /index.xhtml 的摘录:
<!DOCTYPE html>
<html ...>
<f:view contracts="#{fashionSelector.theme}">
<ui:composition template="/template.xhtml">
<ui:define name="mainContent">
<!-- ... -->
<p> This is <code>/index.xhtml</code>. </p>
<p class="fashion-headline">
This is a fashion statement!</p>
<!-- ... -->
<div class="content-wrapper">
<h:form>
<div class="form-group">
<label for="theme">Disabled select menu</label>
<h:selectOneRadio id="theme"
value="#{fashionSelector.theme}"
styleClass="form-control peter-radio-box"
required="true" layout="lineDirection">
<f:selectItem itemValue="default" itemLabel="Default"/>
<f:selectItem itemValue="victoria" itemLabel="Victoria"/>
</h:selectOneRadio>
</div>
<h:commandButton styleClass="btn btn-primary"
action="#{fashionSelector.changeTheme()}"
value="Change Theme" />
</h:form>
</div>
</ui:define> <!--name="mainContent" -->
</ui:composition>
</f:view>
</html>
#{fashionSelector.theme} 控制器引用了一个支撑 Bean 的 getter,我们将在稍后看到。表达式的值设置了所选的资源库合同。我们利用 CSS 段落来直观地看到合同模板的效果。为了更改合同,我们使用了一个带有单选选择元素的表单。<f:selectItem> 标签定义了合同名称。
我们的支撑 Bean FashionSelector 是一个只有一个操作方法的控制器:
package uk.co.xenonique.digital.flows.control;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
import java.io.Serializable;
@Named
@SessionScoped
public class FashionSelector implements Serializable {
private String theme = "default";
public String changeTheme() {
return "/index?faces-redirect=true";
}
// Getters and setters omitted
}
我们将控制器注释为 @SessionScoped Bean,以便在许多请求-响应周期中保持合同更改。
资源库合同也愉快地与 Faces Flows 一起工作。使用静态 URL 模式或模板的动态选择器来等于流程的技术。在本书的源代码中,你会找到更多的演示和源代码。实际上,页面视图 /digitalFlow/digitalFlow.xhtml 看起来如下所示:
<html ...>
<f:view contracts="#{fashionSelector.theme}">
<ui:composition template="/template.xhtml">
... </u:compoition>
</f:view>
</html>
如你所见,原则上没有任何区别。
流程建议
Faces Flows 是 JSF 2.2 中一个非常实用的功能,因为它们允许开发者和设计师组合成实现客户(或以用户为中心)目标的组件。它们还允许架构师将一组页面视图和控制者定义成特定的业务定义组件。如果设计师小心操作,它们可以有效地链接在一起,并从有意义的策略中解耦。在使用 Faces Flows 时,应牢记以下要点:
-
从小处着手:设计一个实现单一责任和单一目标的 Faces Flow。不要试图在一个流程中构建整个流程。
-
传递实体和意义类型:实现接受数据实体和传输对象的 Faces Flows。
-
组合流程:将实现类似目标的常见流程组合在一起。在结账过程中,你可能有一个专门处理送货地址的流程和一个负责支付的流程。这两个流程可以通过处理整个流程的主流程来调用。
-
封装流程:尽可能封装你的流程,使其自给自足。
-
持久化用户数据:当客户完成一项任务时,确保在确定的退出点保存用户数据。
抵制构建完美工作流的诱惑。相反,我建议你在设计 Faces Flow 时,将变化放在心中。
在现代数字团队中,构建网站和应用程序时,最重要的人是用户体验设计师。通常,经过几轮以用户为中心的测试后,你可能会发现网站的页面设计和信息架构在几周甚至几个月内不断变化和反复调整。通过构建小型、目标导向的 Faces Flow 组件,你将保护开发团队免受用户体验设计团队驱动的持续变化的困扰。设计你的流程时,不要为了重用,而是为了替换。
摘要
在本章中,我们研究了 JSF 2.2 版本的宠儿:Faces 流程。我们了解了流程定义和生命周期。我们介绍了隐式导航,并使用 @FlowScoped 范围创建了 POJO。我们深入研究了流程过程的术语,并研究了声明性和嵌套流程。我们看到了如何通过调用从一个流程传递参数到另一个流程。
我们还学习了如何通过处理过期视图来为我们的数字应用增添优雅。然后我们在页面视图和资源库合同中添加了安全性,并扩展了我们的新能力。我们理解了合同如何允许开发者向我们的 JSF 应用程序添加主题和样式。我们还学到的是,资源库合同可能由静态声明驱动或由后端豆控制。
在下一章中,我们将从 JSF 脱身,深入探讨 JavaScript 编程和库框架。
练习
-
验证在多个网络浏览器和标签帧中,流程范围内的豆是唯一的。修改第一个流程类
DigitalFlow中的debugClassName()方法,报告java.lang.System.identityHashCode()的值以及类名。结果是什么? -
每个人实际上都知道如何制作简单的煎蛋早餐;写下制作步骤。你需要哪些流程?你需要哪些输入?我们知道任务的结果;还有其他输出吗?
-
开发一个简单的 Faces 流程应用,从用户那里获取联系详情。想想你需要多少个属性。你需要所有这些吗?(提示:姓名、地址、电子邮件和电话号码目前就足够了。)
-
从上一个问题中的联系详情 Faces 流程应用中获取联系详情,并将其实体记录数据持久化到数据库中。
-
现在将联系详情单流程应用拆分为单独的流程。将地址部分设置为嵌套流程。(提示:您可以将实体记录从一个流程传递到另一个流程。)
-
在联系详情应用中,我们如何允许客户检索实体?开发 Faces 应用程序,以便他或她可以暂时保存数据。(提示:也许客户需要一个临时应用程序 ID,所以将其添加到联系详情实体中。)
-
在这一点上,我们将联系应用复制到一个新项目中。我们将重新命名项目为
welcome-new-bank-customer。在零售银行业务中,这个业务流程被称为on-boarding。你需要一个或两个嵌套流程。一个流程接受个人的工作状态:他们的薪水、他们的职位,以及显然的,职业。如果你很有信心,也许你可以添加一个工作地址作为另一个流程,如果你感觉更强,可以添加国家保险号码和税务记录。对于更复杂的项目,考虑如果可以重新排序流程会发生什么?你的设计封装得有多好?开发者能否轻松地重新排列流程以适应 UX 挑战? -
根据到目前为止的联系方式/银行开户申请,你应该拥有许多联系人的数据库记录。在同一个网络应用程序中编写另一个 Faces Flow,允许信任的员工,如案件工作者,修改和删除客户记录。在真实业务中,这样的员工坐在系统后面,逐个批准每个开户申请请求。你需要编写用于安全的 HTTP 登录表单,并保护非公开页面视图。
第七章。渐进式 JavaScript 框架和模块
| "如果你是一个跑步者,你在比赛中跑步,你可能会输。如果你不跑,你肯定要输。" | ||
|---|---|---|
| --耶稣会士杰西·杰克逊 |
在当今构建网站的方式中,无法避开 JavaScript 语言,因为它是现代网络浏览器的事实标准。JavaScript 对于开发者来说要么是一种乐趣,要么是一种重大不便。如果你为客户编写或打算构建数字网络应用程序,几乎无法避开 HTML5、CSS 和 JavaScript 的知识。幸运的是,你不需要成为 JavaScript 的专家才能变得高效,因为有许多框架可以帮助你,你可以利用这些想法。本质上,在 JavaScript 方面,你需要了解并跟上现代数字的最佳实践。
虽然 JavaScript 对于数字网站来说是一个非常相关的话题,但这一章不能教你所有你需要知道的内容。相反,我将努力指出正确的方向,并提供一个概述,你应该肯定地通过进一步资源扩展你的知识。
我们将从基本的 JavaScript 编程和语言的概念开始。然后我们将直接进入使用 JavaScript 对象的编程。之后,我们将查看世界上一些主要的 JavaScript 框架。
JavaScript 基础
JavaScript 本身是一种备受尊重的编程语言。它有一个名为 ECMAScript 的标准(www.ecmascript.org/),并被 W3C 承认为一个批准的标准。这种语言是基本标准网络技术的三合一之一:HTML5、CSS 和 JavaScript。那么,什么是 JavaScript 呢?它是一种具有对象类型和封闭作用域函数块的动态类型脚本语言。在 JavaScript 中,每个类型严格上是对象。JavaScript 支持函数作为一等公民,并支持将函数分配给相关词法作用域变量、属性或实体的声明。JavaScript 支持字符串、整数、浮点数和原型。JavaScript 本质上是一种属性和原型语言。它通过作用域和闭包支持基于对象的编程。该语言的广泛应用并不显式地有保留关键字,并且它通过支持面向对象继承的结构化。通过巧妙的编程和原型,开发者可以复制对象类继承。
小贴士
我应该学习哪种基础标准的 JavaScript?
本章将探讨 JavaScript 1.5,即 ECMA Script 版本 3。这种语言版本可以在所有主要网络浏览器(Firefox、Chrome、Safari 和 Internet Explorer)中运行。即将推出的 JavaScript ECMA 6 将支持面向对象编程(es6-features.org/)。
JavaScript 是在正常 Java 网络应用程序客户端上的一种流行语言。你应该知道,JavaScript 也可以通过 Node.js 或 Nashorn 等实现方式在服务器端运行。然而,这些内容并不在本章的讨论范围内。
创建对象
让我们深入探讨客户端的 JavaScript。作为数字开发者,你能用对象编写什么样的 JavaScript 程序呢?以下是一个答案——一个嵌入脚本的 HTML5 网页,该脚本创建了一个联系人详细信息,如下所示:
<!DOCTYPE html>
<html lang="en">
<body>
<script>
var contact = new Object();
contact.gender = 'female';
contact.firstName = 'Anne';
contact.lastName = 'Jackson';
contact.age = 28;
contact.occupation = 'Software Developer'
contact.getFullName = function() {
return contact.firstName + " " + contact.lastName;
}
console.log(contact);
</script>
</body>
</html>
这个简单的程序创建了一个具有属性的联系人详细信息。JavaScript 属性可以是整数、数值数字:number、布尔值或 String 类型。JavaScript 对象还可以定义方法,例如 getFullName()。对于一个经验丰富的经典 Java 开发者来说,这种从函数定义属性的方法看起来很奇特;然而,函数是许多语言的一等公民。定义对象的 JavaScript 函数被称为方法。
在现代 JavaScript 编写实践中,你将学会识别类似这种风格的函数,这与 Java 语法相反。以下是一个来自数学的第三阶多项式函数在 JavaScript 中的示例:
var polynomial = function(x1,x2,x3) {
return (2 * x1 * x1 * x1) - ( 3 * x2 * x2 )
+ 4 * x3 + 5;
}
console.log("The answer is: " + polynomial(1.5,2.0,3.75) );
这个变量定义了一个名为 polynomial() 的 JavaScript 函数,它接受三个数字类型的参数,并返回一个数字类型。JavaScript 是一种动态类型语言,因此没有静态类型。
控制台日志
控制台日志是现代网络浏览器(Firefox、Chrome、Opera、Safari 和 Internet Explorer)中的一个标准对象。它通常可以从用于调试的菜单中访问。以前,控制台对象并未完全支持所有浏览器。
幸运的是,我们不会在 2016 年编写以下条件代码:
if ( window.console && window.console.log ) {
// console is available
}
让我们转向对象构造函数。让我提供一条关于编写控制台日志的最终建议:仅在开发代码中使用它。开发者们常常忘记从生产代码中移除控制台日志输出,这最终导致某些网络浏览器崩溃,破坏了数字客户的旅程。利用像 jQuery、RequireJS 或 Dojo 这样的 JavaScript 框架,这些框架通过库函数抽象化控制台日志。
如果你还没有这样做,我强烈建议你下载适用于 Google Chrome 或 Firefox 网络浏览器的 Chrome 开发者工具和 Web 开发者工具。
编写 JavaScript 对象构造函数
与其他语言相比,JavaScript 语言具有有限的原始类型。粗略地说,这些原始类型包括 String、Number、Boolean、Array 和 Object。这些类型可以使用原生的 JavaScript 对象构造函数创建:String()、Number()、Boolean()、Array() 和 Object()。
这里是如何使用这些原生构造函数的示例:
var message = new String('Digital');
var statusFlag = new Boolean(true);
var itemPrice= new Number(1199.95);
var names = new Array();
names[0] = 'Emkala';
names[1] = 'Sharon';
names[2] = 'Timothy';
console.log(message); // Digital
console.log(statusFlag); // true
console.log(itemPrice); // 1199.95
console.log(names); // Object (Surprise ;-)
显然,很少将 String、Boolean 和 Number 类型从原生构造函数中分配。然而,请注意 Array 原生构造函数的使用。在 JavaScript 中,数组被视为对象。它们从索引零开始枚举,就像大多数计算机语言一样。要找到数组的大小,调用隐式长度属性(name.length)。
为了建立 JavaScript 的基础知识,我们可以改进前面的示例,并利用函数引入它们自己的作用域的能力,如下所示:
var ContactDetail = function(
gender, firstName, lastName, age, occupation )
{
this.gender = gender;
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.occupation = occupation
this.getFullName = function() {
return contact.firstName + " " + contact.lastName;
}
return this;
}
var anne = new ContactDetail(
'female', 'Anne', 'Jackson', 28, 'Software Developer');
console.log(anne.female);
console.log(anne.firstName);
console.log(anne.lastName);
在这个第二个示例中,有几件事情正在进行。首先,我们将函数类型分配给 ContactDetail 变量。实际上,这个函数是一个名为变量名称的新对象类型的构造函数。在这个构造函数中,有一个特殊的 this 引用变量,它与当前级别的函数作用域相关联。当引用返回时,它成为对象实例。在函数中,我们允许定义与对象关联的其他函数,例如 getFullName()。这就是现代 JavaScript 中基于对象的编程方式。
我们将使用这个新的对象类型构造函数在名为 anne 的变量中声明联系详情。对于 Java 习惯程序员来说,这种语法一开始可能看起来非常奇怪,但 JavaScript 完全不同于 Java,并且作为一个独立的编程语言被认真接受。作用域在定义对象模块中有实际用途,我在本书的第一章中展示了这些对象模块。
JavaScript 属性表示法
在类型中访问 JavaScript 属性有两种基本方法。第一种方法对所有 Java 程序员来说都很熟悉。它是点符号。第二种方法称为括号符号,在其他非 Java 语言中看起来像映射或字典关联。括号符号等同于点符号,并且有其用途。
检查以下代码,它演示了创建 JavaScript 对象的另一种方法。记住,JavaScript 是一种动态类型语言。
var product = {
'title': 'Java EE Developer Handbook',
'price': 38.75,
'author': 'Peter A. Pilgrim'
};
console.log( product.name );
console.log( product['name']);
product.price = 34.50;
product['price'] = 34.50;
product.subject = 'Enterprise Java';
console.log(product['price']);
product.pi = function() { return 3.14159; };
console.log(product.pi());
你是否注意到了对象中引入了一个名为 subject 的新属性以及一个方法函数?当然,我在这里推广的是我的第一本技术书的标题,但这不是重点。JavaScript 允许程序员在对象内部和属性上相当灵活。对象 product 的声明应该会让人想起一些东西,因为它与事实上的JavaScript 对象表示法(JSON)标准非常相似。开括号表示法是一种定义具有属性键和值的对象的方式。
处理 null 和 undefined 引用指针
查尔斯·安东尼·理查德·霍尔爵士(Tony Hoare)开发了经典的计算机科学算法快速排序,但他也后悔地说,这也是一个价值十亿美元的错误:可怕的 null 引用指针。我个人认为,其他人可能会偶然发现这样一个明显的解决方案,以快速解决一个普遍问题。
JavaScript 将 null 引用视为哨兵,并具有 undefined。以下 JavaScript 摘录尝试在test对象中打印 null 引用:
var testObject = { item: null };
console.log(testObject.item); // prints out 'null'.
null 值告诉你对象类型的东西已定义但尚未可用。undefined 值通知开发者某些东西缺失。记住,JavaScript 是一种动态语言,因此,在对象图中导航并找不到团队认为放置在那里的对象类型是完全可能的。
在 JavaScript 中,如果你需要测试一个 null 值,你必须使用三等号运算符(===),如下所示:
var testbject = null;
console.log(testObject == null); // Wrong!
console.log(testObject === null); // Correct.
关于等价性的写作,我们如何在 JavaScript 中知道两个对象何时是等价的?
JavaScript 真值
在 JavaScript 中,条件表达式为假,如果它匹配以下空值集合之一:false、0、-0、null、空字符串('')、NaN或undefined。一个值在条件表达式中评估为 JavaScript 真值,当且仅当该值不匹配空值集合中的任何元素。任何匹配空值集合中元素的值都是评估为 JavaScript 真的空值集合。
以下 JavaScript 都评估为假:
console.log(Boolean(0));
console.log(Boolean(-0));
console.log(Boolean(''));
console.log(Boolean(null));
console.log(Boolean(false));
console.log(Boolean(undefined));
console.log(Boolean(Number.NAN));
这里,我们使用带有 new 关键字的Boolean构造函数直接实例化一个类型。以下语句评估为真:
console.log(Boolean(1));
console.log(Boolean(-1));
console.log(Boolean('runner'));
console.log(Boolean(true));
console.log(Boolean(1234567);
console.log(Boolean(new Array());
运行时类型信息
为了找出 JavaScript 值的运行时信息,你可以应用typeof运算符。这允许程序员编写专门代码来检查函数的参数。以下是一个typeof查询的示例:
console.log(typeof true); // Prints 'boolean'.
console.log(typeof 'magic'); // Prints 'string'.
console.log(typeof 3.141596527); // Prints 'number'.
如果使用原生构造函数,JavaScript 有一些其他怪癖:
console.log(typeof new Boolean(true)); // Prints 'object'.
console.log(typeof new String('MAGIC')); // Prints 'object'.
console.log(typeof new Number(3.141596527)); // Prints 'object'.
这很令人惊讶!以下证据显示了为什么数字网络开发者因为语言、标准和这些标准的实现不一致而发疯。
JavaScript 函数
在现代 JavaScript 中,与经典 Java(8 版本之前的 Java 和 Lambda 表达式)相比,你会看到很多准函数式编程。函数是一等公民。你可以将函数作为参数传递给函数。你也可以从一个函数中返回一个函数类型。将函数作为参数传递给方法有什么帮助?在 JavaScript 中,你可以编写无名的匿名函数。你可以利用将代码块传递给库函数的优势。这种风格是函数式编程的基础。我们不是用命令式编程,而是可以编写简洁且内联的代码,这几乎是声明式的。
这里有一个匿名函数的示例:
var outside = function (yourIn) {
yourIn(); // Invokes the supplied function.
}
outside( function () {
console.log('inside');
});
outside()函数接受一个匿名yourIn()函数作为单一参数。现在在outside()函数内部,它立即调用参数yourIn,即提供的匿名定义的函数。这是一个强大的技术。
JavaScript 还有一个技巧,有助于模块的声明,尤其是当它与函数对象作用域结合使用时。可以定义一个函数并直接内联调用它。考虑以下示例:
var initializeGui = function() {
console.log("start up the client side GUI...");
} ();
在前面的代码中,我们定义了一个名为initializeGui的变量,并将其分配给一个匿名函数。定义的关键在于方法语句末尾的最终圆括号。JavaScript 立即在定义解析的确切位置调用函数。在这里,我们假装通过写入控制台来初始化客户端 GUI。
你也可以将参数传递给内联函数,如下所示:
var initializeGui2 = function(msg,value) {
console.log("start up GUI..."+msg+","+value);
} ( 'Spirits in the Sky', 1973 );
上述代码演示了参数是从外部全局作用域传递到被调用的函数的。
实际上,我们可以去掉变量initializeGui2并创建一个自调用的匿名函数:
(function(msg,value) {
console.log("start up GUI..."+msg+","+value);
})( 'Spirits in the Sky', 1973 );
这种代码相当典型,在流行的 JavaScript 框架和应用程序中都可以看到。
在函数定义中,我们将利用 JavaScript 的作用域。参见第一章,数字 Java EE 7,了解关于模块命名空间技术的早期解释。
我想我该停在这里了。现代 JavaScript 编程的多样性和更深入的知识轨迹远远超出了我所能描述的范围。我建议你投资于其他入门级编程书籍,例如 Douglas Crockford 的杰出作品JavaScript:The Good Parts,以及作者Stoyan Stefanov和Kumar Chetan Sharma的 Packt Publishing 的面向对象的 JavaScript。
小贴士
Steve Kwan 已经编写了一个关于 JavaScript 模块模式的优秀示例;你可能想调查他的最佳实践,请参阅github.com/stevekwan/best-practices/blob/master/javascript/best-practices.md。
让我们看看 JavaScript 的一个非常重要的编程框架,jQuery。
介绍 jQuery 框架
jQuery (learn.jquery.com/) 是一个跨平台的 JavaScript 框架,用于客户端 Web 应用程序开发。它是 2004 年和 2005 年原始 AJAX 狂潮的幸存者,当时它与 Prototype (prototypejs.org/) 和 Scriptaculous (script.aculo.us/) 竞争,现在仍在竞争。jQuery 被称为 Java 集合框架对 Java 编程语言所做贡献的等价物。根据维基百科,jQuery 占据了世界上 10,000 个最受欢迎的网站中的 70%。换句话说,它是第一个真正引起开发者注意并使他们重新思考底层语言最远能力的 JavaScript 框架。jQuery 是免费的,并且根据 MIT 开源许可证提供。
jQuery 的构建目的是使操作 文档对象模型 (DOM) 更容易,并将 CSS 应用到 HTML 元素上。在 jQuery 中,有一个名为 Sizzle 的秘密配方,它是一个遍历 DOM 的选择器引擎。该引擎结合了选择的灵活性、对函数式编程的尊重以及回调,使工程师能够舒适地编写利用网页中底层 HTML 和 CSS 元素的 JavaScript 代码。
在 JSF 应用程序中包含 jQuery
您可以将 jQuery JavaScript 库包含在您的页面视图中。在 JSF 中,该文件夹位于 src/main/webapp/resources/ 文件夹下:
<!DOCTYPE html>
<html ... >
...
<script src="#{request.contextPath}
/resources/javascripts/jquery-2.1.0.min.js"></script>
<script src="#{request.contextPath}
/resources/app/main.js"></script>
...
</html>
我们将使用表达式语言 #{request.contextPath} 来提供位置独立性。优秀的数字开发者会使用压缩的 JavaScript 来提高性能并增加他们的商业 SEO 机会!
jQuery 准备函数回调
jQuery 框架将全局作用域中的 $ 符号专门用于我们的使用。美元符号是 jQuery 对象实例的别名,显然它很简洁。通过巧妙的编程,这超出了本书的范围,jQuery 接受一个代表 HTML DOM 对象的参数。jQuery 的入口点是 ready() 方法,它接受一个函数对象类型的参数。
这个参数可以是一个匿名函数或一个命名函数,正如我们在这里演示的,以初始化一个虚构的网站:
$( document ).ready(function() {
// Write your code here.
console.log("this page view is starting up!");
MyNamespace.MyModule1.init();
MyNamespace.MyModule2.init();
});
当 jQuery 调用匿名函数时,框架可以保证浏览器已经初始化,所有图片都已下载,事件堆栈已设置,以及某些 Web 客户端的其它专有功能已完成。在前面的示例中,我们将以模块模式初始化模块中的其他 JavaScript 库并将日志记录到控制台:
console.log( $ === jQuery ); // true
$( document ) 表达式可以概括为以下内容:
$( <expression-selector> )
表达式选择器段落可以是 CSS 选择器表达式或 HTML DOM 元素。以下是一些示例选择器:
var divs1 = $('div');
var divs2 = jQuery('div');
var win1 = $( window );
var redBtn = $( "#redButton" );
var offerSubmitBtn = $( "#offerSubmitBtn" );
var navigationControl = $( ".navControls" );
var footerArea = $( ".footerArea div" );
以哈希字符(#)开头的元素选择器等同于 DOM HTML API 调用 getElementById(),这意味着它们可能返回元素也可能不返回。#offerSubmitBtn 选择器检索具有 ID 属性指定的元素:
<h:commandButton value="Apply Now!"
id="offerSubmitButton"
action="#{longTermProvider.applyForCredit}"/>
jQuery 提供了非常强大的类选择器,可以检索一组 HTML 元素。$('div') 选择器检索文档和页面视图中的所有 HTML div 元素。同样,$('div') 类选择器检索所有 HTML 锚点元素。正如有些人所说,知识就是力量!我们可以组合 CSS 类选择器,以降低和微调我们想要操作的元素。$( ".footerArea div" ) 选择器限制了页脚区域中的 HTML div 元素。
对 jQuery 选择器进行操作
jQuery 允许数字开发者访问网页上的 HTML DOM 元素。那么,如何对这些强大的选择器进行操作呢?API 提供了许多接受函数类型参数的回调方法。让我们看看这样一个名为 click() 的方法,当特定的 HTML 元素被按下和释放时,它会触发一个事件。
这是之前看到的红色按钮的代码:
$( document ).ready(function() {
$( "#redButton" ).click(function( event ) {
alert( "You pressed the red button!" );
});
});
当用户点击红色按钮时,jQuery 处理 DOM 事件并调用与 jQuery 匹配选择器关联的匿名函数。用户会看到警告对话框。但这并没有结束。以下是一些使红色按钮从视图中淡出的代码:
$( document ).ready(function() {
$( "#redButton" ).click(function( event ) {
$( "#redButton").animate(
{
'opacity':'0.0'
}, 250);
});
}
这是一个 jQuery 动画能力的示例。animate() 方法接受两个参数:动画属性的关键和值以及持续时间。在这里,我们将指定按钮的不透明度,但我们也可以设置其他属性,例如元素的宽度或高度,甚至可以为仅针对 CSS3 确认的 Web 浏览器提供目标 Web 客户端的 3D 变换。持续时间以毫秒为单位。
如果这是一段旨在重用的代码,并且我想为界面开发者团队编写干净模块化的代码,以下是我会整理代码并避免尴尬的方法:
var DigitalJavaEE7 = DigitalJavaEE7 || {};
DigitalJavaEE7.RedModule = function($) {
var init = function() {
$( document ).ready(function() {
var redButton = $( "#redButton" )
redButton.click(function( event ) {
redButton.animate({'opacity':'0.0'}, 250);
});
}
};
var performOtherOWork = function() {
console.log("other animation stuff.")
/* ... */
};
var oPublic = {
init: init,
performOtherWork: performOtherWork
};
return oPublic;
}(jQuery);
使用流行的模块模式,我将 jQuery 的初始化代码推入一个名为 RedModule 的模块中,该模块具有 DigitalJavaEE7 命名空间。在这个模块的 init() 函数中,我将 CSS 选择器在一个调用中优化为 redButton 变量。结果发现,jQuery 正在努力将 CSS 选择器解释为一组潜在的 HTML DOM 元素。因此,我们将避免让框架对 DOM 进行两次搜索。代码本质上相同,但更简洁且易于理解。
操作 DOM 元素
在前面的章节中,你学习了如何使用 jQuery 选择 DOM 元素。可以使用 jQuery 选择器检索组元素。使用 API 的操纵部分,我们将向元素添加和删除类样式,在组件前后插入元素,并替换元素的内容。有许多调用需要学习;我们在这里将回顾一小部分。
为了演示我们如何操作 DOM 元素,让我们定义两个 HTML 按钮元素。我们将应用红色样式,另一个将是蓝色。以下是代码:
$( document ).ready(function() {
var redButton = $( "#redButton" )
var blueButton = $( "#blueButton" )
var textArea = $( "#messageArea" )
redButton.click(function( event ) {
textArea.addClass('.text-danger');
textArea.html('Danger Will, watch out!');
});
blueButton.click(function( event ) {
textArea.removeClass('.text-danger');
textArea.html('Everthing is fine now.');
});
}
我们将设置匿名函数,为messageArea添加一个 Bootstrap 类和 text-danger 类,你可以合理地假设这是一个用于文本输出的保留div元素。addClass()方法将样式类追加到匹配的元素上。我们将使用redButton元素的回调函数将样式添加到文本区域。为blueButton设置的第二个匿名函数将从元素中删除该类。这两个函数都将改变显示区域中的消息。
html()方法具有双重用途。在 jQuery 框架中,它被重载。当html()方法带有一个单一参数被调用时,它将替换元素的内容。我们将使用html()方法来更改消息区域中的文本。如果没有参数传递给该方法,它将返回元素的内容。jQuery 中有几个具有这种双重性的 API 方法,例如attr()和val()。以下是它们的描述:
-
attr()方法可以检索或操作 DOM 元素的属性。 -
val()方法检索匹配元素集中第一个元素当前的值,或者设置每个匹配元素的值。val()方法特别有用,可以访问 HTML 选择选项元素集中的名称和值。
动画
网站上最复杂的数字应用程序集成了智能动画和提示(显然得到了 UX 负责人的批准),以引导用户在他们的数字旅程中。通常,只需提供一些微妙的提示,告诉用户如何获得最佳的网站体验就足够了,这可以在整体满意度上产生巨大的差异。jQuery 具有基本的动画功能,如滑动div层和弹出窗口,缩小和扩大层,以及透明度技巧,这些都可能提供帮助。
要查看动画,让我们看看如何使用 jQuery 在用户向下滚动页面一定距离时动画化滚动到顶部的箭头。这是一个常见的用户界面设计模式。出于明显的空间原因,我们不会在这里使用模块模式。
假设我们在 JSF 页面视图中有一个简单的 HTML 内容:
<div id="scrollBackTopArrow">
<img src="img/scroll-back-top-arrow.jpg" >
</div>
首先,我们需要编写一个函数处理程序,当用户在页面视图中向上或向下滚动时,它会监听滚动事件。在 DOM 中,全局 Window 对象上有一个标准方法scroll(),它接受一个函数对象作为回调。
有了一个入口点,我们将编写一个处理函数,如下所示:
$(window).scroll( function(){
var epsilon = 0.25;
var scrollingCount = 0;
var minOpacity = 0.0, maxOpacity = 0.85;
var scrollArrow=$('#scrollBackTopArrow');
scrollArrow.each( function(i) {
var windowHeight = $(window).height();
var windowScroll = $(window).scrollTop();
var opacity = scrollArrow.css("opacity")
var upper = windowHeight * 0.525;
var lower = windowHeight * 0.315;
if( windowScroll > upper ){
if ( opacity <= (maxOpacity - epsilon )) {
if ( scrollingCount == 0 ) {
scrollArrow.animate({'opacity':'0.75'}, 100);
scrollingCount = 15;
}
}
}
if( windowScroll < lower ){
if ( opacity >= (minOpacity + epsilon)) {
if ( scrollingCount == 0 ) {
scrollArrow.animate({'opacity':'0.0'}, 100);
scrollingCount = 15;
}
}
}
if ( scrollingCount > 0 ) {
scrollingCount = scrollingCount - 1;
}
});
}); // end of the scroll function
不要害怕这个 JavaScript 函数的长度,因为现在所有内容都将揭晓。结果是,将回调附加为滚动监听器意味着网络浏览器可能会根据用户的设备每秒调用回调 10 次或更多次。因此,我们引入了一个阻尼因子,scrollingCount,作为一个倒计时变量,以防止动画被过度触发。epsilon变量还控制动画激活时的灵敏度。我们可以使用设置的最小和最大不透明度值来限制动画激活。
作为 jQuery 选择器 API,$('#scrollBackTopArrow')可能会检索零个或多个 DOM 元素,我们将调用each()方法来有效地遍历这些元素。我们使用匿名函数这样做,该函数接受一个元素作为单个参数。在这种情况下,我们知道选择器将只返回一个 DOM 元素,如果它确实存在的话。
我们将在函数中捕获当前窗口高度,$(window).height(),并将其存储在一个变量中。使用windowHeight变量,我们将推导出一些垂直限制,其中箭头应该淡入和淡出:下限和上限。原点坐标(0,0)位于设备窗口的左上角。函数调用${window).scrollTop()检索一个整数位置,表示页面当前的滚动位置。
现在我们将解释一些棘手的部分。两个条件语句检查页面视图滚动位置是否在最低或最高边界之上。如果滚动位置超过上限,我们将从视图中淡入箭头。如果滚动位置低于下限,我们将从视图中淡出箭头。我们将设置一个倒计时计时器以防止动画重新触发。请注意,JavaScript 支持访问函数定义外部声明的变量,也称为闭包。minOpacity、maxOpacity、epsilon和scrollingCount变量是闭包变量。
这里是另一个使用 CSS3 三维变换来实现扩展按钮或图标的 jQuery 示例。这个效果借鉴了较老的 Mac OS X 风格用户界面,其中应用程序图标在应用栏中扩展和收缩,如下面的代码所示:
var selector = $("expando-btn");
selector.mouseenter( function() {
$(this).each( function(i) {
css({
'transition': 'all 0.5s',
'-webkit-transform': 'scale(1.667)',
'-moz-transform': 'scale(1.667)',
'-o-transform': 'scale(1.667)',
'transform': 'scale(1.667)',
});
});
});
selector.mouseexit( function() {
$(this).each( function(i) {
css({
'transition': 'all 0.5s',
'-webkit-transform': 'scale(1.0)',
'-moz-transform': 'scale(1.0)',
'-o-transform': 'scale(1.0)',
'transform': 'scale(1.0)',
});
});
});
我们将使用mouseenter()和mouseexit()方法来构建效果。这些方法分别捕获鼠标进入和离开按钮的情况,如果按钮显示且可见。匿名函数设置 CSS 动画。CSS 3 已经具有动画类样式。过渡类声明了动画的总长度,为 0.5 毫秒,我们还声明了一个 2D 变换,该变换可以放大或缩小元素。为了扩展按钮元素,我们将比例因子设置为默认按钮大小的1.667。为了收缩按钮元素,我们将比例因子重置为默认渲染大小1.0。请注意,我们仍然需要声明专有浏览器类,例如 WebKit 浏览器(如苹果的 Safari 和之前的 Google Chrome 版本)的-webkit-transform。最终,这个例子对于触摸屏设备来说并不实用,因为没有设备(目前)能够检测手指在屏幕上非常接近的悬停!(参见本章末尾的练习。)
使用 HTML、JavaScript 和 CSS 可能相当复杂,这是界面开发者的工作,了解需求并构建前端。然而,Java 开发者也应该欣赏这项工作。我希望你能看到一些结果。
RequireJS 框架
如果你认真对待大量 JavaScript 文件和组件的组织,那么你会很高兴地发现依赖注入框架(如 CDI 和 Spring)的想法也已经进入世界。一些专业组织已经依赖于一个小型框架,称为 RequireJS(requirejs.org/)。RequireJS 框架是一个 JavaScript 文件和模块加载器。该框架具有内置的模块脚本加载器,这将提高你代码的速度和质量。
RequireJS 实现了 JavaScript 的异步模块定义(AMD)规范(github.com/amdjs/amdjs-api/wiki/AMD)。这个规范定义了一种机制,该机制反过来定义了模块以及模块之间的依赖关系,以及它们如何异步加载。
AMD 规范解决了当你有许多 JavaScript 模块并定义多个 HTML 脚本元素以加载它们时出现的关键问题,但你发现每个模块都有一个依赖顺序。
假设我们有一个名为 A 的 JavaScript 模块,它依赖于模块 B,然后模块 B 又依赖于模块 C 和 D。你可能会忘记包含模块 D 的依赖项。更糟糕的是,你可能会弄错依赖项的顺序:
<script src="img/module-b.js" ></script>
<!-- Code failure: we forgot to load the module C -->
<script src="img/module-d.js" ></script>
<!-- Oops: module B has a dependency on module D -->
<script src="img/module-a.js" ></script>
RequireJS 帮助处理这些临时依赖。首先,我们必须了解 RequireJS 如何加载 JavaScript 文件。该框架有一个最佳实践文件夹布局。
在 Java Web 应用程序方面,让我们在一个项目中定义一些文件,如下所示:
src/main/webapp/
src/main/webapp/index.xhtml
src/main/webapp/resources/js/
src/main/webapp/resources/js/app.js
src/main/webapp/resources/js/app/
src/main/webapp/resources/js/app/easel.js
src/main/webapp/resources/js/app/nested/sub.js
src/main/webapp/resources/js/lib/
src/main/webapp/resources/js/lib/jquery-2.1.1.js
src/main/webapp/resources/js/lib/bootstrap-3.2.0.js
src/main/webapp/resources/js/require.js
src/main/webapp/resources/js/require-setup.js
在一个 JSF 应用中,我们将 JavaScript 模块放在resources文件夹中。由于 JSF 需要间接引用,所以这与标准的 JavaScript 描述不同。应用文件通常保存在/js/app文件夹中。JavaScript 库存储在/js/lib文件夹中。/js/require.js文件是 RequireJS 框架模块的 JavaScript 文件。
在一个 HTML5 应用中,您首先需要包含对 RequireJS 文件的引用:
<!DOCTYPE html>
<h:html>
<h:head>
<meta charset="UTF-8">
<title>Digital Java EE 7 :: Require JS </title>
<link href="styles/bootstrap.css" rel="stylesheet" />
<script
src="img/require-setup.js" >
</script>
<script
src="img/require.js"
data-main="#{request.contextPath}js/app/app"></script>
<script
src="img/main.js" ></script>
</h:head>
<h:body>
<header>RequireJS Application</header>
<!-- ... -->
</h:body>
</h:html>
上述代码是 RequireJS 的实际应用,因为我们在一个应用中使用了 Bootstrap 和 jQuery。最重要的 HTML 脚本元素是第二个,因为它加载了 RequireJS(require.js)。第一个脚本标签很重要,因为它配置了 RequireJS 框架。我们稍后会看到这一点。第三个脚本标签加载了一个应用 JavaScript 模块。
注意
许多商业网站在页面内容底部放置标签,以遵循最佳实践惯例并提高性能。然而,由于 RequireJS 是为 AMD 设计的,因此这种做法可能会违背在页面继续加载的同时异步加载和执行脚本的目的。换句话说,效果可能因应用而异,您需要在开发工作中进行测试。
RequireJS 配置
让我们逆序查看已加载的 JavaScript 文件,因此这是 /js/app/app.js。这是包含 RequireJS 库的<script>标签元素中引用数据-main 属性的的目标:
requirejs.config({
baseUrl: 'js/lib',
paths: {
app: '../app'
}
});
此文件配置了 RequireJS 如何搜索和加载 JavaScript 文件作为模块。requirejs是库在全局头作用域中定义的 JavaScript 对象类型变量。引用对象有一个名为config()的方法,它接受一个 JavaScript 属性对象。baseUrl属性定义了加载文件的默认位置。paths属性是一个嵌套属性,它列出了一个路径集合,这些路径是默认加载规则的例外。
默认情况下,之前的 RequireJS 配置从js/lib文件夹加载任何模块的 ID。但是,如果模块 ID 以前缀app开头,则它将从js/app目录加载,如路径键指定的那样。
paths属性配置相对于baseUrl,并且从不包含.js后缀扩展名,因为paths属性可以代表目录文件夹。
由于我们将为这个示例加载 jQuery 和 Bootstrap,我们需要将一个方形的木塞塞入一个圆形的洞中。在 JavaScript 编程世界中,为了避免与许多流行的库冲突,作者采用了 shims 的想法。
小贴士
什么是 Shim?
Shim 是 JavaScript 术语中的一个俚语表达,用于强制不同的框架协同工作。它也是一个用于在 JavaScript 上下文中 monkey-patching 以包含所有 ECMAScript 5 方法的术语。
在 RequireJS 中,我们必须在第一个加载的文件(require-setup.js)中设置此配置:
// require-setup.js
var require = {
shim: {
"bootstrap" : { "deps" :['jquery'] },
"jquery": { exports: '$' }
},
paths: {
"jquery" : "jquery-2.1.3",
"bootstrap" : "bootstrap-3.2.0"
}
};
重新审视我们的 JavaScript 文件夹布局是有帮助的。require-setup 文件简单地设置了一个名为 require 的特殊变量,在全局 head 范围内使用对象定义。通过属性名 shim 引用的嵌套对象定义了两个更改。首先,一个名为 bootstrap 的模块依赖于一个名为 jquery 的模块。其次,jquery 模块导出符号($)。
配置中的第二个属性键 paths 定义了一个模块名称的关联对象。每个模块名称都映射到其真实名称。因此,jquery 模块实际上与一个名为 jquery-2.1.3 的文件相关联。由于现在我们可以轻松地升级库版本,这有一个额外的好处。这是一个单行更改!
应用模块
完成 RequireJS 的配置后,我们现在可以编写我们应用程序的默认应用模块,如下所示:
requirejs(['jquery', 'bootstrap', 'easel', 'nested/sub'],
function ($, bootstrap, easel, sub) {
// jquery, easel and the nested/sub module are all
// loaded and can be used here now.
console.log("start it up now!");
var e = new easel();
console.log("module 1 name = "+e.getName() );
var s = new sub();
console.log("module 2 name = "+s.getName() );
console.log("other name = "+s.getCanvasName() );
// DOM ready
$(function(){
// Programmatically add class to toggles
$('.btn.danger').button('toggle').addClass('fat');
console.log("set it off!");
alert("set it off!");
});
}
);
前面的 /js/app/main.js 脚本是我们的简单客户端应用程序的通用文件。全局 requirejs() 函数是库依赖注入功能的路径。以下是这个函数的格式:
requirejs( <MODULE-ARRAY-LIST>, <CALLBACK> )
这里,<MODULE-ARRAY-LIST> 是模块名称依赖项的列表集合,而 <CALLBACK> 是单个函数参数。
因此,代码示例要求 RequireJS 初始化以下模块:jquery、bootstrap、easel 和 nested/sub。请特别注意最后一个模块,因为 sub.js 位于 app 文件夹的子目录中;因此,名称使用了路径分隔符。记住,使用 RequireJS,你不需要添加后缀(.js)。
当 RequireJS 调用回调函数时,模块已经加载。因此,我们将写入控制台日志,如果我们使用 jQuery,我们将在另一个匿名函数声明中做一些关于切换按钮的复杂选择器操作。这应该开始让人明白为什么我们会在先前的 shim 配置中明确导出美元符号。还请注意,我们能够通过函数参数访问引用依赖项。
那么,我们将如何使用 RequireJS 定义模块模式?请阅读下一节。
定义模块
为了使用 RequireJS 定义我们自己的自定义模块,我们将利用框架中的另一个全局作用域方法。遵循 AMD 规范,框架提供了一个名为define()的方法。此方法的格式如下:
define( <MODULE-ARRAY-LIST>, <FUNCTION-OBJECT> )
这几乎与requirejs()调用相同。define()方法接受一个模块名称列表作为依赖项。<FUNCTION-OBJECT>第二个参数意味着该函数必须显式返回一个 JavaScript 对象。换句话说,它不能返回空或无结果。
让我们看看 canvas 模块的定义:
// js/app/easel.js
define([], function () {
console.log("initializing the `easel' module");
var returnedModule = function () {
var _name = 'easel';
this.getName = function () {
return _name;
}
};
return returnedModule;
});
模块列表可以是一个空数组,这意味着该模块没有所需的依赖项。文件路径是/js/app/easel.js。在匿名函数中,我们将使用方法和属性实例化我们的 JavaScript 构造函数对象,并将其返回给 RequireJS。该模块仅定义了一个名为getName()的方法,它返回一个私有可访问变量的值。遵循 JavaScript 的模块模式,可以在示例中声明私有作用域变量和函数,如_name,这些变量和函数在函数定义之外不可访问。
这里是另一个模块的列表,其文件路径为/js/app/nested/sub.js,它依赖于 easel 模块:
// js/app/nested/sub.js
define(['easel'], function (easel) {
var easel = new easel();
console.log("initializing the `nested' module");
var returnedModule = function () {
this.getCanvasName = function () {
return easel.getName();
}
this.getName = function () {
return "sub";
}
};
return returnedModule;
});
nested/sub 模块定义了一个包含两个方法的对象:getName()和getCanvasName()。我们将创建一个名为easel的对象变量。在函数调用期间,RequireJS 将模块引用作为参数提供。getCanvasName()方法使用这个私有引用在依赖模块easel上调用getName()方法。
这里是 RequireJS 在加载模块时的截图:

RequireJS 示例应用的截图
如果一开始觉得这很简略,请记住,要完全理解函数和对象作用域需要一点时间。对于专业的界面开发者来说,其优势很明显,可以克服 JavaScript 原始设计中的严重缺点。我们已经涵盖了足够多的 RequireJS,以便数字开发可以继续广泛进行。我们将继续介绍另一个框架。
UnderscoreJS
我将向您介绍另一个在开发中可能很有用的 JavaScript 框架。UnderscoreJS([underscorejs.org/](http://underscorejs.org/))是一个将函数式编程构造和技巧引入语言的框架。该库包含超过 100 个方法,为 JavaScript 添加了函数式支持。
UnderscoreJS 是一个像 jQuery 和 RequireJS 一样下载的单个 JavaScript 文件。如果你将必需的版本化的 underscore.js 文件添加到 /js/lib 文件夹,那么你已经有方法将其注入到你的应用程序中。以下是文件 require-setup.js 中的附加配置:
var require = {
shim: {
"bootstrap" : { "deps" :['jquery'] },
"jquery": { exports: '$' },
"underscore": { exports: '_'}
},
paths: {
"jquery" : "jquery-2.1.3",
"bootstrap" : "bootstrap-3.2.0",
"underscore" : "underscore-1.8.2"
}
};
UnderscoreJS 将符号下划线(_)导出为其库给开发者,并且其函数方法可以通过该符号访问。我们将回顾这些方法的小子集。
函数式程序员通常对以下五个主要关注点感兴趣:
-
如何在内部遍历元素集合?
-
如何过滤集合中的元素?
-
如何将集合中的元素从一种类型映射到另一种类型?
-
如何将元素集合扁平化成一个集合?
-
最后,如何将集合中的元素收集或归约成一个单一元素或值?
你可能会将这些关注点识别为 JVM 中替代编程语言的标准化思想,例如 Scala、Clojure,甚至是带有 Lambda 的 Java 8。
遍历操作
在 UnderscoreJS 中,我们可以取一个数组对象并简单地遍历它。
each() 函数允许你遍历列表集合,如下所示:
// /js/app/underscore-demo.js
requirejs(['underscore'],
function (_) {
console.log("inside underscore-demo module");
_.each( [1, 2, 3], function(n) { console.log(n); });
}
);
这里,我们使用了在 underscore-demo.js 模块中作为 AMD 加载器的 RequireJS。each() 函数遍历数组对象中的元素,并调用提供的函数,该函数被称为迭代器,以元素作为单个参数。each() 函数替换了命令式编程语言中的典型 foreach 或 for-do 复合语句。
过滤操作
过滤可以通过多种方式实现。让我们以过滤列表的基本示例为例:
var r1 = _.filter(['Anne','Mike','Pauline','Steve'],
function(name){ return name.startsWith('P'); });
console.log(r1); // ['Pauline']
此代码遍历列表中的每个值,返回一个通过真值测试(谓词)的所有值的数组。filter() 的第二个参数被称为谓词,它是一个回调函数,如果提供的元素满足条件测试,则返回一个布尔值。在这里,我们正在过滤以字母 P 开头的列表中的名称。
UnderscoreJS 还提供了一种更复杂的过滤方法。where() 方法搜索列表并返回一个包含所有具有列出的键值对属性的值的数组:
var contacts = [
new ContactDetail( 'F', 'Anne', 'Jackson', 28, 'Developer' ),
new ContactDetail( 'M', 'William', 'Benson', 29, 'Developer' ),
new ContactDetail( 'M', 'Micheal', 'Philips', 33, 'Tester' ),
new ContactDetail( 'M', 'Ian', 'Charles', 45, 'Sales' ),
new ContactDetail( 'F', 'Sarah', 'Hart', 55, 'CEO' ),
];
var r2 = _.where(contacts, {occupation: 'Developer', age: 28 });
console.log(r2);
上一段代码使用了我们在本章前面定义的 ContactDetail JavaScript 对象。我们将使用联系人列表和提供的键值对象(包含我们想要过滤的属性)调用 where() 方法。结果是匹配安妮·杰克逊的 ContactDetail,因为她有匹配的职业(软件开发者)和年龄(28)。
映射操作
map() 函数通过映射列表中的每个元素,并使用用户提供的函数,生成一个新的数组对象:
console.log(
_.map( [1, 2, 3],
function(n){ return n * 3; } )
);
// [3, 6, 9]
console.log(
_.map( [ 1, 2, 3, 4], function(x){ return x * x; } )
);
// [1, 4\. 9, 16]
console.log(
_.map( [ 1, 2, 3, 4],
function(x){ return "A" + x; } )
);
// [ 'A1', 'A2', 'A3', 'A4']
用户提供的函数接受当前元素参数,并负责返回新的元素类型。在这些例子中,我们将创建一个新的包含数字元素的三个元素的数组列表,然后我们将创建一个新的包含数字元素平方的数组列表。最后,我们将创建一个包含字符串元素的数组列表。
展平操作
现在我们知道了如何使用 UnderscoreJS 迭代、过滤和映射集合,我们也应该学习如何展平元素集合。有一个名为 flatten() 的方法,它接受一个元素集合,并在这些元素中有一个或多个本身是集合的情况下将其展平。
让我们看看以下两个例子:
var Sector = function( name, value ) {
this.name = name;
this.value = value;
};
var salesSectorData = [
[
[
new Sector("New York", 3724.23),
new Sector("Boston", 8091.79)
],
[
new Sector("Houston", 9631.54)
],
],
[
new Sector("London", 2745.23),
new Sector("Glasgow", 4286.36)
]
];
var f3 = _.flatten(salesSectorData);
console.log("f3 = "+f3);
// [Sector, Sector, Sector, Sector, Sector ]
var f4 = _.flatten(salesSectorData, true );
console.log("f4 = "+f4);
// [ [Sector, Sector], [Sector], Sector, Sector ]
在这里,我们定义了一个名为 Sector 的对象,它代表,比如说,销售和营销数据。我们创建了一个嵌套集合 salesSectorData,实际上它是一个包含两个元素的数组,但每个元素都是一个更进一步的集合。简而言之,salesSectorData 是一个二级有序数据结构。
第一次 flatten() 调用将数组列表中的数据结构完全展平。因此,我们将得到一个包含五个元素的数组。我们将向第二个 flatten() 调用传递第二个参数,这是一个布尔参数,用于指定是否应该对集合的元素也执行展平操作。f4 的结果是包含四个元素的数组。第一个元素是一个包含两个元素的数组列表,第二个元素是一个包含一个元素的数组列表,然后剩余的元素将依次排列。
应该很明显,为什么一个 JavaScript 接口开发者会对 UnderscoreJS 赞不绝口。我们将继续进行这次火热的审查中的最后一个操作。
减少操作
如果我们不能将这些功能操作缩减为单一的标量值或对象,它们有什么好处呢?幸运的是,RequireJS 为我们提供了几种方法,如 reduce()、reduceRight()、min() 和 max()。
我们现在只看看 reduce() 操作。如果我们想发现所有之前部门对象的销售额总和,我们该如何做呢?下面是答案:
var totalValue= _.reduce(
f3,
function(acc, sector) {
return acc + sector.value;
},
0 );
console.log("totalValue = " + totalValue)
reduce() 操作接受三个参数:集合、迭代函数和初始值。为了将集合缩减为一个单一的标量值,reduce() 操作会在每个元素上调用迭代函数。迭代函数接受标量值参数和元素。匿名函数将销售部门值添加到累加器中。
reduce() 操作符在集合方面是左结合的,而 reduceRight() 是右结合的。这完成了我们在 UnderscoreJS 中的旅程。一个更有兴趣的读者可以进一步在线和通过其他信息资源深入了解这个框架。
GruntJS
在我们结束关于渐进式 JavaScript 编程的这一章之前,我们将快速了解一下一个用于启动动作的工具。这个工具是 GruntJS (gruntjs.com/),其背后的团队将其描述为 JavaScript 任务运行器。GruntJS 是一个 Node.js 工具,在这个生态系统中工作。因此,开发者在使用 GruntJS 之前必须安装 Node.js。
该工具目前在数字社区中很受欢迎。以下是 GruntJS 被视为加分项的一些原因:
-
配置在一个地方,并且可以在你的数字团队中的其他开发人员、测试人员和操作人员之间共享。
-
GruntJS 采用插件系统构建。
-
工具压缩你的 CSS 并最小化你的 JavaScript 文件,以提高性能并将它们交付到产品网站。
-
GruntJS 允许专门的界面开发团队在网站的客户端部分分别或共同工作。然后,这些工具将他们的 JavaScript 和 CSS 组件合并在一起,以便进行生产交付。
-
它可以优化你的图像以减少整体文件大小,同时仍然保持质量,这对于交付大规模的英雄风格视网膜显示屏图形以及创建移动友好型图像是完美的。
-
开发者可以利用 Sass 和 Less 进行 CSS 编写。
有 GruntJS 插件用于 Less、Sass、RequireJS、CoffeeScript 等。
Node.js 是一个 JavaScript 运行平台,因此,它与 Java 平台绝对不同。如果你恰好使用 Gradle,有两个开源插件可以帮助它们之间的桥梁。它们被称为 Gradle-GruntJS 插件 (github.com/srs/gradle-grunt-plugin) 和 Gradle-Node 插件 (github.com/srs/gradle-node-plugin)。Node.js 还有一个自己的包管理器,称为 npm,它处理库的安装、更新和删除。Npm 允许 Node.js 和 JavaScript 开源库与社区共享。
每个 GruntJS 项目都需要在根目录下放置以下两个文件:
-
package.json:此文件指定了 npm 项目的元数据,并包含了项目所需的 JavaScript 工具和库依赖项,包括 GruntJS。 -
gruntfile.js:此文件配置并定义了项目的构建任务。你还可以在此文件中添加 GruntJS 插件的依赖项。
对于一个 JSF 项目,你需要在项目根目录下放置package.json文件。以下是该文件的代码示例:
{
"name": "my-digital-project",
"version": "1.0-SNAPSHOT",
"devDependencies": {
"grunt": "~0.4.5"
}
}
你会发现这个文件看起来几乎像是一个 JSON 文件。关键的devDependencies属性声明了一组 npm 工具和框架。我们肯定希望从版本 0.4.5 或更高版本加载 GruntJS。
现在,我们将直接深入一个实际的 GruntJS 应用案例。在一个数字项目中,我们希望优化性能并确保我们的搜索引擎优化排名。我们需要合并第三方 JavaScript 库并最小化 JavaScript 文件。界面开发者更喜欢继续使用 Sass 工具来管理 CSS 文件的灵活性。我们已经与我们的管理层达成协议,目前保持开发的 JavaScript 文件不变。
这里是实现这一目标的 gruntfile.js 文件:
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
concat: { /* ... */ }
uglify: { /* ... */ }
sass: { /* ... */ }
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.registerTask('default', ['concat', 'uglify', 'sass']);
};
上述 gruntfile.js 文件在 Node.js 系统中定义了一个模块。在另一个模块中有一个模块系统听起来可能有些令人困惑;然而,我们只需知道模块是一种封装形式,它允许通过重用进行共享。module.exports 定义允许 Grunt 参与到 Node.js 系统中。因此,可以将这个 Grunt 模块与其他 Node.js 模块共享。
grunt.initConfig() 段落是初始化 GruntJS 工具所必需的。其中最重要的部分指定了元数据文件的名称,package.json。之后,我们有一个预留的区域用于插件配置。每个 GruntJS 插件都有独立的属性配置。有三个插件:grunt-contrib-concat、grunt-contrib-uglify 和 grunt-contrib-sass。每个插件声明了一个配置属性名称:concat、uglify 和 sass。
要配置连接插件,我们有以下段落:
concat: {
dist: {
src: [
'src/main/webapp/resources/js/libs/*.js',
'src/main/webapp/resources/js/global.js'
],
dest: 'build/webapp/js/build/thirdparty.js',
}
}
grunt-contrib-concat 插件需要一个文件源和一个目标文件。它会将所有现有的 JavaScript 库文件整合在一起,然后生成一个名为 thirdparty.js 的单个文件。在我们的 Gradle(或 Maven)项目中,假设我们有一个将最终打包目标文件的 WAR 任务。
我们必须了解事实上的 Gradle 和 Maven 目录布局配置,这对于 Web 项目来说。因此,我们将 src/main/webapp 添加到文件路径中。
要配置压缩插件,我们将执行以下代码:
uglify: {
build: {
src: 'src/main/webapp/thirdparty.js',
dest: 'src/main/webapp/thirdparty.min.js'
}
}
这个配置非常容易理解;我们只需将 grunt-uglify-contrib 插件指向源并指定目标文件路径。
最后,我们将按照以下方式配置 Sass CSS 构建插件:
sass: {
dist: {
options: {
style: 'compressed'
},
files: {
'src/main/webapp/resources/js/styles/build/mysite.css':
'build/webapp/resources/styles/mysite.scss'
}
}
}
对于 grunt-contrib-sass 插件,我们需要稍微更多的指令。这个插件需要文件的键值属性。目标文件是键属性,SASS 源文件是值。
提示
安装 SASS 还需要安装有效的 Ruby 安装和相应的 RubyGem。
GruntJS 是客户端数字开发者的一款令人兴奋且强大的工具。这一点毫无疑问。插件系统仍然相当不成熟,我建议您检查文档以了解配置更改。
摘要
本章是对现代数字 JavaScript 编程的快速浏览。如果您从未使用过 JavaScript 语言,或者已经多年远离它,那么我真心希望您已经重新振作起来,学习这项基本技能。我们研究了 JavaScript 对象的编程。我们看到了如何构建对象。我们了解了属性表示法,并处理了 JavaScript 的真值。
我们特别拜访了 jQuery——JavaScript 编程的祖母或祖父。如果您没有学到其他东西,那么现在您应该能够理解 jQuery。我们看到了选择器如何搜索 HTML DOM 中的元素,然后可以对这些元素进行操作以产生效果。我们简要地涉猎了动画,这为为顾客和商业主创造更复杂体验打开了大门。
我们尝试了 RequireJS 的模块依赖管理。我们学习了这个框架如何帮助数字开发者组织模块,并将检索它们的顺序留给框架。
在本章末尾,我们跨越了非 Java 平台 Node.js 的桥梁,特别是学习了 GruntJS 的基本细节。我们研究了 GruntJS 定义的一个示例,该定义将 JavaScript 库文件捆绑在一起,从 Sass 生成 CSS,并优化了 JavaScript 文件的大小。
在接下来的章节中,我们将探讨 AngularJS 以及新兴的 Java EE MVC 框架。
练习
以下是为本章的练习和问题:
-
以下 JavaScript 代码定义了什么?还有哪些与之类似?
var hospital = { name: "St Stephen's Hospital", location: "Kensington & Chelsea", patients: 1250, doctors: ['Patel', 'Radeksky', 'Brown', 'Shockley'] }; -
从前面的代码中,解释
hospital.patients和hospital['patients']属性访问器之间的主要区别是什么? -
编写一个 JavaScript 对象,构建一个 Java 用户组成员的参与者。假设您将您的对象命名为
JUGParticipant。您需要捕捉他们的名字、电话联系方式、电子邮件地址(可选),以及他们的特定兴趣(可选)。 -
将
JUGParticipant修改为接受兴趣作为另一个对象。编写一个 JavaScript 标签对象,以便构建包含 Java EE、Android 或 HTML 等技能的数组。演示您能够构建JUGParticipants和标签对象的这种对象图。 -
通过创建一个
JUGEvent会议对象来调整您的 JavaScript 对象图。您的对象需要包含诸如事件标题、描述、地点、演讲者(可选)以及愿意的JUGParticipants等属性。 -
了解 JavaScript 1.5(或 ECMAScript 版本 3)中的九个原生对象构造函数。为了开始,这里有四个:
Number()、String()、Boolean()和Object()。 -
使用以下 HTML,使用 jQuery 改变
div元素messageArea的背景颜色。代码中有三个按钮,表示交通灯的规则。(您允许使内容更美观!)<div class="traffic-light"> <div id="messageArea" class="message-area"> <p class="lead"> This is it! </p> </div> <div> <a id="#redBtn" href="#" class="button" >Red</a> <a id="#yellowBtn" href="#" class="button" >Yellow</a> <a id="#greenBtn" href="#" class="button" >Green</a> </div> </div> -
解释以下两个 jQuery 语句之间的区别:
$('hero-image').attr('alt'); $('hero-image').attr('alt', 'Voted best by Carworld 2015!'); -
使用以下 HTML5 代码,该代码显示一个 Bootstrap 警告框,我们希望用户在点击关闭图标时使警告消失。编写 JavaScript 和 jQuery 来动画化关键通知,使其淡出,然后从 HTML 中移除内容。
<div> <p class="lead"> Here is something I desperately want you to know right here, right now. </p <div class="critical-notice alert-danger"> <button type="button" class="close pull-right" aria-label="Close"> <span aria-hidden="true">×</span> </button> <span class="glyphicon glyphicon-warning-sign" aria-hidden="true" id="closeNotch" ></span> <strong>Danger!</strong> I'm very annoyed. </div> </div> -
查看书籍的源代码,并开始使用 RequireJS。研究代码。你的任务是修改代码并添加你的模块,你可以将其命名为
/js/app/oxygen.js。你需要使用 AMD 规范的define()方法调用。 -
现在你已经编写了新的 RequireJS 模块
oxygen.js,你怎么知道它按预期工作?你是否编写了一个测试网页?你可能需要编写一个/js/app/oxygendemo.jsJavaScript 文件。 -
定义两个模块,分别命名为
/js/app/hydrogen.js和/js/app/water.js。设置水模块,使其依赖于其他两个模块:氢和氧。 -
使用 UnderscoreJS 并假设你已经设置了依赖项,练习以下 JavaScript 示例:
var arr = [1, 2, 3, 4, 5]; var newArr = _.map(arr, function (x) { return (x + 3) * (x - 2) * (x + 1); }); -
查看以下 JavaScript 代码,该代码定义了一个百分比和一个产品价格数组。使用 UnderscoreJS 计算总价格,以及当每个价格增加百分比时的总价格:
var Product = function(name, price, type ) { this.name = name; this.price = price; this.type = type; } var percentage = 3.25, var productPrices = [ new Product("Secutars", 27.99, "Garden"), new Product("Chair", 99.99, "Home" ), new Product("Luxury Red Wines", 79.99, "Food" ), new Product("Microwave", 49.99, "Home" ), new Product("Mower", 169.99, "Garden" ) ]; -
注意,我们有不同类型的产品。使用 UnderscoreJS,计算原始价格的总金额,然后计算增加 5.35% 的总金额。
第八章. AngularJS 和 Java RESTful 服务
| "慢 - 任何超过 50 毫秒的东西对人类来说都是不可察觉的,因此可以被认为是‘瞬间’。" | ||
|---|---|---|
| --AngularJS 的共同创造者 Misko Hevery |
在本章中,我们将走出 JSF 的舒适区,探索一种不同的 Web 应用程序模式。你们大多数人都会熟悉像 Google Mail、Facebook 和 Twitter 这样的流行社交媒体及其 Web 用户界面。这些 Web 应用程序具有特殊用户体验和信息架构,给人一种交互发生在单个网页上的错觉。然而,在幕后,这些应用程序依赖于标准技术:HTML5、CSS 和客户端 JavaScript。它们都使用 AJAX 调用通过 HTTP 与后端服务器通信。当服务器端应用程序向 Web 客户端发送数据时,只有页面的一部分被更新。在当代使用中,许多数字网站利用应用程序后端上的 RESTful 服务端点。一些复杂的企业应用程序可能使用服务器发送事件(SSE)向多个工作用户发送通知,而更前沿的一些则依赖于新近制定的 HTML5 WebSocket 规范,以在客户端和服务器之间提供全双工通信。顺便提一下,来自 Java Community Process 的完整 Java EE 7 规范支持 JAX-RS、SSE 和 WebSocket。
单页应用程序
在单页面上构建应用程序的设计理念,使其类似于桌面应用程序,与 JavaServer Faces 原始的页面间导航链接设计形成鲜明对比。JSF 1.0 是在 2000 年代初创建的,远在 2005 年重新发现XMLHttpRequest JavaScript 对象和 Google Maps 之前,所以这个历史注释不应令人惊讶(en.wikipedia.org/wiki/JavaServer_Faces)。完全有可能将 JSF 编写为单页应用程序,但我不会推荐将方钉强行塞入圆孔的努力!JSF 适合于本质和设计上极度状态化的应用程序,其中客户旅程基于页面间的导航。在前几章中,我们已经详细介绍了使用 JSF 的状态化 Web 应用程序、流作用域、会话和视图作用域的 bean。如果您对这些概念不熟悉,我强烈建议您再次复习这些材料。现在,我们将继续探讨另一种设计模式。
让我们列出单页应用程序的有益特性:
-
SPAs 通常具有适合单页的网站或 Web 应用程序。
-
它们依赖于现代数字 JavaScript 技术,包括 AJAX、HTML5 和 CSS。
-
在导航期间,此类应用程序不是加载整个页面,而是操作文档对象模型(DOM)以提供页面更新。
-
这些应用程序通常使用 HTML 模板引擎在客户端本地渲染内容。客户端的表示逻辑和服务器端的业务逻辑之间存在关注点的分离。
-
单页应用程序与网络服务器动态通信,通常使用 RESTful 服务,JSON 作为流行的有效载荷类型。
单页应用程序存在一些缺点,内容策略师、技术负责人开发人员和显然的股东商业人士都应该了解:
-
将搜索引擎优化应用到单页应用程序中可能有些困难。
-
使用浏览器中的后退按钮可能会导致数据条目丢失;单页应用程序与网页浏览器历史记录不兼容。
-
单页应用程序需要更高程度的应用开发知识来处理响应式编程和概念。值得注意的是,工程师应该意识到关于权衡可扩展性、弹性、事件驱动处理和通知的因素,并保持响应。
最后,让我给你一些建议。行业中的数字界面开发人员拥有 JavaScript、HTML5 和 CSS 技能。在本章中,你将学会认识到 JavaScript 编程能力与 Java 服务器端需求同样重要。换句话说,使用 AngularJS 和类似的客户端框架往往是一种全栈参与。
案件工作者应用程序
对于本章,我们将探讨一种特定类型的单页应用程序,这种应用程序适用于国际政府,称为案件工作者系统。案件工作者的业务用户将坐在办公桌前,他们的大部分时间都在处理申请人的产品申请阶段。
以下是该应用程序的截图:

案件工作者应用程序截图,xen national force
该应用程序名为 xen-national-force,它被设计用来通过微型工作流程处理护照。它远远不能满足真实商业应用的需求。例如,为了尽可能简化,没有实现用户输入安全。它仅适用于一个案件工作者,并且从用户体验的角度来看存在一个非常明显的设计缺陷。然而,xen-national-force 应用程序展示了如何使用 AngularJS 构建具有 CRUD 操作的主从记录系统,并且它具有基本的有限状态机实现。
我们现在将转向学习流行的 AngularJS 框架。
AngularJS
我们如何使用 jQuery 将文本输入与 div 元素中的消息区域连接起来?一个可行的方法是编写如下事件处理程序和回调函数的 JavaScript 模块片段:
$('#helloForm input.greeting-name').on('value', function() {
$('#helloForm div.greeting-name').text('Hello ' + this.val() + '!');
});
input.greeting-name, then jQuery invokes the callback function, which updates the inner HTML in the div element layer, identified with the CSS class div.greeting.name. We could extend this code and write a generic solution with parameters, especially if we have more cases like this in our application, but sooner rather than later, programming at this low-level introduces complexities and bugs.
AngularJS 的设计者意识到有改进的机会。相同的示例可以使用 AngularJS 重新编写,如下所示:
<!DOCTYPE HTML>
<html>
<head>
<script src="img/angular.js"></script>
</head>
<body ng-app ng-init="greeting-name = 'Mr. Anderson'">
<form>
<input ng-model="customer-name" type="text" />
<div class="greeting-name">Hello {{customer-name}}!</div>
</form>
</body>
</html>
前面的片段完全是 HTML。它从一个远程服务器包含了 AngularJS 框架,内容分发网络(CDN)。HTML 的主体元素被一个非标准属性 ng-app 标注,以声明这个 DOM 节点是整体模板的一部分。另一个属性 ng-init 在客户端渲染模板之前声明了一个数据模型。AngularJS 需要知道从哪里开始模板或动态修改 DOM;因此,每个页面都以 ng-app 属性开始。通常,ng-app 属性应用于 HTML 的 body 元素。没有访问数据模型,AngularJS 模板将毫无用处,这就是 ng-init 属性的目的。它设置了一个作用域变量 greeting-name 并将其赋值为字符串字面量 Mr. Anderson。
注意额外的属性类型 ng-model 和特殊的双大括号语法:{{customer-name}}。这个属性是 AngularJS 框架提供的一个特殊扩展,用于在行内标识数据模型,而大括号代表一种特殊的 HTML 模板语法,称为指令。在这里,我们将 ng-model 属性应用于输入字段元素。当页面加载时,输入文本字段显示文本 Mr Anderson。代码还允许用户在输入字段中输入文本,并同时更新消息区域。在这个简单的情况下,不需要编程;实际上,它是声明式的。那么,秘诀是什么呢?下面的代码展示了双向绑定的一种形式。让我们扩展它来演示完整的双向绑定:
<form>
<input ng-model="customer-name" type="text" />
<div class="greeting-name">Hello {{customer-name}}!</div>
<p>
<button class="btn-large" ng-click="user-model = 'Karen'">
Karen </button> </p>
<p>
<button class="btn-large" ng-click="user-model = 'Albert'">
Albert </button> </p>
</form>
我们引入了带有新属性 ng-click 的 HTML button 元素。属性的值是一个 AngularJS JavaScript 表达式。每个按钮都会用新的名字更新数据模型。实际上,它们重置了输入字段和消息区域中的名字。这有多酷?这里根本不需要 jQuery 编程。AngularJS 有许多特殊的自定义属性,如 ng-repeat、ng-switch 和 ng-option,我们将在本章后面遇到。
你可能想知道这些绑定和模板非常聪明;那么客户端是如何工作的呢?
AngularJS 是如何工作的?
AngularJS 作为 HTML 页面内容的一部分在网页浏览器中加载。框架最强大的部分是它鼓励关注点的分离。表示视图应该故意与业务逻辑和数据模型混合。这有几个原因。当 Angular JS 框架加载页面时,框架会在 DOM 中上下移动并寻找某些非标准属性,称为指令。它使用编译器解析和处理这个标记。实际上,AngularJS 将静态加载的 DOM 转换为渲染视图。框架将这些指令创建关联、绑定和额外的行为。
ng-app 属性与一个初始化应用的指令相关联。ng-init 与一个允许程序员设置数据模型的指令相关联。它可以用来给变量赋值。ng-model 与指令访问相关联或存储与 HTML 输入元素相关联的值。AngularJS 允许开发者编写自定义指令。你可能将来会想编写一个来获取对 DOM 的访问权限。
AngularJS 在模板视图中基于嵌套作用域工作。作用域是表达式的执行上下文。作用域可以以层次结构组织,以便它们模仿 DOM 模型。

AngularJS 的基本工作原理
AngularJS 依赖于定义控制器和其他逻辑的 JavaScript 模块。模块可以依赖于其他模块;然而,与 RequireJS 不同,不属于不同 JavaScript 文件的模块不会自动加载到应用程序中。作用域是绑定表示和数据模型的粘合剂。作用域是 AngularJS 中定义观察者和监听器的地方。大多数时候,框架将自动处理表达式处理和数据绑定,并处理 JavaScript 模块和 DOM 元素组件之间的通知。在编译阶段之后,AngularJS 继续到链接阶段,并将表达式关联到模块控制器方法和其他资源。
让我们总结这些步骤:
-
AngularJS 框架启动自身。特别是,它搜索 DOM 中带有
ng-app属性的 HTML 元素。这是框架的触发点。 -
一旦找到
ng-app元素,AngularJS 就会创建一个依赖注入器。 -
然后,它将静态 DOM 编译成渲染中间视图,在过程中收集指令。
-
然后,AngularJS 开始将指令与它们关联的作用域进行链接和组合。这是一个算法性和层次性的操作。在执行链接阶段之前,框架创建了一个初始作用域,称为根作用域。
-
最后,AngularJS 使用根作用域调用 apply 调用,在这个阶段,视图被渲染。
让我们看看案件工作者的视图。在书籍的源代码中,你会找到一个名为 xen-force-angularjs 的 Gradle 项目。它遵循 Java EE 项目的 Maven 惯例。我们的讨论将分为两个部分。我们将查看由 HTML5、JavaScript 和一些 CSS 组成的前端代码。之后,我们将深入研究 Java 服务器端后端。让我们看看以下这张图:

指令与 AngularJS 中的业务逻辑之间的关系
案件工作者概述
案件工作者项目展示了一个主从应用程序。我们的工作人员启动应用程序,看到一系列案件记录,其中包含每个申请人的姓名和护照详情。这是主记录。每个案件记录可能附有零个或多个任务记录。那些是主记录的详细记录。每个主记录还包含一个状态属性,显示每个申请人在过程中的位置。我们的用户被允许访问所有案件记录并将当前状态从开始移动到结束。
案件工作者主视图
在案件工作者示例中只有一个 HTML 文件,它作为 src/main/webapp/index.xhtml 文件中的模板。记住,这是一个单页应用程序!
<!DOCTYPE html>
<html ng-app="app">
<head>
...
<link href="styles/bootstrap.css" rel="stylesheet">
<link href="styles/main.css" rel="stylesheet">
</head>
<body ng-controller="CaseRecordController">
...
<div id="mainContent">
...
<div class="case-record-view" >
...
<div class="actionBar"
ng-controller="NewCaseRecordModalController" >
<button class="btn btn-primary" ng-click="openCreateCaseRecordDialog()" >Add New Case Record</button>
<div ng-show="selected">Selection from a modal: {{ selected }}</div>
</div>
<h2 class="case-record-headline">Case Records</h2>
<table class="table table-bordered" >
<tr>
<th>Id</th>
<th>Last Name</th>
<th>First Name</th>
<th>Sex</th>
<th>Country</th>
<th>Passport No</th>
<th>D.o.B</th>
<th>Expiration Date</th>
<th>Status</th>
</tr>
...
</table>
</div>
</div>
</body>
</html>
HTML 标签元素被赋予了一个 AngularJS 指令 ng-app,它指定了作为应用程序的作用域值名称。我们有常见的 head 和 body 元素。我们包含了 CSS 文件 Bootstrap (bootstrap.css) 和应用程序的样式文件 main.css。直到我们到达带有 ng-controller 属性的 Body 标签,没有太多区别。ng-controller 指令将控制器附加到视图中。控制器是 MVC 模式的一部分 JavaScript 对象。因此,DOM 中的整个 body 标签元素绑定到名为 CaseRecordController 的 JavaScript 对象。我们稍后会看到它的代码,但首先,让我们深入一点。
随着你进一步检查代码,你会在名为 action-bar 的 CSS 选择器的 div 元素上注意到另一个控制器指令。这个元素与一个名为 NewCaseRecordModalController 的不同控制器相关联。每当一个 ng-controller 指令被赋予属性时,AngularJS 就会创建一个新的作用域。因此,作用域可以相互嵌套。这是 AngularJS 框架中的关键概念。如果存在,作用域存在于与嵌套作用域相关联并包含它们的元素上。
主视图渲染了一个案件记录的表格。前面的代码渲染了申请人的姓名、性别、出生日期、ISO 国家代码、护照号码和护照的有效期。
以下内容是渲染主表行的下一部分:
<tr ng-repeat-start="caseRecord in caseRecords">
<td>
<div ng-controller="NewCaseRecordModalController" style="display: inline;">
<a class="btn" href="#" ng-click="showOrHideTasks($parent.caseRecord)">
<i class="glyphicon" ng-class="getIconClass($parent.caseRecord)" ></i>
</a>
</div>
</td>
<td>{{caseRecord.lastName}}</td>
<td>{{caseRecord.firstName}}</td>
<td>{{caseRecord.sex}}</td>
<td>{{caseRecord.country}}</td>
<td>{{caseRecord.passportNo}}</td>
<td>{{caseRecord.dateOfBirth}}</td>
<td>{{caseRecord.expirationDate}}</td>
<td>{{caseRecord.currentState}}</td>
</tr>
这段代码内容有几个部分。ng-repeat-start 是一个特殊的指令,它允许使用表达式迭代内容。表达式是一个 AngularJS 动态评估的表单选择查询。因此,<"caseRecord in caseRecords"> 表达式意味着对名为 caseRecords 的作用域中对象的总体迭代,并将每个元素分配为一个名为 caseRecord 的对象。我们使用 AngularJS 绑定指令表达式在适当的表格单元格元素中渲染每个案件记录的信息。我们对 {{caseRecord.lastName}} 单元格这样做,然后重复此过程。
第一个数据单元格是特殊的,因为它渲染了一个嵌入的 div 元素。它说明了如何关联一个布尔值,并提供了一个展开和折叠的关联到案例记录。我们必须在 div 上创建一个作用域,并将适当的控制器 NewCaseRecordModalController 与 ng-controller 属性关联起来。我们利用 ng-click 指令来调用控制器上的 showOrHideTasks() 方法。注意,我们传递作用域的父元素,其中包含当前的 CaseRecord,因为表格正在渲染。还有一个名为 ng-class 的指令,它通过设置 CSS 选择器将图标元素与 Bootstrap 中的适当矢量图标关联起来。此代码在表格视图中打开和关闭一个二级行,渲染任务视图。它还根据任务视图是打开还是关闭来正确更新矢量图标。
此表格视图内容的第三部分现在如下所示:
<tr ng-repeat-end ng-if="caseRecord.showTasks" >
<td colspan="9">
<div class="case-record-task-view">
<div ng-controller="NewCaseRecordModalController">
<button class="btn btn-info"
ng-click="openEditCaseRecordDialog($parent.caseRecord)" >Edit Case Record Details</button>
<button class="btn btn-info"
ng-click="changeStateCaseRecordDialog($parent.caseRecord)" >Change State</button>
</div>
<br />
<div ng-controller="NewTaskModalController">
<p>
<button class="btn btn-primary"
ng-click="openNewTaskDialog(caseRecord.id)">Add New Task</button>
</p>
</div>
<table class="case-record-task-table">
<tr>
<td> Item </td>
<td> Description </td>
<td> Completed </td>
<td> Due Date </td>
<td> Control </td>
</tr>
<tr ng-repeat="task in caseRecord.tasks">
...
</tr><!-- ng-repeat-end ## tasks in caseRecords.tasks -->
</table>
</div>
</td>
</tr><!-- ng-repeat-end ## caseRecord in caseRecords -->
主表格中的二级行有一个 ng-repeat-end 指令,它通知 AngularJS 每个案例记录元素结束循环迭代的 DOM 元素。实际上还有一个名为 ng-repeat 的指令,它将 ng-repeat-start 和 ng-repeat-end 结合到一个单个 DOM 元素上。该指令通常用于渲染表格中的简单行。
ng-if 指令条件性地向 DOM 中添加或删除内容。我们使用这个 ng-if 来显示或隐藏每个案例记录元素的任务视图区域。AngularJS 提供了其他类似的指令,称为 ng-show 和 ng-hide,但它们不会动态地从 DOM 中添加或删除内容。
小贴士
为什么我们会选择 ng-if 而不是 ng-show?假设你的数据库中有数百个案例记录元素,我们是否想在 Web 前端渲染所有这些案例及其任务历史?
我们有一个 div-layer 元素,专门用于显示与案例记录关联的任务。看看 CSS 选择器,case-record-task-view。我们添加内容以显示每个 task 元素作为表格。有一个使用 ng-repeat 的示例,它有一个表达式任务,在 caseRecord.tasks 中。
有两个其他的内部 div 层。第一个元素绑定到编辑当前案例记录的逻辑,并引用名为 NewCaseRecordModalController 的控制器。第二个元素允许用户创建一个新的任务,并引用一个名为 NewTaskModalController 的新控制器。我们将在稍后看到这些控制器的 JavaScript 代码。
以下截图展示了显示任务的展开和收缩:

这张截图展示了使用 ng-if 的二级行元素的展开和收缩。
为了完成表格视图的内容,我们写入表格数据行以显示 task 元素的属性:
<tr ng-repeat="task in caseRecord.tasks">
<td> {{task.id}} </td>
<td>
<span class="done-{{task.completed}}"> {{task.name}} </span>
</td>
<td>
<label class="checkbox">
<input type="checkbox" ng-model="task.completed" ng-change="updateProjectTaskCompleted(task)">
Done
</label>
</td>
<td>
{{task.targetDate}}
</td>
<td>
<div ng-controller="NewTaskModalController">
<a class="btn" href="#"ng-click="openEditTaskDialog($parent.task)" >
<i class="glyphicon glyphicon-edit"></i></a>
<a class="btn" href="#"ng-click="openDeleteTaskDialog($parent.task)" >
<i class="glyphicon glyphicon-trash"></i></a>
</div>
</td>
</tr><!-- ng-repeat-end ## tasks in caseRecords.tasks -->
在视图的第四部分,我们充分利用 AngularJS 的双向绑定来渲染一个 HTML checkbox元素,并将其与布尔属性caseRecord.completed关联。使用 CSS 选择器,我们使用类选择器表达式class="done-{{task.completed}}"动态更改任务名称的文本。当用户更改复选框时,选择以下 CSS:
.done-true {
text-decoration: line-through; color: #52101d;
}
当任务完成时,文本会被划掉!我们向复选框元素添加了ng-change指令,AngularJS 将其与一个改变事件关联。AngularJS 调用控制器NewTaskModalController上的updateProjectTaskCompleted()方法。此方法调用一个WebSocket调用。我们很快会解释其背后的代码!请注意,方法调用传递了当前的task元素,因为我们仍然处于渲染作用域中。
为了完成任务视图,我们有一个与控制器NewTaskModalController关联的div层,带有用于编辑和删除任务的图标按钮。正如你所看到的,我们需要传递$parent.task以便引用元素循环变量。
是时候查看项目组织以及单个 JavaScript 模块、控制器和工厂了。
项目组织
项目被组织成一个 Java EE 网络应用程序。我们将所有的 JavaScript 代码放入遵循 AngularJS 约定的文件夹中,因为我们很可能在一个全栈环境中专业工作,并与具有混合技能的代码库共享。AngularJS 控制器放在app/controllers下,而工厂和服务放在app/service下,如下所示的结构:
src/main/webapp/app/controllers
src/main/webapp/app/controllers/main.js
src/main/webapp/app/controllers/newcaserecord-modal.js
src/main/webapp/app/controllers/newtask-modal.js
src/main/webapp/app/services
src/main/webapp/app/services/iso-countries.js
src/main/webapp/app/services/shared-services.js
接下来,我们将第三方 JavaScript 库放入指定的区域:
src/main/webapp/javascripts
src/main/webapp/javascripts/angular.js
src/main/webapp/javascripts/bootstrap.js
src/main/webapp/javascripts/jquery-2.1.3.js
src/main/webapp/javascripts/ui-bootstrap-0.12.1.js
src/main/webapp/javascripts/ui-bootstrap-tpl-0.12.1.js
注意,我们的案件工作者应用程序还依赖于 Bootstrap、jQuery 和扩展库,Bootstrap UI for AngularJS。我们在主视图index.html的内容最后部分明确包含了所有这些库,如下所示:
<html ng-app="app"> ...
<body> ...
<script src="img/jquery-2.1.3.js"></script>
<script src="img/angular.js"></script>
<script src="img/bootstrap.js"></script>
<script src="img/ui-bootstrap-tpls-0.12.1.js"></script>
<script src="img/main.js"></script>
<script src="img/newcaserecord-modal.js"></script>
<script src="img/newtask-modal.js"></script>
<script src="img/shared-service.js"></script>
<script src="img/iso-countries.js"></script>
</body>
</html>
如我之前所说,为了演示的目的,我们保持了代码库的简单性,但我们本可以使用 RequireJS 来处理依赖加载。
小贴士
如果你没有在 AngularJS 之前明确加载 jQuery,那么它将加载自己的较小版本的 jQuery,称为 jq-lite。所以如果你的应用程序依赖于 jQuery 库的完整版本,请确保它在 AngularJS 加载之前加载。
最后一步是将 CSS 放入它们自己的特殊区域:
src/main/webapp/styles
src/main/webapp/styles/bootstrap.css
src/main/webapp/styles/bootstrap-theme.css
src/main/webapp/styles/main.css
上述文件在主视图的顶部加载,位于常规的 head HTML 元素内。
应用程序主控制器
我们 AngularJS 应用程序中的第一个模块声明了应用程序的名称。以下是在文件 src/main/webapp/app/controllers/main.js 中的声明:
var myApp = angular.module('app', ['ui.bootstrap', 'newcaserecord','newtask', 'sharedService', 'isoCountries']);
该框架导出一个名为 angular 的函数对象,并且它有一个名为 module 的方法,用于定义一个模块。第一个参数是模块的名称,第二个参数是依赖模块名称的数组。module() 方法将 AngularJS 模块对象返回给调用者。从那里,我们声明初始控制器。
模块 ui.bootstrap 包含 AngularJS 和 Bootstrap 的集成。模块 newcaserecord 是案件工作人员应用程序的一部分,并定义了一个控制器,用于插入和修改主记录。模块 newtask 定义了一个控制器,用于插入、修改和删除详细记录。sharedService 定义了一个工厂提供者,执行应用程序的实用函数,最后,isoCountries 定义了另一个提供者,包含 ISO 护照国家的列表。
AngularJS 框架有一个流畅的 API 用于定义模块、控制器和提供者;因此,我们可以编写类似于以下代码摘录的几乎声明式的 JavaScript:
angular.module('myApp', [ 'depend1', 'depend2'])
.controller( 'controller1', function( depend1, depend2 ) {
/* ... */
})
.controller( 'controller2', function( depend1 ) {
/* ... */
})
.filter('greet', function() {
return function(name) {
return 'Hello, ' + name + '!';
};
})
.service( 'our-factory', function( ... ) {
/* ... */
})
.directive( 'my-directive', function( ... ) {
/* ... */
});
上述编码风格是个人喜好问题,其缺点是所有模块都被合并在一起。许多专业开发者更喜欢将实际的 Angular 模块对象分配给全局模块变量。
视图中的 body 标签元素定义了一个控制器:
<body ng-controller="CaseRecordController">
以下摘录显示了将用户界面绑定到客户端数据模型的控制器 CaseRecordController:
myApp.controller('CaseRecordController', function ($scope, $http, $log, UpdateTaskStatusFactory, sharedService, isoCountries ) {
var self = this;
$scope.caseRecords = [{sex: "F", firstName: "Angela", lastName: "Devonshire", dateOfBirth: "1982-04-15", expirationDate: "2018-11-21", country: "Australia", passportNo: "123456789012", currentState: "Start"},];
$scope.isoCountries = isoCountries;
$scope.getCaseRecords = function () {
$http.get('rest/caseworker/list').success(function(data) {
$scope.caseRecords = data;
});
}
$scope.$on('handleBroadcastMessage', function() {
var message = sharedService.getBroadcastMessage();
if ( message !== "showTasksCaseRecord") {
$scope.getCaseRecords();
}
})
// Retrieve the initial list of case records
$scope.getCaseRecords();
$scope.connect = function() {
UpdateTaskStatusFactory.connect();
}
$scope.send = function( msg ) {
UpdateTaskStatusFactory.send(msg);
}
$scope.updateProjectTaskCompleted = function( task ) {
var message = { 'caseRecordId': task.caseRecordId, 'taskId': task.id, 'completed': task.completed }
$scope.connect()
var jsonMessage = JSON.stringify(message)
$scope.send(jsonMessage)
}
});
AngularJS 对象中的控制器方法接受第一个参数作为名称。第二个参数是函数对象,按照惯例,我们传递一个带有参数的匿名 JavaScript 函数。
function ($scope, $http, $log, UpdateTaskStatusFactory, sharedService, isoCountries ) { /* ... */ }
参数都是 AngularJS 注入到控制器中的对象模块。AngularJS 定义了以美元符号($)开头的标准模块。模块 $scope 是一个特殊参数,表示当前作用域。模块 $http 代表一个核心 AngularJS 服务,具有与远程 HTTP 服务器通信的方法。模块 $log 是另一个核心服务,用于向控制台记录日志。其他参数 UpdateTaskStatusFactory、sharedService 和 isoCountries 是我们应用程序提供的工厂和服务。AngularJS,像许多现代 JavaScript 数字框架一样,鼓励模块化编程,并尽可能避免污染全局作用域。
那么这个控制器具体做什么呢?首先,出于演示目的,控制器初始化了一个虚拟的 JSON 记录,$scope.caseRecord,以防在页面视图加载时服务器不可用。接下来,我们为记录列表定义了一个属性,$scope.caseRecords。是的,向 AngularJS $scope 添加自定义属性是数据模型与用户界面通信的方式。
我们为控制器定义了属性,$scope.isoCountries。
我们定义了第一个函数,getCaseRecords(),如下所示:
$scope.getCaseRecords = function () {
$http.get('rest/caseworker/list').success(function(data) {
$scope.caseRecords = data;
});
}
此函数从同一主机向远程服务器发出 RESTful GET 请求,该主机提供页面视图。URL 可能类似于这样:http://localhost:8080/xen-national-force/rest/caseworker/list。
我们利用流畅的 API 在服务器返回 JSON 结果后执行一个操作。匿名函数用最新的数据覆盖了 $scope.caseRecords 属性。
顺便说一下,当我们构建 CaseRecordController 的函数对象时,我们调用 getCaseRecords() 方法来启动应用程序。
在 AngularJS 中,我们可以通过创建应用程序创建的工厂服务或通过向服务器发出 HTTP 请求,将信息从一个控制器传递到另一个控制器。还有可能监听 AngularJS 在广播频道上发布的事件。
在 CaseRecordController 中的以下代码演示了如何更新除一条消息之外的所有消息的用户界面:
$scope.$on('handleBroadcastMessage', function() {
var message = sharedService.getBroadcastMessage();
if ( message !== "showTasksCaseRecord") {
$scope.getCaseRecords();
}
})
在这里,我们在 AngularJS 作用域上注册了一个事件处理器,以便从我们的 SharedService 提供者获取通知。$on() 方法在特定事件类型上注册了一个监听器。第一个参数是消息类型,第二个参数是回调函数。在函数回调内部,如果消息(因此是自定义事件)不是 showTasksCaseRecord,我们就会向服务器发出 HTTP 请求以检索整个案例记录集。
小贴士
在处理程序回调函数内部,我们读取整个数据集,这在实际的企业应用程序中可能是数千个案例记录。因此,我们可以提高 REST 调用和响应代码的性能。然而,我们应该抵制过早优化的诱惑。你应该优先考虑让用户故事工作。
控制器中的其他方法,connect() 和 send(),分别建立到服务器的 WebSocket 通道并向服务器发送 JSON 消息。我们将在稍后的部分检查 UpdateTaskStatusFactory 模块,以及最后的 updateProjectTaskCompleted() 方法。
如果你以前从未专业地开发过任何 JavaScript,那么这个章节可能一开始看起来非常令人畏惧。然而,请坚持下去,因为这实际上只是关于有足够的耐心才能成功。在这方面,我准备了一个简化的 AngularJS 作用域图,它在我们案件工作人员应用程序中呈现的样子。

案件工作人员应用程序中的 AngularJS 作用域
上述图表描绘了进度之旅,帮助我们了解我们将要走向何方。它还建立了 AngularJS 如何以类似于 DOM 本身的方式分层绑定作用域的概念。幕后,AngularJS 创建内部作用域来处理渲染 HTML table 元素的重复性 DOM 元素,这是案例记录的列表。开发者只能通过编程表达式来访问这些内部数据,我们应该将它们视为不透明对象。
小贴士
在撰写本文时,有一个名为 Batarang 的 Google Chrome 插件(chrome.google.com/webstore/detail/angularjs-batarang-stable/),我强烈推荐检查浏览器中的 AngularJS 作用域。遗憾的是,这个工具似乎不再维护。如果有人采用了它,仍然值得检查。
新案例记录控制器
我们将创建和编辑案例记录的代码放置在一个名为 newcaserecord-modal.js 的单独文件中,该文件包含用户定义的 AngularJS 模块 newcaserecord。此模块依赖于其他模块,其中一些之前已经提到。ui.bootstrap.modal 是来自 AngularJS UI Bootstrap 第三方框架的一个特殊模块。该模块定义了 AngularJS 团队编写的 Bootstrap 组件。特别是,它有一个有用的模态对话框扩展,我们在整个案件工作人员应用程序中使用。
以下是为 newcaserecord 模块和 NewCaseRecordModalController 的简短代码:
var newcaserecord = angular.module('newcaserecord', ['ui.bootstrap.modal', 'sharedService','isoCountries'])
newcaserecord.controller('NewCaseRecordModalController', function($scope, $modal, $http, $log, sharedService, isoCountries ) {
$scope.caseRecord = {
sex: "F", firstName: "", lastName: "", country: "", passportNo: "", dateOfBirth: "", expirationDate: "", country: "", currentState: "", showTasks: false};
$scope.returnedData = null;
$scope.isoCountries = isoCountries;
$scope.openCreateCaseRecordDialog = function () {
var modalInstance = $modal.open({
templateUrl: 'newCaseRecordContent.html', controller: newCaseRecordModalInstanceController, isoCountries: isoCountries, resolve: {
caseRecord: function () {
return $scope.caseRecord;
}
}
});
modalInstance.result.then(function (data) {
$scope.selected = data;
$http.post('rest/caseworker/item', $scope.caseRecord).success(function(data) {
$scope.returnedData = data;
sharedService.setBroadcastMessage("newCaseRecord");
});
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
// . . .
);
控制器函数对象接受注入的参数,如 $http、$log 和 sharedService。我们还注入了 $modal 实例,这使得我们可以在控制器中打开模态对话框。
由于每个控制器都注入了自己的作用域,我们需要提供数据模型元素以便视图可以访问。因此,我们在作用域中创建了一个空的案例记录 $scope.caseRecord。我们还设置了返回数据和 ISO 国家列表。
函数 $scope.openCreateCaseRecordDialog() 生成一个模态对话框,因此用户可以输入主案例记录。
提示
允许用户创建任意应用程序护照记录可能会被禁止,并且仅限于管理员和经理以外的任何员工。我们的演示应用程序根本没有任何角色和权限的概念。开发者应该小心,避免将零日漏洞引入他们的数字应用程序中。
UI Bootstrap 扩展接受多个参数。第一个参数是 HTML 模板指令的引用。第二个参数指的是另一个名为 newCaseRecordModalInstanceController 的控制器,该控制器负责处理与对话框的交互。第三个参数是解析器,它允许库代码在封装作用域内找到用户模态中的引用数据:
var modalInstance = $modal.open({
templateUrl: 'newCaseRecordContent.html',
controller: newCaseRecordModalInstanceController,
resolve: {
caseRecord: function () {
return $scope.caseRecord;
}
}
});
控制器的下一部分,NewCaseRecordModalController 处理模态对话框成功完成后的回调,因为用户输入了数据并按下了确认按钮。我们在名为 then 的对象上注册了两个函数对象作为参数。
modalInstance.result.then(function (data) {...},
function () { /* modal dismissed */ });
第一个函数是回调处理程序,其中包含将案例记录数据发送到服务器的 REST POST 请求的代码。第二个函数保留用于对话框被取消的情况。你会注意到 AngularJS 使用了流畅的接口。即使你不了解 JavaScript 和框架的所有内容,代码也应该相当容易理解。
因此,让我们看看模态对话框实例的代码,即对象 newCaseRecordModalInstanceController:
var newCaseRecordModalInstanceController = function ($scope, $modalInstance, caseRecord ) {
caseRecord.showTasks = true; // Convenience for the user
$scope.caseRecord = caseRecord;
$scope.ok = function () {
$modalInstance.close(true);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
};
如果你注意到,这个变量并不是一个封装的 JavaScript 模块;相反,newCaseRecordModalInstanceController 函数是在全局作用域中声明的。我想总有例外。UI Bootstrap 代码通过 $modalInstance.open() 调用调用这个控制器函数。框架向该函数提供三个参数,即作用域 $scope、模态实例 $modalInstance 和案例记录 caseRecord。我们将案例记录分配给提供的作用域,以便从模态对话框中写回数据。在那里,函数对象实现了两个方法,ok() 和 cancel(),分别处理对话框的确认和取消。
我们只需要编写对话框的 HTML 指令。
案例记录模态视图模板
如我们所知,网站的所有内容都在一个单页应用程序中。HTML 指令也出现在视图index.html中。如何在页面内容中写入指令而不在视图中显示?秘诀是否与 CSS 有关?
虽然样式化是一个好主意,但这并不是正确答案。AngularJS 设计者利用了 HTML Script 标签的正式定义,这是嵌入或引用可执行脚本的元素。
以下是将新案件记录插入应用程序的 HTML 指令:
<script type="text/ng-template" id="newCaseRecordContent.html">
<div class="modal-header">
<h3>New Case Record </h3>
</div>
<div class="modal-body">
<form name="newCaseRecordForm" class="css-form" novalidate>
Sex:<br />
<select ng-model="caseRecord.sex" required>
<option value="F" ng-option="selected caseRecord.sex === 'F'">Female</option>
<option value="M" ng-option="selected caseRecord.sex === 'M'">Male</option>
</select>
<br/>
First Name:<br />
<input type="text" ng-model="caseRecord.firstName" required /><br />
Last Name:<br />
<input type="text" ng-model="caseRecord.lastName" required /><br />
Date of Birth:<br />
<input type="text" ng-model="caseRecord.dateOfBirth" datepicker-popup="yyyy-MM-dd" required /><br />
Country:<br />
<select ng-model="caseRecord.country" required ng-options="item.code as item.country for item in isoCountries.countryToCodeArrayMap">
</select>
<br />
Passport Number:<br />
<input type="text" ng-model="caseRecord.passportNo" required /><br />
Expiration Date:<br />
<input type="text" ng-model="caseRecord.expirationDate" datepicker-popup="yyyy-MM-dd" required /><br />
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()" ng-disabled="newCaseRecordForm.$invalid" >OK</button>
<button class="btn btn-warning" ng-click="cancel()">Cancel</button>
</div>
</script>
前面的 HTML 指令定义了一个 UI Bootstrap 模式对话框,因为 HTML script标签被标记为text/ng-template类型的属性。所有 AngularJS 指令都需要一个标识符。从 CSS 中我们可以看到,该指令包含一个标题、页脚和主体。主要的div层是一个 HTML 表单。
表单中的每个输入字段都与newCaseRecordModalInstanceController实例中的数据模型绑定。一旦 UI Bootstrap 调用了函数对象,案件记录就被分配到了作用域中。因此,ng-model数据模型$scope.caseRecord.firstName对保留用于姓氏的 HTML 文本输入元素是可用的。
AngularJS 有一个优雅的额外标记用于验证表单输入元素。你可以在几乎所有输入上看到额外的必需属性。不幸的是,由于本书无法深入探讨验证检查的细节,我想将你的注意力引向两个微妙的验证检查。
数据输入利用 UI Bootstrap 日期选择器组件,使案件工作人员能够轻松输入日期:
<input type="text" ng-model="caseRecord.dateOfBirth" datepicker-popup="yyyy-MM-dd" required />
日期的格式由datepicker-popup属性定义。
最后,我们在 HTML select元素中显示 ISO 护照国家名称的下拉列表。这部分代码如下:
<select ng-model="caseRecord.country" required
ng-options="item.code as item.country for item in
isoCountries.countryToCodeArrayMap">
</select>
isoCountries是一个服务实例,我们将在后面看到。由于该模块被注入到NewCaseRecordModalController模块中,并且后者的作用域恰好包围了模式实例作用域,AngularJS 允许我们访问该服务。isoCountries实例包含一个护照国家的键值字典列表。代码允许我们将 ISO 代码AUS与国家名称澳大利亚关联起来。ng-option属性接受一个表达式,类似于 SQL 查询。我们声明性地通知 AngularJS 如何为每个 HTML option元素推导显示名称(item.country)和输入表单值(item.code)。
以下是一个带有日期选择器的创建案件记录模式对话框的截图:

带有日期选择器的创建案件记录模式对话框的截图,日期选择器效果全开
让我们转到与案件记录控制器类似的任务记录控制器。
新的任务记录控制器
当案件工作人员使用系统时,他们能够展开和折叠与案例记录相关的任务记录。用户可以创建、编辑和修改任务,还可以更改案件的状态。
AngularJS 模块 newtask 定义如下:
var newtask = angular.module('newtask', ['ui.bootstrap.modal', 'sharedService'])
newtask.config(function($httpProvider) {
$httpProvider.defaults.headers["delete"] = {
'Content-Type': 'application/json;charset=utf-8'
};
})
我们在 AngularJS 的 HTTP 远程处理周围添加了一个配置更改。HTTP DELETE 请求中存在一个微妙的错误。存在于 GlassFish 和 Payara 应用服务器中的 JAX-RS 引用实现 Jersey,在响应代码 415 时引发了一个 HTTP 错误:“不支持的媒体类型”。这迫使 AngularJS 在 DELETE 请求中发送 MIME 类型,将 JSON 作为解决方法。
由于任务控制器的代码非常相似,本书中仅揭示 CRUD 的创建部分。有关其他方法的源代码,请参阅源文件。以下为 NewTaskModalController 的源代码:
newtask.controller('NewTaskModalController', function($scope, $modal, $http, $log, sharedService ) {
$scope.selected = false;
$scope.task = {
id: 0, name: '', targetDate: null, completed: false, caseRecordId: 0
};
$scope.returnedData = null;
$scope.openNewTaskDialog = function(caseRecordId) {
var modalInstance = $modal.open({
templateUrl: 'newTaskContent.html',
controller: newTaskModalInstanceController,
resolve: {
task: function () {
return $scope.task;
}
}
});
modalInstance.result.then(function (data) {
$scope.selected = data;
$http.post('rest/caseworker/item/'+caseRecordId+'/task', $scope.task).success(function(data) {
$scope.returnedData = data;
sharedService.setBroadcastMessage("newTask");
// Reset Task in this scope for better UX affordance.
$scope.task = {
id: 0, name: '', targetDate: null, completed: false, caseRecordId: 0
};
});
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
$scope.openEditTaskDialog = function(taskItem) {
// ...
};
$scope.openDeleteTaskDialog = function(taskItem) {
// ...
};
});
在此控制器中,我们有一个空的、默认的 $scope.task 对象,而不是 $scope.caseRecord。每个 Task 对象都通过 caseRecordId 属性引用其父对象。
函数 openNewTaskDialog() 打开一个 UI Bootstrap 模态对话框,允许用户输入一个新任务。该方法将模态对话框与当前 Task 对象的 AngularJS 作用域连接起来。主要区别是 REST URL 端点,其形式为 rest/caseworker/item/'+caseRecordId+'/task。
我们使用 UI Bootstrap 的 $modal 对象,并创建一个与之前相同的模态对话框实例,但现在我们传递了不同的参数。这些参数是 HTML 指令 ID,即 newTaskContent.html;控制器名为 newTaskModalInstanceController,以及解析函数。AngularJS 调用解析函数,该函数定义为一个匿名函数,以便引用封装的 Task 对象。
在 modalInstance 对象的回调函数中,我们方便地重置了 Task 对象,这样当对话框再次弹出时,用户不会被过时的表单数据所惊讶。我们在 sharedService 中设置了广播消息。
处理任务对话框中模态实例的代码几乎相同:
var newTaskModalInstanceController = function ($scope, $modalInstance, task) {
$scope.task = task;
$scope.ok = function () {
$modalInstance.close(true);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
};
newTaskModalInstanceController 函数接受三个参数:绑定模态实例对话框的 $scope,$modalInstance 本身,以及 Task 对象。最后一个参数,即 Task 对象,被解析,并将其设置为作用域上的一个属性,以便在模板中轻松渲染视图。
任务模态视图模板
AngularJS 指令 newTaskContent.html 渲染允许用户输入新任务的模态对话框视图。由于只有四个属性,因此此视图比案例记录更短。
此视图的定义如下:
<script type="text/ng-template" id="newTaskContent.html">
<div class="modal-header">
<h3>New Task</h3>
</div>
<div class="modal-body">
<form name="newTaskForm" class="css-form" novalidate>
Task Name:<br />
<textarea ng-model="task.name" rows="3" required /><br />
Target Date: <br />
<input type="text" datepicker-popup="yyyy-MM-dd" ng-model="task.targetDate" required /><br />
Task Completed: <br />
Done <input type="checkbox" ng-model="task.completed" />
<br />
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()" ng-disabled="newTaskForm.$invalid" >OK</button>
<button class="btn btn-warning" ng-click="cancel()">Cancel</button>
</div>
</script>
此视图也遵循 UI Bootstrap CSS 样式用于模态对话框。我们演示了一个与数据模型关联的 HTML text area元素,该数据模型是Task对象。每个表单字段都有一个ng-model关联。对于目标日期,我们重用了日期选择器,并说明了如何使用 HTML checkbox元素。
编辑和删除任务记录的代码看起来大致相同。然而,对于编辑,用户在确认模态对话框后不会重置任务记录,而对于删除,我们只显示任务记录的只读视图;模态对话框只是一个确认。
让我们看看我们如何处理状态的变化。
状态变化
案例记录存在于以下状态:
| 状态 | 描述 |
|---|---|
| 开始 | 系统中的每位新申请人都是从这一初始状态开始的 |
| 结束 | 在流程结束时,申请人的案例将结束在这个最终状态 |
| 审查 | 案例工作人员正在审查申请人的记录 |
| 决定 | 案例已经审查完毕,业务正在做出决定 |
| 接受 | 案例已被接受,申请人正在被通知 |
| 拒绝 | 案例已被拒绝,申请人正在被拒绝 |
所有这些业务需求都被捕获在有限状态机中。
控制器代码
到现在为止,代码应该对您来说已经很熟悉了。NewTaskModalController中的控制器方法changeStateCaseRecordDialog()如下所示:
$scope.changeStateCaseRecordDialog = function (caseRecordItem) {
/* Copy */
$scope.caseRecord = {
id: caseRecordItem.id,
firstName: caseRecordItem.firstName,
lastName: caseRecordItem.lastName,
dateOfBirth: caseRecordItem.dateOfBirth,
country: caseRecordItem.country,
passportNo: caseRecordItem.passportNo,
expirationDate: caseRecordItem.expirationDate,
currentState: caseRecordItem.currentState,
nextStates: caseRecordItem.nextStates,
showTask: caseRecordItem.showTasks
};
$scope.caseRecord.nextStates.push( caseRecordItem.currentState );
$scope.saveCurrentState = caseRecordItem.currentState;
var modalInstance = $modal.open({
templateUrl: 'changeStateCaseRecordContent.html', controller: moveStateRecordModalInstanceController, resolve: {
caseRecord: function () {
return $scope.caseRecord;
}
}
});
modalInstance.result.then(function (data) {
$scope.selected = data;
if ( $scope.saveCurrentState !== $scope.caseRecord.currentState ) {
$http.put('rest/caseworker/state/'+$scope.caseRecord.id, $scope.caseRecord).success(function(data) {
$scope.returnedData = data;
sharedService.setBroadcastMessage("editCaseRecord");
});
}
}, function () { $log.info('Modal dismissed."); } );
};
由于我们只是在编辑现有的案例记录,所以我们把CaseRecord的属性从封装作用域复制到控制器作用域。记住,外部作用域是主模块。
服务器发送的每个 JSON 案例记录(我们稍后会看到)都有一个名为nextStates的属性,它是一个列表,包含用户可以将记录移动到的下一个可能的状态。以一个例子来说,开始状态只有一个可能的后继状态,称为Reviewing。
每个案例记录对象都有一个currentState属性。我们将当前状态推送到当前作用域中存储的后继状态列表。这个数组$scope.nextStates允许对话框 HTML 指令在视图中渲染下拉菜单。
您可以看到,这个函数changeStateCaseRecordDialog()打开了一个 UI Bootstrap 模态对话框。
模板视图代码
因此,让我们检查状态变化的 HTML 指令:
<script type="text/ng-template" id="changeStateCaseRecordContent.html">
<div class="modal-header">
<h3>Change State of Case Record</h3>
</div>
<div class="modal-body">
<p>
<table class="table table-bordered">
<tr>
<th> Field </th> <th> Value </th>
</tr>
<tr>
<td> Case Record Id</td> <td> {{caseRecord.id }}</td>
</tr>
...
</table>
</p>
<form name="moveStateCaseRecordForm" class="css-form" novalidate>
Next States:<br />
<select ng-model="caseRecord.currentState" ng-options="state for state in caseRecord.nextStates">
</select>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()" ng-disabled="moveStateCaseRecordForm.$invalid" >OK</button>
<button class="btn btn-warning" ng-click="cancel()">Cancel</button>
</div>
</script>
前面的指令,标识为changeStateCaseRecordContent.html,本质上是对整个案例记录的只读视图。唯一可修改的部分是显示案例记录下一个可能状态的 HTML select元素。为了生成 HTML option元素,有一个不同的表达式形式用于ng-options属性,它声明为state for state in caseRecord.nextStates。这个表达式意味着数组 String 的选项名称和值是相同的,如下所示:

更改案例记录的状态
模态实例代码基本上是相同的。与对话框相关联的相应函数被称为 moveStateRecordModalInstanceController()。
var moveStateRecordModalInstanceController = function ($scope, $modalInstance, caseRecord) {
$scope.caseRecord = caseRecord;
$scope.ok = function () { $modalInstance.close(true); };
$scope.cancel = function () { $modalInstance.dismiss('cancel'); };
};
在我们结束这个关于 AngularJS 和客户端的漫长示例之前,我们将介绍几个更多功能。这些功能是定义 NewCaseRecordModalController 模块的组成部分。
切换任务显示状态
第一个函数 showOrHideTasks(),切换案件记录中的显示属性 showTasks。它还会向服务器发送一个带有案件记录 JSON 数据的 HTTP PUT 请求。代码如下:
$scope.showOrHideTasks = function(caseRecord) {
caseRecord.showTasks = !caseRecord.showTasks;
$http.put('rest/caseworker/showtasks/'+caseRecord.id, caseRecord).success(function(data) {
sharedService.setBroadcastMessage("showTasksCaseRecord");
});
}
第二个函数 getIconClass() 是一种作弊模式。它根据显示状态返回 Bootstrap CSS 图标选择器。AngularJS 确实有一个用于 ng-class 的条件表达式;然而,在编写本文时,作者无法使其对案件记录元素数组起作用。因此,这个函数作为代码库中的解决方案存在。
$scope.getIconClass = function(caseRecord) {
if ( caseRecord.showTasks)
return "glyphicon-minus"
else
return "glyphicon-plus"
}
如果你感兴趣,客户端应该工作的正确代码如下:
<i class="glyphicon" ng-class="{true: 'glyphicon-minus', false: 'glyphicon-plus'}[caseRecord.showTasks]">
我们现在将跳转到服务器端。
服务器端 Java
我们为案件工作者系统构建的 Java EE 应用程序是基于 RESTful 服务、Java WebSocket、JSON-P 和 Java 持久性的。
提示
本书本节依赖于对 Java EE 开发从基础水平到高级水平的前期理解。我建议您阅读姊妹书籍 Java EE 7 开发手册,特别是如果您发现这些主题难以理解的话。
实体对象
没有几个领域对象,服务器端将什么都不是。这些被称为 CaseRecord 和 Task 的名称并不令人惊讶。
以下是从 CaseRecord 实体对象中提取的带有完整注解的代码:
@NamedQueries({
@NamedQuery(name="CaseRecord.findAllCases",
query = "select c from CaseRecord c order by c.lastName, c.firstName"),
/* ... */
})
@Entity
@Table(name = "CASE_RECORD")
public class CaseRecord {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@NotEmpty @Size(max=64) private String lastName;
@NotEmpty @Size(max=64) private String firstName;
@NotEmpty @Size(max=1) private String sex;
@NotEmpty @Size(max=16) private String passportNo;
@NotEmpty @Size(max=32) private String country;
@Past @NotNull @Temporal(TemporalType.DATE) private Date dateOfBirth;
@Future @NotNull @Temporal(TemporalType.DATE) private Date expirationDate;
@NotEmpty private String currentState;
private boolean showTasks;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "caseRecord", fetch = FetchType.EAGER)
private List<Task> tasks = new ArrayList<>();
// Required by JPA
public CaseRecord() {}
/* ... */
}
对于这些实体,我们利用流行的 Hibernate Validator 注解来确保信息被正确地保存到数据库中。详细的 Task 实体如下:
@Entity
public class Task {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="TASK_ID") private Integer id;
@NotEmpty @Size(max=256) private String name;
@Temporal(TemporalType.DATE)
@Column(name="TARGET_NAME") @Future
private Date targetDate;
private boolean completed;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name="CASE_RECORD_ID")
private CaseRecord caseRecord;
public Task() { /* Required by JPA */ }
/* ... */
}
实体对象映射非常紧密地对应于我们在客户端看到的 JavaScript 对象。在实践中,不同领域的业务应用程序可能会选择替代设计,例如数据模型的门面、聚合或投影。
当然,这些实体都有一个持久层,以便将信息检索和存储到数据库中。在源代码中,有一个 CaseRecordTaskService 负责持久化 CaseRecord 和 Task 记录。
RESTful 通信
无状态的会话 EJB 类 CaseWorkerRESTServerEndpoint 作为我们的 RESTful 端点:
package uk.co.xenonique.nationalforce.control;
/* ... */
import javax.json.*;
import javax.json.stream.*;
import javax.ws.rs.*;
import javax.ws.rs.container.*;
import javax.ws.rs.core.*;
import static javax.ws.rs.core.MediaType.*;
@Path("/caseworker/")
@Stateless
public class CaseWorkerRESTServerEndpoint {
static JsonGeneratorFactory jsonGeneratorFactory = Json.createGeneratorFactory(new HashMap<String, Object>() {{
put(JsonGenerator.PRETTY_PRINTING, true);
}});
@Inject
CaseRecordTaskService service;
/* ... */
}
这个类用 @Path 注解了此端点的初始 URI。这个相对 URI /caseworker/ 与 AngularJS 客户端匹配。我们将持久状态会话 EJB CaseRecordTaskService 注入到这个端点中,并且我们还设置了一个 JSON 生成器工厂,它将打印 JSON 输出。我们在整个 Java EE 7 中使用标准的 JSON 生成器工厂。
案件记录检索
为了处理案例工作者记录的检索,我将演示如何使用 JAX-RS 处理异步操作。我们需要从应用程序服务器中获取一个管理的执行器,并确保在部署后 Web 应用程序支持async操作。
对于 Java EE 7 来说,在 Web XML 部署描述符(src/main/web-app/WEB/web.xml)中启用异步支持至关重要。此文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...
version="3.1" ... >
<servlet>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
<resource-env-ref>
<resource-env-ref-name>
concurrent/LongRunningTasksExecutor
</resource-env-ref-name>
<resource-env-ref-type>
javax.enterprise.concurrent.ManagedExecutorService
</resource-env-ref-type>
</resource-env-ref>
</web-app>
重要的 XML 元素是<async-supported>,我们将其内容设置为 true。我们还设置了整个应用程序接收 REST 查询的 URI,即/rest。因此,结合CaseWorkerRESTServerEndpoint类,完整的相对 URI 到目前为止是/rest/caseworker。最后,我们通过在<resource-env-ref>周围添加 XML 元素向 Java EE 7 应用程序服务器声明我们的应用程序需要一个管理的执行器。这个管理的执行器被称为concurrent/LongRunningTasksExecutor(JNDI 查找名称)。
我们现在将在第一个 REST 查询方法中使用它:
@Resource(name="concurrent/LongRunningTasksExecutor")
ManagedExecutorService executor;
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/list")
public void getCaseRecordList(
@Suspended final AsyncResponse asyncResponse) {
executor.submit(new Runnable() {
@Override
public void run() {
final List<CaseRecord> caseRecords = service.findAllCases();
final StringWriter swriter = new StringWriter();
final JsonGenerator generator = jsonGeneratorFactory.createGenerator(swriter);
CaseRecordHelper.generateCaseRecordAsJson(generator, caseRecords).close();
final Response response = Response.ok(swriter.toString()).build();
asyncResponse.resume(response);
}
});
}
我们使用@GET注解标注getCaseRecordList()方法来处理来自完整相对 URI /rest/caseworker/list的 HTTP GET 请求。此方法异步执行。它依赖于注入的ManagedExecutorService实例,这是一个 Java EE 7 管理的线程池执行器。为了参与服务,我们提供了一个方法参数,即标注了@Suspended的AsyncResponse对象。
我们getCaseRecordList()方法的主体将一个工作实例(java.lang.Runnable)提交给管理的执行器服务。该工作实例从持久化服务检索一系列案例记录并将它们转换为 JSON 输出。输出被转换为字符串,然后我们通过AsyncResponse实例的resume()方法请求开始通过输出通道向客户端发送数据。我们使用 JAX RS 的@Produces注解来标注getCaseRecordList()方法,以声明输出内容的 MIME 类型为application.json。
小贴士
顺便提一下,Java EE 7 中有两个@Produces注解。一个是 JAX-RS 的一部分,另一个是 CDI。
我们还有一个通过 ID 检索特定案例记录的 REST 端点。让我们看看我们如何实现这一点:
@GET
@Path("/item/{id}")
@Produces(APPLICATION_JSON)
public String retrieveCase(
@PathParam("id") int caseId ) {
List<CaseRecord> caseRecords = service.findCaseById( caseId );
StringWriter swriter = new StringWriter();
JsonGenerator generator = jsonGeneratorFactory.createGenerator(swriter);
CaseRecordHelper.writeCaseRecordAsJson(generator, caseRecords.get(0)).close();
return swriter.toString();
}
retrieveCase()方法被标注为@GET以处理 HTTP GET 请求。它具有相对 URI /rest/caseworker/item/{id}。该方法通过 ID 搜索案例记录并创建其 JSON 表示。它同步地将输出发送到客户端。简短说明:为了节省空间,我们在这些摘录中移除了健全性检查代码。
创建案例记录
我们已经涵盖了检索方面,现在我们转向创建 REST 端点。在我们的系统中,Web 客户端可以使用 REST 调用创建案例记录。以下代码将一个新的案例记录插入到应用程序中。创建新案例记录的相对 URI 是/rest/caseworker/item。
@POST
@Path("/item")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public String createCase( JsonObject json )
throws Exception {
CaseRecord caseRecord = new CaseRecord();
caseRecord.setSex(json.getString("sex"));
caseRecord.setFirstName(json.getString("firstName"));
caseRecord.setLastName(json.getString("lastName"));
caseRecord.setCountry(json.getString("country"));
caseRecord.setPassportNo(json.getString("passportNo"));
caseRecord.setDateOfBirth( CaseRecordHelper.FMT2.parse(json.getString("dateOfBirth")));
caseRecord.setExpirationDate( CaseRecordHelper.FMT2.parse(json.getString("expirationDate")));
caseRecord.setCurrentState( BasicStateMachine.FSM_START.toString());
caseRecord.setShowTasks(json.getBoolean("showTasks", false));
JsonArray tasksArray = json.getJsonArray("tasks");
if ( tasksArray != null ) {
for ( int j=0; j<tasksArray.size(); ++j ) {
JsonObject taskObject = tasksArray.getJsonObject(j);
Task task = new Task(taskObject.getString("name"), ( taskObject.containsKey("targetDate") ?
CaseRecordHelper.FMT.parse(taskObject.getString("targetDate")) : null ), taskObject.getBoolean("completed"));
caseRecord.addTask(task);
task.setCaseRecord(caseRecord);
}
}
service.saveCaseRecord(caseRecord);
StringWriter swriter = new StringWriter();
JsonGenerator generator = jsonGeneratorFactory.createGenerator(swriter);
CaseRecordHelper.writeCaseRecordAsJson(generator, caseRecord).close();
return swriter.toString();
}
createCase() 方法较长,因为它将 JSON-P 对象实例内部的数据传输到 CaseRecord 实体中。我们使用 @POST 注解该方法,表示此端点处理 HTTP POST 请求。这是一段冗长的样板代码,通过在其他非 Java EE 7 框架(如 GSON code.google.com/p/google-gson/)或 Faster Jackson 处理 JSON API (wiki.fasterxml.com/JacksonInFiveMinutes)上的数据类型绑定来解决。但在这里我必须展示标准方法。我们得等到规范主体交付 JSON-B(Java JSON 绑定 API)之后,我们才能使这段代码更加精简。
更新案例记录
更新案例记录与创建新记录非常相似,不同之处在于我们首先通过其 ID 搜索记录,然后从 JSON 输入逐字段更新记录字段。
updateCase() 方法如下:
@PUT
@Path("/item/{caseId}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public String updateCase(
@PathParam("caseId") int caseId, JsonObject json ) throws Exception {
final List<CaseRecord> caseRecords = service.findCaseById(caseId);
CaseRecord caseRecord = caseRecords.get(0);
caseRecord.setSex(json.getString("sex"));
/* ... omitted */
caseRecord.setDateOfBirth( FMT2.parse( json.getString("dateOfBirth")));
caseRecord.setExpirationDate( FMT2.parse(json.getString("expirationDate")));
caseRecord.setCurrentState( BasicStateMachine.retrieveCurrentState( json.getString("currentState", BasicStateMachine.FSM_START.toString())).toString());
caseRecord.setShowTasks(json.getBoolean("showTasks", false));
service.saveCaseRecord(caseRecord);
final StringWriter swriter = new StringWriter();
final JsonGenerator generator = jsonGeneratorFactory.createGenerator(swriter);
CaseRecordHelper.writeCaseRecordAsJson(generator, caseRecord).close();
return swriter.toString();
}
这个 RESTful 端点使用 @PUT 注解来处理 HTTP PUT 请求。这次,相对 URI 是 /rest/caseworker/item/{id},表示客户端必须提供案例记录 ID。同样,我们复制 JSON 对象中的值,并覆盖从持久化中检索到的 CaseRecord 中的属性;然后我们保存记录。我们生成记录的 JSON 表示,并将其设置为 JAX-RS 将发送回客户端的响应。
静态实例 FMT2 是一个 java.text.SimpleDateFormat,它在过期日期和出生日期字符串与 java.util.Date 实例之间进行转换。模式格式是 yyyy-MM-dd。BasicStateMachine 实例是有限状态机的实现。FSM_START 是可能状态之一的一个单例实例。请参考书籍的源代码,以了解其实现方式。
创建任务记录
我们现在将迅速检查任务记录的创建、更新和删除端点。检索已经解决,因为每个 CaseRecord 实例都有一个零个或多个 Task 实体的集合,这满足了主从详细安排。
创建和更新任务记录是非常相似的操作。所以让我们首先研究创建方法:
@POST
@Path("/item/{caseId}/task")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public String createNewTaskOnCase(
@PathParam("caseId") int caseId, JsonObject taskObject ) throws Exception
{
final List<CaseRecord> caseRecords =
service.findCaseById(caseId);
final CaseRecord caseRecord = caseRecords.get(0);
final Task task = new Task(
taskObject.getString("name"),
( taskObject.containsKey("targetDate") ?
CaseRecordHelper.convertToDate(
taskObject.getString("targetDate")) :
null ),
( taskObject.containsKey("completed")) ?
taskObject.getBoolean("completed") : false );
caseRecord.addTask(task);
service.saveCaseRecord(caseRecord);
final StringWriter swriter = new StringWriter();
JsonGenerator generator =
jsonGeneratorFactory.createGenerator(swriter);
CaseRecordHelper.writeCaseRecordAsJson(
generator, caseRecord).close();
return swriter.toString();
}
我们使用 @POST 注解 createNewTaskOnCase() 方法。相对 URI 是 /rest/caseworker/item/{caseId}/task。客户端提交父案例记录,该方法使用此 ID 来检索适当的 CaseRecord。与从新任务记录控制器引用 AngularJS 客户端端点交叉引用可能是个好主意。在 createNewTaskOnCase() 内部,我们再次移除了健全性检查代码,以便专注于实质内容。代码的下一部分是将 JSON 映射到 Java 实体。之后,我们将 Task 实体添加到 CaseRecord 中,然后持久化主记录。一旦我们写入响应,该方法就完成了。
更新任务记录
updateTaskOnCase()方法执行任务更新。我们用@PUT注解这个方法,并使用两个 RESTful 参数。相对 URI 是/rest/caseworker/item/{caseId}/task/{taskId}。更新任务记录的代码如下:
@PUT
@Path("/item/{caseRecordId}/task/{taskId}")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public String updateTaskOnCase(
@PathParam("caseRecordId") int caseRecordId,
@PathParam("taskId") int taskId,
JsonObject taskObject ) throws Exception
{
final List<CaseRecord> caseRecords =
service.findCaseById(caseRecordId);
final CaseRecord caseRecord = caseRecords.get(0);
caseRecord.getTasks().stream().filter(
task -> task.getId().equals(taskId)).forEach(
task -> {
task.setName( taskObject.getString("name") );
task.setTargetDate(
taskObject.containsKey("targetDate") ?
CaseRecordHelper.convertToDate(
taskObject.getString("targetDate")) : null );
task.setCompleted(taskObject.containsKey("completed") ?
taskObject.getBoolean("completed") : false );
});
service.saveCaseRecord(caseRecord);
final StringWriter swriter = new StringWriter();
final JsonGenerator generator =
jsonGeneratorFactory.createGenerator(swriter);
CaseRecordHelper.writeCaseRecordAsJson(
generator, caseRecord).close();
return swriter.toString();
}
使用我们的两个坐标caseRecordId和TaskId,我们定位适当的Task实体,然后从 JSON 输入中更新属性。在这里,我们利用 Java 8 Lambdas 和 Stream API 进行函数式方法。我们保存实体,并从当前的CaseRecord实体渲染 JSON 响应。
删除任务记录
最后但同样重要的是,我们为客户端前端提供了一种从案例记录中删除任务记录的方法。代码如下:
@DELETE
@Path("/item/{caseRecordId}/task/{taskId}")
@Consumes( { APPLICATION_JSON, APPLICATION_XML, TEXT_PLAIN })
@Produces(APPLICATION_JSON)
public String removeTaskFromCase(
@PathParam("caseRecordId") int caseRecordId,
@PathParam("taskId") int taskId,
JsonObject taskObject )
throws Exception
{
final List<CaseRecord> caseRecords =
service.findCaseById(caseRecordId);
final CaseRecord caseRecord = caseRecords.get(0);
caseRecord.getTasks().stream().filter(
task -> task.getId().equals(taskId))
.forEach( task -> caseRecord.removeTask(task) );
service.saveCaseRecord(caseRecord);
final StringWriter swriter = new StringWriter();
final JsonGenerator generator =
jsonGeneratorFactory.createGenerator(swriter);
CaseRecordHelper.writeCaseRecordAsJson(generator, caseRecord).close();
return swriter.toString();
}
对于 HTTP DELETE 请求,我们用@DELETE注解deleteTaskFromCase()方法。这个方法的相对 URI 是严格 RESTful 服务端点/rest/caseworker/item/{caseId}/task/{taskId}。
在这个方法中,棘手的部分是搜索实际的Task记录。在这里,Java 8 Lambda 和流函数使这项任务变得非常全面且令人愉快。在正确识别了Task实体后,我们从父CaseRecord中删除它,然后使用持久性保存主记录。在消息的末尾,我们发送一个CaseRecord的 JSON 响应。
这涵盖了应用程序的 JAX-RS 方面;我们现在将转向 Java EE WebSocket 支持。
WebSocket 通信
WebSocket 是一种 HTML 协议扩展,允许客户端和服务器在网络中进行全双工异步通信。它通过在向后兼容的 HTTP 之前进行两个端点的初始握手来工作,然后切换到更快的 TCP/IP 流。WebSocket 规范(RFC 6455)是 HTML5 技术集合的一部分,由Web Hypertext Application Technology Working Group(WHATWG)(whatwg.org)和Internet Engineering Task Force(IETF)(www.ietf.org)推动。
WebSocket 支持自 Java EE 7 版本发布以来就已经可用,相关的 JSCP 规范是 JSR 356 (jcp.org/en/jsr/detail?id=356)。我们可以使用注解或直接针对 API 来开发 JavaEE WebSocket。使用注解编写更容易,正如我们将看到的。
AngularJS 客户端
在新任务记录控制器和应用程序主控制器周围再次审查 AngularJS 客户端是有帮助的。让我们检查CaseRecordController中的updateProjectTaskCompleted()方法。每当用户通过选择或取消选择 HTML checkbox元素来决定任务是否完成时,我们通过send()方法将前端连接起来,发送 WebSocket 消息:
$scope.updateProjectTaskCompleted = function( task ) {
var message = { 'caseRecordId': task.caseRecordId, 'taskId': task.id, 'completed': task.completed }
$scope.connect()
var jsonMessage = JSON.stringify(message)
$scope.send(jsonMessage)
}
整个本地的 JavaScript 任务记录都作为 JSON 发送。
为了在客户端的其它模块中提供 WebSocket 通信,AngularJS 建议我们定义一个工厂或服务。工厂通常只初始化一次。另一方面,服务添加功能,并返回不同的实例,这取决于调用上下文。
以下缺失的工厂:
myApp.factory('UpdateTaskStatusFactory', function( $log ) {
var service = {};
service.connect = function() {
if (service.ws) { return; }
var ws = new WebSocket("ws://localhost:8080/
xen-force-angularjs-1.0-SNAPSHOT/update-task-status");
ws.onopen = function() {
$log.log("WebSocket connect was opened"); };
ws.onclose = function() {
$log.log("WebSocket connection was closed"); }
ws.onerror = function() {
$log.log("WebSocket connection failure"); }
ws.onmessage = function(message) {
$log.log("message received ["+message+"]"); };
service.ws = ws;
}
service.send = function(message) {
service.ws.send(message);
}
return service;
});
定义 AngularJS 工厂的惯用语与定义控制器或模块非常相似。在主模块myApp中,我们使用两个参数调用库的factory()方法:工厂的名称和定义服务的函数回调。工厂只依赖于默认的日志模块$log。
这个connect()方法通过实例化 WebSocket 实例来初始化一个 HTML5 WebSocket。通过句柄,我们注册可选的回调来处理事件:当 WebSocket 打开、关闭、接收消息或发生错误时。每个回调都会将消息输出到 Web 浏览器的控制台日志。
我们定义了几个send()方法,这些方法将消息体内容发送到 WebSocket 的对方。在 WebSocket 术语中,远程端点被称为对方,因为客户端和服务器端点之间没有区别。双方都可以开始与对方建立连接并开始通信;因此,术语全双工。
服务器端 WebSocket 端点
如前所述,在 Java EE 7 中,我们可以快速使用标准注解开发 WebSocket 端点。Java WebSocket API 紧密遵循 IETF 规范,你会在浏览器内的许多 JavaScript 实现中认识到这些相似之处。配置和不同注解方法以及直接编程到库的方法太多,无法合理地压缩在这本数字 Java EE 书中。
尽管如此,关键的 Java 类实际上被注解为无状态会话 EJB 以及 WebSocket。这并不令人惊讶,因为 Java EE 规范允许在某些情况下混合使用这些注解。
以下是在CaseRecordUpdateTaskWebSocketEndpoint中的端点:
package uk.co.xenonique.nationalforce.control;
/* ... */
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint("/update-task-status")
@Stateless
public class CaseRecordUpdateTaskWebSocketEndpoint {
@Inject
CaseRecordTaskService service;
static JsonGeneratorFactory jsonGeneratorFactory =
Json.createGeneratorFactory(...);
@OnMessage
public String updateTaskStatus(String message) {
final StringReader stringReader = new StringReader(message);
final JsonReader reader = Json.createReader(stringReader);
final JsonObject obj = reader.readObject();
final int projectId = obj.getInt("caseRecordId");
final int taskId = obj.getInt("taskId");
final boolean completed = obj.getBoolean("completed");
final List<CaseRecord> projects =
service.findCaseById(projectId);
if ( !projects.isEmpty()) {
projects.get(0).getTasks().stream()
.filter(task -> task.getId() == taskId).
forEach(task -> {
task.setCompleted(completed);
service.saveCaseRecord(task.getCaseRecord());
});
return "OK";
}
return "NOT FOUND";
}
@OnOpen
public void open( Session session ) { ... }
@OnClose
public void close( Session session ) { ... }
@OnError
public void error( Session session, Throwable err ){
err.printStackTrace(System.err);
}
}
我们使用@ServerEndpoint注解 bean 来表示服务器端端点。服务器端的概念基本上是 Java EE 术语,用于声明这个端点位于应用程序服务器上。还有像@ClientEndpoint这样的连接。
而不是回调,Java EE 使用注解方法来处理 WebSocket 周围的开启、关闭和失败事件,分别使用@OnOpen、@OnClose和@OnError。
为了正确处理 WebSocket 上的消息接收,POJO 或 bean 必须只有一个方法被注解为@Message。在幕后,库框架将消息转换为一个字符串,以处理我们这里的最简单情况。可以发送二进制和复杂的数据类型通过线和网络。
在 updateTaskStatus() 方法内部,我们利用 JSON-P API 将文本解析为任务的显著属性。从输入文本消息中,我们需要案例记录 ID、任务 ID 和任务的完成属性。我们从持久化中检索匹配的 CaseRecord 实体,并过滤 Task 对象集合以找到正确项。一旦我们找到它,我们就设置完成属性,并将整个记录持久化回持久化存储。
WebSockets 允许向对等方返回一个响应。我们像这里一样同步发送响应,使用像OK或NOT FOUND这样的文本消息。读者应该知道,也可以异步发送响应。
我们已经讨论了服务器端的最后一点。
考虑你的设计需求
AngularJS 是一个强大的客户端 JavaScript MVC 框架。开发者、设计师和数字经理们总是期待着下一个令人兴奋的技术。人们常常倾向于评估新框架的影响。可以公平地说,AngularJS 是一个变革者,因为它使得与模型绑定的组件开发变得容易。默认情况下,不使用 jQuery 实现这种潜在的错误代码所付出的努力是巨大的!
我们知道 AngularJS 适合单页应用。这意味着你的下一个企业应用必须是一个 SPA 吗?嗯,实践顾问的回答是,这总是取决于你的目标。SPA 适合有限的客户旅程,以及在体验主要发生在同一个地方的情况下。案例工作人员应用就是这种类型,因为工作人员是逐个评估护照申请人的,因此,他们在工作日的大部分时间都待在一个网页上工作。
案例工作人员应用演示了主从关系。你的下一个企业应用可能需要更复杂的一组用例。你的领域可能需要广泛的实体集。一个单独的 SPA 可能无法涵盖所有领域。首先,你需要更多的 JavaScript 模块、控制器和工厂,以及 HTML 指令,以完全包围系统的边界上下文。
那对于这些复杂的需求我们该怎么办呢?一种方法是将所有 JavaScript 逻辑打包到客户端脚本中,然后下载。例如,GruntJS 这样的工具,我们在上一章中简要介绍过,可以将文件合并、压缩和优化。我们可以利用 Java EE 的优势,在多个网页和导航中。
单页应用数组集合
我们可以将 SPA 结构化为线性序列,以便系统的客户旅程几乎遵循工作流程。在仓库订单管理、工程和金融交易等领域,这种做法可能是有意义的。在这些领域,业务用户通过一系列复杂的步骤来处理从 A 到 B 的大量工作单元。如果 SPA 数组具有短线性序列,可能包括三到四个步骤,那么它们的优势就会被获得,但如果链的长度大于或等于七个,这些优势就会丧失。
单页应用程序的分层集合
另一种方法是完全从线性序列下降到 SPA 的分层树结构。这种方法非常专业,建议在寻求一些架构保证以确认这条路径对您的业务是可持续的。为什么企业会希望以这种方式组织 SPA?您的利益相关者可能希望以与领域完全一致的方式维持组织功能。设计方法是一种风险程序,因为它在整体模型中引入了僵化性,在我看来,这似乎是由管理层领导的权威权力,而不是有机的。如果层次树是按长度和宽度而不是按长度和深度组织的话,问问自己为什么?
在这些工程师和建筑师都在寻找微服务以实现扩展并优雅地将单一业务功能封装在单个盒子中的时代,HSPA 可能确实很有用。树结构的大小应该大约有 10 个节点。
摘要
在这一长章节中,我们介绍了使用案例工作者应用程序进行客户端 AngularJS 开发。我们学习了 AngularJS 框架如何操作 DOM,以及它是如何通过 MVC 提供数据与元素渲染之间的绑定。我们还学习了 AngularJS 的一些概念,如作用域、模块、控制器和工厂。
通过研究示例,我们说明了 AngularJS 如何从客户端使用 RESTful 服务调用与远程服务器进行通信。我们还简要研究了 WebSocket 交互。在 JavaScript 客户端方面,我们在案例工作者应用程序中走过了整个 CRUD 习语。
在服务器端,我们看到了使用 JAX-RS 实现的 RESTful 服务的实现,它涵盖了四个标准的 HTTP 方法请求。我们还学习了 Java WebSocket 的实现。
AngularJS 适合需要单页应用程序模式的应用。然而,这可能或可能不适合您的业务需求。采用 AngularJS 需要具备 JavaScript 编程和 Java EE 开发的全面知识。转向像 AngularJS 这样的框架会使您的业务面临招聘、保留和学习更多技术的风险。
还有一个三角形的另一面需要考虑:组织动态。苹果公司(美国)著名地将当时负责在线购物商店的敏捷团队分为纯服务器和客户端部门。他们之间唯一允许的通信是一个商定的编程接口。这种划分发生在 iPhone 开发期间(大约 2005-2007 年),显然早于 AngularJS。你的团队可能有所不同,但设计合同的概念仍然相关,因为它展示了可以实现什么,特别是在 RESTful 服务方面。
我将留给你们来自 AngularJS 共同创造者 Misko Hevery 的第二句引言。他说:
"有限 – 你实际上无法在单页面上向人类展示超过 2000 件信息。超过这个数量真的很糟糕的用户界面,而且人类无论如何都无法处理这些信息。"
练习
-
下载 xen-national-force 案件工作人员应用程序的源代码,并研究实现方法几个小时。你注意到了什么?编译代码并将生成的 WAR 部署到您选择的应用程序服务器上。
-
使用本章的材料,创建一个只有一个简单实体
EMPLOYEE的 CRUD AngularJS 应用程序。这个实体应该有一个员工 ID、姓名和社会安全号码。客户端使用 AngularJS 构建,服务器端使用 JAX-RS 构建。(在本书的源代码中,有一个空白项目可以帮助你开始。) -
在从上一个问题构建 AngularJS 和 JavaEE 中的 EMPLOYEE CRUD 时,你是否使用了 UI Bootstrap 的模态对话框?如果没有,调查其他渲染视图以插入、更新和删除记录的方法。(提示:一种可能的方式是动态显示和隐藏不同的
DIV元素。) -
案件工作人员应用程序存在一个明显的设计缺陷。你是否发现了?当案件工作人员显示或隐藏任务视图时,它会更新持久化数据库;解释为什么这是一个问题?
-
想象一下,从今天起,你已经成为整个 xen-national-force 团队的项目负责人,并且业务突然决定他们想要在状态改变后立即向其他案件工作人员广播通知。在技术层面上解释这个用户故事可能如何实现。考虑 AngularJS 客户端的挑战。你将如何构建 Java EE 服务器端?
-
研究一下 xen-national-force 中的工厂模块 (
iso-countries.js),它负责维护 ISO 护照国家名称及其代码的集合。这个模块在前端是如何使用的?它在哪些地方被使用? -
不要在
CaseRecordJPA 实体中使用专门的布尔属性来表示任务视图是否显示,而是编写一个 AngularJS 工厂模块,该模块在客户端本地存储所有案件记录的信息。 -
样本案件工作者应用程序检索数据库中的所有记录并将其返回给用户。假设真实系统有 1,000 个案件记录。这个功能可能存在什么问题?你会如何解决这个问题?如果案件工作者无法看到所有记录,请解释你将如何确保他们能看到相关案件?你需要在 AngularJS 客户端和 Java EE 服务器端实现哪些功能?
第九章. Java EE MVC 框架
| *"与电话或电视的开发相比,Web 发展得非常快。" | |
|---|---|
| --蒂姆·伯纳斯-李爵士,万维网的发明者 |
在过去几章中,我们从客户端的角度回顾了 Web 应用程序服务。在本章的最后,我们将回到主要在服务器端编写的数字应用程序。我们将检查 Java EE 下的一个全新的规范。它被称为模型-视图-控制器(MVC),属于 Java EE 8 版本(2017 年 3 月)下的 JSR 371(jcp.org/en/jsr/detail?id=371)。在撰写本书时,Java EE MVC 的早期草案版本已经发布,展示了参考实现 Ozark(ozark.java.net/index.html)的工作原理。
MVC 框架基于在 Smalltalk 编程语言和环境中所发明的设计模式,这在早期的用户界面应用程序中尤为常见。其理念是,模型指的是存储应用程序数据(如值对象或数据传输对象)的组件。视图是负责将应用程序数据的表示渲染或交付给用户的组件,控制器是包含处理前两个组件(视图和模型)之间输入和输出逻辑的组件。这种设计模式之所以非常流行,是因为它拥抱了关注点的分离,这是良好实用主义面向对象设计中的一个关键概念。
在 2014 年,甲骨文公司对更广泛的 Java EE 社区发布了一项公开的问卷调查,并收集了结果(java.net/downloads/javaee-spec/JavaEE8_Community_Survey_Results.pdf)。数千名技术人员对此调查做出了回应。调查中的一个关键问题是,“Java EE 是否应该提供对 MVC 和 JSF 的支持?”投票结果显示,60.8%的人支持,20%的人反对,19.2%的人不确定。这足以批准 MVC 1.0 规范成为 Java EE 8 的一部分。
Java EE 8 MVC
在我们继续编程之前,我应该提醒您,这里的信息可能会发生变化,因为 MVC 正在我们眼前不断发展。作为一名热心的读者,您应该至少验证 API 是否与当前或最终规范一致。
如此一来,MVC 框架无疑——即使在这个早期阶段——将成为未来许多年数字 Web 开发框架的领先规范,而不仅仅是因为它现在是 Java EE 伞下驱动系统的一部分。MVC 利用了 JAX-RS(Java for RESTful Services)API,并目前与其他 Java EE 技术集成,包括 CDI 和 Bean Validation。
专家小组决定在 JAX-RS 而不是较旧的 Java servlet API 之上分层,因为 JAX-RS 适合现代编程实践,以使用 HTTP 映射能力的完整语义。他们还认为采用 servlet 会将开发者暴露于 JAX-RS 规范中已经重复的低级编程接口。
从数字开发者和现代网络实践的角度来看,在 JAX-RS 之上分层 MVC 确实是一个很好的采用。Servlet 规范受到了像 Play 框架和其他人的严厉批评,因为它是一个宽而厚的上下文映射抽象(埃里克·埃文斯的领域驱动设计)并且是阻止网络和 HTTP 自然设计的障碍。
我们将使用 Java EE 8 MVC 参考实现 Ozark。在撰写本文时,Ozark 仍在开发中。然而,里程碑版本包含 MVC 应用程序的必要组件和接口。
MVC 控制器
在javax.mvc下为 MVC 保留了一个新的包结构。@javac.mvc.Controller注解声明一个类类型或方法作为控制器组件。以下是在方法中使用它的一个示例:
@Controller
public String greet()
{
return "Greetings, Earthling";
}
此控制器方法缺少 HTTP 语义,这正是 JAX-RS 注解帮助的地方。从 MVC 的角度来看,它也是无用的,因为与模型或视图组件有关联。
因此,首先让我们将方法转换为合适的 RESTful 资源,从模型对象开始:
package uk.co.xenonique.basic.mvc;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named(value="user")
@RequestScoped
public class User {
private String name;
public User() {}
public String getName() {return name; }
public void setName(String name) {this.name = name; }
}
用户组件充当我们的模型组件。它有一个属性:我们礼貌问候的人的名字。因此,我们可以编写一个 MVC 资源端点,并将此模型实例注入其中。
这里是我们控制器的初始版本:
package uk.co.xenonique.basic.mvc;
/* ... */
import javax.mvc.Controller;
import javax.mvc.Viewable;
import javax.ws.rs.*;
@Path("/hello")
@Stateless
public class AlienGreetings {
@Inject User user;
@GET
@Controller
@Path("simple1")
@Produces("text/html")
public String simple1( @QueryParam("name") String name )
{
user.setName(name);
return "/hello.jsp";
}
}
我们将使用 JAX RS 的@Path注解注解我们的AlienGreetings类,以声明它是一个资源端点。尽管我们的类型定义为 EJB 无状态会话豆,但 MVC 预计将与 CDI 作用域一起工作,如@ApplicationScoped和@SessionScoped。参考实现正在我编写时发生变化。
我们将使用 MVC 的@Controller注解来注解simple1()方法。此方法接受一个名为@QueryParam的参数。我们将添加其他 JAX-RS 注解,以定义 HTTP 方法协议@GET、相对 URI@Path和 MIME 内容类型@Produces。该方法在User实例中设置名称属性,并返回一个引用字符串,即视图的名称:/hello.jsp。
MVC 控制器方法可以返回一个字符串,这意味着 Servlet 容器接管了最终视图的渲染。然而,由于可扩展的实现,MVC 也可以渲染不同的视图。我们将在稍后看到更多关于这一点的内容。在幕后,MVC 将字符串转换为视图类型。Java 接口javax.mvc.Viewable代表视图技术的抽象。Viewable 是javax.mvc.ViewEngine和javax.mvc.Models之间的关联。
抽象类类型javax.mvc.Engine负责将模型渲染到技术选择。工程师可以开发或添加此引擎以渲染视图。目前,Ozark 支持从 Apache Velocity 到 AsciiDoc 的许多渲染样式。
Java 接口javax.mvc.Models代表一个从视图传递到渲染引擎的关键值存储。模型类型是请求作用域的映射集合。
因此,让我们扩展AlienGreeting控制器并添加一些更多的方法。下面的simple2()方法返回一个Viewable实例:
@GET
@Controller
@Path("simple2")
@Produces("text/html")
public Viewable simple2( @QueryParam("name") String name )
{
user.setName(name);
return new Viewable("/hello.jsp");
}
如您所见,simple2()方法是对simple1()的变体,MVC 相当灵活。它甚至支持不返回类型的 void 方法。我们将使用@javax.mvc.View注解后续的simple3()方法,以声明下一个视图:
@GET
@Controller
@Path("simple3")
@Produces("text/html")
@View("/hello.jsp")
public void simple3( @QueryParam("name") String name )
{
user.setName(name);
}
到目前为止,所有三个方法都是 HTTP GET 请求。由于 MVC 建立在 JAX-RS 之上,我们还可以利用其他协议方法。编写处理 HTTP POST 请求的 HTML 表单处理程序很简单。
这里是helloWebForm()方法的代码片段:
@POST
@Controller
@Path("webform")
@Produces("text/html")
public Viewable helloWebForm( @FormParam("name") String name )
{
user.setName(name);
return new Viewable("/hello.jsp");
}
前面的控制器方法helloWebForm()接受一个名为参数的 HTML 表单。它设置模型对象并返回一个视图实例。在 HTML5 标准中,表单元素正式支持 HTTP GET 和 POST 请求。流行的网络浏览器通常只能通过 JavaScript 访问 HTTP PUT 和 DELETE 协议请求。然而,这种限制并不会阻止 MVC 控制器被注解为@PUT或@DELETE。
MVC 控制器可以访问 JAX-RS 端点可用的完整 URI 空间。以下示例说明了路径参数:
@GET
@Controller
@Path("view/{name}")
@Produces("text/html")
public Viewable helloWebPath( @PathParam("name") String name )
{
user.setName(name);
return new Viewable("/hello.jsp");
}
前面的控制器方法helloWebPath()接受一个路径参数作为用户的名称。@PathParam注解建立了在相对 URI 中使用的参数标记。URI 由@Path注解定义。完整的 URL 将是http://localhost:8080/javaee-basic-mvc/rest/hello/view/Curtis。
MVC 页面视图和模板
根据 MVC 规范,一个view实例可以被视为模板技术参考。到目前为止,我们只看到了 JavaServer Page 视图。只要有一个相应的ViewEngine实例知道如何从关联的模型和其他控制器结果中处理(渲染)视图,视图可以是开发者能想到的任何东西。
让我们看看这本书(ch09/basic-javaee-mvc)示例源代码项目中的第一个基本 JSP 视图,index.jsp:
<%@ page import="uk.co.xenonique.basic.mvc.AlienGreetings" %>
<!DOCTYPE html>
<html>
<head> ...
<link href="${pageContext.request.contextPath}/styles/bootstrap.css" rel="stylesheet">
<link href="${pageContext.request.contextPath}/styles/main.css" rel="stylesheet">
</head>
<body>
<div class="main-content"> ...
<div class="other-content">
<h2>Simple MVC Controller</h2>
<p>
HTTP GET Request with query parameter and invocation of
<code><%= AlienGreetings.class.getSimpleName() %>
</code> with HTML Form:<br>
<a href="${pageContext.request.contextPath}
/rest/hello/simple1?name=James"
class="btn btn-primary"> Person called James
</a>
</p>
<p>...
<a href="${pageContext.request.contextPath}
/rest/hello/simple2?name=Patricia"
class="btn btn-primary"> Person called Patricia
</a>
</p>
<p>...
<a href="${pageContext.request.contextPath}
/rest/hello/simple3?name=Aliiyah"
class="btn btn-primary"> Person called Aliiyah
</a>
</p>
<p>
<form action="${pageContext.request.contextPath}
/rest/hello/webform"method="POST" >
<div class="form-group">
<label for="elementName">Name</label>
<input type="text" class="form-control"
id="elementName" name="name" placeholder="Jane Doe">
</div>
</form>
</p>
...
</div>
</body>
</html>
在这个 JSP 视图中,我们将利用表达式语言(EL)生成带有 Web 应用程序上下文路径的 URL,即 pageContext.request.contextPath。有三个 HTML 锚元素调用 AlienGreeting 控制器方法:simple1()、simple2() 和 simple3()。HTML 表单调用 helloWebForm() 方法。
被称为 hello.jsp 的页面提取 JSP 视图如下所示:
<div class="main-content">
<div class="page-header">
<h1> Java EE 8 MVC - Hello</h1>
<p> Model View Controller </p>
</div>
<div class="other-content">
<div class="jumbotron">
<p> Hello ${user.name} </p>
</div>
<p> How are you?</p>
</div>
</div>
视图模板非常简单;我们将使用请求作用域的用户实例来提供名称。
在撰写本文时,Ozark 支持以下视图呈现技术,如以下表格所示:
| 模板名称 | 描述 |
|---|---|
| AsciiDoc | 这是一个用于编写笔记、文档、文章、书籍、电子书、幻灯片、手册页和博客的文本文档格式。github.com/asciidoctor/asciidoctorj |
| Freemarker | 这是一个 Java 模板引擎,可以生成 HTML、RTF 和源代码。freemarker.org/ |
| Handlebars | 这是一个针对原始 Mustache 模板规范的多种语言和平台扩展,包含有用的实用工具标记。JavaScript 版本:handlebarsjs.com/ 和 Java 版本:github.com/jknack/handlebars.java/ |
| JSR 223 | 这是 MVC 框架的一个扩展,支持 JSR 223 动态脚本语言,如 Groovy 和 Jython。www.jcp.org/en/jsr/detail?id=223 |
| Mustache | 这是一个简单的网络模板语言,将表示与业务视图逻辑分离。它在多个平台和语言上可用。github.com/spullara/mustache.java |
| Thymeleaf | 这是一个基于 Java 的 HTML5 模板库,适用于 Web 和非 Web 环境。它与字符串框架紧密相关。www.thymeleaf.org/ |
| Velocity | Apache Velocity 是一套模板工具集。Velocity 引擎是提供模板功能的组件库。它是为服务器端 Java 编写的第一个网络模板框架之一。velocity.apache.org/ |
小贴士
MVC 是一个服务器端模板解决方案。不要将客户端模板的世界与后端版本混淆。
MVC 模型
MVC 规范支持两种模型形式。第一种是基于javax.mvc.Models实例,第二种形式利用 CDI 的@Named beans。Models接口类型将键名映射到映射集合中的值。所有视图引擎都必须强制支持Models。视图引擎可以可选地支持 CDI。规范建议视图引擎实现者提供 CDI beans 支持。
Models接口的默认实现是一个请求作用域的 bean:com.oracle.ozark.core.ModelsImpl。此类委托给java.util.Map集合。开发者通常永远不会实例化此类,而是更喜欢注入Models类型。正如你稍后将会看到的,有时需要为特定的视图引擎创建一个实例。
让我们在控制器类的第二个版本中演示Models接口的实际应用:
@Path("/hello2")
@Stateless
public class PhobosGreetings {
@Inject
Models models;
@GET
@Controller
@Path("simple2")
@Produces("text/html")
public Viewable simple1( @QueryParam("name") String name )
{
models.put("users", new User(name));
return new Viewable("/hello2.jsp");
}
}
在PhobosGreetings中,我们将用Models实例替换User类型的 CDI 注入。我们将创建一个User实例,并将其存储在属性键user下。方法返回后,框架将检索Models实例中的所有属性并将它们放置在HttpServletResponse的属性集合中。因此,JSP 模板视图可以通过 EL 或内联脚本访问数据。
响应和重定向
MVC 还支持返回 JAX RS 响应类型实例的控制器。这对于将渲染工作推到客户端的网站特别有用。一个高度可扩展的 Web 应用程序可能会选择发送兆字节的 JSON 响应,而不是通过模板在服务器上渲染。
现在我们将检查另一个名为RedirectController的 MVC 控制器:
@Path("redirect")
@Stateless
@Controller
public class RedirectController {
@GET
@Path("string")
public String getString() {
return "redirect:redirect/here";
}
@GET
@Path("response1")
public Response getResponse1() {
return Response.seeOther(
URI.create("redirect/here")).build();
}
@GET
@Path("deliberateError1")
public Response getErrorResponse1() {
return Response.status(Response.Status.BAD_REQUEST)
.entity("/error.jsp").build();
}
@GET
@Path("here")
@Produces("text/html")
public String getRedirectedView() {
return "redirected.jsp";
}
}
我们将使用 JAX-RS 的@Path注解RedirectController,我们必须特别注意基本值redirect。
getString()方法使用特殊的重定向操作符redirect执行 URI 重定向。此方法具有唯一的相对 URI redirect/string。MVC 内部检测到前缀并构建一个 JAX-RS 响应作为重定向请求。返回的 URI 是getRedirectedView()控制器的引用,其相对路径 URI 为redirect/here。
我们可以直接构建响应,就像在getResponse()方法中看到的那样。使用 URI 调用静态Response.seeOther()方法等同于实现 HTTP 重定向响应。此方法有其独特的相对 URI redirect/response1。
getRedirectedView()方法简单地导航到视图模板redirected.jsp。此控制器方法是其他控制器方法(getString()和getResponse())的最终目标。
小贴士
在撰写本文时,MVC 规范正在设计 HTTP 重定向方法。有一个问题关于为 MVC 应用提供 Flash 作用域或 JSF 视图作用域豆的等效物,以便在多个请求作用域之间保存数据。我强烈建议你检查规范更新。
最后,MVC 控制器还可以返回 HTTP 错误响应。getErrorResponse()方法有一个相对 URI redirect/deliberateError1,并返回一个带有 HTTP 错误请求错误代码(401)的 Response 实例。控制器还通知 MVC 框架在视图 ID error.jsp下提供视图模板。RedirectController代表了 MVC 控制器最简单的形式。我们可以通过注入Models或其他 CDI 豆来丰富它。我们还可以在响应构建器中将可视化实例用作实体,而不是使用愚蠢的字符串。
让我们继续介绍不同的模板技术,这也是 MVC 到目前为止的独特卖点。
重新配置视图根
在工作的 MVC 早期草案版本中,视图根默认设置为WEB-INF/views。在程序上,这可以在ViewEngine.DEFAULT_VIEW_ENGINE的静态属性中找到。
这可能对具有相对 URI 的数字工程师来说不方便,尤其是在页面视图的重定向中。幸运的是,Ozark 实现可以从 web XML 描述符(web.xml)中进行重新配置,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<servlet>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<init-param>
<param-name>javax.mvc.engine.ViewEngine.viewFolder</param-name>
<param-value>/</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>...
</web-app>
我们将覆盖javax.mvc.engine.ViewEngine.viewFolder属性,在网页根目录中实现我们想要的行为。
Handlebars Java
Handlebars 框架是一个用于开发 Web 应用的模板库。在其 JavaScript 版本(handlebarsjs.com/)中,你可能已经听到了数字界面开发者对其赞不绝口。在本章的其余部分,我们将使用 Handlebars Java 版本。
Handlebars 框架允许开发者和设计师编写语义模板。它基于一个稍微老旧一些的模板框架,称为 Mustache(mustache.github.io/)。这两个都指的是如果你眯起眼睛看一个旋转 90 度的花括号,可以看到的男性面部毛发。
模板框架强调关注点的分离,尽可能减少在渲染的页面视图中混合业务逻辑。Handlebars 框架从 Mustache 借用了双大括号符号。对于我们的男性读者来说,这是一种故意的文字游戏和计算机编程的怪癖。Handlebars 框架在利用服务器端和客户端相同的模板引擎方面具有明显优势。
一个编译内联模板 servlet
我们将从 Servlet 示例开始介绍。我们将创建一个只有一个依赖项的 Java Servlet,这个依赖项是 Handlebars Java 实现。我们目前不考虑 MVC 框架。我们的 Servlet 将实例化模板框架,调用内联模板脚本,然后直接返回输出。
这里是我们的 Servlet:
package uk.co.xenonique.basic.mvc;
import com.github.jknack.handlebars.*;
import javax.servlet.*;
/* ... other imports omitted */
@WebServlet("/compiled")
public class CompileInlineServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
final Handlebars handlebars = new Handlebars();
final Template template = handlebars.compileInline(
"Hello {{this}}!");
final String text = template.apply("Handlebars.java");
response.setContentType("text/plain");
response.getOutputStream().println(text);
}
}
CompileInlineServlet 类展示了如何在 Java 代码中编译一个基本的 Handlers 模板示例。我们将实例化一个 Handlebars 引擎模板,然后按以下文本所示内联编译模板:
Hello {{this}}!
编译后,我们将获得一个包含内容的 Template 实例。然后我们将调用 apply() 方法,并使用一个简单的字符串作为模板的上下文。{{this}} 占位符指的是上下文对象。然后我们将检索文本表示并回送给网络浏览器。我们将看到以下纯文本输出:
Hello Handlebars.java!
在 Handlebars Java 中没有依赖项将框架绑定到 Java EE。因此,它也可以在 Java SE 中作为一个独立的 Java 可执行文件使用。
Handlebars 中的模板表达式
这里是另一个具有不同上下文的 Handlebars 模板示例:
<div class="trade-entry">
<h1>{{title}}</h1>
<div class="trade-detail">
{{detail}}
</div>
</div>
模板在 DIV 层中渲染,带有嵌入表达式的占位符名称替换。一个表达式在双大括号之间保留和激活。它为交易系统的域渲染输出。在 Handlebars 和 Mustache 语法中,围绕变量名的双大括号表示一个占位符条目。模板中的占位符条目意味着它可以被动态内容替换。因此,在模板渲染过程中,{{title}} 占位符被替换为交易条目的标题,而 {{detail}} 被替换为交易详情。表达式值可以是字面量字符串,也可以是嵌入的 HTML5 标记。
让我们再写一个 Java Servlet,使用 Handlebars 框架渲染这个视图:
package uk.co.xenonique.basic.mvc;
import com.github.jknack.handlebars.*;
import com.github.jknack.handlebars.io.*;
/* ... imports omitted */
@WebServlet("/tradeinfo")
public class TradeInfoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
final TemplateLoader loader =
new ServletContextTemplateLoader(
request.getServletContext());
final Handlebars handlebars = new Handlebars(loader);
final Template template = handlebars.compile("trade");
final Map<String,String> context =
new HashMap<String,String>() {{
put("title", "12345FUND-EURO-FUTURE6789");
put("detail", "Nominal 1.950250M EUR");
put("trade-date", "23-07-2015");
put("maturity-date", "23-07-2018");
}};
final String text = template.apply(context);
response.setContentType("text/html");
response.getOutputStream().println(text);
}
}
这个 TradeInfoServlet Servlet 几乎与之前的 CompileInlineServlet 相同;但这次,我们将利用 ServletContextTemplateLoader 类。这是一个从 Java EE 网络上下文中通过 Servlet 引擎检索视图模板的加载器类。我们将创建加载器,并在构建 Handlebar 实例时将其作为参数传递。然后,我们将使用参考名称 trade 编译一个模板。框架调用加载器检索视图模板,默认情况下模板后缀为 trade.hbs。
我们构建了一个键和值的字面量映射集合。它们将作为我们的视图中的占位符集合,并将它们应用到模板中。通过 http://localhost:8080/handlebars-javaee-mvc-1.0-SNAPSHOT/tradeinfo 访问网络浏览器应该显示以下截图:

TradeInfoServlet 的截图
除了直接的替换之外,Handlebars 支持使用 Block、Partials 和 Helper 表达式。Handlebars 框架提供了标准的构建块;然而,工程师可以注册自定义助手。在 JavaScript 实现中,开发者通常会在页面中定义编译后的脚本模板以便重用内容。Handlebars.js 通常与 RequireJS 或 EmberJS 等一个或多个 JavaScript 框架一起使用,以便提供可重用的内容。
我们将继续使用 Handlebars Java 编写一个 CRUD 示例。我们的应用程序将允许用户操作一个基本的产品目录。我们需要一个欢迎页面,所以让我们使用模板框架来创建它。
欢迎控制器
在 Java 版本中,我们不需要注册或编译模板,因为页面内容是从服务器端提供的。这个项目被称为 handlebars-javaee-mvc。首先要做的是在一个初始的 JSP 中引发一个 HTTP 重定向,如下所示:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<meta http-equiv="refresh" content="0;
url=${pageContext.request.contextPath}/rest/welcome" />
</body>
</html>
上述代码将立即将网络浏览器重定向到发送一个 HTTP GET 请求到 URI,handler-javeee-mvc-1.0/rest/welcome。在这个 URI 路径中,我们已经有一个 MVC 控制器等待这个调用:
@Path("/welcome")
@Stateless
public class WelcomeController {
@Inject Models models;
@GET
@Controller
@Produces("text/html")
public Viewable welcome()
{
models.put("pageTitle", "Handlebars.java Java EE 8 MVC" );
models.put("title", "Welcome");
return new Viewable("/welcome.hbs");
}
}
在模型实例中填充了几个属性之后,WelcomeController 将响应推进到 Handlebars Java 模板 /welcomee.hbs。带有正斜杠字符的绝对 URI 确保 MVC 框架从 Web 上下文根目录中搜索页面模板。后缀扩展 *.hbs 通常在客户端和服务器版本中都保留用于 Handlebars 视图模板。
让我们看看以下 Handlebars 模板 (welcome.hbs):
<!DOCTYPE html>
<html>
{{> header }}
<body>
{{> navbar }}
<div class="main-content">
...
</div>
{{> footer}}
</body>
{{> bottom }}
</html>
Handlebars 支持部分的概念。部分是用于另一个模板的模板。它们对于页面组成非常有用。语法以 {{> NAME }} 开始,其中 NAME 指的是另一个模板。在 JavaScript 堆栈中,部分必须在之前注册;然而,Java 版本知道如何通过从 servlet 容器中加载它们来找到部分模板。因此,部分模板引用 {{> header }} 指示 Handlebars 加载 header.hbs 视图。
在 welcome.hbs 视图中,有四个部分模板:header.hbs、navbar.hbs、footer.hbs 和 bottom.hbs。您可以在本书的代码分发中查看这些模板的源代码。
自定义视图引擎
Handlebars Java 随带几个模板视图引擎。不幸的是,默认扩展视图引擎并没有提供我们希望使用的所有功能。例如,Handlebars 并不直接渲染十进制数字,因此我们必须注册自己的函数。幸运的是,使用 CDI 编写视图引擎扩展是合理的,如下所示:
package uk.co.xenonique.basic.mvc;
import com.github.jknack.handlebars.*;
import com.github.jknack.handlebars.io.*;
import com.oracle.ozark.engine.ViewEngineBase;
// ... other imports ommitted
@ApplicationScoped
public class MyHandlebarsViewEngine extends ViewEngineBase {
@Inject private ServletContext servletContext;
@Override
public boolean supports(String view) {
return view.endsWith(".hbs") || view.endsWith(".handlebars");
}
@Override
public void processView(ViewEngineContext context) throws ViewEngineException {
final Models models = context.getModels();
final String viewName = context.getView();
try (PrintWriter writer = context.getResponse().getWriter();
InputStream resourceAsStream =
servletContext.getResourceAsStream(resolveView(context));
InputStreamReader in =
new InputStreamReader(resourceAsStream, "UTF-8");
BufferedReader buffer = new BufferedReader(in);) {
final String viewContent = buffer.lines()
.collect(Collectors.joining());
final TemplateLoader loader =
new ServletContextTemplateLoader(servletContext);
final Handlebars handlebars = new Handlebars(loader);
models.put("webContextPath",
context.getRequest().getContextPath());
models.put("page", context.getRequest().getRequestURI());
models.put("viewName", viewName);
models.put("request", context.getRequest());
models.put("response", context.getResponse());
handlebars.registerHelper("formatDecimal", new Helper<BigDecimal>() {
@Override
public CharSequence apply(BigDecimal number, Options options) throws IOException {
final DecimalFormat formatter =
new DecimalFormat("###0.##");
return formatter.format(number.doubleValue());
}
});
Template template = handlebars.compileInline(viewContent);
template.apply(models, writer);
} catch (IOException e) {
throw new ViewEngineException(e);
}
}
}
我们将 MyHandlebarsViewEngine 类注释为应用程序范围的 bean,并确保它从 Ozark 继承 ViewEngineBase。我们将向这个类注入 ServletContext,因为我们需要从中检索某些属性,例如网络上下文路径。
我们将重写 supports() 方法以建立对 Handlebars 文件的支持。MVC 视图类型作为单个参数传递。
真正的工作发生在 processView() 方法中,我们完全负责渲染 Handlebars 视图模板。MVC 框架提供了 javax.mvc.engine.ViewEngineContext,它提供了对当前 View 和 Models 实例的访问。我们可以确定需要检索的视图模板的名称。从现在开始,我们可以创建 ServletContextTemplateLoader 和 Handlebars 实例来加载视图,就像我们在早期的 TradeInfoServlet 类中看到的那样。然后,我们需要通过在缓冲区中读取当前视图的内容来稍微处理一些棘手的问题。ViewEngineBase 提供了一个 resolve() 方法,它极大地帮助我们并返回 InputStream。顺便说一句,Java 7 的 acquire/resource 语法简化了 try-catch 语句的样板代码。在方法结束时,由于我们在缓冲区中有内容,我们可以直接编译视图。
我们将在 MyHandlebarsViewEngine 中添加一些有用的功能。首先,我们将向 Models 实例添加额外的属性。我们将添加网络上下文路径以及请求和响应对象到 Models 实例中。其次,我们将注册一个 Handlebars 辅助函数,以便更好地渲染 BigDecimal 类型。
当我们的应用程序部署时,Ozark 依赖于 CDI 来查找 ViewEngineBase 类型。Ozark 扫描 JAR 文件和类的类路径以查找 ViewEengineBase 对象的类型。它构建一个可用的渲染内部列表。MyHandlebarsViewEngine 是目前可以在渲染阶段添加额外辅助函数和实用工具的地方。请关注 MVC 规范,看看是否有这些接口以有意义的公共可访问 API 的形式暴露出来。
我们将转到我们的控制器和产品列表视图。
产品控制器
我们的领域对象是谦逊的 Product 实体,其结构类似于以下形式:
@Entity
@NamedQueries({
@NamedQuery(name="Product.findAll",
query = "select p from Product p order by p.name"),
@NamedQuery(name="Product.findById",
query = "select p from Product p where p.id = :id"),
})
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@NotEmpty @Size(max=64) private String name;
@NotEmpty @Size(max=256) private String description;
@NotNull @DecimalMin("0.010") private BigDecimal price;
public Product() { this(null, null, null); }
public Product(String name, String description, BigDecimal price) {
this.name = name;
this.description = description;
this.price = price;
}
/* ... */
}
注意,Product 充分利用了 Bean Validation 注解。特别是,它使用 BigDecimal 类型来精确价格,而 @DecimalMin 注解防止在边界上下文中存储负数和零价格。
给定我们的实体对象,我们需要 ProductController 来将领域与表示视图接口:
@Path("/products")
@Stateless
public class ProductController {
@Inject Models models;
@Inject ValidatorFactory validatorFactory;
@Inject FormErrorMessage formError;
@EJB ProductService productService;
private void defineCommonModelProperties(String title ) {
models.put("pageTitle", "Handlebars.java Java EE 8 MVC" );
models.put("title", title);
}
private void retrieveAll() {
final List<Product> products = productService.findAll();
models.put("products", products );
models.put("title", "Products");
}
@GET
@Controller
@Path("list")
@Produces("text/html")
public Viewable listProducts()
{
retrieveAll();
defineCommonModelProperties("Products");
return new Viewable("/products.hbs");
}
/* ... */
}
如同往常,我们将使用 JAX-RS 注解注释 ProductController 以引入 RESTful 服务空间。我们将使用 Bean Validation 验证输入对象的状态,因此我们将从 Java EE 容器注入 ValidatorFactory。我们将注入 Models 实例以进行 MVC 操作。我们还有一个自定义的 FormErrorBean POJO 用于捕获错误消息,最后还有一个 EJB,ProductService,用于将记录持久化到数据库中。
listProducts() 控制器方法委托给共享方法:retrieveAll() 和 defineCommonModelProperties()。retrieveAll() 方法使用 EJB 从数据库中检索所有产品。它将列表集合保存在 Models 下的一个已知属性键中。defineCommonModelProperties() 方法将标题作为一个键保存在同一个 Models 实例中。结果证明,许多控制器方法需要相同的功能,因此我们进行了重构。我们将检索到的产品集合放置在名为 products 的 Models 键属性中。最后,listProducts() 将其转发到 Handlebars Java 视图模板 product.hbs。在我们的控制器方法返回后,Ozark 最终将委托给我们的自定义视图引擎:MyHandlebarsViewEngine。
块表达式
我们可以查看视图并了解有用的块表达式。以下是从视图模板 /products.hbs 的摘录:
<div class="other-content">
<div class="jumbotron">
<p> {{title}} </p>
</div>
<p>
<a href="{{webContextPath}}/rest/products/preview-create"
class="btn-primary btn">Add New Product</a>
</p>
<table class="product-container-table table table-bordered table-responsive table-striped">
<tr>
<th> Product Name </th>
<th> Description </th>
<th> Unit Price </th>
<th> Action </th>
</tr>
{{#each products}}
<tr>
<td> {{this.name}} </td>
<td> {{this.description}} </td>
<td> {{formatDecimal this.price}} </td>
<td>
<a class="btn" href="{{webContextPath}}/rest/products/view/{{this.id}}" ><i class="glyphicon glyphicon-edit"></i></a>
<a class="btn" href="{{webContextPath}}/rest/products/preview-delete/{{this.id}}" ><i class="glyphicon glyphicon-trash"></i></a>
</td>
</tr>
{{/each}}
</table>
</div>
如您所见,我们实际上在这个视图模板中使用了 {{title}} 表达式,该表达式是在 defineCommonModelProperties() 方法中设置的。{{webContextPath}} 占位符的值是在我们的扩展类 MyHandlebarsViewEngine 中设置的。
有两个新的表达式:{{#each}} 和 {{/each}}。这些是内置的块表达式,允许我们遍历上下文元素。在这种情况下,我们将遍历产品。循环核心的产品元素可以在 {{this}} 占位符下访问。
为了正确打印 BigDecimal 价格,我们将调用在自定义视图引擎中定义的 {{formatDecimal}} 辅助函数。辅助函数可以接受多个参数。结果是渲染的产品表,包括它们的名称、描述和价格,以及编辑或删除项目的锚点链接。
这里是视图模板 products.jsp 的截图:

由 Handlebars 渲染的产品列表视图模板
检索和编辑操作
一旦客户选择了一个产品进行编辑,我们就需要从数据库中检索数据并将它们推送到不同的视图模板。这就是控制器中预览编辑方法的目的,如下所示:
@GET
@Controller
@Path("view/{id}")
@Produces("text/html")
public Viewable retrieveProduct( @PathParam("id") int id )
{
final List<Product> products = productService.findById(id);
models.put("product", products.get(0) );
defineCommonModelProperties("Product");
return new Viewable("/edit-product.hbs");
}
我们将使用@PathParam注释这个控制器方法retrieveProduct(),以便检索产品 ID。有了标识符,我们只需查找Product实体并将结果放入请求作用域的Models属性中。显然,对于生产应用程序,我们可能会非常谨慎地检查标识符是否有效。方法将响应的最终交付推进到视图模板/edit-products.hbs。视图模板的源代码包含在这本书的源代码分发中。
当用户在这个页面视图模板上提交 HTML 表单时,我们将继续到下一部分。如果客户提交表单,那么他们的旅程将调用ProductController中的下一个控制器方法,该方法称为editProduct():
@POST
@Controller
@Path("edit/{id}")
@Produces("text/html")
public Response editProduct(
@PathParam("id") int id,
@FormParam("action") String action,
@FormParam("name") String name,
@FormParam("description") String description,
@FormParam("price") BigDecimal price)
{
defineCommonModelProperties("Edit Product");
if ("Save".equalsIgnoreCase(action)) {
final List<Product> products =
productService.findById(id);
final Product product2 = new Product(
name, description, price );
final Set<ConstraintViolation<Product>> set =
validatorFactory.getValidator().validate(product2);
if (!set.isEmpty()) {
final ConstraintViolation<?> cv = set.iterator().next();
final String property = cv.getPropertyPath().toString();
formError.setProperty(
property.substring(property.lastIndexOf('.') + 1));
formError.setValue(cv.getInvalidValue());
formError.setMessage(cv.getMessage());
models.put("formError",formError);
return Response.status(BAD_REQUEST).
entity("error.hbs").build();
}
final Product product = products.get(0);
product.setName(name);
product.setDescription(description);
product.setPrice(price);
productService.saveProduct(product);
models.put("product", product);
}
retrieveAll();
return Response.status(OK).entity("/products.hbs").build();
}
我们的控制方法在 HTML 表单提交时被调用,因此我们将使用必要的@PathParam和@FormParam声明来注释它,以便接收产品 ID 和属性。我们的控制器期望一个带有name和action的表单参数。客户可以取消操作,如果这样做,则操作不匹配Save操作。因此,不会发生任何事情,方法将响应推进到产品列表视图。
在Save操作中,我们的方法将利用注入的ValidatorFactory实例来手动验证表单参数。因为实体产品有验证注解,所以我们将使用表单参数构建一个临时实例,然后创建一个验证器来检查它。在这个阶段,我们不会直接更改持久化实体,因为数据可能无效。如果是这种情况,那么 Java EE 容器将在控制器方法退出后引发javax.persistence.RollbackException,因为控制线程通过了事务屏障。在验证临时实例后,将返回一个ConstraintViolation元素的集合。
假设表单数据无效。我们将从第一条违规信息中检索信息,并在请求作用域的FormErrorMessage bean 中填充详细信息。表单错误可以通过视图模板中的属性键formError访问。然后,控制器方法构建一个带有 HTTP Bad Request 错误代码的响应,并将其转发到一个视图模板/error.hbs。
另一方面,如果表单根据 Bean Validation 检查有效,那么我们将通过产品 ID 从数据库中检索Product实体。我们将从表单属性更新Product实体,然后将其保存到数据库中。因为我们已经手动检查了数据,所以在保存数据时不应该出现错误。我们将构建一个 HTTP OK 响应并将其转发到视图模板/products.hbs。
提示
在撰写本文时,MVC 规范领导仍在开发 MVC 早期草案发布后的验证和参数绑定。值得注意的是,如果控制器注入了javax.mvc.BindingResult实例,那么就可以在精确和狭窄的用户故事中处理表单验证,而不是像直接 JAX-RS API 那样全局处理。
为了完整地展示,以下是FormErrorMessagebean 的紧凑版本:
@Named("error")
@RequestScoped
public class FormErrorMessage {
private String property;
private Object value;
private String message;
/* ... getters and setters omitted */
}
这是一个请求作用域的 bean,我们将使用 Handlebars 视图模板error.hbs来显示信息:
<div class="page-header">
<h1> {{pageTitle}} </h1>
</div>
<div class="other-content"> ...
<div class="main-content" >
<table class="table table-stripped table-bordered table-responsive">
<tr>
<th>Name</th><th>Value</th>
</tr>
<tr>
<td>Property</td> <td>{{formError.property}}</td>
</tr>
<tr>
<td>Message</td> <td>{{formError.message}}</td>
</tr>
<tr>
<td>Value</td> <td>{{formError.value}}</td>
</tr>
</table>
</div> ...
</div>
这个视图在FormErrorMessagebean 中显示错误消息。你可能想知道为什么我们将表单验证错误发送到单独的视图。答案很简单:逐步进行。在一个专业的数字应用中,我们会在客户端利用 AJAX 验证和 JavaScript 框架,如 jQuery。我们的客户端 JavaScript 模块将调用一个 HTTP POST 请求到 MVC 控制器方法以验证属性信息。这个方法,比如说validateCheck(),会在一个临时实例上检查,并通过包含约束违规的 JSON 响应报告。也许 JSR-371 专家组的成员会简化 Digital 的开发部分。
JAX-RS 全局验证
editProduct()方法的问题在于我们被迫使用手动验证步骤。目前唯一的替代方案是回退到 JAX-RS 验证。
那么,让我们检查一下控制器方法的新版本,称为altEditProduct():
@POST
@Controller
@Path("alt-edit/{id}")
@Produces("text/html")
public Response altEditProduct(
@PathParam("id") int id,
@FormParam("action") String action,
@NotNull @NotEmpty @FormParam("name") String name,
@NotNull @NotEmpty @FormParam("description") String description,
@NotNull @DecimalMin("0.0") @FormParam("price") BigDecimal price )
{
defineCommonModelProperties("Edit Product");
if ("Save".equalsIgnoreCase(action)) {
final List<Product> products = productService.findById(id);
final Product product = products.get(0);
product.setName(name);
product.setDescription(description);
product.setPrice(price);
productService.saveProduct(product);
models.put("product", product);
}
retrieveAll();
return Response.status(OK).entity("/products.hbs").build();
}
这次,我们将直接在控制器方法altEditProduct()上编写 Bean Validation 注解。你可能担心这会造成重复,因为注解已经在实体 bean 上存在。你的担忧是正确的,但让我们继续。altEditMethod()方法更短,这很好。现在,这种方法的缺点在于验证被全局委托给了 JAX-RS。如果客户向altEditMethod()提交 HTML 表单,那么他们将会收到一个 HTTP Bad Request 错误响应和一个直接从应用服务器发出的用户不友好的错误消息。显然,用户体验团队会非常不满!我们该怎么办?
JAX-RS 规范允许应用程序提供一个错误响应的处理程序。实现这一目标的方法是通过 CDI 配置一个提供者,如下所示:
package uk.co.xenonique.basic.mvc;
import javax.annotation.Priority;
import javax.mvc.*;
import javax.ws.rs.*;
/* ... other import omitted ... */
import static javax.ws.rs.core.Response.Status.*;
@Provider
@Priority(Priorities.USER)
public class ConstraintViolationExceptionMapper
implements ExceptionMapper<ConstraintViolationException> {
@Context HttpServletRequest request;
@Override
public Response toResponse(
final ConstraintViolationException exception) {
final Models models = new com.oracle.ozark.core.ModelsImpl();
final ConstraintViolation<?> cv =
exception.getConstraintViolations().iterator().next();
final String property = cv.getPropertyPath().toString();
final FormErrorMessage formError = new FormErrorMessage();
formError.setProperty(
property.substring(property.lastIndexOf('.') + 1));
formError.setValue(cv.getInvalidValue());
formError.setMessage(cv.getMessage());
models.put("formError",formError);
request.setAttribute("formError", formError);
final Viewable viewable = new Viewable("error.hbs", models);
return Response.status(BAD_REQUEST).entity(viewable).build();
}
}
这个ConstraintViolationExceptionMapper类是一个 JAX-RS 提供者,因为我们用@javax.ws.rs.ext.Provider注解了它。这个类泛型类型化为ConstraintViolationException,因此它处理了 Web 应用程序中的所有失败!这里没有余地。我们将注入到HttpServletRequest POJO 中,以便访问 Web 上下文。toResponse()方法将约束违规转换为新的响应。我们需要Models实例的实现,所以我们将在这个 Ozark 框架中实例化这个类。我们将直接构建一个FormErrorMessage POJO,并从javax.validation.ConstraintViolation类型的第一个实例中填充它。我们将在Models实例和 servlet 请求范围内设置一个键属性。从这里,我们将创建一个带有视图模板引用error.hbs和Models实例的Viewable实例,然后构建并返回一个响应。
值得一看的是参考实现 Ozark 的一些内部细节。我们一直看到ViewEngineBase和ViewEngineContext。以下是某些重要内部类及其包的图示:

MVC 绑定结果验证
MVC 规范有一个用于细粒度验证的 API,这个 API 仍在决定中。在javax.mvc.binding包中有两种接口类型——BindingResult和BindingError。
BindingResult在尝试验证带有@FormParam注解的 MVC 控制器方法的输入参数时捕获约束违规。规范描述了绑定这个术语,以反映表单参数与正在验证的属性的实际情况之间的关联以及可能发生的约束违规。因此,如果 HTML 表单参数(它是一个字符串)不能以有意义的方式转换为数字,则整数属性不能被绑定。BindingResult的接口如下:
package javax.mvc.binding;
public interface BindingResult {
boolean isFailed();
List<String> getAllMessages();
Set<BindingError> getAllBindingErrors();
BindingError getBindingError(String param);
Set<ConstraintViolation<?>> getViolations(String param);
ConstraintViolation<?> getViolation(String param);
}
BindingResult的有趣成员有isFailed()、getAllViolations()和getAllBindingErrors()。
BindingError类型是为了表示在将参数绑定到控制器方法时发生的单个错误而设计的。以下是该类型的简化接口 API:
package javax.mvc.binding;
public interface BindingError {
String getMessage();
String getMessageTemplate();
String getParamName();
}
BindingError类型与 Bean Validation 规范中的插值消息类似。因此,它对国际化很有帮助,因为消息可以从java.util.ResourceBundle检索。
对于我们的最终示例,我们将使用BindingResult来验证我们的新editMethod()方法:
@Path("/products")
@Stateless
public class ProductController {
@Inject BindingResult br;
/* ... */
@POST
@Controller
@Path("edit/{id}")
@Produces("text/html")
@ValidateOnExecution(type = ExecutableType.NONE)
public Response editProduct(
@PathParam("id") int id,
@FormParam("action") String action,
@Valid @BeanParam Product incomingProduct)
{
defineCommonModelProperties("Edit Product");
if ("Save".equalsIgnoreCase(action)) {
Set<ConstraintViolation<?>> set = vr.getAllViolations();
if (!set.isEmpty()) {
final ConstraintViolation<?> cv = set.iterator().next();
final String property = cv.getPropertyPath().toString();
formError.setProperty(
property.substring(property.lastIndexOf('.') + 1));
formError.setValue(cv.getInvalidValue());
formError.setMessage(cv.getMessage());
models.put("formError",formError);
return Response.status(BAD_REQUEST).
entity("error.hbs").build();
}
final List<Product> products = productService.findById(id);
final Product product = products.get(0);
product.setName(incomingProduct.getName());
product.setDescription(incomingProduct.getDescription());
product.setPrice(incomingProduct.getPrice());
productService.saveProduct(product);
models.put("product", product);
}
retrieveAll();
return Response.status(OK).entity("/products.hbs").build();
}
}
为了获得细粒度验证的好处,我们将@BindingResult注入作为属性到ProductController中。我们将更改editProduct()方法周围的注解。为了确保 JAX-RS 执行验证而 CDI 和 Bean Validation 不会终止进程,我们将注解@ValidateOnExecution并将类型参数设置为ExecutableType.NONE。根据 Bean Validation 规范,@ValidateOnExecution注解用于选择性地启用和禁用违规。关闭验证允许 JAX RS 接管我们的控制器方法,editProduct()。我们还将使用@Valid和@BeanParam来指示 MVC 提供者验证Product实体 bean。
当 MVC 注意到控制器类已注入BindingResult实例或有一个接受BindingResult的 JavaBean 设置方法时,它将接管验证。在editProduct()方法中,我们将通过调用isFailed()方法检查验证的布尔状态。如果输入验证失败,我们将从BindResult结果中获取第一个约束违规,然后像以前一样填充FormErrorMessagebean。然后我们将发送一个带有 Bad Request 错误代码的 HTTP 响应,该响应转发到error.hbs视图模板。
注意,我们将使用一个单独的命名变量参数incomingProduct来编写editProduct()方法,以便保持 HTML 表单数据的临时持有者。我们将复制此变量的属性到从数据库检索的产品实体,并保存它。当我们到达控制器方法末尾时,实体 bean 必须有效。我们将检索产品列表并返回一个 OK 响应。使用BindingResult,开发人员可以清楚地看到这种验证更容易编程。需要深思熟虑的代码更少。
设计考虑
MVC 是一个非常有前途的规范,目前,视图技术解决方案对于数字开发者来说是常见的。关于处理 HTTP 重定向响应的故事仍然有待开发,特别是关于保持表单状态的方法。许多数字 Web 开发者已经熟悉设计模式 HTTP POST– REDIRECT–GET (en.wikipedia.org/wiki/Post/Redirect/Get),因此,他们会在 MVC 规范中寻找一个等效且安全的选项。
在等式的另一边,是关于 HTML 表单验证的问题;然而,在这个方面可能会有突破性的新闻。关于表单验证的故事与 HTTP 重定向请求有很多共同点。开发者想要利用实体上的 Bean Validation,但他们也希望在控制器中无缝调用验证并检查验证结果。事实上,JAX-RS 允许通过全局提供者进行验证。然而,这种方法不提供细粒度的验证处理,并且无法将约束违规实例映射到单个 HTML 表单输入元素。尽管如此,Early Draft Release 快照在这一点上已经显示出早期的希望。
正如我们在 Handlebars Java 代码示例中所看到的,许多数字架构师在设计上有一个考虑和权衡:多少展示逻辑位于客户端与服务器端之间?
大多数服务器端模板
考虑到主要在服务器端模板化的选项,为什么技术架构师会选择这种模式?这种方法的理由有很多。其中一个原因可能是团队对 JavaScript 编程有些畏惧,希望最小化依赖和上市时间。这是一个技能、培训和维护的权衡。另一个原因可能是最终端客户设备性能不足,例如物联网设备。一个数字恒温器不太可能需要渲染视网膜版图像。另一个原因可能是为了使非常旧的遗留应用程序更容易迁移以使其更新:这是数字化转型。MVC 可以通过广泛的支持视图模板来帮助这里。
大多数客户端模板
这个设计选择意味着客户端在设备上渲染了大部分内容。据说,具有 JavaScript 和 HTML5 的智能手机在计算能力上比将人类送上月球的那艘火箭船计算机要强大许多个数量级。一个技术架构师可能想要将负载委托给客户端的原因之一是减少服务器应用程序的负载。这是服务器可扩展性上的一个权衡。Handlebars.js 是一个 JavaScript 视图模板实现,在众多互联网上的竞争 JavaScript 框架中,它完全满足这一需求。在这种模式下,MVC 控制器变成了真正将视图绑定到模型上的薄层架构。如果您的团队拥有非常强大的界面设计师和开发者,或者他们缺乏现代 Java EE 开发经验,这种模式可能也是合适的。在 UI 方面,可能存在一个合理的理由,让客户旅程映射到逐页导航。因此,该架构避免了单页应用程序,这可能会排除 AngularJS 等框架。
共享模板
最终的设计选择是将客户端和服务器端模板相结合,并共享展示视图的责任。这可能是在跨职能团队工作的数字技术架构师所青睐的策略。如果有一个强大的接口开发团队和服务器端 Java EE 团队,并且他们沟通良好,那么这是一个合理的方案。除了敏捷团队中的人力资源考虑之外,这种方案在技术上倾向于一个共享模板和愿景来组织数字资产。假设架构是在服务器端使用 Handlebars Java,在客户端使用 Handlebars.js;团队立即面临的是视图模板的组织。哪些模板在 JavaScript 前端编译,哪些模板在服务器端重要?对这个设计选择的解决将导致在服务器端构建 MVC 控制器。
让我给你们留下关于这三个设计考虑的一个警告。如果你真的称得上是行家里手,你会考虑 UX 变化和突如其来的惊喜的影响。因此,数字技术架构师必须将设计变化的影响纳入其计算之中。
摘要
在这本书的最后一章中,我们从底层开始介绍了新兴的 MVC 规范。我们涵盖了模型、视图和控制器的基本元素。我们看到 MVC 是在 JAX-RS 之上构建的,并且重用了相同的注解,包括@GET、@FormParam、@Path、@FormParam和@POST。
为了将一个方法作为 MVC 控制器,我们用@Controller注解了它们。我们编写了生成响应实例的控制器,或者如果它们返回了 void 类型,我们会用@View注解该方法。
你了解了 MVC 参考规范 Ozark 支持的多种视图技术。我们使用了 Handlebars Java 视图模板来构建 CRUD 示例的元素。我们也了解到 MVC 规范可能会在重定向和验证 API 方面发生变化。
练习
这里是本章的问题和练习:
-
Java EE MVC 框架的组成部分是什么?MVC 试图解决什么问题?
-
@Controller、@Model和@View之间的区别是什么? -
响应类型和可视化类型之间有什么区别?
-
由于标准的 HTML 表单元素不支持发送 HTTP PUT 或 DELETE 请求,那么在 MVC 架构中,我们该如何处理从数据库中删除记录的操作呢?这对已经拥有完整 RESTful 接口的企业意味着什么呢?
-
到这本书出版的时候,MVC 框架将会有进一步的发展,很可能会出现许多里程碑式的版本。更新你的知识。有什么变化?特别是关注重定向和验证。
-
从书籍的源代码仓库下载
handlebars-javaee-mvc-validation项目,并调整产品实例以包括description(字符串)、manufacturer(字符串)和manufacturedDate(java.util.Date)属性。为了正确显示格式化的日期(例如,MMM-dd-yyyy 或 dd-MMM-yyyy),需要发生什么? -
从上一个练习开始,确保使用 MVC 模式正确地实施验证技术。你将如何验证用户输入?
-
如果你合理地熟悉 JavaScript 编程,编写一个模块来通过 AJAX 调用验证。(提示:你可能需要理解 JQuery REST 请求和响应。)
-
在这个练习中,以
Product实体和ProductController控制器类为基础,使用它们来实验另一个视图模板技术,例如 Ozark 的参考实现。有很多选择。将新的模板框架适应到产品 CRUD 应用程序中——尝试 Thymeleaf 或甚至 AsciiDoc。模板选择之间的区别是什么?有什么好处?有什么缺点? -
写一篇简短的论文,向你的当前团队提出使用 MVC 框架进行特定项目的建议。MVC 方法的设计考虑因素是什么?你的目标受众适合单页导航、逐页导航还是两者的混合?(提示:角色可能很有用;你可以写一个旨在说服你团队中的技术领导或你可能是技术领导,旨在说服团队成员的提案。)
附录 A. JSF 与 HTML5、资源和 Faces Flows
本附录包含 JSF 库的快速参考,分为两部分。第一部分涵盖了库的参考资料、架构资源库合同和国际化配置。第二部分专门介绍使用 Java API 编写 Faces Flows。
一个 HTML5 友好的标记
在 Java EE 7 和 JSF 2.2 发布之后,框架始终渲染 HTML5 文档类型:<DOCTYPE html>。这是现代数字网站的默认行为。有了 HTML5 支持,开发者只需在 Facelets 视图(一个*.xhtml文件)中编写内容,该视图与现代网络浏览器的渲染引擎兼容。
JSF 2.2 为 HTML 元素提供了两种类型的属性:pass-through属性和pass-through元素。
透传属性
pass-through属性使 JSF 能够无缝支持由 HTML5 定义的新和扩展属性。这些属性适用于 JSF HTML 渲染套件的自定义标签,如<h:inputText>。为了使用pass-through属性,我们将在 Facelets 视图的顶部定义一个 JSF 命名空间,该命名空间引用 URL xmlns.jcp.org/jsf/passthrough。
让我们举一个使用 HTML5 属性作为占位文本的例子:
<html
>
...
<div class="form-group">
<label for="birthday" class="col-sm-3 control-label">
Birthday </label>
<div class="col-sm-9">
<h:inputText p:type="date"
class="form-control" id="birthday"
value="#{customerController.birthday}"
p:placeholder="1977-10-25"/>
</div>
</div>
<div class="form-group">
<label for="email" class="col-sm-3 control-label">
Email </label>
<div class="col-sm-9">
<h:inputText p:type="email"
class="form-control" id="email"
value="#{customerController.email}"
p:placeholder="yourname@yourserverhost.com"/>
</div>
</div>
在前面的示例中,pass-through属性使用完整的命名空间p:placeholder和p:type进行标记。一个输入字段用于日期格式,另一个用于电子邮件地址。占位符的值向客户/用户提供了一个视觉提示,说明如何填写字段。JSF 渲染套件会将此属性(减去 XML 命名空间)传递给渲染的标签输出。类型值覆盖了默认渲染套件的设置。
一些 JavaScript 框架也依赖于扩展属性,例如 AngularJS 或某些 JQuery 插件。JSF 允许页面内容编写者和界面开发者将这些值作为属性传递。透传属性的值可能是动态的,也可以从 EL 中派生。因此,我们可以编写以下标记来验证安全的贷款金额:
<h:inputText p:type="range"
class="form-control" id="loanAmount"
value="#{customerController.loanAmount}"
p:min="#{loanHelper.min}"
p:max="#{loanHelper.max}" p:step="#{loanHelper.step}"
p:placeholder="Decimal number between {loanHelper.min} and {loanHelper.max}"/>
在前面的代码片段中,pass-through属性p:type作为范围值会导致现代浏览器渲染滑动控件而不是文本控件。有一个名为loanHelper的后备 bean,它动态地向控件提供最小值、最大值和步进单位值。
透传元素
pass-through属性的缺点是它们不考虑现代网络浏览器中可用的标准 HTML5 属性。使用快速 UX 标记(如<h:inputText>)定义的输入字段,如果没有在后台运行微服务,则利益相关者客户端无法直接查看。
为了启用我们的 Facelets 视图中的pass-through元素,我们首先必须定义 JSF 命名空间http://xmlns.jcp.org/jsf。为了触发pass-through元素,至少一个 HTML 元素的属性必须在命名空间中。
让我们看看在实际的 Facelets 视图中它是如何工作的:
<html
>
<head jsf:id="head"> ... </head>
<body jsf:id="body">
...
<form jsf:id="vacationForm">
...
<input type="text" jsf:id="companyId"
placeholder="Your company identity number"
jsf:value="#{vacationController.companyNo}"/>
<input type="text" jsf:id="lastName"
placeholder="Enter last name"
jsf:value="#{vacationController.name}"/>
...
<button class="btn btn-primary"
jsf:action="#{vacationController.makeRequest}">
Make Vacation Request</button>
</form>
</body>
</html>
HTML 元素(head、body、form、input和button)至少有一个 JSF 命名空间属性。JSF 检测该属性并向其渲染树添加一个组件。元素名称决定了添加的组件类型。因此,head元素实际上是通过等效的<h:head>自定义标签渲染的,而body元素也是通过等效的 JSF <h:body>自定义标签渲染的。HTML 输入类型被特别处理,并使用<h:inputText>渲染。id和value属性通过 JSF 命名空间传递。占位符属性作为正常属性传递,不带 JSF 命名空间。它们意外地被提供并像pass-through属性一样处理!
我们可以为看起来像是正常的 HTML5 元素添加验证。以下代码确保用户至少输入六个字符:
<input type="text" jsf:id="companyId"
placeholder="Your company identity number"
jsf:value="#{vacationController.companyNo}"/>
<f:validateLength minimum="6" />
</input>
我们将把<f:validateLength>作为 HTML 输入元素的直接体内容插入。HTML5 支持确保该标签被翻译为 JSF 输入组件。支持还扩展到 AJAX 标签和库函数。
资源标识符
在 JSF 2.0 中,资源是一个图像、文档或其他数字资产。它们被放置在 Web 上下文文件夹下的resources文件夹中。在一个 Gradle 或 Maven 项目中,特定的资源位于路径上,如下所示:
src/main/webapp/resources/<RESOURCE-IDENTIFIER>
一个资源也可以存储在WEB-INF/lib文件夹下的 JAR 文件中:
src/main/webapp/WEB-INF/lib/<SOME>.jar
如果资源存储在 JAR 文件中,它必须位于META-INF文件夹中,以便可以通过路径找到:
META-INF/resources/<RESOURCE-IDENTIFIER>
RESOURCE-IDENTIFIER可以进一步细分为单独的路径。最广泛的情况支持完全本地化。组成部分如下:
<RESOURCE-IDENTIFIER> :=
[ <LOCALE-PREFIX> / ] [ <LIBRARY-NAME> / ]
[ <LIBRARY-VERSION> / ]
<RESOURCE-NAME> [ / <RESOURCE-VERSION> ]
子术语允许轻松识别和分离资源标识符。
可选的LOCALE-PREFIX术语表示一个区域设置,例如en_gb(英国英语)或de(德国)。
可选的LIBRARY-NAME术语指定了一个库名称。你可以定义库名称并在 JSF 自定义标签中使用它,如下所示:
<h:outputStylesheet library="default" name="styles/app.css" />
<h:outputStylesheet library="admin" name="styles/app.css" />
之前的示例检索了适合库的应用样式表。这有助于区分你的数字应用的不同部分。资源查找可能解析为以下内容:
<WEB-CONTEXT>/resources/en/default/1_0/styles/app.css
<WEB-CONTEXT>src/main/webapp/resources/en/admin/1_0/styles/app.css
这些 URL 映射到传统项目中的以下源文件资产:
src/main/webapp/resources/en/default/1_0/styles/app.css
src/main/webapp/resources/en/admin/1_0/styles/app.css
开发者不指定库版本。相反,JSF 资源查找机制搜索相应资源的最高版本文件。文件夹名称的版本号必须匹配正则表达式:\d_\d。因此,version文件夹名称2_0大于1_5,而1_5又大于1_0。
资源库合同
JSF 2.2 引入了资源库合同的概念,允许数字开发者将资产和模板组织成一个可重用的资源集。资源库合同必须位于应用程序的 web 上下文根目录中的合同目录文件夹中。在标准的 Maven 或 Gradle 构建约定中,这个文件夹是src/main/webapp/contracts。合同文件夹中的每个子文件夹代表一个命名的资源合同。
第六章的设置,JSF 流程和技巧使用了资源库合同的布局,如下所示:
/src/main/webapp/contracts/
/src/main/webapp/contracts/default/
/src/main/webapp/contracts/default/template.xhtml
/src/main/webapp/contracts/default/styles/app.css
/src/main/webapp/contracts/default/images/
/src/main/webapp/contracts/victoria/
/src/main/webapp/contracts/victoria/template.xhtml
/src/main/webapp/contracts/victoria/styles/app.css
/src/main/webapp/contracts/victoria/images/
每个合同至少必须有一个声明的template.xhtml文件。合同可以拥有多个模板以进行自定义。声明的模板至少有一个声明的插入点,这被定义为<ui:insert>标签。模板通常依赖于数字资产,这些被称为声明的资源。
合同可以被打包成 JAR 文件供客户使用。资源库合同必须放置在 JAR 文件的META-INF/contracts文件夹中。因此,我们可以按照以下布局重新打包流程示例:
META-INF/contracts/
META-INF/contracts/default/javax.faces.contract.xml
META-INF/contracts/default/template.xhtml
META-INF/contracts/default/styles/app.css
META-INF/contracts/default/images/
META-INF/contracts/victoria/
META-INF/contracts/victoria/javax.faces.contract.xml
META-INF/contracts/victoria/template.xhtml
META-INF/contracts/victoria/styles/app.css
META-INF/contracts/victoria/images/
我们需要为每个合同添加一个空的标记文件,javax.faces.contract.xml。这个文件名的引用可以在静态字符串javax.faces.application.ResourceHandler.RESOURCE_CONTRACT_XML中找到。
一个 Faces servlet
在 JSF 中,FacesServlet 充当前端控制器,是所有请求的通道,同时也将响应发送回客户端。这个 servlet 是javax.servlet.Servlet的子类。
网络浏览器客户端向 servlet 容器发送 HTTP 请求。如果是 Faces 请求,则 servlet 容器调用 FacesServlet 的 service() 方法。该方法将请求的处理交给一个成员实例 javax.faces.lifecycle.LifeCycle 对象。该方法还创建了一个 FacesContext 实例。LifeCycle 实例负责处理请求到所有在 第二章 中描述的 JSF 阶段,即 JavaServer Faces 生命周期 以及响应的渲染。
资源路径的重配置
通过配置 web XML 部署描述符,还可以更改资源查找和合同文件夹的路径名称。常量定义在 javax.faces.application.ResourceHandler 类中。WEBAPP_RESOURCES_DIRECTORY_PARAM_NAME 字符串常量定义了资源目录属性名称,而 WEBAPP_CONTRACTS_DIRECTORY_PARAM_NAME 定义了合同目录属性。
我们可以使用以下 web.xml 设置重新定义 JSF 网络应用程序的默认值:
<web-app ...>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<init-param>
<param-name>javax.faces.WEBAPP_RESOURCES_DIRECTORY</param-name>
<param-value>/myresources</param-value>
</init-param>
<init-param>
<param-name>javax.faces.WEBAPP_CONTRACTS_DIRECTORY</param-name>
<param-value>/mycontracts</param-value>
</init-param>
</servlet> ...
</web-app>
我们可以通过在覆盖默认配置的情况下在 Faces servlet 上指定初始参数。
JSF 特定配置
Faces servlet 理解其他几个配置参数。以下是可能的参数名称列表。这些名称以 javax.faces 为前缀:
| 参数名称 | 描述 |
|---|---|
CONFIG_FILES |
这指定了一个逗号分隔的列表,包含与上下文相关的附加资源路径,这些路径会自动与 WEB-INF/faces-config.xml 一起加载。 |
DEFAULT_SUFFIX |
这设置了 JSF 文件的后缀;默认为 *.xhtml。如果您更改此配置,则建议您也更改 web.xml 中的 welcome-file-list。 |
DISABLE_FACELET_JSF_VIEW_HANDLER |
如果此参数设置为 true,则禁用 Facelets 作为默认页面声明语言。默认值为 false。 |
FACELETS_BUFFER_SIZE |
这指定了 JSF 响应的缓冲区大小。默认大小为 -1,表示无限制大小。 |
FACELETS_REFRESH_PERIOD |
这设置 JSF 编译器检查更改的间隔时间(秒)。在生产模式下,此值设置为 -1,表示 JSF 编译器不应检查;否则,在参考实现中默认设置为 2。 |
FACELETS_SKIP_COMMENT |
这是一个布尔值,用于确定 Facelets 视图中的 XML 注释是否包含在响应中。默认值为 true。 |
FACELETS_LIBRARIES |
这指定了一个分号分隔的列表集合,通过路径指定 Facelets 标签库。 |
LIFECYCLE_ID |
这覆盖了 JSF javax.faces.lifecycle.Lifecycle 实例的默认实现。 |
PROJECT_STAGE |
这指定了开发项目阶段。可接受值为 Development、UnitTest、SystemTest 或 Production。 |
STATE_SAVING_METHOD |
这指定了在 JSF 应用程序中保存状态的位置。可接受的值是client和server。 |
WEBAPP_CONTRACTS_DIRECTORY |
这覆盖了 JSF 资源合同的默认位置。默认是<web-context>/contracts。 |
WEBAPP_RESOURCES_DIRECTORY |
这覆盖了 JSF 为数字资产保留的资源的默认位置。默认是<web-context>/resources。 |
这些设置最好在 Web 部署描述符文件(web.xml)中进行配置。
国际化
一个 JSF 应用程序可以使用标准资源包(java.util.ResourceBundle)和消息进行国际化。资源包适合用于国际化组件、控件和数字资产上的文本;而消息文件旨在国际化 JSF 验证错误。
在 Gradle 或 Maven 项目中,我们将添加一个资源包或消息文件到src/main/resources项目位置,并使用必要的 Java 包作为文件夹。
资源包
我们将在 Web 应用程序的配置文件/WEB-INF/faces-config.xml中配置资源包。
对于第四章中关于联系详情的应用程序第四章,我们有两个英语和德语文言包文件:appMessages.properties和appMessage_de.properties。
以下是英语语言包:
// uk/co/xenonique/digital/appMessages.properties
contactForm.firstName=First Name
contactForm.lastName=Last Name
contactForm.houseOrFlatNumber=House or Flat Number
contactForm.street1=Street 1
contactForm.street2=Street 2
contactForm.townOrCity=Town or City
contactForm.region=Region
contactForm.postCode=Post Code
以下是德语文言包:
// uk/co/xenonique/digital/appMessages_de.properties
contactForm.firstName=Vornamen
contactForm.lastName=Nachnamen
contactForm.houseOrFlatNumber=Haus oder Wohnung Nummer
contactForm.street1=Strasse 1
contactForm.street2=Strasse 2
contactForm.townOrCity=Stadt oder Gemeinde
contactForm.region=Lande
contactForm.postCode=PLZ
为了在我们的 JSF 包中使用这些包,我们将按照以下方式配置 Faces 配置:
<?xml version="1.0"?>
<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">
<application>
<resource-bundle>
<base-name>uk.co.xenonique.digital.appMessages</base-name>
<var>appMessages</var>
</resource-bundle>
</application>
</faces-config>
我们将在/WEB-INF/faces-config.xml中为我们的应用程序消息定义一个资源包配置。在这个文件中,我们可以为我们的包定义一个应用范围变量,即appMessages。
从这里,我们可以使用 EL 语法使用这个包。以下是应用程序中房屋号码的代码:
<h:form id="yourAddressForm"
styleClass="form-horizontal" p:role="form">
<div class="form-group">
<h:outputLabel for="houseOrFlatNumber"
class="col-sm-3 control-label">
#{appMessages['contactForm.houseOrFlatNumber']}
</h:outputLabel>...
</div> ...
</h:form>
由于我们在属性名中使用点(.),我们必须使用映射 EL 语法来获取所需的消息:#{appMessages['contactForm.houseOrFlatNumber']}。
资源包也可能包含带有格式化标记参数的参数化消息属性。标记参数在消息解析过程中由 JSF 框架或通过应用程序自定义代码进行展开。以下是一个参数化消息属性的示例:
contactForm.specialNote = proctor records {0}, {1} and {2}
然后,我们将使用<h:outputFormat>标签来渲染输出,如下所示:
<h:outputFormat value="#{appMessages['contactForm.specialNote']}">
<f:param value="first argument" />
<f:param value="second argument" />
<f:param value="third argument" />
</h:outputFormat>
消息包
在 JSF 应用程序中,后端代码通常可以生成输出消息,这些消息也可以进行国际化。这些消息存储在 FacesContext 对象中。我们需要确保用户可以看到所有或部分这些消息。我们将使用<h:messages>或<h:message> JSF 标签来实现这一点。
在第四章中,我们已经查看了消息包的faces-config.xml。
浏览器配置的区域设置
对于许多数字站点,用户的浏览器决定了网站的语言环境。开发者可以在/WEB/faces-config.xml文件中通知 JSF 关于应用程序支持的语言。
这里是联系详情应用的设置:
<faces-config>
<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>fr</supported-locale>
<supported-locale>de</supported-locale>
</locale-config>
</application>
</faces-config>
在此文件中,我们将指定我们的默认语言环境为英语,支持的语言环境有法语和德语。
应用程序控制的语言环境
某些数字站点的要求是允许用户在两个或多个语言环境之间切换。因此,我们必须通过编程实现这个目标。对于 JSF 中渲染的每个视图,我们都需要在控制器方法中设置语言环境。我们将使用 FacesContext 来访问 Faces 响应中的根视图 UI,并在此处设置语言环境。
假设我们为一家在英国伦敦和德国南部海德堡设有办事处的运动汽车制造商工作;那么,我们可以编写以下代码:
final UIViewRoot viewRoot = FacesContext
.getCurrentInstance().getViewRoot();
viewRoot.setLocale(new Locale("de"));
视图将被设置为德语语言环境。
单个页面控制的语言环境
如果利益相关者想要一个数字站点,其中每个单独的页面都可以有自己的配置语言环境,那么 JSF 可以实现这个目标。开发者可以使用<f:view>视图标签来覆盖页面的语言环境。在 JSF 2.0 标准之前,视图标签包装了内容并充当容器元素。在 JSF 2.0 及以后的版本中,这现在是多余的,视图标签可以设置语言环境。
这里是从具有名为primaryLocale属性的配置文件中检索语言环境的代码片段:
<f:view locale="#{userProfile.primaryLocale}"/>
这里是与页面视图相关的会话范围 bean:
@SessionScoped
class UserProfile {
private Locale primaryLocale = Locale.ENGLISH;
public Locale getPrimaryLocale() {
return primaryLocale;
}
}
网络部署描述符
这是为了进入一个单独的附录。
这里是 JSF 2.2 和 Java EE 7 的参考开发 XML 部署描述符:
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout> 30 </session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>index.jsf</welcome-file>
</welcome-file-list>
</web-app>
编程式 Faces 流程
在这个附录中,我们将提供 JavaServer Faces 流程的快速参考。一个流程是根据用户和每个 Web 应用程序的有限状态机,具有节点。有一个默认的入口节点和至少一个出口节点。
视图类型
在第六章,“JSF 流程和优雅”,我们讨论了从 Faces 配置 XML 文件构建流程导航。JSF 2.2 规范描述了使用目录结构存储和设置流程的约定。
存在几种类型的节点。它们在以下表中列出:
| 节点类型 | 描述 |
|---|---|
| 视图节点 | 此节点表示一个视图。JSF 提供者渲染视图,流程仍然活跃。 |
| 返回节点 | 此节点表示从流程中退出到此流程外部的一个出口点。当前流程在调用此节点时终止。 |
| 流程调用节点 | 此节点表示对另一个嵌套流程的调用。 |
| 方法调用节点 | 此节点表示在流程范围内的 bean 中对方法调用的调用。离开此方法调用后,当前流程仍然活跃。 |
| 切换节点 | 此节点表示一个可选择的条件状态。根据状态,流程可能移动到一个或多个替代状态,并且有一个默认结果。当前流程仍然有效。 |
| 导航情况节点 | 此节点表示一个具有导航的通用条件状态。根据状态,流程可能移动到新的结果或执行流程外的 HTTP 重定向。 |
小贴士
通过将 Faces Flows 与其他流程或业务流程技术进行比较,应该承认没有所谓的初始节点。
Faces Flows 程序化接口
Faces Flows 由 CDI 驱动。可以通过声明一个生成 Faces Flow 的 POJO 来编写程序化流程。为了动态生成流程,我们必须将一个 Bean 标注为 CDI 生成器并生成一个流程实例。
这里是一个基本的 SimpleFlowFactory 类,它提供 Faces Flow:
import javax.faces.flow.builder.*;
public class SimpleFlowFactory implements Serializable {
@Inject DocumentIdGenerator generator;
@Produces @FlowDefinition
public Flow createFlow(
@FlowBuilderParameter FlowBuilder flowBuilder)
{
final String documentId = generator.generate();
flowBuilder.id(documentId, "simpleFlow");
flowBuilder.viewNode("simpleFlow",
"/simpleFlow/simpleFlow.xhtml")
.markAsStartNode();
return flowBuilder.getFlow();
}
}
我们将 createFlow() 方法标注为具有特殊限定符的 CDI 生成器,即 @FlowDefinition。我们还将为此方法提供一个单一参数,即 FlowBuilder,它也被标注为 @FlowBuilderParameter。顺便说一下,这些定义可以在保留的导入包 javax.faces.flow.builder 中找到。我们使用注入的 FlowBuilder 来生成特定的流程。实际的节点类型可以在 javax.faces.flow 包中找到。
Faces Flow 需要一个流程标识符,即 simpleFlow。我们可以选择性地也定义一个特定的文档 ID。我们将使用一个依赖生成器实例来演示它是如何工作的。如果没有必要提供文档 ID,则提供一个空字符串。
Faces Flow 至少需要一个视图,这通常在 XML 配置中是默认名称。为了完成第一个视图节点,我们将定义一个 ViewNode 和流程视图模板的 URI:/simpleFlow/simpleFlow.xhtml。我们还需要设置流程的起始节点,调用设置为 markAsStartNode(),因为 Faces Flow 需要一个初始节点。FlowBuilder 的 API 来自流畅构建器模式。在 createFlow() 方法的末尾,我们将生成一个流程实例并返回它。从这一点开始,JSF 提供者接管流程定义。
记住,虽然我们自己在构建流程,但我们不必完全遵循 XML 配置的所有规则。然而,一个流程定义的所有视图模板必须位于 Web 应用程序根上下文下的一个单独文件夹中。
尽管我们定义了流程定义生成器,我们仍然需要实际的流程作用域 Bean,即:
@Named("simpleFlowController")
@FlowScoped("simple")
public class SimpleFlow implements Serializable {
/* ... */
}
我们将在以下章节中查看每种节点类型的详细信息。
以下 UML 类图说明了 API 中的关键类型:

Faces Flow API 的 UML 类图
ViewNode
使用 FlowBuilder,我们可以定义 ViewNode 实例。实际类型是 javax.faces.faces.ViewNode 的子类。
以下代码示例扩展了最后一个流实例并提供了额外的视图节点:
@Produces @FlowDefinition
public Flow createFlow(
@FlowBuilderParameter FlowBuilder flowBuilder)
{
final String documentId = generator.generate();
flowBuilder.id(documentId, "simpleFlow");
/* ... */
flowBuilder.viewNode("page1", "/simpleFlow/page1.xhtml")
flowBuilder.viewNode("page2", "/simpleFlow/page2.xhtml")
flowBuilder.viewNode("page3", "/simpleFlow/page3.xhtml")
return flowBuilder.getFlow();
}
viewNode()方法接受两个参数。第一个参数是视图节点 ID,我们将在视图内容中写入并引用它。第二个参数是视图描述语言文档标识符,它对应于 Facelets 视图的 URI。
URI 的路径也可以是一个相对 URL。这意味着最终路径相对于流 URI 路径是相对的。否则,URI 必须相对于 Web 上下文根路径是绝对的,正如我们可以在前面的代码摘录中看到的那样。
viewNode()方法返回一个ViewBuilder类型,它只有一个额外的markAsStartNode()方法。
ReturnNode
我们还可以从FlowBuilder生成一个ReturnNode实例。使用returnNode()方法非常简单。实际类型是javax.faces.faces.ReturnNode的子类。
这是createFlow()方法的摘录,它指定了一个返回节点:
@Produces @FlowDefinition
public Flow createFlow(
@FlowBuilderParameter FlowBuilder flowBuilder)
{
final String documentId = generator.generate();
flowBuilder.id(documentId, "simpleFlow");
/* ... */
flowBuilder.returnNode("return-backup").fromOutcome("welcome");
return flowBuilder.getFlow();
}
returnNode()方法接受一个参数,即视图节点 ID,并返回一个ReturnBuilder实例。fromOutcome()方法调用指定了返回当前流后要导航到的结果视图的名称。
MethodCall
使用FlowBuilder实例,我们还可以创建一个方法调用节点并将其添加到 Faces Flow 中。实际类型是javax.faces.faces.MethodCallNode的子类。以下是 POJO 类:
@Produces @FlowDefinition
public Flow createFlow(
@FlowBuilderParameter FlowBuilder flowBuilder)
{
/* ... */
flowBuilder.methodCallNode("check-funds")
.expression(
"#{loanController.verifyCustomer(customer.account)}",
new Class[]{String.class});
.defaultOutcome("no-funds-available")
return flowBuilder.getFlow();
}
methodCallNode()调用接受一个视图节点 ID 并创建一个MethodCallBuilder实例。此节点需要一个表达式来通知 JSF 关于要调用的 bean 的名称和方法。在这里,方法是在可访问的后备 bean 控制器loanController上的verifyCustomer()。我们还可以使用表达式语言传递客户的详细信息中的账户记录。如果目标方法调用返回 null 或空字符串,我们可以指定一个默认的结果视图标识符。
我建议你在分层架构中尽可能减少耦合。注意不要将太多表示层信息与业务逻辑混合。
FlowCall
我们将使用相同的抽象类类型FlowBuilder创建一个流调用节点。此节点类型调用嵌套的 Faces Flow。flowCallNode()方法接受一个视图节点 ID 参数,并返回一个FlowCallBuilder实例。实际类型是javax.faces.faces.FlowCallNode的子类。
在这个基于第六章 工业部门碳足迹讨论的代码摘录中,我们将重写名为footprint-flow.xml的流定义 XML 文件,如下面的SectorFlowFactory POJO 类所示:
class SectorFlowFactory implements Serializable {
/* ... */
@Produces @FlowDefinition
public Flow createFlow(
@FlowBuilderParameter FlowBuilder flowBuilder)
{
flowBuilder.id("", "sector-flow");
/* ... */
flowBuilder.flowCallNode("callFootprintFlow")
.flowReference("", "footprint-flow)
.outboundParameter("param1FromSectorFlow",
"param1 sectorFlow value")
.outboundParameter("param2FromSectorFlow",
"param2 sectorFlow value")
.outboundParameter("param3FromSectorFlow",
"#{sectorFlow.footprint}");
return flowBuilder.getFlow();
}
}
在前面的代码中,节点被标识为callFootprintFlow。我们必须通过flowReference()方法提供嵌套流的节点 ID。flowReference()的第一个参数是流文档 ID,这里是一个空字符串。嵌套流通过第二个参数被标识为足迹流。
我们将在FlowCallBuilder上使用outboundParameter()方法来声明出站参数,这些参数将数据从调用者 sector-flow 传递到嵌套足迹流。outboundParameter()的第一个参数是参数名称,第二个参数是字面字符串或表达式。
我们还可以通过编程方式重写嵌套流的流定义 XML 文件。以下是FootprintFlowFactory类的摘录:
class FootprintFlowFactory implements Serializable {
/* ... */
@Produces @FlowDefinition
public Flow createFlow(
@FlowBuilderParameter FlowBuilder flowBuilder)
{
flowBuilder.id("", "footprint-flow");
flowBuilder.inboundParameter("param1FromSectorFlow",
"#{flowScope.param1Value}");
flowBuilder.inboundParameter("param2FromSectorFlow",
"#{flowScope.param2Value}");
flowBuilder.inboundParameter("param3FromSectorFlow",
"#{flowScope.param3Value}");
/* ... */
return flowBuilder.getFlow();
}
}
在此代码中,我们注意到足迹流 ID 必须与调用者引用 ID 匹配。对于此 Faces Flow,我们将使用inboundParameter()方法调用声明入站参数。第一个参数是参数名称,第二个参数是一个表达式语言引用,它定义了值存储的位置。
SwitchNode
我们可以使用FlowBuilder来创建声明切换节点的声明。这些节点类型由条件语句和结果之间的关联组成。结果是视图 ID。切换节点有一个或多个条件语句。它也可能有一个默认结果。在流的处理过程中,JSF 依次执行每个条件语句。如果它评估为真(或者更确切地说,Boolean.TRUE),那么相关的结果就是选择的结果。实际类型是javax.faces.faces.SwitchNode的子类。
让我们看看在 POJO 中切换节点实际操作的例子。这是另一个名为LoanDecisionFlowFactory的货币贷款决策者类:
class LoanDecisionFlowFactory implements Serializable {
/* ... */
@Produces @FlowDefinition
public Flow createFlow(
@FlowBuilderParameter FlowBuilder flowBuilder)
{
flowBuilder.id("", "loan-application");
/* ... */
flowBuilder.switchNode("loan-decision")
.defaultOutcome("home")
.switchCase()
.condition("#{loanMaker.decision=='Agreed'}")
.fromOutcome("loan-agreement")
.switchCase()
.condition("#{loanMaker.decision=='Declined'}")
.fromOutcome("loan-declined")
.switchCase()
.condition("#{loanMaker.decision=='Pending'}")
.fromOutcome("loan-pending")
/* ... */
return flowBuilder.getFlow();
}
}
在createFlow()方法中,我们将创建一个标识为 loan-application 的 Faces Flow。然后我们将使用switchNode()创建一个切换节点声明,并标识为 loan-decision。此方法返回一个SwitchBuilder类型。此构建器类型具有switchCase(),它返回SwitchCaseBuilder。其他包含的重载方法被称为defaultCome()。
建议您还使用defaultOutcome()方法声明一个默认结果,它接受一个视图 ID。这样,流总是移动到下一个可行的位置。
给定SwitchBuilder构建器类型,我们将使用condition()和fromOutCome()方法声明单个案例。condition()调用接受一个表达式字符串,并返回SwitchCaseBuilder。此构建器上的唯一属性是有效的结果,它是一个视图标识符。
导航情况节点
API 中的最后一个构建器类型与一个通用抽象类类型NavigationCaseBuilder相关。我们可以使用FlowBuilder创建导航情况节点。实际类型是javax.faces.faces.FlowNode的子类。
这里是一个来自金融服务领域的 POJO,它被设计成允许投资银行的后台工作人员在工作流中发布流程交易:
class PostProcessTradeFlowFactory implements Serializable {
/* ... */
@Produces @FlowDefinition
public Flow createFlow(
@FlowBuilderParameter FlowBuilder flowBuilder)
{
flowBuilder.id("", "post-process-trade-flow"); /* ... */
flowBuilder.navigationCase()
.fromOutcome("trade-check")
.condition("#{trade.amount > global.risk.limit}")
.toViewId("exceptional");
flowBuilder.navigationCase()
.fromOutcome("trade-check")
.condition("#{trade.settlementCcy != 'USD'")
.redirect().includeViewParams()
.parameter("deskCodeNotice", "OUT_OF_CURRENCY_DEAL");
/* ... */
return flowBuilder.getFlow();
}
}
在这个PostProcessTradeFlowFactory POJO 中,我们再次有一个createFlow()方法,但这次我们将调用navigationCase()两次。这个方法没有参数,并返回一个NavigationCaseBuilder类型的对象,该对象创建指定的 Faces Flow。导航案例流程需要一个触发点、一个条件表达式和一个结果或重定向响应。
在第一个流程中,我们将对NavigationCaseBuilder调用fromOutcome()方法,以声明触发检查的 JSF 视图 ID。condition()方法定义了必须返回布尔值的表达式。toViewId()方法定义了如果条件表达式评估为Boolean.TRUE,则导航到的结果视图 ID。因此,如果交易价值超过公司范围内的风险限制(trade.amount > global.risk.limit),则流程将导航到异常视图。
在第二个流程声明中,我们将定义一个导航案例来处理一种货币交易,这可能是一个掉期、债券或其他衍生品。使用相同的触发结果交易检查,我们将捕获非美元货币的表达式:trade.settlementCcy != 'USD'。使用这个导航案例节点,我们将执行一个 HTTP 重定向,导致客户退出整个流程。我们将调用redirect(),它将返回NavigationCaseBuilder.RedirectBuilder。这个嵌套的抽象类有两个方法:includeViewParams()和parameter()。includeViewParams()方法将所有视图参数添加到重定向 URL 中。parameter()方法将名称和值对添加到重定向 URL 中。记住,在流程完成后,从安全数字开发者的角度来看,这些参数将在最终的 HTTP GET 请求的 URL 中非常明显!
构建器类型
构建器类型——ViewBuilder、ReturnBuilder、MethodCallBuilder、FlowCallBuilder、SwitchBuilder、SwitchCaseBuilder和NavigationCaseBuilder是抽象类。它们是javax.faces.flow.builder.NodeBuilder接口的实现。Faces Flow API 是嵌入式领域特定语言的例子。
附录 B. 从请求到响应
在软件开发者角色的职位面试中,许多人经常被要求描述网络背后实际是如何工作的。一个能够向面试官很好地解释软件应用程序架构层的候选人可能会给人留下所谓的全栈开发者的印象。然而,令人惊讶的是,有多少候选人对此主题只有合理的了解,尤其是如果他们声称在数字领域专业工作的话。本附录提供了一个简洁且权威的现代问题描述。数字开发者应该能够绘制并有效地记录他们当前的工作架构。那么,让我们从 HTTP 开始。
HTTP
HTTP 是一个基本的无状态协议,旨在在服务器和客户端之间传输超媒体。HTTP 1.1 支持资源的细粒度缓存和保留持久连接以及分块传输编码的能力。HTTP 1.1 是在 1999 年创建的(参考已取代的 RFC tools.ietf.org/html/rfc2068)。为了应对现代需求和用法模式,HTTP 现在支持 WebSocket 握手和升级请求(tools.ietf.org/html/rfc6455)。下一个 HTTP 2.0 标准将为单个客户端服务器通道提供流的多路复用。对于 Java EE 8(预计 2017 年 5/6 月发布)和 Java Servlet 4.0 规范中的 HTTP 2.0 支持,这提供了令人兴奋的可能性。有关更多详细信息,请参阅 RFC 7540 (www.rfc-editor.org/rfc/rfc7540.txt)和 JSR 369 (www.jcp.org/en/jsr/detail?id=369)。
一个 HTTP 请求
一个 HTTP 请求由一个带有头部和正文内容的有效载荷组成。头部信息包含 URI 请求、HTTP 方法、代理信息、请求参数和 cookies。对于 POST 和 PUT 请求,它还可能包含带有名称和值的表单编码属性。
每个数字工程师都应该了解的四个基本 HTTP 方法分别是 GET、POST、PUT 和 DELETE。它们在以下列表中描述:
-
GET:这个请求获取与给定 URL 关联的资源内容
-
POST:这会创建一个新的资源,其有效载荷(正文内容)指定了新资源的数据
-
PUT:这通过指定被替换的一些或全部数据来更新现有资源
-
DELETE:这是一个请求,用于删除与 URL 关联的指定资源关联
以下是一些在特殊情况下也使用的罕见 HTTP 方法:
-
HEAD:这个请求通过仅检索头部来确认与给定 URL 关联的资源。这个请求与 GET 请求类似,但没有正文内容。
-
OPTION:此操作检索应用服务器功能或 Web 容器能力。
-
TRACE:此请求允许基础设施找出客户端和服务器之间的网络跳数,从而验证延迟、可用性和性能。
HTTP 响应
HTTP 响应由头部信息和有效载荷数据(即主体内容)组成。头部包含 HTTP 状态码、MIME 类型、数据长度、最后修改日期、字符集编码、cookie 参数和资源的缓存信息。头部还可能包含认证数据。主体内容是返回给客户端请求的数据。
HTTP 状态码由 W3C 定义。这些是具有范围的整数代码。通常,状态码 100-199 是信息性消息,200-299 表示成功的结果,300-399 表示重定向请求,400-499 是服务器端错误,500-599 状态码表示认证失败。因此,HTTP OK 200 和 404 NOT FOUND 状态码对于数字领域外的开发者来说是众所周知的。您可以在 RFC 中找到所有这些状态码的列表:www.w3.org/Protocols/rfc2616/rfc2616-sec10.html。
Java 企业架构
数字领域的应用在多个工业领域架构上具有共同的主线。显然,它们共享并依赖于 Java EE 7 标准平台和 JVM,以便在开发、各种非功能性属性和企业应用基础设施上“搭便车”。在构建应用方面存在细微的差异。
标准 Java EE Web 架构
标准 Java EE Web 架构源于 20 世纪 90 年代的客户端-服务器模型。应用服务器是关键组件,因为它负责 Java EE 7 完整规范中的三个容器:Servlet、CDI 和 EJB。通常,我们将这些容器映射到单体 Web 应用的层架构。这种架构的目的是从坚实的软件工程角度提升最佳实践。我们希望保持层之间的关注点分离,以避免层之间的刚性耦合,并在层之间保持强大的内聚性。以下是三个层:
-
表示层与依赖于 Servlet 容器的代码紧密相关。FacesServlet 作为 JSF 的一部分提供。替代的 Web 应用框架也有前端控制器(Front Controller)的概念,它将请求分发给单独的控制器。表示层还包含控制器和视图模板。
-
领域层与包含业务逻辑和持久化对象投影的代码相关联。它包含应用程序规则、业务逻辑和业务流程管理。领域对象可能是也可能不是依赖注入的一部分。在大多数现代 Web 应用程序中,几乎所有的领域层组件和对象都是依赖注入容器的一部分。因此,领域层与 CDI 容器相关联。
-
集成层与从应用程序到系统的持久化或服务区域的传输数据相关联。这一层映射到一个 EJB 容器的一部分的 POJO。通常,这些对象处理来自应用程序的服务调用。作为 EJB,它们是事务性的,但不一定是上下文相关的,这就是为什么它们不是 CDI 容器的一部分。这些对象通过 JPA 和/或 JDBC 异步与外部系统(如数据库)通信,通过 JMS 消息与其他系统异步通信;它们也可以通过 REST 或 SOAP 调用同步(或异步)调用远程 Web 服务端点。
以下图示说明了架构:

标准 Java EE Web 架构
扩展架构
从标准的 Java Web 架构来看,存在几种适配方式。组织可以选择优化其架构以适应特定的权衡。一种权衡可能是性能与可扩展性之间的权衡,这可能意味着为了使用特定的关系型数据库管理系统(RDMS)解决方案而重构集成层以使用 NoSQL 数据库。
在本节中,我们将检查特定客户的可用性与性能。可用性是系统可访问的程度。如果系统关闭,则不可用。性能是系统在目标时间内执行所需功能的能力。因此,这位客户真正关心的是系统的正常运行时间,因为停机时间将造成经济损失,但他们也希望有一个固定的吞吐量量。
哪些项目会降低性能?如果应用程序代码中有太多的合理性检查(检查点),那么它将降低性能。如果性能降低,那么它也可能减少可用性。然而,如果您没有对足够的参数进行合理性检查,那么系统将因错误而受到指责。如果黑客发现您的安全漏洞,或者您的案件工作人员频繁输入错误数据,那么您的可用性将受到损害,因为您的业务将因修复问题而遭受停机时间。
一种解决方案是将架构分为两个计算堡垒。我们可以利用智能手机、平板电脑和桌面电脑的现代计算能力在客户端渲染内容。因此,这种架构适合具有足够 GPU 能力的富客户端和其他合适的设备。
经过合理性检查和渲染的代码在很大程度上从服务器端移除。根据架构的不同,我们可以使用依赖于模型视图视图模型(MVVM)的 JavaScript 框架技术,该技术由 AngularJS 支持。根据技术选择,我们可以使用 JSF 的替代品,如即将推出的 Java EE 8 MVC 或直接使用 JAX-RS 端点。我们必须确保客户端和服务器端都进行正确的验证。我们还必须设计安全的、安全的和幂等的 REST API 或其他客户端与服务器之间的远程调用。请注意,我们仍然在服务器端拥有 CDI、EJB、JMS 和 JPA 等特性。
在这种扩展的 Java EE 7 架构中,有一些好处有助于在性能与可伸缩性之间的权衡。如果我们引入数据缓存层,我们将获得返回大多数来自静态引用或很少变化或最常请求的数据的能力。从智能客户端缓存请求的关键好处是响应时间最小化。
在 2013 年 Java EE 7 发布之后,JCache 临时缓存 1.0 最终 JSR 107 (jcp.org/en/jsr/detail?id=107) 也随之发布,现在它得到了 HazelCast 和 Terracota 等品牌的支持。
这种架构适合一种混合形式,它扩展了 Java EE,并超越了规格的框框。Java EE 7 的规范并没有指定服务器的编排、服务或系统的监控、深度授权和云配置。让我们看看下面的图示:

一种扩展的混合 Java EE 架构,用于解决性能与可用性之间的平衡问题
无容器系统
另一种架构在那些偏好持续部署和游击式工程的前沿企业中很受欢迎。这被称为无容器应用程序,实际上这是一个误称。如果我们仔细思考,每个实体系统都包含在从最高级容器到硬件 CPU 的某个抽象组件中。操作系统受 CPU 的限制,JVM 进程受操作系统的限制,而应用程序服务器包含部署的 Java EE 应用程序。
技术架构师总是在进行权衡。他们可能会考虑敏捷性如上市时间与成本、创新与可负担性之间的对比,在政治上,则是将遗留系统与新技术的集成,例如无容器系统。无容器应用程序的正确名称应该是嵌入式应用程序控制服务器,这准确地描述了架构。有几个 Java EE 应用程序服务器提供商允许从static void main()入口点启动一个完全嵌入的服务器。例如,JBoss 的 WildFly、Tomitribe 的 Tom EE,以及当然,Oracle 的 GlassFish 都提供了非标准的嵌入式执行 API。一些专有供应商解决方案甚至允许在嵌入式服务器的运行时模块化选择 Java EE 功能。因此,架构师可以选择和选择 JAX-RS、JSF、CDI、EJB 和 JMS 模块的提供。一个平衡且熟练的开发团队能够绕过变更管理。该团队通过编写无容器解决方案来“隐秘操作”,而不是第 100 次被告知他们不允许升级 IBM WebSphere 7 应用程序服务器。
因此,嵌入式服务器在构建跨多个关注点的微服务架构的初期非常受欢迎。允许应用程序控制嵌入式服务器的启动和停止意味着这些原型与现代 DevOps 运动(开发者/运维团队)以及自动配置管理控制的概念相得益彰。
无容器解决方案的明显缺点是,为传统 WAR 部署构建的开发工具不理解这种模式。因此,数字工程师可能会失去使用 Java EE 解决方案进行增量工程的交互性和快速迭代。然而,这可能是一个短期问题,因为 Java IDE 制造商在捕捉工程趋势方面相当出色。我们可以希望 Java EE 技术领导者、架构师和更广泛的社区表达他们对包括无容器 API 的规范的支持。
使用嵌入式服务器,工程团队有责任确保基础设施正确设置。技术架构师通过精确控制外部集成点获得好处,这对于安全性、授权和身份验证、监控和日志记录以及持久访问很有用。然而,从请求到响应的过程与标准 Java Web 架构完全相同。让我们看看下面的截图:

无容器 Java EE 架构图
尽管在嵌入式服务器应用架构方面 Java EE 8 没有标准化,但其他开发者至少已经推动了一些创新,以便将社区引导到这个方向。有 Apache Delta Spike CDI 容器控制(deltaspike.apache.org/documentation/container-control.html),它目前提供了一个跨服务器库,可以在独立的 Java SE 环境中启动和停止 CDI 容器。Delta Spike 是一个获奖的开源项目,它有专门针对 Bean Validation、CDI、数据、安全和 Servlets 以及容器管理的模块。值得密切关注这个项目,因为其中一些创新已经成为了 Java EE 8 标准的一部分。允许 CDI 管理豆参与容器管理事务(Java EE 7 和 JTA 1.2)的@javax.transactional.Transational注解最初是在 Delta Spike 项目中提出和开发的。
微服务
嵌入式应用控制的服务器是 Java 微服务架构的入口。这些微服务是一种设计企业架构的风格,其中每个组件解决和操作整个系统的一个需求。这些关键驱动因素是非功能性需求,可以是可用性、灵活性、可维护性、网络性、性能、鲁棒性和可伸缩性的任何组合。在这些中,企业选择这种风格而不是传统单体架构的原始需求是可用性和可伸缩性。
架构师在微服务中看到了某些优势:这些组件遵循 UNIX 架构原则“只做一件事,做好一件事”以及随意切割和更换组件的能力。微服务风格使得语言无关的通信成为可能;因此,实施者有自由选择在 Java、Groovy 或 Scala 中编写组件,甚至可以在非 JVM 语言如 C++中编写。这种架构强烈倾向于 JSON 或 XML 而不是 REST;然而,没有任何阻止软件店使用 SOAP 和 XML。从技术上讲,微服务是目前地球上一些最激动人心的数字项目之一;由于这些投资,一些企业已经获得了明显的商业优势。
微服务的成本是网络复杂性,包括有效载荷大小、监控(心跳)、日志记录和容错,以及冗余管理和服务路由。还有一些额外的成本,商业经理和利益相关者应该注意,即市场时间、培训、信息孤岛,当然还有变革文化。
与许多运动一样,也存在一个关于努力、能力和可行性的范围。一个企业可能并不一定完全放弃单体架构,尤其是当可扩展性和高可用性不是首要任务时。对于大多数数字业务来说,组件化服务架构确实是一条可行的路线,它吸取了单体架构的最佳部分——事务、持久性和配置——并且尽可能多地采用了微服务风格。因此,专门用于订单管理的嵌入式服务器不应该包含与支付处理相关的代码。相反,订单管理组件应该在外部调用支付处理组件。让我们看看下面的图示:

Java EE 单体架构向微服务架构的演变
上述插图显示了标准 Java EE Web 架构分解为混合组件架构,然后演变为完整的微服务架构。
从请求到响应的路径有很多种。除了作为一个团队我们应该如何到达那里之外,我们还必须问自己关键问题:我们如何到达那里?我们为什么想要到达那里?
是否要成为全栈开发者
被称为全栈开发者是否重要?顾问的典型回答是这总是取决于上下文。一些愤世嫉俗者可能会说“全栈”是一个充满营销意味的术语,但同样这些人也可能认为数字也是如此。显然,这个术语的含义远不止于一些糟糕的招聘顾问脑海中反复出现的想法。对于声称并宣传他们只想招聘全栈开发者的大型组织来说,还有很多需要改进的地方。他们是走捷径吗?还是他们真正对获得最好的编程人才感兴趣?
话虽如此,从本附录的讨论中可以看出,了解项目的架构在一定程度上是很重要的。几十年来,在软件行业中成为万事通已经不可能了,因此,工程师将专门从事某个环境、领域、角色或系统。不再可能完全隐藏在地下的小隔间中,坚决地说:“我只想知道服务器端的 Java,因为这是我唯一关心的技能,而且它将永远如此”。合格的专业工程师必须对其他团队成员表示同情,包括前端开发者、后端开发者、界面和 UX 设计师、测试人员、利益相关者和管理层。每个人都有最终的利益相关者,因此他们与利益相关者共享最终责任并承担相应的责任。这到底是全栈还是不是,很大程度上取决于你对工作生活的态度,以及你对周围现代 Web 架构的了解和尊重。
附录 C.敏捷性能 - 数字团队内部工作
我们处于数字时代。这对我们这些在数字团队工作的人来说是个好消息;但软件开发、设计和架构的实践永远不会一帆风顺。构建高质量工程解决方案确实存在实际可感知的成本。我们就是成本。这消耗了我们的时间、资源和精力,而且对更好的性能、利润率和用户友好性的需求永无止境。数字团队的表现,就像任何其他业务线一样,很大程度上取决于良好的沟通和与同事良好合作的能力。
数字团队与适应
作为数字团队的一部分,我们的事业是帮助适应旧世界的商业,将其在线推进,并乐观地朝着更好的未来前进。
数字工作的另一面是工作表现。如果你的团队负责一个高调的业务或客户,你可能会发现自己受到严格的审查,以确保项目按计划进行。有时满足客户目标与敏捷的放任开发相矛盾。在关键时刻,团队必须团结一致,以便快速交付产品,同时足够平衡,以便让团队根据他们的用户故事自行组织技术决策。通常,数字团队可以迅速前进,很少考虑整体技术架构,导致最终状态和最终交付让利益相关者感到沮丧和担忧。敏捷并不能保证产品的质量,但它有所帮助。
那么,我们如何才能改善这些问题呢?作为本书的读者,你可能是一位 Java EE 7 开发者,并参与编程和测试的各个方面。我们必须在我们的数字团队中的工程角色中保持清晰,并且更重要的是,了解我们各自团队中其他成员的角色和关注点。尽职调查、投资回报、减少技术债务和上市时间真正指明了通往更大透明度、诚信和对他人尊重的道路。这是现代数字团队的首要指令,因此推动我们的表现向前发展。让我们看看下面的截图:

组织人员及其在现代数字团队中的角色
角色
让我们看看下一节中的角色。我们将角色分为不同的视角:开发、设计、架构和管理。
开发视角
在数字团队中有许多角色需要具备可行的软件开发技能,以及用编程逻辑解决问题并带有创意暗示的能力。
Java 工程师
由于你正在阅读这本书,你很可能已经熟悉了 Java 工程师的角色。作为软件开发者的角色意味着你有责任为数字应用程序编写产品代码的实现。我将进一步说明,这个角色包括编写单元测试、功能测试和集成测试。现代开发者遵循敏捷实践以提高效率。他们参与业务和领域逻辑、表示层、领域层和集成层。Java 工程师将与测试人员、界面开发人员和有时与业务分析师保持密切沟通。
界面开发工程师
界面开发人员负责编写数字应用程序前端客户端的 JavaScript 代码。这个头衔也适用于使用 iOS、Android 和其他平台编写代码的手机开发者。界面开发人员通常涉及 HTML5、CSS 和 JavaScript 编码。不可避免的是,他们也了解现代实践和最新的前端测试、调试和测试。
界面开发人员几乎只会在表示层工作。他们也可能帮助设计和向领域层架构师提供输入和想法。据称,苹果美国将他们的网络团队分为用户界面工程和服务器端工程,同时共享领域模型。
界面开发人员将与测试人员、Java 工程师和业务分析师沟通。他们肯定会与用户体验设计师、创意设计师和内容策略师保持密切接触,因为界面开发人员的工作之一是将设计转换为前端产品代码。
质量保证测试员
这个角色范围从在非敏捷环境中独立团队中的传统专用测试人员,他们在产品团队中寻找产品缺陷,到那些作为敏捷团队一部分并参与软件开发生命周期所有方面的测试人员。在数字环境中,测试人员可能负责在各种不同的设备上断言生产代码的质量:智能手机、平板电脑和台式电脑,甚至包括现代可穿戴计算机。测试人员还可能编写需要深度参与行为驱动设计规范的规范,以确保最终系统的质量高,符合整个团队的目标。
测试人员将与团队中的每个人沟通和交流,包括设计师。在熟练的敏捷团队中,他们通常是每个人都要给留下深刻印象的人,因为他们编写了完成的定义。
开发者测试中的软件
在测试中的软件开发者是一种质量保证测试员的变化形式,这个角色根据不同的观点是有争议的。最好的情况下,这个角色是一个流动性和有争议的角色,确保团队中的其他软件开发者编写足够的可测试和可验证的生产代码。因此,这个角色需要具备开发 Java 代码的能力;它可能还需要 JavaScript 能力和自动化测试。由于政治结果的影响,Java 和接口开发者是否编写了足够的质量测试,如单元测试、功能测试和集成测试,这个角色仍然是一个定义不明确的角色。这个角色可能包括测试倡导。
在开发者测试中,一个更广泛的角色可能包括维护和解决构建环境中的问题的责任,以及包括持续集成和持续部署的方面。
设计视角
如果你被邀请参加一个派对,你在那里遇到了另一个专业人士,并且你偶然问他们,他们做什么工作?那个人然后说他们也从事数字领域的工作,但作为设计师。他们日常的工作内容通常并不明确。在“设计师”这个总称下有许多不同类型的设计师。在这里,我们谈论的是软件应用的数字和创意设计师,而不是软件架构师和设计师。这个角色与其他行业的设计师(如汽车(汽车)、航空(飞机)、电子产品和工业产品(吸尘器和 iPod))也不同,因为他们与有形产品打交道。设计师有一个共同点:他们设计一个解决方案来解决一个没有正确或明确答案的问题。
在接下来的章节中,我们将探讨数字领域的设计师。
创意设计师
一个富有创造力的设计师有责任将数字概念展示或呈现给客户和利益相关者的需求。这个角色涉及到艺术能力以及运用图形设计、排版和现代标准方面的知识。
通常,一个创意设计师在一个团队中工作,他们支持他人并在团队环境中进行沟通以探索不同的想法是很重要的。与众不同和个性化是可以的;然而,项目的关键设计必须优先考虑,确保只有最好的图形设计被发布给利益相关者。
一个创意设计师了解流行的专业软件包,包括 Adobe PhotoShop、Fireworks、InDesign、Illustrator 和 Xara Design。
设计师,尤其是那些在数字代理机构工作的设计师,有很多沟通。因此,这个角色需要能够按时完成任务,并交付巧妙的创意概念,以区别于客户的竞争对手,并且有压力以细致入微的关注细节来表现。通常,与团队中的其他成员相比,他们可能需要更长的工作时间,以便交付新的数字工业概念。设计师会与可用性工程师、界面设计师和内容策略师紧密合作。
可用性体验工程师
可用性工程师的角色是为数字应用程序设计和发展引人入胜的用户体验。这个角色是关于增强产品的动觉情感和感觉。可用性工程师负责对设计的视觉构图进行实地测试。这可能意味着在内部或与外部可用性测试实验室合作,并构建交互设计的线框图。
用户界面(UI)和用户体验(UX)工程师/设计师的角色在职场中相对较新,因为企业一直致力于在多个设备屏幕上构建人机交互。这些角色因其专业能力而成为专门领域,并从通用目的创意设计师中分离出来。UX 角色的任务是识别人类层面的界面、元素和核心功能,以及视觉语言,以便帮助客户快速而熟练地进步,从而实现他们的最终目标。UX 工程师会生成原型、线框图,并向利益相关者和团队展示客户的旅程。由于这个角色的关键特征完全关于用户测试,他们还促进 A/B 测试,并在行为和设计语言方面进行大量研究。
UX 工程师通常与创意设计师、界面开发人员和质量保证部门频繁沟通。对于核心开发,他们还会与 Java 开发者沟通。UX 工程师还会直接与内容策略师讨论问题,以明确项目的整体设计语言。
内容策略师
内容策略师是一个编写内容设计的人。这个角色是关于审视一个项目交互的语言。它涉及规划文案写作、写作风格、如何控制语言传递的过程,并成为治理的来源。内容策略师的角色可以比作博物馆馆长、酿酒师或大师品酒师。内容策略师对书面语言有出色的掌握,这可能包括媒体内容、视频、电影制作和摄影,如果需要的话。
内容策略师的一个非常常见的角色是为整个项目奠定业务术语知识的基础。这个角色需要传达数字业务(或政府部门)的关键信息和主题。他们还负责确定搜索引擎优化的内容。策略师将与元数据、内容目的和网页写作一起工作。
在日常活动中,内容策略师与业务分析师和利益相关者紧密合作,以确定设计语言。
建筑视角
在数字领域,还有一些角色与应用程序的架构有关,而不仅仅是创意或软件设计。
数据科学家
近年来,数字领域出现了另一个角色,专门用于统计数据分析、数据建模和信息设计。这是专门数据科学家的职责,这是一个与所谓的大数据相关的新兴领域。数据科学家负责从可能是有结构或无结构的大数据集中提取信息和知识。简单来说,他们可以理解对普通读者来说可能只是噪音的信号。结果是数据可视化和模型,这些将被展示给关键业务利益相关者和管理层。
数据科学家通过应用科学和数学技术分析大量数据。他们可能会使用一些特定的计算机算法来深入了解数据集的模式。一些数据科学家可以编写计算机程序,尽管许多人熟悉统计分析软件包。他们通常拥有大学后的学位,如硕士或博士学位,以便在其角色中实践。
数据科学家可能独立于数字团队工作。他们可能为几个不同的团队工作。他们通常与 Java 工程师、商业和管理层沟通。
技术架构师
数字团队可能有一个特定的技术架构师,以确保应用程序满足某些非功能性要求。非功能性要求是目标软件的一个方面或特性,它不反映其功能操作。对案件工作人员的功能操作是工作人员被允许实时处理案件。非功能性要求(NFRs)是特性和标准,它们指定和描述系统架构的操作。操作特性被称为适应性、可用性、灵活性、可维护性、性能、可重用性、鲁棒性、可伸缩性、安全性和可测试性。通常,架构师必须在这些非功能性要求之间进行权衡。例如,他们考虑可用性与性能之间的权衡,并询问业务利益相关者哪个特性更重要,或者与架构达成妥协。
架构师负责确保最终应用程序按计划进行,并满足非功能性需求。通常会有大量的软件设计和领域模型分析。在软件中具有常见设计模式的经验可能会有所帮助,但一个好的架构师在决定特定策略之前,会非常详细地检查根本需求。
技术架构师直接与核心平台团队沟通:Java 工程师和界面开发者,尤其是在设计过程中。他们将促进团队讨论关于使用哪种编程语言以及是否应该使用开源库框架。架构师也与业务利益相关者和管理层保持联系。他们还可能与数据科学家交谈,以帮助检索或捕获信息进行统计分析。最后,实际操作的架构师预计将开发软件,并需要能够指导其他技术工程师的所有层面。
管理视角
一个项目需要良好的质量管理。你的经理是员工中的关键成员,他坐在业务利益相关者和工程师、测试员、设计师团队之间。他实际上是高级管理层和软件开发团队之间的缓冲器。
商业分析师和联络官
一些数字团队将专门为查看项目的业务需求而设有商业分析师。这种角色取决于项目的敏捷性。一些项目和公司取消了商业分析师的角色,并依赖敏捷的 Scrum 流程来获取需求知识。其他类型的公司,特别是那些实践瀑布软件开发方面,仍然有一个单独的人来弥合工程师和设计师之间的差距,并帮助在团队中建立更深入的领域知识:分析师。
商业分析师评估变更提案。他们定义了不明确且处于早期进展中的用户故事的范围和定义。他们收集信息和证据来评估项目的好处、成本和风险。分析师深入研究业务领域知识,以阐明最终影响项目的法律和商业条款和政策。因此,商业分析师是建立与利益相关者通用语言的关键。这是软件团队理解业务利益相关者需求以及反之亦然的对话。当需要时,分析师会与利益相关者、项目经理、开发经理和其他技术人员广泛沟通。
项目经理/敏捷大师
项目经理负责将产品交付给利益相关者和业务。有时项目经理也被称为产品负责人。在其他组织中,这个角色由两个人分担:开发经理和项目经理。通常,项目经理是认证的敏捷大师或者对敏捷流程有一些了解。
项目经理领导软件团队按时并在预算内交付产品。他们与利益相关者紧密合作,以定义产品交付。在敏捷项目中,他们通过运行各种活动来记录和设置背景故事。然后,项目经理通过组织冲刺规划会议、回顾、每日站立会议和运行迭代来扮演敏捷大师的角色。在冲刺结束时,他们设置展示和感知会议,让利益相关者有机会看到当前的进度、演示和交付的工作。
这个角色需要大量的沟通,以便持续了解项目细节,因为项目经理必须跟踪产品的进度。项目经理本质上依赖于日常站立会议期间的信息。因此,项目经理会定期与项目中的每个人交谈。如果有一个用户故事无法合理推进,团队成员会通知敏捷大师。这些用户故事被称为阻塞器。最好的项目经理专注于用户故事的更新,而不是实际工作和对个人施加压力。
数字发展经理
开发经理负责交付项目和开发团队的运作。在这个角色中,经理与其他部门或部门有密切的合作。他将参与销售和营销、零售、消费者金融、保险和人力资源人员。因此,这个人有责任负责业务的战略和运营。管理内部项目的经理最终将交付给公司的其他职能,而面向客户的项目的涉及与客户和利益相关者的深入对话和互动。
在这个管理角色中,个人将与财务规划一起工作;如果不涉及财务,他们将做出决策并模拟业务。因此,开发经理需要预算、组织规划和数值技能。
尽管开发经理对直接汇报者有总体权威的领导权,但他或她可能将日常责任委托给团队领导,特别是如果有多个并发进行的项目。在扁平化的组织结构中,开发经理可能将责任委托给有效的项目经理和敏捷大师,特别是如果个别项目作为可行的自我组织团队运作。最好的开发经理鼓励个人和整个团队的自然成长。
软件质量
在数字团队中,关键的可交付成果应该完全关乎质量。没有管理、纪律和人员,很难写出高质量的软件。团队必须小心避免走下孤立的道路,即所谓的委员会设计,这在商业上通常被称为康威定律(梅尔文·康威的社会学观察,可在en.wikipedia.org/wiki/Conway%27s_law找到)。
为了避免软件最终交付成果中的功能障碍,数字团队应该实践以下具有以下特征的行为:
-
在架构、设计和构建软件时,他们应该是有计划的。
-
他们应该旨在制定完全可管理的计划。
-
团队应该对软件交付的最终状态有非常明确的想法。每个人都清楚他们为谁编写代码吗?他们是如何实现的?为什么项目存在?
-
团队中的每个人都必须了解客户的旅程,因此,将客户的需求置于业务流程和工具之上。
-
他们应该确保数字软件交付在减少技术债务的同时,最大限度地利用技术。
如果团队正在使用 Java 6 或 Java EE 5 或 Java EE 6 的旧版本,那么在新项目中强烈建议升级。如果允许一个数字团队在没有变革力量的情况下使用过时的技术来构建新的软件,那么软件的质量将会更差。如果你有任何问题,可以考虑边界上下文的概念(埃里克·埃文斯的领域驱动设计,可在martinfowler.com/bliki/BoundedContext.html找到)。
阶级与形式
如果你是一个体育迷,那么你可能听说过这样一句话,阶级是永恒的,形式是暂时的。如果你想一个数字团队能够快速、熟练、准确地交付代码,那么你需要雇佣最优秀的人才。就像在体育中一样,寻找这些有才华的个人是昂贵且困难的。如果你在一个拥有这些灵魂的数字团队中,那么恭喜你!你是精英的一部分,并且已经在能够快速交付的环境中工作。我们大多数人并不这么幸运,尤其是如果你在超大城市以外的省份工作。精英开发、设计和架构是一种负担得起的奢侈。
因此,确保数字团队能够工作,特别是如果你只是一个开发者,以下事项请务必记住:
-
认识到数字团队也是由人组成的,他们会有家庭、朋友和生活中的起伏。
-
不要成为工作狂。给你的大脑和团队一个休息的机会。使用你的年度假期配额。
-
最好的创意、设计和建筑理念有时来自意想不到的地方:健身房淋浴、遛狗和午睡。
-
技术领导者、经理和利益相关者,请注意!组织项目时要包括缓冲时间。不要把任务和项目一个接一个地堆在一起。
-
企业层级底部的团队成员,请注意!不要成为胆小鬼。如果项目困难和具有挑战性,那么就沟通!尽早让团队的其他人知道问题,并向高级管理层提出反馈。
-
不要分心做多项任务。这行不通。一次专注于一件事情。
-
允许个人成长为未来的榜样,以促进成长:有机技术领导力。
-
给你和你的团队一个学习的机会,参加会议,结识其他行业人士,并接受额外培训。
记住,如果你想成为一名优秀的摇滚主音吉他手,你需要练习,练习,再练习。对于在地球上构建一些最佳软件应用的佼佼者来说,也是如此。
附录 D. 精选参考文献
这是一组关于数字软件开发精选参考文献的附录。它们被分为概念和主题。
服务交付
-
美国数字服务手册,美国政府与公民互动的服务标准,2014
playbook.cio.gov/ -
约翰·格特纳所著《奥巴马的隐形创业公司》,FastCompany,2015
www.fastcompany.com/3046756/obama-and-his-geeks -
白宫宣布美国数字服务手册,2014
www.whitehouse.gov/blog/2014/08/11/delivering-customer-focused-government-through-smarter-it -
英国政府 UK GOV.UK 服务手册描述了英国政府的敏捷服务交付政策
www.gov.uk/service-manual/agile -
英国政府数字服务(GDS)博客
gdsengagement.blog.gov.uk/ -
维基百科上的数字转型定义
en.wikipedia.org/wiki/Digital_transformation -
18F,建设 21 世纪数字政府,美国总务管理局
18f.gsa.gov/ -
政府即平台,英国 GDS 400 天交付的概念
gds.blog.gov.uk/2015/02/10/that-was-400-days-of-delivery/ -
重新构想银行:为什么金融服务需要数字化转型,Ben Rossi,《信息时代》
shar.es/1tuJ4D -
澳大利亚宣布数字服务,总理 Tony Abbot MP,
www.pm.gov.au/media/2015-01-23/establishment-digital-transformation-office
敏捷与领导力
-
Agile Coaching 博客,Rachel Davies
rachelcdavies.github.io/archive/ -
Rachel Davies 的《康威定律谁害怕?》,2015
rachelcdavies.github.io/2015/04/20/afraid-conways-law.html -
有机技术领导力——Gerald Weinberg,《成为技术领导者》,
leanpub.com/becomingatechnicalleader -
Martin Fowler 撰写的持续集成文章,2006
www.martinfowler.com/articles/continuousIntegration.html -
来自 Netflix 技术博客的持续交付文章
techblog.netflix.com/2013/08/deploying-netflix-api.html -
持续交付,JavaPosse Round-up 第 459 期播客,2014
javaposse.com/java-posse-459 -
团队组织结构,JavaPosse Round-up 播客,2013 年
javaposse.com/java-posse-446 -
《部落对敏捷可持续性的重要性》托马斯·梅洛奇和吉里·谢德纳,2013 年
www.tomandgeriscrum.com/2013/05/24/the-importance-of-tribe-for-agile-sustainability/ -
《重新设计组织》博客,作者布鲁斯·艾克尔,Java 大奖得主,2015 年
www.reinventing-business.com/2015/01/reinventing-organizations.html -
《自信》文章,作者丽莎·克里平,敏捷教练,博客,2015 年
lisacrispin.com/2015/08/04/confidence/ -
《测试驱动开发》,作者肯特·贝克、沃德·坎宁安等
c2.com/cgi/wiki?TestDrivenDevelopment -
Java 测试驱动开发书籍,作者维克托·法尔西奇、亚历克斯·加西亚,Packt 出版,2015 年
www.packtpub.com/application-development/java-test-driven-development -
《以 Java 为视角的行为驱动开发》,作者约翰·弗格森,2013 年
weblogs.java.net/blog/manningpubs/archive/2013/06/10/introducing-behavior-driven-development
架构
-
《微服务》博客文章,作者马丁·福勒,2013 年
martinfowler.com/articles/microservices.html -
《伟大的微服务与单体架构辩论》,高可扩展性,2014 年
highscalability.com/blog/2014/7/28/the-great-microservices-vs-monolithic-apps-twitter-melee.html -
《微服务——现实检查》(点),作者安德鲁·哈梅尔-劳,凯捷集团,2014 年
capgemini.github.io/architecture/microservices-reality-check/ -
《构建可扩展的 Web 架构和分布式系统》,作者凯特·松达拉,Dr. Dobb's Journal,2012 年
www.drdobbs.com/web-development/building-scalable-web-architecture-and-d/240142422 -
《API 最佳实践》博客,作者 Apigee,2013 年及以后,接口的 RESTful 设计
blog.apigee.com/taglist/restful -
RESTful 网络服务:基础,作者亚历克斯·罗德里格斯,IBM,2014 年
www.ibm.com/developerworks/library/ws-restful/ -
《表示状态转移(REST)》作者罗伊·菲尔德,2000 年
www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm -
Shekhar Gulati 所著的《水平应用扩展的最佳实践》,2013 年
blog.openshift.com/best-practices-for-horizontal-application-scaling/ -
Ilya Grigorik 所著的《高性能浏览器网络》一书,出版社:O'Reilly,2014 年,由 Chimera 赞助
www.liquibase.org/
用户界面
-
Paul Boag 的《与网页设计师合作的 10 个技巧》;Boagworld 网站;2013 年
boagworld.com/design/working-with-web-designers/ -
一篇来自 Good UI 博客的《优秀用户界面》,2015 年,
www.goodui.org/ -
苹果公司的《UI 的做与不做》,2015 年,专注于 iOS 和移动开发,但具有广泛的吸引力
developer.apple.com/design/tips/ -
以用户为中心的设计,维基百科定义
en.wikipedia.org/wiki/User-centered_design -
美国卫生与公众服务部关于提升用户体验的建议,2015 年
www.usability.gov/ -
以用户为中心的设计,GOV.UK,2014 年
www.gov.uk/service-manual/user-centred-design -
Kristina Halvorsson 和 Melissa Rach 所著的《内容策略》,第 2 版,2015 年
contentstrategy.com/ -
Brendan Eich 的《JavaScript 博客》,JavaScript 的发明者和 Mozilla 项目的共同创始人
brendaneich.com/ -
Smashing Magazine,关于 HTML、CSS 和 JavaScript 开发者的相关新闻和文章
www.smashingmagazine.com/ -
Josh on Design,Java 冠军 Joshua Marinacci 的博客观点,前 HP Palm 平板电脑 Java 和 Web UI 设计师
joshondesign.com/ -
Six Revisions 是一个面向界面设计师和开发者的新网站
sixrevisions.com/ -
Jasmine 行为驱动 JavaScript 2.3 测试
jasmine.github.io/ -
Douglas Crockford 所著的《JavaScript:良好的部分》,O'Reilly 书籍,2008 年 5 月出版
javascript.crockford.com/ -
Stoyan Stefanov 所著的《面向对象的 JavaScript 编程》,Packt 出版社,2013 年出版
www.packtpub.com/web-development/object-oriented-javascript-second-edition -
UI Bootstrap,纯 AngularJS 框架编写的 Bootstrap 组件
angular-ui.github.io/bootstrap/ -
Foundation,一个用于响应式设计的分层 UI 框架
foundation.zurb.com/ -
Grunt JS,JavaScript 任务运行器
gruntjs.com/ -
Ionic 框架,用于开发混合 HTML5 移动应用的开源 JDK
ionicframework.com/ -
Backbone JavaScript 客户端框架
backbonejs.org/ -
Ember JS,开源项目
en.wikipedia.org/wiki/Ember.js -
Dojo Toolkit JavaScript 客户端
dojotoolkit.org/ -
Google 的 Polymer 项目,HTML5 网络组件
www.polymer-project.org/1.0/ -
JSLint,由道格拉斯·克罗克福德创建的最古老的 JavaScript 代码检查工具
www.jslint.com/ -
ESLint,具有高级可定制功能的最新 JavaScript 代码检查工具
eslint.org/
Java EE 和 JVM 技术
-
Reza Rahman 的《响应式 Java EE——让我数数方法》,Oracle,2014
www.slideshare.net/reza_rahman/reactive-javaee -
Adam Bien 的《使用 CompletableFutures 和 JavaEE 7 进行异步处理》screencast,2015
www.youtube.com/watch?v=KUISpHbWib0 -
Josh Sureth 的《响应式编程模式》,Typesafe,JavaOne 2014
www.youtube.com/watch?v=tiJEL3oiHIY -
RX Java,响应式扩展框架的 Java 实现,2015
github.com/ReactiveX/RxJava/wiki -
Drop Wizard 度量库来自 Code Hale,捕获 JVM 和应用度量,2014
github.com/dropwizard/metrics -
更快的 XML Jackson 数据处理器和注解,JSON 开源项目
github.com/FasterXML/jackson-annotations -
Jackson JAX-RS 提供程序,用于 JSON、XML 和 YAML 数据格式
github.com/FasterXML/jackson-jaxrs-providers -
JAX-RS 参考实现 Jersey 现在更倾向于使用 MOXy 进行 JSON 的序列化和反序列化 POJOs
jersey.java.net/documentation/latest/media.html -
嵌入式容器和模块 WildFly Swarm(2015 年的 Alpha 版本)
wildfly.org/swarm/ -
Apache TomEE 7.0.0-SNAPSHOT
tomee.apache.org/download/tomee-2.0.0-snapshot.html -
Jersey,Java EE 7 中 JAX-RS 2.0 规范的参考实现
jersey.java.net/ -
RESTeasy,JBoss Red Hat 在 Java EE 7 中实现的 JAX-RS 2.0 规范
resteasy.jboss.org/ -
Payara,由专业支持的与 GlassFish 4.1 兼容的开源应用程序服务器
www.payara.co.uk/home -
Apache MyFaces CODI,CDI 的可移植扩展
myfaces.apache.org/extensions/cdi/ -
Spring Web Flow
projects.spring.io/spring-webflow/ -
Arquillian 集成测试框架
arquillian.org/ -
Selenium Web Driver,集成测试
www.seleniumhq.org/projects/webdriver/ -
Cucumber JVM,功能测试套件 Cucumber 的移植
github.com/cucumber/cucumber-jvm -
Seb Rose、Matt Wynne 和 Aslak Hellesoy 所著的《Cucumber for Java》一书,由 Pragmatic Bookshelf Press 出版,2014 年
pragprog.com/book/srjcuc/the-cucumber-for-java-book -
《Java 性能》由 Charlie Hunt 和 Binu John 著,出版社:Addison Wesley,2011 年
www.amazon.co.uk/Java-Performance-Addison-Wesley-Charlie-Hunt/dp/0137142528 -
《Vanilla Java》,Peter Lawrey 的博客,关于核心 Java 的性能
vanillajava.blogspot.co.uk/ -
《Java 8 Lambdas:面向大众的功能式编程》由 Richard Warburton 著,出版社:O'Reilly,2014 年
www.amazon.com/Java-Lambdas-Functional-Programming-Masses/dp/1449370772 -
Maurice Naftalin 的 Lambda FAQ,作者:Maurice Naftalin,博客
www.lambdafaq.org/ -
Nashorn:JVM 上的 JavaScript
openjdk.java.net/projects/nashorn/ -
Apache JMeter,用于测试应用服务器的负载性能
jmeter.apache.org/ -
《Java EE 7 开发者手册》由 Peter Pilgrim 著,Packt Pub,2013 年
www.packtpub.com/application-development/java-ee-7-developer-handbook -
《铁甲 Java:构建安全 Web 应用》由 Jim Manico 著,出版社:Oracle Press,2014 年
www.amazon.co.uk/Iron-Clad-Java-Building-Secure-Applications/dp/0071835881 -
Salted 密码散列 - 正确的做法,由 Defuse Security,2014 年
crackstation.net/hashing-security.htm -
Swagger,一个开源框架,用于描述和渲染 RESTful API
swagger.io/ -
Apache Shiro,一个开源框架,用于 Java 安全认证、授权和领域
shiro.apache.org/ -
LiquiBase,一个源代码控制 delta 包,用于更新数据库中的模式和信息
www.liquibase.org/ -
NetFlix Hystrix,一个用于隔离远程调用的延迟和容错开源库
github.com/Netflix/Hystrix -
IBM WebSphere WAS Liberty 应用程序服务器,适用于 Java EE 7
developer.ibm.com/wasdev/websphere-liberty/ -
Apache Kafka,一个高吞吐量的分布式消息系统
kafka.apache.org/ -
REST-Assured,一个用于测试和验证 RESTful 服务的 Java 领域特定语言
github.com/jayway/rest-assured.


浙公网安备 33010602011771号