Java-Web-开发学习手册-全-

Java Web 开发学习手册(全)

原文:Learn Java for Web Development

协议:CC BY-NC-SA 4.0

零、简介

这本书是为具有不同经验水平的现代 Java web 开发人员编写的。

学习 Java 编程语言是一项崇高的事业,但是在现实世界中,仅仅学习 Java 语言是不够的。Java 开发人员必须学习 Java EE,这是一个相关服务器端技术的集合,以便将他们的 Java 技能投入到任何实际应用中。

但是学习 Java EE 也是不够的。Java 语言和 Java EE 可能足以为同一个组织中的项目开发 web 应用,作为一种可重用性的手段,但是 web 上 Java 的多样化前景渗透着几个 Web 框架,如 Spring Web MVC,这使得开发更加容易;因此,Java web 开发人员必须了解这些 web 框架。

但是这还不够。在这篇介绍的第一行,我提到这本书是为现代 Java web 开发人员编写的。现代 Java 不仅仅是一种语言;它现在是一个完全优化的平台,因为其他几种语言,如 Groovy 和 Scala,称为 JVM 语言,现在运行在 Java 虚拟机(JVM)上。所有这样的 JVM 语言,尤其是 Groovy,都与 Java 有着密切的联系,不久你就会遇到 Java 和其他 JVM 语言协同工作的 web 应用。最雄心勃勃的项目将要求您使用这些 JVM 语言构建 web 应用。

这本书满足了现代 Java web 开发人员的所有需求。它是为初学者到中级开发人员设计的,并在 Web 上解释了 Java 的细节。例如,这本书非常适合那些知道 MVC 这样的技术,但还不明白它们是如何以及为什么改变了 web 应用构建方式的开发人员。

这本书也适用于那些想学习 JSF 2(与 Java EE 捆绑在一起)之外的框架的开发人员。这本书涵盖了四种类型的 web 框架:基于请求的、基于组件的、快速的和反应式的。在这四种类型中,本书涵盖了五种成熟的 web 框架:Struts 2、Spring Web MVC、JSF 2、Grails 2 和 Play 2。

此外,这本书面向那些没有 Java、Groovy 和 Scala 编程语言经验,但渴望创建 web 应用的开发人员。本书在附录中提供了这三种语言的要点。

Learn Java for web Development通过一个真实的书店应用展示了最流行的 Web 框架的优势,而不是简单地宣布一个 Web 框架是最好的。开发一个完整的真正的应用需要动态功能的无缝协作,而构建这样的组件的代码是人为设计的,而且过于复杂。这本书没有把重点放在开发这样的移动部件上,而是把注意力集中在利用每个 web 框架的优势上。

这本书的结构

这本书由八章组成,我将在接下来描述,加上前面提到的介绍 Java、Groovy 和 Scala 语言的三个附录。

第一章:Java Web 开发简介

第一章解释了塑造本书的主要目标,并强调了后续章节中出现的内容。本章首先讨论了 Java 领域的一个重大变化,它的含义,以及 Java 在今天到底意味着什么。本章随后讨论了构建现代 Java web 应用的三个主要参与者:JVM 语言、Java EE 和 Java web 框架。

本章介绍了现代 Java web 应用的关键特性,如 Ajax 和 REST、用于实时 web 应用的 WebSocket、用于反应式 web 应用的类型安全堆栈,以及用于响应式和单页 web 应用的客户端 MVC 框架。最后,本章介绍了现代 web 开发的一些重要方面,这些方面超出了本书的范围,如 Web 上的 Java 信息检索,并简要介绍了 Web 3.0 的核心组件,它仍然是一个开放的研究主题,即语义 Web。

第二章:使用 Servlets 和 JSP 构建 Web 应用

第二章从讨论 web 应用的发展和架构开始。本章接着强调了如何使用标准的 web API。示例应用的第一步仅使用 servlets 和 JSP。然后,本章将向您展示如何构建与 Model 2 应用相同的应用。

第三章:Java EE Web 开发的最佳实践

第三章分析了导致需要遵循最佳实践的因果链。本章解释了开发项目的必要性,并介绍了表达式语言和 JSTL。本章随后讨论了 Java EE web 层模式。

第四章:使用 Struts 2 构建一个 Web 应用

在第四章中,您将了解 Struts 2。Struts 2 已经不像以前那样流行了,在本书中,Struts 2 被介绍给那些必须维护遗留应用的开发人员。本章首先介绍 Struts 2 的关键架构组件。然后您将学习使用 Struts 2 和 Maven 4 开发您的第一个应用。接下来,您将学习开发书店应用并与 Tiles 3 集成。

第五章:用 Spring Web MVC 构建 Java Web 应用

第五章解释了 Spring 框架的三个关键目标:使用依赖注入的松散耦合,使用 AOP 处理横切关注点,以及使用 Spring 模板移除样板代码。阐明 Spring 3 如何工作,本章介绍了 Spring Web MVC 架构。然后,您将学习使用 Spring 3 web MVC 构建您的第一个 Web 应用。本章还向您展示了如何构建书店应用。你将学会使用最新版本的 SpringSource 工具套件。

第六章:使用 JSF 的基于组件的 Web 开发

第六章向您介绍一个基于组件的框架,名为 JSF 2,它与 Java EE 捆绑在一起。在你熟悉了第四章和第五章中提出的基于请求的框架之后,理解《JSF 2》将会容易得多。这一章向你展示了 JSF 新协议如何代表了 web 开发中的一个范式转变,并向你介绍了 JSF 新协议体系结构的关键组件。在你牢固掌握了体系结构组件之后,本章将向你展示如何开发你的第一个 JSF 2 应用,同时你将学习 JSF 2 应用的生命周期阶段。然后,本章向您展示了如何将 JSF 2 与 Spring 框架集成,以便您可以通过 Spring 模板从 JSF 2 web 层访问数据库。最后,本章向您展示了如何开发书店应用。

第七章:使用 Grails 进行快速 Web 开发

Grails 是一个快速的应用开发框架,可以让您在创纪录的时间内创建 web 应用。第七章向您介绍了用 Grails 生成 web 应用的两种技术:静态和动态搭建。然后,本章将带您浏览生成的代码,并一步一步地解释代码是如何工作的。给出生成的代码后,本章将向您展示如何使用 Grails 2 开发书店应用。本章还介绍了单元测试,这是一个在应用开发中经常被忽视的任务。本章向您展示了如何使用 JUnit 测试框架为您的 web 应用构建测试。然后,本章将向您展示如何使用内存数据库 H2。在本章中,您还将学习使用最新版本的 Groovy-Grails 工具套件。

第八章:玩转 Java 和 Scala

第八章介绍了 Typesafe 堆栈的关键 web 播放器 Play 2 框架,并解释了 Typesafe 堆栈如何提供 Java EE 的替代方案来构建基于 Java 和 Scala 的应用。首先,您将学习使用 Play 2 开发一个基于 Java 的 web 应用。然后您将学习使用 Play 2 开发一个基于 Scala 的 web 应用。随后,本章将展示如何在行动 2 中使用该模型和访问数据库。

一、Java Web 开发简介

一旦被一个新的想法拉伸,头脑就再也不会回到它最初的维度。

拉尔夫·瓦尔多·爱默生 Ralph Waldo Emerson

智能机器是一种能扩展其想象力的机器。这方面的一个例子是名为 invokeDynamic, 1 的指令,它是在 Java 7 中引入的,用于优化动态类型语言在 Java 虚拟机(JVM)上的性能。最初为 Java 设计的 JVM 现在可以托管无数的编程语言,包括 Groovy 2 和 Scala。 3 这导致了 Java web 开发的复兴。这种交叉授粉的新范例和多样化的、有根据的选项在 Java 生态系统中开辟了许多利基市场,导致了比以往任何时候都更加丰富的 web 景观。

开源社区利用运行在 JVM 上的语言所提供的多平台能力,通过 web 框架,极大地提高了 web 开发的效率。Java EE 4 推进了这一势头,由 Spring、 5 等 Java 框架通过标准化和改进 API 和运行时环境开创了这一势头。此外,lambdas 形式的函数式编程结构已经被添加到 Java 8 中。因此,Java 正在反弹,成为一个超级解决方案。

本章通过介绍构建现代 Java web 应用的三个主要参与者为本书做准备:JVM 语言、Java EE 和 Java web 框架。

注意 JVM 语言代表了在 JVM 上运行的一种新的语言类别。有了最新版本 Java 8,Java 不再是一种特权 JVM 语言,现在只是运行在 JVM 上的许多语言之一。

本章首先介绍 JVM 语言,然后介绍 Java EE。Java EE 平台是一组 API 规范,充当开发 web 应用的构建块。然后这一章强调了 Java web 框架,这将是本书从第四章开始的主题。

JVM 语言

JVM 是一个运行时环境,它使您能够使用不同的编程语言来构建 web 应用。JVM 语言可以大致分为两种类型:为 JVM 设计的语言和移植到 JVM 的现有语言。

为 JVM 设计的语言

许多语言是专门为 JVM 设计的;表 1-1 描述了其中的几个。除了 Clojure 之外的所有内容都在本书中讨论。

表 1-1 。为 JVM 设计的语言

|

为 JVM 设计的语言

|

描述

|
| --- | --- |
| Clojure 6 | Clojure 是一种动态类型的函数式语言。 |
| 绝妙的 | Groovy 是一种动态的编译语言,语法类似于 Java,但更灵活。 |
| Java | Java 是一种静态类型的命令式语言。Java 的最新版本 Java 8 支持函数式编程的各个方面。 |
| 斯卡拉 | Scala 是一种静态类型的编译语言,支持函数式编程的各个方面,并执行大量的类型推断,很像一种动态语言。 |

以下是一些重要的定义:

  • 动态类型化 :动态类型化通过携带变量中保存的值的类型信息来跟踪关于变量包含何种值的信息。
  • 静态类型 :在静态类型中,类型信息都是关于变量的,而不是变量中的值。
  • 命令式语言 :这些语言中的指令可以改变语言的状态。
  • 函数式语言 :在函数式语言中,函数像在过程式语言中一样对值进行操作,但函数不是改变状态,而是返回新值的纯数学函数。

图 1-1 显示了 Java 8、Groovy、Scala 和 Clojure 在函数式语言连续体中的位置。Java 8 引入了 lambdas,这使得它稍微有点函数性,Groovy 从一开始就有函数构造,在 Groovy 2.0 中功能性更强,Scala 是三种面向对象(OO)语言中功能性最强的。另一方面,Clojure 是一种纯函数式的非 OO 语言。

9781430259831_Fig01-01.jpg

图 1-1 。 JVM 语言的功能分级

注意在图 1-1 中,没有提到 Groovy、Scala 和 Clojure 的版本号,因为 Java 只支持从 Java 8 开始的函数式编程。

移植到 JVM 的语言

JRuby、Jython 和 Rhino 是现有语言的几个主流 JVM 实现。表 1-2 描述了它们。

表 1-2 。移植到 JVM 的语言

|

移植到 JVM 的语言

|

描述

|
| --- | --- |
| JRuby 7 | JRuby 是 Ruby 编程语言的 JVM 重新实现。Ruby 是一种动态类型的 OO 语言,具有一些功能特性。 |
| jython8 | Jython 是 Python 在 JVM 上的重新实现,所以它是一种动态语言。 |
| 犀牛 9 | Rhino 在 JVM 上提供了 JavaScript 的实现。JavaScript 是一种动态类型的面向对象语言。 |

这本书基于一些专门为 JVM 设计的主流面向对象 JVM 语言,即 Java、Groovy 和 Scala。

Java EE

Java 最初是一种为构建独立应用而设计的编程语言,并迅速发展到其他领域。Java 的流行很大程度上可以归功于它在创建 web 应用中的使用。web 应用由静态和动态(交互式)网页组成。静态网页 包含各种类型的标记语言(HTML、XHTML 等),通常用于提供信息;动态网页 另一方面,能够在附加 web 组件的帮助下生成内容(在第二章中介绍)。因此,网络应用是网页的集合,并且能够响应请求生成动态内容。与仅用于提供信息的网页不同,web 应用允许您执行一些活动并保存结果。然而,开发 web 应用与构建独立的应用有着本质的不同,需要您理解以下三个关键要素:

  • Java EE 平台 :这是一组 API 规范,是 web 应用的构建块。
  • web 容器:web 容器实现 Java EE 平台的 API 规范。具体来说,web 容器提供了用于管理和执行 web 组件的服务,比如 servlets、JSP、过滤器、监听器和向客户端呈现响应。web 容器包含在第二章中。

注意有几种类型的容器,但是本书将集中讨论主要用于 web 应用的 web 容器。您必须根据您想要开发的应用的类型来选择容器。

  • Web 组件:这些组件由容器托管。这些 web 组件,比如 servlets、JSP、过滤器和监听器,将在第二章的中介绍。

Java EE 平台

Java EE 平台由以下两个目标驱动 :

  • 提供作为 web 应用构建块的 API 规范。
  • 标准化和降低企业应用开发的复杂性。它通过提供一个应用模型来实现这一点,该模型定义了将服务实现为多层应用的架构。

图 1-2 总结了 Java EE 的发展,为了简洁起见,只显示了每个版本中添加的新规范。

9781430259831_Fig01-02.jpg

图 1-2 。 Java EE 的演变

注意 修剪(也称为标记为删除)由一个建议的特性列表组成,这些特性可能会在下一个 Java EE 版本中删除,以减小平台的大小或防止其膨胀。

Web Profile 的目标是允许开发人员使用适当的技术创建 Web 应用。

Java EE 平台旨在通过提供一个应用模型来标准化和降低企业应用开发的复杂性,该应用模型定义了将服务实现为多层应用的体系结构。在多层应用中,应用的功能被分成不同的功能区域,称为。图 1-3 展示了 Java EE 应用模型中典型的多层架构 。

9781430259831_Fig01-03.jpg

图 1-3 。Java 中的多层架构

客户端层

客户机层是多层 Java EE 架构中的顶层;它由向 Java EE 服务器发出请求的应用客户机组成,javaee 服务器通常位于不同的机器上。服务器处理请求并向客户端返回响应。客户端的一个例子是 web 浏览器或独立应用。

Web 层

web 层由处理客户端和业务层之间交互的组件组成。从客户端收到请求后,web 层执行以下操作:

  1. 收集来自客户端的输入
  2. 控制客户端上屏幕或页面的流动
  3. 维护用户会话的数据状态
  4. 从业务层中的组件获取结果
  5. 为客户端生成各种格式的动态内容

如图图 1-2 所示,Java EE 7 中增加了一个新的 Web Profile 规范。 10 表 1-3 列出了 Web Profile 规范中包含的技术。如前所述,Web Profile 的目标是允许开发人员使用适当的技术创建 Web 应用。

表 1-3 。Web Profile 7 规范

|

规格

|

版本

|

统一资源定位器

|
| --- | --- | --- |
| 联合打击战斗机 | Two point two | JCP . org/en/JSR/detail?id=344 |
| JSP | Two point three | JCP . org/en/JSR/detail?id=245 |
| 标准标记库(JSP Standard Tag Library) | One point two | JCP . org/en/JSR/detail?id=52 |
| 小型应用 | Three point one | JCP . org/en/JSR/detail?id=340 |
| WebSocket | One | JCP . org/en/JSR/detail?id=356 |
| 表达语言 | Three | JCP . org/en/JSR/detail?id=341 |
| 哦,我的上帝 | Three point two | JCP . org/en/JSR/detail?id=345 |
| 作业的装配区(JobPackArea) | Two point one | JCP . org/en/JSR/detail?id=338 |
| JTA | One point two | JCP . org/en/JSR/detail?id=907 |
| Bean 验证 | One point one | JCP . org/en/JSR/detail?id=349 |
| 受管 Beans | One | JCP . org/en/JSR/detail?id=316 |
| 截击机 | One point two | JCP . org/en/JSR/detail?id=318 |
| 上下文和依赖注入 | One point one | JCP . org/en/JSR/detail?id=346 |
| Java 的依赖注入 | One | JCP . org/en/JSR/detail?id=330 |
| 对其他语言的调试支持 | One | JCP . org/en/JSR/detail?id=45 |
| JAX-RS 啊 | Two | JCP . org/en/JSR/detail?id=339 |
| JSON-P | One | JCP . org/en/JSR/detail?id=353 |

关于表 1-3 中列出的网页简介规格:

  • 在 Java EE 7 中,没有对 JSP 和 JSTL 进行任何更改,因为这些规范还没有更新。
  • 表达式语言已经从 JSP 中移除,现在有了自己的 JSR (341)。
  • Servlets 和 JSF 都得到了更新。
  • WebSocket 1.0 是在 Java EE 7 中引入的。

这本书专注于 Java EE 的 web 层;我们将在第二章中深入探讨 web 层。

Java EE 的多层架构对 Java 企业应用的开发有着巨大的影响。 Java 企业应用 可以定义为利用 Java EE 提供的企业服务的 Java 应用。事实上,如果一个 web 应用以打包在 web 层中的组件的形式利用 Java EE 服务,那么它可以被归类为企业应用。如图 1-3 所示,Java EE 通过提供一个构建 Java 企业应用的应用模型,将这些服务从功能上隔离到不同的层中。因此,Java 企业应用反映了 Java EE 的多层架构。图 1-4 展示了一个典型的 web 应用层的一般视图。

9781430259831_Fig01-04.jpg

图 1-4 。一个企业应用中各层的综合视图

图 1-4 中的每一层都是关注的一个区域,用于应用。例如,web 层只处理使用 Java EE 的 web 层组件。在一个应用中拥有不同的层会导致所谓的关注点分离。就实现而言,这种关注点的分离是使用粗粒度接口实现的。

关注点是应用开发人员需要关注的特性、功能或业务功能。横切这样的关注点在复杂系统中是固有的,并导致代码分散,这是当一个关注点的代码跨越许多模块时,以及代码缠结,这是当一个模块中的代码集中处理多个关注点时。代码分散和代码纠缠导致缺乏清晰性、冗余性、刚性和持续重构。图 1-5 说明了日志、事务和安全的系统服务如何横切应用的业务功能。

9781430259831_Fig01-05.jpg

图 1-5 。涉及系统服务的图书服务

图 1-5 中的 BookService 与系统服务过于相关。每个对象都知道并负责日志记录、安全性和事务。例如,在 BookService 中购买一本书的方法应该只关心如何购买这本书,而不关心它是安全的还是事务性的。关注点分离是软件工程的主要目标之一,它允许您单独处理每个服务,从而完成以下任务:

  • 在系统的整个生命周期中,促进系统中工件内部和之间的可追溯性
  • 控制由变化引起的影响,从而提供进化和非侵入性适应的范围
  • 促进内聚单元的开发,从而促进重用

关注点分离

术语关注点分离 (SoC) 是 Edsger W. Dijkstra 在他的论文《论科学思想的作用》中创造的 11 迪杰斯特拉在以下条款中解释道:

让我试着向你解释,对我来说,什么是所有智能思维的特征。那就是,一个人愿意为了自己的一致性而孤立地深入研究他的主题的一个方面,始终知道自己只专注于其中的一个方面。我们知道一个程序必须是正确的,我们只能从这个角度来研究它;我们也知道它应该是高效的,可以说我们可以改天再研究它的效率。在另一种情绪下,我们可能会问自己,这个项目是否值得,如果值得,为什么值得。但是什么也得不到——相反!—通过同时处理这些不同的方面。这就是我有时称之为“关注点分离”的方法,即使不完全可能,但据我所知,这是有效整理一个人思想的唯一可行的方法。这就是我所说的“将一个人的注意力集中在某个方面”:这并不意味着忽略其他方面,这只是公正地对待这样一个事实,即从这个方面的观点来看,其他方面是不相关的。这是一个和多个轨道的思想同时存在。

网页层

web 应用的 web 层由 Java EE 的 web 层组件组成,如 servlets 和 JSP。web 层可以访问服务层,但是 web 层和服务层之间不应该有紧密的耦合。也就是说,更改服务层不会影响 web 层。

服务层

服务层由 Java EE 的业务层组件组成,比如 Enterprise JavaBean s(EJB)。服务层可以访问数据访问层,但是服务层和数据访问层之间不应该有紧密的耦合。事实上,服务层不应该知道任何关于 web 或数据访问层的事情。服务层为 web 层提供了粗粒度的接口。

数据访问层

数据访问层由 Java EE 的数据层组件组成,如 JDBC 和 JPA。这一层不应该包含任何业务逻辑。该层通过向服务层提供粗粒度接口,从服务层抽象出实际的持久性机制(换句话说,JDBC 或 JPA)。

注意这种架构的调用流程总是从顶层到底层。换句话说,服务层应该能够调用数据访问层,而不是相反。

在本章中,您将构建书店应用的数据访问层,并通过独立的 Java 应用对其进行查询。在第二章中,您将使用 Java EE 的 web 层组件(特别是 servlets 和 JSP)将这个独立的 Java 应用替换为 web 层。在本书中你将会用到这个数据访问层,从第四章开始,你将会通过使用不同的 web 框架重新构建 web 层来重复构建 web 应用。

Oracle 和 Java 社区进程(JCP)提供了标准化的企业组件,如果可以使用这些组件构建成功的企业应用,那么我们为什么还需要 web 框架呢?web 框架是用来做什么的?下一节将回答这些问题。

Java Web 框架

虽然 Java EE 在标准化企业基础设施、提供应用模型和提供足以开发 web 应用的组件方面做得很好,但是有两个主要问题与之相关。

  • 直接与 Java EE 组件交互通常会产生大量样板代码,甚至代码冗余。
  • 使用 Java EE 基础设施创建企业应用是一项艰巨的任务,需要大量的专业知识。通常参与创建企业 Java EE 应用的团队成员扮演着不同的角色,他们可能并不都具有满足 Java EE 标准的专业水平。

框架解决了这两个主要问题(以及在第三章中详细讨论的其他几个问题)。表 1-4 描述了你将在本书中学到的 web 框架。

表 1-4 。基于 JVM 的 Web 框架

|

Web 框架

|

语言

|

从下载

|
| --- | --- | --- |
| 支柱 2 | Java | struts.apache.org/download.cgi#struts2314 |
| 框架 | Java | www.springsource.org/spring-community-download |
| JSF 2 | Java | www.oracle.com/technetwork/java/javaee/downloads/index.html |
| Grails 2 | 绝妙的 | www.grails.org/download |
| 游戏 2 | Java 和 Scala | www.playframework.com/download |

既然您已经看到了构建现代 Java web 应用的三个主要参与者(JVM 语言、Java EE 和 Java web 框架),那么是时候深入研究 Java 的一些细节了。

下一节将介绍 Java,这样您就可以构建自己的第一个独立 Java 应用。由于这本书是以使用 Java 的 web 开发为中心,而不是关于 Java 作为一种编程语言,所以对 Java 的介绍是简短的——这足以帮助语言新手理解后面的章节。

Java 入门

一个 Java 应用是一个当你使用 Java 命令启动 JVM 时执行的计算机程序。在 Java 编程语言中,所有的源代码首先都是用。java 扩展。javac 编译器将源文件编译成。包含字节码指令的类文件。JVM 读取这些字节码指令,并将它们翻译成每台计算机执行的机器语言操作。通过使 JVM 在许多平台上可用,Sun 将 Java 转变成了一种跨平台语言。如图 1-6 所示,完全相同的字节码可以在任何开发了 JVM 的操作系统上运行。

9781430259831_Fig01-06.jpg

图 1-6 。跨平台 Java

因为 JVM 可以在许多不同的操作系统上使用,所以。类文件能够在 Windows、Unix、Linux 或 Mac OS 上运行。在接下来的部分,我将向您展示如何编译和运行您的第一个 Java 应用。但是首先您需要设置开发环境。

设置开发环境

Java 软件有两个发行版。

  • Java 运行时环境(JRE )
  • Java 开发工具包(JDK )

JRE 包括一个 JVM 和核心库;它本质上只是一个运行字节码的环境。JDK 包括 JRE、Java 编译器(javac)和其他工具——编写和编译 Java 程序所需的基本软件。

在开始编译和运行 Java 程序之前,您需要下载并安装 JDK,并配置一些系统环境变量。

本书大部分代码需要 Java 7,但部分代码基于 Java 8,所以你应该安装 Java 8。要获得最新版本的 JDK),请按照下列步骤操作:

  1. 在网络浏览器中打开www.oracle.com/technetwork/java/javase/downloads/index.html
  2. 单击下载 JDK 按钮。
  3. 按照网站提供的说明进行操作。
  4. 运行安装程序并接受任何默认值。

要确认您已经正确安装了 JDK,请在命令行上从您机器上的任何目录键入 javac。如果您看到如何正确运行 javac 的说明,那么您已经成功安装了它。

创建并运行您的第一个 Java 应用

本节演示了如何在 Windows 上创建、编译和执行一个简单的 Java 应用。每个 Java 应用都有一个作为程序起点的类(通常称为入口点)。清单 1-1 展示了一个 HelloWorld 入口点类。

清单 1-1 。一款 HelloWorld Java 应用

1.    public class HelloWorld {
2.    public static void main(String[] args) {
3.    System.out.println("Hello World.");
4.     }
5.    }
  • 第 2 行:第 2 行中的 main 方法使这个类成为入口点类。该方法接受输入并启动程序。

Java 应用的名称应该是入口点类的名称,保存 Java 类的文件必须与该类同名。因此,清单 1-1 中的 HelloWorld 类必须存储在一个名为 HelloWorld.java 的文件中。

注意每个 Java 应用只有一个 main 方法。

您使用 JDK 安装目录的 bin 目录中的 javac 程序来编译 Java 程序。假设您已经在计算机上编辑了 PATH 环境变量,那么您应该能够从任何目录调用 javac。要编译清单 1-1 中的 HelloWorld 类,请执行以下操作:

  1. 打开命令提示符,转到保存 HelloWorld.java 文件的目录。

  2. 键入以下命令:

    javac HelloWorld.java
    

如果一切顺利,javac 将在您的工作目录中创建一个名为 HelloWorld.class 的文件。

运行 Java 应用

要运行您的 java 应用,您必须使用 Java 程序,该程序是带有命令 java 的 JDK 的一部分。同样,添加了 PATH 环境变量后,您应该能够从任何目录调用 java。从您的工作目录中,键入以下内容:

java  HelloWorld

请注意,您不包括。运行 Java 应用时的类扩展。您将在控制台上看到以下内容:

Hello World.

用 IDE 开发 Java 应用

在本书中,您将使用 Eclipse Kepler 集成开发环境(IDE)。要下载软件,请遵循以下步骤:

  1. 在网络浏览器中打开www.eclipse.org/downloads/
  2. 按照网站提供的说明进行操作。
  3. 运行安装程序并接受任何默认值。

在 IDE 中创建您的第一个项目

启动 Eclipse 后,您可以创建一个新项目,如下所示:

  1. 从“文件”菜单中,选择“新建”,然后选择“项目”。将出现“新建项目”窗口。

  2. In the New Project window, double-click Java Project. The New Java Project window appears, as illustrated in Figure 1-7.

    9781430259831_Fig01-07.jpg

    图 1-7 。创建 Java 项目

  3. 在“项目名称”字段中输入 chapter1

  4. 单击完成。您可以在这里更改许多其他选项。然而,对于我们的目的来说,默认设置就可以了。

创建应用

要为您的第一个程序创建一个包含 main 方法的类,请按照下列步骤操作:

  1. Right-click the chapter1 project in the Eclipse Package Explorer, choose New, and then choose Class. The New Java Class window displays, as shown in Figure 1-8.

    9781430259831_Fig01-08.jpg

    图 1-8 。创建 Java 类

    一个将类分组在一起。在 Name 字段中,您可以键入类名,即 HelloWorld。选中提供 main 方法(public static void main(String args[]))的复选框。当你完成后,你应该有一个类似于清单 1-2 中的类。

  2. 点击“生成评论”这个很快会解释。

清单 1-2 。简单的 Java 应用

packageapress.helloworld;

/**
 * A Hello World Java application
 * @author Vishal Layka
 *
 */
public class HelloWorld {

    /**
     * Entry point
     * @paramargs
     */
    public static void main(String[] args){
        System.out.println("Hello World");
    }

}

现在,您可以通过单击工具栏中的“运行”按钮或从“运行”菜单中选择“运行”来运行应用。

然后,Eclipse 会在代码区域下显示一个控制台面板,显示程序的输出。在这种情况下,它说“你好,世界。”

Javadoc 注释

Javadoc 注释 以/**字符序列开始,以*/字符序列结束。编译器会忽略这些字符序列之间的所有内容。在 Eclipse 中,您可以通过选择类或方法名并按 Alt+Shift+J 来添加 Javadoc 注释。

要生成 Javadoc,在 Eclipse 中选择项目,选择项目菜单,点击 Generate Javadoc,如图图 1-9 所示。

9781430259831_Fig01-09.jpg

图 1-9 。生成 Javadoc

将会打开一个窗口(图 1-10 ),您可以在其中选择需要生成 Javadoc 的 Java 项目或其底层资源。还有其他几种选择;您可以选择是否为公共/私有 API 生成 Javadoc,等等。现在,在“Javadoc 命令”字段中配置 javadoc.exe 文件,浏览并选择应该生成 Javadoc 的目标文件夹。

9781430259831_Fig01-10.jpg

图 1-10 。生成 Javadoc

单击完成。在控制台上,您可以看到 Javadoc 生成的进度。图 1-11 显示了生成的 Javadoc。

9781430259831_Fig01-11.jpg

图 1-11 。HelloWorld 类的 Javadoc

现在,您将学习如何创建一个简单但功能强大的独立书店应用版本,您将在本书中使用它。

书店应用

这本书不是简单地宣称一个 web 框架是最好的,而是打算通过一个真实世界的书店应用来展示最流行的 web 框架的优势。开发一个完整的真正的应用需要动态功能的无缝协作,而构建这样的组件的代码是人为设计的,而且过于复杂。这本书没有把重点放在开发这样的移动部件上,而是把注意力集中在利用每个 web 框架的优势上。在整本书中,您将学习如何使用 Java EE 和 Java web 框架来构建书店 web 应用。在本章中,您将通过构建一个传统的独立 Java 书店应用迈出第一步。在第二章中,你将把单机应用转换成 web 应用。

在本书中,我将使用一个 web 应用案例研究来演示如何使用 servlets 和 JSP 以及不同的 web 框架(如 JSF、Struts 2、Spring web MVC)和快速 Web 开发框架(如 Grails 和 Play)来编写 Web 应用。该应用允许用户通过关键字查看和搜索书籍,通常是通过作者的名字或姓氏以及书名。

书店应用的数据模型

本节介绍了一个简单的数据模型,该模型将用于本书中的书店 web 应用。当需要时,我将在每章中逐步扩展这个模型。该模型是一个简单的图书数据库,由三个表组成。

  • 类别表存储不同类别的书籍;类别包括 Java、Scala 等等。
  • 图书表存储图书的详细信息,比如书名。
  • 作者表存储作者的详细信息。

每个类别可以有零本或多本书。例如,书店里可能没有或有更多属于 Java 类别的书籍。换句话说,Category 和 Book 表之间是一对多的关系。同样,每本书可以有一个或多个作者。换句话说,Book 和 Author 表之间是一对多的关系。图 1-12 中的实体关系图说明了这种关系。

9781430259831_Fig01-12.jpg

图 1-12 。数据模型的实体关系图

这个数据模型还不能用于生产,因为您可以在类别和图书之间建立多对多的关系,在图书和作者之间建立多对多的关系。我保持数据模型简单,这样数据模型的复杂性就不会妨碍学习构建 web 应用的技巧。然而,你可以,例如,在书和作者之间建立一个多对多的关系,如图 1-13 所示。

9781430259831_Fig01-13.jpg

图 1-13 。书籍和作者之间的多对多关系

BookAuthor 表的唯一目的是提供图书和作者之间的多对多关系。

如图图 1-13 所示,图书与图书作者之间是一对多关系,作者与图书作者之间是一对多关系。事实上,BookAuthor 表的唯一目的是提供图书和作者之间的多对多关系,换句话说,一个作者可以写很多本书,一本书可以有很多作者。

由于跨几个领域的 web 应用大量涌现,许多关系和非关系数据库如 NoSQL 12 已经出现。在本书中,我将使用 MySQL 13 ,因为它是使用最广泛的免费数据库管理系统(DBMS)。要安装 MySQL,请转到dev.mysql.com/downloads/并点击下载。可以下载 MySQL Server 5.5 或更新版本。你可以在dev.mysql.com/doc/refman/5.5/en/installing.html看到安装 MySQL 的说明。

要创建图书数据库,请使用以下命令:

create database books;

您需要使用以下命令指示 MySQL 在 books 数据库中创建表:

use books;

现在您可以使用清单 1-3 中的语句创建表格。

清单 1-3 。为书店创建桌子

CREATE  TABLE CATEGORY (
ID  INT NOT NULL  AUTO_INCREMENT ,
CATEGORY_DESCRIPTION  VARCHAR(20)  NOT NULL ,
PRIMARY KEY (ID)
);

CREATE  TABLE BOOK (
ID  INT NOT  NULL AUTO_INCREMENT,
CATEGORY_ID  INT  NOT  NULL ,
BOOK_TITLE  VARCHAR(60) NOT NULL,
PUBLISHER  VARCHAR(60) NOT NULL ,
PRIMARY KEY (ID) ,
CONSTRAINT  FK_BOOK_1  FOREIGN KEY (CATEGORY_ID) REFERENCES CATEGORY(ID)

);

CREATE  TABLE  AUTHOR (
ID  INT  NOT NULL AUTO_INCREMENT ,
BOOK_ID  INT  NOT  NULL ,
FIRST_NAME  VARCHAR(20)  NOT NULL ,
LAST_NAME  VARCHAR(20)  NOT NULL ,
PRIMARY KEY (ID) ,
CONSTRAINT FK_AUTHOR_1 FOREIGN KEY (BOOK_ID) REFERENCES BOOK (ID)
);

您可以使用显示表格命令验证创建的表格,如图图 1-14 所示。

9781430259831_Fig01-14.jpg

图 1-14 。数据库中的所有表

您也可以使用命令 describe 或 desc 检查表格的结构,如图图 1-15 所示。

9781430259831_Fig01-15.jpg

图 1-15 。表格的结构

现在使用 insert 语句填充这些表,如下所示:

insert into category (category_description) values ('Clojure');
insert into category (category_description) values ('Groovy');
insert into category (category_description) values ('Java');
insert into category (category_description) values ('Scala');

您可以验证填充的类别表,如图图 1-16 所示。

9781430259831_Fig01-16.jpg

图 1-16 。类别表中的所有类别

insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (1, 'Practical Clojure', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (2, 'Beginning Groovy, Grails and Griffon', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (2, 'Definitive Guide to Grails 2', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (2, 'Groovy and Grails Recipes', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (3, 'Modern Java Web Development', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (3, 'Java 7 Recipes', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (3, 'Java EE 7 Recipes', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (3, 'Beginning Java 7 ', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (3, 'Pro Java 7 NIO.2', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (3, 'Java 7 for Absolute Beginners', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (3, 'Oracle Certified Java Enterprise Architect Java EE7', 'Apress');
insert into Book (CATEGORY_ID, BOOK_TITLE, PUBLISHER) values (4, 'Beginning Scala', 'Apress');

您可以验证如图图 1-17 所示的已填充的图书表。

9781430259831_Fig01-17.jpg

图 1-17 。图书表中的所有图书

insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (1, 'Luke', 'VanderHart');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (2, 'Vishal', 'Layka');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (3, 'Jeff', 'Brown');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (4, 'Bashar', 'Jawad');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (5, 'Vishal', 'Layka');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (6, 'Josh',  'Juneau');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (7, 'Josh', 'Juneau');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (8, 'Jeff', 'Friesen');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (9, 'Anghel', 'Leonard');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (10, 'Jay',  'Bryant');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (11, 'B V', 'Kumar');
insert into Author (BOOK_ID, FIRST_NAME, LAST_NAME) values (12, 'David', 'Pollak');

您可以验证填充的作者表,如图图 1-18 所示。

9781430259831_Fig01-18.jpg

图 1-18 。作者表中的所有作者

书店应用的数据访问层

现在数据库已经准备好了,您将为应用构建数据访问层。数据访问层将通过 JDBC 从数据库中检索数据,并将结果集直接映射到 Java 对象中。这些 Java 对象是应用中的域对象,是数据库中表的 Java 表示。数据访问层负责以透明的方式与底层持久性机制进行交互,以便从数据库中存储和检索对象。这种透明性意味着数据访问层可以将持久化机制从普通的 JDBC 14 切换到 ORM 15 持久化技术如 Hibernate、 16 JPA、 17 等,而不影响数据访问层的客户端。这种透明性是通过数据访问对象(DAO)模式实现的,如图 1-19 所示。DAO 对象提供了到数据库或底层持久化机制的接口,从而从客户端抽象出底层实现。

9781430259831_Fig01-19.jpg

图 1-19 。道模式

DAO 将应用调用映射到持久性机制,并提供特定的数据操作,而不公开数据库的细节。DAO 接口抽象了从客户机(应用对象)访问数据的实现细节,并提供了客户机(应用对象)需要的特定于域的对象。

首先,您需要为数据库表的 Java 对象表示创建特定于领域的类。清单 1-4 、 1-5 和 1-6 分别显示了图书、作者和类别领域类。

清单 1-4 。型号:类别

package com.apress.books.model;

public class Category {
    private Long id;
    private String categoryDescription;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getCategoryDescription() {
        returncategoryDescription;
    }

    public void setCategoryDescription(String categoryDescription) {
        this.categoryDescription = categoryDescription;
    }

    public String toString() {
        return "Category - Id: " + id + ", Category Description: "
                + categoryDescription;
    }

}

清单 1-5 。型号:书本

package com.apress.books.model;

import java.util.List;
import com.apress.books.model.Author;

public class Book {
    private Long id;
    private Long categoryId;
    private String bookTitle;
    private List<Author> authors;
    private String publisherName;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getCategoryId() {
        return categoryId;
    }

    public void setCategoryId(Long categoryId) {
        this.categoryId = categoryId;
    }

    public String getBookTitle() {
        return bookTitle;
    }

    public void setBookTitle(String bookTitle) {
        this.bookTitle = bookTitle;
    }

    public List<Author> getAuthors() {
        return authors;
    }

    public void setAuthors(List272103_1_En authors) {
        this.authors = authors;
    }

    public String getPublisherName() {
        return publisherName;
    }

    public void setPublisherName(String publisherName) {
        this.publisherName = publisherName;
    }

    public String toString() {
        return "Book - Id: " + id + ", Book Title: " + bookTitle;
    }

}

清单 1-6 。型号:作者

package com.apress.books.model;

public class Author {
    private Long id;
    private Long bookId;
    private String firstName;
    private String lastName;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getBookId() {
        return bookId;
    }

    public void setBookId(Long bookId) {
        this.bookId = bookId;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String toString() {
        return "Author - Id: " + id + ", Book id: " + bookId + ", First Name: "
                + firstName + ", Last Name: " +lastName;
    }

}

现在让我们从 BookDAO 的一个简单接口开始,它封装了 web 应用访问的所有数据。清单 1-7 显示了 BookDAO 接口。

清单 1-7 。书道界面

 1.    package com.apress.books.dao;
 2.
 3.    import java.util.List;
 4.
 5.    import com.apress.books.model.Book;
 6.    import com.apress.books.model.Category;
 7.
 8.    public interface BookDAO {
 9.        public List<Book>findAllBooks();
10.
11.        public List<Book>searchBooksByKeyword(String keyWord);
12.
13.        public List<Category>findAllCategories();
14.
15.        public void insert(Book book);
16.
17.        public void update(Book book);
18.
19.        public void delete(Long bookId);
20.
21.    }
  • 第 9 行:这是 findAllBooks()方法,用于列出数据库中的所有书籍。
  • 第 11 行 : SearchBooksByKeyword(字符串关键字)允许用户通过书名中的关键字或者作者的名和姓来搜索书籍。
  • 应用需要 findAllCategories()来提供图书的分类列表。

该接口中的方法对应于应用的 CRUD 术语(换句话说,创建、读取、更新和删除)。清单 1-8 展示了 BookDAO 接口的实现。

清单 1-8 。BookDAO 接口的实现

 1.    package com.apress.books.dao;
 2.
 3.    import java.sql.Connection;
 4.    import java.sql.DriverManager;
 5.    import java.sql.PreparedStatement;
 6.    import java.sql.ResultSet;
 7.    import java.sql.SQLException;
 8.    import java.sql.Statement;
 9.    import java.util.ArrayList;
10.    import java.util.List;
11.
12.    import java.apress.books.model.Author;
13.    import java.apress.books.model.Book;
14.    import java.apress.books.model.Category;
15.
16.    public class BookDAOImpl implements BookDAO {
17.
18.        static {
19.            try {
20.                Class.forName("com.mysql.jdbc.Driver");
21.            } catch (ClassNotFoundException ex) {
22.            }
23.        }
24.
25.        private Connection getConnection() throws SQLException {
26.            return DriverManager.getConnection("jdbc:mysql://localhost:3306/books",
27.                    "root", "password");
28.        }
29.
30.        private void closeConnection(Connection connection) {
31.            if (connection == null)
32.                return;
33.            try {
34.                connection.close();
35.            } catch (SQLException ex) {
36.            }
37.        }
38.
39.        public List<Book> findAllBooks() {
40.            List<Book> result = new ArrayList<>();
41.            List<Author> authorList = new ArrayList<>();
42.
43.            String sql = "select * from book inner join author on book.id = author.book_id";
44.
45.            Connection connection = null;
46.            try {
47.                connection = getConnection();
48.                PreparedStatement statement = connection.prepareStatement(sql);
49.                ResultSet resultSet = statement.executeQuery();
50.                while (resultSet.next()) {
51.                    Book book = new Book();
52.                    Author author = new Author();
53.                    book.setId(resultSet.getLong("id"));
54.                    book.setBookTitle(resultSet.getString("book_title"));
55.                    book.setCategoryId(resultSet.getLong("category_id"));
56.                    author.setBookId(resultSet.getLong("book_Id"));
57.                    author.setFirstName(resultSet.getString("first_name"));
58.                    author.setLastName(resultSet.getString("last_name"));
59.                    authorList.add(author);
60.                    book.setAuthors(authorList);
61.                    book.setPublisherName(resultSet.getString("publisher"));
62.                    result.add(book);
63.                }
64.            } catch (SQLException ex) {
65.                ex.printStackTrace();
66.            } finally {
67.                closeConnection(connection);
68.            }
69.            return result;
70.        }
71.
72.
73.        public List<Book> searchBooksByKeyword(String keyWord) {
74.            List<Book> result = new ArrayList<>();
75.            List<Author> authorList = new ArrayList<>();
76.
77.            String sql = "select * from book inner join author on book.id = author.book_id"
78.                    + " where book_title like '%"
79.                    + keyWord.trim()
80.                    + "%'"
81.                    + " or first_name like '%"
82.                    + keyWord.trim()
83.                    + "%'"
84.                    + " or last_name like '%" + keyWord.trim() + "%'";
85.
86.            Connection connection = null;
87.            try {
88.
89.                connection = getConnection();
90.                PreparedStatement statement = connection.prepareStatement(sql);
91.                ResultSet resultSet = statement.executeQuery();
92.                while (resultSet.next()) {
93.                    Book book = new Book();
94.                    Author author = new Author();
95.                    book.setId(resultSet.getLong("id"));
96.                    book.setBookTitle(resultSet.getString("book_title"));
97.                    book.setPublisherName(resultSet.getString("publisher"));
98.                    author.setFirstName(resultSet.getString("first_name"));
99.                    author.setLastName(resultSet.getString("last_name"));
100.                    author.setBookId(resultSet.getLong("book_id"));
101.                    authorList.add(author);
102.                    book.setAuthors(authorList);
103.                    result.add(book);
104.                }
105.            } catch (SQLException ex) {
106.                ex.printStackTrace();
107.            } finally {
108.                closeConnection(connection);
109.            }
110.
111.            return result;
112.        }
113.
114.        public List<Category> findAllCategories() {
115.            List<Category> result = new ArrayList<>();
116.            String sql = "select * from category";
117.
118.            Connection connection = null;
119.            try {
120.                connection = getConnection();
121.                PreparedStatement statement = connection.prepareStatement(sql);
122.                ResultSet resultSet = statement.executeQuery();
123.                while (resultSet.next()) {
124.                    Category category = new Category();
125.                    category.setId(resultSet.getLong("id"));
126.                    category.setCategoryDescription(resultSet
127.                            .getString("category_description"));
128.                    result.add(category);
129.                }
130.            } catch (SQLException ex) {
131.                ex.printStackTrace();
132.            } finally {
133.                closeConnection(connection);
134.            }
135.            return result;
136.        }
137.
138.        public void insert(Book book) {
139.        }
140.
141.        public void update(Book book) {
142.        }
143.
144.        public void delete(Long bookId) {
145.
146.        }
147.    }

清单 1-8 是 BookDao 接口的一个实现,用于与;这种交互包括连接到数据库,并通过纯 JDBC 选择、删除和更新数据。JDBC 提供了特定于每个数据库的驱动程序,并允许 Java 对数据库进行编码。

  • 第 18 到 37 行:这几行显示了管理 JDBC 连接所需的代码。
  • 第 26 行:getConnection()方法返回一个驱动实现的 java.sql.Connection 接口。这个接口允许您对数据库运行 SQL 语句。为此,您需要提供一个 MySQL 连接器/J JAR 文件。MySQL Connector/J 是一个本地 Java 驱动程序,它将 JDBC 调用转换成 MySQL 数据库可以理解的网络协议。DriverManager 管理驱动程序,并提供建立数据库连接的静态方法。

注意你可以从 http://dev.mysql.com/downloads/connector/j/下载 MySQL 连接器/J。将这个连接器 JAR 放在项目的类路径中。

  • 第 30 行到第 37 行:需要关闭连接,因为就应用的性能而言,连接是很昂贵的。
  • 第 39 到 144 行:这几行是 BookDAO 接口中 CRUD 服务的实现。
  • 第 67、108 和 133 行:您为 CRUD 服务中的每个语句创建了一个连接。您需要关闭这些连接;让它们保持打开状态会导致应用的性能下降。

数据访问层的客户端

现在您的数据访问层已经准备好了,您将使用独立的 Java 应用查询它。在第二章中,你将用一个网络应用替换这个 Java 应用。清单 1-9 展示了 Java 应用。

清单 1-9 。单机书店 Java App

 1.    package com.apress.books.client;
 2.    import java.util.List;
 3.
 4.    import com.apress.books.dao.BookDAO;
 5.    import com.apress.books.dao.BookDAOImpl;
 6.    import com.apress.books.model.Book;
 7.
 8.    public class BookApp {
 9.        private static BookDAO bookDao = new BookDAOImpl();
10.
11.        public static void main(String[] args) {
12.            // List all books
13.            System.err.println("Listing all Books:");
14.            findAllBooks();
15.            System.out.println();
16.            // search book by keyword
17.            System.err.println("Search book by keyword  in book title : Groovy:");
18.
19.            searchBooks("Groovy");
20.            System.out.println();
21.
22.            System.err.println("Search book by keyword  in author's name  : Josh:");
23.
24.            searchBooks("Josh");
25.
26.
27.        }
28.
29.        private static void findAllBooks() {
30.            List<Book> books = bookDao.findAllBooks();
31.            for (Book book : books) {
32.                System.out.println(book);
33.            }
34.        }
35.        private static void searchBooks(String keyWord) {
36.            List<Book> books = bookDao.searchBooksByKeyword(keyWord);
37.            for (Book book : books) {
38.                System.out.println(book);
39.            }
40.        }
41.    }

图 1-20 说明了独立应用的目录结构。

9781430259831_Fig01-20.jpg

图 1-20 。独立书店应用的目录结构

运行此应用会产生以下输出:

pg34.jpg

在下一章中,您将开发 web 层来代替这个客户端,并调用书店 web 应用中的数据访问层。

Java Web 领域的趋势和技术

现在是时候深入研究当今 Java web 领域的趋势和技术了。对于 Java 新手来说,这可能令人望而生畏,但目标是让您熟悉 Java web 应用开发中的工具、技术和趋势,以便让您对现代 Java 前景有一个初步了解。当您学习使用 Grails 2 和 Play 2 等快速 web 框架开发 web 应用时,您会发现这些工具和技术中的大多数都是现成的。

正如本章所提到的,JVM 最初是为 Java 设计的,现在可以支持无数的编程语言,包括 Groovy 和 Scala。作为这种新兴的多道程序设计范例的结果,现代 web 应用通常具有以下一种或多种特征:

  • 响应式 web 应用
  • 单页 web 应用
  • 实时网络应用
  • 反应式 web 应用
  • 混搭和 web 服务

响应式网络应用

网络最大的优势之一就是它的灵活性。然而,这种灵活性也是其最大的弱点。当在一个浏览器上测试的 web 应用在另一个浏览器上被查看并且不能正常运行时,这个弱点就显现出来了。随着智能手机的出现,这种跨浏览器兼容性问题越来越严重。如图 1-21 所示,截至 2013 年底,全球有 68 亿移动用户,到 2016 年这一数字将增长到 80 亿(【www.itu.int/ITU-D/ict/facts/index.html】??)。

9781430259831_Fig01-21.jpg

图 1-21 。移动订阅

Web 应用既可以在桌面上运行,也可以在智能手机上运行,但是为桌面和智能手机创建单独的 web 应用会带来巨大的开发和维护开销。2010 年 5 月,Ethan Marcotte 为 List Apart 写了一篇名为“响应式网页设计”的文章,定义了一种突破性的方法。他使用现有的工具(这将在本节稍后解释)创建了一个在不同设备上显示精美的网站,如图图 1-22 和图 1-23 所示。

9781430259831_Fig01-22.jpg

图 1-22 。Ethan Marcotte 的响应网站

9781430259831_Fig01-23.jpg

图 1-23 。智能手机上的同一个响应网站

web 应用是与设备无关的,因为它们可以适应运行它们的设备。这项技术甚至重新思考了页面布局的设计方式。您可以更进一步,在设计 web 应用时考虑最小的屏幕(智能手机),然后逐步增强应用以在桌面屏幕上运行。这种技术被称为移动优先设计。移动优先设计的理念是,如果你设计的界面和网络组件能够在智能手机上以可接受的性能运行,那么在桌面屏幕上的性能将会非常快。此外,明智地使用智能手机也将适用于桌面屏幕,这将使应用的可用性更好。

开发响应式 web 应用所采用的核心技术有 CSS3、18jQuery19和 jQuery 插件、 20 LESS、21coffee script、 22 以及 Bootstrap、 23 Polyfills

以下是响应式世界中的一些重要定义:

  • 不引人注目的 JavaScript24:不引人注目的 JavaScript 是一种分离关注点的手段,即把外观和感觉从行为关注点中分离出来。这就产生了一个纯标记,JavaScript(行为)在不同的浏览器和设备上不引人注目地工作。jQuery 是一个流行的库,有助于编写不引人注目的 JavaScript。CoffeeScript 编译成 JavaScript。它被用作 JavaScript 的替代品,并大大减少了代码。
  • CSS3 媒体查询 :媒体查询是让 web 应用做出响应的主要手段。媒体查询使用 CSS 文件中的媒体特征(如设备宽度、设备高度、方向和设备纵横比)来开发响应性 web 应用。
  • LESS : LESS 是 CSS3 样式表变得不可管理时使用的 CSS 预处理器。LESS 扩展了 CSS 的动态行为,比如混合和函数。
  • Polyfills : Polyfills 是 JavaScript,用于制作支持 HTML5 的浏览器。聚合填充提供了浏览器中缺少的功能,并提供了一个后备。
  • Modernizr : Modernizr 是一个 JavaScript 库,可以检测浏览器中的 HTML5 和 CSS3 特性,并有条件地加载 polyfills。

单页 Web 应用(SPA)

web 应用开发的另一个趋势是单页面 web 应用的出现。

  • 客户端代码——如 HTML、JavaScript 和 CSS——通过单个页面加载进行检索;在该过程中的任何时候都不会重新加载页面,并且控件也不会转移到另一个页面。
  • 资源(如图像)会动态加载并添加到页面以响应事件。

spa 是使用 Node.js 25 作为 web 服务器构建的。AngularJS 是一个功能齐全的 SPA 框架。

实时 Web 应用

一个实时 web 应用根据事件的性质,通过客户端和服务器之间的异步双向通信,在可测量和可接受的时间段内对事件做出响应。WebSocket 是开发实时 web 应用的核心技术。WebSocket 通过单一 TCP 连接提供全双工和双向通信协议。也就是说,客户端和服务器可以相互发送消息,并且相互独立。

需要实时功能的应用的几个例子是聊天应用、多人在线游戏、股票事务应用等等。

注意【WebSocket 的 Java API 定义为 JSR 356;参见表 1-3 。

反应式网络应用

反应式应用是一类新的应用,与传统的基于网络的应用有着本质的不同,由类型安全 26 反应式平台驱动。Typesafe 反应式平台是一套集成产品,包括 Play 2 框架、Akka、 27 和 Scala,以及用于命令和控制的 Typesafe 控制台。由于多核处理器以及它提倡异步和基于事件的编程,反应式编程变得至关重要。Play 框架是企业 Java 堆栈的替代方案。Play 是为现代 web 和移动应用的需求而构建的,利用了 REST、JSON 和 WebSocket 等技术。这些技术允许创建通过任何现代浏览器呈现的丰富、高度交互的用户界面,同时使并行呈现页面的各个部分以及进行部分页面更新或渐进式增强变得更加容易。

混搭和网络服务

mashup 是一个 web 应用,它使用来自多个来源的内容来创建一个显示在单一图形界面中的新服务。使用 mashups,您可以通过组合 web 服务来开发强大的应用。你可以在 www.programmableweb.com/apis/directory/1?sort=mashups 的找到流行的混搭 API。这一节将关注 web 服务,然后触及一个迷人的趋势,这仍然是一个研究领域:语义 Web。

一个 web 服务 是一个存储在一台机器上的软件组件,可以被另一台机器上的应用(或其他软件组件)通过网络访问。web 服务所在的机器被称为 web 服务主机。客户端应用通过网络向 web 服务主机发送请求,web 服务主机处理请求并返回响应。使 web 服务可用于接收客户端请求被称为发布web 服务;从客户端应用使用 web 服务被称为消费web 服务。

Web 服务使用 XML 和 JSON 28 等技术进行数据交换。

JavaScript 对象符号

JavaScript 对象符号(JSON) 用于表示数据,作为 XML 的替代。JSON 减少了 web 请求的负载,提高了 web 应用的整体性能。JSON 是一种基于文本的数据交换格式,用于将 JavaScript 中的对象表示为由字符串表示的名称-值对的集合。

JSON 处理在 JSR 353 中定义为 JSON 处理的 Java API 参见表 1-3 。

两个 Java APIs 促进了 web 服务:

  • JAX-WS :这是基于简单对象访问协议(SOAP), 29 这是一种基于 XML 的协议,允许 web 服务和客户端进行通信,即使客户端和 web 服务是用不同的语言编写的。
  • JAX-RS :这使用了表述性状态转移(REST),这是一种网络架构,使用 Web 的传统请求-响应机制,如 GET 和 POST 请求。

简单对象访问协议

简单对象访问协议(SOAP)是一个独立于平台的协议,它使用 XML 与 web 服务进行交互,通常是通过 HTTP。每个请求和响应都封装在 SOAP 消息中,SOAP 消息是包含 web 服务处理消息所需信息的 XML 标记。SOAP web 服务的工作方式如下:

  1. 当调用 SOAP web 服务的方法时,请求被封装在 SOAP 信封中的 SOAP 消息中,并发送到 web 服务所在的服务器。
  2. 当 SOAP )web 服务收到此消息时,它会解析表示消息的 XML,然后处理消息的内容。
  3. 然后,web 服务在处理完请求后,在另一个 SOAP 消息中将响应发送给客户端。
  4. 客户端解析响应。

代表性状态转移

罗伊·菲尔丁于 2000 年在加州大学欧文分校的博士论文 30 中引入并定义了术语表征状态转移 (REST) 。REST 指的是一种实现 web 服务的架构风格,称为 RESTful web 服务。RESTful web 服务中的每个方法都由唯一的 URL 标识。

注意 RESTful web 服务被定义为 JSR 339,如表 1-3 所示。

与 SOAP 不同,REST 执行以下操作:

  • 将资源标识为 URI
  • 使用一组定义良好的 HTTP 方法来访问资源
  • 使用资源的多种表示格式

语义网(Web 3.0)

Web 2.0 在 2004 年开始成形。它由谷歌开创,随后是视频分享、社交网络、微博、照片分享、维基百科等社交应用,以及第二人生等虚拟世界。混搭在社交应用和 Web 2.0 的发展中扮演了重要角色。

术语语义网指的是 W3C 对链接数据网的设想。语义网可以被看作是一套标准,允许机器理解网上信息的意思。今天的网络由本身没有意义的数据组成,而这些意义必须在从网络上收集数据后手工构建。语义 Web 技术使您能够在 Web 上创建数据存储,构建词汇表,并编写处理数据的规则。关联数据是通过 RDF、 31 SPARQL、 32 和 OWL 等技术实现的。语义网是网络的未来,也是一个正在研究的课题。我推荐艾伦·施瓦茨的一个可编程的 Web:一个未完成的工作 34 作为语义 Web 上的优秀资源。你也可以关注语义网上的最新消息。

摘要

本章介绍了 Java 语言,然后带您进行了一次旋风式的 Java 之旅。Java 世界的多样化景观由几个 web 框架(如 Struts 2、Spring Web MVC、JSF 2、Grails 2 和 Play 2)组成,这些框架使开发变得容易得多;因此,作为 Java web 开发人员,您需要熟悉这些 web 框架。现代 Java 不仅仅是一种语言;现在,它是一个针对其他几种行业优势语言(如 Groovy、Clojure 和 Scala)的完全优化的平台。所有这些语言,尤其是 Groovy,都与 Java 有着密切的联系,不久您将会遇到 web 应用,其中 Java 和这些替代的 JVM 语言将协同工作。

随后的章节将解决现代 Java web 开发人员的所有这些需求。具体来说,在下一章中,您将创建一个 Hello World web 应用,它利用了 web 应用的基本构件,即 servlets 和 Java 服务器页面。然后,您将把这个独立的应用转换成您的第一个成熟的 web 应用:一个使用 servlets 和 JSP 的书店应用。

Cr . open JDK . Java . net/√jrose/pres/2009 910-vml . pdf

2http://groovy.codehaus.org/】??

3【www.scala-lang.org/】??

4【www.oracle.com/technetwork/java/javaee/overview/index.html】??

5http://spring.io/】??

6http://clojure.org/】??

7http://jruby.org/】??

8【www.jython.org/】??

9T3developer . Mozilla . org/en-US/docs/Rhino _ documentation

10【www.oracle.com/technetwork/java/javaee/tech/index.html】??

11www . cs . ute xas . edu/users/EWD/transcriptions/ewd 04 xx/ewd 447 . html

12http://nosql-database.org/】??

13【www.mysql.com/】??

14【www.oracle.com/technetwork/java/overview-141217.html】??

15http://en.wikipedia.org/wiki/Object-relational_mapping】??

16【www.hibernate.org/】??

17www . Oracle . com/tech network/Java/javaee/tech/persistence-JSP-140049 . html

18【www.w3.org/Style/CSS/current-work.en.html】??

19http://jquery.com/】??

【20】【http://plugins . jquery . com/

21http://lesscss.org/】??

22http://coffeescript.org/】??

23http://getbootstrap.com/】??

24【www.w3.org/wiki/The_principles_of_unobtrusive_JavaScript】??

25http://nodejs.org/】??

26http://typesafe.com/】??

27http://akka.io/】??

28【www.json.org/】??

29【www.w3.org/TR/soap/】??

30www . ics . UCI . edu/∞fielding/pubs/dissertation/rest _ arch _ style . htm

31【www.w3.org/RDF/】??

32【www.w3.org/TR/rdf-sparql-query/】??

33【www.w3.org/TR/owl-features/】??

34www . morganclaypool . com/doi/pdf/10.2200/s 00481 ed 1v 01y 201302 wbe 005

二、使用 Servlets 和 JSP 构建 Web 应用

协议就是一切。

弗朗索瓦·朱利亚尼

核心互联网协议充实并支撑着 web,因此理解这些协议是理解 Web 应用如何开发的基础。

互联网是一个巨大的网络网络,一般来说,互联网上的所有机器都可以分为两类:服务器和客户端。客户端是请求一些信息的机器,而服务器是提供这些信息的机器。从信息提供者(即服务器)流向信息请求者(即客户端)的信息数据受一个明确的规则的约束,该规则管理服务器传输的信息的编组和客户端翻译或读取的信息的解组。这个规则被称为 协议。web 浏览器(即客户端)、web 服务器(即服务器)和 web 应用都通过超文本传输协议(HTTP)相互通信。 客户端向 web 服务器发送 HTTP 请求,web 服务器以 HTTP 响应的形式返回请求的数据。HTTP 客户端和 HTTP 服务器是万维网的基石,HTTP 是万维网的通用语言。

HTTP 是一种请求-响应无状态协议,其必然结果是,从 web 服务器的角度来看,任何请求都是来自 web 浏览器的第一个请求。当客户端请求资源时,该请求还以统一资源定位符(URL) 的形式包含所请求资源的标识。在 RFC 3986 1 中,URL 被描述为唯一标识资源的统一方式。URL 被设计成通过描述资源在网络上的“位置”来隐含地提供定位资源的方法。

URL 是统一资源标识符(URI)的一种具体形式,是一种区分实体的机制。但是 URIs 本身是抽象的。URI 有两种具体形式:URL 和统一资源名(URN)。骨灰盒仍然是实验性的,没有被广泛采用。

通用 URL 是由组件组成的分层序列,其结构为 scheme://hostName:port number/path/resource?查询字符串。

要识别 URL 的各个部分,请考虑一个列出书店网站上某本书的详细信息的 URL,如下所示:

http://www.yourbookstore.com/bookstore/bookServlet?action=bookDetails

图 2-1 展示了这个 URL 的各个部分。

9781430259831_Fig02-01.jpg

图 2-1 。URL 的剖析

主机名和端口号一起被称为机构。默认情况下,像 Tomcat 这样的 web 服务器会监听端口 8080 上的传入请求。在图 2-1 中显示的 URL 的某些部分是可选的,包括端口号(默认为众所周知的端口 80 和 443,分别用于 HTTP 和 HTTPS 方案)和查询字符串。

注意 HTTPS 是安全套接字层(SSL)上的 HTTP;它允许安全、加密的通信。

如果存在,查询字符串是一系列名称-值对,前面有一个问号(?)并用一个&符号分隔这些对。

注意只有 GET 方法支持查询字符串。还有其他 HTTP 协议方法,如 POST、DELETE 和 PUT。

web 应用是协同工作以在 web 上提供特定功能的 Web 组件的集合。在 Java EE 规范中,web 组件被定义为一个 Servlet 或一个 Java 服务器页面(JSP )页面。

注意除了 servlets 和 JSP 页面,web 应用还可以包括静态资源,例如 HTML 文档、图像和定义 web 应用属性的元数据或配置文件;但是,这些不被认为是 web 组件。

web 应用及其组成组件在 web 容器中管理和执行,也称为 servlet 容器 ,它为 web 应用提供了额外的功能,如安全性。当 web 服务器收到对特定 web 组件(如 servlet 或 JSP 页面)可以提供的特定功能的请求时,web 服务器会将请求转发给 web 组件所在的 servlet 容器。所有对动态内容的请求(也就是对负责生成动态内容的 web 组件的所有请求)都由 servlet 容器来协调,如图 2-2 中的所示。

9781430259831_Fig02-02.jpg

图 2-2 。动态内容请求

Java EE servlet 和 JSP 规范描述了 Servlet 容器必须提供的服务契约,并指定了 Servlet 应该如何使用这些服务。就实现而言, servlet 是一个 Java 类,充当动态 web 资源。

小型应用

Servlets 是 Java web 应用的中央处理单元,负责 web 应用所需的大部分处理。具体来说,servlet 是一个实现 javax.servlet.Servlet 接口的 Java 类。Servlet 接口定义了所有 Servlet 必须实现的方法。“一枚戒指统治他们所有人!”这个接口和其他方法一起定义了关键的生命周期方法,比如 init()、service()和 destroy(),分别用于初始化 servlet、服务请求和从服务器中删除 servlet。表 2-1 描述了 javax.servlet.Servlet 接口的所有方法。

表 2-1 。Servlet 接口的生命周期和非生命周期方法

|

修饰符和类型

|

方法

|
| --- | --- |
| 空的 | init(ServletConfig config) |
| 空的 | 服务(ServletRequest req,ServletResponse res) |
| 空的 | 销毁() |
| 如何获取 | getServletConfig() |
| 线 | getServletInfo() |

在 servlet 的生命周期中,容器在适当的时刻按以下顺序调用生命周期方法:

  1. servlet 被构造,然后用 init 方法初始化。
  2. 客户端对服务方法的任何调用都会得到处理。
  3. 然后用 destroy 方法销毁 servlet,回收垃圾,并最终完成。

这里解释一下表 2-1 中说明的 Servlet 接口方法:

  • init(ServletConfig):在实例化 servlet 后由 servlet 容器调用一次。在 servlet 成为接收任何请求的候选者之前,该方法必须成功完成。
  • service():在 servlet 的 init()方法成功完成之后,由 servlet 容器调用,以允许 servlet 响应请求。
  • destroy():由容器调用以销毁 servlet,并作为一种方法,在 servlet 被销毁之前,必须释放获取的资源。
  • getServletConfig():允许 servlet 以该方法返回的 ServletConfig 对象的形式获取启动信息。ServletConfig 对象包含 servlet 的初始化和启动参数。
  • getServletInfo():允许 servlet 返回自己的信息,比如 servlet 的作者和版本。

使用 Servlet 的第一个 Web 应用

在第一章中,你安装了 Eclipse 开普勒 IDE。在本节中,您将在 Eclipse 中开发您的第一个 web 应用。具体来说,您将使用 Tomcat 7 作为 HTTP 服务器和 servlet 容器。你可以从tomcat.apache.org/download-70.cgi下载 Tomcat 的源代码发行版作为 ZIP 文件来安装 Tomcat 7。

启动 Eclipse,选择窗口image首选项菜单选项,显示首选项对话框,如图 2-3 所示。

9781430259831_Fig02-03.jpg

图 2-3 。首选项对话框,验证安装的 JRE

在这个对话框的左窗格中,深入到 Java image Installed JREs,并验证您以前安装的 JRE8/JDK 8 版本是否出现。如果没有,请单击添加按钮添加对您的 JDK 的引用。选择文件image新建image动态 Web 项目,创建一个动态 Web 项目,如图图 2-4 所示。将项目命名为 helloworld ,如图图 2-5 所示。

9781430259831_Fig02-04.jpg

图 2-4 。创建新项目

9781430259831_Fig02-05.jpg

图 2-5 。创建 helloworld 项目

点击下一步,勾选“生成 web.xml 部署描述符”,如图图 2-6 所示。稍后我们将看到如何在没有 web.xml 的情况下配置 web 模块。

9781430259831_Fig02-06.jpg

图 2-6 。配置网络模块设置

点击完成,创建一个新的 Java 类,如图图 2-7 所示。

9781430259831_Fig02-07.jpg

图 2-7 。创建一个 Java 类:servlet

用清单 2-1 中的代码修改生成的 HelloWorld 类。

清单 2-1 。HelloWorld Servlet

1.package apress.helloworld;
2.
3.import java.io.IOException;
4.import java.io.PrintWriter;
5.
6.import javax.servlet.http.HttpServlet;
7.import javax.servlet.http.HttpServletRequest;
8.import javax.servlet.http.HttpServletResponse;
9.
10.public class HelloWorld extends HttpServlet{
11.
12.protected void doGet(HttpServletRequest request,
13.HttpServletResponse response)
14.{
15.try
16.{
17.response.setContentType("text/html");
18.PrintWriter printWriter = response.getWriter();
19.printWriter.println("<h2>");
20.printWriter.println("Hello World");
21.printWriter.println("</h2>");
22.}
23.catch (IOException ioException)
24.{
25.ioException.printStackTrace();
26.}
27.}
28.
29.}

用清单 2-2 中的代码修改 web.xml 文件。

清单 2-2 。Web.xml:部署描述符

1.<?xml version="1.0" encoding="UTF-8"?>
2.<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3.fontname">http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
4.xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
5.id="WebApp_ID" version="3.0">
6.<display-name>helloworld</display-name>
7.<servlet>
8.<servlet-name>HelloWorld</servlet-name>
9.<servlet-class>apress.helloworld.HelloWorld</servlet-class>
10.</servlet>
11.<servlet-mapping>
12.<servlet-name>HelloWorld</servlet-name>
13.<url-pattern>/hello</url-pattern>
14.</servlet-mapping>
15.<welcome-file-list>
16.<welcome-file>index.html</welcome-file>
17.<welcome-file>index.htm</welcome-file>
18.<welcome-file>index.jsp</welcome-file>
19.<welcome-file>default.html</welcome-file>
20.<welcome-file>default.htm</welcome-file>
21.<welcome-file>default.jsp</welcome-file>
22.</welcome-file-list>
23.</web-app>

现在我们需要在 Tomcat 中将 HelloWorld servlet 配置为 web 模块。在 Eclipse 的菜单栏中选择 Window image Show View image Servers,如图图 2-8 所示。

9781430259831_Fig02-08.jpg

图 2-8 。添加服务器

在 Servers 选项卡上,右键单击并添加 Tomcat 7 作为新的服务器,如图图 2-9 所示。

9781430259831_Fig02-09.jpg

图 2-9 。定义服务器

接下来你必须定义一个新的服务器,如图 2-10 所示。

9781430259831_Fig02-10.jpg

图 2-10 。定义 Tomcat 服务器

现在通过将资源移动到 configured 部分的右边来配置 helloworld 项目,如图 2-11 所示。

9781430259831_Fig02-11.jpg

图 2-11 。在服务器上配置资源

单击“添加”,将在服务器上配置该资源。然后单击完成。启动服务器,使用图 2-12 中所示的 URL 访问应用。

9781430259831_Fig02-12.jpg

图 2-12 。启动服务器

你现在可以通过 URLlocalhost:8080/hello world/hello访问你的第一个 web 应用,如图图 2-13 所示。

9781430259831_Fig02-13.jpg

图 2-13 。运行 helloworld web 应用

在下一节中,您将了解请求如何流经应用,以及容器如何在您开发的 helloworld 应用中找到 servlet。然后,您将使用 HelloWorld servlet 来理解 servlet 的生命周期方法。

HelloWorld Servlet 的请求流

在 HelloWorld servlet 可以生成响应之前,来自 web 浏览器的请求会流经 web 服务器和 servlet 容器,如下面几节所述。

HTTP 请求消息

用于访问 helloworld web 应用的 URL 是localhost:8080/hello world/hello。当用户通过这个 URL 访问 web 应用时,web 浏览器创建 HTTP 请求,如清单 2-3 所示。

清单 2-3 。HttpRequest 消息

1.GET /helloworld/hello HTTP/1.1
2.Host: localhost:8080
3.User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.31 (KHTML, like Gecko)
Chrome/26.0.1410.43 Safari/537.31
  • 第 3 行:这一行描述了特定的用户代理(web 浏览器),它通过使用 HTTP 请求特定的资源来发起通信,如第 1 行所述。

  • Line 1: The request made by the user agent is of the form shown in Figure 2-14.

    9781430259831_Fig02-14.jpg

    图 2-14 。HTTP 请求消息剖析

  • Line 2 :运行 web 服务器的这台机器(你的机器)是服务器机器。指定 Localhost,否则您将使用计算机的主机名。例如,将安装在运行 HTTP 服务器的系统上的浏览器指向 localhost ,将显示安装在该系统上的网站的主页,如图图 2-15 所示。

9781430259831_Fig02-15.jpg

图 2-15 。网络服务器的主页

图 2-15 显示的是 Tomcat 的主页,当你在你的机器上运行 Tomcat 的 bin 目录下的 startup.bat,你就会看到这个主页。服务器使用带编号的端口使其服务对互联网可用,服务器上可用的每个服务一个端口。例如,如果一台服务器正在运行一个 web 服务器,那么这个 web 服务器通常位于端口 80 上,对于 Tomcat 来说是 8080。

审查请求

当客户端(web 浏览器)发出请求(在本例中是 GET 请求)时,web 服务器(Tomcat)在第 1 行的请求中看到资源路径/helloworld/hello,并确定用户请求的资源不是静态页面(例如. html 文件),因此将请求转发给 web 容器(Tomcat)。敏锐的读者会注意到 Tomcat 充当了 web 服务器和 web 容器的角色。

定位 Servlet

请求中的资源路径(清单 2-3 中的第 1 行)通过清单 2-2 中的 web.xml 文件映射到 HelloWorld servlet 。这个 web.xml 文件被称为部署描述符,因为它向 web 容器描述了部署的 servlet。通过部署描述符,web 容器确定需要调用哪个 servlet 来服务 web 浏览器发起的原始 HTTP 请求。为了方便读者,web.xml 再次显示在清单 2-4 中。

清单 2-4 。web.xml

1.<?xml version="1.0" encoding="UTF-8"?>
2.<web-app fontname">http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">
3.<servlet>
4.<servlet-name>HelloWorld </servlet-name>
5.<servlet-class>
6.apress.helloworld.HelloWorld
7.</servlet-class>
8.</servlet>
9.<servlet-mapping>
10.<servlet-name>HelloWorld</servlet-name>
11.<url-pattern>/hello </url-pattern>
12.</servlet-mapping>
13.</web-app>
  • 第 1 行到第 2 行:这些行包含样板 XML,说明 XML 文件使用的版本、编码和模式。
  • 第 3 行到第 8 行:标签用于配置我们的 servlet。它包含两个嵌套标签:< servlet-name >定义 servlet 的逻辑名称,而< servlet-class >表示定义 servlet 的 Java 类。
  • 第 9 行到第 12 行:XML 标签用于配置我们的 servlet。它包含两个嵌套标签:< servlet-name >匹配在< servlet >标签中设置的值,而< url-pattern >设置 servlet 将执行的 url 模式。

Java EE web 应用从上下文根中运行。上下文根是 URL 中服务器名称和端口之后的第一个字符串。比如在 URLlocalhost:8080/helloworld/hello 中,字符串 hello world 就是上下文根。< url-pattern >的值相对于应用的上下文根。Java EE web.xml 文件可以包含许多附加的 xml 标记。除了将 URL 映射到实际的 servlets,您还可以使用部署描述符定制 web 应用的其他方面,比如安全角色、错误页面、标记库和初始配置信息。但是,这个 helloworld 应用不需要这些额外的标记。web 容器加载 HelloWorld servlet 类并实例化它。只创建 HelloWorld servlet 的一个实例,对 HelloWorld servlet 的并发请求在同一个实例上执行。每个客户端请求都会生成一对新的请求和响应对象。容器运行多个线程来处理对 HelloWorld servlet 的单个实例的多个请求。

注意在分布式 web 应用中,每个 JVM 都有一个特定 servlet 的实例,但是每个 JVM 仍然只有那个 servlet 的一个实例。

生命周期法

对象的生命周期描述了对象在其存在期间必须经历的一系列步骤。servlet 的生命不同于普通的 Java 类,因为 servlet 必须在 web 容器内部执行。图 2-16 显示了 HelloWorld servlet 的层次结构。

9781430259831_Fig02-16.jpg

图 2-16 。HelloWorld servlet 的层次结构

通用服务器

大多数 servlet 通过 Servlet API 提供的抽象 javax.servlet.GenericServlet 类提供类似的基本功能。GenericServlet 类的实现是独立于协议的,因此它是否必须响应 HTTP 或 FTP 请求并不重要。GenericServlet 抽象类定义了一个 init()方法,默认的 init(ServletConfig)方法调用该方法来执行任何特定于应用的 Servlet 初始化。

http servlet〔??〕

在 web 应用中,GenericServlet 类中缺少任何依赖于协议的处理意味着开发人员必须在自己创建的任何子类中编写处理代码。由于 HTTP 是 Web 上最著名和最广泛使用的协议,Servlet API 还包括 GenericServlet 的一个更抽象的子类:javax.servlet.http.HttpServlet。

注意 HTTP/1.1 定义了七种请求方法。HttpServlet 类为这些方法中的每一个提供了默认实现,您可以在 Servlet 中覆盖这些方法。然而,大多数 web 应用包含只覆盖 doGet()和 doPost()方法的 servlets。

对 HttpServlet 子类的 HTTP 请求要经过许多步骤:

  1. 容器对公共服务(ServletRequest,ServletResponse)方法的调用。
  2. 将此调用委托给 HttpServlet 的受保护服务(HttpServletRequest,HttpServletResponse)方法。
  3. 受保护的服务(HttpServletRequest,HttpServletResponse)方法然后委托给适当的 doXxx 方法,这取决于用于请求的 HTTP 方法。

HelloWorld Servlet

如前一节所述,servlet 的超类包括 init()的两个版本,一个采用 ServletConfig,另一个没有参数。init(ServletConfig)方法调用无参数 init(),因此您只需要覆盖无参数版本。

init( )

容器在 servlet 实例上调用 init()。您可以覆盖它,以便获得向其他对象注册的数据库连接。否则,运行 Genericservlet 中的 init()方法。

  1. 创建 servlet 实例时,会调用它的 init()方法。init()方法允许 servlet 在处理第一个请求之前初始化自己。您可以在 web.xml 文件中或通过注释为 servlet 指定 init()参数。
  2. web 容器调用 servlet 的 init()方法(在 servlet 的生命周期中只调用一次),init()方法必须在容器可以调用 service()方法之前完成。

服务()

容器调用 servlet 的 service()方法。这个方法查看请求,确定 HTTP 方法的类型,并在 servlet 上调用匹配的 doGet()或 doPost()。你永远不能超越它。您的工作是覆盖 doGet()或 doPost(),让来自 HTTPServlet 的服务实现担心调用正确的那个。

  • 当在 servlet 上调用 service()方法时,它将被传递对实现 HttpServletRequest 和 HttpServletResponse 接口的 HttpServletRequest 和 HttpServletResponse 对象的引用。容器实现了这些接口。
  • 对于 servlet 收到的每个请求,都会调用 servlet 的 service()方法。对于 HttpServlet 子类,通常调用 doGet()、doPost()等方法之一。容器创建两个对象:HTTPServletRequest 和 HttpServletResponse。
  • 只要 servlet 在 servlet 容器中是活动的,就可以多次调用 service()方法。
  • service()方法调用 doGet()/doPost()。您总是在 servlet 中覆盖至少其中一个。

destroy( )

servlet 容器调用 servlet 上的 destroy()方法,这个方法只被调用一次。这个方法为 servlet 提供了一个机会,在它被破坏之前释放获得的资源。

ServletContext 和 ServletConfig

调用 init()方法后,servlet 为每个 servlet 获取一个 ServletConfig 对象,为每个 web 应用获取一个 ServletContext。在分布式环境中,每个 JVM 都有一个 ServletContext。ServletContext 是一种方法,通过它 servlet 可以连接容器和 web 应用的其他部分。在 servlet 中,只有当您的 servlet 不是 HttpServlet 或 GenericServlet 时,才需要通过 ServletConfig 来获取 ServletContext。

servlet config 对象可用于执行以下操作:

  • 将您不希望硬编码到 servlet 中的部署时信息(如数据库或企业 bean 查找名称)传递给 servlet。这个部署时信息被称为 servlet 初始化参数。servlet init 参数将在下一节讨论。
  • 访问 ServletContext。

ServletContext 对象可用于执行以下操作:

  • 访问 web 应用参数
  • 设置应用的所有组件都可以访问的属性
  • 获取服务器信息,包括容器的名称和版本以及支持的 API 版本

servlet 可以有三种类型的参数:

  • 请求参数
  • 初始化(init)参数
  • 上下文初始化(context-init)参数

初始化参数

初始化参数在 web.xml 文件中定义,如清单 2-5 所示。

清单 2-5 。定义初始化参数

<servlet>
<init-param>
<param-name>email </param-name>
<param-value>vishalway@.gmail.com</param-value>
</init-param>
</servlet>

在 servlet 初始化之前,不能使用 init-parameters。您的 servlet 继承了 getServletConfig(),因此您可以从 servlet 中的任何方法调用它来获得对 ServletConfig 的引用。一旦有了 ServletConfig 引用,就可以调用 getInitParam()。

当容器初始化 servlet 时,会发生以下情况:

  1. 容器为 servlet 创建一个惟一的 ServletConfig。
  2. 容器从部署描述符中读取 init 参数,并在 ServletConfig 对象中设置它们。
  3. 然后,容器将 ServletConfig 传递给 servlet 的 init (servletConfig)方法。

注意一旦容器在 ServletConfig 中设置了 init 参数,容器就不再从部署描述符中读取 init 参数,除非 servlet 被重新部署。

上下文初始化参数

上下文初始化参数类似于初始化参数。context-init 参数和 init 参数之间的主要区别在于,context 参数适用于整个 web 应用,而 init 参数仅适用于 servlet。清单 2-6 展示了 web.xml 文件中的上下文初始化参数。

清单 2-6 。定义上下文初始化参数

<context-param>
<param-name>email </param-name>
<param-value>vishalway@gmail.com</param-value>
</context-param>

元素没有嵌套在元素中。

清单 2-7 展示了如何从 servlet 中获取 context-init 参数。

清单 2-7 。获取上下文初始化参数

out.println(getServletContext().getInitParameter("email");

每个 servlet 都继承了一个 getServletContext()方法。getServletContext()方法返回一个 ServletContext 对象。

请求调度员

在 web 应用中,有两种方法可以改变请求流。

  • 重定向请求:请求被重定向到一个完全不同的 URL。重定向可以通过在响应对象上调用 sendRedirect() 来完成。重定向是由浏览器完成的。
  • 分派请求:请求被分派给 web 应用中的另一个组件,通常是 JSP 页面。请求分派不同于重定向,因为它在服务器端完成工作。对请求调用 RequestDispatcher,对响应调用 redirect。

图 2-17 显示了 RequestDispatcher 接口中的方法。

9781430259831_Fig02-17.jpg

图 2-17 。请求调度程序接口

您可以通过两种方式获得 RequestDispatcher。

  • 从 ServletRequest 获取 RequestDispatcher
  • 从 ServletContext 获取 RequestDispatcher

从 ServletRequest 获取 RequestDispatcher】

RequestDispatcher view = request.getRequestDispatcher("bookDetails.jsp");

ServletRequest 中的 getRequestDispatcher()方法获取请求转发到的资源的路径。如果路径以正斜杠(/)开头,则容器认为它是从 web 应用的根目录开始的。如果路径不是以正斜杠开头,容器认为它是相对于原始请求的。

从 ServletContext 获取 RequestDispatcher】

RequestDispatcher view = getServletContext().getRequestDispatcher("/bookDetails.jsp");

getRequestDispatcher()方法接受一个资源的字符串路径,请求将被转发到该资源。从上下文或请求中获得的 RequestDispatcher 可用于转发到资源,因为 RequestDispatcher 知道您要转发到的资源,换句话说,就是作为参数传递给 getRequestDispatcher()的资源。清单 2-8 展示了在 RequestDispatcher 上调用 forward。

清单 2-8 。在 RequestDispatcher 上呼叫转发

RequestDispatcher view = request.getRequestDispatcher("bookDetails.jsp");
view.forward(request, response);

过滤器

过滤器是一个可重用的 Java 组件,可以转换 HTTP 请求、响应和头信息的内容。过滤器用于以下用途:

  • 在调用请求之前访问静态或动态内容或修改请求头
  • 在调用后拦截 web 组件的调用
  • 通过以特定顺序使用过滤器链来提供对 web 组件的操作
  • 在呈现响应头和响应数据之前修改它们

过滤器是通过实现 javax.servlet.Filter 接口并提供一个无参数构造函数来创建的。过滤器是在 web 应用中配置的,要么在部署描述符中使用元素,要么在@WebFilterannotation 中配置(在下一节中介绍)。在元素中,必须声明以下内容:

  • :用于将过滤器映射到 servlet 或 URL
  • :容器用来标识过滤器类型

注意你也可以声明一个过滤器的初始化参数。

清单 2-9 展示了过滤器的声明。

清单 2-9 。声明过滤器

<filter>
<filter-name>ResponseFilter</filter-name>
<filter-class>com.apress.ResponseServlet</filter-class>
</filter>

可以使用元素将过滤器与 servlet 相关联。清单 2-10 将响应过滤器 Filter 映射到 ResponseServlet servlet。

清单 2-10 。将过滤器映射到 Servlet

<filter-mapping>
<filter-name>Response Filter</filter-name>
<servlet-name>ResponseServlet</servlet-name>
</filter-mapping>

过滤器可以使用< urlpattern >与 servlets 组相关联,如清单 2-11 所示。

清单 2-11 。将过滤器与一组 Servlets 相关联

<filter-mapping>
<filter-name>Response Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

在清单 2-11 中,响应过滤器应用于 web 应用中的所有 servlets。

web 应用通常包含以下过滤组件:

  • 认证过滤器
  • 缓存过滤器
  • 数据压缩过滤器
  • 加密过滤器
  • 图像转换过滤器
  • 日志和审计过滤器

通过注释配置 Servlet

从 Servlet 3.0 开始,可以通过 web.xml 或使用注释或两者来配置 Servlet。表 2-2 描述了符合 Servlet 3.0 的 web 容器所支持的注释。

表 2-2 。配置 Servlet 的注释

|

注释

|

描述

|
| --- | --- |
| @ web 过滤器 | 在 web 应用中定义筛选器 |
| @WebInitParam | 指定要传递给 servlet 或过滤器的初始化参数 |
| @WebListener | 注释一个侦听器以获取事件 |
| @webservlet | 定义 web 应用中的组件 |
| @MultipartConfig | 指示请求属于 mime/multipart 类型 |

在接下来的小节中,您将开发一个 helloworld 项目,在这个项目中,您将通过注释来配置 servlet。右键单击项目 helloworld,创建一个新的 servlet 类,并将类名命名为 HelloWorld,如图图 2-18 所示。单击下一步。

9781430259831_Fig02-18.jpg

图 2-18 。创建 servlet

在下一个屏幕上,您可以填写与部署描述符相关的信息,例如初始化参数和 URL 映射,如图 2-19 中的所示。对于 HelloWorld 应用,您不必填写初始化参数的值。URL 映射的默认值,在本例中是/HelloWorld,就可以了。“URL mappings”字段中的值是 URL 的 servlet 路径,如前一节所述。单击下一步。

9781430259831_Fig02-19.jpg

图 2-19 。网址映射

在下一个屏幕上,指定修饰符、要实现的接口和要生成的方法存根,如图 2-20 所示。如果尚未检查 doGet 和 doPost,请检查它们。然后单击完成。

9781430259831_Fig02-20.jpg

图 2-20 。指定方法

IDE 生成清单 2-12 中的 HelloWorld servlet。

清单 2-12 。使用注释的 HelloWorld Servlet

1.package apress.helloworld;
2.
3.import java.io.IOException;
4.import javax.servlet.ServletException;
5.import javax.servlet.annotation.WebServlet;
6.import javax.servlet.http.HttpServlet;
7.import javax.servlet.http.HttpServletRequest;
8.import javax.servlet.http.HttpServletResponse;
9.
10./**
11\. * Servlet implementation class HelloWorld
12\. */
13.@WebServlet(urlPatterns = { "/HelloWorld" }, description = "A hello world servlet")
14.public class HelloWorld extends HttpServlet {
15.private static final long serialVersionUID = 1L;
16.
17.    /**
18.     * @see HttpServlet#HttpServlet()
19.     */
20.    public HelloWorld() {
21.        super();
22.        // TODO Auto-generated constructor stub
23.    }
24.
25./**
26\. * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
27\. */
28.protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
29.// TODO Auto-generated method stub
30.}
31.
32./**
33\. * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
34\. */
35.protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
36.// TODO Auto-generated method stub
37.}
38.
39.}
  • 第 13 行:这一行显示了@WebServlet 注释的用法。HelloWorld servlet 使用@WebServlet 注释来指定名称、URL 模式、初始化参数和通常在 web.xml 部署描述符中指定的其他配置项。
  • 第 14 行:这一行显示 HelloWorld servlet 扩展了 HTTPServlet。
  • 第 28 到 37 行:这几行显示了 IDE 生成的 doGet 和 doPost 方法。

将清单 2-13 中的代码添加到 HelloWorld servlet 的 doGet 方法中。

清单 2-13 。印刷“Hello World”

PrintWriter out = response.getWriter();
out.println("<h2>Hello World !</h2>");

您必须导入 java.io.Printwriter。您可以在 Eclipse 中通过选择 Source image Add Import 或按 Ctrl+Shift+M;现在,您可以在服务器上运行应用了。在 Eclipse 的 helloworld 项目中右键单击 HelloWorld.java,然后选择 Run As image Run on Server。在这种情况下,您使用的服务器是 Tomcat 7.0。然后使用以下 URL 访问该应用:

http://localhost:8080/helloworld/HelloWorld

图 2-21 显示了输出。

9781430259831_Fig02-21.jpg

图 2-21 。访问应用

有关 Java Servlet 技术的更多信息,请参考 Java Servlet 3.1 规范 2 和 Java Servlet 网站。 3

Java 服务器页面

Servlets 使 web 服务器能够生成动态内容。然而,servlets 有一个主要的缺点,即 HTML 代码需要硬连接到 Java 代码中。为了消除这种横切关注点,Java Server Pages (JSP) 技术应运而生。JSP 使用静态 HTML 内容和动态内容的组合来生成 web 页面,从而分离了在 Java 代码中嵌入 HTML 内容的顾虑。

您的第一个使用 JSP 的 Web 应用

现在您将使用 JSP 创建一个“Hello World”应用。因为您之前已经创建了一个同名的项目,所以请确保先删除或重命名旧项目。然后在项目浏览器中点击右键,选择动态 Web 项目,如图图 2-22 所示。将项目命名为 helloworld

9781430259831_Fig02-22.jpg

图 2-22 。创建 helloworld 项目

右键单击项目 helloworld,创建一个新的 JSP 文件,命名为helloworld.jsp。单击下一步。在下一个屏幕上单击完成。

9781430259831_Fig02-23.jpg

图 2-23。创建 JSP 文件

修改 helloworld.jsp 的代码,如清单 2-14 所示。

清单 2-14 。helloworld.jsp

<!DOCTYPE html >
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Hello World</title>
</head>
<body>
Hello World!
</body>
</html>

在服务器上部署应用,如图图 2-24 所示。

9781430259831_Fig02-24.jpg

图 2-24 。在服务器上运行应用

使用localhost:8080/hello world/hello . JSP启动应用。

图 2-25 显示了输出。

9781430259831_Fig02-25.jpg

图 2-25 。访问 JSP

JSP 基础

这一节将介绍基本的 JSP 构造。对经典方法的正确理解对于理解其局限性和欣赏更高级技术的力量是必不可少的,比如表达式语言,这是下一章的主题。在表达式语言被添加到 JSP 规范之前,十亿个 JSP 页面是使用传统方法编写的,您可能仍然需要维护它们或重构它们。对向后兼容性的需求是 JSP 规范仍然涵盖经典组件的原因之一。但是当您必须在应用中编写新的 JSP 时,您不应该编写向后兼容的代码;相反,你应该使用最佳实践方法,这将在下一章中解释。当 JSP 1.0 在 1999 年被添加到 JSP 规范中时,它意味着通过在模板数据中嵌入业务逻辑代码来生成动态的、基于 web 的内容。为此,提供了以下 JSP 元素,用于在 JSP 页面中操纵 Java 对象并对其执行操作,从而支持动态内容的生成:

  • 指令
  • 声明
  • 公式
  • 脚本片断
  • 隐式对象
  • 标准动作

JSP 指令

JSP 指令是 JSP 容器的指令,在页面翻译过程中进行处理。指令提供了一种机制,使 JSP 引擎可以使用页面级信息。指令在指令分隔符之间声明,并采用以下形式:

<%@ directive {attribute="value"}* %>

页指令

page 指令用于提供容器生成底层 servlet 所使用的特定 JSP 页面的相关说明。以下是页面指令的基本语法:

<%@ page attribute="value" %>

表 2-3 描述了与页面指令相关的属性。

表 2-3 。页面指令的属性

|

属性

|

目的

|
| --- | --- |
| 自动冲洗 | 控制 servlet 输出缓冲区的行为。它指示缓冲区满时是否应该自动写入。 |
| 缓冲器 | 指定 servlet 输出流的缓冲模型。它表示缓冲区的大小。 |
| 内容类型 | 指定响应的 MIME 类型和字符编码方案。 |
| 错误页 | 指定处理错误条件并报告运行时异常的 JSP 的 URL。 |
| 延伸 | 指示生成的 servlet 必须扩展的超类。 |
| 进口 | 指定在 JSP 页面中使用的类,类似于 Java 中的 import 语句。 |
| 信息 | 为 servlet 的 getServletInfo()方法指定一个字符串。 |
| isELIgnored?isELEnabled | 指定 JSP 页面中是否允许 EL 表达式。 |
| isErrorPage | 指定此 JSP 页面是否用于处理错误情况和报告运行时异常。 |
| isScriptingEnabled | 指定 JSP 页面中是否允许脚本元素。 |
| 是线程安全的 | 指示 JSP 页是否可以处理并发请求。 |
| 语言 | 指示 JSP 页面中使用的脚本语言。 |
| 会议 | 指定 JSP 页面是否参与 HTTP 会话。 |

包含指令

include 指令用于指定应该包含在当前 JSP 页面翻译单元中的静态资源。include 指令有一个名为 file 的属性,它指定应该包含的资源的 URL。该指令的一般形式如下:

<%@ include file="relative url" >

以下示例演示了如何使用 include 指令在当前翻译单元中包含标准的 JSP 页眉和页脚(清单 2-15 )。你可以创建一个像你的第一个 web 应用 helloworld 一样的项目,用 main.jsp 替换 helloworld.jsp。【清单 2-15 展示了 main.jsp 的 ??。

清单 2-15 。main.jsp

1.<%@ include file="header.jsp" %>
2.
3.<p>content</p>
4.
5.<%@ include file="footer.jsp" %>
  • 第 1 行:这一行包括翻译时 main.jsp 文件中的 header.jsp。
  • 第 5 行:这一行包括翻译时 main.jsp 文件中的 footer.jsp。

清单 2-16 展示了 header.jsp。

清单 2-16 。header.jsp

1.<html>
2.<head></head>
3.<body>
4.<%out.print("header"); %>
  • 第 4 行:这一行使用了一个隐式对象 out。隐式对象将在本章后面介绍。隐式 out 对象表示 JspWriter 类的一个实例,用于将字符数据写入响应流。

清单 2-17 展示了 footer.jsp。

清单 2-17 。footer.jsp

1.<%out.print("footer"); %>
2.</body>
3.</html>

图 2-26 显示了输出。

9781430259831_Fig02-26.jpg

图 2-26 。使用 include 指令

Taglib 指令

Java Server Pages API 提供了封装功能的标准动作,下一节将对此进行介绍。JSP API 还允许您定义实现自定义行为的自定义操作。几个这样的定制动作,也称为定制标签,被组装在一个名为标签库的库中。taglib 指令用于定义当前 JSP 页面中标记库的前缀和位置。taglib 指令使用以下语法:

<%@ taglib uri="uri" prefix="tagPrefix" >

uri 属性值是指定标记库位置的绝对或相对路径,prefix 属性指定要在当前 JSP 中使用的自定义操作。清单 2-18 展示了 taglib 指令在一个名为 helloTagLib 的示例标签库中的用法,该标签库包含一个名为 hello 的标签,用于打印“Hello World”消息。

清单 2-18 。Taglib 指令的用法

1.<%@ taglib uri="/ helloTagLib" prefix="helloTag" %>
2.<html>
3.<body>
4.<helloTag:hello/>
5.</body>
6.</html>
  • Line1 :库 helloTagLib 的 URI 和前缀 helloTag
  • 第 4 行:通过前缀使用 hello 标签

声明

使用声明,JSP 允许您在 JSP 页面中声明方法和变量。一旦它们出现在 JSP 页面中,就可以在整个页面中被 scriptlets 和表达式使用。JSP 声明放在声明分隔符之间。由于声明与表达式和 script let 一起使用,所以我将在下面的小节中介绍表达式和 script let,然后我将向您展示如何在 JSP 页面中使用声明、script let 和表达式。

表情

表达式类似于 scriptlets,但是它们计算一个常规的 Java 表达式,并作为响应的一部分向客户机返回一个结果,这个结果是一个字符串或者可以转换成字符串的东西。一般语法如下:

<%= expression %>

小脚本

Scriptlets 是包含在分隔符中的 Java 代码块,用于创建动态内容。清单 2-19 用一个脚本和表达式说明了声明的用法。

清单 2-19 。声明、脚本和表达式的用法

1.<%!
2.public String hello() {
3.String msg = "Hello World";
4.return msg;
5.}
6.%>
7.Message from <b>Scriptlet</b>: <%hello();%><br/>
8.Message from <b>Expression</b>: <%=hello() %>
  • 第 1 行到第 6 行 : 这些行包含一个 JSP 声明,声明了一个 hello()方法。第 1 行是开始标记,第 6 行是声明的结束标记。
  • 第 7 行:第 1 行到第 6 行声明的 hello()方法用在了第 7 行的一个表达式中。

2-27 举例说明了声明、脚本和表达式的用法。

9781430259831_Fig02-27.jpg

图 2-27 。使用声明、scriptlet 和表达式

隐式对象

在 web 应用中,多个 web 组件通过作为四个范围对象的属性维护的对象来相互协作和共享信息。您可以通过使用代表作用域的类的 getAttribute 和 setAttribute 方法来访问这些属性。表 2-4 列出了范围对象。

表 2-4 。范围对象

|

范围对象

|

中文版

|

可从访问。。。

|
| --- | --- | --- |
| 应用/web 上下文 | javax.servlet.ServletContext | 应用中的 Web 组件 |
| 请求 | javax.servlet.ServletRequest 的子类型 | 处理请求的 Web 组件 |
| 会议 | javax.servlet.http.HttpSession | 会话中的 Web 组件 |
| 页 | javax . servlet . JSP . JSP 上下文 | 创建对象的 JSP 页面 |

注意除了标准的 servlet 请求、会话和应用作用域,JSP 添加了第四个作用域,称为页面作用域

JSP 页面可以通过脚本变量访问一些特定的对象。这些对象由 JSP 容器提供,被称为隐式对象。这些隐式对象可以在 scriptlets、表达式或作为 EL 表达式的一部分来访问。(EL 表达式在第三章的中介绍。)表 2-5 列出了对应 API 的九个隐式对象。

表 2-5 。隐式对象

|

隐含对象

|

使用

|

应用接口

|
| --- | --- | --- |
| 应用 | 访问应用级对象 | ServletContext |
| 配置 | 提供配置信息 | 如何获取 |
| 例外 | 访问错误状态 | JSP 异常 |
| 在外 | 访问 JSP 输出流 | 对象 |
| 页,面,张,版 | 提供对当前 JSP 的引用 | 目标 |
| 对象 | 访问 JSP 容器 | 对象 |
| 请求 | 提供对客户端请求的访问 | ServletRequest |
| 反应 | 提供对 JSP 响应的访问 | servlet 响应 |
| 会议 | 跨客户端请求共享信息 | 会话 |

这些隐式对象将在下面的章节中详细描述。

应用

隐式应用对象提供了对 javax.servlet.ServletContext 接口的引用。ServletContext 接口用于提供对任何上下文初始化参数的访问,这些参数是通过 web 应用的部署描述符为 JSP 页面配置的。web 容器存储在其中的 ServletContext 对象和参数可供整个 web 应用使用。application 对象为 JSP 页面的开发人员提供了对 ServletContext 对象的访问。

配置

与 application 对象类似,config 对象提供了对 web 应用的 ServletConfig 接口的引用。ServletConfig 接口用于通过 web 应用的部署描述符提供对已经为 JSP 页面配置的任何初始化参数的访问。config 对象为 JSP 开发人员提供了对 ServletConfig 对象的访问。

例外〔??〕

JSP 可以使用隐式异常对象来处理错误条件,并使用 errorPage 页面指令报告运行时异常。

出局

隐式 out 对象表示用于将字符数据写入响应流的 JspWriter 类的实例。

页〔??〕

JSP 隐式页面对象是 object 类的一个实例,表示当前 JSP 页面。

page context〔??〕

pageContext 通过提供对与 JSP 页面相关联的所有名称空间和几个页面属性的访问来提供上下文信息。此外,它还包含对隐式对象的引用。

请求

请求对象是 javax . servlet . http . http servlet request 接口的实例。它代表客户端请求。请求隐式对象通常用于获取请求参数、请求属性、头信息和查询字符串值。

响应

隐式响应对象是 javax . servlet . http . http servlet response 接口的一个实例,表示要给客户机的响应。隐式响应对象通常用于设置响应内容类型、添加 cookies 和重定向响应。

会话

JSP 隐式会话对象是实现 javax.servlet.http.HttpSession 接口的 Java 类的实例。它用于存储客户端的会话状态。

清单 2-20 展示了常用隐式对象的用法。首先,它展示了在 servlet 的请求、会话和应用范围中设置 book 属性的常见任务。然后显示它们的 JSP 等价物。

清单 2-20 。常见隐式对象的用法

1.getServletContext().setAttribute("book", book);
2.request.setAttribute("book", book);
3.request.getSession().setAttribute("book", book);
4.application.setAttribute("book" book);
5.request.setAttribute("book" book);
6.session.setAttribute("book" book);
7.pageContext.setAttribute("book" book);
  • Line 1 :在 ServletContext 中设置 book 属性,不使用隐式对象。
  • 第 2 行:设置请求对象中的 book 属性。请求对象也是 JSP 中的隐式对象。因此,在 servlet 中设置属性类似于在 JSP 页面中设置属性。
  • 第 3 行:在 session 中设置 book 属性,不使用隐式对象。
  • 第 4 行:使用 application 隐式对象设置 ServletContext 中的 book 属性。
  • 第 5 行:设置请求对象中的 book 属性。request 也是 JSP 中的隐式对象。因此,在 JSP 中设置属性类似于在 servlet 中设置属性。
  • 第 6 行:使用 session 隐式对象设置 session 中的 book 属性。
  • 第 7 行:使用 pageContext 隐式对象设置 PageContext 中的 book 属性。servlet 中没有 pageContext 的等价物。PageContext 实例提供对与 JSP 页面相关联的所有名称空间的访问,提供对几个页面属性的访问,并提供实现细节之上的一层。隐式对象会自动添加到 pageContext。

标准动作

JSP 标准操作提供了一种完成以下任务的方法:

  • 操纵 JavaBeans
  • 动态包含文件
  • 执行 URL 转发

行动

动作提供了一种在运行时包含指令的方法,该指令用于在声明 JSP 页面中包含单独 web 组件的内容。使用标准包含动作的语法如下:

<jsp:include page="relativeURL" flush="true"/>

我们将创建两个 jsp,如清单 2-21 和清单 2-22 所示,来说明< jsp:include >动作的使用。

清单 2-21 。main.jsp

1.<html>
2.<head>
3.</head>
4.<body>
5.<%out.print("Inside main.jsp"); %><br/>
6.<jsp:include page="sub.jsp"/>
7.</body>
8.</html>
  • 第 6 行:使用< jsp:include >来包含目标 jsp 页面(sub.jsp)

清单 2-22 。sub.jsp

1.<html>
2.<head>
3.</head>
4.<body>
5.<%out.print("Inside sub.jsp"); %><br/>
6.</body>
7.</html>

图 2-28 显示了访问 main.jsp 时的输出。

9781430259831_Fig02-28.jpg

图 2-28 。使用< jsp:include >动作

动作

动作用于将当前请求转发给另一个资源,比如静态页面、JSP 页面或 servlet。该操作的语法如下:

<jsp:forward page="relativeURL" />

我们将使用前一节中创建的两个 JSP 来说明动作的使用,如清单 2-23 和清单 2-24 中所示。

清单 2-23 。利用 main.jsp 的前进行动

1.<html>
2.<head>
3.</head>
4.<body>
5.<%out.print("Inside main.jsp"); %><br/>
6.<jsp:forward page="sub.jsp"/>
7.</body>
8.</html>
  • 第 6 行:使用< jsp:forward >转发到目标 jsp 页面(sub.jsp)。

清单 2-24 。sub.jsp

1.<html>
2.<head>
3.</head>
4.<body>
5.<%out.print("Inside sub.jsp"); %><br/>
6.</body>
7.</html>

图 2-29 显示了访问 main.jsp 时的输出。

9781430259831_Fig02-29.jpg

图 2-29 。前进动作的用法

为了理解 include 动作和 forward 动作之间的区别,比较清单 2-21 和清单 2-23 以及图 2-28 和图 2-29 。在清单 2-23 中,我们在 main.jsp 中使用了转发动作,而不是包含动作。转发操作将控制权转移给 sub.jsp,就像包含操作一样。但是当 sub.jsp 完成时,与 include 操作不同,控制权不会回到 main.jsp。

动作

这三个标准动作可以消除大量的脚本代码,包括声明、scriptlets 和表达式。

useBean 动作用于声明和初始化 Bean 对象。一旦 bean 被初始化,就可以使用 jsp:setProperty 和 jsp:getProperty 操作来设置和获取 bean 属性

动作的语法如下:

<jsp:useBean id="someId" class="SomeClass" />

动作设置 bean 的属性。

动作具有以下语法,其中 someId 是 useBean 的 Id:

<jsp:setProperty name="someId" property="someProperty" .../>

<jsp: getproperty="">动作,顾名思义,获取给定属性的值。如果属性不是字符串,它会将其转换为字符串。</jsp:>

<jsp: getproperty="">动作具有以下语法,其中 someId 是 useBean 的 Id:</jsp:>

<jsp:getProperty name="someId" property="someProperty" .../>

清单 2-25 展示了如何创建用户 bean,而清单 2-26 展示了这三个动作在 JSP 页面中的用法。

清单 2-25 。用户 Bean

1.package com.apress.jspactions;
2.
3.public class User {
4.
5.private String name;
6.
7.public String getName() {
8.return name;
9.}
10.
11.public void setName(String name) {
12.this.name = name;
13.}
14.
15.}

清单 2-25 中的用户 bean 将在 user.jsp 使用,如清单 2-26 中的所示。

清单 2-26 。user.jsp

1.<html>
2.<head>
3.</head>
4.<body>
5.<jsp:useBean id="user" class="com.apress.jspactions.User" />
6.<jsp:setProperty name="user" property="name" value="vishal" />
7.Hello&nbsp;<jsp:getProperty name="user" property="name" />
8.</body>
9.</html>

图 2-30 显示了访问 user.jsp 时的输出。

9781430259831_Fig02-30.jpg

图 2-30 。useBean、getProperty 和 setProperty 操作的用法

MVC 模式

自从有了面向对象编程的概念,模型-视图-控制器(MVC) 模式的动机就一直存在。在 MVC 之前,浏览器直接访问 JSP 页面。换句话说,JSP 页面直接处理用户请求。这被称为模型 1 架构,如图 2-31 所示。模型 1 架构展示了分散的应用控制,这导致了紧密耦合和脆弱的表示层。

9781430259831_Fig02-31.jpg

图 2-31 。模型 1 架构

设计 JSP 页面的 Model-2 架构实际上是应用于 web 应用的 MVC 模式。MVC 起源于 Smalltalk,后来发展到了 Java 社区。图 2-32 显示了模型 2(换句话说,MVC)架构。在 Model-2 中,控制器处理用户请求,而不是另一个 JSP 页面。控制器被实现为一个 servlet。当用户提交请求时,执行以下步骤:

  1. 控制器 servlet 处理用户的请求。
  2. 控制器 servlet 根据请求实例化适当的 JavaBeans。
  3. 控制器 servlet 与中间层通信或直接与数据库通信,以检索所需的数据。
  4. 控制器在以下上下文之一中设置 JavaBeans:请求、会话或应用。
  5. 控制器基于请求 URL 将请求分派到下一个视图。
  6. 视图使用步骤 4 中的 JavaBeans 来显示数据。

9781430259831_Fig02-32.jpg

图 2-32 。模型 2 架构

书店应用

在前一章中,我们为书店应用开发了数据访问层,并使用独立的 Java 应用对其进行了查询。在这一章中,我们将用表示层取代独立的 Java 层。底部的数据访问层保持不变,如图图 2-33 所示。

9781430259831_Fig02-33.jpg

图 2-33 。用表示层替换独立的 Java 客户端

在生产就绪的应用中,您还应该添加一个服务层来处理数据库异常。随着应用的增长,一个分区的应用保持关注点的清晰分离。图 2-34 显示了书店应用的目录结构。

9781430259831_Fig02-34.jpg

图 2-34 。书店应用的目录结构

主页

图 2-35 显示了应用的主页。在输入 URL(localhost:8080/book web/books)时,主页会显示菜单,其中包含书店数据库中可用书籍的类别。

9781430259831_Fig02-35.jpg

图 2-35 。书店应用的主页

图 2-36 显示了书店应用的 MVC 架构。为了简洁和理解类别在主页上的显示方式,图中只显示了与主页和类别相关的组件。

9781430259831_Fig02-36.jpg

图 2-36 。MVC 在书店中的应用

在图 2-36 所示的 MVC 架构中, M 代表类别, V 代表 home.jsp, C 代表 BookController。应用流程包括六个步骤,如以下部分所述。

步骤 1:从请求中定位 Servlet

URL(localhost:8080/book Web/books)用于动态内容,因此 web 服务器将请求转发给 servlet 容器(Tomcat)。清单 2-27 展示了部署描述符。

清单 2-27 。BookstoreWeb 应用的部署描述符

1.<?xml version="1.0" encoding="UTF-8"?>
2.<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3.fontname">http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
4.xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
5.id="WebApp_ID" version="3.0">
6.<display-name>bookWeb</display-name>
7.<servlet>
8.<servlet-name>BookServlet</servlet-name>
9.<servlet-class>com.apress.bookweb.controller.BookController</servlet-class>
10.<init-param>
11.<param-name>base</param-name>
12.<param-value>/bookWeb/books</param-value>
13.</init-param>
14.<init-param>
15.<param-name>imageURL</param-name>
16.<param-value>/bookWeb/images</param-value>
17.</init-param>
18.<load-on-startup>1</load-on-startup>
19.</servlet>
20.<context-param>
21.<param-name>param1</param-name>
22.<param-value>/bookWeb/books</param-value>
23.</context-param>
24.<context-param>
25.<param-name>imageURL</param-name>
26.<param-value>/bookWeb/images</param-value>
27.</context-param>
28.<servlet-mapping>
29.<servlet-name>BookServlet</servlet-name>
30.<url-pattern>/books </url-pattern>
31.</servlet-mapping>
32.<welcome-file-list>
33.<welcome-file>index.html</welcome-file>
34.<welcome-file>index.htm</welcome-file>
35.<welcome-file>index.jsp</welcome-file>
36.<welcome-file>default.html</welcome-file>
37.<welcome-file>default.htm</welcome-file>
38.<welcome-file>default.jsp</welcome-file>
39.</welcome-file-list>
40.</web-app>
  • 第 30 行 : url-pattern/books 被映射到< Servlet-mapping >元素中的 BookServlet,该元素被映射到第 9 行的 servlet 类 BookController。
  • 第 20 到 27 行:我们在 web.xml 文件中为 servlet 指定上下文参数,因为上下文参数可用于整个 web 应用。当一个 servlet 实例被创建时,它的 init()方法被 servlet 容器调用。init()方法允许 servlet 在处理第一个请求之前初始化自己。我们在 BookController 中重写 init(ServletConfig config)方法,以便从书店数据库中获取类别。这些类别将对整个应用可用。BookController 中被覆盖的 init(ServletConfig config)如清单 2-28 中的所示。

步骤 2 和步骤 3:通过 DAO 访问数据库,从数据库中获取类别,并在模型中设置类别

清单 2-28 显示了图书控制器。

清单 2-28 。BookController 的 init()方法

1.public void init(ServletConfig config) throws ServletException {
2.super.init(config);
3.BookDAO bookDao = new BookDAOImpl();
4.// calling DAO method to retrieve bookList from Database
5.List<Category> categoryList = bookDao.findAllCategories();
6.ServletContext context = config.getServletContext();
7.context.setAttribute("categoryList", categoryList);
8.}
  • 第 5 行:这个类别列表是通过调用 bookDao 对象上的 findallcocategories()从数据库中获得的。
  • Line7 :类别列表是在 ServletContext 中设置的,这样整个 webapp 都可以使用这个列表。

步骤 4:分派到视图

随着 init()方法在前一步中完成,容器调用 servlet 的 service()方法(在 servlet 的生命周期方法中讨论)。这个方法查看请求,确定 HTTP 方法,并在 servlet 上调用匹配的 doget()或 dopost()。清单 2-29 展示了 servlet 的 doGet()和 doPost()方法。

清单 2-29 。BookController 中的 doGet()和 doPost()

1.protected void doGet(HttpServletRequest request,
2.HttpServletResponse response) throws ServletException, IOException {
3.doPost(request, response);
4.}
5.
6.protected void doPost(HttpServletRequest request,
7.HttpServletResponse response) throws ServletException, IOException {
8.String base = "/jsp/";
9.String url = base + "home.jsp";
10.String action = request.getParameter("action");
11.String category = request.getParameter("category");
12.String keyWord = request.getParameter("keyWord");
13.if (action != null) {
14.switch (action) {
15.case "allBooks":
16.findAllBooks(request, response);
17.url = base + "listOfBooks.jsp";
18.break;
19.case "category":
20.findAllBooks(request, response);
21.url = base + "category.jsp?category=" + category;
22.break;
23.case "search":
24.searchBooks(request, response, keyWord);
25.url = base + "searchResult.jsp";
26.break;
27.
28.}
29.}
30.RequestDispatcher requestDispatcher = getServletContext()
31..getRequestDispatcher(url);
32.requestDispatcher.forward(request, response);
33.}
  • 第 3 行:doPost()方法是从 doGet()方法调用的。
  • 第 9 行:这一行构造了指向主页视图(home.jsp)的 URL。
  • 第 10 行:该行从请求中获取动作参数。但是由于这是一个主页,没有关联的 action 参数,所以变量 action 为 null。
  • 第 13 行到第 29 行:跳过第 13 行到第 29 行的代码块,因为动作为空。如果 action 不为 null,URL 将被重新构造为指向不同的视图,这取决于 action 值是 allBooks 还是 category 或 search。
  • 第 32 行:request dispatcher 转发到第 31 行 URL 中的视图名。

步骤 5:从视图中访问模型

在前面的步骤中,控制器使用 RequestDispatcher 将 home.jsp 转发到视图。

清单 2-30 展示了 home.jsp 的一个片段,其中包括 leftColumn.jsp。leftColumn.jsp 文件使用模型类别在主页的左侧菜单上显示类别。

清单 2-30 。包括 home.jsp 的 leftColumn.jsp

1.<body>
2.<div id="centered">
3.
4.<jsp:include page="header.jsp" flush="true" />
5.<br />
6.<jsp:include page="leftColumn.jsp" flush="true" />
7.<span class="label">Featured Books</span>
8...........
9.</div>
10.</body>
  • 第 6 行:JSP:include标签用于包含 leftColumn.jsp。这样做是因为应用的左侧栏(菜单)对应用中的所有屏幕都是通用的,但是我们没有在所有屏幕中编写左侧栏,而是将它编写在一个 JSP 页面中,并在需要的任何地方包含它,作为一种可重用性的方法。(在接下来关于 web 框架的几章中,我们将看到更多重用 JSP 的高级技术。)

清单 2-31 展示了与 leftColumn.jsp 类别相关的代码片段,其中类别被访问。

清单 2-31 。在 leftColumn.jsp 访问类别模型

1.<li><div><span class="label" style="margin-left: 15px;">Categories</span></div>
2.<ul>
3.<%
4.List<Category> categoryList1 = (List<Category>) application.getAttribute("categoryList");
5.Iterator<Category> iterator1 = categoryList1.iterator();
6.while (iterator1.hasNext()) {
7.Category category1 = (Category) iterator1.next();%>
8.<li><a class="label"href="<%=param1%>?action=category&categoryId=<%=category1.getId()%>&category=<%=category1.getCategoryDescription()%>"><spanclass="label" style="margin-left: 30px;"><%=category1.getCategoryDescription()%></span></a>
9.</li>
10.<%}%>
11.</ul></li>
  • 第 4 行:这一行从 ServletContext 中获取类别列表。我们已经将类别列表保存在步骤 2 中从数据库获得的 ServletContext 中。
  • 第 6 行到第 10 行:类别细节显示在标记中,比如你在主页上看到的类别描述。

注意清单 2-31 中的 JSP 页面使用 scriptlets 和表达式来获取类别并显示它们。使用 scriptlets 和表达式是不好的做法,应该尽可能避免。这是下一章的主题,它将向您展示如何用 JSTL 和 EL 替换 scriptlets 和表达式。

步骤 6:发送响应

上一步中构建的视图被交付给浏览器。

列出所有的书

当用户点击菜单上的所有书籍时,显示所有书籍的列表,如图图 2-37 所示。

9781430259831_Fig02-37.jpg

图 2-37 。列出所有书籍

“所有书籍”链接在 leftColumn.jsp 文件中。清单 2-32 展示了菜单上所有书籍链接的代码片段。

清单 2-32 。leftColumn.jsp 所有图书链接

1.<li><div>
2.<a class="link1" href="<%=param1%>?action=allBooks"><span
3.style="margin-left: 15px;" class="label">All Books</span></a>
4.</div></li>
  • 第 2 行:这一行是菜单中显示的所有书籍链接。当点击这个链接时,action-allBooks 的值作为参数添加到 URL 中,如 URL:

    http:localhost:8080/bookWeb/books?action=allBooks
    

    所示

执行步骤 2,从请求中定位 servlet,但是这一次动作不为 null,值为 allBooks。因此,执行 BookController 中 doPost()方法的代码块,如清单 2-33 所示。

清单 2-33 。BookController 中 doPost()中的所有书籍

1.protected void doPost(HttpServletRequest request,
2.HttpServletResponse response) throws ServletException, IOException {
3.String base = "/jsp/";
4.String url = base + "home.jsp";
5.String action = request.getParameter("action");
6.String category = request.getParameter("category");
7.String keyWord = request.getParameter("keyWord");
8.if (action != null) {
9.switch (action) {
10.case "allBooks":
11.findAllBooks(request, response);
12.url = base + "listOfBooks.jsp";
13.break;
14.case "category":
15.findAllBooks(request, response);
16.url = base + "category.jsp?category=" + category;
17.break;
18.case "search":
19.searchBooks(request, response, keyWord);
20.url = base + "searchResult.jsp";
21.break;
22.
23.}
24.}
25.RequestDispatcher requestDispatcher = getServletContext()
26..getRequestDispatcher(url);
27.requestDispatcher.forward(request, response);
28.}
  • 第 8 行:动作不为空,动作的值为 allBooks。
  • 第 10 行到第 12 行:调用帮助器方法 findAllBooks(request,response),重新构造 URL 指向 listOfBooks.jsp,RequestDispatcher 转发到以 URL 形式提供给 RequestDispatcher 的视图。

清单 2-34 显示了 BookController 中的帮助器方法 findAllBooks(请求,响应)。

清单 2-34 。BookController 中的 findAllBooks()

1.private void findAllBooks(HttpServletRequest request,
2.HttpServletResponse response) throws ServletException, IOException {
3.try {
4.BookDAO bookDao = new BookDAOImpl();
5.List<Book> bookList = bookDao.findAllBooks();
6.request.setAttribute("bookList", bookList);
7.
8.} catch (Exception e) {
9.System.out.println(e);
10.}
11.}
  • 第 5 行到第 6 行:使用 DAO 上的 findAllBooks()方法从数据库中获得所有书籍的列表,并在请求中设置为一个属性。

按类别搜索书籍

当用户点击菜单上的特定类别时,显示该类别的图书列表,如图图 2-38 所示。

9781430259831_Fig02-38.jpg

图 2-38 。按类别搜索图书

我们在上一节中看到,这些类别位于 leftColumn.jsp。清单 2-35 展示了类别的代码片段。

清单 2-35 。菜单上的类别链接(leftColumn.jsp)

1.<li>
2.<a class="label" href="<%=param1%>?action=category&categoryId=<%=category1.getId()%>&category=<%=category1.getCategoryDescription()%>"><span class="label" style="margin-left: 30px;"><%=category1.getCategoryDescription()%></span></a>
3.</li>
  • 第二行:这是菜单中显示的类别的链接。当点击这个链接时,类别的 ID 和描述以及动作类别的名称作为参数被添加到 URL 中,如下面的 URL 所示:

    http:localhost:8080/bookWeb/books?action=category&categoryId=1&category=clojure
    

再次执行步骤 2,从请求中定位 servlet,这一次动作不为 null,并且有一个值类别。因此,执行 BookController 中 doPost()方法的代码块,如清单 2-36 所示。

清单 2-36 。BookController 中的 doPost()

1.protected void doPost(HttpServletRequest request,
2.HttpServletResponse response) throws ServletException, IOException {
3.String base = "/jsp/";
4.String url = base + "home.jsp";
5.String action = request.getParameter("action");
6.String category = request.getParameter("category");
7.String keyWord = request.getParameter("keyWord");
8.if (action != null) {
9.switch (action) {
10.case "allBooks":
11.findAllBooks(request, response);
12.url = base + "listOfBooks.jsp";
13.break;
14.case "category":
15.findAllBooks(request, response);
16.url = base + "category.jsp?category=" + category;
17.break;
18.case "search":
19.searchBooks(request, response, keyWord);
20.url = base + "searchResult.jsp";
21.break;
22.
23.}
24.}
25.RequestDispatcher requestDispatcher = getServletContext()
26..getRequestDispatcher(url);
27.requestDispatcher.forward(request, response);
28.}
  • 第 8 行:动作不为空,动作的值为类别。
  • 第 15 行到第 16 行:调用帮助器方法 findAllBooks(request,response),重新构造 URL 指向 listOfBooks.jsp,RequestDispatcher 转发给以 URL 形式提供给 RequestDispatcher 的视图。

清单 2-37 显示了 BookController 中的帮助器方法 findAllBooks(请求,响应)。

清单 2-37 。BookController 中的 findAllBooks()

1.private void findAllBooks(HttpServletRequest request,
2.HttpServletResponse response) throws ServletException, IOException {
3.try {
4.BookDAO bookDao = new BookDAOImpl();
5.List<Book> bookList = bookDao.findAllBooks();
6.request.setAttribute("bookList", bookList);
7.
8.} catch (Exception e) {
9.System.out.println(e);
10.}
11.}
  • 第 5 行到第 6 行:使用 DAO 上的 findAllBooks()方法从数据库中获得所有书籍的列表,并在请求中设置为属性。

通过关键字搜索书籍

您可以通过作者姓名或书名中的关键词来搜索图书,如图图 2-39 所示。

9781430259831_Fig02-39.jpg

图 2-39 。通过关键字搜索图书

在我们讨论关键字搜索如何工作之前,让我们先来看看搜索的一个可用性方面。搜索栏旁边有一个问号,用于帮助用户。换句话说,鼠标悬停时,会显示一个工具提示,指示要使用的搜索参数,如图 2-40 所示。

9781430259831_Fig02-40.jpg

图 2-40 。搜索参数的工具提示

为了理解工具提示是如何工作的,请看一下搜索字段的标记,它在 leftColumn.jsp,如清单 2-38 所示。

清单 2-38 。搜索字段标记

1.<form class="search">
2.<input type="hidden" name="action" value="search" />
3.<input id="text"type="text" name="keyWord" size="12" />
4.<spanclass="tooltip_message">?</span>
5.<p />
6.<input id="submit" type="submit" value="Search" />
7.</form>
  • 第 4 行:这有一个类 tooltip_message。工具提示在这个类上使用 jQuery 和 CSS。
Listing 2-39 illustrates the jQuery code.

清单 2-39 。工具提示的 jQuery

1.$(document).ready(function () {
2.$("span.tooltip_message").hover(function () {
3.$(this).append('<div class="message"><p>Search by Keyword in:<ul><li>Author First Name </li><li>Author Last Name <li>Title of the book </li></ul></p></div>');
4.},function () {
5.$("div.message").remove();
6.});
7.});
  • 第 2 行:类 tooltip_message 和< span >标签被用作调用悬停功能的选择器。
  • 第 3 行:将在工具提示中显示的消息附加到第 2 行选择器返回的对象上。

以类似的方式,工具提示可以添加到主屏幕上的图像,如图图 2-41 所示。清单 2-40 说明了所使用的 jQuery 函数。

9781430259831_Fig02-41.jpg

图 2-41 。图像的工具提示

清单 2-40 。图像提示的 jQuery 函数

$("span.tooltip_img1").hover(function(){$(this).append('<div class="message"><ul><li>Title - Beginning Groovy, Grails and Griffon</li><li>Author: Vishal Layka</li><li>Publisher: Apress</li></ul></div>');
}, function(){$("div.message").remove();});
Listing 2-41 illustrates the CSS code for the tooltip.

清单 2-41 。工具提示的 CSS

1.span.tooltip_message,span.tooltip_img1 {
2.cursor: pointer;
3.display: inline-block;
4.background-color: #F20B26;
5.width: 16px;
6.height: 18px;
7.color: #ffffff;
8.font-size: 12px;
9.font-weight: bold;
10.text-align: center;
11.position: relative;
12.}
13.
14.span.tooltip_message:hover {
15.background-color: #04FF97;
16.}
17.
18.div.message {
19.background-color: #04FF97;
20.color: #000000;
21.position: absolute;
22.left: 18px;
23.top: -18px;
24.z-index: 1000000;
25.text-align: left;
26.width: 280px;
27.}

This CSS and jQuery code in Listing 2-40 and Listing 2-41 are included in leftColumn.jsp, as illustrated in Listing 2-42.

清单 2-42 。在 leftColumn.jsp 访问 CSS 和 jQuery 文件

1.<link rel="stylesheet" href="css/bookstore.css" type="text/css" />
2.<script src="js/bookstore.js" type="text/javascript"></script>
3.<script type="text/javascript" src="js/jquery-1.9.1.js"></script>
  • 第 1 行:这是清单 2-41 所示规则的外部化 CSS 文件
  • 第 2 行:这是清单 2-40 中 jQuery 函数的外部化 JavaScript 文件
  • 第 3 行:第 3 行指定了我们正在使用的 jQuery 库。

现在,我们将从 web 应用中的关键字搜索功能开始。清单 2-43 展示了搜索字段的标记。

清单 2-43 。搜索字段标记

1.<form class="search">
2.<input type="hidden" name="action" value="search" /><input id="text"
3.type="text" name="keyWord" size="12" /><span
4.class="tooltip_message">?</span>
5.<p />
6.<input id="submit" type="submit" value="Search" />
7.</form>
  • 第 2 行:该行指定动作值搜索。
  • 第 6 行:该行提交请求。

当用户提交搜索请求时(再次从请求中定位 servlet),执行第 2 步,这一次操作是值搜索。因此,执行 BookController 中 doPost()方法中的搜索用例,如清单 2-44 中的所示。

清单 2-44 。BookController 中的 doPost()

1.protected void doPost(HttpServletRequest request,
2.HttpServletResponse response) throws ServletException, IOException {
3.String base = "/jsp/";
4.String url = base + "home.jsp";
5.String action = request.getParameter("action");
6.String category = request.getParameter("category");
7.String keyWord = request.getParameter("keyWord");
8.if (action != null) {
9.switch (action) {
10.case "allBooks":
11.findAllBooks(request, response);
12.url = base + "listOfBooks.jsp";
13.break;
14.case "category":
15.findAllBooks(request, response);
16.url = base + "category.jsp?category=" + category;
17.break;
18.case "search":
19.searchBooks(request, response, keyWord);
20.url = base + "searchResult.jsp";
21.break;
22.
23.}
24.}
25.RequestDispatcher requestDispatcher = getServletContext()
26..getRequestDispatcher(url);
27.requestDispatcher.forward(request, response);
28.}
  • 第 18 行:执行案例搜索,动作值为 search
  • 第 19 行:调用 search books()方法。searchBooks()在列表 2-45 中有说明。
  • 第 20 行:第 20 行构造了视图的 URL。
  • 第 26 行:视图的 URL 被提供给 RequestDispatcher。

清单 2-45 展示了控制器用来调用 DAO 的 searchBooks()助手方法。

清单 2-45 。BookController 中的 searchBooks()

1.private void searchBooks(HttpServletRequest request,
2.HttpServletResponse response, String keyWord)
3.throws ServletException, IOException {
4.try {
5.BookDAO bookDao = new BookDAOImpl();
6.List<Book> bookList = bookDao.searchBooksByKeyword(keyWord);
7.
8.request.setAttribute("bookList", bookList);
9.
10.} catch (Exception e) {
11.System.out.println(e);
12.}
13.}
14.
  • 第 6 行:调用 BookDAO 中定义的 searchBooksByKeyword()方法,根据搜索关键字获取图书列表。我们在第一章中为 web 应用构建数据访问层时使用了这种方法。

摘要

本章介绍了 servlets 和 JSP,并向您展示了如何使用这些 web 组件创建您的第一个 web 应用。然后,本章实现了现实世界中基于 MVC 的 Java web 应用,一个使用 servlets 和 JSP 的书店。在下一章中,我们将扩充这个应用,以使用 JSTL 和表达式语言将业务逻辑关注点从表示中分离出来的最佳实践。

www . IETF . org/RFC/RFC 3986 . txt

JCP . org/en/JSR/detail?id=340

3【www.oracle.com/technetwork/java/index-jsp-135475.html】??

三、Java EE Web 开发的最佳实践

我们所知道的所有进化都是从模糊到明确的过程。

-查尔斯·桑德斯·佩尔萨

好的解决方案经常被发明出来。有时它们会被发现。发明和发现不是同义词, 1 ,它们表示不同的目标;然而,两者都是通过经验和专业知识实现的。经验帮助你获得好的解决方案,当你把这些好的解决方案应用到同一套问题中时,模式就开始出现了。模式是源于开发人员的经验和专业知识的好的解决方案的目录。

一位名叫克里斯托弗·亚历山大的建筑师观察到,建筑师倾向于以或多或少相同的方式解决相同的问题。这种认识促使他为架构师写了一本设计模式的书。他在这本书里反思道:“一个设计模式 描述了一个反复出现的问题,然后描述了这个问题的解决方案的核心,这样你就可以用这个解决方案一百万次,而不用以同样的方式做两次。”

1994 年,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides (Addison-Wesley,1994)(“四人组”或“g of”)所著的《设计模式:可重用面向对象软件的元素》一书将 Alexander 的思想应用于软件。这本书阐述了面向对象模式,并引领了最佳实践解决方案的浪潮,用跨应用可重用的设计策略解决了多年来出现的大量设计需求。

随着服务器系统的发展,Java EE 等企业基础设施出现了,它们提供了技术和服务的抽象。然而,使用 Java EE 并不能自然而然地产生最佳实践架构和设计。John Crupi、Dan Malks 和 Deepak Alur 基于他们设计企业系统的经验首先建立了 Java EE 设计模式。

将这些 Java EE 设计模式应用到基于 Java EE 的应用的开发中,是实现最佳实践架构和设计的必要条件。也就是说,确保最佳实践架构和设计不足以确保可重用、可维护、可扩展的软件。即使有了 Java EE 设计模式,一个软件项目也经常受到一种叫做 的现象的困扰,这是一种无序程度的度量。根据热力学第二定律,宇宙中的一切都从低熵(有序状态)向高熵(无序状态)运动,最终陷入混沌。自然界的一切都受这个物理定律的束缚,大自然用进化的手段来处理这个不可改变的定律。

一个软件项目往往倾向于从低熵到高熵,为了处理这种无序状态,它需要不断进化。这并不是说演进你的项目取代了项目管理和方法;尽管有最精细的项目管理,锐利的方法,以及对风格有敏锐感觉的团队,软件项目可能会陷入高熵状态。图 3-1 展示了 Java EE 的 web 层中帮助处理软件项目中熵的进化步骤。

9781430259831_Fig03-01.jpg

图 3-1 。Java EE web 层的演变

正如您在图 3-1 中所看到的,不仅 Java EE web 层中的技术得到了发展,而且每一个新版本中都添加了新的技术。例如,如果您仍在使用 J2EE 1.4,那么您的工具箱中将没有统一的 EL (EL 2.1)。跟上任何技术的发展是很重要的,这样才能避免陷入最终的高熵状态,这种状态会折磨任何软件项目。

本章阐述了用 Java EE 每个新版本中可用的任何新技术逐步发展项目的重要性,并使用它们来处理项目中的高熵。本章接着讨论了 Java EE 模式的重要性,并解释了如何通过使用 Java EE web 层模式使 web 应用变得可重用、可维护和可扩展。接下来,本章将展示 web 框架如何通过提供开箱即用的最佳实践解决方案,让您不再使用 Java EE web 层模式。

最佳实践解决方案:使用 EL 和 JSTL

Sun 在 1998 年发布了 Servlet 规范。servlets 的唯一目的是帮助 Java web 服务器为客户机生成动态内容。清单 3-1 展示了第一个 servlet 的样子。

清单 3-1 。第一个 Servlet

 1.import java.io.IOException;
 2.import java.io.PrintWriter;
 3.
 4.import javax.servlet.ServletException;
 5.import javax.servlet.http.HttpServlet;
 6.import javax.servlet.http.HttpServletRequest;
 7.import javax.servlet.http.HttpServletResponse;
 8.public class Hello extends HttpServlet {
 9.public void doGet(HttpServletRequest req, HttpServletResponse res)
10.throws ServletException, IOException {
11.res.setContentType ("text/html");
12.PrintWriter out = res.getWriter();
13.out.println("<HTML>");
14.out.println("<HEAD><TITLE>Hello World</TITLE></HEAD>");
15.out.println("<BODY>");
16.out.println("<BIG>Hello World</BIG>");
17.out.println("</BODY></HTML>");
18.}
19.}

Servlets 对于动态内容生成工作得很好,但是有一个大问题。视图被硬连接到 servlet 中,正如您在清单 3-1 中的第 13 到 17 行所看到的。为了解决这个问题,JSP 诞生了。JSP 不需要将视图代码硬连接到业务逻辑代码中。视图关注点与业务逻辑关注点的分离取决于清单 3-2 中的属性和清单 3-3 中的属性。

清单 3-2 。使用属性将表示(视图)与业务(Servlet)代码分离

1.public void doPost(HttpServletRequest request, HttpServletResponse response)
2.throws IOException, ServletException {
3.String name = request.getParameter("userName");
4.request.setAttribute("name", name);
5.RequestDispatcher view = request.getRequestDispatcher("/result.jsp");
6.view.forward(request, response);
7.}

清单 3-2 显示了使用属性的 servlet 片段。

  • 第 3 行:从请求中检索用户名。
  • 第 4 行:将用户名设置为请求中的一个属性。
  • 第 5 行:从请求中检索 RequestDispatcher。
  • 第 6 行:转发给视图,传递请求和响应对象。注意,这个请求对象中为用户名设置了一个属性。视图现在可以利用这个属性了

清单 3-3 。第一 JSP

1.<html><body> Hello
2.<%= request.getAttribute("name") %>
3.</body></html>
4.<html>
5.<body>
6.<% User u = (User) request.getAttribute("user"); %>
7.User  is: <%= u.getName() %>
8.</body>
9.</html>

视图与业务逻辑的分离取决于属性,如清单 3-2 和清单 3-3 所示。JSP 以这种方式解决了 servlets 中表示横切业务逻辑的问题。但是正如您在清单 3-4 中看到的,随着 scriptlets (Java 代码)混合在表示代码(JSP)中,现在业务逻辑横切了表示关注点。

清单 3-4 。在 JSP 中使用 Scriptlet 和表达式

1.<% User u = new User(); %>
2.User  is: <%= u.getName() %>
  • Line 1:script let 创建了一个名为 User 的类的实例。
  • 第 2 行:使用表达式输出用户名。

出了什么问题?Scriptlets 和表达式将 Java 代码引入了 JSP。在 JSP 出现之前,表示代码贯穿了业务代码。使用 JSP,业务代码横切了表示代码。因此,不经意间,JSP 实际上并没有解决任何问题,而是颠倒了横切业务和表示逻辑的问题。清单 3-4 中的 scriptlet 和表达式确实可以被 JSP 标准动作(< useBean >)轻松替换,如清单 3-5 所示。

清单 3-5 。使用 JSP 标准动作沙表达语言

1.<jsp:useBean id="user" class="com.apress.User"/>
2.User  is: ${user.name}

清单 3-5 做了与清单 3-4 相同的事情,但是没有在 JSP 页面中使用任何 Java 代码。

  • 第 1 行:这一行使用标准的 JSP 动作创建类 User 的实例。
  • 第 2 行:这一行介绍了表达式语言的关键特性,称为 EL 表达式 ,它取代了称为表达式的脚本元素。第 2 行显示的语法将在接下来的章节中详细讨论。

一般来说,JSP 标准操作非常有限,因此开发人员不得不求助于使用 scriptlets 来创建功能丰富的 web 应用。在 JSP 中以 scriptlets 的形式使用 Java 代码会导致 JSP 页面不可维护。因此,JSP 规范已经发展到支持无 Java 的 JSP 页面。这种支持主要依赖于 JSP 表达式语言(EL)和 JSP 标准标记库(JSTL)。在接下来的几节中,我们将仔细看看埃尔和 JSTL。

表情语言

没有表情的美是没有生命的,没有表情语言的 JSP 是混乱的。表达式语言的基本原则是提供无脚本的 JSP 组件。表达语言有两种用法。

  • 从限定了作用域的属性中检索对象 (在前一章中解释过)。这些对象是 JavaBeans、映射、数组和列表,它们作为属性存储在四个作用域中的任何一个中(也在前一章中解释过)。EL 首先在最小的范围(页面范围)中搜索属性;然后在请求和会话中;最后是最大的范围,即应用范围。
  • 访问请求参数 、请求头、cookies、上下文初始化参数和 pageContext 对象。

通过使用${ expr }或#{expr}构造来形成 EL 表达式。尽管 EL 以相同的方式对这两个构造进行评估,但是${ expr }构造用于立即评估,而#{expr}构造用于延迟评估。

  • 即时求值 :编译 JSP 页面时编译表达式,执行 JSP 页面时执行表达式。
  • 延迟求值 :表达式直到系统需要它的值时才求值。

注意注意在 JSP 2.1 及更新版本中,#{}表达式只允许用于接受延迟表达式的标签属性。#{expr}如果在其他地方使用,将会产生错误。

在接下来的小节中,您将看到 EL 的语法和该语言的保留字,并了解如何在 JSP 页面上使用它。在您学习了基础知识之后,您将学习如何使用 EL 从 JavaBeans 中读取值,在最后一节中,您将学习如何使用 EL 函数。

文字

EL 文字可以是以下类型 :布尔、整数、浮点、字符串或空。表 3-1 显示了每种文字类型的有效值。

表 3-1 。EL 中的文字

|

文字类型

|

有效的文字值

|
| --- | --- |
| 布尔代数学体系的 | 对还是错 |
| 整数 | -11
0
12345 |
| 浮点 | 4.21
-8.01
1.0e 12
0.12 |
| 线 | 都有效:“你好”和“你好” |
| 空 | 空 |

保留字

与任何其他语言一样,EL 有保留的单词,不应用作标识符。表 3-2 列出了 EL 中的保留字。

表 3-2 。EL 中的保留字

image

EL 运算符

EL 操作是处理数据操作所必需的。EL 支持多种运算符,如关系运算符 、算术运算符 、逻辑运算符 等。

算术运算符

清单 3-6 展示了其中的一些操作符。您可以在 JSP 文件上使用这段代码,并在服务器上运行它。图 3-2 说明了输出。

清单 3-6 。算术运算符

<table border="1">
<tr>
<td><b>Arithmetic Operator</b></td>
<td><b>Boolean Result</b></td>
</tr>
<tr>
<td>${'${'}2 + 2 }</td>
<td>${2 + 2}</td>
</tr>
<tr>
<td>${'${'}2 - 2 }</td>
<td>${2 - 2}</td>
</tr>
<tr>
<td>${'${'}2 * 2 }</td>
<td>${2 * 2}</td>
</tr>
<tr>
<td>${'${'}2 / 2 }</td>
<td>${2 / 2}</td>
</tr>
<tr>
<td>${'${'}2 mod 2 }</td>
<td>${2 mod 2}</td>
</tr>
</table>

9781430259831_Fig03-02.jpg

图 3-2 。算术运算符

关系运算符

这些运算符包括==,!=,,<=, > =,eq,ne,lt,gt,le,ge。

清单 3-7 展示了所有这些操作符。您可以在 JSP 文件上使用这段代码,并在服务器上运行它。输出如图图 3-3 所示。

清单 3-7 。关系运算符

<table border="1">
<tr>
<td><b>Relational Operator</b></td>
<td><b>Boolean Result</b></td>
</tr>
<tr>
<td>${'${'}10 &lt; 20}</td>
<td>${10 < 20}</td>
</tr>
<tr>
<td>${'${'}10 &gt; 20}</td>
<td>${10 > 20}</td>
</tr>
<tr>
<td>${'${'}10 &gt;= 10}</td>
<td>${10 >= 10}</td>
</tr>
<tr>
<td>${'${'}10 &lt;= 10}</td>
<td>${10 <= 10}</td>
</tr>
<tr>
<td>${'${'}10 == 10}</td>
<td>${10 == 10}</td>
</tr>
<tr>
<td>${'${'}10 != 20}</td>
<td>${10 != 20}</td>
</tr>
<tr>
<tr>
<td>${'${'}10 lt 20}</td>
<td>${10 lt 20}</td>
</tr>
<tr>
<td>${'${'}10 gt 20}</td>
<td>${10 gt 20}</td>
</tr>
<tr>
<td>${'${'}10 le 10}</td>
<td>${10 le 10}</td>
</tr>
<tr>
<td>${'${'}10 ge 10}</td>
<td>${10 ge 10}</td>
</tr>
<tr>
<td>${'${'}10 eq 10}</td>
<td>${10 eq 10}</td>
</tr>
<tr>
<td>${'${'}10 ne 20}</td>
<td>${10 ne 20}</td >
</tr>
</table>

9781430259831_Fig03-03.jpg

图 3-3 。关系运算符

逻辑运算符

清单 3-8 展示了逻辑操作符,如& &、||,以及 not 操作符。您可以在 JSP 文件上使用这段代码,并在服务器上运行它。输出如图图 3-4 所示。

清单 3-8 。逻辑运算符

<table border="1">
<tr>
<td><b>Logical Operator</b></td>
<td><b>Result</b></td>
</tr>
<tr>
<td>${'${'}true && false}</td>
<td>${true && false}</td>
</tr>
<tr>
<td>${'${'}true || false}</td>
<td>${true || false}</td>
</tr>
<tr>
<td>${'${'}not true}</td>
<td>${not true}</td>
</tr>
</table>

9781430259831_Fig03-04.jpg

图 3-4 。逻辑运算符

使用 EL

在本节中,您将基于我们的书店应用的模型创建一个简单的应用。这不仅会告诉你如何使用 EL,还会展示它的重要性。图 3-5 展示了应用中图书和作者之间的关系,在清单 3-9 和清单 3-10 中实现。

9781430259831_Fig03-05.jpg

图 3-5 。书和作者之间的关系

清单 3-9 。Author.java

 1.package com.apress.chapter03.model;
 2.
3.public class Author {
 4.private String name;
 5.
 6.public String getName() {
 7.return name;
 8.}
 9.
10.public void setName(String name) {
11.this.name = name;
12.}
13.
14.}

清单 3-10 。Book.java

 1.package com.apress.chapter03.model;
 2.
 3.public class Book {
 4.
 5.private String bookTitle;
 6.private Author author;
 7.
 8.public String getBookTitle() {
 9.return bookTitle;
10.}
11.
12.public void setBookTitle(String bookTitle) {
13.this.bookTitle = bookTitle;
14.}
15.
16.public Author getAuthor() {
17.return author;
18.}
19.
20.public void setAuthor(Author author) {
21.this.author = author;
22.}
23.
24.}

应用的目标是展示如何访问一个属性的属性(图 3-5 中图书的图书标题属性)和一个属性本身的属性(图 3-5 中作者的姓名属性)。在图 3-5 中,需要输出作者姓名属性的值。使用 JSP 标准动作是不可能做到这一点的,您将在本节的后面看到,在这种情况下,使用了 scriptlets。这就是 scriptlets 进入 JSP 的方式。但是,您不应该使用 script let,因为 script let(Java 代码)混合在表示代码(JSP)中;业务逻辑横切了表示关注点,导致了不可维护的 JSP,如前面的清单 3-4 中所解释的。因为 JSP 标准操作不能访问本身就是属性的属性的属性,并且因为使用 scriptlets 会导致不可维护的 JSP,所以应该使用 EL。在本节中,您将借助一个例子学习如何使用 EL,其中 Author class 是 Book 类的属性。图 3-5 显示了书和作者的关系。您将从 Book 中访问 Author 的 name 属性,而不使用 scriptlets。

清单 3-9 展示了带有一个名为 name 的属性的 Author 对象,该属性带有 getters 和 setters。您需要输出 Author 的 name 属性的值。

清单 3-10 展示了 Book 对象的两个属性,bookTitle 和 author,以及它们的 getters 和 setters。Book 中的 author 属性是 Author 类,如前面的清单 3-9 所示。您需要访问 author 属性的 name 属性。

Book 和 Author 对象充当 MVC 应用的模型。清单 3-11 展示了应用的控制器。

清单 3-11 。BookController.java

 1.package com.apress.chapter03.controller;
 2.
 3.import java.io.IOException;
 4.
 5.import javax.servlet.RequestDispatcher;
 6.import javax.servlet.ServletException;
 7.import javax.servlet.http.HttpServlet;
 8.import javax.servlet.http.HttpServletRequest;
 9.import javax.servlet.http.HttpServletResponse;
10.
11.import com.apress.chapter03.model.Author;
12.import com.apress.chapter03.model.Book;
13.
14.public class BookController extends HttpServlet {
15.
16.protected void doGet(HttpServletRequest request,
17.HttpServletResponse response) throws ServletException, IOException {
18.Book book = new Book();
19.book.setBookTitle("Learning Java Web");
20.Author author = new Author();
21.author.setName("Vishal Layka");
22.book.setAuthor(author);
23.
24.request.setAttribute("bookAttrib", book);
25.
26.RequestDispatcher view = request.getRequestDispatcher("/book.jsp");
27.view.forward(request, response);
28.}
29.
30.}

清单 3-11 是 MVC 模式的控制器部分。正如您在清单 3-2 中了解到的,视图关注点和业务逻辑关注点的分离取决于属性。因此,您必须将模型对象保存到视图(JSP)的属性中,以便能够通过属性访问模型。

  • 第 19 行到第 22 行:在这几行中你设置了图书的书名和作者属性。注意,第 21 行已经设置了 Author 的 name 属性。
  • 第 22 行:设置图书的作者属性。
  • 第 24 行:将 Book 对象设置为请求中的一个属性。
  • 第 26 行到第 27 行:第 26 行你现在应该很熟悉了。在这一行中,您将请求发送到 book.jsp。

清单 3-12 提供了这个应用的部署描述符。

清单 3-12 。web.xml

 1.<?xml version="1.0" encoding="UTF-8"?>
 2.<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3\. fontname">http://java.sun.com/xml/ns/javaee"
 4\. xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 5\. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
 6.http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
 7.<display-name>chapter03</display-name>
 8.<servlet>
 9.<servlet-name>BookController</servlet-name>
10.<servlet-class>com.apress.chapter03.controller.BookController</servlet-class>
11.</servlet>
12.<servlet-mapping>
13.<servlet-name>BookController</servlet-name>
14.<url-pattern>/book</url-pattern>
15.</servlet-mapping>
16.<welcome-file-list>
17.<welcome-file>index.html</welcome-file>
18.</welcome-file-list>
19.</web-app>

现在,这个 web 应用中唯一缺少的关键组件是 JSP 页面,这是实际编写 EL 代码的地方。在我们进入 JSP 页面之前,我们将看一下 EL 提供的访问封装数据的两个关键操作符:[ ]和。(也叫运算符)。在了解了[ ]和。运营商,你就能写 book.jsp。图 3-6 说明了应用的目录结构。

9781430259831_Fig03-06.jpg

图 3-6 。目录结构

[ ]和。操作员

使用。符号是访问对象属性的快捷方式。点 操作符是在清单 3-5 中引入的。为了方便起见,在清单 3-13 中再次显示。

清单 3-13 。使用点运算符

1.<jsp:useBean id="user" class="com.apress.User"/>
2.User  is: ${user.name}
  • 第 1 行:在第 1 行中,使用动作创建了类 User。用户被设置为 servlet 代码中的请求属性。
  • 第 2 行:在第 2 行中,name 的值是使用${user.name}访问的用户对象的属性。

表达式${ user . name }中的变量 user 是存储在请求范围中的属性。应用点运算符的 EL 表达式中的变量可以是两种类型之一。

  • 存储在四个作用域中的任何一个中的属性,如本例所示
  • 一个 EL 隐式对象,这将在本章后面解释

无论这个变量是 EL 隐式对象还是存储在四个作用域中的属性,它都可以是 JavaBean 或 map。在清单 3-13 中,这个变量是一个 JavaBean,它被设置为请求范围中的一个属性,因此 name 是变量 user 引用的 JavaBean 的属性。如果用户变量是在四个作用域中的任何一个中设置为属性的映射,那么名称将成为映射的键。

注意应用了点运算符的 EL 表达式中的变量可以是四个作用域中的一个属性集,也可以是一个 EL 隐式对象。此外,不管这个变量是四个作用域中的一个属性集还是一个 EL 隐式对象,它都可以是 JavaBean 或 Map。如果变量是 JavaBean,那么它的属性就在点运算符之后;如果变量是一个 Map,那么它的键就在点运算符之后。

所以,现在应该清楚了,EL 表达式中的变量要么是 JavaBean,要么是 Map。但是,如果您想将一个数组设置为四个作用域中的一个属性,并使用 EL 表达式访问它的元素,该怎么办呢?或者,如果您想将一个列表设置为四个作用域中的一个属性,并使用 EL 表达式访问它的元素,该怎么办呢?答案在于 EL 提供的[]运算符 。

[]操作符用于访问数组、列表、JavaBeans 和映射。也就是说,应用[ ]运算符的变量可以是数组、列表、JavaBean 或映射。

在括号内

[ ] 运算符的括号内可以有下列之一:

  • 带或不带引号的索引。
  • 字符串文字。
  • 四个范围中任何一个的 EL 隐式对象或属性。EL 隐式对象将在本章后面解释。
  • 嵌套表达式。

如果[ ]运算符的括号内有带引号或不带引号的索引,则[ ]运算符所应用到的变量要么是数组,要么是列表。清单 3-14 展示了如何在列表或数组中使用[ ]操作符。

清单 3-14 。对列表或数组使用[ ]运算符

someArray["1"]
someArray[1]
someList["2"]
someList[2]

例如,清单 3-15 中的数组可以像清单 3-16 中所示的那样被访问。

清单 3-15 。在 Servlet 代码中将数组设置为属性

1.   String [ ] books = {"Clojure", "Groovy ", "Java" , "Scala"} ;
2.   request.setAttribute("books", books);

清单 3-16 。使用[ ]运算符

Book: ${books[0]}

清单 3-16 显示的输出如下:

Book: Clojure

注意列表可以像数组一样被访问。

如果[ ]运算符的括号中有一个字符串文字,那么[ ]运算符所应用到的变量要么是 JavaBean,要么是 Map。清单 3-17 展示了在 Servlet 中将地图设置为属性的代码。

清单 3-17 。用于在 Servlet 代码中将地图设置为属性的代码片段

1.Map<String, String>  bookMap  = new HashMap<>();
2.bookMap.put("Groovy", "Beginning Groovy");
3.bookMap.put("Java", " Beginning Java");
4.bookMap.put("Scala", " Beginning Scala");
5.request.setAttribute("books", bookMap);

在清单 3-18 中,EL 在范围内搜索绑定到姓名簿的属性。在清单 3-17 中,books 是请求属性中的一个映射集。因此,EL 搜索在清单 3-18 的[ ]操作符中传递的关键 Groovy 并对其求值。

清单 3-18 。使用[ ]运算符

Book : ${books["Groovy"] }

清单 3-8 显示的输出如下所示:

Book: Beginning Groovy

都是。和[]运算符可以与 JavaBeans 或 Maps 一起使用。例如,清单 3-18 可以用。操作符如下所示:

${books.Groovy}

如果[ ]运算符的括号内既没有字符串也没有带引号或不带引号的索引,并且[ ]运算符的括号内的内容不是 EL 隐式对象,则通过在四个范围中的任何一个范围中搜索具有该名称的属性来评估内容。这用清单 3-19 和清单 3-20 来说明。

清单 3-19 。用于在 Servlet 代码中将地图设置为属性的代码片段

1.Map<String, String>  bookMap  = new HashMap<>();
2.bookMap.put("Groovy", "Beginning Groovy");
3.bookMap.put("Java", " Beginning Java");
4.bookMap.put("Scala", " Beginning Scala");
5.request.setAttribute("books", bookMap);
6.request.setAttribute("java", "Java");

清单 3-20 。使用[ ]运算符

Book : ${books[java] }

让我们看看清单 3-20 中的评估是如何工作的。

  • 在清单 3-20 的代码${ books[java] }中,EL 在作用域中搜索由名称 books 绑定的属性。
  • EL 在请求范围中找到这个属性,因为 books 在清单 3-19 的第 5 行被设置为请求属性。
  • 在清单 3-20 中,【】运算符的内容是 java,既不是字符串文字,也不是 EL 隐式对象;因此,EL 在作用域中搜索由名称 java 绑定的属性,并在请求作用域中找到它,因为 java 在清单 3-19 的第 6 行被设置为请求属性。
  • 使用清单 3-19 的第 6 行 java 的值 Java,EL 表达式现在变成${ books["Java"] }。
  • 现在,因为 books 属性是一个在清单 3-19 的第 5 行设置为请求属性的映射,EL 搜索关键字 Java,它在清单 3-19 的第 3 行中。并打印其值,如以下输出所示:
Book  : Beginning Java

如果[]运算符的括号内有一个 EL 表达式,括号内的内容(在本例中为 EL 表达式)将按照适用于任何 EL 表达式的相同规则进行计算。换句话说,如果 EL 表达式使用点运算符或[]运算符,并且它是[]运算符,则应用前面解释的相同规则。这用清单 3-21 和清单 3-22 来说明。

清单 3-21 。用于在 Servlet 代码中将映射和数组设置为属性的代码片段

1.Map<String, String>  bookMap  = new HashMap<>();
2.bookMap.put("Groovy", "Beginning Groovy");
3.bookMap.put("Java", " Beginning Java");
4.bookMap.put("Scala", " Beginning Scala");
5.request.setAttribute("books", bookMap);
6.
7.String[ ] categories = {"Groovy", "Java", "Scala"};
8.request.setAttribute("category", categories);

清单 3-22 。嵌套 EL 表达式

Book : ${ books[category[1]] }

以下是输出结果:

Book : Beginning Java

现在,您已经了解了使用。和[ ]运算符,是时候看看为什么 EL 如此重要了。现在,通过完成您开始创建的应用,特别是通过编写 JSP 页面,您将明白为什么它如此重要。清单 3-23 展示了 book.jsp。这个 JSP 页面使用脚本元素(scriptlets 和表达式)和 EL。想法是比较两者,即脚本元素和 EL。这种比较在图 3-7 中进行了说明。

清单 3-23 。book.jsp

 1.<%@page import="com.apress.chapter03.model.Book"%>
 2.<%@page import="com.apress.chapter03.model.Author"%>
 3.<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>
 4.<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c"%>
 5.<html>
 6.<head>
 7.</head>
 8.<body>
 9.<table border="1">
10.<tr>
11.<th width= "20px">Description</th>
12.<th >code</th>
13.<th >output</th>
14.</tr>
15.<%
16.Book book = (Book) request.getAttribute("bookAttrib");
17.Author author = book.getAuthor();
18.%>
19.<tr>
20.<td>Author's Name using <b>Scriptlet and Expression</b>
21.</td>
22.<td>${fn:escapeXml("<%= author.getName() %>")}</td>
23.<td><%=author.getName()%></td>
24.</tr>
25.
26.<jsp:useBean id="bookAttrib" class="com.apress.chapter03.model.Book"
27.scope="request" />
28.<tr>
29.<td>Author's Name using <b> jsp:getProperty action </b>
30.</td>
31.<td>
32.<table border="1">
33.<tr>
34.<td>${fn:escapeXml("<jsp:getProperty name = \"bookAttrib \" property= \"author \" />")}</td>
35.</tr>
36.<tr>
37.<td>${fn:escapeXml("<jsp:getProperty name = \"bookAttrib \" property= \"author.name \" />")}</td>
38.</tr>
39.</table>
40.
41.</td>
42.<td>
43.<table border="1">
44.<tr>
45.<td><jsp:getProperty name="bookAttrib" property="author" />
46.</td>
47.</tr>
48.<tr>
49.<td>
50.<%--  <jsp:getProperty name ="bookId" property="author.name" />  - this code will yield run time exception --%>
51.Not possible
52.</td>
53.</tr>
54.</table>
55.</td>
56.</tr>
57.<tr>
58.<td>Author's Name using<b> EL </b></td>
59.<td>${fn:escapeXml("${bookAttrib.author.name}")}</td>
60.<td>${bookAttrib.author.name}</td>
61.</tr>
62.</table>
63.</body>
64.</html>
  • 第 23 行:使用表达式输出作者的名字。
  • 第 50 行:这表明不能使用 JSP 标准动作输出作者的名字。
  • 第 60 行:使用 EL 输出作者的名字。

图 3-7 展示了当你运行这个应用时会看到什么(localhost:8080/chapter 03/book)。本质上,使用标准的 JSP 动作显示 Author 的 name 属性的值是不可能的;如果没有 EL,脚本元素将是唯一的方法。

9781430259831_Fig03-07.jpg

图 3-7 。比较 scriptlets、标准操作和 EL

EL 隐含对象

Scriptlets 可以访问几个 JSP 隐式对象,如第二章中所解释的。这些对象允许访问保存在特定 JSP 范围内的任何变量。EL 也提供了自己的隐式对象,称为 EL 隐式对象。EL 隐式对象和 JSP 隐式对象不一样(pageContext 除外)。所有这些 EL 隐式对象都是映射 ,它们将各自的范围属性名称映射到它们的值。例如,使用隐式对象 param 和 paramValues,可以访问 HTTP 请求参数。表 3-3 描述了 EL 隐式对象。

表 3-3 。EL 中的隐式对象

|

隐含对象

|

描述

|
| --- | --- |
| 甜饼干 | Map:将 cookie 名称映射到单个 Cookie 对象。 |
| 页眉 | Map:包含每个头名称的值 |
| 标题值 | Map:将头名称映射到头的所有可能值的字符串数组。 |
| initParam(启动参数) | 映射:将上下文初始化参数名映射到它们的字符串参数值。 |
| 参数 | Map:包含页面参数的名称。 |
| 参数值 | Map:将参数名映射到参数的所有值的字符串数组。 |
| 对象 | PageContext 对象。 |
| 应用范围 | Map:包含所有应用范围的变量。 |
| pageScope(页面范围) | Map:包含所有页面范围的变量。 |
| 请求作用域 | Map:包含所有请求范围的变量。 |
| 会话范围 | Map:包含所有会话范围的变量。 |

使用 EL 隐式对象

在表 3-3 中列出的 EL 隐式对象中,applicationScope、pageScope、requestScope 和 sessionScope 用于指定作用域。它们用于访问限定了作用域的属性,也就是说,从 JavaBeans、映射、数组和列表中访问数据,这些数据已经作为属性存储在以下四个作用域中:页面、请求、会话和应用。

在表 3-3 中列出的其他隐式对象用于访问请求参数、请求头、cookies、上下文初始化参数和页面上下文对象。本节说明了一些 EL 隐式对象的用法。

访问请求参数

清单 3-24 展示了一个通过 form.jsp 提交请求参数的简单表格。

清单 3-24 。form.jsp

 1.<body>
 2.<form action="books" method="post">
 3.<input type="hidden" name="action" value="books"/>
 4.<p>Book Title: <input type="text" name="bookTitle"></p>
 5.<p>Author 1 Name: <input type="text" name="authorName"></p>
 6.<p>Author 2 Name: <input type="text" name="authorName"></p>
 7.
 8.<input type = "submit"/>
 9.</form>
10.</body>

在清单 3-24 中,<输入>标签的 name 属性是相同的:authorName。

清单 3-25 说明了使用 EL 隐式对象 param 和 paramValues 来检索请求参数并在 result.jsp 页面上显示结果。

清单 3-25 。result.jsp

1.<p>Book Title: ${param.bookTitle}<br>
2.Author 1: ${paramValues.authorName[0]}<br>
3.Author 2: ${paramValues. authorName[1]}
4.</p>
  • Line 1 :使用 EL 隐式对象 param 来获取书名。
  • 第 2 行到第 3 行:这段代码使用 EL 隐式对象 paramValues 来获取作者 1 和作者 2 的名字。

访问标题

EL 隐式对象 header 和 headerValues 使您可以访问可使用 request.getHeader()和 request.getHeaders()方法获得的头值。

清单 3-26 展示了使用表达式\({header.user-agent}或\){header["user-agent"]}访问名为 user-agent 的头。

清单 3-26 。使用 EL 隐式对象头

<span>${header["user-agent"]}</span>

以下是输出结果:

Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20100101 Firefox/12.0

正在访问 Cookie

El 隐式对象 cookie 让我们可以访问 cookie。清单 3-27 展示了存储在 servlet 中的 cookie。

清单 3-27 。在 Servlet 中设置 Cookie

1.String userName = "vishal";
2.Cookie c = new Cookie("userName", userName);
3.c.setPath("/");
4.response.addCookie(c);

清单 3-28 展示了如何使用一个 EL 隐式对象来访问 JSP 页面中的 cookie。

清单 3-28 。使用 EL 隐式对象 Cookie

${cookie.userName.value}

访问限定了作用域的属性

El 隐式对象 sessionScope 提供对存储在会话范围中的属性的访问。清单 3-29 展示了存储在 servlet 会话中的属性。

清单 3-29 。在 Servlet 中设置会话属性

HttpSession session = request.getSession();
Book book = new Book();
book.setBookTitle("Beginning Java");
session.setAttribute("book", book);

清单 3-30 展示了使用一个名为 sessionScope 的 EL 隐式对象来访问 JSP 页面中的书名。

清单 3-30 。使用 EL 隐式对象会话范围

<span>Book title in Session Scope ${sessionScope.book.bookTitle}</span>

EL 函数

EL 函数允许您从 JSP 中调用 Java 方法,而无需使用脚本。EL 函数被映射到 Java 类的静态方法。这种映射是在标签库描述符(TLD)中指定的,这将在本节后面解释。清单 3-31 展示了一个简单的返回当前日期和时间的 Java 方法。

清单 3-31 。具有公共和静态方法的 Java 类

 1.package com.apress.elfunction;
 2.
 3.import java.text.SimpleDateFormat;
 4.import java.util.Calendar;
 5.
 6.public class Now {
 7.
 8.public static String now() {
 9.Calendar currentDate = Calendar.getInstance();
10.SimpleDateFormat formatter = new SimpleDateFormat(
11."yyyy/MMM/dd HH:mm:ss");
12.String now = formatter.format(currentDate.getTime());
13.
14.return now;
15.}
16.}
17.

在 EL 函数中使用 Java 方法的关键要求是该方法必须是公共的和静态的。EL 职能中的三个关键角色如下:

  • 在类中定义的 Java 方法
  • 使用 EL 调用 Java 方法的 JSP 页面
  • 一个标记库描述符文件,它将 Java 类中的 Java 方法映射到调用这个 Java 方法的 JSP 代码

清单 3-32 说明了标签库描述符文件。TLD 是一个声明标记库的 XML 文件。这个 TLD 文件 包含一个或多个 EL 函数的声明和映射。每个函数都有一个名称和一个实现该函数的 Java 类中的特定方法。

清单 3-32 。标签库描述符

 1.<?xml version="1.0" encoding="UTF-8"?>
 2.<taglib version="2.1" fontname">http://java.sun.com/xml/ns/javaee"
 3.xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4.xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
 5.http://java.sun.com/xml/ns/javaee/webjsptaglibrary_2_1.xsd">
 6.<tlib-version>1.2</tlib-version>
 7.<uri>elFunction</uri>
 8.<function>
 9.<name>now</name>
10.<function-class>
11.com.apress.elfunction.Now
12.</function-class>
13.<function-signature>
14.String now()
15.</function-signature>
16.
17.</function>
18.</taglib>
  • 第 7 行:第 7 行是函数的 URI,将在 JSP 页面的 taglib 指令中使用。
  • 第 8 行到第 17 行:这几行指定了将用签名调用的 Java 方法和定义该方法的 Java 类。

可以通过以下方式使用 EL 从 JSP 中调用 Java 方法:${prefix:function-name)。名称空间前缀是通过在 JSP 页面中使用 taglib 指令来声明的。清单 3-33 显示了 JSP 代码。

清单 3-33 。在 JSP 中调用 EL 函数

1.<%@ taglib prefix="elf" uri="elFunction"%>
2.<html>
3.
4.<body>${elf:now() }
5.</body>
6.</html>
  • 第 1 行:第 1 行是一个 taglib 指令,带有前缀 elf 和一个 URI。URI 在标签库描述符中定义。
  • 第 4 行:使用前缀 elf,调用函数。这个函数名在标签库描述中定义。

您可以基于我们在第二章中创建的第一个 JSP 应用来创建这个应用。当这个应用运行时(localhost:8080/El functions/El test . JSP),它给出如图图 3-8 所示的输出。

9781430259831_Fig03-08.jpg

图 3-8 。调用 EL 函数的输出

JSTL

JSP 标准标记库(JSTL)的最终目标是帮助简化 Java 服务器页面的开发。正如上一节所讨论的,scriptlets 导致不可维护的 JSP,并且可以被 JSP 标准操作所替代。然而,标准的行动太有限了;更好的方法是让 Java 开发人员创建他们自己的定制动作。尽管如此,创建一个定制动作仍然是一项不简单的任务。JSTL 提供了这样的自定义操作,可以处理常见的重复任务,而 JSTL 包含了划分为不同功能区域的各种各样的操作。表 3-4 列出了功能区以及用于引用库的 URIs 和 JSTL 规范中使用的前缀。

表 3-4 。JSTL 标签库

|

功能区

|

上呼吸道感染

|

前缀

|
| --- | --- | --- |
| 核心 | java.sun.com/jsp/jstl/core | c |
| XML 处理 | java.sun.com/jsp/jstl/xml | x |
| 支持 I18N 的格式 | java.sun.com/jsp/jstl/fmt | 滤波多音 |
| 关系数据库访问 | java.sun.com/jsp/jstl/sql | 结构化查询语言 |
| 功能 | java.sun.com/jsp/jstl/functions | 【数学】函数 |

许多 JSTL 操作会导出您可以通过表达式语言轻松访问的限定了作用域的变量。正如我们在前一章看到的,作用域变量是存储在 JSP 作用域之一的对象:应用、页面、请求和会话。当 JSTL 操作使一个作用域变量对一个或多个 JSP 页面可用时,它有一个名为 var 的属性,允许您指定该作用域变量的名称。清单 3-34 展示了在 JSTL 的核心标签库中可用的< c:set >动作,用来设置一个作用域变量的值。

清单 3-34 。使用< c:设置>

<c:set var="name" value="hello" scope="session" />

清单 3-34 将名为 name 的变量设置为 hello 值,并允许该变量在会话范围内可见。如果未指定 scope 的值,则默认范围是页面范围。在接下来的部分中,您将学习核心标记库中提供的不同功能区域中的所有操作。

核心标签库

表 3-5 描述了核心标签库中的核心动作。

表 3-5 。核心标签库中的核心动作

|

行动

|

描述

|
| --- | --- |
| | 捕捉在操作体中引发的异常 |
| | 选择许多代码片段中的一个 |
| | 迭代对象集合或迭代固定次数 |
| | 迭代字符串中的标记 |
| | 有条件地执行某些功能 |
| | 导入 URL |
| | 在动作中指定默认功能 |
| | 将输出发送到当前 JspWriter |
| | 为指定一个 URL 参数 |
| | 将响应重定向到指定的 URL |
| | 移除限定了作用域的变量 |
| | 创建一个限定了作用域的变量 |
| | 创建一个 URL,并根据需要重写 URL |
| | 指定动作中的几个条件之一 |

JSTL 核心图书馆可分为四个不同的功能区域,如表 3-6 所述。

表 3-6 。JSTL 核心图书馆

|

功能区

|

描述

|
| --- | --- |
| 通用操作 | 用于操作限定了作用域的变量 |
| 条件动作 | 用于 JSP 页面中的条件处理 |
| 迭代器操作 | 用于循环访问对象集合 |
| 与 URL 相关的操作 | 用于处理 JSP 页面中的 URL 资源 |

通用动作

通用操作提供了使用限定了作用域的变量的方法。表 3-7 描述了核心标签库中的通用动作。

表 3-7 。通用操作

|

行动

|

描述

|
| --- | --- |
| | 计算表达式并将结果输出到 JspWriter 对象 |
| | 设置目标对象的作用域变量或属性的值 |
| | 移除限定了作用域的变量 |
| | 捕捉由其任何嵌套操作引发的 java.lang.Throwable |

动作

动作计算一个表达式并显示结果。它相当于 JSP 语法。

下面是不带正文的语法:

<c:out value="value" [escapeXml="{true|false}"][default="defaultValue"] />

以下是正文的语法:

<c:out value="value" [escapeXml="{true|false}"]>
default value
</c:out>Items in brackets are optional

表 3-8 描述了< c:out >的属性。由于这一章全面地涵盖了 JSTL,我推荐你阅读 JSTL 规范 3 以彻底理解如何使用 JSTL 属性。

表 3-8 。< c:out >属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 价值 | 目标 | 这是要计算的表达式。 |
| escapeXml | 布尔 | 这决定了结果字符串中的字符、&、'、"是否应该转换为它们对应的字符实体代码。默认值为 true。 |
| 系统默认值 | 目标 | 如果结果值为空,则这是默认值。 |

要写入 JspWriter 的值被指定为 value 属性。您可以在 value 属性中使用表达式。

  • value:要计算的表达式通过 value 属性提供,结果在作为响应的一部分返回之前被转换成一个字符串。
  • default:您可以选择指定一个默认值,如果指定的值为 null 或者不是一个有效的表达式,会将该值发送给当前的 JspWriter。您可以使用 default 属性或在动作的主体中指定默认值。
  • escape XML:escape XML 属性指定是否将某些字符转换成在表 3-9 中列出的 HTML 字符实体代码。默认情况下,escapeXmlattribute 设置为 true。如果为 escapeXml 属性指定 false,< c:out >将不会转换这些字符。

表 3-9 。字符实体代码

|

性格;角色;字母

|

字符实体代码

|
| --- | --- |
| < | < |
| > | > |
| & | & |
| ' | ' |
| " | " |

清单 3-35 展示了用< c:out >动作替换脚本元素。

清单 3-35 。比较< c:out >和表情

<%= "hello" %>  // Ouput "hello" using Expression
<c:out  value = "hello"/> // Ouput "hello" using <c:out> action

行动

JSTL 设置标签或动作设置目标对象的作用域变量或属性的值。与使用< jsp:setProperty > JSP 动作相比,< c:set >动作是一个更好的选择。与只允许设置 bean 属性的< jsp:setProperty >不同的是,< c:set >标签可以做以下事情:

  • 设置 bean 属性
  • 设置地图值
  • 在页面、请求、会话或应用范围内创建限定了范围的变量

表 3-10 描述了< c:set >动作的属性。

表 3-10 。< c:设置>属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 价值 | 目标 | 要计算的表达式。 |
| 定义变量 | 线 | 导出的作用域变量的名称,用于保存操作中指定的值。 |
| 范围 | 线 | var 的范围。 |
| 目标 | 目标 | 将设置其属性的目标对象。这必须计算为具有 setter property 属性的 JavaBeans 对象或 java.util.Map 对象。 |
| 财产 | 线 | 要在目标对象中设置的属性的名称。 |

标签用于执行以下操作:

  • 在任何 JSP 作用域中设置作用域变量的值。
  • 设置指定目标对象的属性。目标必须计算为 JavaBean 或 Map 对象。

在任何 JSP 作用域中设置作用域变量的值

动作的任务之一是设置页面上其他动作可以使用的变量。

下面是语法:

<c:set value=""value""var=""varName" " [scope=""{page|request|session|application}""]/>

清单 3-36 显示了一个使用< c:set >来设置作用域变量 helloVar 的值的例子。

清单 3-36 。使用< c:设置>

<c:set var="helloVar" value="hello" />

清单 3-36 创建一个名为 helloVar 的属性,在默认范围内,即页面范围内,该属性的值为“hello”。您还可以在另一个范围内创建属性,比如会话范围;在这种情况下,您需要用< c:set >属性 scope= " "来指定作用域。清单 3-37 使用< c:set >在会话范围内创建一个变量。

清单 3-37 。使用范围

<c:set var="helloVar" value="hello" scope="session" />

我们也可以通过在动作的主体内容中提供值来表达这一点,如清单 3-38 所示。

清单 3-38 。使用< c:将>与机体固定在一起

<c:set var="hello" scope="session" >
helloworld
</c:set>

变量值也可以是 EL 表达式,如清单 3-39 所示。

清单 3-39 。在值中使用 EL 表达式

<c:set var="titleVar" value="${book.title}" scope="session" />

在清单 3-39 中,book bean 的 title 属性设置在 titleVar 变量中。

清单 3-40 显示了相当于< c:set >的 scriptlet,如清单 3-36 所示。

清单 3-40 。相当于<的 c:set >

<%
  String helloVar = "hello";
  pageContext.setAttribute("helloVar ", helloVar);
%>

设置指定目标对象的属性

为了能够使用设置 bean 属性或映射值,我们需要使用 target 和 property,而不是 var,var 将定义要设置的 bean 和属性名称。例如,如果 target 是 map,那么 property 是键的名称,value 是该键的值。

下面是语法:

<c:set value=""value""target=""target"" property=""propertyName""/>
  • 如果使用目标对象,目标必须计算为 JavaBean 或 java.util.Map 对象。
  • 如果目标是 JavaBean,它必须包含适当的 getter/setter 方法。

清单 3-41 展示了如何使用< c:set >标签设置一个映射键。

清单 3-41 。使用< c:set >标签设置地图键

<c:set target="bookMap" property="id" value="1">

这相当于 bookMap.put("id "," 1 ");。您也可以在标签的主体中提供该值。

清单 3-42 展示了如何使用< c:set >来设置 bean 属性。

清单 3-42 。使用< c:set >标签设置 bean 属性

<c:set target="book" property="book.title" value="Learning Java Web">

清单 3-42 将名为 book 的 bean 的 title 属性设置为 Learning Java Web。这相当于 book.setTitle(“学习 Java Web”);。

动作

动作从一个特定的作用域中删除一个变量。通过在 var 属性和 scope 属性中指定变量名,可以使用< c:remove >动作删除< c:set >在任何作用域中设置的变量。

下面是语法:

<c:remove var="varName"[scope="{page|request|session|application}"]/>

表 3-11 描述了< c:移除>动作的属性。

表 3-11 。< c:移除>属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 定义变量 | 线 | 要移除的作用域变量的名称 |
| 范围 | 线 | 风险值的范围 |

清单 3-43 展示了< c:remove >的简单用法。

清单 3-43 。使用< c:移除>

<c:remove var=" helloVar " />

动作

动作提供了一种捕捉任何嵌套动作抛出的 java.lang.Throwableexceptions 的方法。这个动作有一个 var 属性,该属性保存对任何 java.lang.Throwable 异常的引用,这些异常在任何嵌套动作的执行过程中发生。

下面是语法:

<c:catch [var="varName"]>
...nested actions in the body...
</c:catch>

表 3-12 描述了< c:catch >动作的属性。

表 3-12 。< c:抓>属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 定义变量 | 线 | 嵌套操作引发的异常的导出范围变量的名称。 |

通过将这些动作嵌套在中,动作可以处理来自任何动作的错误。当抛出一个异常时,它被存储在一个页面范围的变量中,该变量由标记的 var 属性标识。清单 3-44 演示了< c:catch >的用法。

清单 3-44 。使用< c:catch >

<body>

<c:catch var = "exception">
<% int i = 1/0;%>
</c:catch>

<c:if test = "${exception != null}">
<span> Exception : ${exception}</span>

</c:if>
</body>

以下是输出结果:

Exception : java.lang.ArithmeticException: / by zero

条件句

JSTL 核心标签库提供的条件标签为使用 scriptlets 生成基于条件的动态内容提供了一种替代方案。表 3-13 描述了该功能区的动作。

表 3-13 。核心标签库中的条件动作

|

行动

|

描述

|
| --- | --- |
| | 如果用 test 属性指定的表达式为真,则计算其正文内容 |
| | 为互斥条件执行提供上下文 |
| | 代表一个动作中的替代项 |
| | 代表一个动作中的最后一个选项 |

动作

动作 用于条件处理和评估表达式,仅当表达式评估为真时显示其正文内容。

下面是没有正文内容的语法:

<c:if test="testCondition"
var="varName" [scope="{page|request|session|application}"]/>

以下是正文内容的语法:

<c:if test="testCondition"
[var="varName"] [scope="{page|request|session|application}"]>
. . . body content . . .
</c:if>

使用测试属性,对布尔 EL 表达式进行评估。如果测试条件评估为 true,则只执行操作的主体。使用 var 属性将表达式计算的布尔结果导出到限定了作用域的变量中。var 的默认范围是 page,但是使用 scope 属性,可以将范围设置为任何 JSP 范围。

表 3-14 说明了< c:if >动作的属性。

表 3-14 。< c:如果>属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 试验 | 布尔代数学体系的 | 确定是否应处理正文内容的测试条件。 |
| 定义变量 | 线 | 测试条件结果值的导出范围变量的名称。 |
| 范围 | 线 | var 的范围。 |

清单 3-45 展示了如何在正文内容中使用< c:if >。

清单 3-45 。使用< c:如果>带有正文内容

<c:set var="number" value="9"/>
<c:if test="${ number < 10}" >
<c:out value ="number is less than 10"/>
</c:if>

以下是输出结果:

number is less than 10

动作、

动作 使您能够处理互斥的情况。它的工作方式类似于 Java switch 语句,让您在许多选项中进行选择,使用< c:when >代替 case 语句,使用< c:otherwise >提供默认操作,就像 switch 语句使用默认子句一样。

动作的语法如下:

<c:choose>
body content (<c:when> and <c:otherwise>)
</c:choose>

如您所见,动作有两个可能的嵌套动作构成了它的主体:。每种方法的语法如下:

<c:when test="testCondition">
body
</c:when>
<c:otherwise>
body
</c:otherwise>

表 3-15 说明了>动作时<的属性。

表 3-15 。< c:当>属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 试验 | 布尔代数学体系的 | 确定是否应处理正文内容的测试条件 |

清单 3-46 展示了< c:choose >的简单用法。

清单 3-46 。使用< c:选择>

<body>
<c:set var="number" value="10"/>
<c:choose>
<c:when test="${number < 10}">
       Number is less than 10.
</c:when>
<c:when test="${number > 10}">
        Number is greater than 10.
</c:when>
<c:otherwise>
        Number is equal to 10
</c:otherwise>
</c:choose>
</body>

以下是输出结果:

Number is equal to 10

循环和迭代

JSTL 为循环和迭代提供了两个有用的动作:用于一般数据,用于字符串标记化。

表 3-16 说明了循环和迭代的动作。

表 3-16 。核心标签库中的循环和迭代操作

|

行动

|

描述

|
| --- | --- |
| | 迭代对象的集合 |
| | 遍历由提供的分隔符分隔的标记 |

动作

动作遍历一组对象。

下面是迭代对象集合的语法:

<c:forEach[var="varName"] items="collection"
[varStatus="varStatusName"]
[begin="begin"] [end="end"] [step="step"]>
. . . body content . . . .
</c:forEach>

下面是迭代固定次数的语法:

<c:forEach [var="varName"]
[varStatus="varStatusName"]
begin="begin" end="end" [step="step"]>
. . . . body content . . .
</c:forEach>

表 3-17 说明了< c:forEach >动作的属性。

表 3-17 。< c:forEach >属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 定义变量 | 线 | 迭代的当前项的导出范围变量的名称。 |
| 项目 | "数组、集合、枚举、迭代器、映射、字符串 | 要迭代的项的集合。 |
| varStatus(扫描状态) | 线 | 迭代状态的导出范围变量的名称。导出的对象属于 javax . servlet . JSP . jstl . core . looptagstatus 类型。 |
| 开始 | (同 Internationalorganizations)国际组织 | 如果指定了项,则迭代从位于指定索引处的项开始。集合的第一项的索引为 0。如果未指定项目,则迭代从指定值的索引集开始。 |
| 目标 | (同 Internationalorganizations)国际组织 | 如果指定了项,则迭代在位于指定索引(包括)的项处结束。如果未指定项目,则当索引达到指定的值时,迭代结束。 |
| 步骤 | (同 Internationalorganizations)国际组织 | 迭代将只处理每个步骤项,从第一个开始。 |

若要循环访问对象集合,请使用以下语法:

<c:forEach[var="varName"] items="collection" [varStatus="varStatusName"]
...body  content ....
</c:forEach>

要迭代固定次数,请使用以下语法:

<c:forEach [var="varName"] [varStatus="varStatusName"]
begin="begin" end="end" [step="step"]>
...body content ...
</c:forEach>

清单 3-47 展示了< c:forEach >的一个简单用法。

清单 3-47 。使用<c:forEach>

<body>
<c:forEach var="i" begin="1" end="3">
   Item <c:out value="${i}"/><p>
</c:forEach>
</body>

以下是输出结果:

Item 1
Item 2
Item 3

动作

动作遍历由一组分隔符分隔的一串标记。

下面是语法:

<c:forTokens items="stringOfTokens" delims="delimiters"
[var="varName"] [varStatus="varStatusName"]
[begin="begin"] [end="end"] [step="step"]>
. . . body . . . . . . . . .
</c:forTokens>

表 3-18 描述了< c:forTokens >的所有属性。

表 3-18 。< c:兑换令牌>属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 定义变量 | 线 | 迭代的当前项的导出范围变量的名称。 |
| 项目 | 线 | 要迭代的令牌字符串。 |
| 德利姆 | 线 | 一组分隔符(字符串中分隔标记的字符)。 |
| varStatus(扫描状态) | 线 | 迭代状态的导出范围变量的名称。导出的对象属于 javax . servlet . JSP . jstl . core . looptagstatus 类型。 |
| 开始 | (同 Internationalorganizations)国际组织 | 迭代从位于指定索引处的标记开始。第一个令牌的索引为 0。 |
| 目标 | (同 Internationalorganizations)国际组织 | 迭代在位于指定索引(含)的标记处结束。 |
| 步骤 | (同 Internationalorganizations)国际组织 | 迭代将只处理每一步标记,从第一步开始。 |

标签处理由分隔符分隔的字符串。清单 3-48 展示了< c:forTokens >的用法。

清单 3-48 。使用< c:兑换令牌>

<body>
<c:forTokens items="Clojure,Groovy,Java, Scala" delims="," var="lang">
<c:out value="${lang}"/><p>
</c:forTokens>
</body>

下面是清单 3-48 的输出:

Clojure
Groovy
Java
Scala

URL 相关动作

与 URL 相关的操作用于 web 应用中的链接、导入和重定向。表 3-19 描述了核心库中所有与 URL 相关的动作。

表 3-19 。与 URL 相关的操作

|

行动

|

目的

|
| --- | --- |
| | 导入基于 URL 的资源的内容。 |
| | 向 URL 添加请求参数。这是一个的嵌套动作。 |
| | 构建应用了正确重写规则的 URL。 |
| | 向客户端发送 HTTP 重定向。 |

让我们来看看这些与 URL 相关的操作。

动作

动作 导入基于 URL 的资源的内容,在< jsp:include >动作的基础上提供额外的功能。< c:import >动作的语法如下:

<c:import url ="url  [context="context"] [charEncoding="charEncoding"] [scope="application|page|request|session"] [var= "varName"] >
Optional body content for <c:param> sub tags
</c:import>
  • 唯一必需的属性是 url,它是要导入的资源的 URL。
  • 接下来解释的动作 可以用作< c:import >的主体内容中的嵌套标签。

表 3-20 描述了< c:导入>动作使用的所有属性。

表 3-20 。< c:导入>属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 全球资源定位器(Uniform Resource Locator) | 线 | 要导入的资源的 URL。 |
| 语境 | 线 | 访问属于外部上下文的相对 URL 资源时上下文的名称。 |
| 定义变量 | 线 | 资源内容的导出范围变量的名称。 |
| 范围 | 线 | var 的范围。 |
| 字符编码 | 线 | 输入资源中内容的字符编码。 |
| varReader | 线 | 资源内容的导出范围变量的名称。 |

在前一章中,我们看到了动作如何让我们将功能封装在一个 JSP 页面中,并将它包含在另一个页面中;例如,你可以包含一个页眉和页脚,如清单 3-49 所示。

清单 3-49 。使用< jsp:包含>动作

<body>
<jsp:include page='/WEB-INF/jsp/header.jsp'/>
<%-- content -%>
<jsp:include page='/WEB-INF/jsp/footer.jsp'/>
</body>

动作仅限于包含与包含页面属于同一 web 应用并被指定为相对 URL 的资源。

您可以使用而不是在同一个 web 应用中导入资源;清单 3-50 演示了如何使用< c:import >代替< jsp:include >。

清单 3-50 。使用< c:导入>

<body>
<c:import url='/WEB-INF/jsp/header.jsp'/>
<%-- content --%>
<c:import url='/WEB-INF/jsp/footer.jsp'/>
</body>

使用动作,除了访问同一 web 应用中的资源,您还可以访问外部资源或外部上下文中的资源。要访问外部资源,您需要为 url 属性指定一个绝对 url,而要访问外部上下文中的资源,您需要为 context 属性指定一个值,该值代表外部上下文的上下文路径以及 URL 属性,该值代表资源的上下文相对路径。清单 3-51 展示了如何使用< c:import >动作从外部上下文中导入资源。

清单 3-51 。从外部环境导入资源

<c:import url='/jsp/book.jsp' context='/foreigncontext'/>

注意当访问协议不是 HTTP 且编码不是 ISO-8859-1 的绝对 URL 资源时,需要 charEncoding 属性。

行动

本节稍后解释的动作都处理 URL。动作用于传递请求参数,并用作动作主体中的嵌套标签。动作也执行 URL 编码。

下面是在属性值中指定了参数值的语法。

<c:param name="name" value="value"/>

下面是在正文内容中指定了参数值的语法。

<c:param name="name">
parameter value
</c:param>

表 3-21 描述了< c:param >的属性。

表 3-21 。< c:param >属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 名字 | 线 | 查询字符串参数的名称 |
| 价值 | 线 | 参数值 |

清单 3-52 展示了如何使用< c:import >代替< jsp:include >。您还可以使用< jsp:param >动作为包含的文件指定请求参数,如清单 3-52 所示。

清单 3-52 。使用JSP:param

<body>
<jsp:include page='/WEB-INF/jsp/company/companyHeader.jsp'>
<jsp:param name='user'
value='<%=session.getAttribute("userName")%>'/>
</jsp:include>
<%-- Page content goes here--%>
<jsp:include page='/WEB-INF/jsp/company/companyFooter.jsp'/>
</body>

清单 3-53 展示了如何使用< c:param >代替< jsp:param >。

清单 3-53 。使用< c:param >

<body>
<c:import url='/WEB-INF/jsp/header.jsp'>
<c:param name='user'
value='${sessionScope.userName}'/>
</c:import>
<%-- body content --%>
<c:import url='/WEB-INF/jsp/footer.jsp'/>
</body>

行动

动作 应用适当的重写规则构建一个 URL。它可以格式化 URL 并将其存储在 var 属性指定的变量中。

下面是不带正文内容的语法:

<c:url value [context] [var] [scope]/>

下面是用正文内容指定查询字符串参数的语法:

<c:url value [context] [var] [scope]>
<c:param> actions
</c:url>
  • 唯一必需的属性是 value,它是要处理的 URL。
  • 子标记也可以在的主体中指定,用于添加到 URL 查询字符串参数中,如果需要,这些参数将被正确编码。

的属性列于表 3-22 中。

表 3-22 。< c:url >属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 价值 | 线 | 要处理的 URL。 |
| 语境 | 线 | 指定属于外部上下文的相对 URL 资源时上下文的名称。 |
| 定义变量 | 线 | 已处理 URL 的导出范围变量的名称。 |
| 范围 | 线 | var 的范围。 |

清单 3-54 显示了< c:url >的简单用法。

清单 3-54 。使用<c:URL>

<c:url var="homePage" scope="session"value="http://www.yourbookstore.com" />

如果您为 value 属性指定了相对于上下文或相对于页面的 URL,会将 web 应用的上下文路径添加到 URL 的前面;例如,如果 web 应用的上下文路径是/bookWeb/books,那么动作将导致 url:/bookWeb/books/book.jsp

行动

动作向客户端发送 HTTP 重定向,将浏览器重定向到另一个 URL。< c:redirect >动作为 URL 重写提供了一个重定向。

下面是不带正文内容的语法:

<c:redirect url="value" [context="context"]/>

下面是用正文内容指定查询字符串参数的语法:

<c:redirect url="value" [context="context"]>
<c:param> subtags
</c:redirect>

动作有两个属性:用于重定向的 URL 和一个可选的上下文。

< c:重定向>的属性在表 3-23 中列出。

表 3-23 。< c:重定向> 属性

|

名字

|

类型

|

描述

|
| --- | --- | --- |
| 全球资源定位器(Uniform Resource Locator) | 线 | 要重定向到的资源的 URL |
| 语境 | 线 | 重定向到属于外部上下文的相对 URL 资源时上下文的名称 |

相对或绝对 URL 遵循与相同的 URL 重写规则。清单 3-55 展示了重定向到一个外部资源,这是一个绝对 URL。

清单 3-55 。使用< c:重定向>

<c:redirect url="http://www.yourbookstore.com" />

您可以使用 context 属性重定向到外部上下文中的资源。指定的 URL 必须以/开头,作为相对于上下文的 URL,并且根据定义,上下文名称也必须以/开头。清单 3-56 展示了在外部环境中重定向到外部资源。

清单 3-56 。在外国背景下重新定向到外国资源。

<c:redirect url="/foreignresource.html" context="/foreigncontext" />

在这一章中,我们已经研究了 web 层中 Java EE 机器的基本要素:web 组件(servlets 和 JSP)、功能丰富的表达式语言和现成的定制动作(JSTL)。现在我们将讨论使用模式的最佳实践解决方案。

最佳实践解决方案:使用模式

虽然 Java EE 在标准化企业基础设施、提供应用模型和提供足以开发 web 应用的组件方面做得很好,但是直接与 Java EE 组件交互通常会导致大量样板代码甚至代码冗余。使用 Java EE 并不会自然而然地产生最佳实践架构和设计。为此,Deepak Alur、John Crupi 和 Dan Malks 首先根据他们设计企业系统的经验建立了 Java EE 设计模式。在本节中,将向您介绍由 Alur、Crupi 和 Malks 建立的 web 层中的 Java EE 模式。

注意强烈推荐由 Alur、Crupi 和 Malks (Prentice Hall,2003)撰写的书核心 J2EE 模式:最佳实践和设计策略来学习最佳实践架构和设计。然而,在 Java EE 6 和 Java EE 7 中,业务和持久性 JavaEE 模式发生了实质性的变化。例如,一些模式如服务定位器已经被抛弃,取而代之的是依赖注入模式。但是 web 层模式保持不变。你可以在 Adam Bien 的真实世界 Java EE 模式:反思最佳实践(第二版)中找到大量关于新业务和持久性 Java EE 模式的文献。

web 层封装了向客户端提供服务所需的表示逻辑。表示层 执行以下操作:

  • 拦截客户端请求
  • 提供认证、授权、加密和会话管理等功能
  • 访问业务服务
  • 构建响应
  • 将响应呈现给客户端

一般来说,web 应用开发需要您解决一组常见的问题 。

  • 当请求进入 web 应用时,通常需要对其进行预处理,以提供某些功能,如身份验证、授权和加密。
  • 表示层和业务逻辑经常混合在一起。这使得表示层难以维护。
  • 视图通常用视图导航逻辑进行编码。这导致了视图内容和视图导航的混合。
  • 视图管理没有集中的组件,这导致代码冗余和代码分散在视图中。

这不是一个完整的问题列表,但是这些是 web 应用中最常见的问题。幸运的是,web 应用中的这些问题可以使用 web 层 Java EE 模式来解决。如何使用这些 Java EE 模式因问题而异。表 3-24 描述了这些模式。

表 3-24 。表示层 Java EE 模式

|

表示层模式

|

描述

|
| --- | --- |
| 拦截过滤器 | 客户端 web 请求和响应的预处理和后处理 |
| 前端控制器 | 表示层请求处理的集中访问点,支持系统服务、内容检索、视图管理和导航的集成 |
| 查看助手 | 封装业务逻辑,使其不会与表示逻辑纠缠在一起 |
| 复合视图 | 独立于内容管理视图的布局 |
| 为工人服务 | 用调度器组件组装前端控制器和视图助手的微框架 |
| 调度员视图 | 用调度器组件组装前端控制器和视图助手的微框架 |

注意一个好的企业应用由多层组成,每层专注于自己的职责/关注点,正如第一章和第二章中所解释的。

在接下来的小节中,我们将研究每一个 Java EE web 层模式。

拦截过滤器

web 应用接收不同类型的请求,这些请求需要某种处理,例如,在继续导航之前检查客户端是否必须经过身份验证。为请求提供处理机制的最佳技术是使用一个称为过滤器 的处理组件。当您需要提供请求预处理或后处理功能时,可以使用过滤器。拦截过滤器模式伴随着 Servlet 规范中过滤器的引入而出现。这种模式使用一个或多个插入到当前应用中的过滤器来提供授权、压缩、加密和日志记录等服务。图 3-9 说明了拦截过滤器模式。

9781430259831_Fig03-09.jpg

图 3-9 。拦截过滤器模式

拦截过滤器模式 用于客户端请求的预处理和后处理,以及通过拦截请求和响应的响应。过滤器是可插拔的,您可以在不更改代码的情况下添加或删除它们。

拦截过滤器模式被认为最适合的用例之一是当您想要默认启用浏览器的 IE9 文档模式时。IE 有两种模式:浏览器模式和文档模式。浏览器总是向服务器发送浏览器模式数据,服务器总是用文档模式数据进行响应。浏览器模式数据由带有版本和 trident 令牌信息的用户代理字符串组成,而文档模式数据由元标记组成,这些元标记规定了在浏览器上呈现响应的模式。

清单 3-57 显示了一个简单的响应过滤器,它默认启用浏览器的 IE9 文档模式。

清单 3-57 。简单响应过滤器

 1.package com.apress.filters
 2.import javax.servlet.*;
 3.import javax.servlet.http.HttpServletResponse;
 4.import java.io.IOException;
 5.import java.util.Enumeration;
 6.
 7./**
 8\. * filter for enabling IE9 document mode by default
 9\. *
10\. */
11.public class ResponseHeaderFilter implements Filter {
12.    private FilterConfig filterConfig = null;
13.
14.    public void doFilter(ServletRequest aServletRequest, ServletResponse aServletResponse, FilterChain chain)
15.            throws IOException, ServletException {
16.
17.        HttpServletResponse response = (HttpServletResponse) aServletResponse;
18.
19.        // set the provided HTTP response parameters
20.        for (Enumeration e = filterConfig.getInitParameterNames(); e.hasMoreElements();) {
21.            String headerName = (String) e.nextElement();
22.            response.addHeader(headerName, filterConfig.getInitParameter(headerName));
23.        }
24.
25.        // pass the request/response on
26.        chain.doFilter(aServletRequest, response);
27.    }
28.
29.    public void init(FilterConfig aFilterConfig) {
30.        filterConfig = aFilterConfig;
31.    }
32.
33.    public void destroy() {
34.        filterConfig = null;
35.    }
36.}
  • 第 20 行:这一行从部署描述符中获取所有的初始化参数。
  • 第 22 行:该行将这些参数添加到响应中。

清单 3-58 说明了部署描述符 ?? 中响应过滤器的配置。

清单 3-58 。配置简单响应过滤器

 1.<filter>
 2.<filter-name>HTML5</filter-name>
 3.<filter-class>com.apress.filters.ResponseHeaderFilter</filter-class>
 4.<init-param>
 5.<param-name>X-UA-Compatible</param-name>
 6.<param-value>IE=edge,chrome=1</param-value>
 7.</init-param>
 8.</filter>
 9.<filter-mapping>
10.<filter-name>HTML5</filter-name>
11.<url-pattern>/*</url-pattern>
12.</filter-mapping>
  • 第 5 行到第 6 行:这几行定义了初始化参数 ??。

前端控制器

对于可维护的 web 应用,所有请求都必须通过一个公共的中心组件 。缺乏集中机制会导致以下问题:

  • 视图管理没有集中的组件,这导致代码冗余和代码分散在视图中。
  • 视图通常用视图导航逻辑编码。这导致了视图内容和视图导航的混合。

前端控制器模式为请求处理提供了集中的访问,以提供内容检索、视图管理、导航、验证、错误处理、集中的安全控制等等。前端控制器模式最好用 servlet 实现。使用集中式 servlet 来处理所有请求和响应提供了以下优势 :

  • 它提供了一个单一位置来控制与身份验证和授权相关的决策。
  • 前端控制器需要处理的所有 URL 都可以映射到这个 servlet。
  • 它提供了一个集中的访问点来支持视图管理和导航。
  • 您可以将通用逻辑应用于多个视图。
  • 它提供了表示逻辑与导航和业务逻辑的分离。这导致了两者之间的松散耦合。

图 3-10 显示了前控制器图形的结构 。

9781430259831_Fig03-10.jpg

图 3-10 。前端控制器类图

前端控制器模式的组件如下:

  • 控制器
  • 分配器
  • 视角

控制器

控制器是处理请求的起始点,并与调度程序组件进行协调。控制器管理以下内容:

  • 处理请求,包括调用安全服务,如身份验证和授权
  • 委托给业务服务
  • 处理错误

调度员

调度员负责视图管理和导航。

查看

视图向客户端表示和显示信息。

图 3-11 显示了前控制器的顺序图。

9781430259831_Fig03-11.jpg

图 3-11 。前端控制器序列图

查看助手

在 web 应用中,表示内容(即 JSP 页面的内容)需要处理动态内容。当业务逻辑和表示层逻辑混合在一起时,表示层的变化经常发生,并且很难开发和维护。事实上,混合业务逻辑和表示逻辑会使表示不可维护。视图助手设计模式将表示层与业务层分离,并基于模板生成视图 。视图助手模式中的组件如下:

  • 视角
  • 查看助手

查看

视图包含表示格式逻辑,并将表示中的业务处理逻辑委托给助手。

查看助手

视图助手执行以下操作:

  • 视图助手可以实现为 JavaBean 实现或自定义标记实现。我们在第二章中使用了这个模式。
  • 助手充当视图的中间数据模型。
  • 助手负责从业务服务获取数据。
  • 表示业务逻辑封装在助手中。

图 3-12 说明了视图助手模式的结构。

9781430259831_Fig03-12.jpg

图 3-12 。查看助手类图

复合视图

在 web 应用中,视图通常是通过在每个视图中直接嵌入格式化代码来构建的。这使得修改多个视图的布局变得困难。复合视图模式允许父视图 由子视图组成。因此,整个视图变成了更小的子视图的组合,这些子视图被动态地包含在整个视图中。

复合视图模式的组件如下:

  • 基本视图:这是视图的基本抽象。
  • 复合视图:这是一个由多个子视图组成的视图。
  • 视图:这是一个简单的视图,没有子视图。

图 3-13 显示了复合视图模式的基本结构 。

9781430259831_Fig03-13.jpg

图 3-13 。复合视图类图

图 3-14 说明了复合视图模式的用法。

9781430259831_Fig03-14.jpg

图 3-14 。复合视图用法

调度员查看

dispatcher 视图设计模式将两种模式(前端控制器和视图助手)组装到一个带有 Dispatcher 组件的微框架中,以利用与每种模式相关的优势。

在 dispatcher 视图模式中,Dispatcher 负责选择和提供静态或动态地向用户呈现后续视图的机制。

调度员视图模式的成员如下,如图 3-15 所示:

  • 前端控制器
  • 分配器
  • 助手
  • 视角
  • 查看助手

9781430259831_Fig03-15.jpg

图 3-15 。调度员类图

前控制器

前端控制器执行以下操作:

  • 集中请求处理
  • 使用调度程序委托给视图
  • 使用视图助手将业务逻辑关注点与表示逻辑关注点分开

调度员

调度程序执行以下操作:

  • 负责视图管理和导航
  • 视图的代表
  • 使用助手将数据推入视图

助手

助手执行以下操作:

  • 帮助视图或控制器完成其处理

查看

该视图执行以下操作:

  • 向客户端表示和显示信息
  • 使用视图助手从数据源提取数据

图 3-16 说明了 Dispatcher 视图模式的序列图。

9781430259831_Fig03-16.jpg

图 3-16 。调度员视图序列图

为工人 服务

面向工作人员的服务设计模式将两种模式(前台控制器和视图助手)组装到一个微框架中,并带有一个 dispatcher 组件,以利用与每种模式相关的好处。

服务到工作者模式是一个调度器与视图和助手的组合,用于处理客户端请求并生成动态内容作为响应。当应用需要动态内容生成 时,可以在应用中使用 Service to Worker 模式。

如图图 3-17 所示,服务到工作者模式的成员如下:

  • 前端控制器
  • 分配器
  • 助手
  • 视角
  • 查看助手

9781430259831_Fig03-17.jpg

图 3-17 。工人服务类图

前控制器

前端控制器执行以下操作:

  • 集中请求处理
  • 使用调度程序委托给视图
  • 使用视图助手将业务逻辑关注点与表示逻辑关注点分开

调度员

调度程序执行以下操作:

  • 负责视图管理和导航
  • 视图的代表
  • 使用助手将数据推入视图

助手

助手执行以下操作:

  • 帮助视图或控制器完成其处理
  • 将数据推入视图

图 3-18 说明了服务到工人模式的序列图 。

9781430259831_Fig03-18.jpg

图 3-18 。员工服务顺序图

与 Dispatcher 视图模式一样,Service to Worker 模式也是由目录中的其他模式组合而成的。这两种微框架模式都描述了带有视图和助手的控制器和调度程序的组合,但是不同之处在于,调度程序视图模式将内容检索延迟到动态视图生成之后,而工作人员服务模式在前端控制器中进行内容检索。

我们刚刚介绍了所有的 Java EE web 层模式。使用这些模式,您可以解决架构领域问题和业务问题领域。然而,您还必须使用 OO 模式来解决架构问题域。许多设计模式可以用来获得可重用性、可维护性和可扩展性。例如,您可以在 web 应用中使用 OO 模式来实现这三位一体,如下所示:

  • 抽象工厂
  • 建设者
  • 装饰者
  • 做吧
  • 模板方法

注意尽管应用了 Java EE 模式和增量演进,一些应用还是会因为错误的架构决策而失败。这些错误的架构决策被记录下来,这样你就不会再犯同样的错误。这些记录在案的错误架构决策被称为反模式 。Bill Dudney、Stephen Asbury、Joseph K. Krozak 和 Kevin Wittkopf (Wiley,2003)撰写的 J2EE 反模式是反模式的优秀资源。

Java Web 框架

您了解了 MVC 和 Java EE web 层模式一起为构建可重用、可维护和可扩展的 web 应用提供了架构基础。随着开发人员积累更多的经验,他们开始发现可以反复使用的通用对象,模式开始出现。一旦你有了这些通用对象的集合,一个框架就开始出现了。一个框架 是为应用开发提供基础设施的通用对象和其他支持类的集合。本质上,框架是由下面讨论的基本框架原则保护的设计模式的集合。一个 Java 框架使用两种模式 。

  • 面向对象模式
  • Java EE 模式

一个框架使用面向对象模式进行自身的构建,以解决架构问题域 ,例如可扩展性、可维护性、可重用性、性能和可伸缩性。面向对象模式和 Java EE 模式都处理业务问题领域 领域,例如处理请求、认证、验证、会话管理和视图管理等等。。框架通过提供基于模式的通用对象和支持类来解决这两个主要的架构和业务问题领域,严格遵循以下关键原则:

  • 可配置性 :可配置性是框架能够使用元数据来改变框架行为的能力。
  • 控制反转 :在传统的编程风格中,问题域代码控制着应用执行的流程。控制反转指的是一种技术,其中可重用代码控制问题域代码的执行,从而控制应用执行的流程。
  • 松耦合 :这个原则指的是框架中协作类的独立性,通过这个原则,每个协作类都可以在不影响其他协作类的情况下进行修改。
  • 关注点分离 :这个原则是指需要对问题域区域进行分类,并以隔离的方式进行处理,使得一个问题区域的关注点不会影响到另一个问题区域的关注点。我们在第一章中看到的多层 Java EE 架构是由关注点分离原则驱动的。
  • 自动化公共功能 :框架提供了自动解决领域普通功能的机制。

为什么要使用框架?

虽然 Java EE 在标准化企业基础设施和提供应用模型方面做得很好,但是它几乎没有什么大问题。

  • 直接与 Java EE 组件交互通常会产生大量样板代码,甚至代码冗余。
  • 您必须编写代码来处理常见的业务领域问题。
  • 你必须编写代码来解决架构领域的问题。

您可以推出自己的框架来解决与使用 OO 模式和 Java EE 模式构建基于 Java EE 的 web 应用相关的问题。但是编写内部框架需要与应用的业务目标正交的努力;此外,内部框架不太可能升级,内部框架的新版本永远见不到太阳,不像主流框架那样不断进化,而不是陷入架构熵。记住这一点,是时候看看一些可用的基于 JVM 的 web 框架了(见表 3-25 )。该表远非详尽无遗;有无数的框架可供使用,但是本书将涵盖表中列出的最成功的基于 JVM 的 web 框架。

表 3-25 。基于 JVM 的 Web 框架

image

在表 3-25 中列出的所有 web 框架都遵循 MVC Model-2 架构,这是你在第二章中学到的基本架构模式。在一个典型的 web 应用中,对于每个传入的请求,您都希望完成几项任务,比如加密。web 应用的单个控制器使您可以集中处理控制器必须在 web 应用中执行的所有任务,如下所示:

  • 集中向视图发送请求的逻辑
  • 检查请求操作的用户是否具有有效授权

随着基于 MVC 的 web 应用变得越来越大,要显示的视图越来越多,MVC 应用模型中的控制器变成了一个程序对象,需要做出太多的决定来呈现这些视图。这个问题可以通过使用前端控制器和配置元数据来缓解,而不是使用纯 MVC 架构。该解决方案如图 3-19 所示。

9781430259831_Fig03-19.jpg

图 3-19 。带前端控制器的 MVC

当 HTTP 请求从客户端到达时,前端控制器在配置元数据文件中查找,以决定应该处理 HTTP 请求的正确的应用控制器。应用控制器由 HTTP 请求的业务逻辑调用和表示逻辑控制组成。使用纯 MVC 和使用 MVC 的前端控制器模式之间的区别在于,前端控制器 servlet 查找配置元数据来找出处理 HTTP 请求处理的应用控制器,而不是它自己决定调用特定的 HTTP 请求处理程序对象。

本质上,这是许多领先的 web 框架共有的模式。例如,Struts 2 和 Spring Web MVC 是基于前端控制器的 MVC 框架,它们使用存储在例如 XML 文件中的配置元数据将控制委托给应用控制器。在 Struts 2 中,ServletFilter 是图 3-19 中前端控制器的一种表现形式。在 JSF 2 中,它是 FacesServlet 在 Spring Web MVC 中,它是 DispatcherServlet 在 Grails 2 中,它是 DispatcherServlet 的子类。

本章到此结束。从下一章开始,这本书将浏览利用 Java EE 和 JVM 优势的 Web 框架迷宫。

摘要

本章重点介绍了 Java EE 项目中使用的技术和工具的发展。本章向您介绍了与开发 web 应用相关的常见问题,并展示了如何解决这些问题,以及如何通过使用 Java EE 提供的工具(如 unified EL)使 web 应用可重用、可维护和可扩展。

EL 和 JSTL 是业务和视图分离理论的两个完美实现,它们可以用来构建可重用、易维护、功能丰富的 web 应用。本章还向您介绍了 Java EE web 层模式,以及这些 Java EE 模式如何为特定上下文中反复出现的问题提供解决方案。然后这一章展示了框架如何通过提供基于模式的通用对象和支持类来解决架构和业务问题领域。最后,这一章展示了 web 框架如何通过为您提供开箱即用的最佳实践解决方案,让您不再使用 Java EE 设计模式。

1 发明意为从已有的物体中创造出新的物体。发现是找出已经存在的东西,但不是从中创造新的对象。

2 一种模式语言:城镇、建筑、施工克里斯托弗·亚历山大、莎拉·石川和默里·银色啤酒杯乐队著(牛津大学出版社,1977)。参见克里斯托弗·亚历山大的《永恒的建筑之道》(牛津大学出版社,1979 年)。

Java . Coe . PSU . AC . th/J2EE/jstl 1.2/jstl-1 _ 2-mrel 2-spec . pdf

四、使用 Struts 2 构建 Web 应用

受伤的鹿跳得最高。

—艾米莉·狄金森

Struts 框架是一棵古老的活树,它的环形模式讲述了遗留 Java web 森林的故事。Struts 于 2001 年 6 月发布,开创了 Model-2 开发模型的基本发展,以应对 web 开发的变迁。您可以看到 Struts 的 DNA 被许多其他架构多样的基于动作的框架所吸收,这些框架是为了解决 Model-2 开发而发展的。Struts 诞生了 Tiles 框架 ,现在可以和无数的 web 框架一起使用。

由于 web 应用越来越复杂,以及来自其他不断发展的 web 框架的竞争,Struts 的流行开始失去势头。构建在经典 Struts 之上的 WebWork 框架后来与它统一起来,创建了 Struts 2 框架。Struts 2 是对经典 Struts 架构的完全重写,旨在解决上述需求。Struts 2 为 web 应用提供了架构基础,提供了自动化重复任务和分离横切关注点的架构机制,并通过配置上的约定使开发人员不必维护过多的配置代码。

一章不足以展示任何框架的全部功能,所以我在这里的目的是展示 Struts 2 web 框架的基础。从本章开始,通过后续章节,您将逐步了解现代 web 框架如何提供以 Java EE web 层模式为中心的架构基础。

Struts 2 框架概述

表 4-1 描述了 Struts 2 框架的关键特性。

表 4-1 。Struts 2 框架的主要特性

|

特征

|

描述

|
| --- | --- |
| Ajax 支持 | Struts 2 集成了 Ajax 支持。除了现成的 Ajax,Struts 2 还支持无数以 Ajax 为中心的插件。 |
| 约定胜于配置 | Struts 2 坚持约定优先于配置的原则,消除了不必要的配置。 |
| 使用注释的声明式体系结构 | Struts 2 使用注释的声明式架构减少了 XML 配置,并使配置更接近 action 类。 |
| 数据变换 | Struts 2 提供了从基于字符串的表单字段值到对象或原始类型的自动类型转换,消除了在 action 类中提供转换代码的需要。 |
| 依赖注入 | Struts 2 对动作使用依赖注入来与它需要的组件协作。 |
| 展开性 | Struts 2 是可扩展的,因为框架中的类是基于接口的。 |
| 插件架构 | 核心 Struts 行为可以用插件来增强。您可以在这里找到许多适用于 Struts 2 的插件:
【https://cwiki.apache.org/S2PLUGINS/home.html】 |
| POJO 表单和操作 | 与使用 ActionForms 的传统 Struts 不同,在 Struts 2 中,您可以使用任何 POJO 来接收表单输入。任何 POJO 都可以作为一个动作。 |
| 查看技术 | Struts 支持 JSP、FreeMarker、Velocity、XSLT 等多种视图。 |

在深入研究 Struts 2 之前,有必要了解一下经典 Struts(以下简称 Struts)的架构。Struts 是一个基于 MVC 的框架。Struts 框架的核心组件是 ActionServlet ,它实现了前端控制器 web 层 Java EE 模式。图 4-1 展示了 Struts 的架构。

9781430259831_Fig04-01.jpg

图 4-1 。Struts 框架的体系结构

Struts 中事件 的顺序如下:

  1. ActionServlet 将请求处理委托给 RequestProcessor。
  2. RequestProcessor 处理请求并将表单值存储在 ActionForm 中。
  3. RequestProcessor 然后调用 Action 类。
  4. Action 类访问 ActionForm 来检索表单值。
  5. Action 类调用对服务层的调用。
  6. Action 类返回 ActionForward,用于封装响应视图。

然而,Struts 2 不同于 Struts。与 Struts 不同,Struts 2 遵循的是 push-MVC 架构,数据应该出现在页面或范围中,而 Struts 2 是 pull-MVC 架构;也就是说,可以从操作中提取数据。表 4-2 显示了 Struts 和 Struts 2 框架元素的一对一映射。

表 4-2 。Struts 和 Struts 2 框架元素的一一映射 ??

|

Struts 框架元素

|

Struts 2 框架元素

|
| --- | --- |
| 核心控制器 | Servlet 过滤器 |
| 请求处理器 | 拦截机 |
| 行动 | 行动 |
| ActionForm(操作表单) | 行动或 POJOs |
| 向前行动 | 结果 |

注意你可以在Struts . Apache . org/release/2.3 . x/docs/comparisng-Struts-1-and-2 . html找到 Struts 和 Struts 2 异同的全面列表。

图 4-2 说明了 Struts 2 的关键元素,它提供了一个更清晰的 MVC 实现。

9781430259831_Fig04-02.jpg

图 4-2 。Struts 2 的架构

从图 4-2 可以看出,Struts 2 也是一个基于 MVC 的框架,实现了前台控制器模式。Struts 2 框架中的事件顺序如下:

  1. 该请求被映射到配置元数据。
  2. 请求通过一堆拦截器,这些拦截器为请求和横切特性提供预处理和后处理。
  3. 调用提供处理该请求的逻辑的操作和操作中的方法。
  4. 调用结果来呈现响应。
  5. 该响应被返回给用户。

接下来讨论图 4-2 中所示支柱 2 的关键元件。

动作

动作是面向动作的 Struts 2 框架的核心,因为它们为请求处理提供了必要的逻辑。实现任何接口或扩展任何类都不需要动作,动作可以是 POJOs。清单 4-1 展示了名为 HelloWorldAction 的动作。

清单 4-1 。作为 POJO 的行动

public class HelloWorldAction{
//...
public String execute() {
return "success";
}
}

这个动作类是在 struts.xml 文件中配置的,如清单 4-2 所示。

清单 4-2 。在 struts.xml 中配置操作类

<package name="helloWorld" " extends="struts-default" namespace="/ >

<action name="hello" class=" HelloWorldAction">
<result name="success"> /hello.jsp</result>
</action>

<package>

清单 4-2 中的动作映射使用 name 属性来定义你可以用来访问这个动作类的动作的名称,并使用 result 标签来定义哪个结果页面应该返回给用户。现在,您可以通过。动作扩展。

http://localhost:8080/helloWorldExample//hello.action

即使您可以使用 POJO 动作,Struts 2 也提供了两个可以使用的动作助手:动作接口和 ActionSupport 类。

动作界面

Struts 2 带有一个可选的动作接口,如清单 4-3 所示。

清单 4-3 。动作界面

package com.opensymphony.xwork2;

public interface Action {
public static final String ERROR = "error";
public static final String INPUT = "input";
public static final String LOGIN = "login";
public static final String NONE = "none";
public static final String SUCCESS = "success";

public String execute();
}

该接口提供了基于字符串的常量返回值和默认的 execute()方法,这些方法应该由实现类来实现。

实现这个接口的 action 类可以直接使用常量值,如清单 4-4 所示。

清单 4-4 。使用操作界面

import com.opensymphony.xwork2.Action;
public class HelloWorldAction implements Action{
//..
public String execute() {
return SUCCESS;
}
}

ActionSupport 类

ActionSupport 类实现 Action 接口,并提供返回成功值的 execute()方法的实现。ActionSupport 类还实现了一些为验证、本地化和国际化提供支持的接口,如清单 4-5 中的 ActionSupport 类的代码片段所示。

清单 4-5 。ActionSupportClass

public class ActionSupport implements Action, Validateable, ValidationAware,TextProvider, LocaleProvider, Serializable {
...
public String execute(){
return SUCCESS;
}
}

清单 4-6 显示了代码片段,通过它 ActionSupport 类可以用来提供验证。

清单 4-6 。使用动作支持

import com.opensymphony.xwork2.ActionSupport;

public class LoginAction extends ActionSupport{
private String username;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

//getters and setters
// ...
public String execute() {
return "SUCCESS";
}

public void validate(){
if("".equals(getUsername())){
addFieldError("username", getText("username.required"));
}
if("".equals(getPassword())){
addFieldError("password", getText("password.required"));
}
}
}

拦截器

拦截器通过将横切关注点的实现从操作中分离出来,促进了关注点的分离。Struts 2 附带了一组预构建的拦截器和拦截器堆栈,您可以开箱即用。清单 4-7 展示了一个动作的声明,该动作属于一个扩展 struts-default 包的包,该包包含默认的拦截器集。

清单 4-7 。宣告一个动作 ??

<package name="default" namespace="/" extends="struts-default">
<action name="helloAction"
class="HelloWorldAction" >
<result name="success">/hello.jsp</result>
</action>
</package>

当您从 struts-default 包扩展您的包时,默认情况下,defaultStack 将用于您的包中的所有操作。defaultStack 是在 struts-default.xml 文件中配置的,它提供了 Struts 2 的所有核心功能。struts-default.xml 文件位于 struts 2-core.jar 文件中。要将其他拦截器映射到一个动作,可以使用 interceptor-ref 元素,如清单 4-8 所示。

清单 4-8 。将拦截器映射到行动 ??

<package name="default" namespace="/" extends="struts-default">
<action name="helloAction"
class="HelloWorldAction" >
<interceptor-ref name="logger"/>
<result name="success">/hellot.jsp</result>
</action>
</package>

在清单 4-8 中,动作映射通过 interceptor-ref 元素将记录器拦截器映射到 HelloWorldAction 动作类。由于 HelloWorldAction 被声明为它自己的拦截器,它失去了默认的拦截器集,为了使用它,你必须显式声明 defaultStack,如清单 4-9 所示。

清单 4-9 。声明一个默认堆栈

<package name="default" namespace="/" extends="struts-default">
<action name="helloAction"
class="HelloWorldAction" >
<interceptor-ref name="logger"/>
<interceptor-ref name="defaultStack"/>
<result name="success">/hello.jsp</result>
</action>
</package>

ValueStack 和 OGNL

对象图导航语言(OGNL 1 )是一种功能强大的表达式语言,用于从 JavaBeans 设置和获取属性,以及从 Java 类调用方法。它还有助于数据传输和类型转换。OGNL 类似于 EL 和 JSTL,它们使用点符号来计算表达式和导航对象图。正如你在图 4-2 中看到的,OGNL 和 ValueStack 虽然不是 MVC 的一部分,但却是 Struts 2 框架的核心。所有 MVC 组件都与 ValueStack 交互以提供上下文数据。这些组件使用 OGNL 语法访问 ValueStack,在 Struts 2 中,OGNL 和 ValueStack 一起处理请求。具体来说,当一个请求被发送到 Struts 2 应用时,会为该请求创建一个 ValueStack 对象,对为该请求而创建的所有对象的引用以及范围属性都保存在 ValueStack 中。所有这些物体都可以通过 OGNL 看到。你不会发现 OGNL 很难使用,因为它类似于埃尔和 JSTL(这将在第三章中讲述)。清单 4-10 展示了 OGNL 的样子。注意,OGNL 使用#,不像 JSP EL 使用$。

清单 4-10 。使用 OGNL

<s:property value="#book.bookTitle" />

结果类型和结果

在 Struts 2 中,响应的呈现由结果类型和结果组成。结果类型提供了返回给用户的视图类型的实现细节。动作的每个方法都返回一个结果,其中包括指定结果类型。如果没有指定结果类型,那么使用默认的结果类型,它会转发到一个 JSP 页面,如清单 4-11 所示。

清单 4-11 。默认结果类型

<result name="success">
   /hello.jsp
</result>

Struts 2 附带了许多预定义的结果类型。Struts 允许您使用其他视图技术来呈现结果,包括速度、FreeMarker 和 Tiles,如清单 4-12 所示。

清单 4-12 。将切片声明为结果类型

<action name="login" class="com.apress.bookstore.action.LoginAction">
<result name="success" type="tiles">home</result>
<result name="error" type="tiles">login</result>
</action>

Struts 2 标签

Struts 2 框架提供了一个高级的、可移植的标记 API,您可以将它用于 JSP。在接下来的几节中,您将了解标记如何工作,以及如何使用 OGNL 来引用 ValueStack 上的值。表 4-3 描述了 Struts 2 标签库的不同类别。

表 4-3 。Struts 标签

|

Struts 2 标签

|

描述

|
| --- | --- |
| Ajax 标记 | Struts 通过 Ajax 标签提供 Ajax 支持。 |
| 控制标签 | 这些标签提供了操作元素集合的方法。 |
| 数据标签 | 这些是呈现来自动作、国际化文本和 URL 的数据的标签。 |
| 表单标签 | 这些标签为 HTML 表单标签提供了包装器,还提供了额外的小部件,比如日期选择器。 |
| 非表单用户界面标签 | 该组中的标签用于表单中,但不是直接的表单条目元素。它们包括错误消息显示、选项卡式面板和树视图。 |

注意你可以在Struts . Apache . org/release/2.3 . x/docs/tag-reference . html的在线文档中找到 Struts 2 标签的完整列表。

Struts 2 入门

在本节中,您将开发一个 HelloWorld Struts 2 应用。您将使用名为 Maven 的构建和依赖管理工具。 2 Maven 是一个命令行工具,用于构建和打包项目以及管理依赖关系。通过在多个项目之间提供相同的目录结构,这使得开发人员跨多个项目工作变得更加容易。

Maven 配置文件(pom.xml)中描述的显式配置和可传递的依赖项将在构建过程 中从本地存储库访问或下载到本地存储库。该特性允许开发人员创建新项目,而无需创建公共目录结构、创建配置文件以及从头开始编写默认类和测试。使用 Maven 进行运行时依赖 的好处是,你不需要手动记忆和搜索需要的依赖。您将使用 Maven 4 来导入 Struts 2 运行时依赖项。Maven 4 与 Eclipse-kepler 集成在一起,正如第一章中提到的。因此,您不必为 Eclipse 安装 Maven 插件。创建 Maven 项目 ,点击image新建【其他】,如图图 4-3 所示。

9781430259831_Fig04-03.jpg

图 4-3 。选择一个专家项目

在向导中选择 Maven 项目,如图图 4-4 所示。

9781430259831_Fig04-04.jpg

图 4-4 。选择选项 Maven 项目

点击下一步,配置 选项,如图图 4-5 所示。

9781430259831_Fig04-05.jpg

图 4-5 。配置一个 Maven 项目

点击下一步,如图 4-6 所示,选择目录、组 Id 和工件 Id。

9781430259831_Fig04-06.jpg

图 4-6 。选择原型

点击下一步,输入组 ID、工件 ID 和包细节,如图图 4-7 所示。

9781430259831_Fig04-07.jpg

图 4-7 。指定原型参数

单击完成。图 4-7 中工件 Id 字段中描述的名称的项目被创建。图 4-8 显示了所创建项目的目录结构 。

9781430259831_Fig04-08.jpg

图 4-8 。已创建项目的目录结构

图 4-8 所示的目录结构遵循正常的 Maven 目录结构。

表 4-4 描述了 HelloWorld 应用 的目录结构。

表 4-4 。基于 Maven 的应用的目录结构

|

目录

|

描述

|
| --- | --- |
| 科学研究委员会 | 所有来源 |
| :-主 | 主源目录 |
| ::- java | Java 源代码 |
| ::- helloworld | 由 groupID 参数定义的包 |
| 动作 | 原型的包装 |
| ::-资源 | 资源(配置、属性等) |
| :- webapp | Web 应用文件 |
| ::- WEB-INF | WEB-INF 文件夹 |

非 Maven HelloWorld 应用的目录结构可能类似于表 4-5 。

表 4-5 。非基于 Maven 的应用的目录结构

|

目录

|

描述

|
| --- | --- |
| 科学研究委员会 | 所有来源 |
| :- helloworld | Helloworld 包 |
| -行动 | 行动包 |
| - struts.xml | 资源(配置、属性等) |
| 网 | Web 应用文件 |
| :- WEB-INF | WEB-INF 文件夹 |

您需要将 struts 2-core 依赖项添加到 HelloWorld 应用生成的 pom.xml 文件中。清单 4-13 显示了添加到 pom.xml 的代码片段。

清单 4-13 。struts 双核依赖

<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts 2-core</artifactId>
<version>2.3.15.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>

您的 pom.xml 文件 将类似于清单 4-14 。

清单 4-14 。pom.xml

1.<project fontname">http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2\. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd">
3.<modelVersion>4.0.0</modelVersion>
4.<groupId>com.apress</groupId>
5.<artifactId>helloworldStruts2</artifactId>
6.<packaging>war</packaging>
7.<version>0.0.1-SNAPSHOT</version>
8.<name>helloworldStruts2Maven Webapp</name>
9.<url>http://maven.apache.org</url>
10.<dependencies>
11.<dependency>
12.<groupId>junit</groupId>
13.<artifactId>junit</artifactId>
14.<version>3.8.1</version>
15.<scope>test</scope>
16.</dependency>
17.<dependency>
18.<groupId>org.apache.struts</groupId>
19.<artifactId>struts2-core</artifactId>
20.<version>2.3.15.1</version>
21.<type>jar</type>
22.<scope>compile</scope>
23.</dependency>
24.</dependencies>
25.<build>
26.<finalName>helloworldStruts2</finalName>
27.</build>
28.              </project>

注意你可以在Struts . Apache . org/development/2 . x/Struts 2-core/dependencies . html找到 Struts 2 的依赖项列表。

清单 4-15 显示了在项目中创建的空部署描述符 。

清单 4-15 。web . XML

<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>

您需要在部署描述符中配置 servlet 过滤器,如清单 4-16 所示。

清单 4-16 。web.xml

1.<!DOCTYPE web-app PUBLIC
2\. "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
3\. "http://java.sun.com/dtd/web-app_2_3.dtd" >
4.
5.<web-app>
6.<display-name>Hello World Struts2 Web App</display-name>
7.<filter>
8.<filter-name>struts2</filter-name>
9.<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
10.</filter>
11.
12.<filter-mapping>
13.<filter-name>struts2</filter-name>
14.<url-pattern>/*</url-pattern>
15.</filter-mapping>
16.</web-app>
  • 第 7 行到第 10 行:这几行定义了 Struts prepareendexecutefilter,它被用作 Struts 2 中的 servlet 过滤器,所有的 URL 都被映射到这个过滤器。

注意filter dispatcher(org . Apache . struts2 . dispatcher . filter dispatcher)是早期 Struts 2 开发中使用的,从 Struts 2.1.3 开始就被弃用了。

清单 4-17 展示了 struts.xml 文件。

清单 4-17 。struts . XML

1.<?xml version="1.0" encoding="UTF-8"?>
2.<!DOCTYPE struts PUBLIC
3.    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
4.    "http://struts.apache.org/dtds/struts-2.0.dtd">
5.
6.<struts>
7.<constant name="struts.devMode" value="true" />
8.<package name="basicstruts2" extends="struts-default">
9.<action name="index">
10.<result>/index.jsp</result>
11.</action>
12.</package>
13.</struts>
  • 第 9 行到第 11 行:这几行定义了 index 动作,该动作呈现结果 index.jspT3。

清单 4-18 展示了 index.jsp。

清单 4-18 。index.jsp

<html>
<body>
<h2>Hello World!</h2>
</body>
</html>

将 web 应用部署到 servlet 容器 Tomcat 7,打开 web 浏览器,访问localhost:8080/hello world struts 2/,如图图 4-9 所示。

9781430259831_Fig04-09.jpg

图 4-9 。运行 HelloWorld 应用

您开发的 HelloWorld 应用不包含任何操作,只包含一个在您创建项目时生成的 JSP 文件。您创建 HelloWorld 应用的目的是测试 Struts 2 的配置。您将在下一节中创建操作。

您将创建一个 HelloWorld 项目 ,向用户显示一条欢迎消息,如图图 4-10 所示。

9781430259831_Fig04-10.jpg

图 4-10 。HelloWorld 项目的形式

当您在 Struts 2 web 应用中提交 HTML 表单时,输入被发送到名为 Action 的 Java 类。动作执行后,结果选择一个资源来呈现响应,如图图 4-11 所示。

9781430259831_Fig04-11.jpg

图 4-11 。问候用户

让我们修改你之前创建的 HelloWorld 项目,添加一个动作,接受用户输入的表单,以及问候用户的视图,如图图 4-12 所示的目录结构。

9781430259831_Fig04-12.jpg

图 4-12 。项目的目录结构

清单 4-19 展示了允许用户输入名字并提交的表单。

清单 4-19 。index.jsp

1.<html>
2.<body>
3.
4.<form action="hello">
5.<label for="name">Enter your name</label><br /><input type="text"
6.name="name" /><input type="submit" value="Submit" />
7.</form>
8.</body>
9.</html>
  • 第 4 行:当用户提交表单时,动作名 hello 被发送到容器。

您需要一个映射来将 URL 映射到 HelloWorldAction 控制器。映射告诉 Struts 2 框架哪个类将响应用户的动作,哪个类的方法将被执行,以及哪个视图将被呈现为响应。清单 4-20 展示了这个映射文件。

清单 4-20 。struts.xml

1.<?xml version="1.0" encoding="UTF-8"?>
2.<!DOCTYPE struts PUBLIC
3.    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
4.    "http://struts.apache.org/dtds/struts-2.0.dtd">
5.
6.<struts>
7.<constant name="struts.devMode" value="true" />
8.<package name="basicstruts2" extends="struts-default"
9.namespace="/">
10.
11.<action name="index">
12.<result>/index.jsp</result>
13.</action>
14.
15.<action name="hello" class="com.apress.helloworld.action.HelloWorldAction"
16.method="execute">
17.<result name="success">/hello.jsp</result>
18.</action>
19.</package>
20.</struts>
  • 第 15 行:这一行声明了 HelloWorldAction 的动作映射。HelloWorldAction 映射到操作名 hello。
  • 第 16 行:这一行声明要执行动作的 execute()方法。
  • 第 17 行:这一行声明 hello.jsp 被指定为一个成功页面,并将被呈现为响应。

我们需要一个动作类来充当控制器。Action 类响应提交表单并将 hello 操作发送到容器的用户操作。清单 4-21 展示了 HelloWorldAction。

清单 4-21 。HelloWorldAction.java

1.package com.apress.helloworld.action;
2.
3.public class HelloWorldAction {
4.private String name;
5.
6.public String execute() throws Exception {
7.return "success";
8.}
9.
10.public String getName() {
11.return name;
12.}
13.
14.public void setName(String name) {
15.this.name = name;
16.}
17.}
18.
  • 第 6 行到第 7 行:Struts 2 框架会创建一个 HelloWorldAction 类的对象,并调用 execute 方法来响应用户的动作。execute 方法返回成功字符串,该字符串被映射到 struts.xml 中的 hello.jsp

清单 4-22 展示了 hello.jsp。

清单 4-22 。hello.jsp

1.<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
2.pageEncoding="ISO-8859-1"%>
3.<%@ taglib prefix="s" uri="/struts-tags"%>
4.<html>
5.<head>
6.<title>Hello World</title>
7.</head>
8.<body>
9.Hello
10.<s:property value="name" />
11.</body>
12.</html>
  • 第 3 行:taglib 指令告诉 servlet 容器这个页面将使用 Struts 2 标签。
  • 第 10 行😒:property 标签显示调用 HelloWorldAction 类的方法 getName 返回的值。getName 方法返回一个字符串。s:property 标记显示的就是 getName 返回的这个字符串。

现在,您将学习 Struts 2 提供的一种不同的声明性配置技术:注释。您可以创建新项目或修改之前创建的项目。图 4-13 说明了目录结构。

9781430259831_Fig04-13.jpg

图 4-13 。HelloWorld 项目使用 Struts 2 标注

要使用 Struts 2 注释,需要名为 Struts 2-conventi on-plugin 的插件。使用清单 4-23 中所示的片段将依赖项添加到 pom.xml 文件中。

清单 4-23 。struts 2-约定-插件

<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-convention-plugin</artifactId>
<version>2.3.15.1</version>
</dependency>

清单 4-24 展示了用 Struts 2 注释配置的 HelloWorldAction。

清单 4-24 。hello world action

1.package com.apress.helloworld.action;
2.
3.import org.apache.struts2.convention.annotation.Action;
4.import org.apache.struts2.convention.annotation.Result;
5.
6.@Action(value = "/hello", results = { @Result(name = "success", location = "/hello.jsp") })
7.public class HelloWorldAction {
8.private String name;
9.
10.public String execute() throws Exception {
11.return "success";
12.}
13.
14.public String getName() {
15.return name;
16.}
17.
18.public void setName(String name) {
19.this.name = name;
20.}
21.}
  • 第 6 行 : @Action 定义了一个动作的 URL。因为动作注释的值是“/hello”,所以将为请求 URL“/hello”调用动作。
  • 第 6 行 : @Result 定义了一个动作的结果。结果注释将结果代码映射到结果页面。这里,结果代码“success”被映射到结果“/hello.jsp”。

清单 4-24 使用动作和结果注释只是为了向你展示如何使用它们。您还可以使用约定插件提供的智能默认值。如果在 web.xml 中将 actionPackages filter init 参数设置为包含 action 类的包的逗号分隔列表,如清单 4-25 所示,包及其子包将被扫描。检查指定包中实现 Action 的所有类或没有实现 Action 接口并以 Action 结束的 POJO 操作。

清单 4-25 。actionPackages Init 参数

<init-param>
<param-name>actionPackages</param-name>
<param-value>com.apress.helloworld.action</param-value>
</init-param>

约定插件使用动作类名来映射动作 URL。约定插件首先删除类名末尾的单词 Action,然后将大小写转换成破折号。因此,默认情况下,将为请求 URL hello-world 调用 HelloWorldAction。但是,如果您希望为不同的 URL 调用操作,那么您可以通过使用操作注释来实现。

提示你可以在Struts . Apache . org/release/2.3 . x/docs/annotations . html找到所有 Struts 2 注释的列表。

在接下来的部分中,您将使用 Struts 2 开发书店 web 应用。

书店网络应用

在本节中,您将逐步开发包括以下功能的书店应用:

  • 登录功能
  • 模板
  • 与数据访问层的集成
  • 通过数据库登录
  • 从数据库中选择类别
  • 按类别列出书籍

该应用的完整代码可从 Apress 网站的可下载档案中获得。

登录功能

图 4-14 显示了初始登录屏幕。

9781430259831_Fig04-14.jpg

图 4-14 。书店应用的登录页面

当用户提交有效的用户名和密码组合时,用户将登录,并显示用户名,如图图 4-15 所示。

9781430259831_Fig04-15.jpg

图 4-15 。登录成功

如果用户输入不正确的用户名或密码,将向用户显示一条错误消息,如图图 4-16 所示。

9781430259831_Fig04-16.jpg

图 4-16 。登录失败

图 4-17 说明了应用的目录结构。您可以从 Apress 网站下载该应用的源代码,然后在阅读每一部分时,您可以参考导入和其他工件的源代码。

9781430259831_Fig04-17.jpg

图 4-17 。书店应用的目录结构

您可以将 struts 2-core 依赖项添加到 pom.xml 文件中,如清单 4-26 所示。

清单 4-26 。struts 双核依赖

<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts 2-core</artifactId>
<version>2.3.15.1</version>
<type>jar</type>
<scope>compile</scope>
</dependency>

您可以使用之前在 HelloWorld 项目中使用的相同的 web.xml 文件。修改欢迎文件列表文件,如清单 4-27 所示。

清单 4-27 。欢迎文件

<welcome-file-list>
<welcome-file>login.jsp</welcome-file>
</welcome-file-list>

清单 4-28 展示了 login.jsp 文件。

清单 4-28 。login.jsp

1.<%@ page contentType="text/html; charset=UTF-8"%>
2.<%@ taglib prefix="s" uri="/struts-tags"%>
3.<html>
4.<head>
5.<title>Bookstore Login</title>
6.</head>
7.<body>
8.<h3>Login Bookstore</h3>
9.<s:actionerror />
10.<s:form action="login.action" method="post">
11.<s:textfield name="username" key="label.username" size="30" />
12.<s:password name="password" key="label.password" size="30" />
13.<s:submit method="execute" align="center" />
14.</s:form>
15.</body>
16.</html>

清单 4-28 展示了几个 Struts 2 标签的用法。这是一个允许用户输入用户名和密码的登录表单。在第 10 行,当用户提交表单时,动作 login 的名称被发送到容器。这个动作名通过 struts.xml 映射到 LoginAction,如清单 4-29 所示。

清单 4-29 。struts.xml

1.<?xml version="1.0" encoding="UTF-8" ?>
2.<!DOCTYPE struts PUBLIC
3."-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
4."http://struts.apache.org/dtds/struts-2.3.dtd">
5.
6.
7.<struts>
8.<constant name="struts.enable.DynamicMethodInvocation" value="false" />
9.<constant name="struts.devMode" value="true" />
10.<constant name="struts.custom.i18n.resources" value="ApplicationResources" />
11.
12.<package name="default" extends="struts-default" namespace="/">
13.<action name="login" class="com.apress.bookstore.action.LoginAction">
14.<result name="success">view/home.jsp</result>
15.<result name="error">login.jsp</result>
16.</action>
17.</package>
18.</struts>

在清单 4-29 中,第 13 行将登录名的 LoginAction 类映射到 URL,当 LoginAction 类返回字符串 success 时,第 14 行呈现 home.jsp。如果 LoginAction 类返回字符串 error,则 login.jsp 将再次显示错误消息。

清单 4-30 展示了登录操作。

清单 4-30 。LoginAction.java

1.package com.apress.bookstore.action;
2.import com.opensymphony.xwork2.ActionSupport;
3.public class LoginAction extends ActionSupport {
4.private String username;
5.private String password;
6.public String execute() {
7.if (this.username.equals("vishal") && this.password.equals("password")) {
8.return "success";
9.} else {
10.addActionError(getText("error.login"));
11.return "error";
12.}
13.}
14.public String getUsername() {
15.return username;
16.}
17.public void setUsername(String username) {
18.this.username = username;
19.}
20.
21.public String getPassword() {
22.return password;
23.}
24.
25.public void setPassword(String password) {
26.this.password = password;
27.}
28.}

在清单 4-30 的第 7 到 9 行,用户名和密码是硬编码的。稍后,您将看到如何针对数据库进行身份验证。如果用户名或密码无效,第 10 行中的 addActonError 方法获取映射到 application resources . properties 的 Error.login 消息,如清单 4-31 所示,并返回字符串 error。

清单 4-31 。应用资源.属性

label.username= Username
label.password= Password
error.login= Invalid Username/Password

清单 4-32 展示了 home.jsp,它是在 LoginAction 类返回字符串 success 时呈现的。

清单 4-32 。home.jsp

1.<%@ page contentType="text/html; charset=UTF-8"%>
2.<%@ taglib prefix="s" uri="/struts-tags"%>
3.<html>
4.<head>
5.<title>Home</title>
6.</head>
7.<body>
8.<s:property value="username" />
9.</body>
10.</html>

清单 4-32 当 LoginAction 返回字符串 success 时,使用 s:property 标签显示用户名。

开发模板

在本节中,您将看到如何将 Tiles 框架与 Struts 2 集成。我们将为上一节中创建的 HelloWorld Struts 应用添加 Tiles 支持。Tiles 是一个模板系统,可以减少代码重复,并在 web 应用的所有页面上保持一致的外观。使用磁贴,您可以在配置文件中定义一个通用布局,该布局将扩展到 web 应用的所有网页。这意味着您可以通过更改模板文件来更改 web 应用所有页面的外观,而不是更改所有页面。对于书店应用,您将添加一个标题和菜单,如图图 4-18 所示。

9781430259831_Fig04-18.jpg

图 4-18 。书店应用的模板

图 4-19 显示了带有标题的登录屏幕。认证成功后,用户登录,呈现带有菜单栏的主页,如图图 4-20 所示。如果用户名或密码无效,用户将停留在登录屏幕上,并显示一条错误消息,如图 4-21 中的所示。

9781430259831_Fig04-19.jpg

图 4-19 。带标题的登录屏幕

9781430259831_Fig04-20.jpg

图 4-20 。成功登录时显示的菜单

9781430259831_Fig04-21.jpg

图 4-21 。登录失败,标题为

用户登录后,表头显示用户名,菜单出现,如图图 4-20 所示。

图 4-22 说明了与模板文件的目录结构。

9781430259831_Fig04-22.jpg

图 4-22 。模板文件的目录结构

你需要用 struts 2-tiles3-plugin 和 slf4j-log4j12 修改 pom.xml 文件,如清单 4-33 所示。这些插件是将 Tiles 与 Struts 2 集成所必需的。

清单 4-33 。struts-tiles 和 slf4j-log4j12 插件

<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-tiles3-plugin</artifactId>
<version>2.3.15.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.5.6</version>
</dependency>

清单 4-34 展示了 struts.xml 文件。

清单 4-34 。struts.xml

1.<?xml version="1.0" encoding="UTF-8" ?>
2.<!DOCTYPE struts PUBLIC
3."-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
4."http://struts.apache.org/dtds/struts-2.3.dtd">
5.
6.
7.<struts>
8.<constant name="struts.enable.DynamicMethodInvocation" value="false" />
9.<constant name="struts.devMode" value="true" />
10.<constant name="struts.custom.i18n.resources" value="ApplicationResources" />
11.
12.<package name="default" extends="struts-default" namespace="/">
13.<result-types>
14.<result-type name="tiles"
15.class="org.apache.struts2.views.tiles.TilesResult" />
16.</result-types>
17.<action name="*Link" method="{1}"
18.class="com.apress.bookstore.action.LinkAction">
19.<result name="login" type="tiles">login</result>
20.<result name="allBooks" type="tiles">allBooks</result>
21.</action>
22.<action name="login" class="com.apress.bookstore.action.LoginAction">
23.<result name="success" type="tiles">home</result>
24.<result name="error" type="tiles">login</result>
25.</action>
26.<action name="logout">
27.<result name="success" type="tiles">logout</result>
28.</action>
29.</package>
30.</struts>
  • 第 19、20、23、24 和 27 行:这些行将结果类型定义为 tiles 和 tiles.xml 中定义的 tiles 名称,如 home 和 login。根据操作返回的结果字符串,换句话说,成功或错误,tiles.xml 中定义的相应 JSP 文件将使用 tiles.xml 中的定义名称映射到 struts.xml 中的 Tiles 名称。

清单 4-35 展示了 tiles.xml 文件。

清单 4-35 。tiles.xml

1.<?xml version="1.0" encoding="UTF-8" ?>
2.
3.<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN" "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
4.
5.<tiles-definitions>
6.
7.<definition name="baseLayout" template="/template/baseLayout.jsp">
8.<put-attribute name="title" value="Template" />
9.<put-attribute name="header" value="/template/header.jsp" />
10.<put-attribute name="menu" value="/template/menu.jsp" />
11.<put-attribute name="body" value="/template/body.jsp" />
12.</definition>
13.
14.<definition name="login" extends="baseLayout">
15.<put-attribute name="title" value="Log in" />
16.<put-attribute name="menu" value="" />
17.<put-attribute name="body" value="/login.jsp" />
18.</definition>
19.<definition name="error" extends="baseLayout">
20.<put-attribute name="title" value="Log in" />
21.<put-attribute name="menu" value="" />
22.<put-attribute name="body" value="/login.jsp" />
23.</definition>
24.<definition name="home" extends="baseLayout">
25.<put-attribute name="title" value="Log in" />
26.<put-attribute name="menu" value="/template/menu.jsp" />
27.<put-attribute name="body" value="/view/home.jsp" />
28.</definition>
29.<definition name="logout" extends="baseLayout">
30.<put-attribute name="title" value="Log in" />
31.<put-attribute name="menu" value="" />
32.<put-attribute name="body" value="/login.jsp" />
33.</definition>
34.
35.<definition name="allBooks" extends="baseLayout">
36.<put-attribute name="title" value="All Books" />
37.<put-attribute name="body" value="/allBooks.jsp" />
38.</definition>
39.
40.
41.</tiles-definitions>

清单 4-36 展示了 baselayout.jsp 文件,它定义了页眉、页脚和正文内容应该插入的位置。

清单 4-36 。基地布局

1.<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles"%>
2.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
3.   "http://www.w3.org/TR/html4/loose.dtd">
4.
5.<html>
6.<head>
7.<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
8.<link rel="stylesheet" href="css/bookstore.css" type="text/css" />
9.<script type="text/javascript" src="js/jquery-1.9.1.js"></script>
10.<script src="js/bookstore.js"></script>
11.<title><tiles:insertAttribute name="title" ignore="true" /></title>
12.</head>
13.<body>
14.<div id="centered">
15.
16.
17.<tiles:insertAttribute name="header" />
18.
19.<tiles:insertAttribute name="menu" />
20.
21.<tiles:insertAttribute name="body" />
22.
23.
24.</div>
25.</body>
26.</html>

清单 4-37 展示了 header.jsp 文件。

清单 4-37 。header.jsp

1.<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
2.pageEncoding="ISO-8859-1"%>
3.<%@ taglib prefix="s" uri="/struts-tags"%>
4.
5.<div class="header">
6.<h2>
7.<span style="margin-left: 15px; margin-top: 15px;" class="label">BOOK
8.<span style="color: white;">STORE</span>
9.</span>
10.</h2>
11.<span style="color: black; margin-left: 15px;">
12.
13.
14.<s:if test="%{username!=null && !hasActionErrors() }">Welcome <s:property value="username" /> | <a href='<s:url action="logout.action"/>'>Log out</a></s:if>
15.<s:else>
16.Login
17.</s:else>
18.
19.
20.
21.
22.</span>
23.
24.</div>
25.

第 14 到 17 行使用 Struts 2 s:if、s:else 和 s:property 标记来欢迎用户登录并允许用户注销。由于该代码位于标题中,因此应用中的所有页面都可以使用该功能。

清单 4-38 展示了 web.xml 文件。

清单 4-38 。web.xml

1.<!DOCTYPE web-app PUBLIC
2\. "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
3\. "http://java.sun.com/dtd/web-app_2_3.dtd" >
4.
5.<web-app>
6.<display-name>Archetype Created Web Application</display-name>
7.<context-param>
8.<param-name>org.apache.tiles.impl.BasicTilesContainer.DEFINITIONS_CONFIG</param-name>
9.<param-value>/WEB-INF/tiles.xml</param-value>
10.</context-param>
11.
12.<filter>
13.<filter-name>struts2</filter-name>
14.<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
15.</filter>
16.<filter-mapping>
17.<filter-name>struts2</filter-name>
18.<url-pattern>/*</url-pattern>
19.</filter-mapping>
20.<listener>
21.<listener-class>org.apache.tiles.extras.complete.CompleteAutoloadTilesListener</listener-class>
22.</listener>
23.<welcome-file-list>
24.<welcome-file>index.jsp</welcome-file>
25.</welcome-file-list>
26.
27.</web-app>

需要第 7 行到第 10 行和第 21 行到第 22 行来配置带有 Struts 2 的图块。

集成数据访问层

在本节中,您将把您创建的 web 应用与数据访问层集成在一起。您可以使用您在第一章中创建的数据访问层。然后,根据数据库对用户进行身份验证。接下来,您将在菜单栏上显示书店数据库中的类别。然后,您将按类别从数据库中检索图书列表。图 4-23 说明了应用的目录结构 。

9781430259831_Fig04-23.jpg

图 4-23 。带有数据访问层的目录结构

正如您在目录结构中看到的,应用中只有一个 BookController 。清单 4-39 中说明了该控制器中的方法。

清单 4-39 。记账员

1.      public class BookController extends ActionSupport  {
2.
3.              //properties
4\. 
5.              public String login() {}
6.
7.              public String executelogin() {}
8.
9.              public String error() {}
10.
11.             public String allBooks() {}
12.
13.             public String booksByCategory() {}
14.
15.              public String searchByKeyword() {}
16.
17.              public String home() {}
18.
19.              public String selectedBooks(){}
20.
21.              public String logout() {}
22.
23.             // getters and setters
24.
25.      }
  • 第 5 行:负责显示登录表单。
  • 第 7 行:根据数据库认证用户。
  • 第 9 行:显示错误信息,例如用户无效。
  • 第 11 行:列出书店里所有的书。
  • 第 13 行:按类别列出书籍。
  • 第 15 行:允许用户通过关键字搜索书籍:书名或作者名。
  • 第 17 行:点击首页链接,显示首页。
  • 第 19 行:显示所选书籍列表。
  • 第 21 行:允许用户注销。

使用数据库登录

清单 4-40 说明了 executelogin()方法负责对数据库中的用户进行认证。为此,您需要使用以下 DDL 将用户表添加到您在第一章中开发的数据模型:

CREATE TABLE USER(
ID INT NOT NULL AUTO_INCREMENT,
FIRST_NAME VARCHAR(60) NOT NULL,
LAST_NAME VARCHAR(60) NOT NULL,
USERNAME VARCHAR(60) NOT NULL,
PASSWORD VARCHAR(60) NOT NULL,
PRIMARY KEY (ID)
);

清单 4-40 。BookController 中的 executelogin()方法

1.      public String executelogin() {
2.                      String executelogin = "failed";
3.                      session = ActionContext.getContext().getSession();
4.                      dao = new BookDAOImpl();
5.                      user = new User();
6.                      user.setUserName(getUsername());
7.                      user.setPassword(getPassword());
8.                      setUser(user);
9.                      if (dao.isUserAllowed(user)) {
10.
11.                             setCategoryList(dao.findAllCategories());
12.                             session.put("username", username);
13.                             session.put("categoryList", getCategoryList());
14.                             executelogin = "success";
15.                     }
16.                     else {
17.                             addActionError(getText("error.login"));
18.                             return "error";
19.                     }
20.                     // return result;
21.                     return "executelogin";
22.             }

图 4-24 说明了用户表。

9781430259831_Fig04-24.jpg

图 4-24 。用户表

  • 第 9 行:用户通过数据库的认证。正如您在第 9 行中看到的,通过调用 DAO 上的 isUserAllowed(),替换了清单 4-31 中硬编码的用户名和密码。isUserAllowed()方法根据登录表单中输入的用户名和密码,从结果集中的用户表(如图图 4-24 所示)中选择用户名和密码。
  • 第 11-14 行:如果用户有效,则从数据库中检索类别,并将类别列表存储在会话中。
  • 第 16-19 行:第 16 行将有效布尔变量设置为 true,如果结果集包含用户名和密码,如果用户无效,则返回字符串错误。

显示从数据库中检索到的类别

清单 4-41 展示了显示从数据库返回的类别的菜单。在清单 4-40 中,您检索了类别并将它们存储在有效用户的会话中。这些类别将在 menu.jsp 展示。

清单 4-41 。menu.jsp

1.<li><div>
2.
3.<span class="label" style="margin-left: 15px;">
4.<a href="<s:url action=""/>">Categories</a></span>
5.</div>
6.<ul>
7.<li><s:form action=" booksByCategoryLink">
8.
9.<s:select name="category" list="#session['categoryList']"
10.listValue="categoryDescription" listKey="id" />
11.<s:submit value="Select" />
12.</s:form><a class="label" href=""><span class="label"
13.style="margin-left: 30px;"></span></a></li>
14.
15.</ul></li>
  • 第 9 行到第 10 行:这些行使用 Struts 2 的表达式语言显示存储在 LoginAction 会话中的列表中的类别。
  • 第 11 行:当用户点击 select 按钮时,第 7 行中选择的类别和动作名称 booksByCategoryLink 被发送到容器中,然后通过 struts.xml 映射到动作,该动作检索图书列表。

按类别列出图书

在本节中,您将从数据库中按类别检索图书列表。选定的类别和动作名称 booksByCategoryLink 被发送到容器,然后通过 struts.xml 映射到 BookController。清单 4-42 展示了 struts.xml 的代码片段

清单 4-42 。用 struts.xml 声明 BookController

1.      <action name="*Link" method="{1}"
2.                              class="com.apress.bookstore.controller.BookController">
3.                              <result name="login" type="tiles">login</result>
4.                              <result name="allBooks" type="tiles">booklist</result>
5.                              <result name="booksByCategory" type="tiles">booklist</result>
6.                              <result name="searchByKeyword" type="tiles">booklist</result>
7.                              <result name="home" type="tiles">home</result>
8.                              <result name="executelogin" type="tiles">executelogin</result>
9.                              <result name="selectedBooks" type="tiles">selectedBooks</result>
10.                             <result name="logout" type="tiles">logout</result>
11.                             <result name="error" type="tiles">error</result>
12.                     </action>

清单 4-42 中第一行的是通配符。任何以 Link 结尾的操作名称值都将由该操作映射处理。Link 之前的任何值都将是用于方法属性的值(占位符{1}将被替换为该值)。因此,不用在这个简单应用的配置文件中编写九个单独的操作映射节点,只需在名称值中使用通配符并在方法值中使用属性值占位符({1})。这使得 Struts 2 框架能够在运行时动态选择要调用的正确方法。清单 4-43 展示了 booksByCategory()。

清单 4-43 。booksByCategory()

public String booksByCategory() {
                dao = new BookDAOImpl();
                setBookList(dao.findBooksByCategory(category));
                return "booksByCategory";
        }

当列表 4-43 中的 booksByCategory()返回时,它被映射到图块名称 booklist,如列表 4-42 的第 5 行所示。这映射到 tiles.xml 中定义的图书列表,如呈现 booklist.jsp 文件的清单 4-44 所示。清单 4-44 展示了 tiles.xml 中的代码片段

清单 4-44 。tiles.xml

1.<definition name="booklist" extends="baseLayout">
2.<put-attribute name="title"  value="Log in"/>
3.<put-attribute name="menu"   value="/menu.jsp"/>
4.<put-attribute name="body"   value="/view/bookList.jsp"/>
5.</definition>

清单 4-45 说明了 bookList.jsp。

清单 4-45 。booklist.jsp

1.<%@  taglib uri="/struts-tags" prefix="s"%>
3.<body>
5.<div id="centered">
8.<s:form action=" selectedbooksLink" theme="simple">
9.<center>
10.<table id="grid">
11.<thead>
12.<tr>
13.<th id="th-title">Book Title</th>
14.<th id="th-author">Author</th>
15.<th id="th-price">Price</th>
16.</tr>
17.</thead>
20.<tbody>
22.<s:iterator value="bookList" id="book">
23.<tr>
25.<td>
26.<s:checkboxname="selectedBooks" fieldValue="%{bookId}" />
27\. <s:propertyvalue="#book.bookTitle" />
29.</td>
30.<td>
31.<s:iterator value="#book.authors" id="author">
32.<s:if test="%{#book.id == #author.bookId}">
33.<s:property value="#author.firstName" />
34.<s:property value="#author.lastName" />
35.</s:if>
36.</s:iterator>
37.</td>
39.<td><s:property value="price" /></td>
40.</tr>
42.</s:iterator>
43.</tbody>
45</table>
47.</center><br>
49.<s:submit value="Add to the shopping cart" />
51.</s:form>
52.
53.</div>
54.</body>

清单 4-45 展示了 Struts 2 标签的用法,以及 Struts 2 OGNL 如何用于导航第 27 行的属性 bookTitle 以及第 33 和 34 行的属性 firstName 和 lastName(作者的)。第 31 行演示了简单的嵌套 s:iterator 的用法。第 26 行允许用户选择要添加到购物车的书籍。用户可以通过点击第 49 行上的按钮来选择和提交图书。当用户单击“添加到购物车”按钮时,第 8 行的操作名称 selectedbooksLink 和第 26 行的 bookId 被发送到容器,然后通过 struts.xml 映射到 Add to Cartaction。然后,AddToCart 操作将图书存储到数据库中的购物车,AddToCart 操作返回字符串 success,该字符串通过 struts.xml 中的操作映射和 tiles.xml 文件中的 tiles 映射到视图 selectedbooksLink。用户可以单击 selectedBooks.jsp 页面上的 Purchase 按钮,以同样的方式调用 PurchaseAction。本章简要概述了 Struts 2。关于 Struts 2 的更详细的报道,我推荐伊恩·罗维利的实用 Apache Struts 2 Web 2.0 项目

摘要

在本章中,您看到了遵循 MVC 设计模式的 Struts 2 框架的核心组件,并且看到了 Struts 2 如何通过拦截器来分离横切关注点。您看到了 Struts 2 框架如何以两种形式提供声明式架构:XML 和注释。在下一章,你将学习另一个面向动作的框架,叫做 Spring Web MVC。

【http://commons . Apache . org/proper/commons-every/

【http://maven . Apache . org/

五、使用 Spring Web MVC 构建 Java Web 应用

你能做的任何事,我都能做到。

-丹尼尔丹尼特

马克·吐温曾经说过:“在春天,我已经数出了 136 种不同的天气。”几乎可以肯定,他指的不是 Spring 框架。或者他是千里眼?Spring 框架已经成长为一个项目生态系统;它包括许多不同的模块,集成了许多框架和库,并在不同的领域提供了各种各样的功能,如 Flash、企业应用、web 服务、数据存储、OSGi、 1 甚至. net。Spring 应用在所有流行的云平台上都得到支持,如 Cloud Foundry、2Google App Engine、 亚马逊 EC2 3 可以利用传统的 RDBMSs 以及新的 NoSQL 4 解决方案和数据存储,如 PostgreSQL、 5 MySQL、MongoDB、 6 和 Redis。 与许多其他框架(如 Struts)不同,它只限于开发 web 应用,Spring 框架可以用来构建独立的、web 的和 JEE 的应用。Spring 支持构建现代 web 应用,包括 REST、HTML5 和 Ajax,以及移动客户端平台,包括 Android 和 iPhone。Spring 框架通过将组件与系统连接起来,使您不必编写管道代码,从而使您能够专注于应用的业务,从而显著地永远改变了企业 Java 的前景。

Spring 框架概述

Spring 框架由以下几类特性组成:

  • AOP 和仪器
  • 核心容器
  • 数据访问/集成
  • 试验

以下各节描述了每个类别中的模块。

AOP 和仪器

AOP 和仪器类别包括 AOP、方面和仪器模块,如表 5-1 中所述。

表 5-1 。AOP 模块

|

组件

|

描述

|
| --- | --- |
| 面向切面编程 | Spring 的 AOP 模块提供了一个符合 AOP 联盟的面向方面的编程实现。 |
| 方面 | 方面模块提供了与 AspectJ 的集成。 |
| 使用仪器 | 插装模块提供了类插装支持和类加载器实现。 |

核心容器

核心容器类别由 Beans、核心、上下文和表达式语言模块组成,如表 5-2 中所述。

表 5-2 。核心模块

|

组件

|

描述

|
| --- | --- |
| 豆子 | Beans 模块提供了 IoC 和依赖注入特性。 |
| 核心 | 核心模块提供了 IoC 和依赖注入特性。 |
| 语境 | 上下文模块建立在核心和 Beans 模块的基础上,并增加了对国际化、事件传播、资源加载、EJB 和 JMX 的支持。 |
| 表达语言 | 表达式语言模块提供了 Spring 表达式语言。 |

数据访问/集成

数据访问/集成层由 JDBC、ORM、OXM、JMS 和事务模块组成,如表 5-3 所述。

表 5-3 。数据访问/集成模块

|

模块

|

描述

|
| --- | --- |
| 数据库编程 | JDBC 模块提供了一个 JDBC 抽象层,消除了使用纯 JDBC 的需要。 |
| 对象关系映射(Object Relation Mapping) | ORM 模块提供了 ORM 框架的集成,如 JPA、JDO、Hibernate 和 iBatis。 |
| 泌酸调节素 | OXM 模块支持 JAXB、Castor、XMLBeans、JiBX 和 XStream 的对象/XML 映射实现。 |
| (同 JavaMessageService)Java 消息服务 | JMS 模块提供了生成和使用消息的功能。 |
| 事务 | 事务模块支持编程式和声明式事务管理。 |

试验

测试类别由测试模块组成,如表 5-4 所述。

表 5-4 。测试模块

|

模块

|

描述

|
| --- | --- |
| 试验 | 测试模块支持用 JUnit 或 TestNG 测试 Spring 组件。 |

web 层由 Web、Web-Servlet、Web-Struts 和 Web-Portlet 模块组成,如表 5-5 中所述。

表 5-5 。Web 模块

|

模块

|

描述

|
| --- | --- |
| 网 | Spring 的 web 模块提供了基本的面向 web 的集成特性和面向 Web 的应用上下文。它还提供了几个远程选项,比如远程方法调用(RMI)、Hessian、Burlap、JAX-WS 和 Spring 自己的 HTTP invoker。 |
| Web-Servlet | Web-Servlet 模块包含了 Spring 的 Web 应用的模型-视图-控制器(MVC)实现。 |
| Web-Struts | Web-Struts 模块支持在 Spring 应用中集成传统的 Struts web 层。请注意,从 Spring 3.0 开始,这种支持已经过时了。 |
| 门户网站 | Web-Portlet 模块提供了在 Portlet 环境中使用的 MVC 实现。 |

注意Spring 框架的核心是基于控制反转(IoC)的原理,它提供依赖注入。然而,Spring 并不是唯一提供依赖注入特性的框架;还有其他几个框架,比如 Seam 8 、Google Guice 9 ,以及 JEE6 和更新的版本都提供依赖注入。

在撰写本文时,预计将发布 Spring 3 . 2 . 2;Spring 3.1 发布于 2011 年 12 月。Spring Framework 4.0 预计在 2013 年底发布,计划支持 Java SE 8、Groovy 2 和 Java EE 7 的某些方面。

Spring 框架基础知识

一个应用由 web 组件和业务逻辑组件等组件组成。这些组件需要相互协作来实现应用的共同业务目标,因此这些组件相互依赖。如果不加以控制,这种依赖性通常会导致它们之间的紧密耦合,从而导致应用不可维护。控制这种耦合以使它不会导致一个紧密耦合的应用是一项不简单的任务。相比之下,如果应用的一个组件不依赖于另一个组件,它就不必去寻找它,所有的组件都是完全隔离的,产生的应用将是松散耦合的。但是这样的应用什么都不会做。本质上,组件应该依赖于其他组件,但不应该寻找它们所依赖的组件。相反,这种依赖关系应该提供给依赖组件。这就是控制反转的本质。Spring 框架就是这样一个 IoC 框架,它通过依赖注入的方式为依赖组件提供依赖。

IoC 基于好莱坞原则 10 :“不要召唤我们;我们会打电话给你。”

图 5-1 是 Spring 如何工作的高级视图。

9781430259831_Fig05-01.jpg

图 5-1 。Spring IoC 容器

如图 5-1 所示,Spring IoC 容器通过使用应用 POJO 对象和配置元数据产生完全配置的应用对象。

  • 应用 POJO 对象 :在 Spring 中,由 Spring IoC 容器管理的应用对象被称为bean。Spring bean 是由 Spring IoC 容器实例化、组装和管理的对象。

春天,组件也叫。Spring beans 不同于 JavaBeans 约定。Spring beans 可以是任何普通的旧 Java 对象(POJOs)。POJO 是一个普通的 Java 对象,没有任何特定的要求,比如扩展特定的类或实现特定的接口。

  • 配置元数据 :配置元数据指定了组成应用的对象以及这些对象之间的相互依赖关系。容器读取配置元数据,并从中判断出要实例化、配置和组装哪些对象。然后,容器在创建 bean 时注入这些依赖项。配置元数据可以用 XML、注释或 Java 代码来表示。

就实现而言,Spring 容器可以在实例和静态方法的参数中注入对象,并可以通过依赖注入来注入构造函数。假设您有一个应用,它的组件 ClassA 依赖于 ClassB。换句话说,ClassB 是依赖关系。你的标准代码看起来会像清单 5-1 中的。

清单 5-1 。紧密耦合依赖

1.    public class ClassA {
2.       private ClassB classB;
3.       public ClassA() {
4.          classB = new ClassB();
5.       }
6.    }

我们在清单 5-1 的第 3-4 行中创建了 ClassA 和 ClassB 之间的依赖关系。这将 ClassA 和 ClassB 紧密地结合在一起。这种紧密耦合可以使用 IoC 来规避,为了做到这一点,首先我们需要将清单 5-1 中的代码改为清单 5-2 中的代码。

清单 5-2 。拆除 a 级和 b 级之间的紧耦合

1.    public class ClassA {
2.       private ClassB classB;
3.       public ClassA(ClassB classB) {
4.          this.classB = classB;
5.       }
6.    }

在清单 5-2 中可以看到,ClassB 是独立实现的,Spring 容器在 ClassA 实例化的时候提供了 ClassB 到 ClassA 的这种实现,依赖关系(换句话说就是 class ClassB)通过构造函数注入到 ClassA 中。因此,控制权已经从 ClassA 中移除并保存在别处(也就是说,在一个 XML 配置文件中,如清单 5-3 所示),并因此被依赖注入(DI)所“反转”,因为依赖被委托给外部系统,换句话说,配置元数据。清单 5-3 展示了一个典型的 Spring 配置元数据文件。

清单 5-3 。配置元数据

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <beans fontname">http://www.springframework.org/schema/beans"
 3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4.        xsi:schemaLocation="http://www.springframework.org/schema/beans
 5.            http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
 6.
 7.
 8.    <bean id=" .." class=".."/>
 9.
10.    <!-- more bean definitions -- >
11.
12.    </beans>
  • 第 2 行:要使用、< bean >等标签,需要声明某些名称空间。核心 Spring 框架附带了 10 个配置名称空间。现在让我们专注于 beans 名称空间;随着本章的进展,将使用其他名称空间。然而,在本书中,我们只需要 aop、beans 和上下文模式,所以如果你对使用其他模式感兴趣,你可以在static . springsource . org/spring/docs/3 . 2 . 2 . release/spring-framework-reference/html/xsd-config . html找到它们。
  • 第 2 行:在基于 XML 的配置中描述 bean 时,XML 文件的根元素是 Spring 的 bean 模式中的。整个 Spring 配置,包括< bean >声明,都放在顶层中。
  • 线 8 : <豆>是春季最基本的配置单位。它告诉 Spring 为应用创建一个对象。这些< bean >定义对应于组成应用的实际对象。
  • 第 8 行:id 属性是一个帮助识别单个 bean 定义的字符串。class 属性定义了 bean 的类型,并使用完全限定的类名。id 属性的值指的是协作对象。

注意虽然 XML 是定义配置元数据的经典方式,但是您可以使用注释(来自 Spring 2.5 和更新版本)或 Java 代码(来自 Spring 3.0 和更新版本)。

清单 5-4 说明了应该包含在清单 5-3 中说明的配置元数据文件中的 bean 定义,以在清单 5-2 中注入 classB 依赖。

清单 5-4 。配置依赖关系

1.    <!-- Definition for classA bean -->
2.       <bean id="classA" class="ClassA">
3.          <constructor-arg ref="classB"/>
4.       </bean>
5.
6.       <!-- Definition for classB bean -->
7.       <bean id="classB" class="ClassB">
8.       </bean>
  • Line 2 :这指定了由 Spring 容器创建和管理的 classA bean。
  • 第 3 行 : < constructor-arg >通过在元素中声明 bean 属性,经由构造函数注入来配置 bean 属性。
  • 第 7 行:指定 classB bean 应该由 Spring 容器创建和管理。

清单 5-4 中描述的依赖注入被称为基于构造器的依赖注入。当容器调用带有许多参数的类构造函数时,基于构造函数的 DI 就完成了,每个参数表示对另一个类的依赖。DI 的另一个变种叫做基于设置器的依赖注入。在基于 setter 的 DI 中,容器在调用无参数构造函数或无参数静态工厂方法实例化 bean 后,调用 bean 上的 setter 方法。要使用基于 setter 的 DI,您需要修改清单 5-2 中的,使其看起来像清单 5-5 中的。

清单 5-5 。注入依赖关系的 Setter 方法

 1.    public class ClassA{
 2.       private ClassB classB;
 3.
 4.       // a setter method to inject the dependency.
 5.       public void setClassB(ClassB classB) {
 6.          this.classB = classB;
 7.       }
 8.       // a getter method to return classB
 9.       public ClassB getClassB() {
10.          return classB;
11.       }
12.
13.    }

在清单 5-5 中,DI 通过创建 ClassB 实例的 ClassA 类的 setter 方法发生,这个实例用于调用 setter 方法来初始化 ClassA 的属性。清单 5-6 展示了 bean 定义,它应该包含在清单 5-3 中展示的配置元数据文件中,以实现清单 5-5 中要求的基于 setter 的依赖注入。

清单 5-6 。通过基于 Setter 的 DI 配置依赖关系

<bean id="classA" class="ClassA">
<property name="classB" ref="classB" />
</bean>
<bean id="classB" class="ClassB" />

标签为依赖注入定义了一个属性。清单 5-6 可以被翻译成清单 5-7 所示的 Java 代码。

清单 5-7 。Java 代码相当于清单 5-6 中的

ClassA classA = new ClassA();
ClassB classB = new ClassB();
classA.setClassB(classB);

提示基于构造函数的 DI 和基于设置器的 DI 可以同时使用,但是建议对强制依赖项使用构造函数参数,对可选依赖项使用设置器。

Spring 容器本质上是一个工厂,它创建封装了对象创建的对象,并使用配置元数据来配置这些对象,配置元数据包含关于应用中必须创建的协作对象的信息。Spring 提供了两种 IoC 容器实现。

  • Bean 工厂(由 org . spring framework . beans . factory . bean factory 接口定义)
  • 应用上下文(由 org . spring framework . context . application context 接口定义)

Bean 工厂是最简单的容器,为 DI 提供基本支持。ApplicationContext 是 BeanFactory 的子接口,它提供应用框架服务,例如:

  • 从属性文件解析文本消息的能力
  • 向感兴趣的事件侦听器发布应用事件的能力
  • 特定于应用层的上下文,例如要在 web 层中使用的 WebApplicationContext

注意 Web 应用有自己的 WebApplicationContext。当我们在本章后面讨论基于 web 的 Spring 应用时,将解释 WebApplicationContext。

应用上下文

Spring 自带了 ApplicationContext 接口的几个现成实现。最常用的三种方法如下:

  • ClassPathXmlApplicationContext:从位于类路径中的 XML 文件加载上下文定义
  • FileSystemXmlApplicationContext:从文件系统中的 XML 文件加载上下文定义
  • XmlWebApplicationContext:从 web 应用中包含的 XML 文件加载上下文定义

在独立应用中,创建 ClassPathXmlApplicationContext 或 FileSystemXmlApplicationContext 的实例是很常见的。

在图 5-1 之后,您必须实例化 Spring IoC 容器(ApplicationContext ),通过读取它们的配置(配置元数据)来创建 bean 实例。然后,您可以从 IoC 容器中获取 bean 实例来使用。

清单 5-8 展示了 ClassPathXmlApplicationContext 的实例化,它是 ApplicationContext 的一个实现。ClassPathXmlApplicationContext 实现通过从类路径加载 XML 配置文件来构建应用上下文。

清单 5-8 。ClassPathXmlApplicationContext 的实例化

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

清单 5-9 说明了文件系统 XmlApplicationContext 的实例化。

清单 5-9 。file systemxmlapplicationcontext 的实例化

ApplicationContextcontext=new FileSystemXmlApplicationContext("c:/beans.xml");

注意filesystemxmlaplicationcontext 在文件系统中的特定位置寻找 beans.xml ,而 ClassPathXmlApplicationContext 在类路径(包括 JAR 文件)的任何地方寻找 beans.xml

在接下来的小节中,您将学习如何在创建第一个基于 Spring 的独立应用时使用应用上下文。

Spring 框架的主要目标

依赖注入并不是使用 Spring 框架的唯一好处。Spring 框架的目标是简化开发企业应用的复杂性。这种复杂性在企业应用中以多种方式表现出来,Spring 框架之前的大多数企业应用都无意中遭受了以下几个甚至全部的困难:

  • 紧密结合
  • 贯穿各领域的问题
  • 样板代码

从根本上说,Spring 使您能够从 POJO 构建应用,并将企业服务非介入式地应用到 POJO,这样域模型就不依赖于框架本身。因此,Spring 框架背后的驱动力是通过启用基于 POJO 的编程模型来促进 Java EE 开发中的最佳实践。

使用依赖注入处理紧耦合

现在让我们看看 Spring 如何在一个简单的独立应用的帮助下,通过依赖注入实现松散耦合 。这个应用的代码可以在 Apress 网站的可下载文档中找到。此外,这个应用将成为春天森林的序言。清单 5-10 、 5-11 和 5-12 展示了服务提供者对象的层次结构以及清单 5-13 中展示的 VehicleService 的依赖关系。

清单 5-10 。车辆接口

public interface Vehicle {
public String drive();
}

清单 5-11 。车辆实施:自行车

public class Bike implements Vehicle{
    public String drive() {
        return " driving a bike";
    }
}

清单 5-12 。车辆实施:汽车

public class Car implements Vehicle {

    public String drive() {
        return " driving a car";
    }
}

这些服务提供者对象由 VehicleService 类使用,如清单 5-13 中的所示,然后由客户端对象使用,如清单 5-14 中的所示。

清单 5-13 。车辆服务 ??

 1.    public class VehicleService {
 2.
 3.        private Vehicle vehicle = new Bike();
 4.
 5.        public void driver() {
 6.            System.out.println(vehicle.drive());
 7.
 8.        }
 9.
10.    }
  • 第 3 行:在清单 5-13 中,类 Bike 是类 VehicleService 的依赖,在第 3 行被实例化。这是一个紧耦合的例子,因为 VehicleService 类是实现感知的车辆对象,在这个例子中是 Bike。

清单 5-14 展示了独立的 VehicleApp。

清单 5-14 。独立车辆应用

1.    public class VehicleApp {
2.        public static void main(String[] args) {
3.            VehicleService service = new VehicleService();
4.            service.driver();
5.        }
6.    }

如清单 5-14 所示,VehicleService 知道车辆对象的实现,因此与它紧密耦合。现在让我们通过 Spring 框架的 DI 来分离这个应用。第一步是使用 Eclipse IDE 创建一个 Java 项目。选择文件image新建image项目,然后从向导列表中选择 Java 项目向导。使用向导将你的项目命名为松散耦合应用,如图图 5-2 所示。

9781430259831_Fig05-02.jpg

图 5-2 。创建 Java 项目

现在您需要在项目中添加 Spring 框架和公共日志 API 库 。你可以从 http://projects.spring.io/spring-framework/下载 Spring 框架库。您可以使用 Maven 来配置项目中的 Spring 框架,如前一章所述。使用 Maven 时,需要将以下内容添加到 pom.xml 文件中:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>3.2.5.RELEASE</version>
    </dependency>
</dependencies>

使用 Maven 要容易得多,但是要知道您将在本节中创建的三个应用使用了哪些关键库,您可以在 Eclipse 中手动配置这些库。这些库可以在可下载的档案中找到,可以从该书的 press 网页上的源代码/下载标签中找到(www.apress.com/9781430259831)。

要将 Spring 框架添加到您的项目中,右键单击您的项目并选择 Build PathimageConfigure Build Path 以显示 Java Build Path 窗口,如图图 5-3 所示。现在添加您从 Apress 网站下载到文件系统中的外部 jar。

9781430259831_Fig05-03.jpg

图 5-3 。添加外部罐子

现在让我们在 looselyCoupledApplication 项目下创建实际的源文件。首先我们需要创建一个名为 com.apress.decoupled 的包。为此,在 Package Explorer 部分右键单击 src,选择 New image Package,并创建包 com.apress.decoupled。然后创建 Vehicle.java、Car.java 和 Bike.java,这是在 com.apress.decoupled 包下的清单 5-10 、 5-11 和 5-12 中所示的代码。

然后创建 VehicleService 类,如清单 5-15 所示。

清单 5-15 。松散耦合车辆服务

1.    package com.apress.decoupled;
2.
3.    public class VehicleService {
4.
5.        private Vehicle vehicle;
6.
7.        public void setVehicle(Vehicle vehicle) {
8.            this.vehicle = vehicle;
9.        }
10.
11.        public void driver() {
12.            System.out.println(vehicle.drive());
13.
14.        }
15.
16.    }
17.
  • 第 7 行:在清单 5-15 中,我们已经从 VehicleService 类中移除了总控制权,并将其保留在 XML 配置文件中,并且通过第 7 行的 setter 方法将依赖注入到 VehicleService 类中。

现在创建一个客户端类 VehicleApp,如清单 5-16 所示。

清单 5-16 。车辆申请 ??

 1.    package com.apress.decoupled;
 2.    import org.springframework.context.ApplicationContext;
 3.    import org.springframework.context.support.ClassPathXmlApplicationContext;
 4.
 5.    public class VehicleApp {
 6.
 7.        public static void main(String[] args) {
 8.            ApplicationContext  context = new ClassPathXmlApplicationContext(
 9.                    "beans.xml");
10.            VehicleService contestService = (VehicleService) context
11.                    .getBean("vehicleService");
12.            contestService.driver();
13.        }
14.
15.    }
  • 第 8 到 9 行:这几行实例化应用上下文并传递配置文件。
  • 第 10 行到第 11 行:这几行从配置文件中获取 bean。要从 Bean 工厂或应用上下文中获取声明的 bean,您需要调用 getBean()方法 ,传入唯一的 bean 名称,并在使用之前将返回类型转换为其实际类型。

现在,您需要创建一个 bean 配置文件,这是一个连接 bean 的 XML 文件(如清单 5-17 所示)。

清单 5-17 。配置文件

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <beans fontname">http://www.springframework.org/schema/beans"
 3.           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4.           xsi:schemaLocation="http://www.springframework.org/schema/beans
 5.           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 6.    <bean id="car" class="com.apress.decoupled.Car" />
 7.    <bean id="bike" class="com.apress.decoupled.Bike" />
 8.    <bean id="vehicleService" class="com.apress.decoupled.VehicleService">
 9.    <property name="vehicle">
10.    <ref local="car" />
11.    </property>
12.    </bean>
13.    </beans>

您可以为 beans.xml 选择任何您喜欢的名称。您必须确保该文件在类路径中可用,并且在创建应用上下文时在主应用中使用相同的名称,如 VehicleApp.java 文件所示。图 5-4 显示了应用的目录结构。

9781430259831_Fig05-04.jpg

图 5-4 。目录结构

使用 AOP 解决横切关注点

Spring 通过面向方面编程(AOP)实现了关注点的分离。AOP 将横切关注点封装在称为方面的独立的、可重用的组件中,并将它们添加到应用中。这个过程叫做编织。这导致内聚的组件专注于业务功能,而完全不知道系统服务,如日志、安全、事务等等。为了理解如何在 Spring 中应用方面,您现在将创建一个简单的 BookService 来检索所有书籍的列表,向 BookService 添加一个基本的日志方面。这个应用的代码可以在 Apress 网站的可下载文档中找到。清单 5-18 展示了图书服务。

清单 5-18 。图书服务界面

package com.apress.aop;

import java.util.List;

public interface BookService {
    public List<Book> getAllBooks();

}

清单 5-19 展示了这个图书服务的实现 。

清单 5-19 。图书服务实施

 1.    package com.apress.aop;
 2.
 3.    import java.util.ArrayList;
 4.    import java.util.List;
 5.
 6.    public class BookServiceImpl implements BookService{
 7.        private static List<Book> bookList;
 8.        static {
 9.
10.            Book book1 = new Book();
11.            book1.setId((long)1);
12.            book1.setBookTitle("Modern Java");
13.
14.            Book book2 = new Book();
15.            book2.setId((long)2);
16.            book2.setBookTitle("Beginning Groovy");
17.
18.            Book book3 = new Book();
19.            book3.setId((long)2);
20.            book3.setBookTitle("Beginning Scala");
21.
22.            bookList = new ArrayList<Book>();
23.            bookList.add(book1);
24.            bookList.add(book2);
25.            bookList.add(book3);
26.        }
27.
28.
29.        public List<Book> getAllBooks() {
30.            for(Book b: bookList){
31.                System.out.println("Books:"+b.getBookTitle());
32.            }
33.            return bookList;
34.        }
35.    }

BookServiceImpl 不关心日志记录。对于日志记录,创建一个如清单 5-20 所示的方面,它将被编织到 BookService 对象中,在任何需要的地方提供日志记录。

清单 5-20 。伐木方面

 1.    package com.apress.aop;
 2.
 3.    public class LoggingAspect {
 4.        public void logBefore() {
 5.
 6.            System.out.println("Before calling getAllBooks");
 7.        }
 8.
 9.        public void logAfter() {
10.            System.out.println("After calling getAllBooks");
11.        }
12.    }
13.
  • 第 3 行 : LoggingAspect 是一个简单的类,有两个方法。
  • 第 4 行:调用 getAllBooks()之前应该调用的 logBefore()方法。
  • 第 9 行:调用 getAllBooks()后应该调用的 logAfter()方法。

LoggingAspect 在没有 BookServiceImpl 要求的情况下完成它的工作。此外,因为 BookServiceImpl 不需要知道 LoggingAspect,所以不需要将 LoggingAspect 注入到 BookServiceImpl 中。这消除了 BookServiceImpl 代码中不必要的复杂性,即必须注入 LoggingAspect 并检查 LoggingAspect 是否为 null。您可能已经注意到,LoggingAspect 是一个 POJO。当它在 Spring 上下文中被声明为一个方面时,它就变成了一个方面。LoggingAspect 可以应用于 BookServiceImpl,而不需要 BookServiceImpl 显式调用它。事实上,BookServiceImpl 仍然完全不知道 LoggingAspect 的存在。要使 LoggingAspect 作为一个方面工作,您需要做的就是在 Spring 配置文件中将其声明为一个方面。清单 5-21 展示了将 LoggingAspect 声明为一个方面的应用上下文 XML 文件。

清单 5-21 。配置文件

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <beans fontname">http://www.springframework.org/schema/beans"
 3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context=
           "http://www.springframework.org/schema/context"
 4.        xmlns:aop="http://www.springframework.org/schema/aop"
 5.        xsi:schemaLocation="http://www.springframework.org/schema/beans
 6.            http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
 7.            http://www.springframework.org/schema/context
 8.            http://www.springframework.org/schema/context/spring-context-3.2.xsd
 9.            http://www.springframework.org/schema/aop
10.            http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
11.
12.
13.        <bean id="bookService" class="com.apress.aop.BookServiceImpl"/>
14.
15.        <bean id="logAspect" class="com.apress.aop.LoggingAspect"/>
16.
17.        <aop:config>
18.            <aop:aspect  ref = "logAspect">
19.                <aop:pointcut id = "log"
20.                    expression="execution(* *.getAllBooks())" />
21.                <aop:before  pointcut-ref = "log"
22.                    method="logBefore" />
23.                <aop:after  pointcut-ref = "log"
24.                    method="logAfter" />
25.            </aop:aspect>
26.        </aop:config>
27.    </beans>
  • 第 9 行到第 10 行:您使用 Spring 的 aop 配置名称空间来声明 LoggingAspect bean 是一个方面。
  • 第 15 行:您将 LoggingAspect 声明为一个 bean。即使 Spring 框架通过在上下文中将 POJO 声明为一个方面来将其转换为一个方面,它仍然必须被声明为一个 Spring < bean >。
  • 第 18 行:然后你引用了< aop:aspect >元素中的那个 bean。
  • 第 19 到 20 行:在前面的<切入点>元素中定义了切入点,并设置了一个表达式属性来选择应该在哪里应用通知。表达式语法是 AspectJ 的切入点表达式语言。
  • 第 21 行到第 22 行:你声明(使用< aop:before >)在执行 getAllBooks()方法之前,应该调用 LoggingAspect 的 logBefore 方法。这叫做之前的建议。pointcut-ref 属性引用一个名为 log 的切入点。
  • 第 23 到 24 行:你(使用< aop:after >)声明 logAfter 方法应该在 getAllBooks()执行之后被调用。这就是后知后觉的。pointcut-ref 属性引用一个名为 log 的切入点。

*清单 5-22 展示了独立的 Java 应用。

清单 5-22 。独立 Java 应用

1.    package com.apress.aop;
2.    import org.springframework.context.ApplicationContext;
3.    import org.springframework.context.support.ClassPathXmlApplicationContext;
4.
5.    public class Driver {
6.
7.            public static void main(String...args){
8.        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
9.
10.        BookService bookService = (BookService)context.getBean("bookService");
11.                bookService.getAllBooks();
12.
13.            }
14.    }

图 5-5 说明了该应用的目录结构 。

9781430259831_Fig05-05.jpg

图 5-5 。目录结构

因此,Spring AOP 可以被用来以声明的方式提供诸如事务和安全之类的服务,而不会打乱您的代码,首先,您的代码应该只关心它的业务功能。

使用模板删除样板代码

在第一章中,我们构建了书店应用的数据访问层,并在 BookDAOImpl 中使用纯 JDBC 来连接数据库。在本节中,您将看到 Spring 框架如何通过 Spring 的 JDBCTemplate 转换 BookDAOImpl,消除使用纯 JDBC 获得数据存储连接所产生的样板代码,并清理资源。这个应用的代码可以在 Apress 网站的可下载文档中找到。清单 5-23 展示了图书服务。

清单 5-23 。图书服务

package com.apress.books.service;

import java.util.List;

import com.apress.books.model.Book;

public interface BookService {
    public List<Book> getAllBooks();

}

清单 5-24 展示了 BookService 的实现。

清单 5-24 。图书服务实施

package com.apress.books.service;

import java.util.List;

import com.apress.books.dao.BookDAO;
import com.apress.books.model.Book;

public class BookServiceImpl implements BookService{

    private  BookDAO bookDao ;

    public void setBookDao(BookDAO bookDao) {
        this.bookDao = bookDao;
    }

    public List<Book> getAllBooks() {
        List<Book> bookList = bookDao.findAllBooks();

        return bookList;
    }
}

清单 5-25 说明了 BookDAO。

清单 5-25 。书道

package com.apress.books.dao;

import java.util.List;

import com.apress.books.model.Book;
import com.apress.books.model.Category;

public interface BookDAO {
    public List<Book> findAllBooks();

}

清单 5-26 展示了 BookDAO 的实现。

清单 5-26 。BookDAO 实现

 1.    package com.apress.books.dao;
 2.
 3.    import java.sql.Connection;
 4.    import java.sql.DriverManager;
 5.    import java.sql.PreparedStatement;
 6.    import java.sql.ResultSet;
 7.    import java.sql.SQLException;
 8.    import java.sql.Statement;
 9.    import java.util.ArrayList;
10.    import java.util.List;
11.
12.    import javax.sql.DataSource;
13.
14.    import org.springframework.beans.factory.annotation.Autowired;
15.    import org.springframework.jdbc.core.JdbcTemplate;
16.
17.    import com.apress.books.model.Author;
18.    import com.apress.books.model.Book;
19.    import com.apress.books.model.Category;
20.
21.    public class BookDAOImpl implements BookDAO {
22.
23.
24.        DataSource dataSource;
25.
26.
27.        public void setDataSource(DataSource dataSource) {
28.            this.dataSource = dataSource;
29.        }
30.
31.        public List<Book> findAllBooks() {
32.          List<Book> bookList = new ArrayList<>();
33.
34.        String sql = "select * from book inner join author on book.id = author.book_id";
35.
36.        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
37.        bookList = jdbcTemplate.query(sql, new BookRowMapper());
38.          return bookList;
39.         }
40.
41.    }

将 BookDAOImpl 的 findAllBooks()方法与第一章的 findAllBooks()方法进行比较,您将会看到 JDBCTemplate 消除了样板代码,这是使用纯 JDBC 来获得到我们的数据存储的连接并清理资源的结果。

  • 第 36 行:使用传递给它的数据源创建一个 JDBCTemplate。注意,为了便于解释,JDBCTemplate 是以这种方式实例化的。在一个生产就绪的应用中,你应该像任何其他依赖一样注入 JDBCTemplate 作为一个依赖,如清单 5-15 中所解释的。
  • 第 37 行:使用了行映射器实现。接下来解释 BookRowMapper。

清单 5-27 说明了 BookRowMapper 对象查询一行或多行,然后将每一行转换成相应的域对象,而不是检索单个值。

清单 5-27 。书签器对象

 1.    package com.apress.books.dao;
 2.
 3.    import java.sql.ResultSet;
 4.    import java.sql.SQLException;
 5.
 6.    import org.springframework.jdbc.core.RowMapper;
 7.
 8.    import com.apress.books.model.Book;
 9.
10.    public class BookRowMapper implements RowMapper<Book> {
11.
12.         @Override
13.         public Book mapRow(ResultSet resultSet, int line) throws SQLException {
14.          BookExtractor bookExtractor = new BookExtractor();
15.          return bookExtractor.extractData(resultSet);
16.         }
17.
18.        }
  • Line 10 : Spring 的 RowMapper < T >接口(在 org.springframework.jdbc.core 包下)提供了一个简单的方法来执行从 jdbc 结果集到 POJOs 的映射。
  • 第 14 行:使用 BookExtractor 提取数据。

清单 5-28 展示了 BookExtractor 对象。

清单 5-28 。【BookExtractor 对象

 1.    package com.apress.books.dao;
 2.
 3.    import java.sql.ResultSet;
 4.    import java.sql.SQLException;
 5.    import java.util.ArrayList;
 6.    import java.util.List;
 7.
 8.    import org.springframework.dao.DataAccessException;
 9.    import org.springframework.jdbc.core.ResultSetExtractor;
10.
11.    import com.apress.books.model.Author;
12.    import com.apress.books.model.Book;
13.
14.    public class BookExtractor implements ResultSetExtractor<Book> {
15.
16.         public Book extractData(ResultSet resultSet) throws SQLException,
17.           DataAccessException {
18.
19.          Book book = new Book();
20.          Author author = new Author();
21.        List<Author> authorList = new ArrayList<>();
22.
23.          book.setId(resultSet.getLong(1));
24.          book.setCategoryId(resultSet.getLong(2));
25.          book.setBookTitle(resultSet.getString(3));
26.          book.setPublisherName(resultSet.getString(4));
27.          book.setAuthorId(resultSet.getLong(5));
28.          author.setBookId(resultSet.getLong(6));
29.          author.setFirstName(resultSet.getString(7));
30.          author.setLastName(resultSet.getString(8));
31.          authorList.add(author);
32.          book.setAuthors(authorList);
33.
34.          return book;
35.         }
36.
37.        }
  • 第 14 行 : BookExtractor 实现 Spring 提供的 ResultSetExtractor(在 org.springframework.jdbc.core 包下)。RowMapper 仅适用于映射到单个域对象。但是由于我们在第 34 行连接了清单 5-26 中的两个表,我们需要使用 ResultSetExtractor 接口 将数据转换成一个嵌套的域对象。

清单 5-29 展示了这个独立应用的配置文件。

清单 5-29 。配置文件

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <beans fontname">http://www.springframework.org/schema/beans"
 3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context=
           "http://www.springframework.org/schema/context"
 4.        xmlns:aop="http://www.springframework.org/schema/aop"
 5.        xsi:schemaLocation="http://www.springframework.org/schema/beans
 6.            http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
 7.            http://www.springframework.org/schema/context
 8.            http://www.springframework.org/schema/context/spring-context-3.2.xsd
 9.            http://www.springframework.org/schema/aop
10.            http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
11.
12.            <!-- telling container to take care of annotations stuff -->
13.    <context:annotation-config />
14.
15.    <!-- declaring base package -->
16.    <context:component-scan base-package="com.apress.books" />
17.
18.
19.        <bean id="dao" class="com.apress.books.dao.BookDAOImpl" >
20.        <property name="dataSource" ref="dataSource">
21.            </property>
22.        </bean>
23.
24.        <bean id="service" class="com.apress.books.service.BookServiceImpl">
25.            <property name="bookDao" ref="dao">
26.            </property>
27.        </bean>
28.
29.        <bean id="dataSource"
30.      class="org.springframework.jdbc.datasource.DriverManagerDataSource">
31.    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
32.    <property name="url" value="jdbc:mysql://localhost:3306/books" />
33.    <property name="username" value="root" />
34.    <property name="password" value="password" />
35.    </bean>
36.    </beans>
  • 第 20 行:用数据源 ?? 配置 dao

这样,Spring 框架消除了样板代码。现在有了独立的 Java 应用,您可以查询我们使用 Spring 框架构建的新数据访问层。清单 5-30 说明了通过服务层组件 BookService 查询数据访问的独立 Java 应用。

清单 5-30 。单机应用

package com.apress.books.client;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.apress.books.model.Book;
import com.apress.books.service.BookService;

public class BookApp {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        BookService bookService = (BookService)context.getBean("service");
        // List all books
        System.err.println("Listing all Books:");
        List<Book> bookList= bookService.getAllBooks();
        for(Book b: bookList){
            System.out.println(b.getId()+"--"+b.getBookTitle());
        }
    }
}

在本节中,您了解了 Spring 框架的关键目标,该框架旨在通过紧密耦合、横切关注点和样板代码来简化任何企业应用的复杂性。下一节,我们将使用 Spring 框架的 web MVC 模块 实现 Web 层。

用 Spring Web MVC 构建 Web 应用

在第一章中,Spring Web MVC 提供了模型-视图-控制器架构和无数的组件,共同帮助你开发基于 Spring IoC 容器的松散耦合的 Web 应用。

Spring Web MVC 架构

Spring 的 web MVC 框架像许多其他 Web MVC 框架一样,是请求驱动的,围绕一个名为 DispatcherServlet 的中央 Servlet 设计,该 servlet 将请求分派给控制器,并提供其他功能来促进 Web 应用的开发。DispatcherServlet 实现了 Java EE web 层模式之一,称为前端控制器。因此,DispatcherServlet 充当 Spring MVC 框架的前端控制器,每个 web 请求都必须经过它,这样它才能控制和管理整个请求处理过程。Spring Web MVC DispatcherServlet 的请求处理工作流程如图图 5-6 所示。

9781430259831_Fig05-06.jpg

图 5-6 。Spring Web MVC DispatcherServlet 的请求处理工作流

从图 5-6 开始,工作流程的高级概述如下:

  1. 客户端以 HTTP 请求的形式向 web 容器发送请求。
  2. DispatcherServlet 截获请求,找出适当的处理程序映射。
  3. 在如此计算出的处理程序映射的帮助下,DispatcherServlet 将请求分派给适当的控制器。
  4. 控制器处理请求并将模型和视图对象以 model and view 实例的形式返回给 DispatcherServlet。
  5. DispatcherServlet 然后通过查询 ViewResolver 对象解析视图(可以是 JSP、FreeMarker、Velocity 等等)。
  6. 然后,将所选视图呈现回客户端。

DispatcherServlet 是 Spring Web MVC 框架的核心,但是在深入研究 DispatcherServlet 之前,首先必须了解 Web 应用中的 ApplicationContext。如前所述,web 应用有自己专门的 WebApplicationContext,必须在初始化 DispatcherServlet 之前加载。当 Spring Web MVC 应用启动时,在 Web 应用准备好服务请求之前,WebApplicationContext 和 DispatcherServlet 开始工作,如下所述:

  1. servlet 容器初始化 web 应用,然后触发 contextInitialized 事件,该事件由 ContextLoaderListener 侦听。
  2. ContextLoaderListener 创建根 WebApplicationContext。
  3. DispatcherServlet 被初始化,创建自己的 WebApplicationContext 并将其嵌套在根 WebApplicationContext 中。
  4. DispatcherServlet 搜索组件,如 ViewResolvers 和 HandlerMappings。如果找到一个组件,它将被初始化;否则,组件的默认值将被初始化。

在接下来的小节中,您将更详细地了解这些步骤。

文对象

在 web 应用中,使用的 ApplicationContext 称为 WebApplicationContext ,它是一个专用的 ApplicationContext,能够感知 servlet 环境。它是 web 应用中的根 ApplicationContext,必须在 DispatcherServlet 初始化之前加载,以确保 web 应用所需的所有服务(如数据源)都可用。使用 ContextLoaderListener 在 web.xml 文件中配置 WebApplicationContext,如清单 5-31 所示。

清单 5-31 。在 web.xml 中配置 ContextLoaderListener

<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>

默认情况下,ContextLoaderListener 加载存储在 WEB-INF 目录中的应用上下文文件。这个位置可以通过在 web.xml 中定义 contextConfigLocation 上下文参数来覆盖,如清单 5-32 所示。

清单 5-32 。使用 contextConfigLocation 参数的文件位置

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:service-context.xml
classpath:data-access-context.xml
</param-value>
</context-param>

此时,web.xml 中 WebApplicationContext 的配置看起来像是清单 5-33 。

清单 5-33 。带有 ContextLoaderListener 和 contextConfigLocation 的 Web.xml

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:service-context.xml
classpath:data-access-context.xml
</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>

注意如果没有指定名为 contextConfigLocation 的<上下文参数>,默认情况下,ContextLoaderListener 会查找/we b-INF/application context . XML 文件。

现在,您已经知道如何在 web 应用中配置 WebApplicationContext,我们可以继续讨论第二个对象 DispatcherServlet,它是在 web.xml 文件中配置的。

调度员服务网

与任何 servlet 一样,DispatcherServlet 需要在 web.xml 中进行配置,以便能够处理请求。配置和使用 DispatcherServlet 需要满足以下要求:

  1. 您必须指示容器加载 DispatcherServlet 并将其映射到 URL 模式 。
  2. 加载 DispatcherServlet 后,它会创建自己的 org . spring framework . web . context . webapplicationcontext。
  3. DispatcherServlet 然后从这个应用上下文中检测 SpringMVC 组件,如果没有找到,它将使用缺省值。这些 SpringMVC 组件和它们的缺省值将在后面解释。
  4. DispatcherServlet 然后根据请求将任务委派给每个 SpringMVC 组件(或它们的缺省值)。

注意 DispatcherServlet 创建自己的 WebApplicationContext,其中包含特定于 web 的组件,如控制器和 ViewResolver。然后,此 WebApplicationContext 嵌套在根 WebApplicationContext 中,该根 WebApplicationContext 在 DispatcherServlet 初始化之前加载,以确保 DispatcherServlet 的 WebApplicationContext 中的 web 组件可以找到它们的依赖项。

与任何其他 Servlet 一样,DispatcherServlet 是在 web 应用的 web.xml 文件中声明的。您需要通过在同一个 web.xml 文件中使用 URL 映射来映射希望 DispatcherServlet 处理的请求。清单 5-34 展示了 DispatcherServlet 声明和映射。

清单 5-34 。声明和映射 DispatcherServlet

<web-app>
<servlet>
<servlet-name>bookstore</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>bookstore</servlet-name>
<url-pattern>/bookstore/*</url-pattern>
</servlet-mapping>
</web-app>

在 Servlet 3.0 和更新的环境中,您还可以使用 WebApplicationInitializer(Spring MVC 框架提供的一个接口)以编程方式配置 Servlet 容器。清单 5-35 展示了前面的 web.xml 示例的等效程序。

清单 5-35 。相当于清单 5-43 的程序

public class ExampleWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext container) {
        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet());
        registration.setLoadOnStartup(1);
        registration.addMapping("/bookstore/*");
    }
}

默认情况下,DispatcherServlet 会查找一个名为 WEB-INF/ -servlet.xml 的文件,其中的被替换为在 web.xml 的标记中声明的值。DispatcherServlet 使用这个 -servlet.xml 文件来创建 WebApplicationContext。

Spring MVC 组件

如前所述,DispatcherServlet 从它创建的 WebApplicationContext 中搜索 SpringMVC 组件,如果没有找到,就使用默认的。这些 Spring MVC 组件被表示为接口。表 5-6 给出了请求处理工作流中涉及的所有主要组件类型的概述。

表 5-6 。Spring MVC 组件

|

豆类

|

说明

|
| --- | --- |
| 的配置 | 将传入的请求映射到处理程序和拦截程序 |
| 处理器适配器 | 用于扩展 DispatcherServlet 以定制 web 工作流 |
| 处理器异常解析器 | 将异常映射到视图 |
| 视图解析器 | 将逻辑视图名称解析为实际视图 |
| LocaleResolver | 解析客户端用于国际化视图的区域设置 |
| 主题解析器 | 解析个性化布局的主题 |
| 多重解析器 | 解析文件上传的多部分 |
| FlashMapManager | 支持 FlashMap 将属性从一个请求传递到另一个请求 |

Spring DispatcherServlet 使用需要在 WebApplicationContext 中配置的 Spring MVC 组件来处理请求。但是,如果不配置这些组件,Spring Web MVC 会使用默认组件。表 5-7 列出了组件的默认实现。

表 5-7 。DispatcherServlet 的默认组件

|

成分

|

默认实现

|
| --- | --- |
| 多重解析器 | 无违约;需要显式配置 |
| LocaleResolver | AcceptHeaderLocaleResolver |
| 主题解析器 | FixedThemeResolver |
| 的配置 | beannameurlhandlermapping 对映
defaultannotationandhandler 对映 |
| 处理器适配器 | HttpRequestHandlerAdapter
SimpleControllerHandlerAdapter
AnnotationMethodHandlerAdapter |
| 处理器异常解析器 | annotation method handler exception resolve
response status exception resolve
default handler exception resolve |
| RequestToViewNameTranslator | DefaultRequestToViewNameTranslator |
| 视图解析器 | InternalResourceViewResolver |
| FlashMapManager | SessionFlashMapManager |

Spring Web MVC 应用入门

在这一节中,我将带您浏览使用 Spring Tool Suite(一个基于 Eclipse 的 IDE)创建 Hello World Spring MVC 应用的步骤。在构建示例应用时,您将学习 Spring MVC 的基本概念。该应用中使用的工具包括:

  • Spring 框架
  • Spring 工具套件 IDE 3.2.0(基于 Eclipse Juno 4.2.2)
  • v fabric TC Server Developer Edition v 2.8(基于 Apache Tomcat 并针对 Spring 应用进行了优化)

Spring Tool Suite (STS) 是一个基于 Eclipse 的 IDE,由 SpringSource 社区积极开发和维护。STS 提供了 Spring Batch、Spring Integration、Spring Persistence(Hibernate+JPA)、Spring MVC 等项目模板。此外,STS 总是从 Maven 存储库中获得 Spring 工件的最新更新。

您可以选择以下三种方式下载并安装 STS:

  • 从安装程序下载并安装 STS。
  • 通过 Eclipse 更新安装 STS。
  • 下载并解压 zip 存档文件。

在您自己的工作空间中启动 STS。从主菜单中选择文件image新建image Spring 模板项目(参见图 5-7 )。

9781430259831_Fig05-07.jpg

图 5-7 。选择 SpringTemplate 项目

在新建模板项目对话框中,选择 Spring MVC 项目(参见图 5-8 )。

9781430259831_Fig05-08.jpg

图 5-8 。选择 SpringMVC 项目

点击下一步,需要下载模板的更新,如图图 5-9 所示(第一次使用该模板或有更新时)。

9781430259831_Fig05-09.jpg

图 5-9 。下载更新

单击 Yes 下载更新,这将打开新的 Spring MVC 项目对话框。

在图 5-10 所示的窗口中输入以下信息:

  • 项目名称 : helloworld
  • 顶层包:com . a press . hello world

9781430259831_Fig05-10.jpg

图 5-10 。新建 Spring MVC 项目对话框

单击 Finish,STS 将创建一个基于 Spring MVC 的项目,其中包含一些控制器、视图和配置的默认值。我们还没有写任何代码,但是应用已经准备好部署和运行了。

在 Servers 视图中右键单击并选择 New image Server。

在“新建服务器”对话框中,选择 VMware image VMware vFabric tc 服务器...,如图图 5-11 所示。

9781430259831_Fig05-11.jpg

图 5-11 。定义新服务器

单击下一步。在下一个屏幕上,保持选择“创建新实例”选项(见图 5-12 )。

9781430259831_Fig05-12.jpg

图 5-12 。创建新实例

单击下一步。在下一个屏幕上,键入 tcServer 作为新实例的名称,并选择 base 作为模板(参见图 5-13 )。

9781430259831_Fig05-13.jpg

图 5-13 。指定实例参数

添加 helloworld 并点击完成以完成服务器设置(参见图 5-14 )。现在部署 helloworld 应用。

9781430259831_Fig05-14.jpg

图 5-14 。在服务器上配置资源

如果我们在服务器名称下看到应用,那么它就部署在服务器上,如图图 5-15 所示。

9781430259831_Fig05-15.jpg

图 5-15 。部署的应用

启动服务器,使用 URLlocalhost:8080/hello world运行应用(参见图 5-16 )。

9781430259831_Fig05-16.jpg

图 5-16 。运行应用

现在让我们探索一下 Spring MVC 项目模板创建了什么。展开项目浏览器视图中的分支,查看项目的结构,如图 5-17 所示。

9781430259831_Fig05-17.jpg

图 5-17 。Hello World 应用的目录结构

我们将仔细检查图 5-17 中所示的每个组件。图 5-18 说明了生成的 web.xml 文件的内容。

9781430259831_Fig05-18.jpg

图 5-18 。生成的 web.xml

这是基于 Spring MVC 的应用的典型配置,声明如下:

  • Spring's ContextLoaderListener
  • 春天的调度员服务网
  • Spring 配置文件 root-context.xml
  • 弹簧配置文件 servlet-context.xml
  • Spring 的 DispatcherServlet 的 URL 映射

我们将看看其中每一个的用法,但在此之前,我们将修改 web.xml。在 Spring MVC 模板项目中,它生成的 web.xml 文件支持 Servlet 2.5。在本章中,我们将使用 Servlet 3.0(STS 附带的 tcServer 构建在 Apache Tomcat 7 之上,后者已经支持 Servlet 3.0),因此我们也需要将 XML 头从 2.5 更改为 3.0。清单 5-36 显示了修改后的<网络应用>标签。

清单 5-36 。Spring MVC 的 Web 部署描述

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <web-app fontname">http://java.sun.com/xml/ns/javaee" xmlns:xsi=
       "http://www.w3.org/2001/XMLSchema-instance"
 3.        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
           http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
 4.        version="3.0">
 5.
 6.        <!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
 7.        <context-param>
 8.            <param-name>contextConfigLocation</param-name>
 9.            <param-value>/WEB-INF/spring/root-context.xml</param-value>
10.        </context-param>
11.
12.        <!-- Creates the Spring Container shared by all Servlets and Filters -->
13.        <listener>
14.            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
15.        </listener>
16.
17.        <!-- Processes application requests -->
18.        <servlet>
19.            <servlet-name>appServlet</servlet-name>
20.            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
21.            <init-param>
22.                <param-name>contextConfigLocation</param-name>
23.                <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
24.            </init-param>
25.            <load-on-startup>1</load-on-startup>
26.        </servlet>
27.
28.        <servlet-mapping>
29.            <servlet-name>appServlet</servlet-name>
30.            <url-pattern>/</url-pattern>
31.        </servlet-mapping>
32.
33.    </web-app>
  • 第 2 行到第 4 行:在< web-app >标签中,版本属性和对应的 URL 被修改为 3.0 版本,向 web 容器表明 web 应用将使用 Servlet 3.0。
  • 第 7 行到第 10 行:在< context-param >标签中,提供了 contextConfigLocation 参数,定义了 Spring 的根 WebApplicationContext 配置文件的位置。
  • 第 13 行到第 15 行:定义了一个类 org . spring framework . web . context . context loader listener 的监听器。这是为了让 Spring 加载根 WebApplicationContext。
  • 第 18 到 26 行:定义了一个调度器 servlet(称为 appServlet) 。我们使用模板项目为应用的表示层生成的那个。dispatcher servlet 的 WebApplicationContext 位于/src/main/WEB app/we b-INF/spring/app servlet/servlet-context . XML。

servlet-context.xml 文件由 Spring 的 DispatcherServlet 加载,它接收所有进入应用的请求。清单 5-37 展示了 servlet-context.xml

清单 5-37 。Hello World 应用的 servlet-context.xml

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <beans:beans fontname">http://www.springframework.org/schema/mvc"
 3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4.        xmlns:beans="http://www.springframework.org/schema/beans"
 5.        xmlns:context="http://www.springframework.org/schema/context"
 6.        xsi:schemaLocation="http://www.springframework.org/schema/mvc
 7.        http://www.springframework.org/schema/mvc/spring-mvc.xsd
 8.            http://www.springframework.org/schema/beans
 9.            http://www.springframework.org/schema/beans/spring-beans.xsd
10.            http://www.springframework.org/schema/context
11.            http://www.springframework.org/schema/context/spring-context.xsd">
12.
13.        <!-- DispatcherServlet Context: defines this servlet's request-processing
14.        infrastructure -->
15.
16.        <!-- Enables the Spring MVC @Controller programming model -->
17.        <annotation-driven />
18.
19.        <!-- Handles HTTP GET requests for /resources/** by efficiently serving up
20.        static resources in the ${webappRoot}/resources directory -->
21.        <resources mapping="/resources/**" location="/resources/" />
22.
23.        <!-- Resolves views selected for rendering by @Controllers to .jsp resources
24.        in the /WEB-INF/views directory -->
25.        <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
26.            <beans:property name="prefix" value="/WEB-INF/views/" />
27.            <beans:property name="suffix" value=".jsp" />
28.        </beans:bean>
29.
30.        <context:component-scan   base-package="com.apress.helloworld" />
31.
32.
33.
34.    </beans:beans>
  • 第 17 行 : <注释驱动/ > 告诉框架使用基于注释的方法来扫描包中的文件。因此,我们可以对控制器类使用@Controller 注释,而不是声明 XML 元素。
  • 第 21 行 : <资源映射=.../ > 直接用 HTTP GET 请求映射静态资源。例如,图像、JavaScript 和 CSS 资源不必通过控制器。
  • 第 25 行到第 28 行:这个 bean 声明告诉框架如何根据控制器返回的逻辑视图名,通过给视图名附加前缀和后缀来找到物理 JSP 文件。例如,如果控制器的方法返回 home 作为逻辑视图名,那么框架将在/WEB-INF/views 目录下找到一个物理文件 home.jsp。
  • 第 30 行 : <上下文:组件扫描.../ >告诉框架在使用基于注释的策略时要扫描哪些包。在这里,框架将扫描包 com.apress.helloworld 下的所有类。当应用增长时,您可以为业务 beans、DAOs 、事务等添加更多配置。

现在,我们已经有了检测将处理请求的控制器的基础设施,是时候看看控制器了。

注意在 Spring 2.5 之前,使用了一种基于接口的控制器。从 Spring 3.0 开始,基于接口的控制器被弃用,取而代之的是带注释的类。

清单 5-38 展示了 STS 生成的控制器类 HomeController 的代码。

清单 5-38 。Hello World 应用的 HomeController

 1.    package com.apress.helloworld;
 2.
 3.    import java.text.DateFormat;
 4.    import java.util.Date;
 5.    import java.util.Locale;
 6.
 7.    import org.slf4j.Logger;
 8.    import org.slf4j.LoggerFactory;
 9.    import org.springframework.stereotype.Controller;
10.    import org.springframework.ui.Model;
11.    import org.springframework.web.bind.annotation.RequestMapping;
12.    import org.springframework.web.bind.annotation.RequestMethod;
13.
14.    /**
15.     * Handles requests for the application home page.
16.     */
17.    @Controller
18.    public class HomeController {
19.
20.        private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
21.
22.        /**
23.         * Simply selects the home view to render by returning its name.
24.         */
25.        @RequestMapping(value = "/", method = RequestMethod.GET)
26.        public  String  home(Locale locale, Model model) {
27.            logger.info("Welcome home! The client locale is {}.", locale);
28.
29.            Date date = new Date();
30.            DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG,
               DateFormat.LONG, locale);
31.
32.            String formattedDate = dateFormat.format(date);
33.
34.            model.addAttribute("serverTime", formattedDate );
35.
36.            return "home";
37.        }
38.
39.    }
  • 第 17 行😡 Controller 注释用于指定这个类是一个 Spring 控制器。DispatcherServlet 通过@RequestMapping 注释扫描这种带注释的类,以查找映射的处理程序方法。
  • 第 25 行😡 request mapping 注释指定 home()方法将处理带有 URL /(应用的默认页面)的 GET 请求。
  • 第 26 行到第 37 行:home()方法创建一个字符串对象来保存基于当前地区的当前日期,并将这个对象添加到模型中,并命名为 serverTme 。最后,该方法返回一个名为 home 的视图,它将由 servlet-context.xml 文件中指定的视图解析器进行解析,以找到实际的视图文件。在一个控制器类中,我们可以编写许多方法来处理不同的 URL。

@Controller 和@RequestMapping 以及许多其他注释构成了 Spring MVC 实现的基础。要在 Spring 3.0 和更新版本中定义控制器类,必须用@Controller 注释标记该类。当@Controller 注释的类收到请求时,它会寻找合适的处理方法来处理请求。请求要映射到的每个方法都用@RequestMapping 注释修饰,使该方法成为一个处理程序方法,请求通过处理程序映射映射到该方法。

正如您在清单 5-38 中看到的,HomeController 中的 home()方法返回一个名为 home 的视图,该视图由 servlet-context.xml 中指定的视图解析器解析。现在是时候查看视图了,这是在/WEB-INF/views 目录中生成的 home.jsp 文件。清单 5-39 显示了 home.jsp 的。

清单 5-39 。Hello World 应用的 home.jsp

 1.    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
 2.    <%@ page session="false" %>
 3.    <html>
 4.    <head>
 5.        <title>Home</title>
 6.    </head>
 7.    <body>
 8.    <h1>
 9.        Hello world!
10.    </h1>
11.
12.    <P>  The time on the server is ${serverTime}. </P>
13.    </body>
14.    </html>

清单 5-39 看起来很熟悉。这是一个简单的 JSP 文件,它在第 12 行使用一个 EL 表达式来打印控制器传递的变量 serverTime 的值。

您可能已经注意到,STS 创建了两个 Spring 配置文件 : root-context.xml 和 servlet-context.xml。我们还没有查看 root-context.xml,因为我们的 Hello World 应用不需要这个文件来显示 home.jsp 的内容。该文件默认为空,如图图 5-19 所示。

9781430259831_Fig05-19.jpg

图 5-19 。生成的 root-context.xml

顾名思义,这个文件指定了 Spring 容器的根配置。root-context.xml 文件由 Spring 的 ContextLoaderListener 在应用启动时加载,正如您在上一节中所了解的。

到目前为止,我们已经浏览了由 Spring MVC 项目模板生成的所有文件,因此您应该有足够的能力来更深入地学习,一路构建书店应用。

在书店应用中实现 Spring Web MVC

在本节中,您将学习如何使用 Spring Web MVC 框架开发书店 web 应用。该应用的代码可以从 Apress 网站下载。如前所述,所有传入的请求都流经 DispatcherServlet。因此,像 Java EE 应用中的任何其他 servlet 一样,Java EE 容器需要被通知在启动时通过 web.xml 加载这个 servlet。清单 5-40 展示了书店应用的 web.xml。

清单 5-40 。web.xml

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <web-app fontname">http://java.sun.com/xml/ns/javaee" xmlns:xsi=
       "http://www.w3.org/2001/XMLSchema-instance"
 3.        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
           http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
 4.        version="3.0">
 5.        <!-- Processes application requests -->
 6.        <servlet>
 7.            <servlet-name>bookstore</servlet-name>
 8.            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
 9.            <init-param>
10.                <param-name>contextConfigLocation</param-name>
11.                <param-value>/WEB-INF/spring/bookstore/bookstore-servlet.xml</param-value>
12.            </init-param>
13.            <load-on-startup>1</load-on-startup>
14.        </servlet>
15.
16.        <servlet-mapping>
17.            <servlet-name>bookstore</servlet-name>
18.            <url-pattern>*.html</url-pattern>
19.        </servlet-mapping>
20.
21.        <welcome-file-list>
22.            <welcome-file>/list_book.html</welcome-file>
23.        </welcome-file-list>
24.    </web-app>
  • 第 7 行到第 8 行 : DispatcherServlet 注册为一个名为 bookstore 的 Servlet。
  • 第 10 行:可以在 contextConfigLocation servlet 参数中明确指定 Spring 配置文件,要求 Spring 加载默认< servletname > -servlet.xml 之外的配置

清单 5-41 展示了 bookstore-servlet.xml。

清单 5-41 。书店-servlet.xml

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <beans:beans fontname">http://www.springframework.org/schema/mvc"
 3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans=
           "http://www.springframework.org/schema/beans"
 4.        xmlns:context="http://www.springframework.org/schema/context"
 5.        xsi:schemaLocation="http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd
 6.            http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/
               beans/spring-beans.xsd
 7.            http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/
               context/spring-context.xsd">
 8.
 9.        <!-- DispatcherServlet Context: defines this servlet's request-processing
10.            infrastructure -->
11.
12.        <beans:bean name="/list_book.html"
13.            class="com.apress.bookstore.controller.BookController" />
14.
15.        <!-- Resolves views selected for rendering by @Controllers to .jsp resources
16.            in the /WEB-INF/views directory -->
17.        <beans:bean
18.            class="org.springframework.web.servlet.view.InternalResourceViewResolver">
19.            <beans:property name="prefix" value="/WEB-INF/views/" />
20.            <beans:property name="suffix" value=".jsp" />
21.        </beans:bean>
22.    </beans:beans>
23.

一旦用户使用localhost:8080/book store请求一个图书列表,该请求就命中 servlet 引擎,该引擎将调用路由到部署在 servlet 容器中的 bookstore web 应用。在清单 5-40 中显示的 web.xml 文件提供了服务于请求的欢迎文件。

21.        <welcome-file-list>
22.            <welcome-file>/list_book.html</welcome-file>
23.        </welcome-file-list>

欢迎文件中的 URL 与已经为 DispatcherServlet 注册的 URL 模式相匹配,请求被路由到它。基于 bookstore-servlet.xml 中可用的配置,请求被路由到特定的控制器,如清单 5-41 的第 12 行所示。这里,list_book.html 文件被声明为一个 bean,并映射到 BookController 类。这意味着如果请求一个带有/list_book.html 的 URL,它将要求 BookController 处理这个请求。清单 5-42 展示了基于界面的 BookController。稍后您将看到如何用带注释的控制器替换这个基于接口的控制器。

清单 5-42 。书店应用的基于界面的控制器

 1.    package com.apress.bookstore.controller;
 2.
 3.    import javax.servlet.http.HttpServletRequest;
 4.    import javax.servlet.http.HttpServletResponse;
 5.
 6.    import org.springframework.web.servlet.ModelAndView;
 7.    import org.springframework.web.servlet.mvc.Controller;
 8.
 9.    import com.apress.bookstore.service.BookService;
10.
11.    public class BookController implements Controller{
12.
13.        @Override
14.        public ModelAndView handleRequest(HttpServletRequest arg0,
15.                HttpServletResponse arg1) throws Exception {
16.            BookService bookservice = new BookService();
17.            ModelAndView modelAndView = new ModelAndView("bookList");
18.            modelAndView.addObject("bookList", bookservice.getBookList());
19.            return modelAndView;
20.        }
21.    }

控制器实例化负责返回所需图书数据的 BookService。modeland view(“booklist”)通过将 bookList 传递给 Spring 的视图解析器来调用名为 bookList 的视图,以确定应该将哪个视图返回给用户。在本例中,BookController 返回一个名为 bookList 的 ModelAndView 对象。bookstore-servlet.xml 中的视图解析器片段(来自清单 5-41 )如下所示:

17.        <beans:bean
18.            class="org.springframework.web.servlet.view.InternalResourceViewResolver">
19.            <beans:property name="prefix" value="/WEB-INF/views/" />
20.            <beans:property name="suffix" value=".jsp" />
21.        </beans:bean>

根据定义,视图解析器使用以下机制查找文件:

Prefix + ModelAndView name + suffix, which translates to : /WEB-INF/jsp/bookList.jsp

modeland view . add object(" bookList ",bookService.getBookList())将 getBookList()返回的图书数据添加到名为 bookList 的模型中,该模型由视图格式化并呈现。

最后,servlet 引擎通过指定的 JSP 呈现响应,如清单 5-43 所示。

清单 5-43 。视角

 1.    <%@page contentType="text/html" pageEncoding="UTF-8"%>
 2.    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 3.    <!DOCTYPE html>
 4.    <html>
 5.    <head>
 6.    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 7.    <title>Your Book store</title>
 8.    </head>
 9.    <body>
10.        <h1>Books List</h1>
11.        <table border="1">
12.            <tr>
13.                <th align="left">Author</th>
14.                <th align="left">Book Title</th>
15.            </tr>
16.            <c:forEach items="${bookList}" var="book">
17.                <tr>
18.                    <td>${book.author.authorName}</td>
19.
20.                    <td>${book.bookTitle}</td>
21.                </tr>
22.            </c:forEach>
23.        </table>
24.    </body>
25.    </html>

图 5-20 展示了书店应用的目录结构。

9781430259831_Fig05-20.jpg

图 5-20 。书店应用的目录结构

让我们将清单 5-42 中基于界面的控制器替换为基于注释的控制器。清单 5-44 展示了基于注释的 BookController。

清单 5-44 。基于注释的图书管理员

1.    package com.apress.bookstore.controller;
2.    import com.apress.bookstore.service.BookService;
3.    import org.springframework.stereotype.Controller;
4.    import org.springframework.web.bind.annotation.RequestMapping;
5.    import org.springframework.web.bind.annotation.RequestMethod;
6.    import org.springframework.web.servlet.ModelAndView;
7.
8.    @Controller
9.    @RequestMapping("/list_book.html")
10.    public class BookController {
11.        @RequestMapping(method = RequestMethod.GET)
12.        public ModelAndView bookListController() {
13.            BookService bookManager = new BookService();
14.            ModelAndView modelAndView = new ModelAndView("bookList");
15.            modelAndView.addObject("bookList", bookManager.getBookList());
16.            return modelAndView;
17.        }
18.    }
  • 在基于注释的应用中,表单控制器是用@Controller 创建的。@Controller 表示特定的类充当控制器的角色。@Controller 还允许自动检测,这与 Spring 对检测类路径中的组件类和为它们自动注册 bean 定义的一般支持相一致。在这个例子中,@Controller 注释表明 BookListControler 类是一个控制器类。
  • 第 9 行 : @RequestMapping 用于将/list_book.html 这样的 URL 映射到整个类或特定的处理程序方法上。类级别的@RequestMapping 表示该控制器上的所有处理方法都是相对于/list_book.html 路径的。
  • 第 13 行 : @RequestMapping 在方法层表示该方法只接受 GET 请求;换句话说,一个/list_book.html 的 HTTP GET 涉及 bookListController()。

清单 5-45 展示了修改后的 bookstore-servlet.xml 来发现基于注释的 BookController。

清单 5-45 。bookstore-servlet.xml 支持基于注释的控制器

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <beans:beans fontname">http://www.springframework.org/schema/mvc"
 3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans=
           "http://www.springframework.org/schema/beans"
 4.        xmlns:context="http://www.springframework.org/schema/context"
 5.        xsi:schemaLocation="http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd
 6.            http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/
               beans/spring-beans.xsd
 7.            http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/
               context/spring-context.xsd">
 8.
 9.        <!-- DispatcherServlet Context: defines this servlet's request-processing
10.            infrastructure -->
11.
12.        <context:component-scan base-package="com.apress.bookStore.controller" />
13.        <beans:bean name="/list_book.html"
14.            class="com.apress.bookstore.controller.BookController" />
15.
16.
17.        <!-- Resolves views selected for rendering by @Controllers to .jsp resources
18.            in the /WEB-INF/views directory -->
19.        <beans:bean
20.            class="org.springframework.web.servlet.view.InternalResourceViewResolver">
21.            <beans:property name="prefix" value="/WEB-INF/views/" />
22.            <beans:property name="suffix" value=".jsp" />
23.        </beans:bean>
24.    </beans:beans>
  • 第 12 行 : <上下文:dispatcher servlet 的组件扫描>将@Controller 注释的类注册为 beans。BookListController 类被自动发现并注册为 bean。

使用注释处理表单

Spring Web MVC 中的注释驱动配置极大地简化了表单处理。Spring 通过数据绑定机制,从提交的表单中自动填充 Java 对象,并支持验证和错误报告,从而消除了对传统表单处理的需求。清单 5-46 演示了如何使用一个表单,然后处理用户输入的数据。图 5-21 说明了添加到书店目录结构中的新文件。

9781430259831_Fig05-21.jpg

图 5-21 。用于表单处理的应用中的新文件

添加了一个新的控制器 AddBookController ,它负责所有使用注释的表单处理。清单 5-46 展示了 AddBookController。

清单 5-46 。为表单处理添加 BookController

 1.    package com.apress.bookstore.controller;
 2.
 3.    import java.util.List;
 4.
 5.    import org.springframework.stereotype.Controller;
 6.    import org.springframework.ui.ModelMap;
 7.    import org.springframework.validation.BindingResult;
 8.    import org.springframework.web.bind.WebDataBinder;
 9.    import org.springframework.web.bind.annotation.InitBinder;
10.    import org.springframework.web.bind.annotation.ModelAttribute;
11.    import org.springframework.web.bind.annotation.RequestMapping;
12.    import org.springframework.web.bind.annotation.RequestMethod;
13.    import org.springframework.web.bind.support.SessionStatus;
14.    import org.springframework.web.context.request.WebRequest;
15.
16.    import com.apress.bookstore.model.Author;
17.    import com.apress.bookstore.model.Book;
18.    import com.apress.bookstore.service.AuthorService;
19.    import com.apress.bookstore.service.BookService;
20.
21.    @Controller
22.    @RequestMapping("/addBook.html")
23.    public class AddBookController {
24.        @RequestMapping(value="/addBook.html", method = RequestMethod.GET)
25.        public String initForm(ModelMap model) {
26.        Book book = new Book();
27.        book.setBookTitle("Add  Book :");
28.            model.addAttribute("book", book);
29.        return "addBook";
30.        }
31.
32.        @InitBinder
33.        public void initBinder(WebDataBinder binder, WebRequest request) {
34.        binder.setDisallowedFields(new String[] {"author"});
35.            Book book = (Book)binder.getTarget();
36.        AuthorService authorService = new AuthorService();
37.        Long authorId = null;
38.        try {
39.                authorId = Long.parseLong(request.getParameter("author"));
40.            } catch (Exception e) {}
41.            if (authorId != null) {
42.                Author author = authorService.getAuthorById(authorId);
43.                book.setAuthor(author);
44.            }
45.        }
46.
47.        @ModelAttribute("authorList")
48.        public List<Author> populateAuthorList() {
49.        AuthorService authorService = new AuthorService();
50.        return authorService.getAuthorList();
51.        }
52.
53.        @RequestMapping(method = RequestMethod.POST)
54.        public String processSubmit(@ModelAttribute("book") Book book, BindingResult result,
           SessionStatus status) {
55.            BookService bookService = new BookService();
56.            bookService.createBook(book);
57.        return "redirect:/list_book.html";
58.        }
59.    }
  • 第 22 行:addbook controller 类用@RequestMapping("/addBook.html ")进行了注释,这意味着这个类中的所有方法都会处理对 URL"/ addBook.html "的请求。

  • 第 24 行:绑定的初始化是通过用@ request mapping(method = request method)注释方法名来完成的。获取)。

  • 第 25 行 : initForm()处理 GET 请求类型并显示添加新书表单。

  • 第 28 行 : initForm()也向模型映射添加了一个新实例,这样这个新实例就可以与表单相关联了。

  • 第 32 行:通过用@InitBinder 标注方法名来定义绑定。

  • 用@InitBinder 注释控制器方法允许直接在控制器类中配置 web 数据绑定。@InitBinder 标识初始化 WebDataBinder 的方法,该 WebDataBinder 用于填充命令并形成带注释的处理程序方法的对象参数。这样的 init-binder 方法支持@RequestMapping 支持的所有参数,除了命令/表单对象和相应的验证结果对象。被声明的 Init-binder 方法不能有返回值。因此,它们通常被宣布为无效。

  • 第 33 行:典型的参数包括与 WebRequest 或 java.util.Locale 结合的 WebDataBinder,允许代码注册特定于上下文的编辑器。

  • 数据绑定是使用 WebDataBinder 类配置的。WebDataBinder 是一个特殊的 DataBinder,用于从 web 请求参数到 JavaBean 对象的数据绑定。

  • Spring 将这个类的一个实例注入到任何用@InitBinder 注释的控制器方法中。然后,该对象用于定义控制器的数据绑定规则。

  • WebRequest 允许通用请求参数访问以及请求/会话属性访问,而无需绑定到本地 Servlet API。

  • 第 34 行 : setDisallowedFields()注册不允许绑定的字段。

  • 第 47 行:通过用@ModelAttribute 注释方法名,引用数据被放入模型中,以便表单视图可以访问它。

    • 当@ModelAttribute 放在方法参数上时,它将模型属性映射到特定的带注释的方法参数。这就是控制器如何获得对保存表单中输入的数据的对象的引用。
    • @ModelAttribute 注释通知 Spring MVC 框架 authorList 实例应该被分配为 Author 类的实例,并且应该被传递以填充 AuthorList()。
  • 第 53 行:表单提交通过用@ request mapping(method = request method)注释方法名来处理。贴)。

  • 第 54 行 : processSubmit()接受 POST 请求;也就是说,针对/new_book.html 的 HTTP POST 调用 processSubmit()。processSubmit()处理表单数据。processSubmit()有三个参数:

    • @ model attribute(value = " book ")Book Book:模型属性注释通知 Spring MVC 框架,Book 模型实例应该被赋值为 Book 类的实例,并且应该被传递给 processSubmit()方法。
    • BindingResult 结果:Spring 在 Book 类的创建过程中确定错误(如果有的话)。如果发现错误,其描述将作为 BindingResult 实例传递给该方法。
    • SessionStatus 状态:SessionStatus 是一个状态句柄,用于将表单处理标记为完成。
  • 第 57 行:return 语句中的 redirect:前缀触发 HTTP 重定向回浏览器。当将响应委托给另一个控制器,而不仅仅是呈现视图时,这是必要的。

清单 5-47 展示了修改后的书店应用的服务层,用于表单处理。

清单 5-47 。图书服务

package com.apress.bookstore.service;

import java.util.LinkedList;
import java.util.List;

import com.apress.bookstore.model.Author;
import com.apress.bookstore.model.Book;

public class BookService {

    private static List<Book> bookList;

    static {

        Author author1 = new Author();
        author1.setAuthorId((long) 1);
        author1.setAuthorName("Vishal Layka");
        Book book1 = new Book();
        book1.setBookId((long) 1);
        book1.setBookTitle("Beginning Groovy, Grails and Griffon");
        book1.setAuthor(author1);

        Book book2 = new Book();
        book2.setBookId((long) 2);
        book2.setBookTitle("Modern Java Web Development");
        book2.setAuthor(author1);

        bookList = new LinkedList<Book>();
        bookList.add(book1);
        bookList.add(book2);
    }

    public List<Book> getBookList() {
        return bookList;
    }

     public Book createBook(Book b) {
            Book book = new Book();
            book.setBookId((long)bookList.size() + 1);
            book.setAuthor(b.getAuthor());
            book.setBookTitle(b.getBookTitle());
            bookList.add(book);
            return book;
            }

}

清单 5-48 展示了用于表单处理的书店应用的修改后的 bookList.jsp。

清单 5-48 。bookList.jsp

 1.    <%@page contentType="text/html" pageEncoding="UTF-8"%>
 2.    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 3.    <!DOCTYPE html>
 4.    <html>
 5.    <head>
 6.    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 7.    <title>Your Book store</title>
 8.    </head>
 9.    <body>
10.        <h1>Books List</h1>
11.        <table border="1">
12.            <tr>
13.                <th align="left">Author</th>
14.                <th align="left">Book Title</th>
15.            </tr>
16.            <c:forEach items="${bookList}" var="book">
17.                <tr>
18.                    <td>${book.author.authorName}</td>
19.
20.                    <td>${book.bookTitle}</td>
21.                </tr>
22.            </c:forEach>
23.        </table>
24.        <br/>
25.        <a href="addBook.html">Add books.</a>
26.    </body>
27.    </html>
  • 第 25 行:使用 AddBookController 调用表单控制器,它被映射到清单 5-46 中 AddBookController 的第 22 行和第 24 行。

清单 5-49 展示了用于表单处理的书店应用的新 JSP 页面,当使用Add books 调用 AddBookController 时,将显示该页面。清单 5-48 第 25 行< /a >。

清单 5-49 。addBook.jsp

 1.    <%@page contentType="text/html" pageEncoding="UTF-8"%>
 2.    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
 3.    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
 4.
 5.    <!DOCTYPE html>
 6.    <html>
 7.    <head>
 8.    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 9.    <title>Your Book store</title>
10.    </head>
11.    <body>
12.    <h1>Add  Book</h1>
13.        <form:form method="post" commandName="book">
14.                Author<br />
15.    <form:select path="author">
16.    <form:options items="${authorList}" itemValue="authorId" itemLabel="authorName" />
17.    </form:select>
18.    <br /><br />
19.                Book Name<br />
20.    <form:input path="bookTitle"/><br /><br />
21.    <br />
22.    <input type="submit" value="Submit">
23.        </form:form>
24.    </body>
25.    </html>

清单 5-49 展示了显示的表单。它还展示了 Spring form 标签的用法。

基于注释的验证

下一节演示了如何使用注释来验证用户输入的数据。图 5-22 说明了应用中修改的文件和新增的文件。

9781430259831_Fig05-22.jpg

图 5-22 。目录结构中的修改文件和新文件,用于基于注释的验证

清单 5-50 展示了 BookValidator。

清单 5-50 。BookValidator 中的验证

 1.    package com.apress.bookstore.validator;
 2.
 3.    import org.springframework.validation.Errors;
 4.    import org.springframework.validation.ValidationUtils;
 5.    import org.springframework.validation.Validator;
 6.
 7.    import com.apress.bookstore.model.Book;
 8.
 9.    public class BookValidator implements Validator {
10.        @Override
11.        public boolean supports(Class clazz) {
12.            return Book.class.equals(clazz);
13.        }
14.
15.        @Override
16.        public void validate(Object obj, Errors errors) {
17.            Book book = (Book) obj;
18.            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "bookTitle", "field.required",
               "Required Field");
19.            if ( ! errors.hasFieldErrors("bookTitle")) {
20.                if (book.getBookTitle().isEmpty())
21.                    errors.rejectValue("Title", "", "Cannot be left empty!");
22.            }
23.        }
24.
25.
26.    }
  • 第 19 到 23 行:应用中的典型验证

名为 AddBookController 的控制器被更新用于验证,如清单 5-51 中的所示。

清单 5-51 。更新 AddBookController

 1.    package com.apress.bookstore.controller;
 2.
 3.    import java.util.List;
 4.
 5.    import org.springframework.beans.factory.annotation.Autowired;
 6.    import org.springframework.stereotype.Controller;
 7.    import org.springframework.ui.ModelMap;
 8.    import org.springframework.validation.BindingResult;
 9.    import org.springframework.web.bind.WebDataBinder;
10.    import org.springframework.web.bind.annotation.InitBinder;
11.    import org.springframework.web.bind.annotation.ModelAttribute;
12.    import org.springframework.web.bind.annotation.RequestMapping;
13.    import org.springframework.web.bind.annotation.RequestMethod;
14.    import org.springframework.web.bind.support.SessionStatus;
15.    import org.springframework.web.context.request.WebRequest;
16.
17.    import com.apress.bookstore.model.Author;
18.    import com.apress.bookstore.model.Book;
19.    import com.apress.bookstore.service.AuthorService;
20.    import com.apress.bookstore.service.BookService;
21.    import com.apress.bookstore.validator.BookValidator;
22.
23.    @Controller
24.    @RequestMapping("/addBook.html")
25.    public class AddBookController {
26.        BookValidator bookValidator;
27.
28.        @Autowired
29.        public AddBookController(BookValidator bookValidator) {
30.            this.bookValidator = bookValidator;
31.        }
32.
33.        @RequestMapping(value="/addBook.html", method = RequestMethod.GET)
34.        public String initForm(ModelMap model) {
35.        Book book = new Book();
36.        book.setBookTitle("Add  Book :");
37.            model.addAttribute("book", book);
38.        return "addBook";
39.        }
40.
41.        @InitBinder
42.        public void initBinder(WebDataBinder binder, WebRequest request) {
43.        binder.setDisallowedFields(new String[] {"author"});
44.            Book book = (Book)binder.getTarget();
45.        AuthorService authorService = new AuthorService();
46.        Long authorId = null;
47.        try {
48.                authorId = Long.parseLong(request.getParameter("author"));
49.            } catch (Exception e) {}
50.            if (authorId != null) {
51.                Author author = authorService.getAuthorById(authorId);
52.                book.setAuthor(author);
53.            }
54.        }
55.
56.        @ModelAttribute("authorList")
57.        public List<Author> populateAuthorList() {
58.        AuthorService authorService = new AuthorService();
59.        return authorService.getAuthorList();
60.        }
61.
62.        @RequestMapping(method = RequestMethod.POST)
63.        public String processSubmit(@ModelAttribute("book") Book book, BindingResult result,
           SessionStatus status) {
64.            BookService bookService = new BookService();
65.            bookService.createBook(book);
66.            if(result.hasErrors()) {
67.                return "addBook";
68.            } else {
69.        bookService.createBook(book);
70.                return "redirect:/list_book.html";
71.            }
72.
73.        }
74.    }
  • 第 29 行:使用 setter 方法注入 BookValidator 类。
  • 第 63 行:在 processSubmit()中,调用 BookValidator 的 validate()检查图书明细是否是用户输入的。向 validate()传递图书模型和 BindingResult 对象的值以保存错误(如果有)。
  • 第 66 行:检查结果变量是否有错误。如果有错误,应用将显示带有错误消息的相同页面。如果没有错误,也就是说,用户已经输入了所有正确的数据,那么应用将显示图书详细信息列表以及新输入的图书详细信息。

配置验证程序

现在你必须在 bookstore-servlet.xml 中声明 URL addBook.html 的验证器,如清单 5-52 所示。

清单 5-52 。声明 BookValidator

 1.    <?xml version="1.0" encoding="UTF-8"?>
 2.    <beans:beans fontname">http://www.springframework.org/schema/mvc"
 3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans=
           "http://www.springframework.org/schema/beans"
 4.        xmlns:context="http://www.springframework.org/schema/context"
 5.        xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/
           schema/mvc/spring-mvc.xsd
 6.            http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/
               beans/spring-beans.xsd
 7.            http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/
               context/spring-context.xsd">
 8.
 9.        <!-- DispatcherServlet Context: defines this servlet's request-processing
10.            infrastructure -->
11.
12.        <context:component-scan base-package="com.apress.bookstore.controller" />
13.        <beans:bean class="com.apress.bookstore.validator.BookValidator" />
14.
15.        <!-- Resolves views selected for rendering by @Controllers to .jsp resources
16.            in the /WEB-INF/views directory -->
17.        <beans:bean
18.            class="org.springframework.web.servlet.view.InternalResourceViewResolver">
19.            <beans:property name="prefix" value="/WEB-INF/views/" />
20.            <beans:property name="suffix" value=".jsp" />
21.        </beans:bean>
22.    </beans:beans>
  • 第 13 行:定义了 BookValidator 类。容器通过调用其构造函数来创建 BookValidator 类。

摘要

本章首先简要介绍了 Spring 框架,然后展示了如何处理紧耦合、横切关注点和样板代码。本章转换了在第一章中构建的数据访问层,以消除因使用纯 JDBC 而产生的样板代码。然后讨论了 Spring MVC 的架构,包括它的请求处理生命周期。接下来展示了如何使用 Spring Web MVC 开发 Hello World 应用。然后,它开始使用 Spring Web MVC 实现书店应用,随着应用的发展,它引入了 Spring Web MVC 的注释编程模型,并在 Web 应用中处理表单。

1【www.osgi.org/Main/HomePage】??

2【www.cloudfoundry.com/】??

3http://aws.amazon.com/ec2/】??

4http://nosql-database.org/】??

5【www.postgresql.org/】??

6【www.mongodb.org/】??

7http://redis.io/】??

8【www.seamframework.org/】??

9http://code.google.com/p/google-guice/】??

martinfowler . com/bliki/reverse of control . html*

六、将 JSF 2 用于基于组件的 Web 开发

它在盘子里排列得如此美丽——你就知道有人用手指在上面摸过。

—茱莉亚·切尔德

JavaServer Faces (JSF) 是一个用于开发 web 应用的基于组件的框架。使基于组件的框架与众不同的显著特征是创建和分发可重用 UI 组件的能力。一般来说,组件代表一种抽象,一种定义良好的契约,对组件的用户隐藏了实现细节。也就是说,组件的用户不需要知道组件的内部机制就能够使用它。Struts 和 Spring Web MVC 减轻了为 Web 构建复杂用户界面的复杂性,但是这些 Web 框架不是以组件为中心的,因此不适合设计真正可重用的 UI 组件。为此,出现了不同的 web 框架,如 Tapestry 和 Wicket,为 web 应用开发提供了一种以组件为中心的方法。然而,由于缺乏以组件为中心的开发的现有标准,这些 web 框架实现可重用 UI 组件的方式对于一个有经验的 web 开发人员来说似乎是乏味的或有限的。

*JSF 标准化了基于组件的 web 开发,并提供了大量广泛的 UI 组件来降低 web 应用开发的复杂性。JSF 提供开箱即用的可重用 UI 组件,以便应用开发人员可以专注于应用的业务逻辑,而不是努力开发和维护动态和丰富的用户界面。JSF 是 Struts 等一些框架的发展,并受到 Swing 组件模型的启发。JSF 代表并要求一种范式转变,让你从组件而不是请求和响应的角度来考虑问题。它的目标是通过促进和标准化一个生态系统来设计可重用的 UI 组件,从而加快 web 开发。

JSF 的建筑

JSF web 框架使用模型-视图-控制器(MVC)设计模式,就像 Struts 和 SpringMVC 这样基于请求的 web 框架一样。图 6-1 显示了 JSF 框架的高级架构。

9781430259831_Fig06-01.jpg

图 6-1 。JSF 框架的高层架构

图 6-1 展示了 JSF 的几个重要部分,这些部分使其建筑丰富而灵活。该体系结构允许您执行以下操作:

  • 插入任何视图声明语言(VDL ),如 JSP 和 Facelets
  • 在不同的设备上渲染显示,如台式机、平板电脑等
  • 使用组件创建页面

核心控制器

facessservlet 是 MVC 中的控制器,如图图 6-1 所示,它实现了前端控制器模式,拦截 Facelets(视图)和模型之间的每一次交互。FacesServlet 是通过托管 beans、转换器、组件、呈现器和验证器上的注释来配置的,也可以通过 faces-config.xml 描述符文件来配置。

受管 Bean

受管 bean 充当 UI 组件的模型。他们负责以下工作:

  • 将数据与组件同步
  • 处理业务逻辑
  • 处理页面之间的导航

VDL〔??〕

JSF 使用视图声明语言(VDL) 在各种设备上向客户端显示页面,比如台式机、笔记本电脑等等。JavaServer Faces (JSF)的默认 VDL 是 Facelets ,但是 JSF 允许多个 VDL,比如 JSP。

JSF EL(联合王国)

在第五章的 Hello World 应用中,您看到了如何使用 EL 表达式 和分隔符#{和},访问受管 bean 属性和调用受管 bean 操作。JSF 1.0 和 1.1(以及后来的 JSP 版本 1.2 和 2.0)中使用的 EL 是 JSP 标准标记库 (JSTL)的一部分的 EL 的扩展,如第三章中的所述。JSF EL 和 JSP EL 的区别在于评估。在 JSP 中,正如你在第三章中看到的,页面中出现的任何${}表达式都会在页面呈现期间立即被求值。这样的表达式被称为立即表达式

JSF 允许表达式在页面呈现期间和页面再次回发时都可用。这种类型的表达式在 JSF 被称为延迟表达式 ,用分隔符#{}表示。

JSF 标签库

标准的 JSF 库由页面需要访问的四个部分组成,以便使用 JSF 组件。

  • HTML 组件库:定义了表示普通 HTML 用户界面组件的元素。标准的 HTML 库可以在 Facelets 和 JSP 中作为标签库访问,标签库的 URI 是 http://java.sun.com/jsf/html 的 ??,默认前缀是 h
  • JSF 核心库:标准核心库与 f:命名空间相关联,为验证和转换提供通用的应用开发工具。
  • Facelets 库:标准的 Facelets 模板库可以在 Facelets 中作为标签库访问,其 URI 为java.sun.com/jsf/facelets,默认前缀为 ui。
  • 复合库:标准复合组件库可以在 Facelets 中作为标签库访问,其 URI 为java.sun.com/jsf/composite,默认前缀为 Composite。

标准的 JSF 组件库是规范的一部分,也是任何标准 JSF 实现的一部分,比如参考实现或 MyFaces 实现。接下来的部分展示了如何下载和安装 JSF 实现,即 Mojarra,并将其集成到 web 应用中。

UI 组件

JSF 在标准 UI 组件框架中提供了丰富的组件模型,如图 6-2 所示。JSF 组件模型包括以下:

  • 一种呈现模型,它定义了组件可以呈现的各种形式,例如用于桌面应用设备和移动应用设备
  • 定义如何处理组件事件的事件和事件监听器模型
  • 定义将数据转换器注册到组件以进行数据转换的方法的转换模型
  • 一个验证模型,它定义了为服务器端验证向组件注册验证器的方法

9781430259831_Fig06-02.jpg

图 6-2 。JSF 组件模型

UI 组件模型是 JSF 的核心;它允许您从标准的、现成的 UI 组件集合中开发 web 应用的视图。这些 UI 组件负责行为,并根据您想要使用的 UI 组件类型,通过包含 JSF 提供的四个标记库,在 JSF 页面中使用。

渲染器

渲染器负责显示组件,换句话说,将标记呈现给客户端,并将用户的输入转换为组件的值。JSF 支持两种显示组件的编程模型。

  • 直接渲染器模型:当使用直接模型时,组件从视图解码并编码到视图。解码和编码过程将在下一节解释。
  • 委托渲染器模型:使用委托模型时,解码和编码委托给渲染器。

转换器和验证器

JSF 提供了现成的转换器,可以将其 UI 组件的数据转换为托管 bean 中使用的对象,反之亦然。例如,它们将组件的日期值与来自 HTML 标记的字符串值相互转换。

JSF 还提供开箱即用的验证器来验证其 UI 组件,以确保用户输入的值是有效的。例如,这些标签可以验证一个长整型范围或一个字符串的长度。

事件和事件侦听器

当用户单击 JSF 页面上的按钮或链接时,JSF UI 组件会触发一个事件。为了处理这样的事件,在受管 bean 上注册了一个事件监听器。UI 组件调用特定事件的事件侦听器上的事件通知。

正如您所看到的,JSF 页面由组件树组成。这个组件树由 JSF 请求处理生命周期在幕后管理。为了理解 JSF 请求处理生命周期,首先您将创建一个 Hello World web 应用,然后通过这个应用您将了解 JSF 生命周期如何在幕后工作。

JSF 入门

在本节中,您将使用支持 JSF2 的 Eclipse 3.8 或更新版本创建一个简单的 Hello World JSF web 应用。 x 。通过选择文件image新建image项目image web image动态 Web 项目,创建如图图 6-3 所示的动态 Web 项目。在目标运行时中指定 Apache Tomcat v7.0,在配置中选择 JavaServer Faces 项目,然后单击 Next。

9781430259831_Fig06-03.jpg

图 6-3 。创建一个 JSF 项目

配置用于构建 Java 应用的项目,如图 6-4 所示,然后单击 Next。

9781430259831_Fig06-04.jpg

图 6-4 。配置动态 web 项目

配置网络模块设置,如图 6-5 所示,并点击下一步。

9781430259831_Fig06-05.jpg

图 6-5 。配置网络模块设置

你需要选择 JSF 实现库,如图图 6-6 所示。您可以通过单击“下载”来下载库。

9781430259831_Fig06-06.jpg

图 6-6 。下载 JSF 实现库

如图图 6-7 所示,MyFaces 和 Mojarra 被列为 JSF 2.0 的两个开源参考实现。

9781430259831_Fig06-07.jpg

图 6-7 。JSF 实现库

选择 Mojarra,点击 Next,接受许可条款,如图图 6-8 所示。

9781430259831_Fig06-08.jpg

图 6-8 。接受许可条款

单击完成。Mojarra 将被列为选中的实现库,如图图 6-9 所示。

9781430259831_Fig06-09.jpg

图 6-9 。增加 JSF 能力

单击完成。项目创建完成,如图图 6-10 所示。

9781430259831_Fig06-10.jpg

图 6-10 。创建的 JSF 项目的目录结构

您将在项目中创建以下文件:

  • 托管豆:HelloBean.java。
  • form.xhtml :这是一个视图文件,包含 JSF 核心标签和延迟 EL。当应用运行时,form.xhtml 文件看起来像图 6-11 。该屏幕提供了一个输入字段和一个提交按钮。

9781430259831_Fig06-11.jpg

图 6-11 。输入姓名的表单

  • hello.xhtml :当用户在 form.xhtml 中输入姓名并点击提交按钮时,会被问候姓名。hello.xhtml 显示 form.xhtml 中输入的用户名以及问候语,如图图 6-12 所示。

9781430259831_Fig06-12.jpg

图 6-12 。你好屏幕

清单 6-1 展示了 form.xhtml 文件的代码。

清单 6-1 。表单. xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <!DOCTYPE html >
3.    <html fontname">http://www.w3.org/1999/xhtml"
4.    xmlns:h="http://java.sun.com/jsf/html">
5.
6.    <h:head>
7.         <title>First JSF app</title>
8.    </h:head>
9.    <h:body>
10.        <h3>Enter your name:</h3>
11.
12.        <h:form>
13.            <h:inputText value="#{helloBean.name}"></h:inputText>
14.            <h:commandButton value="Submit" action="hello"></h:commandButton>
15.        </h:form>
16.    </h:body>
17.    </html>
  • 第 4 行:xmlns 属性声明了 JSF 名称空间。
  • 第 6、9、12、13、14 行:有些标签有前缀,比如 h:head,h:inputText。这些是 JSF 的标签。h:inputText 和 h:commandButton 标签对应于图 6-11 中的文本字段和提交按钮。
  • 第 13 行:输入字段链接到对象属性。例如,attribute value = " # { hello bean . name } "告诉 JSF 实现将文本字段与用户对象的 name 属性链接起来。
  • 第 14 行:第{...}分隔符将 JSF 表达式语言中的表达式括起来。
  • 第 14 行:当您输入名称并单击 Submit 按钮时,会显示 hello.xhtml 文件,这是在 h:commandButton 标记的 action 属性中指定的。

清单 6-2 展示了 hello.xhtml 文件的代码。

清单 6-2 。hello.xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <!DOCTYPE html>
3.    <html fontname">http://www.w3.org/1999/xhtml"
4.        xmlns:h="http://java.sun.com/jsf/html">
5.
6.    <h:head>
7.        <title>Hello world</title>
8.    </h:head>
9.    <h:body>
10.        <h2>Hello  #{helloBean.name}</h2>
11.    </h:body>
12.    </html>
  • 第 10 行:当页面提交后,JSF 会找到 helloBean 并通过 setName()方法设置提交的 name 值。当显示 hello.xhtml 时,JSF 将找到 helloBean 并通过其 getName()方法显示 name 属性值。

清单 6-3 展示了 helloBean。

清单 6-3 。受管 Bean

1.    package com.apress.jsf.helloworld;
2.    import javax.faces.bean.ManagedBean;
3.    import javax.faces.bean.SessionScoped;
4.    import java.io.Serializable;
5.
6.    @ManagedBean
7.    @SessionScoped
8.    public class HelloBean implements Serializable {
9.
10.        private static final long serialVersionUID = 1L;
11.
12.        private String name;
13.
14.        public String getName() {
15.            return name;
16.        }
17.        public void setName(String name) {
18.            this.name = name;
19.        }
20.    }
  • 第 6 行:托管 bean 是从 JSF 页面访问的 Java bean。@ManagedBean 注释指定了在 JSF 页面中引用该类的对象的名称。
  • 第 7 行:受管 bean 必须有名称和作用域。会话范围表示 bean 对象对跨多个页面的一个用户可用。

注意命名一个 bean 有两种注释。@Named 是 Java EE 6 和更新的应用服务器的最佳选择。

像 Struts 和 Spring MVC web 应用一样,当您在应用服务器内部部署 JSF web 应用时,您需要提供一个名为 web.xml 的部署描述符文件。为了简洁起见,web-app 声明和 welcome-file-list 中的文件列表没有完全显示出来。

清单 6-4 。web.xml

1.    <web-app  ..... >
2.    <display-name>helloWorldJSF</display-name>
3.    <welcome-file-list>
4.        ...
5.    </welcome-file-list>
6.    <servlet>
7.    <servlet-name>Faces Servlet</servlet-name>
8.    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
9.    <load-on-startup>1</load-on-startup>
10.    </servlet>
11.    <servlet-mapping>
12.    <servlet-name>Faces Servlet</servlet-name>
13.    <url-pattern>/faces/*</url-pattern>
14.    </servlet-mapping>
15.    <servlet-mapping>
16.    <servlet-name>Faces Servlet</servlet-name>
17.    <url-pattern>*.faces</url-pattern>
18.    </servlet-mapping>
19.    <context-param>
20.    <description>State saving method: 'client' or 'server' (=default). See JSF Specification 2.5.2</description>
21.    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
22.    <param-value>client</param-value>
23.    </context-param>
24.    <context-param>
25.    <param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name>
26.    <param-value>resources.application</param-value>
27.    </context-param>
28.    <listener>
29.    <listener-class>com.sun.faces.config.ConfigureListener</listener-class>
30.    </listener>
31.    </web-app>
  • 第 11 到 14 行:servlet-mapping 元素确保所有带有前缀/faces 的 URL 都由 FacesServlet 处理。
  • 第 15 到 18 行:servlet-mapping 元素确保所有以 faces 扩展名结尾的 URL 都由 FacesServlet 处理。

图 6-13 显示了 Hello World web 应用的目录结构。

9781430259831_Fig06-13.jpg

图 6-13 。Hello World web 应用的目录结构

JSF 应用的生命周期

与 Struts 和 Spring Web MVC 不同,JSF 生命周期在定义明确的阶段中执行以下请求处理任务,在此期间执行特定的任务:

  • 检查输入数据是否有效
  • 触发应用逻辑以满足请求
  • 将字段属性直接绑定到模型的属性,并在表单提交时更新它们的值
  • 将响应呈现给客户端

以下是 JSF 应用生命周期的六个阶段,如图图 6-14 所示:

  • 恢复视图阶段
  • 应用请求值阶段
  • 流程验证阶段
  • 更新模型阶段
  • 调用应用阶段
  • 渲染响应阶段

9781430259831_Fig06-14.jpg

图 6-14 。JSF 生命周期的各个阶段

阶段 1:恢复视图

一旦点击链接或按钮,JSF 收到请求,JSF 就开始恢复视图阶段。在此阶段,JSF 执行以下操作:

  • 从页面构建组件树。此组件树包含与页面所有组件相关的信息。如果页面是第一次被请求,JSF 会创建一个空视图。
  • 将事件处理程序和验证程序连接到 UI 组件。
  • 将视图保存在 FacesContext 实例中。

阶段 2:应用请求值

创建/恢复组件树后,JSF 运行时运行组件树中每个组件的 decode 方法,从请求参数中提取值。如果需要,在执行数据转换之后,由 decode 方法提取的值存储在组件中。如果转换失败,将生成一条错误消息并在 FacesContext 上排队。

阶段 3:流程验证

在这个阶段,JSF 运行时处理在恢复视图阶段在组件树上注册的验证器。如果存在验证错误,JSF 会向 FacesContext 实例添加一条错误消息,跳过第四和第五阶段,进入渲染响应阶段,并显示错误消息。

阶段 4:更新模型

如果在流程验证阶段没有验证错误,JSF 运行时将使用 UI 组件的新值更新绑定到 UI 组件的受管 bean 的属性。如有必要,转换也在此阶段执行。

阶段 5:调用应用

在这个阶段,JSF 运行时通过执行相应的事件侦听器来处理应用事件。当用户提交表单时,JSF facessservlet 生成一个应用事件,该事件返回一个传递给导航处理程序的结果字符串。导航处理程序查找下一个要呈现的页面。

阶段 6:呈现响应

在这个阶段,组件树中的每个组件都会呈现自己,并且保存响应的状态,以便 FacesServlet 可以在 Restore View 阶段访问它,如果对同一个页面发出后续请求,就会发生这种情况。

让我们从请求处理生命周期的角度来看看 Hello World 应用的幕后。

  1. 浏览器首先连接localhost:8080/hello world JSF/form . faces

  2. The JSF implementation initializes the JSF code and reads the form.xhtml page. That page contains tags, such as h:form, h:inputText, and h: commandButton. Each tag has an associated tag handler class. When the page is read, the tag handler class associated with each tag is executed, and a component tree is constructed. This is the first phase: Restore View. Since this is the first request and the component tree does not already exist, a new but empty component tree is created instead of restoring the component tree. Figure 6-15 shows the component tree for the code fragment of the form.xhtml file in Listing 6-1.

    <h:form>
    <h:inputText value="#{helloBean.name}"></h:inputText>
    <h:commandButton value="Submit" action="hello"></h:commandButton>
    </h:form>
    

    9781430259831_Fig06-15.jpg

    图 6-15 。form.xhtml 组件树

  3. 现在,JSF 运行时进入第二阶段:应用请求值。UIForm 对象对应于 h:form,UIInput 对象对应于 h:inputText,UICommand 对象对应于 JSF 文件中的 h:commandButton。由于这是对该页面的第一个请求,并且没有可用的请求参数或要处理的事件,没有要更新模型的内容,没有要转换和验证的内容,也没有应用级别的事件,因此 JSF 运行时跳过第二、第三、第四和第五阶段,进入第六阶段:呈现响应。创建的组件树中的每个组件对象都有一个生成 HTML 的渲染器。这个由组件的渲染器生成 HTML 的过程叫做编码。这个编码后的页面会显示在浏览器中。

  4. 用户现在填写表单中的 name 字段,并单击 Submit 按钮。

  5. 浏览器将表单数据发送到 web 服务器,格式化为 POST 请求。JSF 运行时再次进入第一个阶段,还原视图,组件树被还原以反映用户在表单中输入的值。然后 JSF 运行时进入第二阶段,应用请求值。

  6. 在应用请求值阶段,JSF 运行时执行名为解码 的过程,其中组件树中的每个组件解码表单数据,组件存储该值。如果存储时转换失败,则会生成一条错误消息,并在 FacesContext 上排队。

  7. JSF 运行时进入第三阶段:流程验证。在这一点上,JSF 运行时处理在第一阶段向组件树注册的验证器。如果有任何验证错误,比如 Hello World 应用中的 name 字段为空,JSF 会向 FacesContext 实例添加一条错误消息,跳过其他阶段,进入第六个阶段:呈现响应。它还显示错误消息,如“名称字段不能为空”因为在 Hello World 应用中没有验证,所以 JSF 运行时进入第四个阶段:更新模型。

  8. 在更新模型阶段,JSF 运行时用表单上输入的值更新受管 bean,即 helloBean 的属性名。UIInput 组件更新 value 属性中引用的 helloBean 属性名,并使用用户输入的值调用 setter 方法。在此阶段,如有必要,还会通过向组件注册的转换器执行转换。因为在这种情况下不需要转换,所以 JSF 运行时进入第五个阶段,调用应用,或者触发一个动作事件。

  9. 在调用应用阶段,UICommand 组件检查按钮是否被点击。如果是,它会触发一个 action 事件,即 action 属性中引用的 hello 操作,该操作告诉导航处理程序寻找 hello.xhtml 页面,JSF 运行时进入第六个阶段:呈现响应。

  10. facessservlet 创建一个响应组件树,当 JSF 运行时遍历相应的 JSF 标签时,每个组件都会呈现自己。在这个阶段的最后,保存响应的状态,以便 FacesServlet 可以在对同一页面的后续请求的 Restore View 阶段访问它。

受管 Beans

一个托管 bean 是一个常规的 JavaBean 类,在 JSF 注册,由 JSF 框架管理,充当 UI 组件的模型。受管 bean 包含 getter 和 setter 方法、业务逻辑或支持 bean,即包含表单所有值的 bean。受管 beans 负责以下内容:

  • 将服务器端数据与组件树中的组件同步
  • 处理业务逻辑
  • 处理页面之间的导航

该组件通过 EL 与特定的托管 bean 属性或操作相关联。正如您在 Hello World 应用中看到的,您不需要编写任何代码来构造和操作 HelloBean。JSF 运行时构建 beans 并访问它们。可以很容易地在 JSF 配置文件(即 faces-config.xml)中或使用注释注册受管 beans。

清单 6-5 展示了使用 XML 配置注册一个被管理的 bean。

清单 6-5 。使用 XML 注册受管 Bean

<managed-bean>
<managed-bean-name>helloWorld</managed-bean-name>
<managed-bean-class>com..apress.jsf.helloWorld.HelloBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>

清单 6-6 展示了使用注释注册一个受管 bean。

清单 6-6 。使用注释注册受管 Bean

@ManagedBean
@SessionScoped
public class HelloWorld {

}

注意上下文和依赖注入(CDI)bean 比托管 bean 更强大。如果您在 Java EE 应用服务器(比如 Glassfish)中部署应用,那么您应该使用 CDI beans。Java EE 6 和更新的应用服务器自动支持 CDI。CDI Beans 的使用方式与受管 bean 相同,但它们不是用@ManagedBean,而是用@Named 批注声明的,如下所示:

@ Named(" helloBean ")
@ session scoped
公共类 hello bean 实现 Serializable {
...
}

Facelets

Facelets 被创建来代替 JSP 作为 JSF 的视图声明语言;它是专门为 JSF 设计的,提供了模板和可扩展标记库,以避免在 HTML 页面中使用 script let(Java 代码)。Facelets 和 JSP 之间的显著区别在于,Facelets 提供了用纯 HTML 标记编写页面的能力,并提供了服务器端模板。

使用 Facelets 进行模板化

您在第四章中学习了使用 Tiles 框架进行模板化,在那里您看到了模板是如何封装通用布局以供所有页面使用的,现在您理解了模板是如何工作的。Facelets 类似于 Tiles 框架,用于模板化和组合页面。因此,除了 Facelets 为 ui: tag 库中的模板提供的六个标记之外,模板化是非常相同的。

  • ui:构图
  • ui:装饰
  • ui:定义
  • ui:包含
  • 用户界面:插入
  • ui:停止〔??〕

ui:构图

ui:composition 标记用在充当模板客户端的模板客户端文件中,并指示在页面中的特定点,封装内容应该包含在 UIComponent 层次结构中。清单 6-7 说明了 ui:composition 的语法。

清单 6-7 。ui:合成标签

<ui:composition template="optional">

可选属性声明一个模板,应该使用 template 属性将包含的内容应用于该模板。

ui:装饰

ui:decoration 标记和 ui:composition 标记的区别在于,与 ui:composition 不同,ui:decoration 也将周围的内容包含在页面中。清单 6-8 说明了 ui:decorate 的语法。

清单 6-8 。ui:装饰标签

<ui:decorate template="required">

ui:定义

ui:define 标记用于模板客户端文件中的 ui:composition 标记内,以定义将在 ui:insert 标记提供的点插入到合成中的区域。清单 6-9 说明了 ui:define 的语法。

清单 6-9 。用户界面:定义标签

<ui:define name="required">

用户界面:插入

ui:insert 标记在模板文件中用来指示 ui:define 在模板客户机中应该插入的位置。清单 6-10 说明了 ui:insert 的语法。

清单 6-10 。用户界面:插入标签

<ui:insert name="optional">

如果没有指定名称,那么 ui:insert 标记体中的内容将被添加到视图中。

ui:包含

ui:include 可以在模板文件或模板客户端文件中使用。清单 6-11 展示了 ui:include 的语法。

清单 6-11 。ui:包含标签

<ui:include src="required">

ui:停止〔??〕

ui:param 标记用在 ui:include 标记中,为页面的参数化包含定义名称-值对。清单 6-12 展示了 ui:param 的语法。

清单 6-12 。ui:停止标记

<ui:param name="required" value="required">

接下来,您将使用 Facelets 实现模板化。创建一个类似于前面部分中 Hello World 项目的 JSF 项目。在这个应用中,您将创建将在书店应用中使用的标题和侧栏模板。图 6-16 说明了应用的目录结构。

9781430259831_Fig06-16.jpg

图 6-16 。JSFTemplate 应用的目录结构

清单 6-13 展示了模板文件 common.xhtml.

清单 6-13 。common.xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <html fontname">http://www.w3.org/1999/xhtml"
3.        xmlns:h="http://java.sun.com/jsf/html"
4.        xmlns:ui="http://java.sun.com/jsf/facelets">
5.    <h:head>
6.        <link rel="stylesheet" href="css/bookstore.css" type="text/css" />
7.    </h:head>
8.    <h:body>
9.        <div id="centered">
10.            <div>
11.                <ui:insert name="header">
12.                    <ui:include src="/templates/header.xhtml" />
13.                </ui:insert>
14.            </div>
15.            <div>
16.                <ui:insert name="sideBar">
17.                    <ui:include src="/templates/sideBar.xhtml" />
18.                </ui:insert>
19.            </div>
20.            <div>
21.                <ui:insert name="content">
22.                    <ui:include src="/templates/contents.xhtml" />
23.                </ui:insert>
24.            </div>
25.        </div>
26.    </h:body>
27.    </html>
  • 第 3 行:声明 HTML 库的名称空间
  • 第 4 行:声明 Facelet 库的名称空间
  • 第 5 行到第 7 行:显示 h:head 标签的用法,而不是使用标记< head/ >
  • 第 8 行:显示了 h:body 标签的用法,而不是使用标记< body/ >
  • 第 11 行到第 13 行:展示了使用 u:insert 标签来模板化添加到视图中的< ui:insert >标签的主体内容
  • 第 12 行:显示了包含 header.xhtml 的 ui:include 标签的用法
  • 第 17 行:展示了包含 sideBar.xhtml 的 ui:include 标签的用法
  • 第 22 行:显示了包含 contents.xhtml 的 ui:include 标签的用法

清单 6-14 展示了模板客户端 header.xhtml.

清单 6-14 。header.xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <html fontname">http://www.w3.org/1999/xhtml"
3.        xmlns:h="http://java.sun.com/jsf/html"
4.        xmlns:ui="http://java.sun.com/jsf/facelets">
5.    <h:body>
6.        <ui:composition>
7.            <div class="header">
8.                <h2>
9.                <span style="margin-left: 15px; margin-top: 15px;" class="label">BOOK
10.                        <span style="color: white;">STORE</span>
11.                    </span>
12.                </h2>
13.            </div>
14.        </ui:composition>
15.    </h:body>
16.    </html>
  • 第 6 行:ui:composition 标签向 Facelets 系统表明,封闭的子元素将被移植到 UIComponent 层次结构中,其中 header.xhtml 被插入到 common.xhtml 中。

清单 6-15 展示了 sideBar.xhtml.

清单 6-15 。sideBar.xhtml

1.    <div class="leftbar">
2.        <ul id="menu">
3.            <li><div>
4.                    <a class="link1" href=""><span class="label"
5.                        style="margin-left: 15px;">Home</span>
6.                    </a>
7.                </div></li>
8.            <li><div>
9.            <a class="link1" href="listOfBooks.xhtml"><span
10.                style="margin-left: 15px;" class="label">All Books</span></a>
11.                </div></li>
12.            <li><div>
13.            <span class="label" style="margin-left: 15px;">Categories</span>
14.                </div>
15.                <ul>
16.                    <li><a class="label" href=""><span class="label"
17.                            style="margin-left: 30px;"></span></a></li>
18.                </ul></li>
19.            <li><div>
20.                    <span class="label" style="margin-left: 15px;">Contact Us</span>
21.                </div></li>
22.        </ul>
23.    </div>

清单 6-15 是在第二章中使用的同一个侧边栏文件。

清单 6-16 展示了模板客户端内容. xhtml.

清单 6-16 。内容. xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <html fontname">http://www.w3.org/1999/xhtml"
3.        xmlns:ui="http://java.sun.com/jsf/facelets">
4.    <body>
5.        <ui:composition>
6.            <h1>Book Store Home</h1>
7.        </ui:composition>
8.    </body>
9.    </html>
  • 第 5 行:ui:composition 标签向 Facelets 系统表明,应该在 contents.xhtml 插入的地方将封闭的子元素移植到 UIComponent 层次结构中。

清单 6-17 展示了定义 content.xhtm 的 home.xhtml 文件。

清单 6-17 。home.xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <!DOCTYPE html>
3.    <html fontname">http://www.w3.org/1999/xhtml"
4.        xmlns:h="http://java.sun.com/jsf/html"
5.        xmlns:ui="http://java.sun.com/jsf/facelets">
6.    <h:body>
7.        <ui:composition template="templates/common.xhtml">
8.            <ui:define name="content">
9.                <ui:include src="/contents.xhtml" />
10.            </ui:define>
11.        </ui:composition>
12.    </h:body>
13.    </html>
  • 第 7 行:ui:composition 标签声明了模板 common.xhtml,应该使用 template 属性将包含的 content contents.xhtml 应用于该模板。

你现在可以使用 URLlocalhost:8080/JSF template/home . faces运行这个应用,如图图 6-17 所示。

9781430259831_Fig06-17.jpg

图 6-17 。使用模板的标题和侧栏

使用 JSF 2 构建书店应用

在本节中,您将使用 JSF 开发书店应用。在第一章中,您开发了书店应用的数据访问层,并通过使用 Java 开发的独立应用对其进行查询。在第五章中,你重构了独立应用,将其与 Spring 框架集成,以便使用 Spring 的 JDBCTemplate 提供的健壮特性。然后你将 Spring JDBCTemplate 用于 Spring Web MVC web 应用。在本章中,您将在一个基于 JSF 的 web 应用中使用相同的 Spring JDBCTemplate,以利用在第五章中讨论的 Spring 模板的优势。为此,您需要将 JSF 与 Spring 框架集成在一起。在接下来的部分中,您将学习如何将 JSF 与 Spring 集成在一起。然后,您将使用 JSF 开发应用的 web 层。您将通过以下四个步骤开发该应用:

  1. 将 JSF 与 Spring 框架集成。
  2. 通过 Spring JDBCTemplate 从 web 层访问数据库。
  3. 重用之前开发的模板来开发模板。
  4. 使用 UI 组件和 JSF EL 开发用户界面。

图 6-18 展示了你将要开发的应用的目录结构。

9781430259831_Fig06-18.jpg

图 6-18 。书店 web 应用的目录结构

将 JSF 与 Spring 框架集成

要将 JSF 与 Spring 依赖注入框架集成,您必须做以下事情:

  • 在 web.xml 中添加由 Spring 框架提供的 ContextLoaderListener 和 RequestContextListener 侦听器
  • 在 faces-config.xml 中添加一个 el-resolver 条目,指向 Spring 类 SpringBeansFacesELResolver

清单 6-18 展示了在 web.xml 文件中添加 ContextLoaderListener 和 RequestContextListener。

清单 6-18 。web.xml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" fontname">http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" metadata-complete="true" version="3.0">
3.    <display-name>JSFBooks</display-name>
4.    <servlet>
5.    <servlet-name>Faces Servlet</servlet-name>
6.    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
7.    <load-on-startup>1</load-on-startup>
8.    </servlet>
9.    <servlet-mapping>
10.    <servlet-name>Faces Servlet</servlet-name>
11.    <url-pattern>/faces/*</url-pattern>
12.    </servlet-mapping>
13.    <servlet-mapping>
14.    <servlet-name>Faces Servlet</servlet-name>
15.    <url-pattern>*.jsf</url-pattern>
16.    </servlet-mapping>
17.    <servlet-mapping>
18.    <servlet-name>Faces Servlet</servlet-name>
19.    <url-pattern>*.faces</url-pattern>
20.    </servlet-mapping>
21.    <context-param>
22.    <description>State saving method: 'client' or 'server' (=default). See JSF Specification 2.5.2</description>
23.    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
24.    <param-value>client</param-value>
25.    </context-param>
26.    <context-param>
27.    <param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name>
28.    <param-value>resources.application</param-value>
29.    </context-param>
30.    <listener>
31.    <listener-class>com.sun.faces.config.ConfigureListener</listener-class>
32.    </listener>
33.    <listener>
34.    <listener-class>
35.             org.springframework.web.context.ContextLoaderListener
36.    </listener-class>
37.    </listener>
38.    <listener>
39.    <listener-class>
40.             org.springframework.web.context.request.RequestContextListener
41.    </listener-class>
42.    </listener>
43.    </web-app>
  • 第 33 到 37 行:配置 ContextLoaderListener
  • 第 38 到 42 行:配置 RequestContextListener

SpringBeanFacesELResolver 是一个 ELResolver 实现,它委托给 Spring 的 WebApplicationContext 和底层 JSF 实现的默认解析器。清单 6-19 说明了如何添加 el-resolver。

清单 6-19 。faces-config.xml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <faces-config
3.        fontname">http://java.sun.com/xml/ns/javaee"
4.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5.        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
6.        version="2.0">
7.    <application>
8.        <el-resolver>
9.            org.springframework.web.jsf.el.SpringBeanFacesELResolver
10.        </el-resolver>
11.        </application>
12.    </faces-config>
  • 第 7 行到第 11 行:配置弹簧 EL 分解器

JSF 现在与 Spring 框架集成在一起,您应该能够通过 Spring JDBCTemplate 访问数据库。

通过 Spring JDBCTemplate 从 Web 层访问数据库

接下来,您将创建受管 bean 来从 web 层访问数据库。您已经在第五章中创建了 Spring JDBCTemplate。您现在将要创建的受管 bean 将使用先前通过 BookService 创建的 JDBCTemplate,它也是在第五章中创建的。换句话说,我们将通过在第五章中创建的服务层和数据访问层创建托管 bean 来访问数据库。清单 6-20 展示了 BookController 管理的 bean。

清单 6-20 。BookController.java

1.    package com.apress.books.controller;
2.
3.    import javax.faces.bean.ManagedBean;
4.    import javax.faces.bean.RequestScoped;
5.    import com.apress.books.model.Book;
6.    import com.apress.books.service.BookService;
7.    import java.util.List;
8.
9.    @ManagedBean
10.    @RequestScoped
11.    public class BookController {
12.
13.          private BookService bookService ;
14.          private List<Book> bookList;
15.
16.        public String listAllBooks() {
17.            bookList = bookService.getAllBooks();
18.                return "bookList.xhtml";
19.            }
20.
21.        public BookService getBookService() {
22.            return bookService;
23.        }
24.
25.        public void setBookService(BookService bookService) {
26.            this.bookService = bookService;
27.        }
28.
29.        public List<Book> getBookList() {
30.              return bookList;
31.        }
32.        public void setBookList(List<Book> bookList) {
33.            this.bookList = bookList;
34.        }
35.    }
  • 第 17 行:调用 bookService 上的 getAllBooks()方法
  • 第 18 行:返回由模板和 list.xhtml 组成的 booklist.xhtml 文件,显示图书列表,见后文图 6-20

清单 6-21 展示了提供给 Spring IoC 容器的配置元数据。该文件与在第五章中创建的文件相同,只是稍微修改了一下,用 BookService 配置受管 bean BookController。

清单 6-21 。application context . XML

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <beans fontname">http://www.springframework.org/schema/beans"
3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
4.        xmlns:aop="http://www.springframework.org/schema/aop"
5.        xsi:schemaLocation="http://www.springframework.org/schema/beans
6.            http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
7.            http://www.springframework.org/schema/context
8.            http://www.springframework.org/schema/context/spring-context-3.2.xsd
9.            http://www.springframework.org/schema/aop
10.            http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
11.
12.        <!-- telling container to take care of annotations stuff -->
13.        <context:annotation-config />
14.
15.        <!-- declaring base package -->
16.        <context:component-scan base-package="com.apress.books" />
17.
18.        <bean id="bookController" class="com.apress.books.controller.BookController">
19.            <property name="bookService" ref="service"></property>
20.        </bean>
21.
22.        <bean id="dao" class="com.apress.books.dao.BookDAOImpl" >
23.        <property name="dataSource" ref="dataSourceBean">
24.            </property>
25.        </bean>
26.
27.        <bean id="service" class="com.apress.books.service.BookServiceImpl">
28.            <property name="bookDao" ref="dao">
29.            </property>
30.        </bean>
31.
32.        <bean id="dataSourceBean"
33.            class="org.springframework.jdbc.datasource.DriverManagerDataSource">
34.            <property name="driverClassName" value="com.mysql.jdbc.Driver" />
35.            <property name="url" value="jdbc:mysql://localhost:3306/books" />
36.            <property name="username" value="root" />
37.            <property name="password" value="password" />
38.        </bean>
39.    </beans>
  • 第 18 到 20 行:用 bookService 配置 bookController

开发模板

您将重用先前为书店应用的标题和侧栏开发的模板和模板客户机文件。但是,您需要修改 sideBar.xhtml 来调用 bookController bean 中的 listOfAllBooks 动作,如清单 6-22 所示。

清单 6-22 。sideBar.xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <!DOCTYPE html >
3.    <html fontname">http://www.w3.org/1999/xhtml"
4.        xmlns:h="http://java.sun.com/jsf/html"
5.        xmlns:ui="http://java.sun.com/jsf/facelets">
6.    <h:form>
7.        <div class="leftbar">
8.            <ul id="menu">
9.                <li><div>
10.                        <a class="link1" href=""><span class="label"
11.                            style="margin-left: 15px;">Home</span>
12.                        </a>
13.                    </div></li>
14.                <li><div>
15.                <h:commandLink class="link1" action="#{bookController.listAllBooks}">
16.                    <span style="margin-left: 15px;" class="label">All Books</span>
17.                </h:commandLink>
18.                    </div></li>
19.                <li><div>
20.                    <span class="label" style="margin-left: 15px;">Categories</span>
21.                    </div>
22.                    <ul>
23.                        <li><a class="label" href=""><span class="label"
24.                        style="margin-left: 30px;"></span></a></li>
25.                    </ul></li>
26.                <li><div>
27.                    <span class="label" style="margin-left: 15px;">Contact Us</span>
28.                    </div></li>
29.            </ul>
30.        </div>
31.    </h:form>
32.
33.    </html>
  • 第 6 行:显示了 h:form 标签的用法,而不是使用标记<表单>
  • 第 15 行:显示了 h:commandlink 标签的用法,该标签使用延迟 EL 触发 bookController 管理的 bean 上的 listAllBooks 操作

使用 UI 组件和 JSF EL 开发用户界面

现在您将开发应用的 UI。 图 6-19 说明了应用的主页。当用户点击侧边栏中的所有书籍时,显示所有书籍的列表,如图图 6-20 所示。

9781430259831_Fig06-19.jpg

图 6-19 。书店 web 应用的主页

9781430259831_Fig06-20.jpg

图 6-20 。图书清单

清单 6-23 说明了图 6-19 的代码。

清单 6-23 。home.xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <!DOCTYPE html>
3.    <html fontname">http://www.w3.org/1999/xhtml"
4.        xmlns:h="http://java.sun.com/jsf/html"
5.        xmlns:ui="http://java.sun.com/jsf/facelets">
6.    <h:body>
7.        <ui:composition template="templates/common.xhtml">
8.            <ui:define name="content">
9.                <ui:include src="/contents.xhtml" />
10.            </ui:define>
11.        </ui:composition>
12.    </h:body>
13.    </html>
  • 第 9 行:包含 contents.xhtml 的 ui:include 标签的用法

当用户点击首页侧边栏中的所有书籍时,显示所有书籍的列表,如图图 6-20 所示。

清单 6-24 和清单 6-25 说明了图 6-20 的代码。

清单 6-24 。bookList.html

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <!DOCTYPE html>
3.    <html fontname">http://www.w3.org/1999/xhtml"
4.        xmlns:h="http://java.sun.com/jsf/html"
5.        xmlns:ui="http://java.sun.com/jsf/facelets">
6.    <h:body>
7.        <ui:composition template="templates/common.xhtml">
8.            <ui:define name="content">
9.                <ui:include src="/list.xhtml" />
10.            </ui:define>
11.        </ui:composition>
12.    </h:body>
13.    </html>
  • 第 9 行:包含 list.xhtml 的 ui:include 标签的用法

清单 6-25 展示了图 6-20 中显示的图书列表的 list.xhtml 文件。

清单 6-25 。list.xhtml

1.    <?xml version="1.0" encoding="UTF-8"?>
2.    <!DOCTYPE html>
3.    <html fontname">http://www.w3.org/1999/xhtml"
4.        xmlns:h="http://java.sun.com/jsf/html"
5.        xmlns:ui="http://java.sun.com/jsf/facelets"
6.        xmlns:f="http://java.sun.com/jsf/core">
7.    <h:head>
8.        <title>List of All books</title>
9.        <link rel="stylesheet" href="css/bookstore.css" type="text/css" />
10.        <script type="text/javascript" src="js/jquery-1.9.1.js"></script>
11.        <script src="js/bookstore.js"></script>
12.    </h:head>
13.    <h:body>
14.        <h:dataTable id="grid" value="#{bookController.bookList}" var="book">
15.            <h:column>
16.                <f:facet name="header" id="th-title">
17.                    <h:outputText value="Title" />
18.                </f:facet>
19.                <h:outputText value="#{book.bookTitle}" />
20.            </h:column>
21.            <h:column>
22.                <f:facet name="header" id="th-author">
23.                    <h:outputText value="Author" />
24.                </f:facet>
25.                <ui:repeat value="#{book.authors}" var="content">
26.                    <h:outputText value="#{content.firstName} #{content.lastName}" />
27.                </ui:repeat>
28.            </h:column>
29.            <h:column>
30.                <f:facet name="header" id="th-price">
31.                    <h:outputText value="Publisher" />
32.                </f:facet>
33.                <h:outputText value="#{book.publisherName}" />
34.            </h:column>
35.        </h:dataTable>
36.    </h:body>
37.    </html>
  • 第 6 行:声明 JSF 核心库的名称空间
  • 第 14 行:显示了 h:dataTable 标签的用法,而不是使用标记< table/ >
  • 第 14 行:显示了使用受管 bean bookController 的 bookList 属性的延迟 EL 表达式
  • 第 15 行:显示 h:column 的用法,不使用标记< td/ >
  • 第 16 行:显示了使用 f:facet 标签来添加 facet
  • 第 17 行:显示显示书名的 h:outputText 的用法

摘要

JSF 是一个基于组件的 MVC 框架,其核心是 UI 组件模型;该模型允许从标准的、现成的、可重用的 UI 组件集合中开发 web 应用的视图。与 Struts 和 Spring Web MVC 不同,JSF 生命周期在定义明确的阶段执行普通的和重复的请求处理任务,允许开发人员专注于 Web 应用的业务逻辑。*

七、使用 Grails 快速开发 Web

不再活在碎片里,只有连接。

埃德加·摩根·福斯特

Grails 将 web 开发带到了下一个抽象层次。Java EE 不是用应用级的抽象编写的,这一事实导致了 Spring、Hibernate 等 Java 框架的开发和随后的流行。但是大多数 Java 框架对 web 开发采取了一种分散的方法。您必须维护每一层的配置。Grails 支持配置之上的约定,并通过 Groovy 语言用一个抽象层包装这些强大的框架,从而提供一个完整的开发平台,允许您充分利用 Java 和 JVM。

本章将深入 Grails 机器的内部,查看它的各个部分:它的轮子和齿轮都在协调运动,它的可工作性,它的前沿引擎,以及它的底层形式。它将进一步研究 Grails 生态系统中的交互。它将展示控制器如何处理、管理、指导和编排应用的逻辑流,以及它们如何处理请求、重定向请求、执行和委托操作,或者根据需要呈现视图。它将探索视图并揭示 Grails 如何使用 SiteMesh(页面装饰框架)为页面提供一致的外观,以及视图如何利用 Grails 的内置标签和标签库中的动态标签来创建格式良好的标记并促进关注点的清晰分离。它是一台相当好的机器。

Grails 特性

Grails 是一个基于请求的、MVC、开源的 web 开发框架。不仅如此,它还是一个完整的开发平台,一切都运行在健壮的 Java 和 Java EE 平台之上,如图 7-1 所示。它利用了现有的流行 Java 框架,并且包含了利用 Groovy 语言动态性的 web 容器、数据库、构建系统和测试工具。

9781430259831_Fig07-01.jpg

图 7-1 。Grails 平台

Grails 提供了最佳实践,例如约定优于配置,以及使用 Spring、Hibernate 和 SiteMesh 等框架进行单元测试。本节将重点介绍一些重要的最佳实践。

约定胜于配置

与配置相比,Grails 优先考虑约定。约定优于配置 ,简单来说,就是只在背离约定的时候才写配置代码。这些巧妙的约定对应于目录结构;Grails 使用文件的名称和位置,而不是通过连接 XML 配置文件来依赖显式配置。这意味着如果你创建一个遵循 Grails 惯例的类,Grails 会把它连接到 Spring 中,或者把它当作一个 Hibernate 实体。如果创建一个名为 book 的新域类,Grails 会自动在数据库中创建一个名为 Book 的表。通过使用约定胜于配置的范例,Grails 可以根据组件的名称及其在目录结构中的位置来设想组件。除了加速应用开发之外,这样做的一个直接后果是,只有当配置偏离标准时,您才必须配置组件的特定方面。

脚手架

Grails 脚手架在运行时或开发时从域类生成应用的 CRUD 功能。生成的应用由与域类相关联的控制器和 GSP 视图组成。脚手架还生成数据库模式,包括每个域类的表。

对象关系映射

Grails 包括一个强大的对象关系映射(ORM) 框架,称为 Grails 对象关系映射(GORM)。像大多数 ORM 框架一样,GORM 将对象映射到关系数据库;但是与其他 ORM 框架不同,GORM 是基于动态语言的。因此,GORM 可以将 CRUD 方法直接注入到类中,而不必实现它们或从持久超类继承它们。

注意 ORM 是一种将面向对象世界中的对象映射到关系数据库中的表的方法,它提供了 SQL 之上的抽象。

外挂程式

Grails 没有为每一个可能的需求提供现成的解决方案,而是提供了一个插件架构,您可以找到大量功能的插件。

单元测试

为了提高可交付成果的质量,Grails 为自动化 web 界面提供了单元测试、集成测试和功能测试。

集成开源

Grails 集成了行业标准和成熟的开源框架,本节将简要介绍其中的几个。

表 7-1 展示了 Grails 利用的框架。

表 7-1 。Grails 利用的框架

|

集成开源技术

|

描述

|
| --- | --- |
| Ajax 框架 | Grails 附带了 jQuery 库,但也通过插件系统提供了对其他框架的支持,如 Prototype、Dojo、Yahoo UI 和 Google Web Toolkit。 |
| 冬眠 | Hibernate 是一个 ORM 框架,为 GORM 提供了基础。 |
| 氘 | Grails 使用内存中的 H2 1 数据库,并在开发模式下启用 H2 数据库控制台(在 URI /dbconsole),以便可以从浏览器轻松查询内存中的数据库。 |
| 春天 | Spring Framework 在 Java EE API 之上提供了一个应用级的抽象。Grails 开发人员可以构建一个内部使用 Spring 和 Hibernate 的应用,而无需了解这些框架。Grails 从 Grails 开发人员那里抽象出了这些框架的大部分细节。 |
| SiteMesh | SiteMesh 2 是一个布局呈现框架,它实现了 Decorator 设计模式来呈现带有页眉、页脚和导航元素的 HTML。Grails 从 Grails 开发人员那里抽象出了大多数 SiteMesh 细节。 |
| 雄猫 | 默认情况下,Grails 使用嵌入式 Tomcat 容器。 |

安装 Grails

在安装 Grails 之前,您至少需要一个 Java 开发工具包(JDK)版本 1.6 或更新版本。下载适用于您的操作系统的 JDK,运行安装程序,然后设置一个名为 JAVA_HOME 的环境变量,指向这个安装的位置。

注意在您的 Grails 开发环境中需要一个 JDK。一个 JRE 是不够的。

开始使用 Grails 的第一步是安装发行版。为此,请按照下列步骤操作:

  1. grails.org/下载 Grails 的二进制发行版,并将生成的 ZIP 文件解压到您选择的位置。
  2. 将 GRAILS_HOME 环境变量设置为解压缩 ZIP 文件的位置。
    • 在基于 Unix/Linux 的系统上,这通常是在您的概要文件中添加如下内容:export GRAILS _ HOME =/path/to/GRAILS。
    • 在 Windows 上,这通常是在我的电脑/高级/环境变量下设置一个环境变量的问题。
  3. 然后将 bin 目录添加到 PATH 变量中。
    • 在基于 Unix/Linux 的系统上,这可以通过将 export PATH = " $ PATH:$ GRAILS _ HOME/bin "添加到您的概要文件中来完成。
      • 在 Windows 上,这是通过修改我的电脑/高级/环境变量下的 PATH 环境变量来完成的。

如果 Grails 工作正常,您现在应该能够在终端窗口中键入 grails -version,并看到类似如下的输出:

E:\>grails -version
Grails version: 2.2.4

Hello World 应用

在本节中,您将创建您的第一个 Grails web 应用。要创建一个 grails 应用,您需要熟悉 Grails 命令的用法:

grails [command name]

运行 create-app 创建应用。

grails create-app helloworld

这将在包含项目的当前目录中创建一个名为 helloworld 的新目录,换句话说,就是您的工作区。在控制台中导航到此目录:

cd helloworld

进入刚刚创建的 helloworld 目录,通过键入 grails 命令启动 Grails 交互式控制台。

\grails2-workspace\helloworld>grails

这会下载几个资源,然后你应该会看到一个提示,如图图 7-2 所示。

9781430259831_Fig07-02.jpg

图 7-2 。Grails 交互控制台

我们想要的是一个简单的页面,它只是将消息“Hello World”打印到浏览器。在 Grails 中,每当您想要一个新页面时,您就为它创建一个新的控制器动作。由于我们还没有控制器,现在让我们用 create-controller 命令创建一个。

grails> create-controller hello

前面的命令将在 grails-app/controllers/hello world 目录下创建一个名为 HelloController.groovy 的新控制器,如清单 7-1 中的所示。

清单 7-1 。HelloController.groovy

package helloworld

class HelloController {

    def index() { }
}

我们现在有了一个控制器,所以让我们添加一个动作来生成“Hello World”页面。代码看起来像清单 7-2 。

清单 7-2 。修改索引操作

def index() { render "Hello World" } }

动作只是一个方法。在这种特殊情况下,它调用 Grails 提供的特殊方法来呈现页面。

要查看应用的运行情况,您需要使用另一个名为 run-app 的命令启动服务器。

grails> run-app

这将在端口 8080 上启动一个托管您的应用的嵌入式服务器。现在,您应该能够通过 URLlocalhost:8080/hello world/访问您的应用。结果将看起来像图 7-3 。

9781430259831_Fig07-03.jpg

图 7-3 。Grails 的欢迎屏幕

这是 Grails 简介页面,由 grails-app/view/index.gsp 文件呈现。它检测控制器的存在,并提供指向它们的链接。单击 HelloController 链接,查看包含文本“Hello World”的自定义页面您有了第一个工作的 Grails 应用。

注意图 7-3 中的有一个到 Dbdoc 控制器的链接。单击此链接将产生一条错误消息,因为控制器尚未实现。DbdocController 的目的是生成静态 HTML 文件来查看更改日志信息。您可以在 conf/Config.groovy 中通过设置 dbDocController.enabled = true 来启用它

书店应用

在本章中,您将学习如何利用 Grails 约定和搭建来创建一个简单但功能强大的书店应用版本。然而,这个应用的初始版本还不能用于生产;这个应用的目的是向你展示如何使用脚手架,你可以用除了你的域类代码之外几乎没有任何代码来呈现一个 CRUD web 应用。此外,Grails 将生成一个数据库模式,并在应用运行时用该模式填充数据库。

创建书店应用

要创建书店应用,您需要在命令行上使用可选的项目名称来执行 create-app 目标,如下所示:

>grails create-app bookstore

前面命令行中的整行都是命令,其中 create-app 是目标。目标是您希望 Grails 执行的特定任务。

注意使用 help 命令会产生一个可用目标列表:> grails 帮助。

如果您在使用 create-app 时没有提供项目名称,系统会提示您提供项目名称。

在 create-app 目标运行之后,您将拥有一个与项目名称相匹配的新目录。这是新项目的根目录,您必须从这个目录中进行所有后续的 Grails 命令行调用。现在使用 cd 命令进入目录是个好主意,这样你就不会忘记了。在新的项目目录中,您会发现一个与图 7-4 所示目录结构相匹配的结构。

9781430259831_Fig07-04.jpg

图 7-4 。书店应用的目录结构

您可以使用自己选择的 IDE,而不是从命令行创建应用。我们推荐 Groovy/GrailsTool 套件(GGTS),你可以从www.springsource.org/downloads/sts-ggts下载。这本书用的是最新版本,GGTS 3.0。GGTS 为构建 Groovy 和 Grails 应用提供了最好的基于 Eclipse 的开发环境。GGTS 提供了对 Groovy 和 Grails 最新版本的支持,并基于最新的 Eclipse 版本。图 7-5 显示了如何在 GGTS 配置 Grails。在 Preferences 下,单击 Grails,然后单击 Add 按钮。在“配置 Grails 安装”窗口中,通过单击“浏览”按钮浏览 Grails,这将打开“Grails 安装目录”窗口。选择 Grails 安装目录,然后单击 OK。

9781430259831_Fig07-05.jpg

图 7-5 。用 Grails 配置 GGTS

Grails 现在被添加到构建路径中,如图 7-6 所示。单击确定。现在 GGTS 已经配置了 Grails,您可以创建一个 Grails 项目了。

9781430259831_Fig07-06.jpg

图 7-6 。构建路径中的 grails

在 GGTS 新建一个项目,使用菜单选项 File image New image Grails Project,如图图 7-7 所示。

9781430259831_Fig07-07.jpg

图 7-7 。创建新项目

因为您已经从命令行创建了项目,所以您可以在 GGTS 中导入创建的项目。在导入窗口中选择已有的项目到工作区,如图图 7-8 所示。

9781430259831_Fig07-08.jpg

图 7-8 。导入现有项目

单击下一步。选择项目的根目录,如图图 7-9 所示。

9781430259831_Fig07-09.jpg

图 7-9 。选择一个目录来搜索现有的 Eclipse 项目

单击完成。图 7-10 展示了 GGTS 书店应用的目录结构。

9781430259831_Fig07-10.jpg

图 7-10 。书店应用的目录结构

运行应用

此时,您已经有了一个可以通过 web 浏览器运行和访问的功能性应用。它还没有做很多事情,但是现在运行它将使您能够在添加域和控制器类时获得即时反馈。

要运行 Grails 应用,请从项目根目录执行 run-app 目标,如下所示:

> grails run-app

执行 run-app 目标的输出如下所示:

Server running. Browse to http://localhost:8080/bookstore

在 URLlocalhost:8080/book store访问应用,显示欢迎屏幕,如图图 7-11 所示。

9781430259831_Fig07-11.jpg

图 7-11 。书店应用的欢迎屏幕

要在 GGTS 运行应用,请单击 Grails 命令历史,如图 7-12 中突出显示的。在命令窗口中键入 run-app ,并按回车键。或者你也可以在 IDE 中右击项目,选择 Run As image Grails (run app)。

9781430259831_Fig07-12.jpg

图 7-12 。Grails 命令历史

创建控制器

控制器处理请求并创建或准备响应。控制器可以直接生成响应或委托给视图。要创建控制器类,请使用 Grails create-controller 目标。这将在 grails-app/controllers 目录中创建新的 Grails 控制器类,并在 test/unit 中为控制器类创建单元测试。如果 grails-app/views/ 目录不存在,它还会创建一个目录。

要创建 BookController 类,需要使用可选的类名执行 create-controller 目标,如下所示:

>grails create-controller book

如果不提供类名,系统会提示您提供一个。执行 create-controller 目标的输出如下所示:

| Created file grails-app/controllers/bookstore/BookController.groovy
| Created file grails-app/views/book
| Created file test/unit/bookstore/BookControllerTests.groovy

注意,当运行带有可选类名的 create-controller 时,您可以让类名保持小写,Grails 会自动将其大写,这样它就遵循了标准的 Groovy 类命名约定。

要使用 GGTS 创建控制器,单击书店项目层次中的控制器,然后使用新的image控制器。在 Grails 命令向导中键入 Book ,如图图 7-13 所示。

9781430259831_Fig07-13.jpg

图 7-13 。使用 GGTS 创建控制器

单击完成。GGTS 将生成控制器并进行测试,如以下输出所示:

Loading Grails 2.2.4
| Environment set to development.....
| Created file grails-app/controllers/bookstore/BookController.groovy
| Created file grails-app/views/book
| Compiling 1 source files.....
| Created file test/unit/bookstore/BookControllerTests.groovy

清单 7-3 展示了生成的控制器。

清单 7-3 。Grails 生成的 BookController

package bookstore

class BookController {

    def index() {
}
}

现在刷新浏览器,您可以在欢迎屏幕上看到该控制器,如图 7-14 所示。

9781430259831_Fig07-14.jpg

图 7-14 。书店应用的欢迎屏幕

修改 index(){}的代码,如清单 7-4 所示。

清单 7-4 。修改索引操作

    def index() {
render "book list"
}

现在你可以点击图 7-14 中的 BookController 链接,你将得到如图图 7-15 所示的简单文本响应。

9781430259831_Fig07-15.jpg

图 7-15 。简单的文字回复

测试控制器

清单 7-5 展示了 Grails 生成的 BookControllerTests 测试。

清单 7-5 。由 Grails 生成的 BookControllerTests

package bookstore

import grails.test.mixin.*
import org.junit.*

/**
 * See the API for {@link grails.test.mixin.web.ControllerUnitTestMixin} for usage instructions
 */
@TestFor(BookController)
class BookControllerTests {

void testSomething() {
       fail "Implement me"
}
}

修改 testSomething(),如清单 7-6 所示。

清单 7-6 。添加断言

1.void testSomething() {
2.controller.index()
3.assert "book list" == response.text
4.}

现在通过 Run as image Grails 命令(test-app)运行测试,如图 7-16 中的所示。

9781430259831_Fig07-16.jpg

图 7-16 。Grails 中的 test-app 命令

在运行 test-app 命令时,Grails 运行测试,如以下输出所示:

| Loading Grails 2.2.4
| Configuring classpath.
| Environment set to test.....
| Running 1 unit test... 1 of 1
| Completed 1 unit test, 0 failed in 3444ms
| Packaging Grails application.....
| Packaging Grails application.....
| Tests PASSED - view reports in E:\ModernJava\grails2-workspace\bookstore\target\test-reports

您可以从先前输出的最后一行所示的路径生成图 7-17 所示的报告。

9781430259831_Fig07-17.jpg

图 7-17 。测试报告:通过测试

注意通过这种方式,您可以在 Grails 生成的 ControllerTests 中增加控制器的单元测试。

测试通过了,因为在清单 7-6 中,第 3 行的断言是正确的。现在替换清单 7-6 中的第 3 行,如下所示:

assert "xyz" == response.text

这是一个不正确的断言,因为在图 7-15 中显示的简单文本响应是“书单”而不是“xyz”所以这个测试应该会失败。要查看 Grails 如何报告失败的测试,通过用清单 7-7 中的替换 testSomething()来使测试失败。

清单 7-7 。用不正确的断言替换测试

void testSomething() {
controller.index()
assert "xyz" == response.text
}

在运行 test-app 命令时,Grails 运行测试,如以下输出所示:

| Loading Grails 2.2.4
| Configuring classpath.
| Environment set to test.....
| Compiling 1 source files.
| Running 1 unit test... 1 of 1
| Failure:  testSomething(bookstore.BookControllerTests)
|  Assertion failed:

assert "xyz" == response.text
             |  |        |
             |  |        book list
             |  org.codehaus.groovy.grails.plugins.testing.GrailsMockHttpServletResponse@14cf61d
             false

at bookstore.BookControllerTests.testSomething(BookControllerTests.groovy:16)
| Completed 1 unit test, 1 failed in 3210ms
| Packaging Grails application.....
| Packaging Grails application.....
| Tests FAILED  - view reports in E:\ModernJava\grails2-workspace\bookstore\target\test-reports

您可以从先前输出的最后一行所示的路径生成图 7-18 所示的报告。

9781430259831_Fig07-18.jpg

图 7-18 。测试报告:测试失败

创建域类

此时,我们创建的应用实际上并不做任何事情;它只是呈现一个简单的文本响应。我们将继续创建一个域类。要创建域类,请使用 Grails create-domain-class 目标。这将在 grails-app/domain 目录中创建一个新的 Grails 域类,并在 test/unit 中为该域类创建一个单元测试。

要创建 Book domain 类,您需要使用可选的类名执行 create-domain-class 目标,如下所示:

> grails createdomain-class book

如果不提供类名,系统会提示您提供一个。

请注意,当运行带有可选类名的 create-domain-class 目标时,您可以将类名保留为小写,Grails 会自动将其大写,以便它遵循标准的 Groovy 类命名约定。

要使用 GGTS 创建域类,在项目层次中点击“域”,然后使用新的image域类,如图 7-19 中的所示。

9781430259831_Fig07-19.jpg

图 7-19 。使用 GGTS 创建域类

当你点击图 7-19 中的域类时,会显示 Grails 命令向导窗口,如图图 7-20 所示。

9781430259831_Fig07-20.jpg

图 7-20 。使用 GGTS 创建域类

在名称字段中输入域类的名称,然后单击完成。Grails 创建 Book domain 类和 BookTests,如以下输出所示:

Loading Grails 2.2.4
| Environment set to development.....
| Created file grails-app/domain/bookstore/Book.groovy
| Compiling 1 source files.....
| Created file test/unit/bookstore/BookTests.groovy

清单 7-8 展示了 Grails 生成的图书类。

清单 7-8 。Grails 生成的图书领域类

package bookstore

class Book {

    static constraints = {
    }
}

清单 7-8 中的 Book 类为空。现在你可以完成这个域类,如清单 7-9 所示。

清单 7-9 。图书领域类

1.package bookstore
2.
3.class Book {
4.String bookTitle
5.Long price
6.Long isbn
7.
8.static constraints = {
9.bookTitle(blank:false)
10.price(blank:false)
11.}
12.String toString() {
13.bookTitle
14.}
15.}

清单 7-9 中的第 8 到 11 行的约束为 Grails 提供了定义验证规则的声明性机制。表 7-2 展示了 Grails 可用的约束。

表 7-2 。Grails 可用的约束

|

限制

|

描述

|
| --- | --- |
| 空白的 | 验证字符串值不为空 |
| 信用卡呢 | 验证字符串值是有效的信用卡号 |
| 电子邮件 | 验证字符串值是有效的电子邮件地址 |
| 在列表中 | 验证值是否在受约束值的范围或集合内 |
| 比赛 | 验证字符串值是否与给定的正则表达式匹配 |
| 最大 | 验证值不超过给定的最大值 |
| maxSize(最大值) | 验证值的大小没有超过给定的最大值 |
| 部 | 验证值不低于给定的最小值 |
| 最小尺寸 | 验证值的大小不低于给定的最小值 |
| 不等 | 验证属性不等于指定的值 |
| 可空的 | 允许将属性设置为 null 默认为假 |
| 范围 | 使用 Groovy 范围来确保属性值出现在指定的范围内 |
| 规模 | 设置浮点数所需的小数位数(即小数点右边的位数) |
| 大小 | 使用 Groovy 范围来限制集合或数字的大小或字符串的长度 |
| 独一无二的 | 将属性约束为数据库级别的唯一属性 |
| 全球资源定位器(Uniform Resource Locator) | 验证字符串值是有效的 URL |
| 验证器 | 向字段添加自定义验证 |

脚手架

Scaffolding 允许您为给定的域类自动生成整个应用,包括 CRUD 操作的视图和控制器动作。脚手架可以是静态的也可以是动态的;这两种类型生成相同的代码。主要区别在于,在静态搭建中,生成的代码在编译之前就可供用户使用,因此如果需要的话可以很容易地修改。然而,在动态搭建中,代码是在运行时在内存中生成的,对用户是不可见的。在接下来的部分中,您将学习动态和静态搭建。

动态脚手架

如前所述,动态脚手架在运行时为 CRUD 应用生成控制器动作和视图。要动态搭建一个域类,您需要一个控制器。您在清单 7-3 的中创建了一个控制器(BookController)。要使用动态脚手架,将索引动作更改为脚手架属性,并为其分配域类,如清单 7-10 中的所示。这导致为指定的域类生成列表页面、创建页面、编辑页面、显示页面视图以及删除功能。

清单 7-10 。启用动态脚手架的记账员

package bookstore

class BookController {

    static scaffold = Book
}

在将 BookController 更改为类似于清单 7-10 中的之后,执行 run-app 目标。

执行 run-app 目标的输出如下所示:

| Loading Grails 2.2.4
| Configuring classpath.
| Environment set to development.....
| Packaging Grails application.....
| Running Grails application
| Server running. Browse tohttp://localhost:8080/bookstore

单击欢迎页面上的 BookController 链接。

点击 BookController 链接,进入图 7-21 所示的图书列表视图。

9781430259831_Fig07-21.jpg

图 7-21 。书单查看

您可以通过点按“新书”来创建或添加新书。图 7-22 显示了创建新书的屏幕。图 7-22 也显示了验证 的运行,你不必为其编写任何代码。这个验证的代码包含在域类手册中,如清单 7-9 第 8 到 11 行的所示。

9781430259831_Fig07-22.jpg

图 7-22 。创建视图并进行验证

图 7-23 显示了通过完成所有验证来创建一本新书。

9781430259831_Fig07-23.jpg

图 7-23 。创建视图

图 7-24 显示了新创建的图书。

9781430259831_Fig07-24.jpg

图 7-24 。显示视图

您可以编辑、删除或添加新创建的图书。您可以通过点按“更新”来编辑创建的图书。图 7-25 说明了编辑视图。

9781430259831_Fig07-25.jpg

图 7-25 。编辑视图

图 7-26 说明了更新后的图书。

9781430259831_Fig07-26.jpg

图 7-26 。显示带有更新消息的视图

您可以通过点按“新书”来添加新书。图 7-27 显示了以这种方式添加的图书列表。

9781430259831_Fig07-27.jpg

图 7-27 。带有已添加图书列表的列表视图

静态脚手架

Static scaffolding 提供了一个优秀的学习工具,帮助您熟悉 Grails 框架以及一切是如何组合在一起的。现在,是时候将静态脚手架作为一种学习工具来使用了。动态和静态搭建的域类没有区别。为了快速参考,Book 类如清单 7-11 所示。

清单 7-11 。图书领域类

package bookstore

class Book {
String bookTitle
Long price
Long isbn

static constraints = {
bookTitle(blank:false)
price(blank:false)
}
String toString() {
bookTitle
}
}

静态搭建与动态搭建的不同之处在于视图和控制器的生成方式。在这两种情况下,域类保持不变。然而,在动态搭建中,您需要控制器向 Grails 表明您需要动态搭建来为您生成应用。如果你想让 Grails 通过静态搭建生成应用,你必须使用清单 7-12 中的命令。

清单 7-12 。通过静态搭建生成应用的命令

>grails generate-all bookstore.Book

从命令行或 GGTS 运行该命令后,Grails 会生成如下所示的应用:

Loading Grails 2.2.4
| Configuring classpath.
| Environment set to development.....
| Packaging Grails application.....
| Packaging Grails application.....
| Generating views for domain class bookstore.Book
| Generating controller for domain class bookstore.Book
| Finished generation for domain class bookstore.Book

如果我们现在运行应用,我们将拥有一个完整的 CRUD 应用。这个 generate-all 命令为我们的域类 Book 生成一个控制器(BookController)和四个视图,并为我们的控制器 BookControllerTest 生成一个单元测试。这些文件为我们提供了一个完整的 CRUD 应用,充当了我们可以添加定制代码的存根。让我们仔细看看我们生成的代码。我们从图 7-28 中的所示的图书控制器开始。

9781430259831_Fig07-28.jpg

图 7-28 。BookController(帐簿控制器)

在图 7-28 所示的 BookController 代码中,你会注意到的第一件事是 Grails 控制器是一个普通的 Groovy 类,它不扩展任何类,也不实现任何接口。接下来您会注意到 BookController 有八个动作。

  • 创建行动
  • 删除操作
  • 编辑操作
  • 索引操作
  • 列表操作
  • 保存操作
  • 显示动作
  • 更新操作

这些动作是控制器的闭包属性。控制器中的所有工作都在动作中完成。控制器中声明的每个闭包都是一个动作,可以通过 URL 访问,默认情况下,URL 映射到控制器动作。URL 的第一部分代表控制器名称,第二部分代表操作名称。在接下来的几节中,您将更深入地了解这些操作,但在此之前,有必要了解如何退出控制器操作。正确退出控制器动作有三个选项。

  • 调用 render()方法
  • 调用 redirect()方法
  • 返回模型或空值

在接下来的章节中,在探究图 7-28 中的所示的 BookController 中的每个动作之前,您将查看这三个选项。

调用 render()方法

退出控制器动作的第一个选项是调用 render()方法来呈现视图或文本响应。为了理解 render()方法是如何工作的,清单 7-13 展示了一个简单的 Grails 控制器,当它被调用时,会向您问候。

清单 7-13 。呈现文本响应

package chapter5
class HelloController {
def index() {
render 'hello'
}
defshow(){}
def someOtherAction(){}
}

如清单 7-13 所示,控制器在被/hello/index 请求调用时,将执行控制器中定义的 index()动作,index()动作将使用 render()方法呈现文本响应。在 HelloController 中调用 index()动作的完整 URL 是localhost:8080/chapter 5/hello/index。如清单所示,控制器中可以有任意数量的动作。

如果您在 render()方法中指定视图名称,如清单 7-14 中的所示,Grails 会假设您指的是位于 Grails-app/views/hello/hello . gsp 位置的视图,并呈现一个名为 hello 的视图。

清单 7-14 。渲染视图

class HelloController {
...
def show() {
render view: "hello"
}
...
}

调用 redirect()方法

退出控制器操作的第二个选项是调用 redirect()方法来发出一个到另一个 URL 的 HTTP 重定向。Grails 为所有控制器提供了一个接受 Map 作为参数的 redirect()方法。该映射应该包含 Grails 执行重定向所需的所有信息,包括重定向到的动作的名称。

此外,映射可以包含要重定向到的控制器的名称。清单 7-15 显示了在同一个控制器中从第一个动作到第二个动作的标准重定向。

清单 7-15 。重定向到同一控制器中的动作

class HelloController {
def first() {
redirect action: "second"
}
def second() {
...
}
}

如果重定向是针对另一个控制器中的操作,则必须指定另一个控制器的名称。清单 7-16 展示了如何重定向到另一个控制器中的动作。

清单 7-16 。重定向到另一个控制器中的操作

class HelloController {
def first() {
redirect action: "second", controller: "other"
}
}

在清单 7-16 中,HelloController 中的第一个()动作重定向到另一个控制器中的第二个()动作。

返回模型

退出控制器动作的第三个选项是返回一个模型,它是一个包含数据的地图,如清单 7-17 中的所示。

清单 7-17 。归还模型

class HelloController {
def show() {
    [user: User.get(params.id)]
}
}

Grails 将尝试呈现与动作同名的视图。它将在以控制器的基本名称命名的目录中查找这个视图。在清单 7-17 中,从 HelloController 的 show()操作返回将导致 Grails 呈现 view /views/hello/show.gsp。

现在你知道如何调用控制器的动作,以及如何退出。有了这些知识,看一看图 7-28 ,让我们开始逐一研究每个动作。首先让我们把 allowedMethods 属性去掉。

static allowedMethods = [save: "POST", update: "POST", delete: "POST"]

allowedMethods 属性提供了一个简单的声明性语法来指定控制器操作允许哪些 HTTP 方法。默认情况下,所有控制器操作都允许所有请求方法。allowedMethods 属性是可选的,只有当控制器的操作需要限制为某些请求方法时,才需要定义该属性。BookController 中的这个属性指定只有 save、update 和 delete 可以是 POST 方法。

索引操作

index()动作是导航到 BookController 时调用的默认动作。默认情况下,这个动作只是使用之前解释的 redirect()方法重定向到 list()动作,如清单 7-18 所示。

清单 7-18 。图书管理员的索引操作

def index() {
        redirect(action: "list", params: params)
    }

redirect()方法将 HTTP 重定向到由这些参数构造的 URL。如果未指定操作,将使用 index()操作。params 保存请求参数,如果有的话。

列表操作

清单 7-19 展示了 BookController 的 list()动作。

清单 7-19 。BookController 的列表操作

def list(Integer max) {
        params.max = Math.min(max ?: 10, 100)
        [bookInstanceList: Book.list(params), bookInstanceTotal: Book.count()]
    }

列表闭包的第一行使用 params 属性,这是一个包含传入请求的所有参数的映射。

最后一行返回一个包含两个元素的映射:bookInstanceList 和 bookInstanceTotal。bookInstanceList 是通过对 Book.list()的调用加载的。向 list()传递参数映射,它将从该映射中提取任何可以使用的参数。bookInstanceTotal 是用 Book.count()加载的。bookInstanceTotal 的使用将在后面的“列表视图”部分提到。list()操作使用从该操作返回的地图中的数据呈现列表视图。

创建行动

清单 7-20 展示了 BookController 的 create()动作。

清单 7-20 。BookController 的创建操作

def create() {
        [bookInstance: new Book(params)]
    }

create()动作创建一个新的 Book 实例,然后将参数分配给 Book instance 的属性,因为稍后会用到它,如下面在清单 7-21 中讨论的“保存动作”部分所述。然后它用 bookInstance 的键在 Map 中返回该实例。最后,它呈现 create 视图。

保存操作

清单 7-21 展示了 BookController 的 save()动作。

清单 7-21 。BookController 的保存操作

def save() {
        def bookInstance = new Book(params)
        if (!bookInstance.save(flush: true)) {
            render(view: "create", model: [bookInstance: bookInstance])
            return
        }

        flash.message = message(code: 'default.created.message', args: [message(code: 'book.label', default: 'Book'), bookInstance.id])
        redirect(action: "show", id: bookInstance.id)
    }

如果有错误,用户将被重定向到 create()操作。对于这个重定向到 create()动作的操作,params 被分配给 bookInstance 的属性,正如前面在清单 7-20 中的 create()动作所提到的。如果没有错误,将使用新创建的实例呈现 show 视图。

显示动作

清单 7-22 展示了 BookController 的 show()动作。

清单 7-22 。BookController 的显示操作

def show(Long id) {
        def bookInstance = Book.get(id)
        if (!bookInstance) {
            flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
            redirect(action: "list")
            return
        }

        [bookInstance: bookInstance]
    }

show()操作需要一个 id 参数。show()操作的第一行调用 Book.get()方法来检索 id 参数所引用的图书。如果不存在带有传入 id 的 Book 实例,则在 flash 范围内会存储一条错误消息,用户会被重定向到列表视图。

如果找到一个带有传入 id 的 Book 实例,它将在一个 Map 中返回,键为 Book instance,show()操作将呈现 show 视图。

编辑操作

清单 7-23 展示了 BookController 的 edit()动作。

清单 7-23 。BookController 的编辑操作

def edit(Long id) {
        def bookInstance = Book.get(id)
        if (!bookInstance) {
            flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
            redirect(action: "list")
            return
        }

        [bookInstance: bookInstance]
    }

edit()动作加载将在编辑过程中使用的必要数据,并将其传递给 edit 视图。edit()操作与 show()操作非常相似。edit()动作的名称 edit 用于呈现编辑视图。

更新操作

当提交来自编辑视图的更改时,将调用 update()操作。清单 7-24 展示了 BookController 的 update()动作。

清单 7-24 。BookController 的更新操作

def update(Long id, Long version) {
        def bookInstance = Book.get(id)
        if (!bookInstance) {
            flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
            redirect(action: "list")
            return
        }

        if (version != null) {
            if (bookInstance.version > version) {
                bookInstance.errors.rejectValue("version", "default.optimistic.locking.failure",
                          [message(code: 'book.label', default: 'Book')] as Object[],
                          "Another user has updated this Book while you were editing")
                render(view: "edit", model: [bookInstance: bookInstance])
                return
            }
        }

        bookInstance.properties = params

        if (!bookInstance.save(flush: true)) {
            render(view: "edit", model: [bookInstance: bookInstance])
            return
        }

        flash.message = message(code: 'default.updated.message', args: [message(code: 'book.label', default: 'Book'), bookInstance.id])
        redirect(action: "show", id: bookInstance.id)
    }

update()操作试图用 id 参数检索一个 Book 实例。id 是从编辑视图中提供的。如果找到实例,将执行开放式并发检查。如果没有错误,edit 视图中的所有值都被分配给 Book 实例的适当属性,包括任何必要的数据转换。

bookInstance.properties = params

如果这两个步骤都成功,flash 中会存储一条“成功”消息,用户会被定向到 show 视图。如果任一步骤失败,flash 中会存储一条“失败”消息,用户将返回到编辑视图。

删除操作

默认情况下,delete()操作在编辑和显示视图中可用。清单 7-25 展示了 BookController 的 delete()动作。

清单 7-25 。BookController 的删除操作

def delete(Long id) {
     def bookInstance = Book.get(id)
     if (!bookInstance) {
         flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
         redirect(action: "list")
         return
     }

     try {
         bookInstance.delete(flush: true)
         flash.message = message(code: 'default.deleted.message', args: [message(code: 'book.label', default: 'Book'), id])
         redirect(action: "list")
     }
     catch (DataIntegrityViolationException e) {
         flash.message = message(code: 'default.not.deleted.message', args: [message(code: 'book.label', default: 'Book'), id])
         redirect(action: "show", id: id)
     }
}

delete()操作试图检索一个 Book 实例,如果找不到,就重定向到列表视图。如果找到一个实例,则进入 try/catch 块,尝试删除该实例。如果删除成功,消息将存储在闪存中,并重定向到列表视图。如果出现异常,不同的消息将存储在 flash 中,并重定向到 show 视图。

现在,您已经看到了 BookController 中为 Book 类生成的所有操作,让我们检查一下由静态搭建为 Book 类生成的视图。

Grails 视图

Grails 使用 Groovy 服务器页面(GSP) 作为其视图层。Grails 还使用页面装饰框架 SiteMesh 来帮助页面布局。SiteMesh 合并每个。gsp 文件合并到一个名为 main.gsp 的文件中,以使所有页面具有一致的外观。您将从 main.gsp 开始生成视图,它可以在\views\layouts 中找到,然后是为 Book 类生成的四个视图:list.gsp、show.gsp、create.gsp 和 edit . gsp。清单 7-26 展示了 main.gsp

清单 7-26 。main .普惠制

1.<!DOCTYPE html>
2.<!--[if lt IE 7 ]><html lang="en" class="no-js ie6"><![endif]-->
3.<!--[if IE 7 ]><html lang="en" class="no-js ie7"><![endif]-->
4.<!--[if IE 8 ]><html lang="en" class="no-js ie8"><![endif]-->
5.<!--[if IE 9 ]><html lang="en" class="no-js ie9"><![endif]-->
6.<!--[if (gt IE 9)|!(IE)]><!--><html lang="en" class="no-js"><!--<![endif]-->
7.<head>
8.<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
9.<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
10.<title><g:layoutTitle default="Grails"/></title>
11.<meta name="viewport" content="width=device-width, initial-scale=1.0">
12.<link rel="shortcut icon" href="${resource(dir: 'images', file: 'favicon.ico')}" type="image/x-icon">
13.<link rel="apple-touch-icon" href="${resource(dir: 'images', file: 'apple-touch-icon.png')}">
14.<link rel="apple-touch-icon" sizes="114x114" href="${resource(dir: 'images', file: 'apple-touch-icon-retina.png')}">
15.<link rel="stylesheet" href="${resource(dir: 'css', file: 'main.css')}" type="text/css">
16.<link rel="stylesheet" href="${resource(dir: 'css', file: 'mobile.css')}" type="text/css">
17.<g:layoutHead/>
18.<r:layoutResources />
19.</head>
20.<body>
21.<div id="grailsLogo" role="banner"><a href="http://grails.org"><imgsrc="${resource(dir: 'images', file: 'grails_logo.png')}" alt="Grails"/></a></div>
22.<g:layoutBody/>
23.<div class="footer" role="contentinfo"></div>
24.<div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading&hellip;"/></div>
25.<g:javascript library="application"/>
26.<r:layoutResources />
27.</body>
28.</html>
  • 第 1 行到第 6 行:main . gsp 页面以<开头!doctype html >。这是一个 HTML5 文档类型。Grails 支持现成的 HTML5。
  • 第 10 行 : < g:layoutTitle >在布局中用于呈现装饰页面的 Title 标签的内容。< g:layoutTitle >标签替换了正在被合并的视图中的<标题>,并将它链接到所有视图都将使用的样式表和 favicon 中。
  • 第 17 行:<g:layout head>标签合并到目标视图的< head >部分的内容中。< g:layoutHead >在布局中用于呈现装饰页面的 Head 标签的内容。
  • 第 22 行 : < g:layoutBody >在 layouts 中用来输出装饰页面的 Body 标签的内容。< g:layoutBody >标签合并到目标视图的<主体>内容中。
  • 第 25 行 : < g:javascript >包括 javascript 库和脚本,并提供了内联 JavaScript 的简写。指定一个库告诉 Ajax 标签使用哪个 JavaScript 提供者。

列表视图

列表视图如清单 7-27 所示。

清单 7-27 。list.gsp

1.<%@ page import="bookstore.Book" %>
2.<!DOCTYPE html>
3.<html>
4.<head>
5.<meta name="layout" content="main">
6.<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
7.<title><g:message code="default.list.label" args="[entityName]" /></title>
8.</head>
9.<body>
10.<a href="#list-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
11.<div class="nav" role="navigation">
12.<ul>
13.<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
14.<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
15.</ul>
16.</div>
17.<div id="list-book" class="content scaffold-list" role="main">
18.<h1><g:message code="default.list.label" args="[entityName]" /></h1>
19.<g:if test="${flash.message}">
20.<div class="message" role="status">${flash.message}</div>
21.</g:if>
22.<table>
23.<thead>
24.<tr>
25.
26.<g:sortableColumn property="bookTitle" title="${message(code: 'book.bookTitle.label', default: 'Book Title')}" />
27.
28.<g:sortableColumn property="price" title="${message(code: 'book.price.label', default: 'Price')}" />
29.
30.<g:sortableColumn property="isbn" title="${message(code: 'book.isbn.label', default: 'Isbn')}" />
31.
32.</tr>
33.</thead>
34.<tbody>
35.<g:each in="${bookInstanceList}" status="i" var="bookInstance">
36.<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
37.
38.<td><g:link action="show" id="${bookInstance.id}">${fieldValue(bean: bookInstance, field: "bookTitle")}</g:link></td>
39.
40.<td>${fieldValue(bean: bookInstance, field: "price")}</td>
41.
42.<td>${fieldValue(bean: bookInstance, field: "isbn")}</td>
43.
44.</tr>
45.</g:each>
46.</tbody>
47.</table>
48.<div class="pagination">
49.<g:paginate total="${bookInstanceTotal}" />
50.</div>
51.</div>
52.</body>
53.</html>
  • 第 14 行:<g:link>标签创建了一个到 BookController 的 create 动作的链接。
  • 第 19 行:<g:if>标签检查存储在动作中的 flash.message 是否存在,如果存在,就显示出来。
  • 第 26 到 31 行:<g:sortable column>标签用于在我们的列表视图中提供排序。
  • 第 35 到 45 行:<g:each>标签遍历 bookInstanceList。列表中的每一项都被分配给 bookInstance 变量。< g:each >标签的主体用 bookInstance 的属性填充表格行。在第<行 tr class="${(i % 2) == 0?even' : 'odd'}" >,用 Groovy 表达式确定< tr >的样式类,用 fieldValue()方法渲染每个 Book 属性的值。
  • 第 49 行:<g:paginate>标签显示列表视图中是否有足够的元素的分页控件。如前所述,bookInstanceTotal 是从清单 7-19 中使用的。

创建视图

创建视图如清单 7-28 中的所示。

清单 7-28 。create.gsp

1.<%@ page import="bookstore.Book" %>
2.<!DOCTYPE html>
3.<html>
4.<head>
5.<meta name="layout" content="main">
6.<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
7.<title><g:message code="default.create.label" args="[entityName]" /></title>
8.</head>
9.<body>
10.<a href="#create-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
11.<div class="nav" role="navigation">
12.<ul>
13.<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
14.<li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
15.</ul>
16.</div>
17.<div id="create-book" class="content scaffold-create" role="main">
18.<h1><g:message code="default.create.label" args="[entityName]" /></h1>
19.<g:if test="${flash.message}">
20.<div class="message" role="status">${flash.message}</div>
21.</g:if>
22.<g:hasErrors bean="${bookInstance}">
23.<ul class="errors" role="alert">
24.<g:eachError bean="${bookInstance}" var="error">
25.<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
26.</g:eachError>
27.</ul>
28.</g:hasErrors>
29.<g:form action="save" >
30.<fieldset class="form">
31.<g:render template="form"/>
32.</fieldset>
33.<fieldset class="buttons">
34.<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
35.</fieldset>
36.</g:form>
37.</div>
38.</body>
39.</html>
  • 第 22 行到第 28 行:<g:has errors>标签检查分配给其 bean 属性的 Book 实例,如果发现错误,则呈现其主体。
  • 第 29 行到第 36 行:<g:form>标签建立了一个 HTML 表单。这个标签有一个动作,这个动作将导致表单提交到的 URL。
  • Line 31 : < g:render >对模型应用内置或用户定义的 Groovy 模板,这样模板就可以被共享和重用。在这种情况下,模板称为 form,位于 views 目录中,名为 _form.gsp。gsp 文件是一个模板。

显示视图

展示视图如清单 7-29 中的所示。

清单 7-29 。show.gsp

1.<%@ page import="bookstore.Book" %>
2.<!DOCTYPE html>
3.<html>
4.<head>
5.<meta name="layout" content="main">
6.<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
7.<title><g:message code="default.show.label" args="[entityName]" /></title>
8.</head>
9.<body>
10.<a href="#show-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
11.<div class="nav" role="navigation">
12.<ul>
13.<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
14.<li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
15.<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
16.</ul>
17.</div>
18.<div id="show-book" class="content scaffold-show" role="main">
19.<h1><g:message code="default.show.label" args="[entityName]" /></h1>
20.<g:if test="${flash.message}">
21.<div class="message" role="status">${flash.message}</div>
22.</g:if>
23.<ol class="property-list book">
24.
25.<g:if test="${bookInstance?.bookTitle}">
26.<li class="fieldcontain">
27.<span id="bookTitle-label" class="property-label"><g:message code="book.bookTitle.label" default="Book Title" /></span>
28.
29.<span class="property-value" aria-labelledby="bookTitle-label"><g:fieldValue bean="${bookInstance}" field="bookTitle"/></span>
30.
31.</li>
32.</g:if>
33.
34.<g:if test="${bookInstance?.price}">
35.<li class="fieldcontain">
36.<span id="price-label" class="property-label"><g:message code="book.price.label" default="Price" /></span>
37.
38.<span class="property-value" aria-labelledby="price-label"><g:fieldValue bean="${bookInstance}" field="price"/></span>
39.
40.</li>
41.</g:if>
42.
43.<g:if test="${bookInstance?.isbn}">
44.<li class="fieldcontain">
45.<span id="isbn-label" class="property-label"><g:message code="book.isbn.label" default="Isbn" /></span>
46.
47.<span class="property-value" aria-labelledby="isbn-label"><g:fieldValue bean="${bookInstance}" field="isbn"/></span>
48.
49.</li>
50.</g:if>
51.
52.</ol>
53.<g:form>
54.<fieldset class="buttons">
55.<g:hiddenField name="id" value="${bookInstance?.id}" />
56.<g:link class="edit" action="edit" id="${bookInstance?.id}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
57.<g:actionSubmit class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
58.</fieldset>
59.</g:form>
60.</div>
61.</body>
62.</html>
  • 第 55 行到第 56 行:这个?bookInstance 引用后是一个安全的导航运算符。计算该表达式时,如果 bookInstance 为 null,则整个表达式的计算结果为 null,并且不会引发异常。
  • Line 57:<g:action submit>标签生成一个提交按钮,该按钮映射到一个特定的动作,这使得在一个表单中可以有多个提交按钮。可以使用与 HTML 中相同的参数名添加 JavaScript 事件处理程序。

编辑视图

编辑视图如清单 7-30 中的所示。

清单 7-30 。edit.gsp

1.<%@ page import="bookstore.Book" %>
2.<!DOCTYPE html>
3.<html>
4.<head>
5.<meta name="layout" content="main">
6.<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
7.<title><g:message code="default.edit.label" args="[entityName]" /></title>
8.</head>
9.<body>
10.<a href="#edit-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
11.<div class="nav" role="navigation">
12.<ul>
13.<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
14.<li><g:link class="list" action="list"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
15.<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
16.</ul>
17.</div>
18.<div id="edit-book" class="content scaffold-edit" role="main">
19.<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
20.<g:if test="${flash.message}">
21.<div class="message" role="status">${flash.message}</div>
22.</g:if>
23.<g:hasErrors bean="${bookInstance}">
24.<ul class="errors" role="alert">
25.<g:eachError bean="${bookInstance}" var="error">
26.<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
27.</g:eachError>
28.</ul>
29.</g:hasErrors>
30.<g:form method="post" >
31.<g:hiddenField name="id" value="${bookInstance?.id}" />
32.<g:hiddenField name="version" value="${bookInstance?.version}" />
33.<fieldset class="form">
34.<g:render template="form"/>
35.</fieldset>
36.<fieldset class="buttons">
37.<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
38.<g:actionSubmit class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" formnovalidate="" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
39.</fieldset>
40.</g:form>
41.</div>
42.</body>
43.</html>
  • 第 31 到 32 行:在清单 7-24 中,提到了 update()动作中的 id 是从这个编辑视图中提供的。id 来自于< g:隐藏字段>标签,如代码所示。

至此,我们已经完成了 Book 类的静态搭建所生成的所有视图。

H2 控制台

如前所述,Grails 在开发模式下启用了 H2 数据库控制台(在 URI /dbconsole 处),这样就可以从浏览器轻松地查询内存中的数据库。要查看 dbconsole 的运行情况,请浏览到localhost:8080/book store/dbconsole。默认的登录参数应该与 grails-app/conf/data source . groovy 中的默认参数相匹配,如图图 7-29 所示。

9781430259831_Fig07-29.jpg

图 7-29 。H2 的登录界面

你可以从 Datasource.groovy 中获取用户名和密码图 7-30 说明了 H2 控制台 。

9781430259831_Fig07-30.jpg

图 7-30 。H2 控制台

现在,在我们的书店应用中添加一本书,然后在数据库控制台中输入 SELECT * from BOOK 。我们在应用中创建的用户将会出现,如图图 7-31 所示。

9781430259831_Fig07-31.jpg

图 7-31 。查询 H2 表

创建域关系 s

在应用域中,类与类之间有关系。域关系定义了域类如何交互。Grails 支持领域类之间的几种关系。一对多关系是指一个类 Author 拥有 Book 类的多个实例。使用 Grails,你可以用 hasMany 设置定义这样的关系,如清单 7-31 所示。

清单 7-31 。作者和书之间的一对多关系

class Author {
    static hasMany = [books: Book]

String name }

class Book {
    String title
}

在清单 7-31 中有一个单向的一对多。默认情况下,Grails 会用一个连接表来映射这种关系。Grails 会根据 hasMany 设置自动将 java.util.Set 类型的属性注入到域类中。Grails 通过在关系的两端定义 hasMany 并在关系的拥有方定义 belongsTo 来支持多对多关系。

您将创建一个在作者和图书之间具有多对多域关系的应用。您可以在源代码档案的第七章中的 books 项目中找到这个应用的源代码,您可以从 Apress 网站下载。清单 7-32 说明了图书类。

清单 7-32 。创建图书和作者之间的域关系

1.package books
2.
3.class Book {
4.    static belongsTo = Author
5.    static hasMany = [authors:Author]
6.    String title
7.Long isbn
8.String publisher
9.static constraints = {
10.title(blank:false)
11.}
12.
13.String toString() {
14.title
15.}
16.}
  • 第 4 行:这一行通知 Grails Book 类属于它自己的作者。
  • 第 5 行:这一行通知 Grails Book 类有许多 Author 实例。

清单 7-33 说明了作者类。

清单 7-33 。作者类。

1.package books
2.
3.class Author {
4.
5.    static hasMany = [books:Book]
6.    String firstName
7.String lastName
8.static constraints = {
9.firstName(blank:false)
10.lastName(blank:false)
11.}
12.String toString() {
13."$lastName, $firstName"
14.}
15.}
  • 第 5 行:这一行告诉 Grails 一个作者有许多 Book 的实例。

Grails 使用数据库级别的连接表映射多对多关系。关系的拥有方(本应用中的作者)负责保持关系,因为 Grails 使用 Hibernate 作为 ORM 框架,而在 Hibernate 中,多对多关系中只有一方负责管理关系。

创建了域类之间的关系后,您需要做的就是创建 BookController 和 AuthorController,并设置它们的 scaffold 属性。在您创建的 BookController 中,用设置为 Book 的 scaffold 属性替换 index()动作,如清单 7-34 所示。

清单 7-34 。图书管理员

class BookController {

    static scaffold = Book
}

在 AuthorController 中,将 scaffold 属性设置为 Author,如清单 7-35 所示。

清单 7-35 。AuthorController

class AuthorController {

    static scaffold = Author
}

现在您可以运行应用了。脚手架将为您生成应用,并将您指向 URL。图 7-32 显示了应用的欢迎界面。

9781430259831_Fig07-32.jpg

图 7-32 。图书应用的欢迎界面

点击 AuthorController,显示作者列表界面,如图图 7-33 所示。

9781430259831_Fig07-33.jpg

图 7-33 。作者列表屏幕

在图 7-33 中,您可以通过点击新作者链接来创建新作者。图 7-34 显示了这样创建的作者列表。

9781430259831_Fig07-34.jpg

图 7-34 。作者名单

现在你可以通过点击欢迎界面上的图书控制器链接来查看图书列表,如图图 7-35 所示。

9781430259831_Fig07-35.jpg

图 7-35 。图书列表屏幕

您可以通过单击新书链接来添加新书。单击“新建图书”时,将显示“创建图书”屏幕。如果你试图创建一个标题和 Isbn 字段为空的书,你会得到如图图 7-36 所示的确认信息。在清单 7-32 中,您没有为 ISBN 编写任何约束,但是 ISBN 字段仍然有验证,因为 Grails 默认为一些字段提供了约束,ISBN 就是其中之一(如本章表 7-2 中所列)。

9781430259831_Fig07-36.jpg

图 7-36 。验证错误

现在,您可以通过为这些字段提供值来创建图书。图 7-37 显示了创建了两本图书的图书列表屏幕。

9781430259831_Fig07-37.jpg

图 7-37 。图书列表屏幕

现在你可以进入作者列表界面,点击编辑按钮,如图图 7-38 所示。

9781430259831_Fig07-38.jpg

图 7-38 。编辑作者

点击编辑按钮,会看到有两本书可用,如图图 7-39 所示。您可以选择这两本书,然后单击“更新”按钮。

9781430259831_Fig07-39.jpg

图 7-39 。编辑作者屏幕

这样,您可以为您创建的所有作者添加图书。现在,当你进入图书列表界面并点击其中一个书名链接时,显示图书界面会显示该书所有作者的名字,如图图 7-40 所示。

9781430259831_Fig07-40.jpg

图 7-40 。显示图书屏幕

本章到此结束,它概述了 Grails 2 的概况。对于更详细的报道,我推荐 Vishal Layka、Christopher M. Judd、Joseph Faisal Nusairat 和 Jim shiller(2012 年出版)的 Beginning Groovy、Grails和 Griffon ,以及 Jeff 斯科特·布朗和 Graeme Rocher(2012 年出版)的Grails 2权威指南。

摘要

在本章中,您了解了 Grails 是一个快速的 web 开发框架,它结合了 Java 开源、约定、Groovy 动态语言和 Java 平台的优势。您看到了使用 Grails 脚手架来完成大部分工作,开发一个全功能的应用是多么容易。您使用静态搭建作为学习工具,为一个领域类(书籍)生成控制器和视图。然后浏览控制器,看到控制器中的所有工作都在动作中完成了。然后,您学习了负责相应视图的每个动作的代码。最后,您浏览了视图,看到了视图如何利用 Grails 标记库来促进关注点的清晰分离。

1【www.h2database.com/html/main.html】??

2http://wiki.sitemesh.org/display/sitemesh/Home】??

八、使用 Java 和 Scala 玩游戏

哦,把它不好的部分扔掉。和另一半一起更纯洁地生活。

—威廉·莎士比亚

Java EE 继续在一种良性的连续体中发展。受 Spring 等框架的启发,Java EE 引入了注释和依赖注入等特性来处理事务和数据库连接等复杂任务。撰写本文时的最新版本 Java EE 7 进一步加强了 Java 持久性 API (JPA)和基于 REST 的 web 服务的 JAX-RS 的进步,这只是其中的几个例子。Java web 开发所涉及的大部分复杂性将继续通过商业或开放 web 框架的创新在许多方面得到解决。

然而,Play 2 web 框架偏离了常规。play 2不是以 Java EE 为中心,并且不受 Java EE 的约束。它是 Typesafe 堆栈的一部分,提供了 Java EE 堆栈的替代方案。Typesafe 重新定义了以前由 Java EE 定义的现代 Java 应用的层,在这个新的划分中,它构成了 web 层。

Play 是一个开源的 web 应用框架,用 Scala 和 Java 编写,为现代 Web 提供开箱即用的支持。Play 是为现代 web 和移动应用的需求而构建的,利用了 REST、JSON 和 Web Sockets 等技术。

Play 以 JVM 为目标,并通过提供诸如约定优于配置、热代码重载和浏览器中的错误等特性来提高开发人员的工作效率。Play 通过将 HTTP 请求映射到控制器和表示结果的视图模板的路由文件来实现 MVC 架构。Play 2 通过提供对 Scala 编程语言的本地支持,构建在函数范式之上,并提供了一个改编的特定于 Java 的 API,该 API 形成了一个高度反应性的 web 框架。

游戏 2 的特点

Play 2.0 于 2012 年与 Typesafe 堆栈一起发布。Play 2 使用 Scala 作为核心语言构建,而 Play 1 使用 Java 语言并通过插件提供对 Scala 的支持。Play 2.2 发布于 2013 年 9 月。表 8-1 描述了 Play 2 的主要特征。

表 8-1 。重头戏 2 的主要特点

|

特征

|

描述

|
| --- | --- |
| 异步输入输出 | 由于使用 JBoss Netty 1 作为 web 服务器,Play 2 可以异步服务长请求。 |
| 内置 web 服务器 | Play 2 提供了开箱即用的 JBoss Netty web 服务器,但是 Play web 应用也可以打包成 WAR 文件分发到 Java EE 应用服务器。 |
| 依赖性管理 | Play 2 提供 sbt 2 进行依赖管理。 |
| 热重装 | 在基于 Play 2 的应用中,每次新请求到达时,都会检查开发模式下的代码是否有更新,并且任何更改的文件都会自动重新编译;如果有任何错误,错误会直接显示在浏览器中。 |
| 内存数据库 | 像 Grails 一样,Play 2 支持开箱即用的 H2。 |
| 原生 Scala 支持 | Play 2 本身使用 Scala,但完全可以与 Java 互操作。 |
| 对象关系映射(Object Relation Mapping) | Play 2 提供了 Ebean 3 作为 JPA 访问数据库的 ORM 替代。 |
| 无国籍的 | Play 2 是完全 RESTful 的,每个连接都没有 Java EE 会话。 |
| 模板 | Play 2 使用 Scala 作为模板引擎。 |
| 测试框架 | Play 2 为单元测试和功能测试提供了一个内置的测试框架,比如 JUnit 和 Selenium。 4 |
| Web 套接字 | Play 2 实现了开箱即用的 Web 套接字,以实现客户机和服务器之间的双向连接。 |

游戏 2 中的 MVC

Play 2 应用遵循 MVC 架构模式。在 Play 2 应用中,MVC 层在 app 目录中定义,每个层都在一个单独的包中(见图 8-1 )。

9781430259831_Fig08-01.jpg

图 8-1 。行动 2 中的 MVC

图 8-1 中所示的 MVC 架构中的请求流如下:

  1. 路由器收到一个 HTTP 请求。
  2. 路由器找到控制器中定义的动作来处理该请求。
  3. 控制器监听 HTTP 请求,从请求中提取相关数据,并将更改应用于模型。
  4. 控制器呈现模板文件以生成视图。
  5. 然后,action 方法的结果被写成一个 HTTP 响应。

路由器

web 应用的主要入口点是 conf/routes 文件,它定义了应用所需的路由。每个路由都由一个 HTTP 方法和一个与动作方法调用相关联的 URI 模式组成。Conf/routes 是称为路由器的内置组件使用的配置文件,它将每个传入的 HTTP 请求转换为操作调用。

注意HTTP 方法可以是 HTTP 支持的任何有效方法(GET、POST、PUT、DELETE 和 HEAD)。

控制器

在基于 Java EE 的 web 应用中, 控制器是一个扩展 servlet 类型的 Java 类。因为 Play 不是以 Java EE 为中心的,所以 Play 2 中的控制器是 Java 中的一个类,或者是 Scala 中的一个对象,它扩展了控制器类型(Java 和 Scala 中都是如此)。play.api.mvc 包中提供了这种控制器类型。Play 2 中的控制器包含一个名为动作的公共静态方法。动作基本上是一种处理请求参数并产生要发送给客户机的结果的方法。控制器响应请求,处理它们,并调用模型上的更改。

默认情况下,控制器是在源根目录(app 文件夹)下的控制器包中定义的。

注意控制器是一种扩展 play.api.mvc 包中提供的控制器的类型。

模型

模型 是应用操作的信息的特定领域表示。这种表示最常用的对象是 JavaBean。然而,JavaBeans 会导致大量样板代码。Play 2 和 Grails 一样,通过字节码增强为您生成 getters 和 setters,减少了这种样板代码。例如,如果需要将模型对象保存到持久性存储中,那么它们可能包含诸如 JPA 注释之类的持久性工件。

注意即使 Play 2 对 ORM 使用 Ebean,您也可以继续在您的实体上使用 JPA 注释。

视角

在基于 Java EE 的 web 应用中,视图通常是使用 JSP 开发的。也就是说,基于 Java EE 的 web 应用中的视图由 JSP 元素和模板文本组成。由于 Play 不是以 Java EE 为中心的,因此 Play 中的视图由混合了 HTML 和 Scala 代码的模板组成。在 Play 1 中,模板基于 Groovy,但是从 Play 2 开始,模板基于 Scala。使用 Play 2,您可以开发基于 Java 和基于 Scala 的 web 应用,两者中的模板是相同的。

注意在 Play 1 中,模板基于 Groovy,但是从 Play 2 开始,模板基于 Scala。

玩耍入门

要运行 Play 框架,您需要 JDK 6 或更高版本。你可以从这里下载 Play 2:www.playframework.com/download。Play 2 有两种版本:标准版和类型安全激活器。

下载最新的单机版 Play 发行版,将归档文件解压到您选择的位置,并通过在环境变量对话框中添加/编辑 Path 变量来更新 Path 环境变量,使用 Play 安装的路径,如图图 8-2 所示。

9781430259831_Fig08-02.jpg

图 8-2 。添加/编辑路径变量

现在,在命令行工具中输入以下命令,检查是否已经正确设置了播放环境:

> play

如果 Play 安装正确,您应该会在控制台上看到输出,如图 8-3 中的所示。

9781430259831_Fig08-03.jpg

图 8-3 。验证 Play 2 是否安装正确

你也可以通过执行 help 命令获得一些帮助,如图图 8-4 所示。

> play help

9781430259831_Fig08-04.jpg

图 8-4 。游戏 2 中的帮助

现在您可以用 Play 创建您的第一个 Java web 应用了。让我们来玩吧!

Hello World Java 应用与播放

要创建一个新的应用,你只需使用 play 命令行工具,带参数 new,后跟新应用的名称,在这里是 helloworld,如图 8-5 所示。

9781430259831_Fig08-05.jpg

图 8-5 。创建 helloworld 应用

Play 2 会要求你指定你的应用是 Scala 还是 Java 应用,如图 8-6 所示。

9781430259831_Fig08-06.jpg

图 8-6 。指定应用是 Scala 还是 Java 应用

您必须指定 2,因为您想要创建一个 Java 应用。指定 2 为 Java 语言创建源文件和应用结构,如图 8-7 所示。

9781430259831_Fig08-07.jpg

图 8-7 。helloworld 项目的 创建

您可以使用 helloworld 目录中的 run 命令运行应用。为此,进入游戏控制台,如图 8-8 中的所示。

> cd helloworld
>play

9781430259831_Fig08-08.jpg

图 8-8 。进入游戏控制台

现在输入 run。这将启动运行您的应用的服务器。

$ run

控制台上的输出如下所示:

[helloworld] $ run
[info] Updating {file:/E:/ModernJava/play2-workspace/helloworld/}helloworld...
[info] Resolving org.scala-lang#scala-library;2.10.2 ...
[info] Resolving com.typesafe.play#play-java-jdbc_2.10;2.2.0 ...
  [info] Resolving com.typesafe.play#play-jdbc_2.10;2.2.0 ...
  [info] Resolving com.typesafe.play#play_2.10;2.2.0 ...
...............................................
  [info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
--- (Running the application from SBT, auto-reloading is enabled) ---
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
(Server started, use Ctrl+D to stop and go back to the console...)

正如您所看到的,控制台显示它已经启动了应用,并且一个 HTTP 服务器正在侦听端口 9000 上的 HTTP 请求。现在,您可以通过访问 URLlocalhost:9000/向该服务器发送请求。在请求服务器时,显示一个欢迎屏幕,如图图 8-9 所示。

9781430259831_Fig08-09.jpg

图 8-9 。Play 2 框架的默认欢迎页面

图 8-9 说明了默认的 Play 2 欢迎页面。对于初学者来说,应用的默认欢迎页面是一个很好的信息来源,建议您阅读一下。

run 命令在 helloworld 目录中创建应用的结构。其结构如图图 8-10 所示。

9781430259831_Fig08-10.jpg

图 8-10 。helloworld 应用的目录结构

图 8-10 中的各个文件夹描述如下:

  • app:这是所有服务器端源文件的根,比如 Java 和 Scala 源代码、模板和编译资产的源代码。在创建时,只创建两个子文件夹:控制器和视图,分别用于 MVC 架构模式的控制器和视图组件。您可以为 MVC 的模型组件添加目录 app/models。还有一个名为 app/assets 的可选目录,用于编译的资源,如 LESS 5 源和 CoffeeScript 6 源。

  • conf: The conf directory contains the application’s configuration files meant to configure the application itself, external services, and so on. There are two main configuration files:

    application.conf:应用的主配置文件,包含标准配置参数

    路线:路线定义文件

  • 项目:项目文件夹包含配置 Scala 构建工具 SBT 所需的所有文件。 7

  • public:这个目录包含图像、CSS 样式表和 JavaScript 文件的三个标准子目录。

注意存储在公共目录中的资源是静态资产,将由 web 服务器直接提供服务。

  • target: The target directory contains everything generated by the build system such as the following:

    classes:包含所有编译过的类(来自 Java 和 Scala 源代码)。

    classes_managed:这仅包含由框架管理的类(例如由路由器或模板系统生成的类)。

    resource_managed:这包含生成的资源,通常是编译的资产,如 LESS CSS 和 CoffeeScript 编译结果。

    src_managed:这包含生成的源代码,比如模板系统生成的 Scala 源代码。

  • test:最后一个文件夹将包含所有的测试文件以及框架提供的一些样本。

现在您已经看到了应用的目录结构,您将了解在测试 URL:localhost:9000时默认的欢迎页面是如何显示的。

conf/routes 文件中的每一行都是定义如何使用 HTTP 访问服务器组件的路由。如果您在 conf/routes 文件中看到生成的 routes 文件,您将看到第一条路由:

GET        /       controllers.Application.index()

这条路线由三部分组成:

  • GET:这是包含请求中使用的 HTTP 方法的路由(行)的第一部分。
  • /:这是路由中包含相对路径的第二部分。
  • 控制器。Application.index():这是路由中包含要调用的动作的第三部分。

路由中的这三个部分通知 Play 2,当 web 服务器收到对/ path 的 GET 请求时,它必须调用控制器。Application.index(),它调用驻留在控制器包中的应用类中的 index 方法。

现在你将看到控制器。Application.index 方法如下所示。为此,您需要打开 app/controllers/application . Java 源文件。这个文件在清单 8-1 中有说明。

清单 8-1 。应用控制器

 1.    package controllers;
 2.
 3.    import play.*;
 4.    import play.mvc.*;
 5.
 6.    import views.html.*;
 7.
 8.    public class Application extends Controller {
 9.
10.      public static Result index() {
11.        return ok(index.render("Your new application is ready."));
12.      }
13.
14.    }
  • 第 8 行:应用控制器类扩展 play.mvc.Controller。
  • 第 10 行:public static index()动作返回结果。所有操作方法都返回一个结果。结果表示要发送回浏览器的 HTTP 响应。
  • 第 11 行:这里,动作返回一个 200 OK 响应,带有 HTML 响应体。HTML 内容由模板提供。一个动作总是返回一个 HTTP 响应,这在 Play 2 中由结果类型表示。结果类型必须是有效的 HTTP 响应,因此它必须包含有效的 HTTP 状态代码。确定将其设置为 200。render()方法引用了 Play 2 中的模板文件。

模板在 app/views/index.scala.html 源文件中定义。这个文件在清单 8-2 中有说明。

清单 8-2 。index.scala.html

1.    @(message: String)
2.
3.    @main("Welcome to Play") {
4.
5.        @play20.welcome(message, style = "Java")
6.
7.    }
  • 第 1 行:Scala 语句以特殊的@字符开始。第一行定义了函数签名。这里它只接受一个字符串参数。模板就像一个函数,因此它需要参数,这些参数在模板文件的顶部声明。
  • 第 3 行:这一行用一个字符串参数调用一个名为 main 的函数。
  • 第 3 行到第 7 行:这些行组成了主功能块。
  • Line 5 : Line 5 使用了 Play 2 提供的一个叫 welcome 的功能。该函数呈现默认的欢迎 HTML 页面,它位于名为 main.scala.html 的文件中,该文件也位于 apps/views 文件夹中。
  • 第 5 行:欢迎函数有一个额外的参数 style,通知模板这是一个基于 Java 的应用,然后这个 style 参数被欢迎模板用来显示文档的链接。

为 Java 配置 Eclipse

Play 2 为您提供了使用 Eclipse IDE 的可能性。为此,您需要让 Play 2 生成 Eclipse 项目配置。你可以通过调用游戏控制台中的 Eclipse 来实现,如图 8-11 所示。

9781430259831_Fig08-11.jpg

图 8-11 。为 Eclipse 生成项目

现在你可以启动 Eclipse,如图图 8-11 所示,并将项目导入其中,如图图 8-12 所示。

9781430259831_Fig08-12.jpg

图 8-12 。选择工作区

导入项目时,选择文件image导入,在工作区选择常规image已有项目,点击下一步,如图图 8-13 所示。

9781430259831_Fig08-13.jpg

图 8-13 。导入项目

现在浏览你的文件系统,选择项目文件夹 helloworld,点击 OK,然后点击 Finish,如图 8-14 所示。

9781430259831_Fig08-14.jpg

图 8-14 。选择根目录

配置 Eclipse 项目所需的所有文件都会生成。您看到了如何创建一个项目并将其导入到您的开发环境中。现在您将修改应用。在 Application.java,改变 index 动作中响应的内容,如清单 8-3 所示。

清单 8-3 。修改指标动作

public static Result index() {
  return ok("Hello world");
}

索引动作现在将响应“Hello world”,如图 8-15 所示。

9781430259831_Fig08-15.jpg

图 8-15 。“你好,世界”

Play 2 在 play-2.2.0\samples\java\的 samples 文件夹中提供了一些示例应用。你可以运行 helloworld 应用,如图 8-16 所示。

9781430259831_Fig08-16.jpg

图 8-16 。Play 2 提供的 helloworld 示例应用

点击【提交查询】,根据选择显示用户名称,如图图 8-17 所示。

9781430259831_Fig08-17.jpg

图 8-17 。运行示例 helloworld 应用

您可以自己检查代码并改进应用。

Helloworld Scala 应用与 Play 2

如前所述,Play 2 允许您创建基于 Java 和基于 Scala 的 web 应用。生成基于 Scala 的应用的过程与生成 Java 应用是一样的。你可以创建一个 helloworld-scala 应用,如图 8-18 所示。

9781430259831_Fig08-18.jpg

图 8-18 。创建 helloworld-scala 应用

现在你可以从 Play 控制台使用 run 命令运行 helloworld-scala 项目,如图 8-19 所示。

9781430259831_Fig08-19.jpg

图 8-19 。为 helloworld-scala 应用播放控制台

现在你将看到 Play 2 为 helloworld-scala 生成的控制器(见清单 8-4 )。你可以在 hello world-Scala \ app \ controllers 中找到控制器。

清单 8-4 。Scala 中的应用控制器

 1.    package controllers
 2.
 3.    import play.api._
 4.    import play.api.mvc._
 5.
 6.    object Application extends Controller {
 7.
 8.      def index = Action {
 9.        Ok(views.html.index("Your new application is ready."))
10.      }
11.
12.    }
  • 第 6 行:正如你在第 6 行看到的,在 Java 中控制器是一个类,但是在 Scala 中控制器是一个对象。
  • 第 8 行:正如你在第 8 行看到的,在 Java 中 action 是一个静态方法,但是在 Scala 中 action 是一个函数(一个对象的方法)。
  • 第 9 行:如果与 Java 控制器进行比较,返回类型和关键字是缺失的。
  • 第 9 行 : Scala 使用了一种叫做 Action 的结构,这是一个代码执行器的块。

现在你已经看到了 Scala 中的控制器,它在语法上不同于 Java 控制器,是时候看看 helloworld-scala 中的模板了(见清单 8-5 ,你可以在 helloworld-scala\app\views 中找到它。

清单 8-5 。helloworld-scala 中的模板

1.    @(message: String)
2.
3.    @main("Welcome to Play") {
4.
5.        @play20.welcome(message)
6.
7.    }

正如您所注意到的,helloworld 和 helloworld-scala 中的模板是相同的,除了第 5 行。Scala 版本没有初始化样式参数,因为它的默认值是 Scala。

为 Scala 配置 Eclipse】

您可以将 Scala IDE 用于基于 Scala 的应用。Scala IDE 是一个 Eclipse 插件,你可以通过选择 Help image Install New Software 来安装这个插件。在“工作方式”字段中,输入插件的路径(scala-ide.org/download/current.html),如图 8-20 中所示。你还可以在 http://scala-ide.org/documentation.html找到为 Scala 配置 Eclipse 的详细说明。

9781430259831_Fig08-20.jpg

图 8-20 。为 Scala IDE 安装 Eclipse 插件

要导入项目,只需重复前面在 helloworld Java 应用中为 Eclipse 生成项目配置时执行的相同步骤。

现在您可以修改应用控制器中的 index 动作来显示“Hello world”,如清单 8-6 所示。

清单 8-6 。修改索引动作以显示“Hello world”

def index = Action {
    Ok("Hello world")
  }

一个基本的 CRUD Play 2 Java 应用

在本节中,您将学习编写一个简单的 CRUD 应用,它允许您创建、查看、编辑和删除书籍。对于这些操作,您需要动作和 URL 来调用这些动作。这个应用的代码可以在 Apress 网站的可下载文档中找到。

定义路线

第一步是在 conf/routes 文件中为这些操作定义路线,如清单 8-7 所示。

清单 8-7 。编辑会议/路线文件

1.    # Home page
2.    GET     /                    controllers.Application.index()
3.
4.    # Books
5.    GET     /books               controllers.Application.books()
6.    POST    /books               controllers.Application.newBook()
7.    POST    /books/:id/delete    controllers.Application.deleteBook(id: Long)
  • 第 5 行:在第 5 行中,您创建了一个列出所有书籍的路径。
  • 第 6 行:在第 6 行中,您创建了一个处理图书创建的路由。
  • 第 7 行:在第 7 行中,您创建了一个处理删除的路由。处理图书删除的路由在 URL 路径中定义了一个变量参数 ID。然后,该值被传递给 deleteBook 操作。

现在如果你刷新你的浏览器,你会看到 Play 2 不能编译你的 routes 文件,如图 8-21 所示。

9781430259831_Fig08-21.jpg

图 8-21 。路线文件编译错误

Play 无法编译您的路线文件,因为它引用了不存在的动作。下一步是将这些操作添加到 Application.java 文件中。

创建控制器和动作

在本节中,您将创建动作,如清单 8-8 中的所示。

清单 8-8 。图书应用中的应用控制器

 1.    public class Application extends Controller {
 2.
 3.      public static Result index() {
 4.        return ok(index.render("Your new application is ready."));
 5.      }
 6.
 7.      public static Result books() {
 8.        return TODO;
 9.      }
10.
11.      public static Result newBook() {
12.        return TODO;
13.      }
14.
15.      public static Result deleteBook(Long id) {
16.        return TODO;
17.      }
18.
19.    }
  • 第 7、11 和 15 行:这些行显示了在清单 8-7 中的 routes 文件中指定的动作。
  • 第 8、12 和 16 行:使用内置结果 TODO,返回“未实现”响应 503。这个结果告诉 Play 2,稍后将提供动作实现。当您通过localhost:9000/books访问该应用时,您会看到在图 8-22 中显示的结果。

9781430259831_Fig08-22.jpg

图 8-22 。Play 2 中的内置 TODO 结果

创建模型

下一步是定义可以存储在关系数据库中的模型书。为此,在 app/models/Book.java 文件中创建一个类,如清单 8-9 所示。

清单 8-9 。Book.java

 1.    package models;
 2.    import java.util.*;
 3.    public class Book {
 4.      public Long id;
 5.      public String label;
 6.      public static List<Book> all() {
 7.        return new ArrayList<Book>();
 8.      }
 9.      public static void create(Book book) {
10.      }
11.      public static void delete(Long id) {
12.      }
13.    }
  • 第 6 行到第 12 行:您创建静态方法来管理 Book 上的 CRUD 操作。稍后,您将实现这些操作来将书籍存储在关系数据库中。

表单和视图模板

表单对象封装了一个 HTML 表单定义,包括验证约束。要为 Book 类创建一个表单,需要将以下内容添加到应用控制器中:

static  Form<Book>  bookForm = Form.form(Book.class);

前面的代码用于定义包装现有类的 play.data.Form。书单类型为表格

您可以使用 JSR-303 注释向图书类型添加约束。清单 8-10 说明了如何使标签字段成为必填字段。

清单 8-10 。添加验证约束

package models;

import java.util.*;

import play.data.validation.Constraints.*;

public class Book {

  public Long id;

  @Required
  public String label;

  ...

现在您需要修改视图模板来显示创建图书和列出所有图书的屏幕。

模板被编译成标准的 Scala 函数。如果你创建了一个 views/Application/index . scala . html 模板文件,Scala 会生成一个 views.html.Application.index 类,这个类有一个 render()方法。 清单 8-11 显示了一个简单的模板。

清单 8-11 。简单模板

@(books: List[Book])
 <ul>
@for(book <- books) {
  <li>@book.getTitle()</li>
}
</ul>

然后,您可以从任何 Java 代码中调用它,就像您通常调用类的方法一样。

Content html = views.html.Application.index.render(books);

清单 8-12 展示了 index.scala.html 模板的代码,你可以在 app/views 文件夹中找到。

清单 8-12 。index.scala.html

 1.    @(books: List[Book], bookForm: Form[Book])
 2.
 3.    @import helper._
 4.
 5.    @main("books") {
 6.
 7.    <h1>@books.size() book(s)</h1>
8.
 9.    <ul>
10.            @for(book <- books) {
11.    <li>
12.                    @book.label
13.
14.                    @form(routes.Application.deleteBook(book.id)) {
15.    <input type="submit" value="Delete">
16.                    }
17.    </li>
18.            }
19.    </ul>
20.
21.    <h2>Add a new book</h2>
22.
23.        @form(routes.Application.newBook()) {
24.
25.            @inputText(bookForm("label"))
26.
27.    <input type="submit" value="Create">
28.
29.        }
30.
31.    }

在清单 8-12 中,模板有两个参数。@content 是一个参数,表示要写入文档主体的有效 HTML。内容的类型是 HTML,它是 Scala 结构,被模板调用时可以写成 Html。输入的助手。_ 提供了表单创建助手——也就是说,表单函数创建填充了动作和方法属性的 HTML <表单>,输入文本函数创建作为表单字段给出的 HTML 输入。

注意play . data 包包含几个助手来处理 HTTP 表单数据提交和验证。

现在你可以实现 books()动作,,如清单 8-13 所示。

清单 8-13 。实施图书行动

public static Result books() {
    return ok(
    views.html.index.render(Book.all(), bookForm)
  );
}

books()操作呈现一个用 HTML 填充的 200 OK 结果,该结果由用图书列表和 bookForm 表单调用的 index.scala.html 模板呈现。

你现在可以尝试在你的浏览器中访问localhost:9000/books(参见图 8-23 )。

9781430259831_Fig08-23.jpg

图 8-23 。展示书籍创作形式

如果您提交图书创建表单,您仍然会看到 TODO 页面。您需要实现 newBook()操作才能创建图书。清单 8-14 展示了 newBook()动作的实现。

清单 8-14 。newBook()动作的实现

 1.    public static Result newBook() {
 2.        Form<Book> filledForm = bookForm.bindFromRequest();
 3.      if(filledForm.hasErrors()) {
 4.        return badRequest(
 5.          views.html.index.render(Book.all(), filledForm)
 6.        );
 7.      } else {
 8.        Book.create(filledForm.get());
 9.        return redirect(routes.Application.books());
10.      }
11.    }
  • Line 2 :我们使用 bindFromRequest 来创建一个新的表单,其中填充了请求数据。
  • 第 3 行到第 7 行:如果表单中有任何错误,我们重新显示它(这里我们使用 400“错误请求”而不是 200“好”)。
  • 第 7 行到第 10 行:如果没有错误,我们创建图书,然后重定向到图书列表。

访问数据库

Play 2 支持一个现成的对象关系映射(ORM)Ebean 来填补领域模型和关系数据库之间的空白,如图 8-24 所示。为 Java 提供 ORM 的其他流行选项是 Hibernate 和 Java Persistence API,后者是由 Oracle 标准化的。

9781430259831_Fig08-24.jpg

图 8-24 。使用 Ebean 查询数据库

像任何其他 ORM 一样,Ebean 旨在通过实现基于模型属性的查找器,在处理关系数据库时方便模型的使用。您将使用 Play 2 附带的轻量级 DBMS H2。Play 的配置包含使用 H2 和 Ebean 的默认设置,但它们被注释掉了。因此,打开应用目录中的文件 conf/application.conf,找到并取消注释以下行,以便在应用中启用数据库:

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"

您将使用 Ebean 来查询数据库。因此,您还必须在 application.conf 文件中启用它:

ebean.default="models.*"

这将创建一个连接到默认数据源的 Ebean 服务器,管理 models 包中的所有实体。

现在是时候将 Book 类转换成有效的 Ebean 实体了。你可以通过让 Book 类扩展 play.db.ebean.Model 超类来访问 play 的内置 ebean 助手,如清单 8-15 所示。

清单 8-15 。将 Book 类转换为有效的 Ebean 实体

 1.    package models;
 2.
 3.    import java.util.*;
 4.    import play.db.ebean.*;
 5.    import play.data.validation.Constraints.*;
 6.
 7.    import javax.persistence.*;
 8.
 9.    @Entity
10.    public class Book extends Model {
11.      @Id
12.      public Long id;
13.      @Required
14.      public String label;
15.
16.    public static Finder<Long,Book> find = new Finder(
17.        Long.class, Book.class
18.      );
19.
20.
21.    public static List<Book> all() {
22.      return find.all();
23.    }
24.
25.    public static void create(Book book) {
26.      book.save();
27.    }
28.    public static void update(Long id, Book book) {
29.        book.update(id);
30.    }
31.
32.    public static void delete(Long id) {
33.      find.ref(id).delete();
34.    }
35.
36.
37.    }
  • 第 13 行:第 13 行增加了持久性注释。
  • 第 16 行到第 18 行:这几行创建了一个名为 find 的查找助手来启动查询。
  • 第 21 行到第 34 行:这些行执行 CRUD 操作。例如,当您调用 create()操作时, Ebean 将 save()方法调用转换成一个或多个 SQL INSERT 语句,这些语句使用 SQL 在数据库表中存储一条新记录。

现在当你测试网址 localhost:9000/ 时,你会看到如图图 8-25 所示的页面。

9781430259831_Fig08-25.jpg

图 8-25 。在默认数据库上应用脚本

当您定义连接到 H2 数据库的数据库连接时,它不会自动创建模式,换句话说,就是表和列定义。为了创建模式,Play 2 生成一个 SQL 脚本并要求运行它:“现在应用这个脚本。”一旦你点击“立即应用这个脚本”按钮运行这个脚本,你就可以测试 URLlocalhost:9000/books和新书,如图图 8-26 所示。

9781430259831_Fig08-26.jpg

图 8-26 。添加图书

删除书籍

既然您可以创建图书,那么您需要能够删除它们。为此你需要实现 deleteBook()动作,如清单 8-16 所示。

清单 8-16 。执行删除动作

1.    public static Result deleteBook(Long id) {
2.      Book.delete(id);
3.      return redirect(routes.Application.books());
4.    }

本章和本书到此结束。一章不足以涵盖 Play 2 框架(或任何框架)的所有特性。与此同时,框架和 web 架构正朝着实时处理的方向快速发展,集成了更多的并发实时数据,因此 web 框架需要支持完整的异步 HTTP 编程模型,并需要通过 Web 套接字使用事件模型。Play 2 提供了异步 HTTP API,而不是标准的 Servlet API,从而脱离了标准的 JEE 约定。Play 2.0 采用基于角色的模型通过 Akka 处理高度并发的系统。Akka 是 Java 和 Scala 中基于角色模型的最佳实现。Play 2.0 提供了原生的 Akka 支持,使得编写高度分布式的系统成为可能。

这本书的目的是向您展示 Java 语言在 90 年代末引入 Web 开发的浪潮仅仅是今天正在被超越的类型安全时代的开始,Java 的流行应该归功于 Java 虚拟机。这是一部相当不错的机器。

摘要

在本章中,您对 Play 2 框架进行了高度概括。您为 Java 和 Scala 开发了一个 helloworld web 应用,并且学习了所有 Play 2 web 应用共有的基础知识:Java 和 Scala 控制器、动作,甚至一些视图。此外,您还研究了 Java 和 Scala 控制器之间的差异。您看到了 Play 2 提供的最佳特性,例如动态编译和浏览器上显示的错误。然后您开发了一个简单的基于 Java 的 CRUD web 应用。

1http://netty.io/】??

2【www.scala-sbt.org/】??

3【www.avaje.org/】??

4【www.seleniumhq.org/】??

5http://lesscss.org/】??

6http://jashkenas.github.io/coffee-script/】??

7【www.scala-sbt.org/】??

九、附录 A:Java 简介

处理复杂性的基本方法之一是抽象,它几乎应用于每一个学科。例如,抽象艺术是不表现或模仿外部现实或自然物体的艺术。再比如,有些词是抽象的,只存在于头脑中,像真理正义。在 Java 中,抽象允许开发人员通过使用类层次结构将复杂的系统分成可管理的部分,这些类层次结构突出了对象的基本属性和行为,将它与其他对象区分开来。

类和对象

一个是面向对象(OO)程序的基本构件。每个类通常代表一个现实世界的对象。类是一个模板,用于表示和创建对象的类别,从而通过定义表示抽象的对象的属性和行为来对抽象进行建模。对象的属性由字段定义,字段是可以存储表示该属性的值的变量。对象的行为是由方法定义的。一个类的字段和方法被称为它的成员。Java 中的类定义由成员声明(变量声明和方法声明)组成,并以 class 关键字开始,如清单 A-1 中的所示。

上市 A-1 。Java 类

class ClassA {
// members declarations
}

一个对象是一个类的实例。对象是以类为蓝本构建的,是类所代表的抽象的具体实例。必须先创建对象,然后才能在程序中使用它。从一个类创建对象的过程被称为实例化。当一个类被实例化时,返回一个引用值,该值表示所创建的对象。参考值表示特定的对象。对象引用(或者简单地说, reference )是一个变量,它可以存储一个引用值并提供一个对象的句柄。清单 A-2 中的代码创建了一个 ClassA 的对象。这个对象的引用值存储在变量 var1 中。

列表 A-2 。创建对象

ClassA  var1 = new ClassA();

创建对象的过程包括声明一个引用变量来存储对象的引用值,然后使用 new 关键字创建对象,接着通过调用构造函数来初始化对象。构造函数的概念将在后面的附录中解释。清单 A-3 将清单 A-2 分开,将声明和创建显示为单独的步骤。

列表 A-3 。在单独的步骤中进行声明和创建

1.    ClassA  var1 ;
2.    var1 = new ClassA();
  • 第 1 行声明了变量 var1。引用变量 var1 现在可以用来操作引用值存储在引用变量中的对象。
  • 第 2 行使用 new 关键字创建对象,并通过调用构造函数 ClassA()进行初始化。

变量

在 Java 中,变量存储原始数据类型的值和对象的引用值。

列出 A-4 说明了可以存储原始值的变量声明。

上市 A-4 。变量声明

int a, b, c;  // a, b and c are integer variables.
boolean flag; // flag is a boolean variable.
int i = 10,   // i is an int variable with initial value 10

存储对象参考值的变量称为参考变量。引用变量指定引用的类型,可以是类、数组或接口。清单 A-5 说明了引用变量的声明。

列表 A-5 。引用变量声明

ClassA  var1 ; // Variable var1 can reference objects of class ClassA.

清单 A-5 中的声明没有创建 ClassA 的任何对象;它只是创建一个变量来存储 ClassA 对象的引用。

实例成员

创建的每个对象(如清单 A-2 中的所示)都有自己在类中定义的字段的副本。对象的字段被称为实例变量。对象中实例变量的值构成了对象的状态。对象的方法定义了它的行为。这些方法被称为实例方法。属于对象的实例变量和实例方法被称为实例成员 (见清单 A-6 )以区别于只属于类的静态成员。

列表 A-6 。实例成员

1.    class ClassA{
2.    // instance Members
3.    int i ; // instance variable
4.    void methodA(){// instance method
5.    // do something
6.    }
7.    }
  • 在第 3 行,I 是 int 类型的实例变量,int 是 I 的原始数据类型。
  • 在第 4 行,methodA(){}是一个实例方法。

静态成员

用关键字 Static 声明的静态成员是只属于类而不属于类的任何特定对象的成员。一个类可以有静态变量和静态方法。静态变量在运行时类被加载时被初始化。类似地,一个类可以有静态方法,这些方法属于该类,而不属于该类的任何特定对象,如清单中的 A-7 所示。

上市 A-7 。静态成员

1.    class ClassA{
2.    static int i ;
3.    static void methodA(){
4.    // do something
5.    }
6.    }

与实例成员不同,类中的静态成员可以使用类名来访问,如下所示:

ClassA.i          // accessing static variable in Line 2 of Listing A-7

ClassA.methodA(); // accessing static method in Line 3 of Listing A-7

尽管类中的静态成员可以通过对象引用来访问,但这样做被认为是不好的做法。

方法重载

每个方法都有一个名字和一个形参表。方法的名称及其形参表与形参表中形参的类型和顺序一起构成了方法的签名。只要方法签名不同,一个以上的方法可以有相同的方法名。这种方法名称相同,签名不同的方法称为重载方法,这种现象称为方法重载 。因此,重载方法是具有相同名称但参数列表不同的方法。清单 A-8 显示了方法 methodA 的五个实现。

列表 A-8 。重载方法 a()

1.    void methodA{(int a, double b) }
2.    int methodA(int a) { return a; }
3.    int methodA() { return 1; }
4.    long methodA(double a, int b) { return b; }
5.    long methodA(int c, double d) { return a; } //  Not ok.
  • 方法的前四个实现被正确重载,每次都有不同的参数列表,因此也有不同的签名。
  • 第 5 行的声明与第 1 行的声明具有相同的签名方法 A(int,double)。仅仅改变返回类型不足以重载一个方法;声明中的参数列表必须不同。

注意只有在同一个类中声明的方法和被该类继承的方法才能被重载。

数组

一个数组是一个数据结构,由固定数量的基本上相同数据类型的数据元素组成。数组中的任何元素都可以使用索引来访问。第一个元素总是在索引 0 处,最后一个元素在索引 n-1 处,其中 n 是数组中长度字段的值。在 Java 中,数组是这样的对象,数组中的所有元素都可以是特定的原始数据类型或特定的引用类型。清单 A-9 声明了引用数组对象的引用。

列表 A-9 。数组声明

int [] intArray;
ClassA[]  classAArray ;

清单 A-9 中的两个声明将 intArray 和 classAArray 声明为引用变量,可以引用 int 值数组和 ClassA 对象数组。使用 new 运算符,可以为特定类型的固定数量的元素构造数组。给定前面的数组声明,数组可以按如下方式构造:

intArray = new int[10];      // array for 10 integers
classAArray = new ClassA[5]; // array of 5 objects of ClassA

构造函数

当使用 new 运算符创建对象时,调用构造函数来设置对象的初始状态。构造函数声明由可访问性修饰符组成,后跟带有以下声明的参数列表:

  • 构造函数头中不允许除可访问性修饰符之外的修饰符。
  • 构造函数不能返回值,因此,不要在构造函数头中指定返回类型,甚至是 void。
  • 构造函数名必须与类名相同。

当在一个类中没有指定构造函数时,则由编译器为该类生成隐式默认构造函数,即没有任何参数的隐式构造函数,该编译器包括对超类的构造函数的调用。编译器将这个调用插入到超类的构造函数中,以确保对象的继承状态被初始化。列出 A-10 说明了对隐式默认构造函数的调用。

上市 A-10 。隐式默认构造函数

class ClassA {
int i;
}
class ClassB {

ClassA var1 = new ClassA(); // (1) Call to implicit default constructor.
}

在清单中,当在 ClassB 中创建 ClassA 对象时,调用下面的隐式默认构造函数:

ClassA() { super(); }

在清单 A-11 的中,ClassA 类在第 4 行提供了一个显式的默认构造函数。

列表 A-11 。显式默认构造函数

1.    class ClassA {
2.    int i ;
3.    // Explicit Default Constructor:
4.    ClassA() {
5.    i = 1;
6.    }
7.
8.    }
9.    class ClassB {
10.    // ...
11.    ClassA var1 = new ClassA(); //  Call of explicit default constructor.
12.    }

显式默认构造函数确保任何使用对象创建表达式 new ClassA()创建的对象(如 ClassB 所示)都将其字段 I 初始化为 1。如果一个类定义了任何显式构造函数,那么编译器不会通过调用超类的构造函数来生成隐式默认构造函数,因此对象的状态不会被设置。在这种情况下,需要提供默认构造函数的实现。在清单 A-12 的中,ClassA 类在第 4 行只提供了一个非默认的构造函数。当使用 new 操作符创建 ClassA 类的对象时,在第 8 行调用它。任何调用默认构造函数的尝试都将被标记为编译时错误,如第 11 行所示。

列表 A-12 。非默认构造函数

1.    class ClassA {
2.    int i;
3.    // Only non-default Constructor:
4.    ClassA(int i) {
5.    this.i = i;
6.    }
7.    }
8.    class ClassB {
9.    // ...
10.    ClassA var1 = new ClassA(2);
11.    //ClassA var2 = new ClassA(); // Compile-time error.
12.    }

像方法一样,构造函数可以重载,因为所有构造函数的名称都被限制为与类名相同,所以只有当这些构造函数的参数列表不同时,它们的签名才能不同。在清单 A-13 的中,ClassA 在第 4 行提供了默认构造函数的显式实现,在第 8 行提供了非默认构造函数。当在第 14 行创建 ClassA 类的对象时,调用非默认构造函数,在第 15 行调用默认构造函数。

列表 A-13 。默认和非默认构造函数

1.    class ClassA {
2.    int i;
3.    // Explicit Default Constructor:
4.    ClassA() {
5.    i = 3;
6.    }
7.    // Non-default Constructor:
8.    ClassA(int i) {
9.    this.i = i;
10.    }
11.    }
12.    class ClassB {
13.    // ...
14.    ClassA var1 = new ClassA(4);
15.    ClassA var2 = new ClassA();
16.    }

封装

封装是通过防止数据和代码被外部代码(即来自类外的代码)随机访问和操纵来实现数据隐藏的技术。在实现方面,封装是通过将类中的字段私有并通过公共方法提供对这些字段的访问来实现的。如果一个字段被声明为私有,那么该类之外的任何人都不能访问它。清单 A-14 展示了一个封装的 Book 类。

列表 A-14 。包装

public class Book {
       private String title ;
       public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
 }

继承

继承是面向对象编程的基本原则之一。在 Java 中,默认情况下,所有类都扩展 java.lang.Object 类。一个类可以使用 extends 关键字扩展另一个类。Java 通过实现继承支持单一继承,其中一个类通过类扩展从另一个类继承状态和行为。Java 也以两种方式支持多重继承。

  • 通过实现,类可以从一个或多个接口继承行为。
  • 一个接口可以通过扩展从一个或多个接口继承行为。

图 A-1 展示了一个 UML 1 类图,描述了一个类 ClassA 和一个子类 ClassB 之间的父子关系。ClassB 是 ClassA 的子类。请注意,带箭头的线条用于描述泛化,换句话说,就是父子关系。

9781430259831_AppA-01.jpg

图 A-1 。父子关系。ClassB 扩展了 ClassA

清单 A-15 展示了如何用代码实现图 A-1 。它在第 26 行使用关键字 extends 说明了实现继承。

列表 A-15 。实现继承

1.    package apress.appendix_A
2.
3.    public class ClassA {
4.
5.        // Instance methods
6.        public void method1() {
7.            System.out.println(" classA - method1");
8.
9.        }
10.
11.        private void method2() {
12.            System.out.println(" classA - method2");
13.
14.        }
15.
16.        // Static methods
17.        public static void method3() {
18.            System.out.println(" classA - method3");
19.
20.        }
21.
22.    }
23.
24.    package apress.appendix_A;
25.
26.    public class ClassB extends ClassA {
27.
28.
29.    }

清单 A-16 是一个驱动类,用于测试清单 A-15 中的继承。

列表 A-16 。测试继承

package apress.appendix_A;

public class Test {

    public static void main(String[] args) {
        ClassB var1 = new ClassB();

        var1.method1();
        // var1.method2(); // private method not Inherited

        ClassB.method3();// static method

    }

}

清单 A-16 说明了即使 ClassB 中没有定义方法,ClassA 的方法在 ClassB 中也是可用的,并且可以在实际对象类型为 ClassB 的引用变量上调用。第 9 行显示私有方法没有被继承。

以下是输出:

classA - method1
classA - method3

构造函数链接

当子类通过调用它的一个构造函数被实例化时,该构造函数首先调用超类的无参数构造函数。在超类中,构造函数也调用其超类的构造函数。这个过程不断重复,直到到达 java.lang.Object 类的构造函数。换句话说,当你创建一个子类的对象时,它的所有超类也被实例化。清单 A-17 说明了这个构造函数链接

列表 A-17 。构造函数链接

package apress.appendix_A;

public class ClassA {

    public ClassA() {

        System.out.println("Class A no-arg constructor");

    }

    public ClassA(String title) {
        System.out.println("Class A constructor");

    }

}

package apress.appendix_A;

public class ClassB extends ClassA {

    public ClassB(String title){
        System.out.println("Class B constructor ");

    }

}

package apress.appendix_A;

public class Test {

    /**
     * @param args
     */
    public static void main(String[] args) {
        ClassB var1 = new ClassB("classB");

    }

}

以下是输出:

Class A no-arg constructor
Class B constructor

输出证明了子类的构造函数调用了基类的无参数构造函数。Java 编译器将 ClassB 的构造函数更改为:

public ClassB(String title) {
 super();
 System.out.println("Class B constructor ");
}

关键字 super 表示当前对象的直接超类的一个实例。因为 super 是从子类的实例中调用的,所以 super 表示 ClassA 的实例。可以使用 super 关键字从子类的构造函数中显式调用父类的构造函数,但 super 必须是构造函数中的第一条语句。如果希望调用超类中的另一个构造函数,使用 super 关键字会很方便。

public ClassB(String title) {
 super(title);
 System.out.println("Class B constructor ");
}

多态性

多态性是面向对象编程最重要的原则之一,也是面向对象编程的核心。多态性指的是 Java 中的一个对象可以有多种形式。为了理解多态在 Java 中是如何工作的,让我们看一个扩展另一个类的例子(见图 A-2 )。

9781430259831_AppA-02.jpg

图 A-2 。扩展一个类

图 A-2 显示了一个超类-子类的关系。此关系允许您将对象分配给类型不同于对象类型的引用变量,如下所示:

ClassA var1 = new ClassB();

这将 ClassB 类型的对象分配给引用类型为 ClassA 的引用变量 var1。这种赋值在编译时和运行时有不同的含义。在编译时,由于 var1 的类型是 ClassA,编译器将不允许调用 var1 上不在 ClassA 中的方法,即使该方法在 ClassB 中。清单 A-18 显示了图 A-2 的代码实现。

列表 A-18 。遗产

package apress.appendix_A;
public class ClassA {

    public void methodA() {
        System.out.println("methodA() in ClassA");
    }

}

package apress.appendix_A;

public class ClassB extends ClassA {

    public void methodB() {
        System.out.println("methodB() in ClassB");
    }

}

清单 A-19 说明了该测试。

清单 A-19 。清单 A-18 的驱动程序

package apress.appendix_A;

public class Test {
    public static void main(String[] args) {

        ClassA var1 = new ClassB();
        // var1.methodB(); uncommenting this code will result in compile time
        // error
        var1.methodA();

    }
}

在清单 A-19 的第 2 行,即使 methodB()是 ClassB,也不可能对 var1 调用 methodB(),因为 var1 的引用类型是 ClassA,而 ClassA 没有 methodB()。

现在让我们考虑 ClassB 覆盖 ClassA 的 methodA()的情况(见图 A-3 )。

9781430259831_AppA-03.jpg

图 A-3 。重写方法

现在,ClassB 的代码看起来像清单 A-20 中的所示。

上市 A-20 。重写方法 a()

package apress.appendix_A;

public class ClassB extends ClassA {

    public void methodA() {
        System.out.println("methodA() in ClassB");
    }

}

现在 ClassA 和 ClassB 都有相同的方法,methodA()。

以下作业:

ClassA var1 = new ClassB();

确认可以对 var1 调用 methodA(),因为 ClassA 有 methodA()。因此,下面的测试将会编译:

package apress.appendix_A;

public class Test {
    public static void main(String[] args) {

        ClassA var1 = new ClassB();
        var1.methodA();

    }
}

也就是说,在编译时,编译器将对照 var1 的引用类型 ClassA 检查 var1 上的调用 methodA(),并且编译器将允许它,因为 methodA()存在于 ClassA 中。

但是如果我们进行测试会发生什么呢?也就是会调用哪个 methodA(),ClassA 中的 methodA()还是 ClassB 中的 methodA()。运行测试时,它会给出以下输出:

methodA() in ClassB

编译器检查了类 a 中的方法 a(),但执行了类 b 中的方法 a()。这是因为在运行时,JVM 根据实际的对象类型验证而不是编译 methodA()。代码中的实际对象类型(class a var 1 = new class b();)是 ClassB,而 ClassA 是引用类型。因此,JVM 检查调用方法 a()是否在 ClassB()中,并调用它。这种现象被称为多态性。如果 methodA()不在 ClassB 中会发生什么?为了理解这一点,我们实现了人物 A-4 的代码。

9781430259831_AppA-04.jpg

图 A-4 。b 类层次结构

图 A-4 显示了 InterfaceA,只是为了说明引用类型可以是一个接口或者一个类(或者抽象类),编译器会以类似的方式对照其中任何一个的引用类型检查方法的存在性。清单 A-21 实现了图 A-4 所示的层级。

列表 A-21 。B 类层次结构

package apress.appendix_A;
public interface InterfaceA {

    public void methodA();

}

package apress.appendix_A;
public class ClassA implements InterfaceA{

    @Override
    public void methodA() {
        System.out.println("methodA() in ClassA");
    }
}

package apress.appendix_A;
public class ClassB extends ClassA {
    public void methodB() {
        System.out.println("methodB() in ClassB");
    }
}

在清单 A-21 中,调用 methodA()对照引用类型 InterfaceA 进行验证,在检查到 methodA()存在于 InterfaceA 中后,编译器批准调用;也就是说,没有编译时错误。运行清单 A-22 中所示的测试。

列表 A-22 。测试应用

package apress.appendix_A;
public class Test {
    public static void main(String[] args) {
        InterfaceA var1 = new ClassB();
        var1.methodA();
    }
}

您将获得以下输出:

methodA() in ClassA

清单 A-22 的第 4 行中的实际对象类型是 ClassB,所以在运行时 JVM 检查 methodA()是否存在于 ClassB 中。在 ClassB 中找不到 methodA()时(因为它不存在于 ClassB 中),JVM 检查 methodA()在 ClassB 的层次结构中是否存在,因为 JVM 认为在 ClassB 的层次结构中一定存在 methodA ();否则,编译器不会批准该调用。因为 methodA()存在于 ClassA 中,所以 JVM 在运行时执行 ClassA 中的 methodA()。

摘要

本附录向您介绍了 Java 和面向对象编程的基础。您了解了类是面向对象程序的基本构件,以及如何从类中实例化对象。接下来,向您介绍了面向对象编程的三大支柱:封装、继承和多态。

1【www.uml.org/】??

十、附录 B:Groovy 简介

Groovy 是一种用于 Java 虚拟机的敏捷而动态的语言,它建立在 Java 的优势之上,但受 Python、Ruby 和 Smalltalk 等语言的启发,它还增加了一些强大的特性。它与所有现有的 Java 类和库无缝集成,并编译成 Java 字节码,因此您可以在任何可以使用 Java 的地方使用它。Groovy 提供了静态类型检查和静态编译代码以提高健壮性和性能的能力,并支持特定领域语言和其他紧凑语法,因此您的代码变得易于阅读和维护。

Groovy 让 Java 开发人员几乎没有学习曲线就可以使用现代编程特性;在开发 web、GUI、数据库或控制台应用时,通过减少脚手架代码来提高开发人员的工作效率。并通过支持单元测试和开箱即用来简化测试。

入门指南

让我们从一个传统的“你好,世界”节目开始。但是首先你需要安装 Groovy。Groovy 以. zip 文件或特定平台安装程序的形式捆绑在 Windows、Ubuntu 和 Debian(以及 openSUSE,直到最近的版本)上。本节解释如何安装压缩版本,因为它涵盖了大多数平台。

要安装 Groovy,请遵循以下步骤:

  1. 下载最新稳定的 Groovy 二进制版本。来自 http://groovy.codehaus.org/Download 的 zip 文件。
  2. 将 groovy-binary-X.X.X.zip 解压缩到您选择的位置。
  3. 将 GROOVY_HOME 环境变量设置为解压缩。压缩文件。
  4. 将%GROOVY_HOME%\bin 目录添加到您的系统路径中。

注意 Groovy 需要 Java,所以你需要有一个可用的版本(虽然 Groovy 1.6 支持 JDK 1.4 或更高版本,但对于 Groovy 1.7 以后,至少需要 JDK 1.5)。

要验证您的安装,请打开控制台并键入以下内容:

>groovy -v

您应该会看到类似这样的内容:

Groovy Version: 2.0.0 JVM: 1.6.0_31 Vendor: Sun Microsystems Inc. OS: Windows 7

现在你可以编写你的第一个“Hello World”程序了(见清单 B-1 )。

列表 B-1 。Java 中的“Hello World”

1.   public class HelloWorld {
2.      public static void main( String[] args )
3.   System.out.println("Hello World!");
4.      }
5.   }
  • 第 1 行到第 2 行:方法和字段的默认可见性是 public,所以可以去掉 public 修饰符。
  • 第 2 行 : Groovy 支持动态类型,所以可以在 main()上丢弃类型信息和返回类型 void。
  • 第 3 行:每个 Groovy 对象都有自己的 disposure println,可以看作是 System.out.println 的快捷方式。
  • 第 3 行:行尾的分号是可选的,所以你也可以删除它。

根据这些规则,您可以将清单 B-1 的转换为清单 B-2 的。

列表 B-2 。应用 Groovy 规则转换“Hello World”

1.   class HelloWorld {
2.      static main( args ){
3.   println "Hello World!"
4.      }
5.   }

如您所见,清单 B-2 要紧凑得多。您可以以脚本的形式编写和执行 Groovy 代码,这些脚本也被编译成字节码。因此,你可以为“Hello World”程序编写清单 B-3 中的 Groovy 代码。

注意任何 Java 类/对象也是 Groovy 类/对象。

列表 B-3 。“Hello World”的精彩脚本

println "Hello World!"

您可以通过命令行、GroovyShell 或 GroovyConsole 运行 Groovy 脚本和类。

格罗维谢尔

GroovyShell 是一个交互式命令行应用(Shell ),允许您创建、运行、保存和加载 Groovy 脚本和类。要启动 GroovyShell,请运行 groovysh。图 B-1 展示了如何使用 GroovyShell 来执行一个简单的脚本。

9781430259831_AppB-01.jpg

图 B-1 。使用 GroovyShell

如你所见,这个脚本打印了 vishal。然后你看到===> null。按照惯例,Groovy 总是返回方法的结果。在这种情况下,没有结果,因此返回 null。GroovyShell 包含一个内置的帮助工具,您可以使用它来了解关于 Shell 的更多信息。要访问它,请在提示符下键入 help。图 B-2 显示了帮助列表。

9781430259831_AppB-02.jpg

图 B-2 。使用 GroovyShell 帮助

GroovyConsole

图 B-3 中的【GroovyConsole 是 GroovyShell 的图形化版本。它是使用 SwingBuilder 编写的,Swing builder 是一个 Groovy 模块,它使构建 Swing 用户界面变得更加容易。

9781430259831_AppB-03.jpg

图 B-3 。groovycconsole(groovycconsole)

图 B-4 显示了输出分离的 GroovyConsole。您可以通过多种方式启动 GroovyConsole,这取决于您的环境和您安装 Groovy 的方式。最简单的方法是执行 GroovyConsole,它位于 Groovy bin 目录中。

9781430259831_AppB-04.jpg

图 B-4 。使用 GroovyConsole 并分离输出

控制台提供了创建、保存、加载和执行类和脚本的能力。控制台的一些不错的特性是撤销/重做和检查变量的能力。如果你必须在使用 GroovyShell 和 GroovyConsole 之间做出选择,我推荐 GroovyConsole。您还可以在脚本中定义类并立即使用它们,如清单 B-4 中的所示。

清单 B-4 。在脚本中定义类

class HelloWorld {
   def hello( name ){
      "Hello ${name}!"
   }
}
def hw = new HelloWorld()
println hw.hello("Vishal")
  • 方法 hello 的返回类型不是特定的类型,所以使用保留关键字 def。
  • 字符串“Hello”不是一个简单的 java.lang.String,事实上,它是 Groovy 的一个特性:GString。这些类型的字符串允许字符串插值,这将在下一节中解释。

Groovy 支持两种类型的字符串:普通 Java 字符串和 GStrings。如果 Groovy 中的字符串被单引号或双引号或三引号包围,并且没有未转义的美元符号($),则该字符串是 java.lang.String 的实例。

GStrings

g string 是 groovy.lang.GString 的一个实例,允许文本中包含占位符。gstring 不是 String 的子类,因为 String 类是最终的,不能扩展。GString 就像一个普通的字符串,但是它允许使用${..}.只有当嵌入变量是表达式的占位符时,才需要花括号。Groovy 支持在 Perl 和 Ruby 等许多其他语言中发现的一个概念,称为字符串插值 ,即在字符串中替换表达式或变量的能力。如果您有使用 Unix shell 脚本、Ruby 或 Perl 的经验,这应该很熟悉。Java 不支持字符串插值。您必须手动连接这些值。清单 B-5 是您需要用 Java 编写的代码类型的一个例子,而清单 B-6 展示了使用 GString 的相同代码。

清单 B-5 。用 Java 构建字符串

String name = "Vishal" ;
String helloName = "Hello " + name ;
System.out.println(helloName) ;

清单 B-6 。Groovy/GString 中的字符串插值

1.   str1= "Vishal"
2.   str2 = "Hello "
3.   println "$str2$str1"

在第 3 行中,没有使用花括号,因为只有当嵌入的变量是表达式的占位符时才需要花括号。当 Groovy 看到一个用双引号或斜杠定义的字符串和一个嵌入的表达式时,Groovy 会构造一个 org . code Haus . Groovy . runtime . gstringimpl,而不是 java.lang.String。注意,可以在${}符号中包含任何有效的 Groovy 表达式;这包括方法调用或变量名。

Groovy 支持单行字符串和跨多行的字符串。在接下来的小节中,您将学习 Groovy 中支持的各种字符串。

单行字符串

单行字符串可以是单引号或双引号。单引号中的字符串是字面意思。用单引号定义的字符串不解释嵌入的表达式,如清单 B-7 所示。

清单 B-7 。单引号字符串不解释嵌入的表达式

name = "vishal"
s1 = 'hello $name'
println s1
Here is the output:
hello $name

您可以在单引号中嵌套双引号,如清单 B-8 中的所示。

清单 B-8 。单引号中嵌套的双引号

s1 = 'hello "vishal"'
println s1

使用双引号定义的字符串将解释字符串中嵌入的表达式,如清单 B-9 中的所示。

清单 B-9 。双引号字符串解释嵌入的表达式

def name = "vishal"
s1 = "hello $name"
println s1
Here is the output:
hello vishal

您可以在双引号中嵌套单引号,如清单 B-10 所示。

清单 B-10 。双引号中嵌套的单引号

s1 = "hello 'vishal'"
println s1
Here is the output:
hello 'vishal'

多行字符串

Groovy 支持跨多行的字符串。多行字符串通过使用三个双引号或三个单引号来定义。多行字符串支持对于创建模板或嵌入式文档(如 XML 模板、SQL 语句、HTML 等)非常有用。例如,您可以使用多行字符串和字符串插值来构建电子邮件消息的正文,如清单 B-11 所示。多行字符串的字符串插值与常规字符串的工作方式相同:用双引号创建的多行字符串计算表达式,而单引号字符串则不计算表达式。

清单 B-11 。使用多行字符串

def name = "Vishal"
def multiLineString = """
Hello, ${name}
This is a multiline string with double quotes
"""
println multiLineString
Hello, Vishal
This is a multiline string with double quotes

斜线弦

如前所述,斜线可以用来定义字符串。斜线符号有一个好处:不需要额外的反斜杠来转义特殊字符。唯一的例外是转义反斜杠:/。当创建需要反斜杠或路径的正则表达式时,斜杠符号会很有帮助。清单 B-12 展示了使用正则引号和斜杠定义正则表达式来匹配文件系统路径的区别。

清单 B-12 。使用斜线字符串

def quotedString = 'Hello Vishal'
def slashyString = /Hello Vishal/
println slashyString
Hello Vishal

清单 B-12 定义了两个变量,并将它们分配给一个目录路径。第一个变量定义 quotedString 使用单引号符号来定义字符串。使用单引号符号要求使用额外的反斜杠对嵌入的反斜杠进行转义。

多行斜线字符串

斜线字符串也可以跨越多行。当使用 regex freespacing 注释风格时,这对于多行 regex 特别有用(见清单 B-13)。

清单 B-13 。使用多行斜线字符串

1.    def name = "vishal"
2.    def path= "c:/groovy"
3.    def multilineSlashy = /
4.    Hello $name
5.    path= $path
6.    dollar = $
7.    path = c:\/groovy
8.    /
9.    println multilineSlashy
Hello vishal
path= c:/groovy
dollar = $
path = c:/groovy

让我们更详细地看看清单 B-13 。

  • 第 1 行定义了一个变量 name,并将值“vishal”赋给它。
  • 第 2 行定义了一个变量 path,并将值“c:/groovy”赋给它。
  • 第 3 行定义了一个变量 multilineSlashy,并为它分配了一个多行字符串,包括斜杠之间的第 8 行。
  • 第 4 行有一个表达式$name,其计算结果为 vishal,如输出所示。
  • 第 5 行有一个表达式$path,其计算结果为 c:/groovy,如输出所示。
  • 第 6 行有一个$符号,但它不是一个表达式,所以它显示在输出中。
  • 第 7 行有一个斜杠,需要转义。

美元斜线字符串

在多行斜线字符串中,斜线仍然需要转义。此外,在多行斜线字符串中,不是表达式的未转义美元符号会导致 MissingPropertyException,如清单 B-14 中的所示。

清单 B-14 。多行斜线字符串中缺少 PropertyException

1.    def name = "vishal"
2.    def path= "c:/groovy"
3.    def multilineSlashy = /
4.    Hello $name
5.    path= $path
6.    dollar = $test
7.    path = c:\/groovy
8.     /
9.    println multilineSlashy
Caught: groovy.lang.MissingPropertyException: No such property: test for class:
hello
groovy.lang.MissingPropertyException: No such property: test for class: hello
at hello.run(hello.groovy:3)

在清单 B-14 中,没有 test 这样的属性;第 6 行中的$test 被解释为一个表达式,这会导致 MissingPropertyException。

现在,让我们看看清单 B-15 中的代码,特别是第 6 行。

清单 B-15 。多行斜线字符串中的非转义美元符号

1.    def name = "vishal"
2.    def path= "c:/groovy"
3.    def multilineSlashy = /
4.    Hello $name
5.    path= $path
6.    dollar = $ test
7.    path = c:\/groovy
8.     /
9.    println multilineSlashy

这一次,Groovy 没有将第 6 行中的$ test 解释为表达式,因为$和 test 之间有一个空格,它的输出如下所示:

Hello vishal
path= c:/groovy
dollar = $ test
path = c:/groovy

有了美元斜杠字符串,您不再需要用前面的反斜杠对斜杠进行转义(多行斜杠字符串需要对斜杠进行转义),如果需要,您可以使用$$对\(进行转义,或者使用\)/对斜杠进行转义,如清单 B-16 中的所示。

清单 B-16 。使用美元斜线字符串

1.    def name = "vishal"
2.    def path= "c:/groovy"
3.    def dollarSlashy = $/
4.    Hello $name
5.    path = $path
6.    dollar = $$test
7.    path = c:/groovy
8.    /$
9.    println dollarSlashy
Hello vishal
path= c:/groovy
dollar = $test
path = c:/groovy

让我们更详细地看看清单 B-16 。

  • 第 3 行定义了一个直到第 8 行的 dollarSlashy 字符串。
  • 第 6 行有一个\(test,它在清单 B-14 中的多行斜线字符串的情况下导致了 MissingPropertyException,现在使用一个\)对其进行转义。

集体数据类型

Groovy 支持许多不同的集合,包括数组、列表、映射、范围和集合。让我们看看如何创建和使用每种集合类型。

数组

Groovy 数组是一个对象序列,就像 Java 数组一样(见列出 B-17 )。

上市 B-17 。创建和使用数组

1.    def stringArray = new String[3]
2.    stringArray[0] = "A"
3.    stringArray[1] = "B"
4.    stringArray[2] = "C"
5.    println stringArray
6.    println stringArray[0]
7.    stringArray.each { println it}
8.     println stringArray[-1..-3]
  • 第 1 行创建了一个大小为 3 的字符串数组。
  • 第 2 到 4 行使用一个索引来访问数组。
  • 第 7 行演示了如何使用 each()方法遍历数组。each()方法用于遍历每个元素并对其应用闭包。
  • 第 8 行显示了一些有趣的东西——它使用一个范围来访问数组,稍后将讨论这个范围。
[A, B, C]
A
A
B
C
[C, B, A]

列表

Groovy 列表是对象的有序集合,就像在 Java 中一样。它是 java.util.List 接口的实现。清单 B-18 说明了如何创建一个清单和常见用法。

清单 B-18 。创建和使用列表

1.    def emptyList = []
2.    def list = ["A"]
3.    list.add "B"
4.    list.add "C"
5.    list.each { println it }
6.    println list[0]
7.    list.set(0, "Vishal")
8.    println list.get(0)
9.     list.remove 2
10.    list.each { println it }
  • 在第 1 行,通过给属性赋值[]来创建一个空列表。
  • 第 2 行到第 4 行创建一个列表,其中已经有一个条目,并向列表中添加条目。
  • 第 5 行遍历列表,调用闭包来打印内容。each()方法提供了遍历列表中所有元素的能力,在每个元素上调用闭包。
  • 第 6 行演示了如何使用索引来访问列表。列表是从零开始的。
  • 第 7 行显示了如何使用一个索引为位置 0 赋值“Vishal”。
  • 第 8 行使用 get()方法访问列表。
A
B
C
A
Vishal
Vishal
B

地图

Groovy 映射是一个无序的键-值对集合,其中的键是惟一的,就像在 Java 中一样。它是 java.util.Map 的一个实现。

清单 B-19 。创建和使用地图

1.    def map = ['a':'Value A', 'b':'Value B ']
2.    println map["a"]
3.    println map."a"
4.    println map.a
5.    println map.getAt("b")
6.    println map.get("b")
7.    println map.get("c", "unknown")
8.    println map
9.    map.d = "Value D"
10.    println map.d
11.    map.put('e', 'Value E')
12.    println map.e
13.    map.putAt 'f', 'Value F'
14.    println map.f
15.    map.each { println "Key: ${it.key}, Value: ${it.value}" }
16.    map.values().each { println it }
  • 第 1 行演示了如何定义一个包含多个条目的映射。使用方括号符号时,冒号将键和值分开。
  • 第 2 行到第 10 行展示了几种不同的访问地图的技术。
  • 第 11 行到第 13 行展示了将项目放入地图的几种不同技术。
Value A

Value A

Value A

Value B

Value B

unknown

[a:Value A, b:Value B , c:unknown]

Value D

Value E

Value F

Key: a, Value: Value A

Key: b, Value: Value B

Key: c, Value: unknown

Key: d, Value: Value D

Key: e, Value: Value E

Key: f, Value: Value F

Value A

Value B

unknown

Value D

Value E

Value F

范围

范围是连续值的列表。从逻辑上来说,你可以认为它是从 1 到 10 或者从 a 到 z。事实上,范围的声明就是:1..10 或者 a。z '。范围是实现 java.lang.Comparable 的任何对象的列表。这些对象具有 next()和 previous()方法,以便于在范围内导航。清单 B-20 展示了你可以用范围做的一些事情。

清单 B-20 。创建和使用范围

1.    def numRange = 0..9
2.    numRange.each {print it}
3.    println ""
4.    println numRange.contains(5)
5.    def reverseRange = 9..0
6.    reverseRange.each {print it}
7.    def exclusiveRange = 1..<10
8.    println ""
9.    exclusiveRange.each {print it}
10.    def alphabet = 'a'..'z'
11.    println ""
12.    println alphabet.size()
13.    println alphabet[25]
  • 第 1、5、7 和 10 行说明了如何定义范围。
  • 第 1 行定义了数字的包含范围。
  • 第 10 行定义了小写字母的包含范围。
  • 第 7 行定义了一个排他的数字列表。该范围产生 1 到 9 的数字范围,不包括 10。
  • 第 5 行以相反的顺序创建一个范围,从 9 到 0。通常,范围用于迭代。在清单 B-20 的中,each()用于在范围内迭代。清单 B-21 展示了使用范围进行迭代的三种方式:一种在 Java 中,两种在 Groovy 中。
0123456789

true

9876543210

123456789

26

z

清单 B-21 。使用范围进行迭代

for (i in 0..9) {
println i
}
(0..9).each { i->
println i
}

each()方法用于遍历每个元素并对其应用闭包。

设置

Groovy 集合是一个无序的对象集合,没有副本,就像 Java 一样。它是 java.util.Set 的实现。默认情况下,除非您另外指定,否则 Groovy 集合是 java.util.HashSet。如果您需要一个除 HashSet 之外的集合,您可以通过实例化它来创建任何类型的集合,如 def TreeSet = new TreeSet()。清单 B-22 展示了如何创建集合和常用用法。

清单 B-22 。创建和使用集合

def  set = ["A", "B" ] as Set
set.add "C"
println set
set.each { println it }
set.remove "B"
set.each { println it }
[A, B, C]

A

B

C

A

C

创建空集类似于创建空列表。不同的是增加了 Set 子句。列表和集合之间的一个重要区别是,列表提供基于索引的访问,而集合不提供。

方法

清单 B-23 展示了用 Java 方式在 Groovy 中定义一个方法,而清单 B-24 展示了同样的事情,但是使用了 Groovy 语法。

清单 B-23 。用 Java 方式定义方法

public String hello(String name) {
return "Hello, " + name;
}

清单 B-24 。使用 Groovy 习惯用法定义方法

def hello(name) {
"Hello, ${name}"
}
  • 返回类型和返回语句不包含在方法体中。Groovy 总是返回最后一个表达式的结果——在本例中,是 GString“Hello,。。."。
  • 未定义访问修饰符 public。除非您另外指定,否则 Groovy 默认所有的类、属性和方法都是公共访问。

关闭

函数式编程为您提供了在底层原则 : 【参考透明性】高阶函数,以及不可变值的基础上考虑并发性的正确基础。理解这些关键元素对于理解闭包(以及 Groovy 中最近引入的其他功能特性)至关重要。函数式编程是建立在纯函数的前提下的。在数学中,函数是纯粹的,因为它们没有副作用。考虑经典函数 sin(x): y = sin(x)。无论调用 sin(x)多少次,sin(x)都不会在内部修改全局或上下文状态。这样的函数是纯粹的,没有副作用,并且不受上下文影响。这种对周围环境的遗忘被称为参照透明性。如果没有修改全局状态,函数的并发调用是稳定的。在函数式编程中,函数是一等公民,这意味着函数可以赋给变量,函数可以传递给其他函数,函数可以作为其他函数的值返回。而这种以函数为自变量或者返回一个函数的函数,叫做高阶函数。**

**引用透明性、高阶函数和不可变值一起使函数式编程成为编写并发软件的更好方式。虽然函数式语言都是关于消除副作用的,但是一种从不考虑副作用的语言是没有用的。事实上,引入副作用对任何语言都是至关重要的。所有函数式语言都必须提供以受控方式引入副作用的机制,因为即使函数式语言是关于纯编程的,不认可副作用的语言也是无用的,因为输入和输出本质上是副作用的衍生物。

理解闭包

以受控方式引入副作用的技术之一是关闭。Groovy 中的闭包定义遵循以下语法:

{ [closure parameters ->] closure body}

其中[closure parameters->]是可选的逗号分隔的参数列表,闭包体可以是一个表达式以及零个或多个 Groovy 语句。参数看起来类似于方法的参数列表,这些参数可以是有类型的,也可以是无类型的。Groovy 闭包是花括号{}内的可重用代码块,它可以赋给属性或变量,或者作为参数传递给方法。闭包只在被调用时执行,而不是在被定义时执行。清单 B-25 说明了这一点。

清单 B-25 。叫停

1.    def closureVar = {println 'Hello world'}
2.    println "closure is not called yet"
3.    println " "
4.    closureVar.call()
  • 第 1 行:这一行包含不带参数的闭包,由一个 println 语句组成。因为没有参数,所以省略了参数列表和->分隔符。闭包由标识符 closureVar 引用。
  • 第 4 行:这一行通过 call()方法使用显式机制来调用闭包。您也可以使用隐式的无名调用方法:closureVar()。如输出所示,闭包在第 4 行调用时打印“Hello world ”,而不是在第 1 行定义时。

以下是输出:

closure is not called yet
Hello world

清单 B-26 展示了与清单 B-25 中相同的闭包,但带有参数。

清单 B-26 。带参数的闭包

1.    def closureVar = {param -> println "Hello ${param}"}
2.    closureVar.call('world')
3.    closureVar ('implicit world')
  • 第 2 行:这是一个带有实际参数‘world’的显式调用。
  • 第 3 行:这是一个隐式调用,实际参数为‘隐式世界’。
Hello world
Hello implicit world

如清单 B-27 所示,闭包的形参可以被赋予默认值。

清单 B-27 。具有默认值的参数

1.    def sayHello= {str1, str2= " default world" -> println "${str1} ${str2}" }
2.    sayHello("Hello", "world")
3.    sayHello("Hello")
  • 第 1 行:say hello 闭包有两个参数,其中一个参数 str2 有一个默认值。
  • 第 3 行:闭包只提供一个实际参数,使用第二个参数的默认值。

以下是输出:

Hello world
Hello default world

闭包总是有返回值。该值可以通过闭包体内的一个或多个显式 return 语句来指定,如清单 B-28 中的所示,或者如果 return 没有显式指定,则作为最后执行的语句的值,如清单 B-29 中的所示。

清单 B-28 。使用 Return 关键字

def sum = {list -> return list.sum()}
assert sum([2,2]) == 4

清单 B-29 。Return 关键字可选

def sum = {list -> list.sum()}
assert sum([2,2]) == 4

要理解闭包,你必须理解自由变量的概念。当函数体引用一个或多个自由变量时,就形成了闭包。自由变量不是函数的局部变量,不作为参数传递给函数,而是在定义函数的封闭作用域中定义的。因此,闭包指的是参数列表中没有列出的变量(自由变量)。它们被“绑定”到定义它们的范围内的变量。清单 B-30 说明了这一点。

清单 B-30 。自由变量

def myConst = 5
def incByConst = { num -> num + myConst }
assert  incByConst(10) == 15

运行时“关闭”自由变量(清单 B-30 中的中的 myConst ),以便在执行函数时它是可用的。也就是说,编译器创建一个闭包来封装自由变量的外部上下文并绑定它们。

隐式变量

在 Groovy 闭包内,定义了一个具有特殊含义的隐式变量(it)。如果只有一个参数被传递给闭包,那么参数列表和->符号可以省略,闭包将可以访问它,它代表那个参数,如清单 B-31 所示。

清单 B-31 。使用它

def closure = {println "Hello ${it}"}
closure.call('world')
Hello world

一个闭包总是至少有一个参数,如果没有定义显式参数,那么这个参数可以通过隐式参数 it 在闭包体内使用。开发人员永远不必声明 it 变量——就像对象中的 this 参数一样,它是隐式可用的。如果闭包被调用时没有参数,那么它将是 null。

明确宣布关闭

Groovy 中定义的所有闭包本质上都是从类型闭包派生出来的。因为 groovy.lang 是自动导入的,所以您可以在代码中将 Closure 作为一种类型来引用。这是一个闭包的显式声明。显式声明闭包的优点是不会无意中将非闭包赋给这样的变量。清单 B-32 展示了如何显式声明一个闭包。

清单 B-32 。闭包的显式声明

Closure closure = { println it }

将该方法作为闭包重用

Groovy 提供了方法闭包运算符(。&) 重用方法作为闭包。方法闭包操作符允许像闭包一样访问和传递方法。清单 B-33 说明了这一点。

清单 B-33 。将该方法作为闭包重用

1.    def list = ["A","B","C","D"]
2.    String printElement(String element) {
3.    println element
4.    }
5.    list.each(this.&printElement)

清单 B-33 创建一个名字列表,并遍历列表打印出名字。在第 5 行,方法闭包运算符(。&)使方法 printElement 作为闭包被访问。以下是输出:

A

B

C

D

将闭包作为参数传递

闭包是一个对象。您可以像传递任何其他对象一样传递闭包。一个常见的例子是使用闭包迭代一个集合(参见清单 B-34 )。

清单 B-34 。将闭包作为参数传递

def list = ["A", "B", "C"]
def x = { println it }
list.each(x)
A

B

C

专业操作员

Groovy 包括几个在其他编程语言中可以找到的标准操作符,以及特定于 Groovy 的操作符,这些操作符使 Groovy 变得如此强大。在接下来的小节中,您将学习 Groovy 中的特殊操作符,比如 spread、Elvis、安全导航、字段、方法闭包和菱形操作符。

传播算子

展开运算符(*。) 是一种在对象集合上调用方法或闭包的速记技术。清单 B-35 展示了 spread 操作符在列表中迭代的用法。

清单 B-35 。使用扩展运算符

1.    def map = [1:"A", 2:"B", 3:"C", 4:"D"]
2.    def keys = [1, 2, 3, 4]
3.    def values = ["A", "B", "C", "D"]
4.    assert map*.key == keys
5.    assert map*.value == values

第 4 行和第 5 行使用 spread 操作符来访问映射的键和值。

Elvis 操作员

猫王操作员(?😃 是 Java 三元运算符的简写版本。比如 b= a?:1 可以解释如下:

if(a != 0)
b = a
else
b = 1

清单 B-36 展示了在 Groovy 中使用 Java 三元和 Elvis 操作符。

清单 B-36 。使用 Elvis 操作员

def firstName = author.firstName == null ? "unknown" : author.firstName // Java ternary
def firstName2 = author.firstName ?: "unknown" // Groovy Elvis

在这两种情况下,如果 author.firstName 为 null,那么 firstName 将设置为 unknown。Elvis 运算符示例的 author.firstName 片段被称为表达式。如果表达式的计算结果为 false 或 null,则返回冒号后的值。

安全导航/解引用操作员

安全导航/解引用运算符(?。) 用于避免空指针异常。考虑一下的情况,您有一个 Author 对象,您想打印这个名字。如果当您访问 firstName 属性时,Author 对象为空,您将得到一个 NullPointerException(参见清单 B-37 )。

清单 B-37 。使用安全导航/取消引用运算符

class Author {
String firstName
String lastName
def printFullName = {
println "${firstName} ${lastName}"
}
}
Author author
println author.firstName

清单 B-37 中的代码抛出一个 NullPointerException。在 Java 中,您可以这样添加一个空检查:

if (author != null) {
println "Author FirstName = ${author.firstName}"
}

清单 B-38 展示了如何在 Groovy 中使用安全导航/解引用操作符来添加 null 检查。

清单 B-38 。使用安全导航/取消引用运算符

class Author {
String firstName
String lastName
def printFullName = {
println "${firstName} ${lastName}"
}
}
Author author
println "Author FirstName = ${author?.firstName}"

现场操作员

Groovy 提供了一种绕过 getter 直接访问底层字段的方法。然而,不建议绕过 getter 访问底层字段,因为这违反了封装。清单 B-39 显示了使用字段操作符(.@)。

清单 B-39 。使用字段运算符

class Author {
String name
}
def author = new Author(name: "Vishal")
println author.name
printlnauthor.@name
Vishal

Vishal

在这个例子中,第一个 println 使用 getter 访问 name,第二个 println 绕过 getter 直接访问 name。

方法闭包运算符

方法闭包运算符(。&) 允许像闭包一样访问和传递方法(参见清单 B-40 )。

列出 B-40 。使用方法闭包运算符

def list = ["A","B","C"]
list.each { println it }
String printName(String name) {
println name
}
list.each(this.&printName)
A

B

C

A

B

C

本示例创建一个姓名列表,并遍历该列表以打印出姓名。创建一个 printName()方法来打印 Name 参数。最后,也是这个例子的要点,列表被迭代,执行 printName()方法作为闭包。使用方法闭包操作符,您能够将 Java 方法公开为闭包。

菱形算子

Groovy 中引入了菱形运算符(<> ) ,以避免参数化类型的重复。参数化类型可以省略,用尖括号代替,尖括号看起来像一个菱形。清单 B-41 展示了一种定义列表的常见冗长方式。

列出 B-41 。一个简单的 Groovy 脚本:Hello.groovy

List<List<String>> list1 = new ArrayList<List<String>>()

清单 B-42 展示了如何使用菱形运算符。

清单 B-42 。使用菱形运算符

List<List<String>> list1 = new ArrayList<>()

摘要

本附录介绍了 Groovy 的基础知识。学习任何语言或技术,一个附录是不够的,但是本附录中对 Groovy 的介绍对于使用 Grails 编写 web 应用来说已经足够了。在本附录中,您首先学习了如何安装 Groovy 然后展示了如何用 Groovy 编写一个“Hello World”程序。然后,您学习了如何运行 Groovy 脚本和类,并查看了 Groovy 中支持的各种字符串。然后这一章简要介绍了 Groovy 的集合数据类型,您学习了什么是闭包以及如何在 Groovy 中使用它。最后,您学习了如何使用 Groovy 中可用的特殊操作符。**

十一、附录 C:Scala 简介

Scala 无缝集成了面向对象和函数式编程。Scala 是一种静态类型语言,由 Martin Odersky 于 2001 年提出,他也是 Java 参考编译器的作者和 Java 泛型的合著者。Scala 为 Java 虚拟机(JVM)编译成字节码,使其与平台无关。这也意味着从 Scala 程序中你可以使用现有的 Java 库,反之亦然。

Scala 入门

你可以从 www.scala-lang.org/download/下载 Scala。这个 Scala 软件发行版可以安装在任何类似 Unix 或 Windows 的系统上。它需要 Java 运行时版本 1.6 或更高版本。

>scala  -version
Scala code runner version 2.10.3 -- Copyright 2002-2013, LAMP/EPFL

有三种方法可以执行 Scala 代码。

  • 使用交互式解释器
  • 将 Scala 代码作为脚本执行
  • 编译 Scala 代码

使用交互式解释器

Scala 解释器(称为读取-评估-打印循环,或 REPL )是执行一行 Scala 代码最简单的方法。您可以使用 scala 命令行工具 Scala 启动交互式解释器,该工具位于 Scala 安装文件夹的 bin 文件夹中。

从命令行输入以下内容,打开交互式解释器,如图 C-1 所示。

>scala

9781430259831_AppC-01.jpg

图 C-1 。Scala 交互式解释器

使用交互式解释器,您可以通过使用 println 方法运行您的第一个“Hello world”程序。

scala> println("Hello world");
Hello world

要退出解释器,请键入 exit。

scala> exit

将 Scala 代码作为脚本执行

另一种执行 scala 代码的方式是将其输入到一个文本文件中,并用扩展名. Scala 保存。然后,您可以通过键入 filename .scala 来执行该代码。例如,您可以创建一个名为 hello.scala 的文件,其中包含“Hello world”。

println("Hello world")

要执行它,需要将文件名指定为 Scala 命令行工具的参数。

>scala  hello.scala

编译 Scala 代码

您也可以通过首先使用 scalac 命令行工具编译 Scala 代码来执行它。然后代码将需要在应用的上下文中执行,因此您将需要添加一个带有 main()方法的对象(参见清单 C-1 )。

列表 C-1 。“你好,世界”节目

1.    object HelloWorld {
2.    def main(args: Array[String]) {
3.    println("Hello, world")
4.       }
5.    }

注意语句末尾的分号通常是可选的。

  • 第 1 行:main()方法是在对象中定义的,而不是在类中。Scala 有一个对象构造,你可以用它来声明一个单例对象。在本附录的后面,您将了解更多关于单例的内容。
  • 第二行 : Scala 程序处理从 main()方法开始,这是每一个 Scala 程序必不可少的一部分。main()方法未标记为 static。在 Scala 中,一切都是对象。main()方法是自动实例化的 singleton 对象上的实例方法。
  • 第 2 行:没有返回类型。其实是有 Unit 的,和 void 差不多,不过是编译器推断出来的。您可以通过在参数后加上冒号和类型来显式指定返回类型。
def main(args: Array[String]) : Unit = {
                       }
  • 第 2 行:Scala 中没有访问级修饰符。在这个上下文中,Java 中有一个 public 修饰符,但是 Scala 没有指定 public 修饰符,因为默认的访问级别是 public。
  • 第 2 行 : Scala 使用 def 关键字告诉编译器这是一个方法。

将清单 C-1 中的代码保存在名为 HelloWorld.scala 的文件中,并使用以下命令编译代码:

>scalac HelloWorld.scala

现在使用以下命令运行程序:

>scala HelloWorld
Hello, World!

注意 Java 要求你把一个公共类放在一个以该类命名的文件中。例如,您应该将类 HelloWorld 放在 HelloWorld.java 文件中。在 Scala 中,你可以命名。scala 可以保存你想要的任何东西,不管你在里面放了什么 Scala 类或者代码。但是,建议您像在 Java 中一样,根据文件包含的类来命名文件,以便根据文件名轻松定位类。

变量

Scala 允许你在声明一个变量时决定它是否是不可变的(只读的)。不可变变量是用关键字 val 声明的。这意味着它是一个不可改变的变量。清单 C-2 展示了创建一个不可变的变量,而图 C-2 展示了当你试图改变它时会发生什么。

列表 C-2 。不可变变量

val immutableVar : String = "Hello"
immutableVar = "Hi"

9781430259831_AppC-02.jpg

图 C-2 。尝试更改 val 时出错

清单 C-3 展示了创建一个可变变量,而图 C-3 展示了它被成功改变。

列表 C-3 。可变变量

var mutableVar = "Hello"
mutableVar = "Hi"

9781430259831_AppC-03.jpg

图 C-3 。var 更改成功

当你给一个变量赋一个初始值时,Scala 编译器可以根据赋给它的值推断出变量的类型。这被称为类型推理,如清单 C-4 所示。

清单 C-4 。类型推理

var  var1= 10
var var2 = "Hello world"

在清单 C-4 中,Scala 将推断 var1 为 Int 类型,var2 为 String 类型变量。

收集

Scala 集合区分可变和不可变集合。可变集合可以就地更新或扩展。这意味着你可以更改、添加或删除收藏的元素。相比之下,不可变集合永远不会改变。您仍然有模拟添加、删除或更新的操作,但是这些操作在每种情况下都将返回一个新集合,而旧集合保持不变。Scala 有一个丰富的集合库。最常用的集合是列表、集合和映射,这些将在下面的章节中介绍。你可以在docs . Scala-lang . org/overviews/collections/introduction . html找到关于 Scala 收藏库的详细信息。

列表

列表是不可变的,这意味着列表的元素不能通过赋值来改变。包含 T 类型元素的列表类型被写成 List[T],如下所示:

val numberList: List[Integer] = List(1, 2, 3)

清单 C-5 展示了如何创建和使用一个不可变列表。

清单 C-5 。创建不可变列表

val list = List(1, 2, 3, 2, 3)
println (list.head)
println(list.tail)
println(list.length)
println(list.max)
println(list.min)
println(list.sum)
println(list.sorted)
println(list.reverse)
head   --- 1

tail   --- List(2, 3, 2, 3)

length --- 5

max    --- 3

min    --- 1

sum    --- 11

sorted --- List(1, 2, 2, 3, 3)

reverse--- List(3, 2, 3, 2, 1)

Scala 只定义了一个不可变的列表。但是,它也定义了一些可变的列表类型,比如 ArrayBuffer。清单 C-6 展示了如何创建一个可变列表。

清单 C-6 。创建可变列表

import collection.mutable
val list = mutable.ArrayBuffer(1, 2, 3, 2, 3)
assert (list.length  == 5)

设置

集合是不包含重复元素的集合。有两种集合,不可变的和可变的。清单 C-7 展示了如何创建一个不可变的集合。

清单 C-7 。创建不可变集合

val set = Set(1, 2, 3, 2, 3)
println ("head -- "+set.head)
println("tail -- "+set.tail)
println("size -- "+set.size)
println("sum  -- "+set.sum)
head -- 1

tail -- Set(2, 3)

size -- 3

sum  -- 6

默认情况下,Scala 使用不可变集合。如果你想使用可变集合,你必须导入 scala.collection.mutable.Set。

清单 C-8 。创建可变集合

import collection.mutable
val set = mutable.HashSet(1, 2, 3, 2, 3)
assert (set.size == 3)

地图

Scala map 是键值对的集合。默认情况下,Scala 使用不可变的 map。如果想使用可变映射,就必须显式导入 scala.collection.mutable.Map 类。清单 C-9 展示了如何创建和使用一个不可变的地图。

清单 C-9 。创建不可变的地图

val map = Map("1" -> 1, "2" -> 2, "3" -> 3, "2" -> 2, "3" -> 3)

println ("head  -- "+map.head)
println("tail  -- "+map.tail)
println("size  -- "+map.size)
head  -- (1,1)

tail  -- Map(2 -> 2, 3 -> 3)

size  -- 3

类别

Scala 中的类的声明非常像 Java 类。一个区别是 Scala 类可以有参数,如清单 C-10 中的所示。

列出 C-10 。带参数的 Scala 类

class Vehicle (speed : Int){
val mph :Int = speed
    def race() = println("Racing")
}

车辆类接受一个参数,即车辆的速度。创建 Vehicle 类的实例时,必须传递该参数,如下所示:new Vehicle(100)。该类包含一个名为 race()的方法。

扩展一个类

在 Scala 中覆盖从超类继承的方法是可能的,如清单 C-11 中的所示。

列出 C-11 。扩展一个 Scala 类

1.    class Car (speed : Int) extends Vehicle(speed) {
2.    override val mph: Int= speed
3.    override  def race() = println("Racing Car")
4.    }
  • 第 1 行:Car 类使用关键字 extends 扩展了 Vehicle 类。
  • 第 2 行到第 3 行:字段 mph 和方法 race()需要使用关键字 override 来覆盖。

清单 C-12 展示了另一个叫做 Bike 的类,它扩展了 Vehicle。

清单 C-12 。扩展一个 Scala 类

class Vehicle (speed : Int){
val mph :Int = speed
    def race() = println("Racing")
}
class Car (speed : Int) extends Vehicle(speed) {
override val mph: Int= speed
override  def race() = println("Racing Car")

}
class Bike(speed : Int) extends Vehicle(speed) {
override val mph: Int = speed
override  def race() = println("Racing Bike")

}

将清单 C-12 保存在 vehicle.scala 文件中,并使用以下代码进行编译:

>scalac vehicle.scala

现在您可以使用 scala 命令进入 REPL 并创建 vehicle 对象,如下所示:

scala> val vehicle1 = new Car(200)

使用这个命令,Scala 创建了 vehicle1 对象,如下所示:

vehicle1: Car = Car@19a8942

现在,您可以使用 Scala 创建的 vehicle1 对象来访问汽车的速度。

scala> vehicle1.mph

斯卡拉 REPL 发出汽车的速度,如下所示:

res1: Int = 200

以类似的方式,您可以执行 vehicle1 的 race()方法,如下所示:

scala>vehicle1.race()

Scala 解释器发出输出,如下所示:

Racing Car

现在您可以创建 Bike 对象并访问它的属性和方法,如下所示:

scala> val vehicle2 = new Bike(100)
vehicle2: Bike = Bike@b7ad3
scala>vehicle2.mph
res4: Int = 100
scala> vehicle2.race()
Racing Bike

特征

假设您想在车辆层次结构中添加另一个类。这一次你要添加一个蝙蝠战车。蝙蝠战车可以赛跑、滑行和飞行。但是你不能在 Vehicle 类中添加 glide 和 fly 方法,因为在一个非冲突的世界中,汽车和自行车不能滑行或飞行。至少现在还没有。所以,如果你想把蝙蝠战车添加到你的车辆等级中,你可以使用一个特征。特性就像 Java 中的接口,也可以包含代码。在 Scala 中,当一个类从 trait 继承时,它实现了 trait 的接口,并继承了 trait 中包含的所有代码。清单 C-13 展示飞行和滑翔特性。

清单 C-13 。Scala 特征

trait flying {
    def fly() = println("flying")
}

trait gliding {
def gliding() = println("gliding")
}

现在你可以创建蝙蝠战车类来扩展飞行器类以及飞行和滑翔特性,如清单 C-14 中的所示。

清单 C-14 。使用特征

1.    Batmobile(speed : Int) extends Vehicle(speed)  with flying with gliding{
2.    override val mph: Int = speed
3.    override  def race() = println("Racing Batmobile")
4.    override def fly() = println("Flying Batmobile")
5.    override def glide() = println("Gliding Batmobile")
6.
7.    }

现在,您可以在 REPL 创建一辆蝙蝠战车,如下图所示:

scala> val vehicle3 = new Batmobile(300)
vehicle3: Batmobile = Batmobile@374ed5

现在您可以访问 Batmobile 的 fly()方法,如下所示:

scala> vehicle3.fly()
Flying Batmobile

创建一个车辆列表,然后可以使用 Scala collections 库提供的 maxBy()方法来查找列表中最快的车辆。

scala> val vehicleList = List(vehicle1, vehicle2, vehicle3)
vehicleList: List[Vehicle] = List(Car@562791, Bike@e80317, Batmobile@374ed5)
scala> val fastestVehicle = vehicleList.maxBy(_.mph)
fastestVehicle: Vehicle = Batmobile@374ed5

单一对象

Scala 没有静态成员。相反,Scala 有单例对象。单例对象定义看起来像一个类定义,除了你使用关键字对象代替关键字类。singleton 是一个只能有一个实例的类。清单 C-15 展示了如何在应用中使用单例对象。

清单 C-15 。在应用中使用单例对象

1.    class Vehicle (speed : Int){
2.    val mph :Int = speed
3.    def race() = println("Racing")
4.    }
5.    class Car (speed : Int) extends Vehicle(speed) {
6.    override val mph: Int= speed
7.    override  def race() = println("Racing Car")
8.
9.    }
10.    class Bike(speed : Int) extends Vehicle(speed) {
11.    override val mph: Int = speed
12.    override  def race() = println("Racing Bike")
13.
14.    }
15.    trait flying {
16.    def fly() = println("flying")
17.    }
18.
19.    trait gliding {
20.    def glide() = println("gliding")
21.    }
22.
23.    class Batmobile(speed : Int) extends Vehicle(speed)  with flying with gliding{
24.    override val mph: Int = speed
25.    override  def race() = println("Racing Batmobile")
26.    override def fly() = println("Flying Batmobile")
27.    override def glide() = println("Gliding Batmobile")
28.
29.    }
30.    object Vehicle {
31.    def main(args: Array[String]) {
32.    val vehicle1 = new Car(200)
33.    val vehicle2 = new Bike(100)
34.    val vehicle3 = new Batmobile(300)
35.
36.    val vehicleList = List(vehicle1, vehicle2, vehicle3)
37.    val fastestVehicle = vehicleList.maxBy(_.mph)
38.
39.    printVehicle
40.
41.    def printVehicle{
42.    println ("speed of Bike : " + vehicle1.mph);
43.    println ("speed of Car : " + vehicle2.mph);
44.    println ("speed of Batmobile : " + vehicle3.mph);
45.    println ("Fastest Vehicle : " + fastestVehicle.mph + " mph");
46.
47.         }
48.      }
49.    }

当编译并执行前面的代码时,它会产生以下结果:

>scalac vehicle.scala
>scala Vehicle
speed of Bike : 200 mph

speed of Car : 100 mph

speed of Batmobile : 300 mph

Fastest Vehicle : 300 mph

摘要

本附录向您介绍了 Scala 的基础知识。您学习了三种交互执行 Scala 代码的方式,一种是作为脚本执行,另一种是作为编译程序执行。然后您学习了如何使用 Scala collections 库。最后,您学习了如何使用 traits 以及如何在应用中使用 singleton 对象。

posted @ 2024-08-19 17:24  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报