JavaEE8-和-Eclipse-开发指南-全-

JavaEE8 和 Eclipse 开发指南(全)

原文:zh.annas-archive.org/md5/1ba964ecb0ccb77760bf120f36de84c7

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:介绍 JEE 和 Eclipse

Java 企业版(JEE,之前称为 J2EE)已经存在很多年了。它是一个用于开发企业应用的非常健壮的平台。J2EE 首次于 1999 年发布,但在 2006 年发布第 5 版时经历了重大变化。自第 5 版以来,它已被更名为Java 企业版JEE)。JEE 的最新版本使得开发多层分布式应用变得更加容易。J2EE 专注于核心服务,并将使应用开发变得更容易的任务留给了外部框架,例如 MVC 和持久化框架。但 JEE 将这些框架中的许多引入了核心服务。随着对注解的支持,这些服务在很大程度上简化了应用开发。

任何运行时技术如果没有好的开发工具都是不好的。集成开发环境IDE)在快速开发应用中起着重要作用,Eclipse 为 JEE 提供了这样的环境。在 Eclipse 中,你不仅得到良好的代码编辑支持,还得到构建、单元测试、版本控制和软件应用开发不同阶段的重要任务的支持。

在本章中,我们将介绍以下主题:

  • JEE 不同技术的介绍

  • Eclipse 开发环境的介绍

  • 本书介绍的一些常用软件的安装和配置,例如,JEE 服务器、Eclipse IDE 和 MySQL 数据库服务器

本书的目标是展示如何通过使用 Eclipse 在应用开发的各个阶段使用其许多功能,高效地开发 JEE 应用。但首先,这里简要介绍 JEE 和 Eclipse。

2017 年,Oracle 同意将 Java EE 的控制权移交给 Eclipse 基金会。2018 年 4 月,Eclipse 基金会将 Java EE 更名为 Jakarta EE。您可以在jakarta.ee/找到有关 Jakarta EE 的更多信息。在撰写本文时,最新的 Java EE 版本是 8。但所有未来的 Java EE 版本都将被称为 Jakarta EE。

JEE

JEE 是 Java 社区过程(www.jcp.org)的许多项目的集合。目前,JEE 处于第 8 版。然而,JEE 的不同规范处于它们各自的不同版本。

JEE 规范可以大致分为以下几组:

  • 表示层

  • 业务层

  • 企业集成层

注意,JEE 规范并不一定将 API 分类到前面提到的广泛组中,但这种分类有助于更好地理解 JEE 中不同规范和 API 的目的。

在我们查看每个这些类别的 API 之前,让我们了解一个典型的 JEE Web 应用流程,如图中所示,以及前面的每一层适合的位置:

图 1.1:典型的 JEE Web 应用流程

请求从客户端开始。客户端可以是任何请求从远程应用程序获取服务的应用程序——例如,它可以是浏览器或桌面应用程序。请求首先由目标处的 Web 服务器接收。Web 服务器的例子包括 Apache Web 服务器、IIS 和 nginx。如果是一个静态内容的请求,则由 Web 服务器(们)提供。然而,动态请求通常需要一个应用服务器来处理。JEE 服务器就是这样处理动态请求的应用服务器。大多数 JEE 规范 API 在应用服务器中执行。JEE 应用服务器的例子包括 WebSphere、GlassFish 和 WildFly。

大多数非平凡 JEE 应用程序访问外部系统,例如数据库或 企业集成服务器EIS),以访问数据并处理它。响应从应用服务器返回到 Web 服务器,然后到客户端。

以下各节简要描述了不同层中每个 JEE 规范的概述。我们将在后续章节中更详细地了解如何使用这些规范及其 API。然而,请注意,以下不是 JEE 中所有规范的详尽列表。我们将在本节中查看最常用的规范。要获取详尽列表,请访问 www.oracle.com/technetwork/java/javaee/tech/index.html

表示层

此层的 JEE 规范或技术从 Web 服务器接收请求并返回响应,通常是 HTML 格式。然而,也可以仅从表示层返回数据,例如在 JavaScript 对象表示法JSON)或 可扩展标记语言XML)格式中,这可以通过 异步 JavaScript 和 XMLAJAX)调用仅更新页面的一部分,而不是渲染整个 HTML 页面。表示层中的类大多在 Web 容器中执行——它是处理 Web 请求的应用服务器的一部分。Tomcat 是流行的 Web 容器的一个例子。

现在,让我们来看看这一层的一些规范。

Java Servlets

Java Servlets 是服务器端模块,通常用于在 Web 应用程序中处理请求并返回响应。Servlets 对于处理不生成大量 HTML 标记响应的请求很有用。它们通常用作 模型-视图-控制器MVC)框架中的控制器,用于转发/重定向请求,或用于生成非 HTML 响应,例如 PDF。要从 Servlet 生成 HTML 响应,您需要在 Java 代码中嵌入 HTML 代码(作为 Java 字符串)。因此,它不是生成大量 HTML 响应最方便的选项。JEE 8 包含 Servlet API 4.0。

JavaServer Pages

类似于 servlet,JavaServer PagesJSP)也是用于处理 Web 请求的服务器端模块。JSP 非常适合处理生成大量 HTML 标记响应的请求。在 JSP 页面中,Java 代码或 JSP 标签可以与其他 HTML 代码(如 HTML 标签、JavaScript 和 CSS)混合使用。由于 Java 代码嵌入到更大的 HTML 代码中,因此从 JSP 页面生成 HTML 响应更容易(比 servlet)。JSP 规范 2.3 包含在 JEE 8 中。

JavaServer Faces

JavaServer FacesJSF)通过在其实现中结合 MVC 设计模式,使服务器端创建用户界面模块化。它还提供了易于使用的标签,用于常见的用户界面控件,可以在客户端和服务器之间的多个请求-响应交换中保存状态。例如,如果你有一个从浏览器提交表单数据的页面,你可以让 JSF 将数据保存到一个 Java Bean 中,以便在随后的响应中用于相同或不同的请求。JSF 还使在服务器端处理 UI 事件和指定应用程序中的页面导航变得更容易。

你可以在 JSP 中使用为 JSF 创建的自定义 JSP 标签来编写 JSF 代码。JavaServer Faces API 2.3 是 JEE 8 的一部分。

业务层

业务层是通常编写代码来处理应用程序业务逻辑的地方。对这个层的请求可能来自表示层、直接来自客户端应用程序,或来自由但不限于 Web 服务的中间层。这个层的类在 JEE 服务器的应用程序容器部分中执行。GlassFish 和 WebSphere 是 Web 容器加应用程序容器的例子。

让我们浏览一下这个组中的一些规范。

企业 JavaBeans

企业 JavaBeanEJB)是你可以编写业务逻辑的 Java 类。尽管使用 EJB 来编写业务逻辑不是强制性的要求,但它们确实提供了企业应用程序中许多基本的服务。这些服务包括安全性、事务管理、组件查找、对象池等。

你可以将 EJB 分布到多个服务器上,并让应用程序容器(也称为 EJB 容器)负责组件查找(搜索组件)和组件池(对可扩展性很有用)。这可以提高应用程序的可扩展性。

EJB 有两种类型:

  • 会话 Bean:会话 Bean 可以直接由客户端或中间层对象调用

  • 消息驱动的 Bean:消息驱动的 Bean 是在响应Java 消息服务JMS)事件时被调用的

JMS 和消息驱动豆可以用于处理异步请求。在一个典型的异步请求处理场景中,客户端将请求放入消息队列或主题,并不等待立即响应。服务器端的应用程序获取请求消息,要么直接使用 JMS API,要么通过使用 MDBs。它处理请求,并将响应放入不同的队列或主题,客户端会监听并获取响应。

Java EE 8 包含 EJB 规范 3.2 和 JMS 规范 2.0。

企业集成层

此层中的 API 用于与企业外部(相对于 JEE 应用程序)的系统进行交互。大多数应用程序都需要访问数据库,用于访问该数据库的 API 属于这一组。

Java 数据库连接

Java 数据库连接JDBC)是一种规范,用于以通用和一致的方式访问关系数据库。使用 JDBC,您可以使用通用 API 执行 SQL 语句并在不同的数据库上获取结果。数据库特定的驱动程序位于 JDBC 调用和数据库之间,并将 JDBC 调用转换为数据库供应商特定的 API 调用。JDBC 可以直接在表示层和业务层中使用,但建议将数据库调用从 UI 和业务代码中分离出来。通常,这是通过创建数据访问对象DAOs)来完成的,这些对象封装了访问数据库的逻辑。实际上,JDBC 是 Java 标准版的一部分。Java SE 8 包含 JDBC 4.2。

Java 持久化 API

使用 JDBC API 直接的一个问题是您必须不断在 Java 对象和关系数据库中列或行中的数据之间映射数据。例如 Hibernate 和 Spring 这样的框架通过使用称为对象关系映射ORM)的概念使这个过程变得简单。ORM 以Java 持久化 APIJPA)的形式融入 JEE。

JPA 为您提供了将对象映射到关系数据库中的表,并使用或不用结构化查询语言SQL)执行查询的灵活性。当在 JPA 的内容中使用时,查询语言被称为Java 持久化查询语言。JPA 规范 2.2 是 JEE8 的一部分。

Java 连接器架构

Java 连接器架构JCA)API 可以在 JEE 应用程序中使用,用于与企业集成系统(EISes),如 SAP 和 Salesforce 进行通信。就像您有数据库驱动程序在 JDBC API 和关系数据库之间进行通信中介一样,您在 JCA 调用和 EISes 之间有 JCA 适配器。现在大多数 EIS 应用程序都提供 REST API,这些 API 轻量级且易于使用,因此 REST 可以在某些情况下替代 JCA。然而,如果您使用 JCA,您将从 JEE 应用程序服务器获得事务和连接池支持。

互联网服务

Web 服务是远程应用程序组件,并公开自包含的 API。Web 服务可以根据以下两个标准进行广泛分类:

  • 简单对象访问协议SOAP

  • 表示状态转移REST

Web 服务可以在集成不同应用程序中发挥重要作用,因为它们基于标准且平台无关。

JEE 提供了许多规范以简化开发和对两种类型 Web 服务的使用,例如,JAX-WS(Java API for XML—web services)和 JAX-RS(Java API for RESTful web services)。

上述只是 JEE 中的一部分规范。还有许多其他独立规范和许多启用规范,如依赖注入和并发工具,我们将在后续章节中看到。

Eclipse IDE

一个好的 IDE 对于提高编码时的生产力至关重要。Eclipse 就是这样一种 IDE,它具有出色的编辑器功能和许多与 JEE 技术的集成点。本书的主要目的是向您展示如何使用 Eclipse 开发 JEE 应用程序。因此,以下是对 Eclipse 的快速介绍,如果您还不熟悉它的话。

Eclipse 是一个开源 IDE,用于开发多种不同编程语言的应用程序。它因开发多种不同类型的 Java 应用程序而非常受欢迎。其架构是可插拔的——有一个核心 IDE 组件,可以添加许多不同的插件。实际上,许多语言的支持都是作为 Eclipse 插件添加的,包括对 Java 的支持。

除了编辑器支持外,Eclipse 还提供了插件来与开发过程中使用的许多外部系统进行交互。例如,包括源代码控制系统如 SVN 和 Git,构建工具如 Apache Ant 和 Maven,用于远程系统(使用 FTP)的文件浏览器,管理服务器如 Tomcat 和 GlassFish,数据库浏览器,内存和 CPU 分析器。我们将在后续章节中看到许多这些功能。以下截图显示了 Eclipse 为 JEE 应用程序开发提供的默认视图:

图片

图 1.2:默认 Eclipse 视图

在使用 Eclipse 时,了解以下术语是很有帮助的。

工作空间

Eclipse 工作空间是一组项目、设置和首选项的集合。这是一个 Eclipse 存储这些信息的文件夹。您必须创建一个工作空间才能开始使用 Eclipse。您可以创建多个工作空间,但一次只能由一个 Eclipse 运行实例打开一个。然而,您可以使用不同工作空间的多个 Eclipse 实例。

插件

Eclipse 具有可插拔架构。Eclipse 的许多功能都是作为插件实现的,例如,Java 和其他语言的编辑器插件,SVN 和 Git 插件,等等。Eclipse 的默认安装包含许多内置插件,并且您可以添加更多插件以获得您想要的特性。

编辑器和视图

Eclipse 中的大多数窗口都可以归类为编辑器或视图。编辑器是你可以更改其中显示的信息的地方。视图仅显示信息,不允许你更改它。Java 编辑器是一个编辑器的例子,你在其中编写代码。大纲视图是一个视图的例子,它显示你正在编辑的代码的分层结构(在 Java 编辑器的情况下,它显示正在编辑的文件中的类和方法)。

要查看给定 Eclipse 安装中的所有视图,请打开“窗口 | 显示视图 | 其他”菜单:

图 1.3:显示所有 Eclipse 视图

视图

视图是一个编辑器和视图的集合,以及它们如何在主 Eclipse 窗口中布局或排列。在开发的各个阶段,你需要显示不同的视图。例如,当你编辑代码时,你需要看到项目资源管理器和任务视图,但在调试应用程序时,你不需要这些视图,而是希望看到变量和断点视图。因此,编辑视图显示其他视图和编辑器中的项目资源管理器和任务视图,而调试视图显示与调试活动相关的视图和编辑器。你可以更改默认视图以适应你的需求。

Eclipse 首选项

Eclipse 首选项窗口(图 1.4)是自定义许多插件/功能的地方。首选项在 Eclipse 的 Windows 和 Linux 安装的“窗口”菜单中可用,在 Mac 的 Eclipse 菜单中:

图 1.4:Eclipse 首选项

安装产品

在随后的章节中,我们将学习如何在 Eclipse 中开发 JEE 应用程序。但是,这些应用程序将需要一个 JEE 应用程序服务器和一个数据库。在前几章中,我们将使用 Tomcat 网络容器,然后使用 GlassFish JEE 应用程序服务器。我们将使用 MySQL 数据库。

我们将需要这些产品来开发我们即将开发的大多数应用。因此,以下章节将描述如何安装和配置 Eclipse、Tomcat、GlassFish 和 MySQL。

安装 Eclipse

eclipse.org/downloads/下载 Eclipse 的最新版本。你会看到许多不同的 Eclipse 包。确保安装 Eclipse IDE for Java EE Developers 包。根据你的操作系统和 JVM 架构(32 位或 64 位)选择合适的包。你可能需要运行命令java -version来了解 JVM 是 32 位还是 64 位。

如果你计划使用 Eclipse 进行 AWS 开发,那么建议从 Oomph 安装程序下载 Eclipse。请参阅wiki.eclipse.org/Eclipse_Installerdocs.aws.amazon.com/toolkit-for-eclipse/v1/user-guide/setup-install.html

解压下载的 ZIP 文件,然后运行 Eclipse 应用程序(在运行 Eclipse 之前,您必须安装 JDK)。第一次运行 Eclipse 时,您将被要求指定一个工作空间。在您的文件系统中创建一个新的文件夹,并将其作为初始工作空间文件夹选择。如果您打算在每次启动 Eclipse 时使用相同的文件夹作为工作空间,请勾选“使用此作为默认值,不再询问”复选框:

图 1.5:选择 Eclipse 工作空间

您将看到如图 1.2 所示的默认 Java EE 视图。

安装 Tomcat 服务器

Tomcat 是一个 Web 容器。它支持之前描述的表示层 API。此外,它还支持 JDBC 和 JPA。配置简单,如果您不想使用 EJBs,它可能是一个不错的选择。

tomcat.apache.org/ 下载最新版本的 Tomcat。将下载的文件解压到一个文件夹中。设置 JAVA_HOME 环境变量,使其指向 JDK 安装文件夹(文件夹路径应该是包含 bin 子文件夹的 JDK 文件夹)。要在 Windows 的命令提示符中启动服务器,请运行 startup.bat;在 Mac 和 Linux 的终端窗口中运行 startup.sh。如果没有错误,您应该会看到消息 Server startup in --msTomcat started

默认的 Tomcat 安装配置为使用端口 8080。如果您想更改端口,请打开位于 conf 文件夹下的 server.xml 文件,并查找如下所示的连接器声明:

<Connector port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443" /> 

将端口号更改为您想要的任何端口号,尽管在这本书中我们将使用默认端口 8080。在我们打开 Tomcat 的默认页面之前,我们将为 Tomcat 服务器的管理添加一个用户。使用任何文本编辑器打开位于 conf 文件夹下的 tomcat-users.xml 文件。在文件末尾,您将看到如何添加用户的注释示例。在 </tomcat-users> 标签关闭之前添加以下配置:

  <role rolename="manager-gui"/> 
  <user username="admin" password="admin" roles="manager-gui"/> 

在这里,我们正在向名为 manager-gui 的角色添加一个用户 admin,密码也是 admin。此角色可以访问 Tomcat 中管理应用程序的网页。此角色和其他安全角色在 manager 应用程序的 web.xml 文件中定义。您可以在 webapps/manager/WEB-INF/web.xml 中找到它。

关于管理 Tomcat 服务器的更多信息,请参阅 tomcat.apache.org/tomcat-8.0-doc/manager-howto.html

在进行上述更改后,打开一个网页浏览器,并浏览到 http://localhost:8080(如果您已更改默认端口,请修改端口号)。您将看到以下默认的 Tomcat 页面:

图 1.6:默认的 Tomcat Web 应用程序

点击右侧的“Manager App”按钮。您将被要求输入用户名和密码。输入您之前在 tomcat-users.xml 中为 manager-gui 配置的用户名和密码,如前所述。成功登录后,您将看到 图 1.7 所示的 Tomcat 网络应用程序管理器页面。您可以在该页面上看到 Tomcat 中部署的所有应用程序。您也可以从该页面部署应用程序:

图片

图 1.7:Tomcat 网络应用程序管理器

要停止 Tomcat 服务器,请按 Ctrl/cmd + C 或在 bin 文件夹中运行关机脚本。

安装 GlassFish 服务器

glassfish.java.net/download.html 下载 GlassFish。GlassFish 有两种版本:Web Profile 和 Full Platform。Web Profile 类似于 Tomcat,它不包括 EJB 支持。因此,请下载 Full Platform。

在文件夹中解压下载的文件。GlassFish 服务器的默认端口是 8080。如果您想更改它,请在文本编辑器中打开 glassfish/domains/domain1/config/domain.xml(您也可以使用 Eclipse,通过“文件 | 打开文件”菜单选项打开)并查找 8080。您应该在 <network-listener> 中的一个位置看到它。如果您想更改端口(如果其他应用程序已经使用该端口,则可能需要这样做),请更改它。

要启动服务器,运行 startserv 脚本(.bat.sh 取决于您使用的操作系统)。一旦服务器启动,打开网页浏览器并浏览到 http://localhost:8080。您应该会看到一个像以下这样的页面:

图片

图 1.8:默认 Glassfish 网络应用程序

此页面位于 glassfish/domains/domain1/docroot/index.html。点击页面上的“转到管理控制台”链接以打开 GlassFish 管理员(见以下截图):

图片

图 1.9:Glassfish 管理员

有关管理 GlassFish 服务器详情,请参阅 javaee.github.io/glassfish/doc/5.0/administration-guide.pdf

要停止 GlassFish 服务器,在 glassfish/bin 文件夹中运行 stopserv 脚本。

安装 MySQL

我们将在本书的许多示例中使用 MySQL 数据库。以下各节描述了如何为不同平台安装和配置 MySQL。

我们还希望安装 MySQL Workbench,这是一个用于管理 MySQL 服务器的客户端应用程序。从 dev.mysql.com/downloads/workbench/ 下载 MySQL Workbench。

在 Windows 上安装 MySQL

dev.mysql.com/downloads/mysql/ 下载 MySQL Community Server。您可以选择下载网络安装程序或全功能安装程序。网络安装程序只会下载您所选的组件。以下说明使用网络安装程序显示下载选项。

网络安装程序首先下载一个小应用程序,并为你提供了选择要安装的组件的选项:

  1. 选择自定义选项并点击下一步:

img/00014.jpeg

图 1.10:Windows 的 MySQL 安装程序

  1. 选择 MySQL 服务器和 MySQL Workbench 产品并完成安装。在服务器安装过程中,你将被要求设置root密码,并可以选择添加更多用户。为应用程序添加一个非 root 用户总是个好主意:

img/00015.jpeg

图 1.11:选择要安装的 MySQL 产品和功能

  1. 确保在添加用户时选择所有主机,这样你就可以从任何有网络访问 MySQL 安装机器的远程机器访问 MySQL 数据库:

img/00016.jpeg

图 1.12:添加 MySQL 用户

  1. 安装完成后运行 MySQL Workbench。你会发现默认的本地 MySQL 实例连接已经为你创建好了!img/00017.jpeg

图 1.13:MySQL Workbench 连接

  1. 点击本地连接,系统会要求你输入root密码。输入你在安装 MySQL 服务器时设置的root密码。MySQL Workbench 打开并显示默认的测试模式:

img/00018.jpeg

图 1.14:MySQL Workbench

在 macOS X 上安装 MySQL

OS X 版本在 10.7 之前的系统默认安装了 MySQL 服务器。如果你使用的是 OS X 10.7 或更高版本,那么你需要从dev.mysql.com/downloads/mysql/下载并安装 MySQL 社区服务器。

在 OS X 上安装 MySQL 也有多种不同的方法。有关 OS X 的安装说明,请参阅dev.mysql.com/doc/refman/5.7/en/osx-installation.html。请注意,OS X 用户在安装 MySQL 服务器时需要有管理员权限。

安装服务器后,你可以从命令提示符或系统偏好设置中启动它:

  1. 要从命令提示符启动,请在终端中执行以下命令:
sudo /usr/local/mysql/support-files/mysql.server start  
  1. 要从系统偏好设置启动,打开偏好设置并点击 MySQL 图标:

img/00019.jpeg

图 1.15:MySQL 系统偏好设置 - OS X

  1. 点击“启动 MySQL 服务器”按钮。

在 Linux 上安装 MySQL

在 Linux 上安装 MySQL 有多种不同的方法。有关详细信息,请参阅dev.mysql.com/doc/refman/5.7/en/linux-installation.html

创建 MySQL 用户

你可以从命令提示符或使用 MySQL Workbench 创建 MySQL 用户:

  1. 要从命令提示符执行 SQL 和其他命令,请打开终端并输入以下命令:
mysql -u root -p<root_password> 
  1. 登录成功后,你会看到mysql命令提示符:
mysql>  
  1. 要创建用户,首先选择mysql数据库:
mysql>use mysql;
Database changed
mysql>create user 'user1'@'%' identified by 'user1_pass';  
mysql>grant all privileges on *.* to 'user1'@'%' with grant option

上述命令将创建一个名为 'user1' 的用户,密码为 'user1_pass',拥有所有权限,例如插入、更新和从数据库中选择。而且因为我们指定了主机为 '%',这个用户可以从任何主机访问服务器。

有关将用户添加到 MySQL 数据库的更多详细信息,请参阅dev.mysql.com/doc/refman/5.7/en/adding-users.html

如果你更喜欢使用 图形用户界面GUI)来管理用户,那么运行 MySQL Workbench,连接到本地 MySQL 服务器(参见 图 1.13 MySQL Workbench 连接),然后在管理部分下点击用户和权限:

图 1.16:在 MySQL Workbench 中创建用户

安装完所有前面的产品后,你应该能够开始开发 JEE 应用程序。我们可能需要一些额外的软件,但我们将在适当的时候看到如何安装和配置它。

摘要

在本章中,我们简要介绍了用于表示层、业务层和企业集成层的不同 JEE 规范。我们学习了 Eclipse IDE 中的一些重要术语。然后我们学习了如何安装 Eclipse、Tomcat、Glassfish、MySQL 和 MySQL Workbench。我们将在本书中使用这些产品来开发 JEE 应用程序。

在下一章中,我们将配置 JEE 服务器并使用 servlets、JSPs 和 JSFs 创建一个简单应用程序。我们还将学习如何使用 Maven 构建和打包 JEE 应用程序。

第二章:创建一个简单的 JEE Web 应用程序

上一章为您简要介绍了 JEE 和 Eclipse。我们还学习了如何安装 Eclipse JEE 包以及如何安装和配置 Tomcat。Tomcat 是一个 servlet 容器,它易于使用和配置。因此,许多开发者使用它在本地上运行 JEE Web 应用程序。

本章我们将涵盖以下主题:

  • 在 Eclipse 中配置 Tomcat 并从 Eclipse 部署 Web 应用程序

  • 使用不同的技术创建 JEE 中的 Web 应用程序,例如 JSP、JSTL、JSF 和 servlet

  • 使用 Maven 依赖管理工具

在 Eclipse 中配置 Tomcat

我们将执行以下步骤以在 Eclipse 中配置 Tomcat:

  1. 在 Eclipse 的 Java EE 视图中,您将在底部找到“服务器”选项卡。由于尚未添加服务器,您将在选项卡中看到一个链接,如下面的截图所示——没有可用的服务器。点击此链接以创建一个新的服务器……

图 2.1:Eclipse JEE 中的“服务器”选项卡

  1. 点击“服务器”选项卡中的链接以添加新的服务器。

  2. 展开Apache组并选择您已安装的 Tomcat 版本。如果 Eclipse 和 Tomcat 服务器在同一台机器上,则将服务器的计算机名保留为localhost。否则,输入 Tomcat 服务器的计算机名或 IP 地址。点击“下一步”:

图 2.2:在新建服务器向导中选择服务器

  1. 点击“浏览...”按钮并选择 Tomcat 安装的文件夹。

  2. 点击“下一步”直到完成向导。在最后,您将在“服务器”视图中看到添加的 Tomcat 服务器。如果 Tomcat 还未启动,您将看到状态为“停止”。

图 2.3:在新建服务器向导中配置 Tomcat 文件夹

  1. 要启动服务器,右键单击服务器并选择“启动”。您也可以通过点击“服务器”视图工具栏中的“启动”按钮来启动服务器。

图 2.4:添加到“服务器”视图中的 Tomcat 服务器

服务器成功启动后,您将看到状态更改为“已启动”。如果您点击“控制台”选项卡,您将看到 Tomcat 服务器在启动期间输出的控制台消息。

如果您在“项目资源管理器”视图中展开“服务器”组,您将看到您刚刚添加的 Tomcat 服务器。展开 Tomcat 服务器节点以查看配置文件。这是一种编辑 Tomcat 配置的简单方法,这样您就不必在文件系统中查找配置文件。

双击 server.xml 以在 XML 编辑器中打开它。您将获得“设计视图”以及“源视图”(编辑器底部的两个选项卡)。我们在上一章学习了如何更改 Tomcat 的默认端口。您可以通过打开 server.xml 并转到 Connector 节点来轻松地在 Eclipse 编辑器中更改它。如果您需要搜索文本,您可以将编辑器切换到源选项卡(编辑器底部的选项卡)。

图 2.5:打开 server.xml

你也可以轻松编辑tomcat-users.xml以添加/编辑 Tomcat 用户。回想一下,我们在第一章,“介绍 JEE 和 Eclipse”中添加了一个 Tomcat 用户来管理 Tomcat 服务器。

默认情况下,当你在 Eclipse 中添加服务器时,Eclipse 不会在 Tomcat 安装文件夹中做任何更改。相反,它会在工作区中创建一个文件夹,并将 Tomcat 配置文件复制到这个文件夹中。在 Tomcat 中部署的应用程序也会从这个文件夹中复制和发布。这在开发阶段工作得很好,当你不想修改 Tomcat 设置或服务器中部署的任何应用程序时。然而,如果你想使用实际的 Tomcat 安装文件夹,那么你需要修改 Eclipse 中的服务器设置。在服务器视图中双击服务器以在编辑器中打开它。

图片

图 2.6:Tomcat 设置

注意服务器位置下的选项。如果你想使用实际的 Tomcat 安装文件夹进行配置和从 Eclipse 内部发布应用程序,请选择第二个选项,使用“Tomcat 安装”。

JavaServer Pages

我们将从一个创建简单 JSP 的项目开始。我们将创建一个登录 JSP,该 JSP 将数据提交给自己并验证用户。

创建动态网络项目

我们将执行以下步骤来创建动态网络项目:

  1. 选择“文件 | 新建 | 其他”菜单。这会打开选择向导。在向导的顶部,你将找到一个带有交叉图标在极右侧的文本框。

  2. 在文本框中输入web。这是过滤器框。Eclipse 中的许多向导和视图都有这样的过滤器文本框,这使得查找项目变得非常容易。

图片

图 2.7:新建选择向导

  1. 选择动态网络项目并点击“下一步”以打开动态网络项目向导。输入项目名称,例如,LoginSampleWebApp。注意,在此页面的动态网络模块版本字段中列出了 Servlet API 版本号。选择 3.0 或更高版本。点击“下一步”。

图片

图 2.8:新建动态网络项目向导

  1. 在以下页面中点击“下一步”,在最后一页点击“完成”以创建LoginSimpleWebApp项目。此项目也添加到项目资源管理器中。

图片

图 2.9:新建网络项目

Java 源文件放在Java Resources下的src文件夹中。Web 资源,如 HTML、JS 和 CSS 文件,放在WebContent文件夹中。

在下一节中,我们将创建一个登录 JSP 页面。

在第一个 JSP 中,为了保持页面简单,我们不会遵循许多最佳实践。我们将 UI 代码与应用程序业务代码混合。这种设计在真实的应用程序中不被推荐,但可能对快速原型设计有用。我们将在本章的后面部分看到如何编写具有清晰分离 UI 和业务逻辑的更好的 JSP 代码。

创建 JSP

我们将执行以下步骤来创建 JSP:

  1. 右键单击WebContent文件夹,选择“新建”|“JSP 文件”。将其命名为index.jsp。文件将在编辑器中以分割视图打开。顶部部分显示设计视图,底部部分显示代码。如果文件未在分割编辑器中打开,请右键单击项目资源管理器中的index.jsp,然后选择“打开方式”|“网页编辑器”。

图片

图 2.10:JSP 编辑器

  1. 如果您不喜欢分割视图,并想看到完整的设计视图或代码视图,请使用右上角的相应工具栏按钮,如下面的截图所示:

图片

图 2.11:JSP 编辑器显示按钮

  1. 将标题从Insert title here更改为Login

  2. 现在我们来看看 Eclipse 如何为 HTML 标签提供代码辅助。请注意,输入字段必须位于form标签内。我们稍后会添加form标签。在body标签内,输入User Name:标签。然后,输入<。如果您稍等片刻,Eclipse 会弹出代码辅助窗口,显示所有有效 HTML 标签的选项。您也可以手动调用代码辅助。

  3. <之后放置一个光标,并按Ctrl + Spacebar

图片

图 2.12:JSP 中的 HTML 代码辅助

代码辅助也适用于部分文本;例如,如果您在文本<i之后调用代码辅助,您将看到以i开头的 HTML 标签列表(iiframeimginput等)。您还可以使用代码辅助进行标签属性和属性值的操作。

目前,我们想要插入用户名的input字段。

  1. 从代码辅助建议中选择input,或者直接输入它。

  2. 在插入input元素后,将光标移至关闭的>内部,并再次调用代码辅助(Ctrl/Cmd + Spacebar)。您将看到input标签属性的提议列表。

图片

图 2.13:标签属性值的代码辅助

  1. 输入以下代码以创建登录表单:
<body> 
  <h2>Login:</h2> 
  <form method="post"> 
    User Name: <input type="text" name="userName"><br> 
    Password: <input type="password" name="password"><br> 
    <button type="submit" name="submit">Submit</button> 
    <button type="reset">Reset</button> 
  </form> 
</body> 

下载示例代码

您可以从您在www.packtpub.com的账户下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

如果您正在使用分割编辑器(设计和源页面),您可以在设计视图中看到登录表单的渲染效果。如果您想查看网页在浏览器中的外观,请点击编辑器底部的预览标签。您将看到网页在编辑器内的浏览器视图中显示。因此,您不需要离开 Eclipse 来测试您的网页。

图片

图 2.14:设计和源视图

如果你在设计视图中点击任何用户界面控件,你将在属性视图中看到其属性(见图 2.14)。你可以编辑属性,例如所选元素的名称和值。点击属性窗口的“样式”选项卡来编辑元素的 CSS 样式。

在先前的表单中,我们没有指定action属性。此属性指定当用户点击“提交”按钮时,表单数据要提交到的 URL。如果此属性未指定,则请求或表单数据将提交到同一页面;在这种情况下,表单数据将提交到index.jsp。我们现在将编写处理表单数据的代码。

如第一章中所述,“介绍 JEE 和 Eclipse”,你可以在同一个 JSP 中编写 Java 代码和客户端代码(HTML、CSS 和 JavaScript)。将 Java 代码与 HTML 代码混合通常不被认为是好的做法,但我们将在这个例子中这样做以使代码更简单。本书的后面部分,我们将看到如何使我们的代码模块化。

Java 代码在 JSP 中用<%%>编写;JSP 中的这些 Java 代码块被称为脚本片段。你还可以在 JSP 中设置页面级属性。它们被称为页面指令,并包含在<%@%>之间。我们创建的 JSP 已经有一个页面指令来设置页面的内容类型。内容类型告诉浏览器服务器返回的响应类型(在这种情况下,html/text)。浏览器根据内容类型显示适当的响应:

<%@ page language="java" contentType="text/html; charset=UTF-8" 
    pageEncoding="UTF-8"%> 

在 JSP 中,你可以访问一些对象来帮助你处理和生成响应,如下表所述:

对象名称 类型
request HttpServletRequest (docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequest.html). 使用此对象获取请求参数和其他与请求相关的数据。
response HttpServletResponse (docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletResponse.html). 使用此对象发送响应。
out JSPWriter (docs.oracle.com/javaee/7/api/javax/servlet/jsp/JspWriter.html). 使用此对象生成文本响应。
session HttpSession (docs.oracle.com/javaee/7/api/javax/servlet/http/HttpSession.html). 使用此对象在会话中获取或放置对象。
application ServletContext (docs.oracle.com/javaee/7/api/javax/servlet/ServletContext.html). 使用此对象在上下文中获取或放置对象,这些对象在同一个应用程序中的所有 JSP 和 servlet 之间共享。

在这个例子中,我们将利用requestout对象。我们首先检查是否使用POST方法提交了表单。如果是,我们将获取用户名和密码字段的值。如果凭证有效(在这个例子中,我们将硬编码用户名为admin和密码),我们将打印一条欢迎信息:

<% 
  String errMsg = null; 
  //first check whether the form was submitted 
  if ("POST".equalsIgnoreCase(request.getMethod()) && 
   request.getParameter("submit") != null) 
  { 
    //form was submitted 
    String userName = request.getParameter("userName"); 
    String password = request.getParameter("password"); 
    if ("admin".equalsIgnoreCase(userName) && 
     "admin".equalsIgnoreCase(password)) 
    { 
      //valid user 
      System.out.println("Welcome admin !"); 
    } 
    else 
    { 
      //invalid user. Set error message 
      errMsg = "Invalid user id or password. Please try again"; 
    } 
  } 
%> 

在前面的代码中,我们使用了两个内置对象——requestout。我们首先检查表单是否已提交——“"POST".equalsIgnoreCase(request.getMethod())”。然后,我们检查是否使用了提交按钮来提交表单——“request.getParameter("submit") != null”。

我们通过调用request.getParameter方法来获取用户名和密码。为了使代码简单,我们将其与硬编码的值进行比较。在实际应用程序中,您很可能会将凭证与数据库或某些命名和文件夹服务进行验证。如果凭证有效,我们将使用outJSPWriter)对象打印一条消息。如果凭证无效,我们将设置一个错误消息。我们将在登录表单之前打印任何错误消息:

<h2>Login:</h2> 
  <!-- Check error message. If it is set, then display it --> 
  <%if (errMsg != null) { %> 
    <span style="color: red;"><%=;"><%=;"><%=errMsg %></span> 
  <%} %> 
  <form method="post"> 
  ... 
  </form> 

在这里,我们通过使用<%%>开始另一个 Java 代码块。如果错误消息不为空,我们将使用span标签显示它。注意错误消息的值是如何打印的——“<%=errMsg %>”。这是一个<%out.print(errMsg);%>的简写语法。同时注意,第一个 Java 代码块开始的大括号在下一个独立的 Java 代码块中完成。在这两个代码块之间,您可以添加任何 HTML 代码,并且只有当if语句中的条件表达式评估为真时,它才会包含在响应中。

这里是我们在本节中创建的 JSP 的完整代码:

<%@ page language="java" contentType="text/html; charset=UTF-8" 
    pageEncoding="UTF-8"%> 
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
 "http://www.w3.org/TR/html4/loose.dtd"> 
<html> 
<head> 
<meta http-equiv="Content-Type" content="text/html; 
charset=UTF-8"> 
<title>Login</title> 
</head> 
<% 
  String errMsg = null; 
  //first check whether the form was submitted 
  if ("POST".equalsIgnoreCase(request.getMethod()) && 
   request.getParameter("submit") != null) 
  { 
    //form was submitted 
    String userName = request.getParameter("userName"); 
    String password = request.getParameter("password"); 
    if ("admin".equalsIgnoreCase(userName) && 
     "admin".equalsIgnoreCase(password)) 
    { 
      //valid user 
      out.println("Welcome admin !"); 
      return; 
    } 
    else 
    { 
      //invalid user. Set error message 
      errMsg = "Invalid user id or password. Please try again"; 
    } 
  } 
%> 
<body> 
  <h2>Login:</h2> 
  <!-- Check error message. If it is set, then display it --> 
  <%if (errMsg != null) { %> 
    <span style="color: red;"><%out.print(errMsg); %></span> 
  <%} %> 
  <form method="post"> 
    User Name: <input type="text" name="userName"><br> 
    Password: <input type="password" name="password"><br> 
    <button type="submit" name="submit">Submit</button> 
    <button type="reset">Reset</button> 
  </form> 
</body> 
</html> 

在 Tomcat 中运行 JSP

要在 Web 浏览器中运行上一节中创建的 JSP,您需要在 servlet 容器中部署应用程序。我们已经看到了如何在 Eclipse 中配置 Tomcat。确保 Tomcat 正在运行,可以通过检查 Eclipse 的“服务器视图”中的状态来确认:

图片

图 2.15:在服务器视图中启动 Tomcat

有两种方法可以将项目添加到配置的服务器,以便在服务器上运行应用程序:

  1. 在“服务器视图”中右键单击服务器,选择“添加和移除”选项。从左侧的列表(可用资源)中选择您的项目,然后单击“添加”将其移动到“配置”列表。单击“完成”。

图片

图 2.16:将项目添加到服务器

  1. 将项目添加到服务器的另一种方法是右键单击“项目资源管理器”中的项目,选择“属性”。这将打开“项目属性”对话框。在列表中单击“服务器”,然后选择您想要部署此项目的服务器。单击“确定”或“应用”。

图片

图 2.17:在项目属性中选择服务器

在第一种方法中,项目将立即在服务器上部署。在第二种方法中,只有在您在服务器上运行项目时才会部署。

  1. 要运行应用程序,在项目资源管理器中右键单击项目,然后选择“运行”|“在服务器上运行”。第一次运行时,将提示您重新启动服务器。一旦应用程序部署,您将在服务器视图中看到它:

图 2.18:在服务器上部署的项目

  1. 在用户名和密码框中输入除 admin 之外的其他文本

    然后点击提交。您应该看到错误消息,并且应该再次显示相同的表单。

图 2.19:在 Eclipse 内置浏览器中运行的项目

  1. 现在输入 admin 作为用户名和密码,然后提交表单。您应该看到欢迎信息。

JSPs 是动态编译成 Java 类的,所以如果您在页面上进行了任何更改,在大多数情况下,您不需要重新启动服务器;只需刷新页面,如果页面已更改,Tomcat 将重新编译页面,并显示修改后的页面。在需要重新启动服务器以应用更改的情况下,Eclipse 将提示您是否要重新启动服务器。

在 JSP 中使用 JavaBeans

我们之前创建的 JSP 并未遵循 JSP 最佳实践。一般来说,在 JSP 中包含脚本(Java 代码)是一个不好的主意。在大多数大型组织中,UI 设计师和程序员是不同的角色,由不同的人执行。因此,建议 JSP 主要包含标记标签,以便设计师更容易进行页面设计。Java 代码应放在单独的类中。从可重用性的角度来看,将 Java 代码移出 JSP 也是有意义的。

您可以从 JSP 将业务逻辑的处理委托给 JavaBeans。JavaBeans 是具有属性和获取器/设置器方法的简单 Java 对象。JavaBeans 中获取器/设置器方法的命名约定是前缀 get/set 后跟属性名称,每个单词的首字母大写,也称为驼峰式命名法。例如,如果您有一个名为 firstName 的类属性,则获取器方法将是 getFirstName,设置器将是 setFirstName

JSP 有一个用于使用 JavaBeans 的特殊标签——jsp:useBean

<jsp:useBean id="name_of_variable" class="name_of_bean_class" 
 scope="scope_of_bean"/>

范围表示豆子的生命周期。有效值有 applicationpagerequestsession

作用域名称 描述
page 豆子只能在当前页面中使用。
request 豆子可以在处理相同请求的任何页面中使用。如果一个页面将请求转发到另一个页面,一个 Web 请求可以由多个 JSP 处理。
session 豆子可以在相同的 HTTP 会话中使用。如果您的应用程序想要在与应用程序的每次交互中保存用户数据,例如在在线商店应用程序中保存购物车中的项目,会话非常有用。
application Bean 可以在同一个 web 应用程序中的任何页面中使用。通常,web 应用程序作为 web 应用程序存档 (WAR) 文件部署在 web 应用程序容器中。在应用程序范围内,WAR 文件中的所有 JSP 都可以使用 JavaBeans。

我们将把验证用户的代码移动到我们的登录示例中的 JavaBean 类。首先,我们需要创建一个 JavaBean 类:

  1. 在项目资源管理器中,右键单击 src 文件夹,选择 New | Package 菜单选项。

  2. 创建一个名为 packt.book.jee_eclipse.ch2.bean 的包。

  3. 右键单击包,选择 New | Class 菜单选项。

  4. 创建一个名为 LoginBean 的类。

  5. 按照以下方式创建两个私有 String 成员:

public class LoginBean { 
  private String userName; 
  private String password; 
} 
  1. 在类内部(在编辑器中)的任何位置右键单击,并选择 Source | Generate Getters and Setters 菜单选项:

图片 00040

图 2.20:生成获取器和设置器

  1. 我们希望为类的所有成员生成获取器和设置器。因此,点击全选按钮,并从下拉列表中选择插入点为最后一个成员,因为我们希望在声明所有成员变量之后插入获取器和设置器。

LoginBean 类现在应该如下所示:

public class LoginBean { 
private String userName; 
  private String password; 
  public String getUserName() { 
    return userName; 
  } 
  public void setUserName(String userName) { 
    this.userName = userName; 
  } 
  public String getPassword() { 
    return password; 
  } 
  public void setPassword(String password) { 
    this.password = password; 
  } 
} 
  1. 我们将向其中添加一个额外的验证用户名和密码的方法:
public boolean isValidUser() 
  { 
    //Validation can happen here from a number of sources 
    //for example, database and LDAP 
    //We are just going to hardcode a valid username and 
    //password here. 
    return "admin".equals(this.userName) && 
            "admin".equals(this.password); 
  } 

这完成了我们用于存储用户信息和验证的 JavaBean。

现在,我们将使用这个 Bean 在我们的 JSP 中,并将验证用户的任务委托给这个 Bean。打开 index.jsp。将前面代码中 <body> 标签上面的 Java 脚本替换为以下内容:

<%String errMsg = null; %> 
<%if ("POST".equalsIgnoreCase(request.getMethod()) && request.getParameter("submit") != null) {%> 
  <jsp:useBean id="loginBean" 
   class="packt.book.jee_eclipse.ch2.bean.LoginBean"> 
    <jsp:setProperty name="loginBean" property="*"/> 
  </jsp:useBean> 
  <% 
    if (loginBean.isValidUser()) 
    { 
      //valid user 
      out.println("<h2>Welcome admin !</h2>"); 
      out.println("You are successfully logged in"); 
    } 
    else 
    { 

      errMsg = "Invalid user id or password. Please try again"; 

    } 
  %> 
<%} %> 

在我们讨论前面代码中的更改之前,请注意,您还可以调用并获取 <jsp:*> 标签的属性和值的代码辅助。如果您不确定代码辅助是否可用,只需按 Ctrl/Cmd + C

图片 00041

图 2.21:JSP 标签中的代码辅助

注意,Eclipse 显示了我们刚刚添加的 JavaBean 的代码辅助。

让我们现在理解我们在 JSP 中做了什么更改:

  • 我们创建了多个脚本,一个用于声明 errMsg 变量,另外两个用于单独的 if 块。

  • 我们在第一个 if 条件中添加了一个 <jsp:useBean> 标签。当 if 语句中的条件为真时,即当通过点击提交按钮提交表单时,会创建该 Bean。

  • 我们使用了 <jsp:setProperty> 标签来设置 Bean 的属性:

<jsp:setProperty name="loginBean" property="*"/> 

我们正在设置 loginBean 的成员变量值。此外,我们通过指定 property="*" 来设置所有成员变量的值。然而,我们在哪里指定这些值呢?因为这些值是隐式指定的,因为我们已经将 LoginBean 的成员名称命名为与表单中的字段相同的名称。因此,JSP 运行时会从 request 对象中获取参数,并将具有相同名称的值分配给 JavaBean 成员。

如果 JavaBean 成员的名称与请求参数不匹配,那么您需要显式设置这些值:

<jsp:setProperty name="loginBean" property="userName" 
  value="<%=request.getParameter("userName")%>"/> 
<jsp:setProperty name="loginBean" property="password" 
  value="<%=request.getParameter("password")%>"/> 
  • 我们通过调用 loginBean.isValidUser() 来检查用户是否有效。处理错误信息的代码没有变化。

要测试页面,请执行以下步骤:

  1. 在项目资源管理器中右键点击 index.jsp

  2. 选择“运行”|“在服务器上运行”菜单选项。Eclipse 将提示您重启 Tomcat 服务器。

  3. 点击“确定”按钮以重启服务器。

页面将在内部 Eclipse 浏览器中显示。它应该与上一个示例中的行为相同。

尽管我们已经将用户验证移动到 LoginBean,但我们仍然在 Java 脚本中有很多代码。理想情况下,我们应该在 JSP 中尽可能少地使用 Java 脚本。我们仍然有用于检查条件和变量赋值的脚本。我们可以通过使用标签来编写相同的代码,这样它就与 JSP 中剩余的基于标签的代码保持一致,并且对网页设计师来说更容易工作。这可以通过使用 JSP 标准标签库JSTL)来实现。

使用 JSTL

JSTL 标签可以用来替换 JSP 中的大部分 Java 脚本。JSTL 标签分为五大类:

  • 核心:包括流程控制和变量支持等

  • XML:处理 XML 文档的标签

  • i18n:支持国际化的标签

  • SQL:访问数据库的标签

  • 函数:执行一些常见的字符串操作

有关 JSTL 的更多详细信息,请参阅 docs.oracle.com/javaee/5/tutorial/doc/bnake.html

我们将修改登录 JSP 以使用 JSTL,这样它就不会包含任何 Java 脚本。

  1. 下载 JSTL 库及其 API 实现。在撰写本文时,最新的 .jar 文件是 javax.servlet.jsp.jstl-api-1.2.1.jar (search.maven.org/remotecontent?filepath=javax/servlet/jsp/jstl/javax.servlet.jsp.jstl-api/1.2.1/javax.servlet.jsp.jstl-api-1.2.1.jar) 和 javax.servlet.jsp.jstl-1.2.1.jar (search.maven.org/remotecontent?filepath=org/glassfish/web/javax.servlet.jsp.jstl/1.2.1/javax.servlet.jsp.jstl-1.2.1.jar)。确保将这些文件复制到 WEB-INF/lib。此文件夹中的所有 .jar 文件都添加到 Web 应用的 classpath 中。

  2. 我们需要在我们的 JSP 中添加 JSTL 的声明。在第一个页面声明(<%@ page language="java" ...>)下方添加以下 taglib 声明:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 

taglib 声明包含 tag 库的 URL 和 prefix。在 JSP 中使用 prefix 访问 tag 库中的所有标签。

  1. <%String errMsg = null; %> 替换为 JSTL 的 set 标签:
<c:set var="errMsg" value="${null}"/> 
<c:set var="displayForm" value="${true}"/> 

我们将值放在 ${} 中。这被称为 表达式语言EL)。在 JSTL 中,您将 Java 表达式放在 ${} 中。

  1. 替换以下代码:
<%if ("POST".equalsIgnoreCase(request.getMethod()) && 
request.getParameter("submit") != null) {%> 

使用 JSTL 的 if 标签:

<c:if test="${"POST".equalsIgnoreCase(pageContext.request 
.method) && pageContext.request.getParameter("submit") != 
 null}">

在 JSTL 标签中,通过 pageContext 访问 request 对象。

  1. JavaBean 标签位于 if 标签内。这段代码没有变化:
<jsp:useBean id="loginBean" 
  class="packt.book.jee_eclipse.ch2.bean.LoginBean"> 
  <jsp:setProperty name="loginBean" property="*"/> 
</jsp:useBean> 
  1. 我们接着添加标签来调用 loginBean.isValidUser(),并根据其返回值设置消息。然而,我们在这里不能使用 JSTL 的 if 标签,因为我们还需要写 else 语句。JSTL 没有用于 else 的标签。相反,对于多个 if...else 语句,你需要使用 choose 语句,这与 switch 语句有些相似:
<c:choose> 
  <c:when test="${!loginBean.isValidUser()}"> 
    <c:set var="errMsg" value="Invalid user id or password. Please 
     try again"/> 
  </c:when> 
  <c:otherwise> 
    <h2><c:out value="Welcome admin !"/></h2> 
    <c:out value="You are successfully logged in"/> 
    <c:set var="displayForm" value="${false}"/> 
  </c:otherwise> 
</c:choose>

如果用户凭据无效,我们设置错误信息。或者(在 c:otherwise 标签中),我们打印欢迎信息并将 displayForm 标志设置为 false。如果用户成功登录,我们不希望显示登录表单。

  1. 我们现在将替换另一个 if 脚本代码为 <%if%> 标签。替换以下代码片段:
<%if (errMsg != null) { %> 
  <span style="color: red;"><%out.print(errMsg); %></span> 
<%} %> 

使用以下代码:

<c:if test="${errMsg != null}"> 
  <span style="color: red;"> 
    <c:out value="${errMsg}"></c:out> 
  </span> 
</c:if>

注意,我们使用了 out 标签来打印错误信息。

  1. 最后,我们将整个 <body> 内容包裹在另一个 JSTL if 标签中:
<c:if test="${displayForm}"> 
<body> 
   ... 
</body> 
</c:if> 

这里是完整的 JSP 源代码:

<%@ page language="java" contentType="text/html; charset=UTF-8" 
    pageEncoding="UTF-8"%> 
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
<html> 
<head> 
<meta http-equiv="Content-Type" content="text/html; charset=UTF- 
 8"> 
<title>Login</title> 
</head> 

<c:set var="errMsg" value="${null}"/> 
<c:set var="displayForm" value="${true}"/> 
<c:if test="${"POST".equalsIgnoreCase(pageContext.request.method) 
&& pageContext.request.getParameter("submit") != null}"> 
  <jsp:useBean id="loginBean" 
   class="packt.book.jee_eclipse.ch2.bean.LoginBean"> 
    <jsp:setProperty name="loginBean" property="*"/> 
  </jsp:useBean> 
  <c:choose> 
    <c:when test="${!loginBean.isValidUser()}"> 
      <c:set var="errMsg" value="Invalid user id or password. 
       Please try again"/> 
    </c:when> 
    <c:otherwise> 
    <h2><c:out value="Welcome admin !"/></h2> 
      <c:out value="You are successfully logged in"/> 
      <c:set var="displayForm" value="${false}"/> 
    </c:otherwise> 
  </c:choose> 
</c:if> 

<c:if test="${displayForm}"> 
<body> 
  <h2>Login:</h2> 
  <!-- Check error message. If it is set, then display it --> 
  <c:if test="${errMsg != null}"> 
    <span style="color: red;"> 
      <c:out value="${errMsg}"></c:out> 
    </span> 
  </c:if> 
  <form method="post"> 
    User Name: <input type="text" name="userName"><br> 
    Password: <input type="password" name="password"><br> 
    <button type="submit" name="submit">Submit</button> 
    <button type="reset">Reset</button> 
  </form> 
</body> 
</c:if> 
</html> 

如你所见,前面的代码中没有 Java 脚本。所有这些,从之前的代码,都被标签所替代。这使得网页设计师可以轻松编辑页面,而不用担心 Java 脚本。

在我们离开 JSP 主题之前,有一点需要注意。在实际应用中,用户成功登录后,你可能会将请求转发到另一个页面,而不是在同一个页面上只显示欢迎信息。你可以使用 <jsp:forward> 标签来实现这一点。

Java Servlet

我们现在将看到如何使用 Java Servlet 实现登录应用程序。在 Eclipse 中创建一个新的 动态 Web 应用程序,如前所述。我们将称这个为 LoginServletApp

  1. 在项目资源管理器中,在 Java Resources 下的 src 文件夹上右键单击项目。选择“新建 | Servlet”菜单选项。

  2. 在创建 Servlet 向导中,输入包名为 packt.book.jee_eclipse.book.servlet,类名为 LoginServlet。然后,点击完成。

图 2.22:创建 Servlet 向导

  1. Servlet 向导为您创建了类。注意类声明上方有 @WebServlet("/LoginServlet") 注解。在 JEE 5 之前,你必须在 WEB-INF 文件夹中的 web.xml 中声明 servlet。你仍然可以这样做,但如果你使用适当的注解,你可以跳过这个声明。使用 WebServlet,我们告诉 servlet 容器 LoginServlet 是一个 servlet,并且我们将其映射到 /LoginServlet URL 路径。因此,我们通过使用这个注解避免了 web.xml 中的以下两个条目:<servlet><servlet-mapping>

我们现在将映射从 /LoginServlet 更改为 /login。因此,我们将修改注解如下:

@WebServlet("/login") 
public class LoginServlet extends HttpServlet {...} 
  1. 工具程序还创建了doGetdoPost方法。这些方法是从以下基类中重写的:HttpServletdoGet方法被调用以创建对Get请求的响应,而doPost被调用以创建对Post请求的响应。

我们将在doGet方法中创建一个登录表单,并在doPost方法中处理表单数据(Post)。然而,因为doPost可能需要显示表单,以防用户凭证无效,我们将编写一个createForm方法,该方法可以从doGetdoPost中调用。

  1. 添加一个如下所示的createForm方法:
protected String createForm(String errMsg) { 
 StringBuilder sb = new StringBuilder("<h2>Login</h2>"); 
//check whether error message is to be displayed 
  if (errMsg != null) { 
    sb.append("<span style='color: red;'>") 
      .append(errMsg) 
      .append("</span>"); 
  } 
  //create form 
  sb.append("<form method='post'>n") 
    .append("User Name: <input type='text' 
     name='userName'><br>n")    .append("Password: <input type='password' 
     name='password'><br>n")    .append("<button type='submit' 
     name='submit'>Submit</button>n") 
    .append("<button type='reset'>Reset</button>n") 
    .append("</form>"); 

  return sb.toString(); 
} 
  1. 现在,我们将修改一个doGet方法以调用createForm方法并返回响应:
protected void doGet(HttpServletRequest request, 
 HttpServletResponse response) 
  throws ServletException, IOException { 
  response.getWriter().write(createForm(null)); 
} 

我们在response对象上调用getWrite方法,并通过调用createForm函数将表单内容写入其中。请注意,当我们显示表单时,最初没有错误信息,因此我们传递一个null参数给createForm

  1. 我们将修改doPost以处理用户通过点击提交按钮提交的表单内容:
protected void doPost(HttpServletRequest request, 
HttpServletResponse response) 
  throws ServletException, IOException { 
    String userName = request.getParameter("userName"); 
    String password = request.getParameter("password"); 

  //create StringBuilder to hold response string 
  StringBuilder responseStr = new StringBuilder(); 
  if ("admin".equals(userName) && "admin".equals(password)) { 
    responseStr.append("<h2>Welcome admin !</h2>") 
    .append("You are successfully logged in"); 
  } 
  else { 
    //invalid user credentials 
    responseStr.append(createForm("Invalid user id or password. 
     Please try again")); 
  } 

  response.getWriter().write(responseStr.toString()); 
} 

我们首先通过调用request.getParameter方法从request对象中获取用户名和密码。如果凭证有效,我们在response字符串中添加一条欢迎信息;否则,我们调用带有错误信息的createForm方法,并将表单的标记(表单的标记)添加到response字符串中。

最后,我们从response字符串中获取Writer对象,并写入响应。

  1. 在项目资源管理器中右键单击LoginServlet.java文件,然后选择“运行方式 | 在服务器上运行”选项。我们尚未将此项目添加到 Tomcat 服务器。因此,Eclipse 将询问您是否要使用配置的服务器运行此 servlet。点击向导的“完成”按钮。

  2. 由于服务器上部署了新的 Web 应用程序,Tomcat 需要重新启动。Eclipse 将提示您重新启动服务器。点击“确定”。

当在 Eclipse 的内部浏览器中运行 servlet 时,请注意 URL;它以/login结尾,这是我们已在 servlet 注解中指定的映射。然而,您将观察到,页面没有渲染 HTML 表单,而是显示了标记文本。这是因为我们在response对象上遗漏了一个重要的设置。我们没有告诉浏览器我们返回的内容类型,所以浏览器假设它是文本,并以纯文本的形式渲染它。我们需要告诉浏览器它是 HTML 内容。我们通过在doGetdoPost方法中调用response.setContentType("text/html")来实现这一点。以下是完整的源代码:

package packt.book.jee_eclipse.book.servlet; 

// skipping imports to save space

/** 
 * Servlet implementation class LoginServlet 
 */ 
@WebServlet("/login") 
public class LoginServlet extends HttpServlet { 
  private static final long serialVersionUID = 1L; 

  public LoginServlet() { 
    super(); 
  } 
  //Handles HTTP Get requests 
  protected void doGet(HttpServletRequest request, HttpServletResponse response)  
    throws ServletException, IOException { 
  response.setContentType("text/html"); 
  response.getWriter().write(createForm(null)); 
  } 
  //Handles HTTP POST requests 
  protected void doPost(HttpServletRequest request, 
   HttpServletResponse response) 
      throws ServletException, IOException { 
    String userName = request.getParameter("userName"); 
    String password = request.getParameter("password"); 

    //create StringBuilder to hold response string 
    StringBuilder responseStr = new StringBuilder(); 
    if ("admin".equals(userName) && "admin".equals(password)) { 
      responseStr.append("<h2>Welcome admin !</h2>") 
        .append("You're are successfully logged in"); 
    } else { 
      //invalid user credentials 
      responseStr.append(createForm("Invalid user id or password. 
       Please try again")); 
    } 
    response.setContentType("text/html"); 
    response.getWriter().write(responseStr.toString()); 
  }

  //Creates HTML Login form 
  protected String createForm(String errMsg) { 
    StringBuilder sb = new StringBuilder("<h2>Login</h2>"); 
    //check if error message to be displayed 
    if (errMsg != null) { 
      sb.append("<span style='color: red;'>") 
        .append(errMsg) 
        .append("</span>"); 
    } 
    //create form 
    sb.append("<form method='post'>n") 
      .append("User Name: <input type='text' 
       name='userName'><br>n")      .append("Password: <input type='password' 
       name='password'><br>n")      .append("<button type='submit' 
       name='submit'>Submit</button>n") 
      .append("<button type='reset'>Reset</button>n") 
      .append("</form>"); 
    return sb.toString(); 
  } 
} 

如您所见,在 servlet 中编写 HTML 标记并不太方便。因此,如果您正在创建一个包含大量 HTML 标记的页面,那么使用 JSP 或纯 HTML 会更好。Servlets 适用于处理不需要生成太多标记的请求,例如,在模型-视图-控制器MVC)框架中的控制器,处理生成非文本响应的请求,或创建 Web 服务或 WebSocket 端点。

创建 WAR

到目前为止,我们一直在 Eclipse 中运行我们的 Web 应用程序,它负责将应用程序部署到 Tomcat 服务器。这在开发过程中工作得很好,但当你想要将其部署到测试或生产服务器时,你需要创建一个Web 应用程序存档WAR)。我们将看到如何从 Eclipse 创建 WAR。然而,首先我们将从 Tomcat 中取消部署现有应用程序。

  1. 前往“服务器视图”,选择应用程序,然后右键单击并选择“移除”选项:

图片

图 2.23 从服务器取消部署 Web 应用程序

  1. 然后,在项目资源管理器中右键单击项目,选择导出 | WAR 文件。选择 WAR 文件的目标位置:

图片

图 2.24 导出 WAR

要将 WAR 文件部署到 Tomcat,将其复制到<tomcat_home>/webapps文件夹。然后如果服务器尚未运行,请启动服务器。如果 Tomcat 已经运行,则不需要重新启动它。

Tomcat 监视webapps文件夹,并将任何复制到其中的 WAR 文件自动部署。您可以通过在浏览器中打开应用程序的 URL 来验证此操作,例如,http://localhost:8080/LoginServletApp/login

JavaServer Faces

在使用 JSP 时,我们看到了将脚本片段与 HTML 标记混合不是一个好主意。我们通过使用 JavaBean 解决了这个问题。JavaServer Faces 将这种设计进一步发展。除了支持 JavaBeans 之外,JSF 还提供了用于 HTML 用户控制的内置标签,这些标签是上下文感知的,可以执行验证,并且可以在请求之间保留状态。我们现在将使用 JSF 创建登录应用程序:

  1. 在 Eclipse 中创建一个动态 Web 应用程序;让我们称它为LoginJSFApp。在向导的最后一步,确保勾选“生成 web.xml 部署描述符”复选框。

  2. maven.java.net/content/repositories/releases/org/glassfish/javax.faces/2.2.9/javax.faces-2.2.9.jar下载 JSF 库,并将它们复制到项目中的WEB-INF/lib文件夹。

  3. JSF 遵循 MVC 模式。在 MVC 模式中,生成用户界面(视图)的代码与数据容器(模型)是分开的。控制器作为视图和模型之间的接口。它根据配置选择模型来处理请求,一旦模型处理了请求,它根据模型处理的结果选择要生成的视图并返回给客户端。MVC 的优点是 UI 和业务逻辑(需要不同的专业知识)有明确的分离,因此它们可以独立开发,在很大程度上。在 JSP 中,MVC 的实现是可选的,但 JSF 强制执行 MVC 设计。

视图是作为xhtml文件创建的 JSF。控制器是 JSF 库中的 servlet,模型是管理 Bean(JavaBean)。

</web-app>:
<servlet> 
  <servlet-name>JSFServlet</servlet-name> 
  <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> 
  <load-on-startup>1</load-on-startup> 
</servlet> 

<servlet-mapping> 
  <servlet-name>JSFServlet</servlet-name> 
  <url-pattern>*.xhtml</url-pattern> 
</servlet-mapping> 

注意,在创建前面的元素时,按Ctrl/Cmd + C可以获取代码辅助。

您可以为servlet-name指定任何名称;只需确保在servlet-mapping中使用相同的名称。servlet 的类为javax.faces.webapp.FacesServlet,它位于我们下载作为 JSF 库并复制到WEB-INF/lib的 JAR 文件中。此外,我们将所有以.xhtml结尾的请求映射到这个 servlet 上。

接下来,我们将为我们的登录页面创建一个管理 Bean。这与我们之前创建的 JavaBean 相同,但增加了 JSF 特定的注解:

  1. 在项目资源管理器中,在Java Resources下的src文件夹上右键单击。

  2. 选择“新建 | 类”菜单选项。

  3. 按照本章中“在 JSP 中使用 JavaBeans”部分所述,创建 JavaBean LoginBean

  4. userNamepassword创建两个成员。

  5. 为它们创建 getter 和 setter。然后,添加以下两个注解:

package packt.book.jee_eclipse.bean; 
import javax.faces.bean.ManagedBean; 
import javax.faces.bean.RequestScoped; 

@ManagedBean(name="loginBean") 
@RequestScoped 
public class LoginBean { 
  private String userName; 
  private String password; 
  public String getUserName() { 
    return userName; 
  } 
  public void setUserName(String userName) { 
    this.userName = userName; 
  } 
  public String getPassword() { 
    return password; 
  } 
  public void setPassword(String password) { 
    this.password = password; 
  } 
}

(您也可以在创建前面的元素时通过按Ctrl/Cmd + C来获取代码辅助。代码辅助也适用于注解的key-value属性对,例如,对于ManagedBean注解的name属性)。

  1. 通过选择“文件 | 新建 | 文件”菜单选项,在项目的WebContent文件夹内创建一个名为index.xhtml的新文件。当使用 JSF 时,您需要在文件的顶部添加一些命名空间声明:
<html  

  > 

在这里,我们正在声明 JSF 内置tag库的命名空间。我们将使用前缀f访问核心 JSF tag库中的标签,并使用前缀h访问 HTML 标签。

  1. 添加标题并开始body标签:
<head> 
<title>Login</title> 
</head> 
<body> 
  <h2>Login</h2> 

对于headbody,有相应的 JSF 标签,但我们没有使用任何特定的 JSF 属性;因此,我们使用了简单的 HTML 标签。

  1. 然后,我们添加代码来显示错误消息,如果它不为空:
<h:outputText value="#{loginBean.errorMsg}" 
              rendered="#{loginBean.errorMsg != null}" 
              style="color:red;"/> 

在这里,我们使用 JSF 和表达式语言特定的标签来显示错误消息的值。OutputText标签类似于我们在 JSTL 中看到的c:out标签。我们还添加了一个条件,仅在托管 Bean 中的错误消息不是null时渲染它。此外,我们还设置了此输出文本的颜色。

  1. 我们还没有将errorMsg成员添加到托管 Bean 中。因此,让我们添加声明、获取器和设置器。打开LoginBean类并添加以下代码:
private String errorMsg; 
public String getErrorMsg() { 
  return errorMsg; 
} 
public void setErrorMsg(String errorMsg) { 
  this.errorMsg = errorMsg; 
} 

注意,我们通过使用ManagedBean注解的name属性的值来访问 JSF 中的托管 Bean。此外,与 JSP 中的 JavaBean 不同,我们不是通过<jsp:useBean>标签来创建它的。如果它不在所需的范围内,JSF 运行时会创建 Bean,在这个例子中,是Request范围。

  1. 让我们回到编辑index.xhtml。我们现在将添加以下表单:
<h:form> 
  User Name: <h:inputText id="userName" 
                value="#{loginBean.userName}"/><br/> 
  Password: <h:inputSecret id="password" 
                value="#{loginBean.password}"/><br/> 
  <h:commandButton value="Submit" 
   action="#{loginBean.validate}"/> 
</h:form> 

这里发生了很多事情。首先,我们使用了 JSF 的inputText标签来创建用户名和密码的文本框。我们使用loginBean的相应成员设置它们的值。我们使用了 JSF 的commandButton标签来创建一个提交按钮。当用户点击提交按钮时,我们将其设置为调用loginBean.validate方法(使用action属性)。

  1. 我们还没有在loginBean中定义一个validate方法,所以让我们添加它。打开LoginBean类并添加以下代码:
public String validate() 
{ 
  if ("admin".equals(userName) && "admin".equals(password)) { 
    errorMsg = null; 
    return "welcome"; 
  } else { 
    errorMsg = "Invalid user id or password. Please try 
     again"; 
    return null; 
  } 
}

注意,validate方法返回一个字符串。返回值是如何使用的?它用于 JSF 中的导航目的。JSF 运行时会查找与在commandButtonaction属性中评估的表达式返回的字符串值相同的 JSF 文件。在validate方法中,如果用户凭据有效,我们返回welcome。在这种情况下,我们告诉 JSF 运行时导航到welcome.xhtml。如果凭据无效,我们设置错误消息并返回null,在这种情况下,JSF 运行时会显示同一页面。

  1. 我们现在将添加welcome.xhml页面。它只包含欢迎消息:
<html  

      > 
  <body> 
    <h2>Welcome admin !</h2> 
      You are successfully logged in 
  </body> 
</html> 

这里是index.html的完整源代码:

<html  

  > 

  <head> 
    <title>Login</title> 
  </head> 
  <body> 
  <h2>Login</h2> 
  <h:outputText value="#{loginBean.errorMsg}" 
  rendered="#{loginBean.errorMsg != null}" 
  style="color:red;"/> 
  <h:form> 
    User Name: <h:inputText id="userName" 
     value="#{loginBean.userName}"/><br/>    Password: <h:inputSecret id="password" 
     value="#{loginBean.password}"/><br/> 
  <h:commandButton value="Submit" action="#{loginBean.validate}"/> 
  </h:form> 
</body> 
</html>

这里是LoginBean类的源代码:

package packt.book.jee_eclipse.bean; 
import javax.faces.bean.ManagedBean; 
import javax.faces.bean.RequestScoped; 

@ManagedBean(name="loginBean") 
@RequestScoped 
public class LoginBean { 
  private String userName; 
  private String password; 
  private String errorMsg; 
  public String getUserName() { 
    return userName; 
  } 
  public void setUserName(String userName) { 
    this.userName = userName; 
  } 
  public String getPassword() { 
    return password; 
  } 
  public void setPassword(String password) { 
    this.password = password; 
  } 
  public String getErrorMsg() { 
    return errorMsg; 
  } 
  public void setErrorMsg(String errorMsg) { 
    this.errorMsg = errorMsg; 
  } 
  public String validate() 
  { 
    if ("admin".equals(userName) && "admin".equals(password)) { 
      errorMsg = null; 
      return "welcome"; 
    } 
    else { 
      errorMsg = "Invalid user id or password. Please try again"; 
      return null; 
    } 
  } 
} 

要运行应用程序,在项目资源管理器中右键单击index.xhtml,然后选择“运行”|“在服务器上运行”选项。

JSF 可以做的不仅仅是我们在这个小示例中看到的内容——它支持验证输入和创建页面模板。然而,这些主题超出了本书的范围。

访问docs.oracle.com/cd/E11035_01/workshop102/webapplications/jsf/jsf-app-tutorial/Introduction.html以获取 JSF 教程。

使用 Maven 进行项目管理

在本章中我们创建的项目中,我们已经管理了许多项目管理任务,例如下载项目依赖的库,将它们添加到适当的文件夹中以便 Web 应用程序可以找到它,以及导出项目以创建部署的 WAR 文件。这些只是我们迄今为止执行的一些项目管理任务,但还有很多更多,我们将在后续章节中看到。有一个工具为我们执行许多项目管理任务是有帮助的,这样我们就可以专注于应用程序开发。Java 有一些知名的构建管理工具可用,例如 Apache Ant (ant.apache.org/) 和 Maven (maven.apache.org/)).

在本节中,我们将了解如何将 Maven 用作项目管理工具。通过遵循创建项目结构的惯例并允许项目定义层次结构,Maven 使得项目管理比 Ant 更简单。Ant 主要是一个构建工具,而 Maven 是一个项目管理工具,它也进行构建管理。请参阅maven.apache.org/what-is-maven.html了解 Maven 能做什么。

尤其是 Maven 简化了依赖管理。在本章前面的 JSF 项目中,我们首先下载了 JSF 所需的相应.jar文件,并将它们复制到lib文件夹中。Maven 可以自动化这个过程。您可以在pom.xml中配置 Maven 设置。POM代表项目对象模型

在我们使用 Maven 之前,了解它是如何工作的是非常重要的。Maven 使用仓库。仓库包含许多知名库/项目的插件。一个插件包括项目配置信息、在您的项目中使用此项目所需的.jar文件以及任何其他支持性工件。默认的 Maven 仓库是一个插件集合。您可以在默认的 Maven 仓库中找到插件列表,网址为maven.apache.org/plugins/index.html。您还可以浏览 Maven 仓库的内容,网址为search.maven.org/#browse。Maven 还在您的机器上维护一个本地仓库。这个本地仓库只包含您的项目指定的依赖项的插件。在 Windows 上,您可以在C:/Users/<username>.m2找到本地仓库,而在 macOS X 上,它位于~/.m2

您在 pom.xmldependencies 部分定义了项目所依赖的插件(我们将在创建 Maven 项目时稍后看到 pom.xml 的结构)。例如,我们可以指定对 JSF 的依赖。当您运行 Maven 工具时,它首先检查 pom.xml 中的所有依赖项。然后,它会检查所需版本的依赖插件是否已经下载到本地仓库中。如果没有,它会从中央(远程)仓库下载它们。您也可以指定要查找的仓库。如果您没有指定任何仓库,则依赖项将在中央 Maven 仓库中搜索。

我们将创建一个 Maven 项目并更详细地探索 pom.xml。然而,如果您想了解 pom.xml 是什么,请访问 maven.apache.org/pom.html#What_is_the_POM

Eclipse JEE 版本已内置 Maven,因此您不需要下载它。但是,如果您计划在 Eclipse 外部使用 Maven,则可以从 maven.apache.org/download.cgi 下载它。

Eclipse JEE 中的 Maven 视图和首选项

在我们创建 Maven 项目之前,让我们探索 Eclipse 中特定的 Maven 视图和首选项:

  1. 选择 Window | Show View | Other... 菜单。

  2. 在过滤器框中输入 Maven。您将看到两个 Maven 视图:

图 2.25:Maven 视图

  1. 选择 Maven Repositories 视图并点击 OK。此视图将在 Eclipse 底部选项卡窗口中打开。您可以看到本地和远程仓库的位置。

  2. 右键单击全局仓库以查看索引仓库的选项:

图 2.26:Maven 仓库视图

  1. 打开 Eclipse 首选项,并在过滤器框中输入 Maven 以查看所有 Maven 首选项:

图 2.27:Maven 首选项

您应将 Maven 首选项设置为在启动时刷新仓库索引,以便在您向项目添加依赖项时可以使用最新的库(我们将在稍后学习如何添加依赖项)。

  1. 在首选项中点击 Maven 节点并设置以下选项:

图 2.28:启动时更新索引的 Maven 首选项

创建 Maven 项目

在以下步骤中,我们将看到如何在 Eclipse 中创建 Maven 项目:

  1. 选择 New | Maven Project 菜单:

图 2.29:Maven 新建项目向导

  1. 接受所有默认选项并点击 Next。在过滤器框中输入 webapp 并选择 maven-archetype-webapp:

图 2.30:新建 Maven 项目 - 选择原型

Maven 原型

在前面的向导中,我们选择了 maven-archetype-webapp。原型是一个项目模板。当您使用原型创建项目时,模板(原型)中定义的所有依赖项和其他 Maven 项目配置都将导入到您的项目中。

更多关于 Maven 原型的信息请参阅maven.apache.org/guides/introduction/introduction-to-archetypes.html

  1. 继续使用 New Maven Project 向导,点击 Next。在 Group Id 字段中输入packt.book.jee_eclipse。在 Artifact Id 字段中输入maven_jsf_web_app

图片

图 2.31:新建 Maven 项目 - 原型参数

  1. 点击 Finish。在项目资源管理器中添加了一个maven_jsf_web_app项目。

探索 POM

在编辑器中打开pom.xml文件并切换到 pom.xml 标签页。文件应包含以下内容:

<project   
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
   http://maven.apache.org/maven-v4_0_0.xsd"> 
  <modelVersion>4.0.0</modelVersion> 
  <groupId>packt.book.jee_eclipse</groupId> 
  <artifactId>maven_jsf_web_app</artifactId> 
  <packaging>war</packaging> 
  <version>0.0.1-SNAPSHOT</version> 
  <name>maven_jsf_web_app Maven Webapp</name> 
  <url>http://maven.apache.org</url> 
  <dependencies> 
    <dependency> 
      <groupId>junit</groupId> 
      <artifactId>junit</artifactId> 
      <version>3.8.1</version> 
      <scope>test</scope> 
    </dependency> 
  </dependencies> 
  <build> 
    <finalName>maven_jsf_web_app</finalName> 
  </build> 
</project> 

让我们详细看看前面代码片段中使用的不同标签:

  • modelVersion: 在pom.xml文件中,这代表 Maven 的版本。

  • groupId: 这是用于将项目分组在一起的业务单元或组织中的通用 ID。尽管使用包结构格式对于组 ID 不是必需的,但它通常被使用。

  • artifactId: 这是项目名称。

  • version: 这是项目的版本号。在指定依赖项时,版本号很重要。你可以有一个项目的多个版本,并且可以在不同的项目中指定不同的版本依赖项。Maven 还会将其创建的项目 JAR、WAR 或 EAR 文件的版本号附加到其中。

  • packaging: 这告诉 Maven 在项目构建时我们想要什么样的最终输出。在这本书中,我们将使用 JAR、WAR 和 EAR 打包类型,尽管存在更多类型。

  • name: 这实际上是项目的名称,但在 Eclipse 的项目资源管理器中显示为artifactid

  • url: 如果你在网上托管项目信息,这是你项目的 URL。默认情况下是 Maven 的 URL。

  • dependencies: 这个部分是我们指定项目所依赖的库(或其他 Maven 工件)。我们为这个项目选择的原型已经将 JUnit 的默认依赖项添加到我们的项目中。我们将在第五章,单元测试中了解更多关于 JUnit 的内容。

  • finalName: 在build标签中的这个标签表示 Maven 为你的项目生成的输出文件(JAR、WAR 或 EAR)的名称。

添加 Maven 依赖项

我们为项目选择的原型不包括 JEE Web 项目所需的某些依赖项。因此,你可能会在index.jsp中看到错误标记。我们将通过添加 JEE 库的依赖项来修复这个问题:

  1. 在编辑器中打开pom.xml文件后,点击 Dependencies 标签页。

  2. 点击 Add 按钮。这会打开 Select Dependency 对话框。

  3. 在过滤器框中,输入javax.servlet(我们想在项目中使用 servlet API)。

  4. 选择 API 的最新版本并点击 OK 按钮。

图片

图 2.32:添加 servlet API 依赖项

然而,我们只需要在编译时使用 servlet API 的 JAR 文件;在运行时,这些 API 由 Tomcat 提供。我们可以通过指定依赖的范围来表示这一点;在这种情况下,将其设置为 provided,这告诉 Maven 仅为此依赖项进行评估,并将其打包到 WAR 文件中。有关依赖范围的更多信息,请参阅 maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html

  1. 要设置依赖的范围,从 POM 编辑器的“依赖”选项卡中选择依赖项。

  2. 点击“属性”按钮。然后,从下拉列表中选择提供的范围:

图片

图 2.33:设置 Maven 依赖范围

  1. 现在我们需要为 JSF API 及其实现添加依赖。再次点击“添加”按钮,并在搜索框中输入 jsf

  2. 从列表中选择 Group Id 为 com.sun.facesjsf-api,然后点击“确定”按钮:

图片

图 2.34:为 JSF 添加 Maven 依赖

  1. 类似地,添加一个 jsf-impl 依赖,Group Id 为 com.sun.faces。你的 pom.xml 文件中的依赖部分应如下所示:
<dependencies> 
    <dependency> 
      <groupId>junit</groupId> 
      <artifactId>junit</artifactId> 
      <version>3.8.1</version> 
      <scope>test</scope> 
    </dependency> 
    <dependency> 
      <groupId>javax.servlet</groupId> 
      <artifactId>javax.servlet-api</artifactId> 
      <version>3.1.0</version> 
      <scope>provided</scope> 
    </dependency> 
    <dependency> 
      <groupId>com.sun.faces</groupId> 
      <artifactId>jsf-api</artifactId> 
      <version>2.2.16</version> 
      </dependency> 
    <dependency> 
      <groupId>com.sun.faces</groupId> 
      <artifactId>jsf-impl</artifactId> 
      <version>2.2.16</version> 
    </dependency> 
  </dependencies> 

如果 Tomcat 抛出找不到 javax.faces.webapp.FacesServlet 的异常,那么你可能需要下载 jsf-api-2.2.16.jar (central.maven.org/maven2/com/sun/faces/jsf-impl/2.2.16/jsf-impl-2.2.16.jar) 和 jsf-impl-2.2.16.jar (central.maven.org/maven2/com/sun/faces/jsf-impl/2.2.16/jsf-impl-2.2.16.jar),并将它们复制到 <tomcat-install-folder>/lib 文件夹中。

Maven 项目结构

Maven 项目向导在主项目文件夹下创建 srctarget 文件夹。正如其名所示,所有源文件都放在 src 下。然而,Java 包结构从 main 文件夹开始。按照惯例,Maven 预期 Java 源文件在 java 文件夹下。因此,在 src/main 下创建一个 java 文件夹。Java 包结构从 java 文件夹开始,即 src/main/java/<java-packages>。Web 内容,如 HTML、JS、CSS 和 JSP,放在 src/main/webapp 下的 webapp 文件夹中。由 Maven 构建过程生成的编译类和其他输出文件存储在 target 文件夹中:

图片

图 2.35:Maven 网络应用程序项目结构

我们登录 JSF 页面的源代码与之前 LoginJSFApp 示例中的相同。因此,将那个项目的 src 文件夹中的 packt 文件夹复制到这个 Maven 项目的 src/main/java 文件夹中。这会将 LoginBean.java 添加到项目中。然后,将 web.xmlWEB-INF 文件夹复制到这个项目的 src/main/webapp/WEB-INF 文件夹中。将 index.xhtmlwelcome.xhtml 复制到 src/main/webapp 文件夹:

图片

图 2.36:添加源文件后的项目结构

在源代码中不需要进行任何更改。要运行应用程序,请右键单击index.xhtml并选择运行方式 | 在服务器上运行。

在本书的其余部分,我们将使用 Maven 进行项目管理。

使用 Maven 创建 WAR 文件

在前面的示例中,我们使用 Eclipse 的导出选项创建了 WAR 文件。在 Maven 项目中,你可以通过调用 Maven Install 插件来创建 WAR 文件。右键单击项目并选择运行方式 | Maven 安装选项。WAR 文件将在target文件夹中创建。然后你可以通过将其复制到 Tomcat 的webapps文件夹中来部署 WAR 文件。

摘要

在本章中,我们学习了如何在 Eclipse 中配置 Tomcat。我们学习了同一个网页可以使用三种不同的技术实现,即 JSP、Servlet 和 JSF。所有这些都可以用于开发任何动态 Web 应用程序。然而,JSP 和 JSF 更适合于更注重 UI 的页面,而 servlets 更适合于控制器,以及作为 Web 服务和 WebSocket 的端点。JSF 强制执行 MVC 设计模式,并且与 JSP 相比提供了许多额外的服务。

我们还学习了如何使用 Maven 进行许多项目管理任务。

在下一章中,我们将学习如何配置和使用源代码管理系统,特别是 SVN 和 Git。

第三章:Eclipse 中的源代码管理

在上一章中,我们学习了如何使用 JSP、JSF 和 servlets 创建简单的 Web 应用程序。我们还学习了如何使用 Maven 进行构建和项目管理。

在本章中,我们将学习如何将 Eclipse 与 SVN 和 Git 集成。本章涵盖了以下主题:

  • 安装 Eclipse 的 SVN 和 Git 插件

  • 在 Eclipse 中执行源控制任务,例如检出文件、提交更改等

  • 与远程仓库同步项目

源代码管理SCM)是软件开发的一个基本组成部分。通过使用 SCM 工具,您可以确保在重要的里程碑处能够访问到代码的版本。SCM 还有助于在团队工作中管理源代码,通过提供工具确保您不会覆盖他人的工作。无论您的项目大小如何,无论您是单独工作还是在一个大型团队中,使用 SCM 都会为您带来好处。

Eclipse 很早就支持集成各种 SCM 工具——这包括对 CVS、Microsoft SourceSafe、Perforce 和 SubversionSVN)的支持。Eclipse 的最新版本也内置了对 Git 的支持。

我们将首先学习如何在 Eclipse 中使用 SVN。

Eclipse 子版本控制插件

在本节中,我们将学习如何安装和使用 SVN Eclipse 插件。我们将创建一个小型项目,并查看如何在 Eclipse 内部将项目检入 SVN。我们还将了解如何与现有的 SVN 仓库同步。

您需要访问一个 SVN 仓库才能遵循本章中的步骤。如果您没有访问 SVN 仓库的权限,您可以从网上的一些免费 SVN 提供中选择。本书不推荐或建议使用任何特定的在线 SVN 托管,但为了解释 SVN Eclipse 插件的功能,作者使用了 riouxsvn.com。然而,该插件与任何 SVN 服务器的工作方式相同。

安装 Eclipse Subversion 插件

  1. 通过选择 Help | Eclipse Marketplace 菜单打开 Eclipse 市场 place。搜索 subversion

图 3.1:安装 Subversion 插件

  1. 安装插件。在我们配置 Eclipse 中的 SVN 仓库之前,我们需要选择/安装一个 SVN 连接器。转到 Eclipse 首选项,并在过滤器框中输入 svn。然后,转到 SVN 连接器选项卡:

图 3.2:SVN 连接器首选项

如果没有安装连接器,您将看到一个 Get Connectors... 按钮。点击该按钮。

  1. Eclipse 显示了多个可用的连接器。我们将选择 SVN Kit 连接器并安装它(点击 Finish 按钮):

图 3.3:SVN 连接器发现向导

  1. 我们现在将在 Eclipse 中配置现有的 SVN 仓库。选择 Window | Open Perspective | Other 菜单,然后选择 SVN Repository Exploring 视图:

图 3.4:打开 SVN 视图

将项目添加到 SVN 仓库

执行以下步骤将项目添加到 SVN 仓库:

  1. 在 SVN 仓库视图中右键单击,然后选择新建 | 仓库位置。

  2. 输入您的 SVN 仓库的 URL、用户名和密码。如果您需要设置 SSH 或 SSL 信息以连接到您的 SVN 仓库,请单击相应的选项卡并输入信息。单击“完成”将仓库添加到 Eclipse:

图片

图 3.5:配置 SVN 仓库

现在我们将创建一个简单的 Java 项目,我们将将其检查入 SVN 仓库。在本章中,您在项目中编写的代码并不重要;我们只是使用项目来了解如何将项目文件检查入 SVN,然后查看如何同步项目。

  1. 创建一个如图所示的简单 Java 项目:

图片

图 3.6:SVN 测试的示例项目

  1. 该项目有一个源文件。我们现在将此项目检查入 SVN。在项目上右键单击,然后选择团队 | 共享项目...。

  2. 选择 SVN 并单击“下一步”按钮。向导为您提供选项,要么创建一个新的 SVN 仓库,要么选择一个已经配置好的 SVN 仓库:

图片

图 3.7:与 SVN 仓库共享项目

  1. 我们将使用已经配置好的仓库。因此,选择仓库:

图片

图 3.8:选择 SVN 仓库或创建一个新的

  1. 我们可以单击“下一步”并配置高级选项,但我们将保持配置简单并单击“完成”。您将被提示检查项目中的现有文件:

图片

图 3.9:与 SVN 仓库共享项目

  1. 选择要检查入的文件并输入检查入注释。然后单击“确定”。要查看 SVN 仓库中的已检查入文件,切换到 SVN 仓库浏览视角,然后切换到 SVN 仓库视图:

图片

图 3.10:SVN 仓库视图中的已检查入文件

将更改提交到 SVN 仓库

现在让我们修改一个文件并检查更改。切换回 Java 视角,从包资源管理器或导航器打开SVNTestApplication.java。修改文件并保存更改。要比较工作目录中的文件或文件夹与仓库中的文件或文件夹,在导航器中右键单击文件/文件夹/项目,然后选择与 | 仓库最新版本比较。

现在我们已经修改了SVNTestApplication.java,让我们看看它与仓库中的版本有何不同:

图片

图 3.11:比较 SVN 文件

现在我们将在项目的根目录下添加一个新文件,比如readme.txt。要将文件添加到仓库,右键单击文件并选择团队 | 添加到版本控制...:

图片

图 3.12:将文件添加到 SVN 仓库

与 SVN 仓库同步

要将本地项目与远程仓库同步,右键单击项目并选择 Team | Synchronize with Repository。这将使用远程仓库中的文件更新项目,显示本地文件夹中新的文件,并显示已更改的文件:

图片

图 3.13:同步视图

你可以按传入模式(来自远程仓库的更改)、传出模式(工作目录中的更改)或两者都进行过滤。如图 3.13 所示,我们在工作目录中有两个已更改的文件;一个已修改,一个新创建。要提交更改,右键单击项目并选择 Commit.... 如果你想从导航器或包资源管理器提交,则右键单击项目并选择 Team | Commit.... 输入提交注释并点击 OK。要更新项目(接收来自远程仓库的所有更改),右键单击项目并选择 Team | Update。

要查看文件或文件夹的修订历史,右键单击导航器或包资源管理器并选择 Team | Show History:

图片

图 3.14:SVN 文件修订历史

从 SVN 检出项目

从 SVN 仓库检出项目到新工作空间很容易。在 SVN Repositories 视图中,点击你想要检出的项目,然后选择 Check Out 选项:

图片

图 3.15:SVN 文件修订历史

此选项将在当前工作空间中检出项目。你还可以使用导入项目选项从 SVN 检出项目。选择 File | Import 菜单选项,然后选择 SVN | Project from SVN 选项。

你可以从 Eclipse 使用许多其他 SVN 功能。请参阅 www.eclipse.org/subversive/documentation.php

Eclipse Git 插件

近期版本的 Eclipse 预装了 Eclipse Git 插件EGit)。如果没有,你可以从 Eclipse 市场安装插件。选择 Help | Eclipse Marketplace... 选项,并在 Find 文本框中输入 egit

图片

图 3.16:在 Eclipse 市场中搜索 EGit 插件

如果插件已经安装,它将被标记为已安装。

将项目添加到 Git

Git 是一个分布式仓库。与一些其他源代码管理系统不同,Git 还维护了完整的本地仓库。因此,你可以在不连接任何远程仓库的情况下,在本地仓库中执行诸如检出和提交等活动。当你准备好将代码移动到远程仓库时,你可以连接到它并将文件推送到远程仓库。

如果你刚开始使用 Git,请查看以下文档和教程:

git-scm.com/docwww.atlassian.com/git/tutorials/

要学习如何将项目添加到 Git,让我们在工作区中创建一个简单的 Java 项目。同样,在上一节中,你在这个项目中编写的代码现在并不重要:

  1. 在项目中创建一个 Java 类。

  2. 要将此项目添加到 Git,请在包资源管理器或导航器中右键单击项目,并选择团队 | 共享项目...:

图片

图 3.17:使用 Git 共享 Eclipse 项目

  1. 选择 Git 并点击下一步。勾选使用或创建项目父文件夹中的仓库。

  2. 选择项目(勾选项目复选框)并点击创建仓库按钮。然后点击完成:

图片

图 3.18:为项目创建 Git 仓库

  1. 这将在项目文件夹中创建一个新的 Git 仓库。切换到 Git 视图(或从窗口 | 显示视图 | 其他选项打开 Git 仓库视图),你应该在 Git 仓库视图中看到项目列表(见以下截图):

图片

图 3.19:Git 仓库视图

在 Git 仓库中提交文件

在 Git 中,新文件或修改后的文件已暂存以供提交。要查看暂存文件,请单击 Git 视图中的 Git 预览选项卡:

图片

图 3.20:Git 预览视图

如果你不想将文件添加到 Git 仓库中,那么在该文件(或多个文件选择)上右键单击并选择忽略选项。在将文件提交到 Git 之前,你需要将未暂存更改移动到暂存更改。我们将把所有文件添加到 Git 中。因此,在未暂存更改视图中选择所有文件,并将它们拖放到暂存更改视图中。还建议设置作者名称和提交者。通常以 Name <email> 格式。要在 Eclipse 中设置全局选项(这样你就不必在每次提交时设置这些字段),请转到 Eclipse 首选项并搜索 Git。然后转到团队 | Git | 配置页面并点击添加条目...按钮:

图片

图 3.21:添加 Git 配置条目

类似地,添加 user.email 条目:

图片

图 3.22:首选项中的 Git 配置

返回到 Git 预览视图,输入作者、提交者和提交信息。然后点击提交按钮。

修改后查看文件差异

让我们修改之前项目中创建的单个 Java 类。如果你在更改文件后转到 Git 预览视图,你会看到该文件出现在未暂存更改列表中。要查看自上次提交以来对文件所做的更改,请双击 Git 预览视图中的文件。

要提交这些更改,将其移动到暂存更改视图,输入提交信息,并点击提交按钮。你还可以通过在包资源管理器中单击文件并选择与 | 头版本比较来查看文件差异:

图片

图 3.23:查看文件差异

要查看项目或文件(夹)的更改历史,右键单击并选择“团队 | 显示在历史中”:

图片

图 3.24:Git 历史视图

创建新分支

当你使用源代码管理创建单独的分支以用于功能或甚至用于错误修复时,这是典型的。想法是主分支或主分支应始终包含工作代码,你可以在可能不稳定的分支上进行开发。当你完成一个功能或修复一个错误,并且知道该分支是稳定的,然后你将那个分支的代码合并到主分支。

要创建一个新分支,转到 Git 仓库视图,然后右键单击要分支的仓库。然后选择“切换到 | 新分支...”选项:

图片

图 3.25:创建新分支

注意“检出新分支”框应该被勾选。因为这个选项,新分支一旦创建就变为活动分支。你提交的任何更改都将在这个分支和主分支中进行,而主分支保持不变。点击完成以创建分支。

让我们对代码进行一些更改,比如在GitTestApp类的main方法中:

public class GitTestApp { 

  public static void main(String[] args) { 
    System.out.println("Hello Git, from branch bug#1234 !!"); 
  } 
} 

将前面的更改提交到新分支。

现在让我们检出主分支。在 Git 仓库视图中右键单击仓库,并选择“切换到 | master”。打开新分支中修改的文件。你会观察到你对文件所做的更改不存在。如前所述,你对分支所做的任何更改都不会提交到主分支。你必须显式合并更改。

要将 bug#1234 分支的更改合并到主分支,在 Git 仓库视图中右键单击仓库,并选择“合并...”:

图片

图 3.26:合并 Git 分支

选择分支 bug#1234。此分支将被合并到主分支中。点击合并。Git 将显示合并摘要。点击确定以完成合并操作。现在主分支中的文件将包含来自 bug#1234 分支所做的更改。

我们已经将 bug#1234 分支的所有更改合并到主分支,不再需要它。所以,让我们删除 bug#1234 分支。在 Git 仓库视图中展开分支节点,然后右键单击要删除的分支(删除时选中的分支不应是活动分支)。然后选择“删除分支”菜单选项:

图片

图 3.27:删除 Git 分支

将项目提交到远程仓库

到目前为止,我们一直在本地 Git 仓库中工作。但如果你想要共享你的代码并确保你不会丢失本地更改,你可能想要将你的项目推送到远程仓库。所以在本节中,我们将学习如何将本地项目推送到远程 Git 仓库。如果你没有访问 Git 仓库的权限,你可以在www.github.com创建一个。

  1. 在远程 Git 服务器上创建一个名为GitPluginTest的新仓库。

  2. 在 Git Repositories 视图中,右键单击 Remotes 节点并选择创建远程...选项:

图 3.28:添加远程 Git 仓库

  1. 按照惯例,远程仓库的名称是origin。点击 OK。在下一页,设置推送的配置。点击 URI 文本框旁边的 Change 按钮:

图 3.29:设置远程 Git URI

  1. 输入远程 Git 仓库的 URI。向导从 URI 中提取主机、仓库路径和协议。输入你的用户 ID 和密码,然后点击 Finish:

图 3.30:配置 Git push

  1. 点击保存并推送。这会将本地 master 分支中的文件发送到远程 Git 仓库。

从远程仓库拉取更改

当你在团队中工作时,团队成员也会对远程仓库进行更改。当你想要将远程仓库中的更改获取到本地仓库时,你使用 Pull 选项。但在执行 Pull 操作之前,你需要进行配置。

在 Package Explorer 中,右键单击项目并选择团队 | 远程 | 从上游配置 Fetch...:

图 3.31:配置 Git Fetch

在 Git 中,Pull 和 Fetch 都可以从远程仓库获取更改。然而,Fetch 操作不会将更改合并到本地仓库中。Pull 操作首先获取更改,然后将其合并到本地仓库中。如果你想在合并之前检查文件,那么请选择 Fetch 选项。

我们需要将本地 master 分支映射到远程仓库中的一个分支。这告诉 Pull 操作从远程仓库的分支获取更改并将其合并到指定的(在这种情况下,master)本地仓库中。点击 Add...按钮:

图 3.32:配置 Git Fetch

在源文本框中开始键入分支名称,向导将从远程仓库获取分支信息并自动完成。点击 Next 然后 Finish。这会将你带回到配置 Fetch 页面,并添加了映射的分支:

图 3.33:配置 Git Fetch 并添加映射

点击保存并获取以从远程仓库拉取更改。

克隆远程仓库

我们已经学习了如何使用本地 Git 仓库开始开发,然后将更改推送到远程仓库。现在让我们学习如何获取现有的远程 Git 仓库并创建本地副本;换句话说,我们将学习如何克隆远程 Git 仓库。最简单的方法是导入远程 Git 项目。从主菜单中选择文件 | 导入...,然后 Git | 从 Git 导入 | 克隆 URI。

向导将显示一个类似于图 3.29的页面。输入远程仓库的 URI、用户名和密码,然后点击 Next。选择远程分支并点击 Next:

图 3.34:选择要克隆的远程分支

在分支选择页面点击 Next 按钮:

图片

图 3.35:选择克隆项目的位置

选择项目要保存的位置并点击“下一步”:

图片

图 3.36:导入克隆项目的选项

有三种导入克隆项目的选项。如果远程仓库包含整个 Eclipse 项目,则选择“导入现有 Eclipse 项目”,否则选择剩下的两个选项之一。由于我们在远程仓库中已经提交了 Eclipse 项目,我们将选择第一个选项。点击“下一步”然后点击“完成”。

关于 Eclipse Git 插件的更多信息,请参阅wiki.eclipse.org/EGit/User_Guide

摘要

有许多 Eclipse 插件可供各种 SCM 系统使用。在本章中,我们学习了如何使用 Eclipse 插件进行 SVN 和 Git 操作。使用这些插件,你可以在 Eclipse IDE 中执行许多典型的 SCM 操作,例如检出源代码、比较版本和提交更改,这提供了极大的便利并可以提高你的工作效率。

在下一章中,我们将看到如何使用 JDBC 和 JDO 创建 JEE 数据库应用程序。

第四章:创建 JEE 数据库应用程序

在上一章中,我们学习了如何从 Eclipse 使用源代码管理软件。具体来说,我们学习了如何从 Eclipse 使用 SVN 和 Git。在本章中,我们将回到讨论 JEE 应用程序开发。如今,大多数网络应用程序都需要访问数据库。在本章中,我们将学习两种从 JEE 网络应用程序访问数据库的方法:使用 JDBC API 和使用 JPA API。

JDBC4 自 JDK 1.1 版本以来一直是 JDK 的一部分。它提供了统一的 API 来访问不同的关系型数据库。在 JDBC API 和数据库之间,是针对该数据库的 JDBC 驱动程序(由数据库供应商提供或第三方供应商提供)。JDBC 将常见的 API 调用转换为数据库特定的调用。数据库返回的结果也被转换为通用数据访问类的对象。尽管 JDBC API 要求你编写更多的代码来访问数据库,但由于其简单性、使用数据库特定 SQL 语句的灵活性以及低学习曲线,它仍然在 JEE 网络应用程序中很受欢迎。

JPA 是 Java 规范请求 220(代表 JSR)的结果。使用 JDBC API 直接的一个问题是将数据对象表示转换为关系数据。对象表示位于你的 JEE 应用程序中,需要映射到关系数据库中的表和列。处理从关系数据库返回的数据时,这个过程是相反的。如果有一种方法可以自动将网络应用程序中的面向对象的数据表示映射到关系数据,这将节省开发者大量的时间。这也被称为 对象关系映射(ORM)。Hibernate (hibernate.org/) 是 Java 应用程序中非常流行的 ORM 框架。

许多流行的第三方 ORM 框架的概念都被纳入了 JPA。正如 JDBC 为访问关系型数据库提供了统一的 API 一样,JPA 为访问 ORM 库提供了统一的 API。第三方 ORM 框架在其自己的框架之上提供了 JPA 的实现。JPA 实现可能使用 JDBC API 作为底层。

在本章中,我们将构建使用这些框架的应用程序,同时探索 JDBC 和 JPA 的许多特性。实际上,我们将构建同一个应用程序,一次使用 JDBC,然后使用 JPA。

我们将要构建的应用程序是用于学生课程管理。目标是提供一个示例,展示如何建模表之间的关系并在 JEE 应用程序中使用它们。我们将使用 MySQL 数据库和 Tomcat 网络应用程序容器。尽管本章是关于 JEE 的数据库编程,但我们将回顾我们在第二章,“创建简单的 JEE 网络应用程序”中学到的一些关于 JSTL 和 JSF 的内容。我们将使用它们来创建数据库网络应用程序的用户界面。请确保您已按照第二章,“创建简单的 JEE 网络应用程序”中所述在 Eclipse 中配置了 Tomcat。

我们将涵盖以下主题:

  • 核心 JDBC 概念

  • 使用 JDBC 访问数据库

  • 使用 JDBC 连接池

  • 核心 JPA 概念

  • 使用 JPA 将实体(类)映射到数据库中的表

  • 配置 JPA 实体之间的关系

让我们先为这个应用程序创建一个数据库和表。

创建数据库架构

在 MySQL 中创建数据库表和关系有许多方法:

  • 您可以直接在终端的 MySQL 命令提示符中使用 数据描述语言DDL)语句

  • 您可以使用 MySQL Workbench 直接创建表

  • 您可以在 MySQL Workbench 中创建实体-关系图,将其导出以创建 DDL 脚本,然后运行此脚本以创建表和关系

我们将使用第三个选项。如果您只想获取创建表的脚本并跳过创建 ER 图,请跳转到本章的“创建表和关系的脚本”部分。

如果您尚未安装 MySQL 和 MySQL Workbench,请参阅第一章,“介绍 JEE 和 Eclipse”,获取说明:

  1. 打开 MySQL Workbench。选择“文件 | 新模型”菜单。将创建一个空白模型,并可以选择创建 ER 图:

图 4.1:创建新的 MySQL Workbench 模型

  1. 双击“添加图”图标;将打开一个空白 ER 图:

图 4.2:创建新的 ER 图

  1. 默认情况下,新架构命名为 mydb。双击它以打开架构属性。重命名架构为 course_management

    图 4.3:重命名架构

    1. 在页面左侧的工具栏按钮上悬停,您将看到有关其功能的工具提示。单击创建新表的按钮,然后单击空白页面。这将插入一个名为 table1 的新表。双击表图标以打开表的属性页面。在属性页面中,将表名更改为 Course

      图 4.4:在 ER 图中创建表

      1. 现在,我们将创建表的列。双击第一列并命名为 id。勾选 PK(主键)、NN(非空)和 AI(自动递增)复选框。添加其他列,如下面的截图所示!

        图 4.5:在 ER 图中创建表格的列

        1. 创建其他表格,即StudentTeacher,如下面的截图所示!

          图 4.6:创建额外的表格

          注意,如果您想编辑任何表格的列属性,请在 ER 图中双击该表格。仅通过单次点击选择表格不会改变属性页中的表格选择。所有表格中的所有列都是必需的(非空),除了StudentTeacher表中的last_name列。

          我们现在将在表格之间创建关系。一门课程可以有多个学生,学生也可以选修多门课程。因此,CourseStudent之间存在多对多关系。

          我们将假设只有一位教师教授一门课程。然而,一位教师可以教授多门课程。因此,CourseTeacher之间存在多对一关系。

          现在,让我们在 ER 图中建模这些关系:

          1. 首先,我们将创建CourseTeacher之间的非标识关系。

          2. 在工具栏中点击非标识的一对多按钮(虚线和 1:n)。

          3. 然后,首先点击Course表,然后点击Teacher表。这将创建如图图 4.7所示的关联。注意,在Course表中创建了一个外键Teacher_id。我们不想在Course中使Teacher_id字段成为必填项。在我们的应用程序中,课程可以没有教师而存在。因此,双击连接CourseTeacher表的链接。

          4. 然后,点击 外键标签页。

          5. 在 引用表一侧,取消勾选必填复选框!

            图 4.7:在表格之间创建一对一关系

            创建多对多关系需要创建一个链接表。要创建CourseStudent之间的多对多关系,点击多对多(n:m)图标,然后点击 Course表和 Student表。这将创建一个名为Course_has_Student的第三张表(链接表)。我们将此表重命名为Course_Student。最终的图如下所示:

            图 4.8:课程管理示例的 ER 图

            按照以下步骤从 ER 图创建 DDL 脚本:

            1. 选择 文件 | 导出 | 前向工程 SQL 创建脚本... 菜单。

            2. 在 SQL 导出选项页,选择两个选项的复选框:

              • 在每个 CREATE 语句之前生成 DROP 语句

              • 生成 DROP SCHEMA

            3. 如果您想保存脚本,请指定 输出 SQL 脚本文件路径。

            4. 在导出向导的最后一步,您将看到 MySQL Workbench 生成的脚本。通过点击 复制到剪贴板按钮来复制此脚本。

            创建表和关系的脚本

            下面的 DDL 脚本用于创建表和关系,用于课程管理示例:

            -- MySQL Script generated by MySQL Workbench 
            -- Sun Mar  8 18:17:07 2015 
            -- Model: New Model    Version: 1.0 
            -- MySQL Workbench Forward Engineering 
            
            SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; 
            SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; 
            SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES'; 
            
            -- ----------------------------------------------------- 
            -- Schema course_management 
            -- ----------------------------------------------------- 
            DROP SCHEMA IF EXISTS `course_management` ; 
            
            -- ----------------------------------------------------- 
            -- Schema course_management 
            -- ----------------------------------------------------- 
            CREATE SCHEMA IF NOT EXISTS `course_management` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ; 
            USE `course_management` ; 
            
            -- ----------------------------------------------------- 
            -- Table `course_management`.`Teacher` 
            -- ----------------------------------------------------- 
            DROP TABLE IF EXISTS `course_management`.`Teacher` ; 
            
            CREATE TABLE IF NOT EXISTS `course_management`.`Teacher` ( 
              `id` INT NOT NULL AUTO_INCREMENT, 
              `first_name` VARCHAR(45) NOT NULL, 
              `last_name` VARCHAR(45) NULL, 
              `designation` VARCHAR(45) NOT NULL, 
              PRIMARY KEY (`id`)) 
            ENGINE = InnoDB; 
            
            -- ----------------------------------------------------- 
            -- Table `course_management`.`Course` 
            -- ----------------------------------------------------- 
            DROP TABLE IF EXISTS `course_management`.`Course` ; 
            
            CREATE TABLE IF NOT EXISTS `course_management`.`Course` ( 
              `id` INT NOT NULL AUTO_INCREMENT, 
              `name` VARCHAR(45) NOT NULL, 
              `credits` INT NOT NULL, 
              `Teacher_id` INT NULL, 
              PRIMARY KEY (`id`), 
              INDEX `fk_Course_Teacher_idx` (`Teacher_id` ASC), 
              CONSTRAINT `fk_Course_Teacher` 
                FOREIGN KEY (`Teacher_id`) 
                REFERENCES `course_management`.`Teacher` (`id`) 
                ON DELETE NO ACTION 
                ON UPDATE NO ACTION) 
            ENGINE = InnoDB; 
            
            -- ----------------------------------------------------- 
            -- Table `course_management`.`Student` 
            -- ----------------------------------------------------- 
            DROP TABLE IF EXISTS `course_management`.`Student` ; 
            
            CREATE TABLE IF NOT EXISTS `course_management`.`Student` ( 
              `id` INT NOT NULL AUTO_INCREMENT, 
              `first_name` VARCHAR(45) NOT NULL, 
              `last_name` VARCHAR(45) NULL, 
              `enrolled_since` MEDIUMTEXT NOT NULL, 
              PRIMARY KEY (`id`)) 
            ENGINE = InnoDB; 
            
            -- ----------------------------------------------------- 
            -- Table `course_management`.`Course_Student` 
            -- ----------------------------------------------------- 
            DROP TABLE IF EXISTS `course_management`.`Course_Student` ; 
            
            CREATE TABLE IF NOT EXISTS `course_management`.`Course_Student` ( 
              `Course_id` INT NOT NULL, 
              `Student_id` INT NOT NULL, 
              PRIMARY KEY (`Course_id`, `Student_id`), 
              INDEX `fk_Course_has_Student_Student1_idx` (`Student_id` ASC), 
              INDEX `fk_Course_has_Student_Course1_idx` (`Course_id` ASC), 
              CONSTRAINT `fk_Course_has_Student_Course1` 
                FOREIGN KEY (`Course_id`) 
                REFERENCES `course_management`.`Course` (`id`) 
                ON DELETE NO ACTION 
                ON UPDATE NO ACTION, 
              CONSTRAINT `fk_Course_has_Student_Student1` 
                FOREIGN KEY (`Student_id`) 
                REFERENCES `course_management`.`Student` (`id`) 
                ON DELETE NO ACTION 
                ON UPDATE NO ACTION) 
            ENGINE = InnoDB; 
            
            SET SQL_MODE=@OLD_SQL_MODE; 
            SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; 
            SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; 
            

            在 MySQL 中创建表

            让我们现在使用上一节中创建的脚本在 MySQL 数据库中创建表和关系。

            确保 MySQL 正在运行,并且从 MySQL Workbench 到服务器的连接是开放的(有关更多详细信息,请参阅第一章,介绍 JEE 和 Eclipse):

            1. 创建一个新的查询标签页(工具栏中的第一个按钮)并粘贴前面的脚本。

            2. 执行查询。

            3. 执行完成后,在左侧面板中刷新模式。你应该会看到 course_management 模式以及其中创建的表!img/00101.jpeg

            图 4.9:课程管理示例的 MySQL 模式

            使用 JDBC 创建数据库应用程序

            在本节中,我们将使用 JDBC 创建一个简单的课程管理 Web 应用程序。我们将使用上一节中创建的 MySQL 模式。此外,我们将使用 Tomcat 创建 Web 应用程序;我们已经在第二章中看到了如何创建它,创建简单的 JEE Web 应用程序。在同一章中,我们还学习了如何使用 JSTL 和 JSF。在本节中,我们将使用 JSTL 和 JDBC 创建课程管理应用程序,在下一节中,我们将使用 JSF 和 JPA 创建相同的应用程序。我们将使用 Maven(如第二章中所述,创建简单的 JEE Web 应用程序)进行项目管理,当然,我们的 IDE 将是 Eclipse JEE。

            创建项目和设置 Maven 依赖项

            我们将执行以下步骤来创建我们应用程序的 Maven 项目:

            1. 按照第二章中所述创建 Maven Web 项目,创建简单的 JEE Web 应用程序。

            2. 将项目命名为 CourseManagementJDBC

            3. 添加 servlet 和 JSP 的依赖项,但不要添加 JSF 的依赖项。

            4. 要添加 JSTL 的依赖项,打开 pom.xml 并转到依赖项选项卡。点击“添加...”按钮。在搜索框中输入 javax.servlet 并选择 jstl:img/00102.jpeg

            图 4.10:添加 jstl 的依赖项:img/00103.jpeg

            1. 也添加 MySQL JDBC 驱动程序的依赖项:img/00103.jpeg

              图 4.11:添加 MySQL JDBC 驱动程序的依赖项

              在添加依赖项后,这是 pom.xml 文件:

              <project  
              
                xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
               http://maven.apache.org/xsd/maven-4.0.0.xsd"> 
                <modelVersion>4.0.0</modelVersion> 
                <groupId>packt.book.jee.eclipse</groupId> 
                <artifactId>CourseManagementJDBC</artifactId> 
                <version>1</version> 
                <packaging>war</packaging> 
                <dependencies> 
                  <dependency> 
                    <groupId>javax.servlet</groupId> 
                    <artifactId>javax.servlet-api</artifactId> 
                    <version>3.1.0</version> 
              <scope>provided</scope> 
                  </dependency> 
                  <dependency> 
                    <groupId>javax.servlet</groupId> 
                    <artifactId>jstl</artifactId> 
                    <version>1.2</version> 
                  </dependency> 
                  <dependency> 
                    <groupId>mysql</groupId> 
                    <artifactId>mysql-connector-java</artifactId> 
                    <version>8.0.9-rc</version> 
                  </dependency> 
                  <dependency> 
                    <groupId>javax.servlet.jsp</groupId> 
                    <artifactId>jsp-api</artifactId> 
                    <version>2.2</version> 
                    <scope>provided</scope> 
                  </dependency> 
                </dependencies> 
              </project> 
              

              注意,servlet 和 JSP 的依赖项被标记为 provided,这意味着它们将由 Web 容器(Tomcat)提供,并且不会与应用程序一起打包。

              这里省略了如何配置 Tomcat 并向其中添加项目的描述。有关这些详细信息,请参阅第二章,“创建简单的 JEE Web 应用程序”。本节也不会重复介绍如何在第二章“创建简单的 JEE Web 应用程序”中提到的运行 JSP 页面和关于 JSTL 的信息。

              创建用于数据存储的 JavaBeans

              我们将首先为 StudentCourseTeacher 创建 JavaBean 类。由于学生和教师都是人,我们将创建一个新的类 Person,并让 StudentTeacher 类继承它。按照以下方式在 packt.book.jee.eclipse.ch4.beans 包中创建这些 JavaBeans。

              Course 实体的代码如下:

              package packt.book.jee.eclipse.ch4.bean; 
              
              public class Course { 
                private int id; 
                private String name; 
                private int credits; 
                public int getId() { 
                  return id; 
                } 
                public void setId(int id) { 
                  this.id = id; 
                } 
                public String getName() { 
                  return name; 
                } 
                public void setName(String name) { 
                  this.name = name; 
                } 
                public int getCredits() { 
                  return credits; 
                } 
                public void setCredits(int credits) { 
                  this.credits = credits; 
                } 
              } 
              

              Person Bean 的代码如下:

              package packt.book.jee.eclipse.ch4.bean; 
              
              public class Person { 
                private int id; 
                private String firstName; 
                private String lastName; 
              
                public int getId() { 
                  return id; 
                } 
                public void setId(int id) { 
                  this.id = id; 
                } 
                public String getFirstName() { 
                  return firstName; 
                } 
                public void setFirstName(String firstName) { 
                  this.firstName = firstName; 
                } 
                public String getLastName() { 
                  return lastName; 
                } 
                public void setLastName(String lastName) { 
                  this.lastName = lastName; 
                } 
              } 
              

              Student Bean 的代码如下:

              package packt.book.jee.eclipse.ch4.bean; 
              
              public class Student extends Person { 
                private long enrolledsince; 
              
                public long getEnrolledsince() { 
                  return enrolledsince; 
                } 
              
                public void setEnrolledsince(long enrolledsince) { 
                  this.enrolledsince = enrolledsince; 
                } 
              } 
              

              Teacher 实体的代码如下:

              package packt.book.jee.eclipse.ch4.bean; 
              
              public class Teacher extends Person { 
                private String designation; 
              
                public String getDesignation() { 
                  return designation; 
                } 
              
                public void setDesignation(String designation) { 
                  this.designation = designation; 
                } 
              } 
              

              创建用于添加课程的 JSP 页面

              现在让我们创建一个用于添加新课程的 JSP 页面。在包资源管理器中右键单击项目,然后选择“新建 | 其他...”选项。在过滤器框中输入 jsp 并选择 JSP 文件。将文件命名为 addCourse.jsp。Eclipse 将在项目的 src/main/webapp 文件夹中创建该文件。

              addCourse.jsp 文件中输入以下代码:

              <%@ page language="java" contentType="text/html; charset=UTF-8" 
                  pageEncoding="UTF-8"%> 
              <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
              
              <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
              <html> 
              <head> 
              <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
              <title>Add Course</title> 
              </head> 
              <body> 
                <c:set var="errMsg" value="${null}"/> 
                <c:set var="displayForm" value="${true}"/> 
                  <c:if test="${\"POST\".equalsIgnoreCase(pageContext.request.method) 
                      && pageContext.request.getParameter(\"submit\") != null}"> 
                  <jsp:useBean id="courseBean" class="packt.book.jee.eclipse.ch4.bean.Course"> 
                    <c:catch var="beanStorageException"> 
                      <jsp:setProperty name="courseBean" property="*" /> 
                    </c:catch> 
                  </jsp:useBean> 
                  <c:choose> 
                    <c:when test="${!courseBean.isValidCourse() || beanStorageException != null}"> 
                      <c:set var="errMsg" value="Invalid course details. Please 
                       try again"/> 
                    </c:when> 
                    <c:otherwise> 
                      <c:redirect url="listCourse.jsp"/> 
                    </c:otherwise> 
                  </c:choose> 
                </c:if> 
              
                <h2>Add Course:</h2> 
                <c:if test="${errMsg != null}"> 
                  <span style="color: red;"> 
                    <c:out value="${errMsg}"></c:out> 
                  </span> 
                </c:if> 
                <form method="post"> 
                  Name: <input type="text" name="name"> <br> 
                  Credits : <input type="text" name="credits"> <br> 
                  <button type="submit" name="submit">Add</button> 
                </form> 
              
              </body> 
              </html> 
              

              如果您已经阅读了第二章“创建简单的 JEE Web 应用程序”(见 使用 JSTL 部分),那么大部分代码应该都很熟悉。我们有一个表单来添加课程。在文件顶部,我们检查是否发出了 post 请求;如果是,将表单内容存储在 courseBean 中(确保 form 字段的名称与在 Bean 中定义的成员名称相同)。我们在这里使用的新标签是 <c:catch>。它类似于 Java 中的 try-catch 块。在 <c:catch> 的主体中抛出的任何异常都将分配给在 var 属性中声明的变量名称。在这里,我们不对 beanStorageException 做任何事情;我们只是抑制异常。当抛出异常时,Course Bean 的 credits 字段将保持为零,并在 courseBean.isValidCourse 方法中被捕获。如果课程数据有效,则使用 JSTL <c:redirect> 标签将请求重定向到 listCourse.jsp 页面。

              我们需要在 Course Bean 中添加 isValidCourse 方法。因此,在编辑器中打开该类,并添加以下方法:

                public boolean isValidCourse() { 
                  return name != null && credits != 0; 
                } 
              

              我们还需要创建 listCourse.jsp。目前,只需创建一个简单的 JSP 文件,不包含 JSTL/Java 代码,且在 body 标签中只有一个标题:

              <h2>Courses:</h2> 
              

              在包资源管理器中右键单击addCourse.jsp并选择运行方式 | 在服务器上运行。如果你已经正确配置了 Tomcat 并将你的项目添加到 Tomcat 中(如第二章[part0037.html#1394Q0-d43a3a5ee6dd4ebc9d7c7e1cc8d7df55],创建一个简单的 JEE Web 应用程序)中所述),那么你应该看到 JSP 页面在 Eclipse 内部浏览器中运行。使用有效和无效数据(例如,错误的学分值;例如,非数值)测试页面。如果输入的数据有效,则会被重定向到listCourse.jsp,否则会显示相同的页面并带有错误消息。

              在我们开始编写 JDBC 代码之前,让我们学习一些 JDBC 的基本概念。

              JDBC 概念

              在 JDBC 中执行任何操作之前,我们需要与数据库建立连接。以下是 JDBC 中用于执行 SQL 语句的一些重要类/接口:

              JDBC 类/接口 描述
              java.sql.Connection 表示应用程序与后端数据库之间的连接。执行数据库上的任何操作所必需的。
              java.sql.DriverManager 管理应用程序中使用的 JDBC 驱动程序。通过调用DriverManager.getConnection静态方法来获取连接。
              java.sql.Statement 用于执行静态 SQL 语句。
              java.sql.PreparedStatement 用于准备参数化 SQL 语句。SQL 语句被预编译,可以重复使用不同的参数执行。
              Java.sqlCallableStatement 用于执行存储过程。
              java.sql.ResultSet 表示由StatementPreparedStatement执行 SQL 查询后返回的结果集中的数据库表中的行。

              你可以在docs.oracle.com/javase/8/docs/api/java/sql/package-frame.html找到所有 JDBC 接口。

              其中许多是接口,这些接口的实现由 JDBC 驱动程序提供。

              创建数据库连接

              确保你想要连接到的数据库的 JDBC 驱动程序已下载并位于类路径中。在我们的项目中,我们已经通过在 Maven 中添加依赖项来确保这一点。Maven 下载驱动程序并将其添加到我们的 Web 应用程序的类路径中。

              确保当应用程序运行时 JDBC 驱动程序类是可用的。如果不是,我们可以设置一个合适的错误消息并且不执行任何 JDBC 操作。MySQL JDBC 驱动程序类的名称是com.mysql.cj.jdbc.Driver

              try { 
                Class.forName("com.mysql.cj.jdbc.Driver"); 
              } 
              catch (ClassNotFoundException e) { 
                //log excetion 
                //either throw application specific exception or return 
                return; 
              } 
              
              

              然后,通过调用DriverManager.getConnection方法来获取连接:

              try { 
                Connection con = 
               DriverManager.getConnection("jdbc:mysql://localhost:3306/schema_name?" + 
                    "user=your_user_name&password=your_password"); 
              //perform DB operations and then close the connection 
                con.close(); 
              } 
              catch (SQLException e) { 
                //handle exception 
              } 
              https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html.
              

              连接 URL 包含以下详细信息:MySQL 数据库服务器的 hostname、它运行的端口号(默认为 3306)以及模式名称(你想要连接到的数据库名称)。你可以通过 URL 参数传递用户名和密码来连接到数据库。

              创建连接是一个昂贵的操作。此外,数据库服务器允许连接到它的最大连接数,因此应该谨慎创建连接。建议缓存数据库连接并重用。但是,确保在不再需要时关闭连接,例如在代码的final块中。稍后,我们将看到如何创建连接池,以便我们创建有限数量的连接,在需要时从池中取出,执行所需的操作,然后将它们返回到池中以便重用。

              执行 SQL 语句

              使用Statement执行静态 SQL(没有参数)和PreparedStatement执行参数化语句。

              为了避免 SQL 注入的风险,请参阅www.owasp.org/index.php/SQL_injection

              要执行任何Statement,首先需要使用Connection对象创建语句。然后,您可以执行任何 SQL 操作,例如createupdatedeleteselectSelect语句(查询)返回一个ResultSet对象。遍历ResultSet对象以获取单独的行。

              例如,以下代码从Course表中获取所有行:

              Statement stmt = null; 
              ResultSet rs = null; 
              try { 
                stmt = con.createStatement(); 
                rs = stmt.executeQuery("select * from Course"); 
              
                List<Course> courses = new ArrayList<Course>(); 
                //Depending on the database that you connect to, you may have to  
                //call rs.first() before calling rs.next(). In the case of a MySQL 
                //database, it is not necessary to call rs.first() 
                while (rs.next()) { 
                  Course course = new Course(); 
                  course.setId(rs.getInt("id")); 
                  course.setName(rs.getString("name")); 
                  course.setCredits(rs.getInt("credits")); 
                  courses.add(course); 
                } 
              } 
              catch (SQLException e) { 
                //handle exception 
                e.printStackTrace(); 
              } 
              finally { 
                try { 
                  if (rs != null) 
                  rs.close(); 
                  if (stmt != null) 
                  stmt.close(); 
                } 
                catch (SQLException e) { 
                  //handle exception 
                } 
              } 
              

              注意事项:

              • 调用Connection.createStatement()来创建Statement实例。

              • Statement.executeQuery返回ResultSet。如果 SQL 语句不是一个查询,例如createupdatedelete语句,那么调用Statement.execute(如果语句执行成功则返回true;否则返回false)或调用Statement.executeUpdate(返回受影响的行数或如果没有行受影响则返回零)。

              • 将 SQL 语句传递给Statement.executeQuery函数。这可以是数据库理解的任何有效 SQL 字符串。

              • 通过调用next方法遍历ResultSet,直到它返回false

              • 调用不同的get方法变体(根据列的数据类型而定)以获取当前行中ResultSet所指向的列的值。您可以选择传递传递给executeQuery的 SQL 中的列的位置索引,或者传递数据库表或 SQL 语句中指定的别名中使用的列名。例如,如果我们已经在 SQL 中指定了列名,我们会使用以下代码:

              rs = stmt.executeQuery("select id, name, credits as courseCredit from Course"); 
              

              然后,我们可以按以下方式检索列值:

              course.setId(rs.getInt(1)); 
              course.setName(rs.getString(2)); 
              course.setCredits(rs.getInt("courseCredit")); 
              
              • 确保关闭ResultSetStatement

              如果您想获取特定的课程而不是所有课程,您应该使用PreparedStatement

              PreparedStatement stmt = null; 
              int courseId = 10; 
              ResultSet rs = null; 
              try { 
                stmt = con.prepareStatement("select * from Course where id = 
                 ?"); 
                stmt.setInt(1, courseId); 
                rs = stmt.executeQuery(); 
              
                Course course = null; 
                if (rs.next()) { 
                  course = new Course(); 
                  course.setId(rs.getInt("id")); 
                  course.setName(rs.getString("name")); 
                  course.setCredits(rs.getInt("credits")); 
                } 
              } 
              catch (SQLException e) { 
                //handle exception 
                e.printStackTrace(); 
              } 
              finally { 
                try { 
                  if (rs != null) 
                  rs.close(); 
                  if (stmt != null 
                  stmt.close(); 
                } 
              catch (SQLException e) { 
                  //handle exception 
                } 
              } 
              

              在这个例子中,我们试图获取 ID 为 10 的课程。我们首先通过调用 Connection.prepareStatement 获取 PreparedStatement 的实例。请注意,您需要将 SQL 语句作为参数传递给此函数。查询中的参数由 ? 占位符替换。然后,我们通过调用 stmt.setInt 设置参数的值。第一个参数是参数的位置(它从 1 开始),第二个参数是值。对于不同的数据类型,set 方法有许多变体。

              处理事务

              如果您想将多个更改作为单个单元对数据库进行操作,也就是说,所有更改都应该完成或一个都不做,那么您需要在 JDBC 中启动一个事务。您通过调用 Connection.setAutoCommit(false) 来启动事务。一旦所有操作都成功执行,通过调用 Connection.commit 将更改提交到数据库。如果出于任何原因想要中止事务,请调用 Connection.rollback()。直到您调用 Connection.commit,更改都不会在数据库中完成。

              这里是一个将一系列课程插入数据库的示例。虽然在实际应用程序中,当其中一个课程未插入时中止事务可能没有意义,但在这里我们假设要么所有课程都必须插入到数据库中,要么一个都不插入:

              PreparedStatement stmt = con.prepareStatement("insert into Course (id, name, credits) values (?,?,?)"); 
              
              con.setAutoCommit(false); 
              try { 
                for (Course course : courses) { 
                  stmt.setInt(1, course.getId()); 
                  stmt.setString(2, course.getName()); 
                  stmt.setInt(3, course.getCredits()); 
                  stmt.execute(); 
                } 
                //commit the transaction now 
                con.commit(); 
              } 
              catch (SQLException e) { 
                //rollback commit 
                con.rollback(); 
              } 
              

              关于事务,还有更多内容需要学习,这里没有解释。请参阅 Oracle 的 JDBC 教程 docs.oracle.com/javase/tutorial/jdbc/basics/transactions.html

              使用 JDBC 数据库连接池

              如前所述,JDBC 数据库连接是一个昂贵的操作,并且连接对象应该被重用。为此目的,使用连接池。大多数 Web 容器都提供了自己的连接池实现,并提供了使用 JNDI 配置它的方法。Tomcat 也允许您使用 JNDI 配置连接池。使用 JNDI 配置连接池的优势在于,数据库配置参数,如主机名和端口号,保留在源代码之外,并且可以轻松修改。请参阅 tomcat.apache.org/tomcat-8.0-doc/jdbc-pool.html

              然而,Tomcat 连接池也可以在不使用 JNDI 的情况下使用,如前一个链接中所述。在这个例子中,我们将使用不带 JNDI 的连接池。优势在于,您可以使用第三方提供的连接池实现;然后,您的应用程序可以轻松地移植到其他 Web 容器。使用 JNDI,只要您在您要切换到的 Web 容器中创建 JNDI 上下文和资源,您也可以移植您的应用程序。

              我们将向 Maven 的pom.xml文件中添加 Tomcat 连接池库的依赖项。打开pom.xml文件,并添加以下依赖项(参见第二章,创建一个简单的 JEE Web 应用程序,了解如何向 Maven 添加依赖项):

                <dependency> 
                  <groupId>org.apache.tomcat</groupId> 
                  <artifactId>tomcat-jdbc</artifactId> 
                  <version>9.0.6</version> 
                </dependency> 
              

              注意,你可以使用任何其他的 JDBC 连接池实现。其中一个这样的连接池库是 HikariCP (github.com/brettwooldridge/HikariCP)。

              我们还希望将数据库属性从代码中移除。因此,在src/main/resources中创建一个名为db.properties的文件。Maven 将所有文件放在这个文件夹中,并将其放在应用程序的类路径中。在db.properties中添加以下属性:

              db_host=localhost 
              db_port=3306 
              db_name=course_management 
              db_user_name=your_user_name 
              db_password=your_password 
              db_driver_class_name=com.mysql.cj.jdbc.Driver 
              

              我们将创建一个单例类来使用 Tomcat 连接池创建 JDBC 连接。创建一个packt.book.jee.eclipse.ch4.db.connection包,并在其中创建一个DatabaseConnectionFactory类:

              package packt.book.jee.eclipse.ch4.db.connection; 
              
              // skipping imports to save space here
              
              /** 
               * Singleton Factory class to create JDBC database connections 
               * 
               */ 
              public class DatabaseConnectionFactory { 
                //singleton instance 
                private static DatabaseConnectionFactory conFactory = new 
                 DatabaseConnectionFactory(); 
              
                private DataSource dataSource = null; 
              
                //Make the construction private 
                private DatabaseConnectionFactory() {} 
              
                /** 
                 * Must be called before any other method in this class. 
                 * Initializes the data source and saves it in an instance 
                 variable 
                 * 
                 * @throws IOException 
                 */ 
                public synchronized void init() throws IOException { 
                  //Check if init was already called 
                if (dataSource != null) 
                  return; 
              
                  //load db.properties file first 
                  InputStream inStream = 
               this.getClass().getClassLoader().getResourceAsStream("db.properties"); 
                  Properties dbProperties = new Properties(); 
                  dbProperties.load(inStream); 
                  inStream.close(); 
              
                  //create Tomcat specific pool properties 
                  PoolProperties p = new PoolProperties(); 
              p.setUrl("jdbc:mysql://" + dbProperties.getProperty("db_host") + 
              ":" + dbProperties.getProperty("db_port") + "/" + 
              dbProperties.getProperty("db_name")); 
              
              p.setDriverClassName(dbProperties.getProperty("db_driver_class_name")); 
                  p.setUsername(dbProperties.getProperty("db_user_name")); 
                  p.setPassword(dbProperties.getProperty("db_password")); 
                  p.setMaxActive(10); 
              
                  dataSource = new DataSource(); 
                  dataSource.setPoolProperties(p); 
                } 
              
                //Provides access to singleton instance 
                public static DatabaseConnectionFactory getConnectionFactory() { 
                  return conFactory; 
                } 
              
                //returns database connection object  
                public Connection getConnection () throws SQLException { 
                  if (dataSource == null) 
                    throw new SQLException("Error initializing datasource"); 
                  return dataSource.getConnection(); 
                } 
              } 
              

              在从它获取连接之前,我们必须调用DatabaseConnectionFactoryinit方法。我们将创建一个 servlet 并在启动时加载它。然后,我们将从 servlet 的init方法中调用DatabaseConnectionFactory.init

              创建package packt.book.jee.eclipse.ch4.servlet,然后在其中创建一个InitServlet类:

              package packt.book.jee.eclipse.ch4.servlet; 
              import java.io.IOException; 
              import javax.servlet.ServletConfig; 
              import javax.servlet.ServletException; 
              import javax.servlet.annotation.WebServlet; 
              import javax.servlet.http.HttpServlet; 
              
              import packt.book.jee.eclipse.ch4.db.connection.DatabaseConnectionFactory; 
              
              @WebServlet(value="/initServlet", loadOnStartup=1) 
              public class InitServlet extends HttpServlet { 
                private static final long serialVersionUID = 1L; 
              
                public InitServlet() { 
                  super(); 
                } 
              
                public void init(ServletConfig config) throws ServletException { 
                  try { 
                    DatabaseConnectionFactory.getConnectionFactory().init(); 
                  } 
                  catch (IOException e) { 
                    config.getServletContext().log(e.getLocalizedMessage(),e); 
                  } 
                } 
              } 
              

              注意,我们使用了@WebServlet注解来标记这个类为 servlet,并将loadOnStartup属性设置为1,以告诉 web 容器在启动时加载这个 servlet。

              现在,我们可以在应用程序的任何地方调用以下语句来获取一个Connection对象:

              Connection con = DatabaseConnectionFactory.getConnectionFactory().getConnection(); 
              

              如果连接池中没有更多的连接可用,那么getConnection方法会抛出一个异常(特别是在Tomcat数据源的情况下,它会抛出PoolExhaustedException)。当你关闭从连接池获得的连接时,连接会被返回到池中以供重用。

              使用 JDBC 在数据库表保存课程

              现在我们已经弄清楚如何使用 JDBC 连接池并从中获取连接,让我们编写将课程保存到数据库的代码。

              我们将创建课程数据访问对象CourseDAO),它将具有直接与数据库交互所需的功能。因此,我们将访问数据库的代码与 UI 和业务代码分离。

              创建package packt.book.jee.eclipse.ch4.dao。在它里面创建一个名为CourseDAO的类:

              package packt.book.jee.eclipse.ch4.dao; 
              
              import java.sql.Connection; 
              import java.sql.PreparedStatement; 
              import java.sql.ResultSet; 
              import java.sql.SQLException; 
              import java.sql.Statement; 
              
              import packt.book.jee.eclipse.ch4.bean.Course; 
              import packt.book.jee.eclipse.ch4.db.connection.DatabaseConnectionFactory; 
              
              public class CourseDAO { 
              
                public static void addCourse (Course course) throws SQLException 
                 { 
                  //get connection from connection pool 
                  Connection con = 
               DatabaseConnectionFactory.getConnectionFactory().getConnection(); 
                  try { 
                    final String sql = "insert into Course (name, credits) 
                     values (?,?)";      //create the prepared statement with an option to get auto- 
                     generated keys      PreparedStatement stmt = con.prepareStatement(sql, 
                     Statement.RETURN_GENERATED_KEYS); 
                    //set parameters 
                    stmt.setString(1, course.getName()); 
                    stmt.setInt(2, course.getCredits()); 
              
                    stmt.execute(); 
              
                    //Get auto-generated keys 
                    ResultSet rs = stmt.getGeneratedKeys(); 
              
                    if (rs.next()) 
                      course.setId(rs.getInt(1)); 
              
                    rs.close(); 
                    stmt.close(); 
                  } 
                  finally { 
                    con.close(); 
                  } 
                } 
              } 
              

              我们已经看到了如何使用 JDBC 插入记录。前述代码中唯一的新内容是获取自动生成的 ID。回想一下,Course表中的id列是自动生成的。这就是为什么我们没有在插入 SQL 中指定它的原因:

              String sql = "insert into Course (name, credits) values (?,?)"; 
              

              当我们准备一个语句时,我们是在告诉驱动程序获取自动生成的 ID。在行被插入到表中之后,我们通过调用以下代码来获取自动生成的 ID:

              ResultSet rs = stmt.getGeneratedKeys(); 
              

              我们已经创建了 addCourse.jsp。某种方式下,addCourse.jsp 需要将表单数据发送到 CourseDAO 以将数据保存到数据库中。addCourse.jsp 已经可以访问 Course 实体,并将表单数据保存在其中。因此,Course 实体在 addCourse.jspCourseDAO 之间作为接口是有意义的。让我们修改 Course 实体,添加一个 CourseDAO 实例作为成员变量,然后创建一个向数据库添加课程(CourseDAO 实例)的功能:

              public class Course { 
              .... 
              
                private CourseDAO courseDAO = new CourseDAO(); 
              
              ... 
              
                public void addCourse() throws SQLException { 
                  courseDAO.addCourse(this); 
                } 
              } 
              

              然后,我们将修改 addCourse.jsp 以调用 Course 实体的 addCourse 方法。我们将在表单提交和数据验证后添加此代码:

              <c:catch var="addCourseException"> 
                ${courseBean.addCourse()} 
              </c:catch> 
              <c:choose> 
                <c:when test="${addCourseException != null}"> 
                  <c:set var="errMsg" value="${addCourseException.message}"/> 
                </c:when> 
                <c:otherwise> 
                  <c:redirect url="listCourse.jsp"/> 
                </c:otherwise> 
              </c:choose> 
              

              在前面的代码中需要注意的一点是以下语句:

              ${courseBean.addCourse()} 
              

              你可以在 JSP 中插入 表达式语言EL),正如之前所讨论的。这种方法不返回任何内容(它是一个无返回值的方法)。因此,我们没有使用 <c:set> 标签。此外,请注意调用是在 <c:catch> 标签内进行的。如果方法抛出任何 SQLException,则它将被分配给 addCourseException 变量。然后我们在 <c:when> 标签中检查 addCourseException 是否已设置。如果值不为 null,则意味着抛出了异常。我们设置错误消息,该消息稍后将在同一页面上显示。如果没有抛出错误,则请求将被重定向到 listCourse.jsp。以下是 addCourse.jsp 的完整代码:

              <%@ page language="java" contentType="text/html; charset=UTF-8" 
                  pageEncoding="UTF-8"%> 
              <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
              
              <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
               "http://www.w3.org/TR/html4/loose.dtd"> 
              <html> 
              <head> 
              <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
              <title>Insert title here</title> 
              </head> 
              <body> 
                <c:set var="errMsg" value="${null}"/> 
                <c:set var="displayForm" value="${true}"/> 
                <c:if 
               test="${"POST".equalsIgnoreCase(pageContext.request.method) 
              && pageContext.request.getParameter("submit") != null}"> 
                <jsp:useBean id="courseBean" 
                 class="packt.book.jee.eclipse.ch4.bean.Course"> 
                  <c:catch var="beanStorageException"> 
                  <jsp:setProperty name="courseBean" property="*" /> 
                  </c:catch> 
                  </jsp:useBean> 
                  <c:choose> 
                    <c:when test="${!courseBean.isValidCourse() || 
                     beanStorageException != null}">      <c:set var="errMsg" value="Invalid course details. Please 
                     try again"/> 
                    </c:when> 
                    <c:otherwise> 
                      <c:catch var="addCourseException"> 
                      ${courseBean.addCourse()} 
                      </c:catch> 
                      <c:choose> 
                        <c:when test="${addCourseException != null}"> 
                        <c:set var="errMsg" 
                         value="${addCourseException.message}"/> 
                        </c:when> 
                        <c:otherwise> 
                          <c:redirect url="listCourse.jsp"/> 
                        </c:otherwise> 
                      </c:choose> 
                    </c:otherwise> 
                  </c:choose> 
                </c:if> 
              
                <h2>Add Course:</h2> 
                <c:if test="${errMsg != null}"> 
                  <span style="color: red;"> 
                    <c:out value="${errMsg}"></c:out> 
                  </span> 
                </c:if> 
                <form method="post"> 
                  Name: <input type="text" name="name"> <br> 
                  Credits : <input type="text" name="credits"> <br> 
                  <button type="submit" name="submit">Add</button> 
                </form> 
              
              </body> 
              </html> 
              

              运行页面,无论是在 Eclipse 中还是在 Eclipse 的内部浏览器外(参见第二章,创建一个简单的 JEE Web 应用程序,了解如何在 Eclipse 中运行 JSP 并在 Eclipse 的内部浏览器中查看它),并添加几门课程。

              使用 JDBC 从数据库表获取课程

              我们现在将修改 listCourses.jsp 以显示我们使用 addCourse.jsp 添加的课程。然而,我们首先需要在 CourseDAO 中添加一个方法来从数据库获取所有课程。

              注意,Course 表与 Teacher 表之间存在一对一的关系。它存储了教师 ID。此外,教师 ID 不是必填字段,因此课程可以在 Course 表中以 nullteacher_id 存在。要获取一个课程的全部详细信息,我们需要获取该课程的教师。然而,我们无法在 SQL 查询中创建一个简单的连接来获取课程和每个课程的教师的详细信息,因为可能没有为课程设置教师。在这种情况下,我们使用 左外连接,它返回连接左侧表的所有记录,但只返回连接右侧表匹配的记录。以下是获取所有课程和每个课程的教师的 SQL 语句:

              select course.id as courseId, course.name as courseName, 
                course.credits as credits, Teacher.id as teacherId, 
                Teacher.first_name as firstName,Teacher.last_name as lastName, 
                Teacher.designation designation 
              from Course left outer join Teacher on 
              course.Teacher_id = Teacher.id 
              order by course.name 
              

              我们将在 CourseDAO 中使用前面的查询来获取所有课程。打开 CourseDAO 类并添加以下方法:

              public List<Course> getCourses () throws SQLException { 
                //get connection from connection pool 
                Connection con = 
               DatabaseConnectionFactory.getConnectionFactory().getConnection(); 
              
                List<Course> courses = new ArrayList<Course>(); 
                Statement stmt = null; 
                ResultSet rs = null; 
                try { 
                  stmt = con.createStatement(); 
              
                  //create SQL statement using left outer join 
                  StringBuilder sb = new StringBuilder("select course.id as 
                   courseId, course.name as courseName,")      .append("course.credits as credits, Teacher.id as teacherId, 
                     Teacher.first_name as firstName, ")      .append("Teacher.last_name as lastName, Teacher.designation 
                     designation ") 
                    .append("from Course left outer join Teacher on ") 
                    .append("course.Teacher_id = Teacher.id ") 
                    .append("order by course.name"); 
              
              //execute the query 
                  rs = stmt.executeQuery(sb.toString()); 
              
              //iterate over result set and create Course objects 
              //add them to course list 
                  while (rs.next()) { 
                    Course course = new Course(); 
                    course.setId(rs.getInt("courseId")); 
                    course.setName(rs.getString("courseName")); 
                    course.setCredits(rs.getInt("credits")); 
                    courses.add(course); 
              
                    int teacherId = rs.getInt("teacherId"); 
              //check whether teacher id was null in the table 
                    if (rs.wasNull()) //no teacher set for this course. 
                      continue; 
                    Teacher teacher = new Teacher(); 
                    teacher.setId(teacherId); 
                    teacher.setFirstName(rs.getString("firstName")); 
                    teacher.setLastName(rs.getString("lastName")); 
                    teacher.setDesignation(rs.getString("designation")); 
                    course.setTeacher(teacher); 
                  } 
              
                  return courses; 
                } 
              finally { 
                try {if (rs != null) rs.close();} catch (SQLException e) {} 
                try {if (stmt != null) stmt.close();} catch (SQLException e) {} 
                try {con.close();} catch (SQLException e) {} 
                } 
              } 
              

              我们使用Statement来执行查询,因为它是一个静态查询。我们使用StringBuilder来构建 SQL 语句,因为它是一个相对较大的查询(与之前所写的查询相比),我们希望避免字符串对象的连接,因为字符串是不可变的。在执行查询后,我们遍历结果集,创建一个Course对象并将其添加到课程列表中,最后返回该列表。

              这里一个有趣的事情是使用ResultSet.wasNull。我们想检查特定行的Course表中的teacher_id字段是否为 null。因此,在调用rs.getInt("teacherId")之后,我们通过调用rs.wasNull来检查通过ResultSet获取的值是否为 null。如果teacher_id为 null,则表示该课程没有设置教师,所以我们继续循环,跳过创建Teacher对象的代码。

              在最后的代码块中,我们在关闭ResultSetStatementConnection时捕获异常并忽略它。

              现在我们向Course对象中添加一个方法来通过调用CourseDAOgetCourses方法来获取课程。打开Course对象并添加以下方法:

              public List<Course> getCourses() throws SQLException { 
                return courseDAO.getCourses(); 
              } 
              

              我们现在准备好修改listCourse.jsp以显示课程。打开 JSP 并替换现有的代码为以下内容:

              <%@ page language="java" contentType="text/html; charset=UTF-8" 
                  pageEncoding="UTF-8"%> 
              <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
              
              <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
              <html> 
              <head> 
              <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
              <title>Courses</title> 
              </head> 
              <body> 
                <c:catch var="err"> 
                  <jsp:useBean id="courseBean" 
                   class="packt.book.jee.eclipse.ch4.bean.Course"/> 
                  <c:set var="courses" value="${courseBean.getCourses()}"/> 
                </c:catch> 
                <c:choose> 
                  <c:when test="${err != null}"> 
                    <c:set var="errMsg" value="${err.message}"/> 
                  </c:when> 
                  <c:otherwise> 
                  </c:otherwise> 
                </c:choose> 
                <h2>Courses:</h2> 
                <c:if test="${errMsg != null}"> 
                  <span style="color: red;"> 
                    <c:out value="${errMsg}"></c:out> 
                  </span> 
                </c:if> 
                <table> 
                  <tr> 
                    <th>Id</th> 
                    <th>Name</th> 
                    <th>Credits</th> 
                    <th>Teacher</th> 
                  </tr> 
                  <c:forEach items="${courses}" var="course"> 
                    <tr> 
                      <td>${course.id}</td> 
                      <td>${course.name}</td> 
                      <td>${course.credits}</td> 
                      <c:choose> 
                        <c:when test="${course.teacher != null}"> 
                          <td>${course.teacher.firstName}</td> 
                        </c:when> 
                        <c:otherwise> 
                          <td></td> 
                        </c:otherwise> 
                      </c:choose> 
                    </tr> 
                  </c:forEach> 
                </table> 
              </body> 
              </html> 
              

              大部分代码应该很容易理解,因为我们已经在之前的例子中使用过类似的代码。在脚本开始时,我们创建一个Course对象并获取所有课程,将课程列表赋值给名为courses的变量:

              <c:catch var="err"> 
                  <jsp:useBean id="courseBean" 
                   class="packt.book.jee.eclipse.ch4.bean.Course"/> 
                  <c:set var="courses" value="${courseBean.getCourses()}"/> 
              </c:catch> 
              

              要显示课程,我们创建一个 HTML 表格并设置其标题。在前面的代码中,新的一步是使用<c:forEach> JSTL 标签来遍历列表。forEach标签包含以下两个属性:

              • 对象列表

              • 遍历列表时单个项目的变量名

              在前面的例子中,对象列表是由我们在脚本开始时设置的courses变量提供的,我们用变量名course来标识列表中的单个项目。然后我们显示课程详情和教师信息(如果有)。

              编写添加TeacherStudent并列出它们的代码留给读者作为练习。代码将与course非常相似,但表和类名不同。

              完成添加课程功能

              我们还没有完成添加新课程的功能;我们需要在添加新课程时提供一个选项来为课程分配教师。假设你已经实现了TeacherDAO并在Teacher对象中创建了addTeachergetTeachers方法,我们现在可以完成添加课程的功能。

              首先,修改CourseADO中的addCourse以保存每个课程的教师 ID,如果它不是零。插入课程更改的 SQL 语句如下:

              String sql = "insert into Course (name, credits, Teacher_id) values (?,?,?)"; 
              

              我们添加了Teacher_id列和相应的参数持有者?。如果它是零,我们将Teacher_id设置为 null;否则设置为实际值:

              if (course.getTeacherId() == 0) 
                stmt.setNull(3, Types.INTEGER); 
              else 
                stmt.setInt(3,course.getTeacherId()); 
              

              然后,我们将修改Course对象以保存将随 HTML 表单的POST请求一起传递的教师 ID:

              public class Course { 
              
                private int teacherId; 
                public int getTeacherId() { 
                  return teacherId; 
              } 
                public void setTeacherId(int teacherId) { 
                  this.teacherId = teacherId; 
                } 
              } 
              

              接下来,我们将修改addCourse.jsp以在添加新课程时显示教师下拉列表。我们首先需要获取教师列表。因此,我们将创建一个Teacher对象,并在其上调用getTeachers方法。我们将在“添加课程”标题之前做这件事:

              <jsp:useBean id="teacherBean" class="packt.book.jee.eclipse.ch4.bean.Teacher"/> 
              <c:catch var="teacherBeanErr"> 
              <c:set var="teachers" value="${teacherBean.getTeachers()}"/> 
              </c:catch> 
              <c:if test="${teacherBeanErr != null}"> 
                <c:set var="errMsg" value="${err.message}"/> 
              </c:if> 
              

              在表单中显示 HTML 下拉列表,并用教师姓名填充它:

              Teacher : 
              <select name="teacherId"> 
              <c:forEach items="${teachers}" var="teacher"> 
              <option value="${teacher.id}">${teacher.firstName} 
              </option> 
              </c:forEach> 
              </select> 
              

              下载本章的配套代码,以查看CourseDAOaddCourse.jsp的完整源代码。

              通过这种方式,我们结束了关于使用 JDBC 创建使用数据库的 Web 应用程序的讨论。通过您迄今为止看到的示例,您应该能够通过添加修改和删除数据库记录的功能来完成剩余的应用程序。updatedelete SQL 语句可以通过StatementPreparedStatement执行,就像insert语句使用这两个类一样执行。

              使用 Eclipse 数据源浏览器

              如果您可以从 IDE 中查看数据库表中的数据并修改它,有时会很有用。在 Eclipse JEE 中使用数据源浏览器就可以做到这一点。此视图在 Java EE 视图中显示在底部面板的标签中,位于编辑器下方。如果您看不到此视图或已关闭视图,可以通过选择“窗口 | 显示视图 | 其他”菜单重新打开它。在过滤器文本框中输入“数据源”,您应该在“数据管理”组下看到视图名称。打开视图:

              图片 5

              图片 3

              右键点击数据库连接节点,选择新建。从列表中选择 MySQL:

              图片 6

              图 4.13:选择 MySQL 连接配置文件

              点击“下一步”。如果驱动程序列表为空,您尚未配置驱动程序。点击下拉列表旁边的图标以打开配置页面:

              图片 7

              图片 2

              选择适当的 MySQL 版本,然后点击 JAR 列表标签:

              图片 1

              图片 4

              从驱动程序文件列表中删除任何文件。点击“添加 JAR/ZIP...”按钮。这将打开文件打开对话框。选择您已选择的 MySQL 驱动程序的 JAR 文件。由于 Maven 已经为您下载了 JAR 文件,您可以从本地 Maven 仓库中选择它。在 OS X 和 Linux 上,路径是~/.m2/repository/mysql/mysql-connector-java/<version_num>/mysql_connector_java_version_num/mysql-connector-java-version_num.jarversion_num是路径中实际版本号的占位符)。在 Windows 上,您可以在C:\Users\{your-username}\.m2找到 Maven 仓库,然后 MySQL 驱动程序的相对路径与 OS X 上的相同。

              如果你在本地 Maven 仓库中找不到 JAR 文件,你可以从 dev.mysql.com/downloads/connector/j/ 下载 JAR 文件(MySQL JDBC 驱动程序)。

              一旦指定了正确的驱动程序 JAR 文件,你需要设置以下属性:

              img/00108.jpeg

              图 4.16:设置 JDBC 驱动程序属性

              点击“下一步”然后“完成”。将在数据源资源管理器中添加一个新的数据库连接。你现在可以浏览数据库模式和表:

              img/00109.jpeg

              图 4.17:在数据源资源管理器中浏览表

              右键单击任何表以查看不同操作可用的菜单选项:

              img/00110.jpeg

              图 4.18:数据源资源管理器中的表菜单选项

              选择“编辑”菜单以在编辑器中打开一个页面,你可以看到表中现有的记录。你还可以在同一页面上修改或添加新数据。选择“加载”选项将数据从外部文件加载到表中。选择“提取”选项将数据从表中导出。

              使用 JPA 创建数据库应用程序

              在上一节中,我们学习了如何使用 JDBC 和 JSTL 创建 课程管理 应用程序。在本节中,我们将使用 JPA 和 JSF 构建相同的应用程序。我们在 第二章,创建一个简单的 JEE Web 应用程序 中学习了如何使用 JSF 创建一个 Web 应用程序。我们将在本节中大量使用这些知识。

              如本章开头所述,JPA 是一个 ORM 框架,现在是 JEE 规范的一部分。在撰写本文时,它处于 2.2 版本。随着我们开发应用程序,我们将学习很多关于 JPA 的知识。

              创建一个名为 CourseManagementJPA 的 Maven 项目,其组 ID 为 packt.book.jee_eclipse,组件 ID 为 CourseManagementJPA。Eclipse JEE 有创建使用 JPA 的应用程序的强大工具,但你需要将你的项目转换为 JPA 项目。我们将在本节后面看到如何做到这一点。

              使用 JSF 创建添加课程的用户界面

              在我们使用 JPA 编写任何数据访问代码之前,让我们首先使用 JSF 创建用户界面。正如我们在 第二章,创建一个简单的 JEE Web 应用程序 中所学的,我们需要在 pom.xml 中添加 Maven 依赖项以支持 JSF。在 pom.xml 中添加以下依赖项:

                <dependencies> 
                  <dependency> 
                    <groupId>javax.servlet</groupId> 
                    <artifactId>javax.servlet-api</artifactId> 
                    <version>3.1.0</version> 
                    <scope>provided</scope> 
                  </dependency> 
                  <dependency> 
                    <groupId>com.sun.faces</groupId> 
                    <artifactId>jsf-api</artifactId> 
                    <version>2.2.16</version> 
                  </dependency> 
                  <dependency> 
                    <groupId>com.sun.faces</groupId> 
                    <artifactId>jsf-impl</artifactId> 
                    <version>2.2.16</version> 
                  </dependency> 
                </dependencies> 
              

              当您稍后运行应用程序时,如果 Tomcat 抛出找不到 javax.faces.webapp.FacesServlet 的异常,那么您可能需要下载 jsf-api-2.2.16.jar (central.maven.org/maven2/com/sun/faces/jsf-impl/2.2.16/jsf-impl-2.2.16.jar) 和 jsf-impl-2.2.16.jar (central.maven.org/maven2/com/sun/faces/jsf-impl/2.2.16/jsf-impl-2.2.16.jar),并将它们复制到 <tomcat-install-folder>/lib 文件夹中。将这些库的作用域设置为提供:在 pom.xml 中设置为 <scope>provided</scope>。然后清理项目(运行 As | Maven Clean)并重新安装(运行 As | Maven Install)。

              我们需要添加 web.xml,在其中添加对 JSF Servlet 的声明,并添加 Servlet 映射。Eclipse 提供了一种非常简单的方法来添加 web.xml(它应该位于 WEB-INF 文件夹中)。在项目上右键单击,选择 Java EE Tools | Generate Deployment Descriptor Stub 菜单。这将在 src/main/webapp 下的 WEB-INF 文件夹中创建 WEB-INF 文件夹,并在 WEB-INF 文件夹中创建具有默认内容的 web.xml。现在,添加以下 Servlet 和映射:

                <servlet> 
                  <servlet-name>JSFServlet</servlet-name> 
                  <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> 
                  <load-on-startup>1</load-on-startup> 
                </servlet> 
                <servlet-mapping> 
                  <servlet-name>JSFServlet</servlet-name> 
                  <url-pattern>*.xhtml</url-pattern> 
                </servlet-mapping> 
              

              现在,让我们创建 CourseTeacherStudentPerson 的 JavaBeans,就像我们在上一个例子中为 JDBC 创建它们一样。创建一个名为 packt.book.jee.eclipse.ch4.jpa.bean 的包,并创建以下 JavaBeans。

              这是 Course Bean 的源代码(在 Course.java 中):

              package packt.book.jee.eclipse.ch4.jpa.bean; 
              
              import java.io.Serializable; 
              import javax.faces.bean.ManagedBean; 
              import javax.faces.bean.RequestScoped; 
              
              @ManagedBean (name="course") 
              @RequestScoped 
              public class Course implements Serializable { 
                private static final long serialVersionUID = 1L; 
              
                private int id; 
                private String name; 
                private int credits; 
                private Teacher teacher; 
              
                public int getId() { 
                  return id; 
                } 
                public void setId(int id) { 
                  this.id = id; 
                } 
                public String getName() { 
                  return name; 
                } 
                public void setName(String name) { 
                  this.name = name; 
                } 
                public int getCredits() { 
                  return credits; 
                } 
                public void setCredits(int credits) { 
                  this.credits = credits; 
                } 
                public boolean isValidCourse() { 
                  return name != null && credits != 0; 
                } 
                public Teacher getTeacher() { 
                  return teacher; 
                } 
                public void setTeacher(Teacher teacher) { 
                  this.teacher = teacher; 
                } 
              } 
              

              这是 Person Bean 的源代码(在 Person.java 中):

              package packt.book.jee.eclipse.ch4.jpa.bean; 
              
              import java.io.Serializable; 
              
              public class Person implements Serializable{ 
                private static final long serialVersionUID = 1L; 
              
                private int id; 
                private String firstName; 
                private String lastName; 
              
                public int getId() { 
                  return id; 
                } 
                public void setId(int id) { 
                  this.id = id; 
                } 
                public String getFirstName() { 
                  return firstName; 
                } 
                public void setFirstName(String firstName) { 
                  this.firstName = firstName; 
                } 
                public String getLastName() { 
                  return lastName; 
                } 
                public void setLastName(String lastName) { 
                  this.lastName = lastName; 
                } 
              } 
              

              这是 Student Bean 的源代码(在 Student.java 中):

              package packt.book.jee.eclipse.ch4.jpa.bean; 
              
              import javax.faces.bean.ManagedBean; 
              import javax.faces.bean.RequestScoped; 
              import java.util.Date; 
              
              @ManagedBean (name="student") 
              @RequestScoped 
              public class Student extends Person { 
                private static final long serialVersionUID = 1L; 
              
                private Date enrolledsince; 
              
                public Date getEnrolledsince() { 
                  return enrolledsince; 
                } 
              
                public void setEnrolledsince(Date enrolledsince) { 
                  this.enrolledsince = enrolledsince; 
                } 
              } 
              

              最后,以下是 Teacher Bean 的源代码(在 Teacher.java 中):

              package packt.book.jee.eclipse.ch4.jpa.bean; 
              
              import javax.faces.bean.ManagedBean; 
              import javax.faces.bean.RequestScoped; 
              
              @ManagedBean (name="teacher") 
              @RequestScoped 
              public class Teacher extends Person { 
                private static final long serialVersionUID = 1L; 
              
                private String designation; 
              
                public String getDesignation() { 
                  return designation; 
                } 
              
                public void setDesignation(String designation) { 
                  this.designation = designation; 
                } 
                public boolean isValidTeacher() { 
                  return getFirstName() != null; 
                } 
              } 
              

              所有这些都是 RequestScope 中的 JSF 管理 Bean。有关管理 Bean 和作用域的更多信息,请参阅第二章中的 JSF 讨论内容,创建一个简单的 JEE Web 应用程序

              这些豆子现在可以用于 JSF 页面。创建一个 JSF 页面,并将其命名为 addCourse.xhtml,然后添加以下内容:

              <html  
              
                  > 
              
                <h2>Add Course:</h2> 
                <h:form> 
                  <h:outputLabel value="Name:" for="name"/> 
                    <h:inputText value="#{course.name}" id="name"/> <br/> 
                  <h:outputLabel value="Credits:" for="credits"/> 
                    <h:inputText value="#{course.credits}" id="credits"/> 
                  <br/> 
                  <h:commandButton value="Add" action=" 
                   #{courseServiceBean.addCourse} "/> 
                </h:form> 
              
              </html> 
              

              该页面使用 JSF 标签和管理 Bean 来获取和设置值。注意 h:commandButton 标签的 action 属性值——它是 courseServiceBean.addCourse 方法,当点击添加按钮时将被调用。在我们使用 JDBC 创建的应用程序中,我们编写了与 JavaBean 中的 DAO 交互的代码。例如,Course Bean 有 addCourse 方法。然而,在 JPA 项目中,我们将以不同的方式处理。我们将创建服务 Bean 类(它们也是管理 Bean,就像 Course 一样)来与数据访问对象交互,并让 Course Bean 只包含用户设置的值。

              创建一个名为 packt.book.jee.eclipse.ch4.jpa.service_bean 的包。在这个包中创建一个名为 CourseServiceBean 的类,其代码如下:

              package packt.book.jee.eclipse.ch4.jpa.service_bean; 
              
              import javax.faces.bean.ManagedBean; 
              import javax.faces.bean.ManagedProperty; 
              import javax.faces.bean.RequestScoped; 
              
              import packt.book.jee.eclipse.ch4.jpa.bean.Course; 
              
              @ManagedBean(name="courseServiceBean") 
              @RequestScoped 
              public class CourseServiceBean { 
                @ManagedProperty(value="#{course}") 
                private Course course; 
              
                private String errMsg= null; 
              
                public Course getCourse() { 
                  return course; 
                } 
              
                public void setCourse(Course course) { 
                  this.course = course; 
                } 
              
                public String getErrMsg() { 
                  return errMsg; 
                } 
              
                public void setErrMsg(String errMsg) { 
                  this.errMsg = errMsg; 
                } 
              
                public String addCourse() { 
                  return "listCourse"; 
                } 
              } 
              

              CourseServiceBean 是一个托管豆,它包含 errMsg 字段(用于存储在请求处理过程中的任何错误消息)、addCourse 方法以及 course 字段(该字段注解了 @ManagedProperty)。

              ManagedProperty 注解告诉 JSF 实现将另一个豆(指定为 value 属性)注入当前豆中。在这里,我们期望 CourseServiceBean 在运行时能够访问 course 豆,而不需要实例化它。这是 Java EE 支持的 依赖注入(DI) 框架的一部分。我们将在后面的章节中了解更多关于 Java EE 中的 DI 框架的内容。在这一点上,addCourse 函数并没有做什么,它只是返回了 "listCourse" 字符串。如果你现在想执行 addCourse.xhtml,创建一个包含一些占位符内容的 listCourse.xml 文件并测试 addCourse.xhtml。我们将在本节的后面添加更多内容到 listCourse.xml

              JPA 概念

              JPA 是 JEE 中的一个 ORM 框架。它提供了一组 API,JPA 实现提供者预期将实现这些 API。有许多 JPA 提供者,例如 EclipseLink (eclipse.org/eclipselink/)、Hibernate JPA (hibernate.org/orm/) 和 OpenJPA (openjpa.apache.org/)。在我们开始使用 JPA 编写持久化代码之前,了解 JPA 的基本概念是很重要的。

              实体

              实体代表一个单个对象实例,通常与一个表相关联。任何 纯 Java 对象(POJO)都可以通过在类上注解 @Entity 转换为实体。类的成员映射到数据库表中的列。实体类是简单的 Java 类,因此它们可以扩展或包含其他 Java 类,甚至另一个 JPA 实体。我们将在我们的应用程序中看到这个例子。您还可以为实体类的成员指定验证规则;例如,您可以使用 @NotNull 注解标记一个成员为非空。这些注解由 Java EE Bean Validation API 提供。有关验证注解的列表,请参阅 javaee.github.io/tutorial/bean-validation002.html#GIRCZ

              管理实体管理器

              EntityManager提供了实体存在的持久化上下文。持久化上下文还允许您管理事务。使用EntityManager API,您可以在实体上执行查询和写操作。实体管理器可以是 Web 容器管理的(在这种情况下,EntityManager的实例由容器注入),或者应用程序管理的。在本章中,我们将探讨应用程序管理的实体管理器。当学习 EJB 时,我们将在第七章“使用 EJB 创建 JEE 应用程序”中访问容器管理的实体管理器。实体管理器的持久化单元定义了数据库连接信息并将实体分组为持久化单元的一部分。它在名为persistence.xml的配置文件中定义,并期望在类路径中的META-INF中。

              EntityManager有自己的持久化上下文,这是一个实体缓存。实体的更新首先在缓存中完成,然后在事务提交或数据被显式推送到数据库时推送到数据库。

              当应用程序管理EntityManager时,建议为持久化单元只有一个EntityManager实例。

              EntityManagerFactory

              EntityManagerFactory创建EntityManagerEntityManagerFactory本身是通过调用静态的Persistence.createEntityManagerFactory方法获得的。这个函数的参数是一个在persistence.xml中指定的persistence-unit名称。

              创建 JPA 应用程序

              创建 JPA 应用程序的典型步骤如下:

              1. 创建数据库模式(表和关系)。可选地,您可以从 JPA 实体创建表和关系。我们将看到这个示例。然而,应该在这里提到,虽然从 JPA 实体创建表对于开发来说是可以的,但在生产环境中并不推荐这样做;这样做可能会导致非优化的数据库模型。

              2. 创建persistence.xml并指定数据库配置。

              3. 创建实体和关系。

              4. 通过调用Persistence.createEntityManagerFactory来获取EntityManagerFactory的实例。

              5. EntityManagerFactory创建EntityManager的实例。

              6. 如果您在实体上执行insertupdate操作,请在EntityManager上启动事务。

              7. 对实体执行操作。

              8. 提交事务。

              这里有一个示例片段:

              EntityManagerFactory factory = 
               Persistance.Persistence.createEntityManagerFactory("course_management") 
              EntityManager entityManager = factory.createEntityManager(); 
              EntityTransaction txn = entityManager.getTransaction(); 
              txn.begin(); 
              entityManager. persist(course); 
              txn.commit(); 
              

              您可以在www.eclipse.org/eclipselink/documentation/2.7/jpa/extensions/annotations_ref.htm找到 JPA 注解的描述。

              在 Eclipse EE 中的 JPA 工具使得添加许多注解变得非常容易,正如我们将在本节中看到的。

              创建新的 MySQL 模式

              对于此示例,我们将创建一个单独的 MySQL 模式(我们不会使用为 JDBC 应用程序创建的相同模式,尽管这样做是可能的)。打开 MySQL Workbench 并连接到您的 MySQL 数据库(如果您不知道如何从 MySQL Workbench 连接到 MySQL 数据库,请参阅第一章介绍 JEE 和 Eclipse)。

              在模式窗口中右键单击并选择创建模式...:

              图 4.19:创建新的 MySQL 模式

              将新模式命名为 course_management_jpa 并点击应用。我们将使用此模式进行 JPA 应用程序。

              设置 JPA 的 Maven 依赖项

              在此示例中,我们将使用 EclipseLink (eclipse.org/eclipselink/) JPA 实现。我们将使用 MySQL JDBC 驱动程序和 Bean 验证框架来验证实体的成员。最后,我们将使用由 JSR0250 提供的 Java 注解。因此,让我们为所有这些添加 Maven 依赖项:

                    <dependency> 
                      <groupId>org.eclipse.persistence</groupId> 
                      <artifactId>eclipselink</artifactId> 
                      <version>2.5.2</version> 
                    </dependency> 
                    <dependency> 
                      <groupId>mysql</groupId> 
                      <artifactId>mysql-connector-java</artifactId> 
                      <version>5.1.34</version> 
                    </dependency> 
                    <dependency> 
                      <groupId>javax.validation</groupId> 
                      <artifactId>validation-api</artifactId> 
                      <version>1.1.0.Final</version> 
                    </dependency> 
                    <dependency> 
                      <groupId>javax.annotation</groupId> 
                      <artifactId>jsr250-api</artifactId> 
                      <version>1.0</version> 
                    </dependency> 
              

              将项目转换为 JPA 项目

              许多 JPA 工具仅在项目是 JPA 项目时才在 Eclipse JEE 中激活。尽管我们创建了一个 Maven 项目,但很容易向其中添加 Eclipse JPA 特性:

              1. 右键单击项目并选择配置 | 转换为 JPA 项目!

              图 4.20:向项目添加 JPA 特性

              1. 确保已选择 JPA。

              2. 在下一页上,选择 EclipseLink 2.5.x 作为平台。

              3. 对于 JPA 实现类型,选择禁用库配置。

              4. 连接下拉列表列出了在数据源资源管理器中配置的所有连接。目前,请不要选择任何连接。在页面底部,选择自动发现注解类选项!

              图 4.21:配置 JPA 特性

              1. 点击完成。

              2. 注意,JPA 内容组是在项目下创建的,persistence.xml 也创建在其中。在编辑器中打开 persistence.xml

              3. 点击“连接”选项卡并将事务类型更改为资源本地。我们选择资源本地是因为,在本章中,我们将管理 EntityManager。如果您想让 JEE 容器管理 EntityManager,则应将事务类型设置为 JTA。我们将在第七章创建 JEE 应用程序与 EJB 中看到一个 JTA 事务类型的示例。

              4. 按照以下截图输入 EclipseLink 连接池属性并保存文件!

              图 4.22:设置持久化单元连接

              1. 接下来,点击“模式生成”选项卡。在这里,我们将设置从实体生成数据库表和关系的选项。选择以下截图所示的选项!

              图 4.23:设置持久化单元的模式生成选项

              在设置前面的选项后,以下是 persistence.xml 文件的内容:

              <?xml version="1.0" encoding="UTF-8"?> 
              <persistence version="2.1"   xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> 
                <persistence-unit name="CourseManagementJPA" transaction-type="RESOURCE_LOCAL"> 
                  <properties> 
                    <property name="javax.persistence.jdbc.driver" 
                     value="com.mysql.jdbc.Driver"/>      <property name="javax.persistence.jdbc.url" 
                     value="jdbc:mysql://localhost/course_management_jpa"/> 
                    <property name="javax.persistence.jdbc.user" value="root"/> 
                    <property name="javax.persistence.schema- 
                     generation.database.action" value="create"/>      <property name="javax.persistence.schema- 
                     generation.scripts.action" value="create"/> 
              <property name="eclipselink.ddl-generation" value="create- tables"/> 
                    <property name="eclipselink.ddl-generation.output-mode" value="both"/> 
                  </properties> 
                </persistence-unit> 
              </persistence> 
              

              创建实体

              我们已经为CoursePersonStudentTeacher创建了 JavaBeans。现在,我们将使用@Entity注解将它们转换为 JPA 实体。打开Course.java并添加以下注解:

              @ManagedBean (name="course") 
              @RequestScoped 
              @Entity 
              public class Course implements Serializable 
              

              同一个 Bean 可以同时作为 JSF 的管理 Bean 和 JPA 的实体。请注意,如果类的名称与数据库中的表名不同,你需要指定@Entity注解的name属性。例如,如果我们的Course表被命名为SchoolCourse,那么实体声明将如下所示:

              @Entity(name="SchoolCourse") 
              

              要指定Entity的主键,使用@Id注解。在Course表中,id是主键,并且是自动生成的。为了指示值的自动生成,使用@GeneratedValue注解。使用@Column注解来指示成员变量对应于表中的列。因此,id的注解如下所示:

              @Id 
              @GeneratedValue(strategy=GenerationType.IDENTITY) 
              @Column(name="id") 
              private int id; 
              

              你可以使用前面提到的 Bean Validation 框架注解为列指定验证。例如,课程名称不应为空:

              @NotNull 
              @Column(name="name") 
              private String name; 
              

              此外,学分的最低值应为1

              @Min(1) 
              @Column(name="credits") 
              private int credits;
              

              在前面的示例中,如果字段名与列名相同,则不需要@Column注解来指定列名。

              如果你使用 JPA 实体来创建表并希望精确指定列的类型,那么你可以使用@Column注解的columnDefinition属性;例如,为了指定类型为varchar且长度为20的列,你可以使用@Column(columnDefinition="VARCHAR(20)")

              请参考javaee.github.io/javaee-spec/javadocs/javax/persistence/Column.html以查看@Column注解的所有属性。

              我们将在需要时向Course Entity添加更多注解。现在,让我们将注意力转向Person类。这个类是StudentTeacher类的父类。然而,在数据库中,没有Person表,PersonStudent的所有字段都在Student表中;对于Teacher表也是如此。那么,我们如何在 JPA 中建模这种情况?JPA 支持实体的继承,并提供控制它们如何映射到数据库表的方式。打开Person类并添加以下注解:

              @Entity 
              @Inheritance(strategy=TABLE_PER_CLASS) 
              public abstract class Person implements Serializable { ... 
              

              我们不仅将Person类标识为Entity,而且还表明它用于继承(使用@Inheritance)。继承策略决定了如何将表映射到类。有三种可能的策略:

              • SINGLE_TABLE:在这种情况下,父类和子类的字段将被映射到父类的表中。如果我们使用这种策略,那么PersonStudentTeacher的字段将被映射到Person实体对应的表中。

              • TABLE_PER_CLASS:在这种情况下,每个具体类(非抽象类)映射到数据库中的一个表。父类的所有字段也映射到子类的表中。例如,PersonStudent的所有字段都将映射到Student表的列中。由于Person被标记为抽象的,Person类不会映射任何表。它仅存在于为应用程序提供继承支持。

              • JOINED:在这种情况下,父级及其子级映射到单独的表。例如,Person将映射到Person表,而StudentTeacher将映射到数据库中的相应表。

              根据我们为 JDBC 应用程序创建的架构,我们有StudentTeacher表,包含所有必要的列,并且没有Person表。因此,我们在这里选择了TABLE_PER_CLASS策略。

              在 JPA 中了解更多关于实体继承的信息,请访问javaee.github.io/tutorial/persistence-intro003.html#BNBQN

              Person表中的idfirstNamelastName字段被StudentTeacher共享。因此,我们需要将它们标记为表中的列,并设置主键。所以,向Person类中的字段添加以下注解:

                @Id 
                @GeneratedValue(strategy=GenerationType.IDENTITY) 
                @Column(name="id") 
                private int id; 
              
                @Column(name = "first_name") 
                @NotNull 
                private String firstName; 
              
                @Column(name = "last_name") 
                private String lastName; 
              

              在这里,表中的列名与类字段不匹配。因此,我们必须在@Column注解中指定名称属性。

              现在将Student类标记为Entity

              @Entity 
              @ManagedBean (name="student") 
              @RequestScoped 
              public class Student extends Person implements Serializable
              

              Student类有一个名为enrolledSinceDate字段,其类型为java.util.Date。然而,JDBC 和 JPA 使用的是java.sql.Date类型。如果您希望 JPA 自动将java.sql.Date转换为java.util.Date,则需要使用@Temporal注解标记该字段:

              @Temporal(DATE) 
              @Column(name="enrolled_since") 
              private Date enrolledSince; 
              

              打开Teacher类,并向其添加@Entity注解:

              @Entity 
              @ManagedBean (name="teacher") 
              @RequestScoped 
              public class Teacher extends Person implements Serializable 
              

              然后,映射类中的designation字段:

              @NotNull 
              @Column(name="designation") 
              private String designation; 
              

              我们现在已为所有不参与表关系的表和字段添加了注解。接下来,我们将建模我们类中表之间的关系。

              配置实体关系

              首先,我们将建模CourseTeacher之间的关系。它们之间存在一对一的关系:一位教师可以教授多门课程。在编辑器中打开Course.java。在 Eclipse JEE 中打开 JPA 视角(窗口 | 打开视角 | JPA 菜单)。

              配置多对一关系

              在编辑器中打开Course.java文件,点击下窗口中的 JPA Details 标签页(位于编辑器窗口下方)。在Course.java中,点击teacher成员变量。JPA Details 标签页会显示这个属性的详细信息:

              图 4.24:实体属性的 JPA 详情

              目标实体自动选择(作为Teacher),因为我们已将Teacher标记为实体,且teacher字段的类型为Teacher

              然而,Eclipse 假设CourseTeacher之间存在一对一关系,这是不正确的。CourseTeacher之间存在多对一关系。要更改此设置,请点击 JPA 详情视图顶部的(一对一)超链接,并在映射类型选择对话框中选择多对一。

              仅选择合并和刷新级联选项;否则,对于你为课程选择的每个TeacherTeacher表将添加重复条目。

              有关实体关系和级联选项的更多详细信息,请参阅javaee.github.io/tutorial/persistence-intro002.html#BNBQH

              当你选择合并和刷新级联选项时,添加到注解中的cascade属性将被添加到Course实体的teacher字段中:

                @ManyToOne(cascade = { MERGE, REFRESH }) 
                private Teacher teacher; 
              

              滚动到 JPA 详情页面以查看连接策略。这决定了CourseTeacher表中的列如何连接:

              图片

              图 4.25:在实体关系图中编辑连接策略

              注意,默认的连接策略是Course表中的teacher_id列映射到Teacher表中的id列。Eclipse 刚刚猜测了teacher_id(添加到Course实体中teacher字段的附加id),但如果我们在Course表中有一个不同的连接列,例如teacherId,那么我们就需要覆盖默认的连接列。点击覆盖默认选项复选框,然后点击文本框右侧的编辑按钮:

              图片

              图 4.26:编辑连接列

              在我们的案例中,默认选项与表列匹配,因此我们将保持不变。当你选择覆盖默认选项复选框时,@JoinColumn注解将被添加到Course实体的teacher字段中:

                @JoinColumn(name = "teacher_id", referencedColumnName = "id") 
                @ManyToOne(cascade = { MERGE, REFRESH }) 
                private Teacher teacher; 
              

              现已为teacher字段添加所有必要的注释。

              配置多对多关系

              现在我们将配置CourseStudent实体以实现多对多关系(一门课程可以有多个学员,一个学员可以选修多门课程)。

              多对多关系可以是单向的或双向的。例如,你可能只想跟踪注册课程的学员(因此Course实体将有一个学员列表),而不跟踪选课的学员(Student实体不会保留课程列表)。这是一个单向关系,其中只有Course实体知道学员,但Student实体不知道课程)。

              在双向关系中,每个实体都知道另一个实体。因此,Course实体将保留学员列表,而Student实体将保留课程列表。我们将在此示例中配置双向关系。

              多对多关系也有一个拥有方和一个反向方。您可以在关系中的任何实体上标记为拥有实体。从配置的角度来看,反向方通过mappedBy属性标记为@ManyToMany注解。

              在我们的应用程序中,我们将Student作为关系的拥有方,将Course作为反向方。数据库中的多对多关系需要一个连接表,该表通过@JoinTable注解在拥有实体中进行配置。

              我们首先将在Course实体中配置一个多对多关系。在Course中添加一个成员变量以保存Student实体列表,并添加其 getter 和 setter:

              private List<Student> students; 
              public List<Student> getStudents() { 
                return students; 
              } 
              public void setStudents(List<Student> students) { 
                this.students = students; 
              } 
              

              然后,点击之前添加的students字段,并注意 JPA Details 视图中的设置:

              图片 3

              图 4.27:Course 实体中 students 字段的默认 JPA 详情

              因为students字段是一个Student实体的列表,Eclipse 已经假设了一个一对一的关系(参见 JPA Details 视图顶部的链接)。我们需要更改这一点。点击one_to_many链接并选择多对多。

              检查合并和刷新级联选项。由于我们在关系的反向侧放置了一个Course实体,请选择通过映射作为连接策略。在属性文本字段中输入courses。编译器将显示错误,因为我们还没有在Student实体中添加courses字段。我们将很快修复这个问题。students字段的 JPA 设置应如图所示:

              图片 2

              图 4.28:Course 实体中 students 字段的修改后的 JPA 设置

              Course实体中students字段的注解应如下所示:

              @ManyToMany(cascade = { MERGE, REFRESH }, mappedBy = "courses") 
              private List<Student> students; 
              

              在编辑器中打开Student.java。添加courses字段及其 getter 和 setter。在文件中点击courses字段,并在 JPA Details 视图中将关系从一对一更改为多对多(如之前对Course实体中的students字段所描述)。选择合并和刷新级联选项。在连接策略部分,确保选择了连接表选项。Eclipse 通过连接拥有表和反向表,并用下划线分隔(在这种情况下为Student_Course)创建默认的连接表。将其更改为Course_Student以使其与为 JDBC 应用程序创建的模式保持一致。

              在连接列部分,选择覆盖默认选项。Eclipse 已将连接列命名为students_id->id,但在我们在 JDBC 应用程序中创建的Course_Student表中,我们有一个名为student_id的列。因此,点击编辑按钮并将名称更改为student_id

              同样,将反向连接列从courses_id->id更改为course_id->id。在这些更改之后,courses字段的 JPA Details 应如图所示:

              图片 1

              图 4.29:学生实体中 courses 字段的 JPA 详情

              之前的设置为courses字段创建了以下注解:

              @ManyToMany(cascade = { MERGE, REFRESH }) 
              @JoinTable(name = "Course_Student", joinColumns = @JoinColumn(name = "student_id", referencedColumnName = "id"), inverseJoinColumns = 
               @JoinColumn(name = "course_id", referencedColumnName = "id")) 
              
              List<Course> courses; 
              

              我们已经设置了应用程序所需的所有实体关系。下载本章附带的代码,以查看CourseStudentTeacher实体的完整源代码。

              我们需要在persistence.xml中添加我们之前创建的实体。打开文件,确保“常规”选项卡已打开。在“管理类”会话中,点击“添加”按钮。输入要添加的实体名称(例如,Student),并从列表中选择类。添加我们创建的所有四个实体:

              img/00122.jpeg

              图 4.30:在 persistence.xml 中添加实体

              从实体创建数据库表

              按照以下步骤从我们已建模的实体和关系创建数据库表:

              1. 右键单击项目,选择 JPA 工具 | 从实体生成表:img/00123.jpeg

              图 4.31:学生实体中 courses 字段的 JPA 详情

              1. 由于我们没有为我们的 JPA 项目配置任何模式,因此“模式”下拉菜单将为空。点击“添加 JPA 项目连接”链接:img/00124.jpeg

              图 4.32:JPA 项目属性

              1. 点击“添加连接”链接,创建到我们之前创建的course_management_jpa模式的连接。我们已经在本章的“使用 Eclipse 数据源资源管理器”部分中看到了如何创建到 MySQL 模式的连接。

              2. 在图 4.31 所示的下拉列表中选择course_management_jpa,然后点击下一步:img/00125.jpeg

              图 4.33:从实体生成模式

              1. 点击完成。

              Eclipse 生成创建表和关系的 DDL 脚本,并在所选模式中执行这些脚本。一旦脚本成功运行,打开数据源资源管理器视图(参见本章的“使用 Eclipse 数据源资源管理器”部分),在course_management_jpa连接中浏览表。确保表和字段是根据我们创建的实体创建的:

              img/00126.jpeg

              图 4.34:从 JPA 实体创建的表

              Eclipse 和 JPA 的这个特性使得在修改实体时更新数据库变得非常容易。

              使用 JPA API 管理数据

              我们现在将创建使用 JPA API 来管理课程管理应用程序数据的类。我们将为CourseTeacherStudent实体创建服务类,并添加直接通过 JPA API 访问数据库的方法。

              如同在 JPA 概念 部分所述,将 EntityManagerFactory 的实例缓存到我们的应用程序中是一个好的实践。此外,JSF 的托管 Bean 作为 UI 和后端代码之间的链接,以及 UI 和数据访问对象之间传输数据的通道。因此,它们必须有一个数据访问对象(使用 JPA 从数据库访问数据)的实例。为了缓存 EntityManagerFactory 的实例,我们将创建另一个托管 Bean,其唯一的工作是使 EntityManagerFactory 实例对其他托管 Bean 可用。

              packt.book.jee.eclipse.ch4.jpa.service_bean 包中创建一个 EntityManagerFactoryBean 类。此包包含所有托管 Bean。EntityManagerFactoryBean 在构造函数中创建一个 EntityManagerFactory 实例并提供一个获取器方法:

              package packt.book.jee.eclipse.ch4.jpa.service_bean; 
              
              import javax.faces.bean.ApplicationScoped; 
              import javax.faces.bean.ManagedBean; 
              import javax.persistence.EntityManagerFactory; 
              import javax.persistence.Persistence; 
              
              //Load this bean eagerly, i.e., before any request is made 
              @ManagedBean(name="emFactoryBean", eager=true) 
              @ApplicationScoped 
              public class EntityManagerFactoryBean { 
              
                private EntityManagerFactory entityManagerFactory; 
              
                public EntityManagerFactoryBean() { 
                  entityManagerFactory = 
               Persistence.createEntityManagerFactory("CourseManagementJPA"); 
                } 
              
                public EntityManagerFactory getEntityManagerFactory() { 
                  return entityManagerFactory; 
                } 
              
              } 
              

              注意以下传递的参数:

              entityManagerFactory = 
               Persistence.createEntityManagerFactory("CourseManagementJPA");
              

              这是 persistence.xml 中持久化单元的名称。

              现在让我们创建实际使用 JPA API 访问数据库表的服务类。

              创建一个名为 packt.book.jee.eclipse.ch4.jpa.service 的包。创建一个名为 CourseService 的类。每个服务类都需要访问 EntityManagerFactory。因此,创建一个如下所示的私有成员变量:

              private EntityManagerFactory factory; 
              

              构造函数接受一个 EntityManagerFactoryBean 实例,并从中获取 EntityManagerFactory 的引用:

              public CourseService(EntityManagerFactoryBean factoryBean) { 
                this.factory = factoryBean.getEntityManagerFactory(); 
              } 
              

              现在让我们添加一个从数据库获取所有 courses 的函数:

              public List<Course> getCourses() { 
                EntityManager em = factory.createEntityManager(); 
                CriteriaBuilder cb = em.getCriteriaBuilder(); 
                CriteriaQuery<Course> cq = cb.createQuery(Course.class); 
                TypedQuery<Course> tq = em.createQuery(cq); 
                List<Course> courses = tq.getResultList(); 
                em.close(); 
                return courses; 
              } 
              

              注意 CriteriaBuilderCriteriaQueryTypesQuery 如何用于获取所有课程。这是一种类型安全的查询执行方式。

              有关如何使用 JPA 条件 API 的详细讨论,请参阅 javaee.github.io/tutorial/persistence-criteria.html#GJITV

              我们可以使用 Java Persistence Query Language (JQL)——www.oracle.com/technetwork/articles/vasiliev-jpql-087123.html——来完成相同的事情,但它不是类型安全的。然而,这里有一个使用 JQL 编写 getCourses 函数的示例:

              public List<Course> getCourses() { 
                EntityManager em = factory.createEntityManager(); 
                List<Course> courses = em.createQuery("select crs from Course crs").getResultList(); 
                em.close(); 
                return courses; 
              } 
              

              添加一个将课程插入数据库的方法:

              public void addCourse (Course course) { 
                EntityManager em = factory.createEntityManager(); 
                EntityTransaction txn = em.getTransaction(); 
                txn.begin(); 
                em.persist(course); 
                txn.commit(); 
              } 
              

              代码相当简单。我们获取实体管理器并开始一个事务,因为这是一个 update 操作。然后,我们通过传递一个 Course 实例来调用 EntityManager 上的 persist 方法以保存。然后,我们提交事务。更新和删除的方法也很简单。以下是 CourseService 的整个源代码:

              package packt.book.jee.eclipse.ch4.jpa.service; 
              
              // imports skipped
              
              import packt.book.jee.eclipse.ch4.jpa.bean.Course; 
              import packt.book.jee.eclipse.ch4.jpa.service_bean.EntityManagerFactoryBean; 
              
              public class CourseService { 
                private EntityManagerFactory factory; 
              
                public CourseService(EntityManagerFactoryBean factoryBean) { 
                  factory = factoryBean.getEntityManagerFactory(); 
                } 
              
                public List<Course> getCourses() { 
                  EntityManager em = factory.createEntityManager(); 
                  CriteriaBuilder cb = em.getCriteriaBuilder(); 
                  CriteriaQuery<Course> cq = cb.createQuery(Course.class); 
                  TypedQuery<Course> tq = em.createQuery(cq); 
                  List<Course> courses = tq.getResultList(); 
                  em.close(); 
                  return courses; 
                } 
              
              public void addCourse (Course course) { 
                  EntityManager em = factory.createEntityManager(); 
                  EntityTransaction txn = em.getTransaction(); 
                  txn.begin(); 
                  em.persist(course); 
                  txn.commit(); 
              } 
              
                public void updateCourse (Course course) { 
                  EntityManager em = factory.createEntityManager(); 
                  EntityTransaction txn = em.getTransaction(); 
                  txn.begin(); 
                  em.merge(course); 
                  txn.commit(); 
                } 
              
                public Course getCourse (int id) { 
                  EntityManager em = factory.createEntityManager(); 
                  return em.find(Course.class, id); 
                } 
              
                public void deleteCourse (Course course) { 
                  EntityManager em = factory.createEntityManager(); 
                  EntityTransaction txn = em.getTransaction(); 
                  txn.begin(); 
                  Course mergedCourse = em.find(Course.class, course.getId()); 
                  em.remove(mergedCourse); 
                  txn.commit(); 
                } 
              } 
              

              现在让我们创建具有以下方法的 StudentServiceTeacherService 类:

              public class StudentService { 
                private EntityManagerFactory factory; 
              
                public StudentService (EntityManagerFactoryBean factoryBean) { 
                  factory = factoryBean.getEntityManagerFactory(); 
                } 
              
                public void addStudent (Student student) { 
                  EntityManager em = factory.createEntityManager(); 
                  EntityTransaction txn = em.getTransaction(); 
                  txn.begin(); 
                  em.persist(student); 
                  txn.commit(); 
                } 
              
                public List<Student> getStudents() { 
                  EntityManager em = factory.createEntityManager(); 
                  CriteriaBuilder cb = em.getCriteriaBuilder(); 
                  CriteriaQuery<Student> cq = cb.createQuery(Student.class); 
                  TypedQuery<Student> tq = em.createQuery(cq); 
                  List<Student> students = tq.getResultList(); 
                  em.close(); 
                  return students; 
                } 
              
              } 
              
              public class TeacherService { 
                private EntityManagerFactory factory; 
              
                public TeacherService (EntityManagerFactoryBean factoryBean) { 
                  factory = factoryBean.getEntityManagerFactory(); 
                } 
              
                public void addTeacher (Teacher teacher) { 
                  EntityManager em = factory.createEntityManager(); 
                  EntityTransaction txn = em.getTransaction(); 
                  txn.begin(); 
                  em.persist(teacher); 
                  txn.commit(); 
                } 
              
                public List<Teacher> getTeacher() { 
                  EntityManager em = factory.createEntityManager(); 
                  CriteriaBuilder cb = em.getCriteriaBuilder(); 
                  CriteriaQuery<Teacher> cq = cb.createQuery(Teacher.class); 
                  TypedQuery<Teacher> tq = em.createQuery(cq); 
                  List<Teacher> teachers = tq.getResultList(); 
                  em.close(); 
                  return teachers; 
                } 
              
                public Teacher getTeacher (int id) { 
                  EntityManager em = factory.createEntityManager(); 
                  return em.find(Teacher.class, id); 
                } 
              } 
              

              将用户界面与 JPA 服务类连接

              现在我们已经准备好了所有数据访问类,我们需要将我们为添加课程创建的用户界面 addCourse.xhtml 连接到 JPA 服务类,以便传递数据和从 JPA 服务类获取数据。如前所述,我们将使用托管 Bean 来完成这项工作,在这种情况下,CourseServiceBean

              CourseServiceBean需要创建CourseService的一个实例并调用addCourse方法。打开CourseServiceBean并创建如下成员变量:

              private CourseService courseService ; 
              

              我们还需要我们之前创建的EntityManagerFactoryBean托管 Bean 的一个实例:

              @ManagedProperty(value="#{emFactoryBean}") 
              private EntityManagerFactoryBean factoryBean; 
              

              factoryBean实例由 JSF 运行时注入,并且只有在托管 Bean 完全构建之后才可用。然而,为了使此 Bean 能够注入,我们需要提供一个 setter 方法。因此,为factoryBean添加一个 setter 方法。我们可以通过在方法上注解@PostConstruct来让 JSF 在 Bean 完全构建后调用我们的 Bean 的方法。所以,让我们创建一个名为postConstruct的方法:

              @PostConstruct 
              public void init() { 
                courseService = new CourseService(factoryBean); 
              } 
              

              然后,修改addCourse方法以调用我们的服务方法:

              public String addCourse() { 
                courseService.addCourse(course); 
                return "listCourse"; 
              } 
              

              由于listCourse.xhtml页面需要获取课程列表,因此我们也在CourseServiceBean中添加了getCourses方法:

              public List<Course> getCourses() { 
                return courseService.getCourses(); 
              } 
              

              在前面的更改之后,这是CourseServiceBean的代码:

              @ManagedBean(name="courseServiceBean") 
              @RequestScoped 
              public class CourseServiceBean { 
              
                private CourseService courseService ; 
              
                @ManagedProperty(value="#{emFactoryBean}") 
                private EntityManagerFactoryBean factoryBean; 
              
                @ManagedProperty(value="#{course}") 
                private Course course; 
              
                private String errMsg= null; 
              
                @PostConstruct 
                public void init() { 
                  courseService = new CourseService(factoryBean); 
                } 
              
                public void setFactoryBean(EntityManagerFactoryBean factoryBean) 
                 { 
                  this.factoryBean = factoryBean; 
                } 
              
                public Course getCourse() { 
                  return course; 
                } 
              
                public void setCourse(Course course) { 
                  this.course = course; 
                } 
              
                public String getErrMsg() { 
                  return errMsg; 
                } 
              
                public void setErrMsg(String errMsg) { 
                  this.errMsg = errMsg; 
                } 
              
                public String addCourse() { 
                  courseService.addCourse(course); 
                  return "listCourse"; 
                } 
              
                public List<Course> getCourses() { 
                  return courseService.getCourses(); 
                } 
              
              } 
              

              最后,我们将编写在listCourse.xhtml中显示课程列表的代码:

              <html  
              
                  > 
              
                  <h2>Courses:</h2> 
                  <h:form> 
                    <h:messages style="color:red"/> 
                  <h:dataTable value="#{courseServiceBean.courses}" 
                   var="course"> 
                    <h:column> 
                      <f:facet name="header">ID</f:facet> 
                      <h:outputText value="#{course.id}"/> 
                    </h:column> 
                    <h:column> 
                      <f:facet name="header">Name</f:facet> 
                      <h:outputText value="#{course.name}"/> 
                    </h:column> 
                    <h:column> 
                      <f:facet name="header">Credits</f:facet> 
                      <h:outputText value="#{course.credits}" 
                       style="float:right" /> 
                    </h:column> 
                  </h:dataTable> 
                </h:form> 
              
                <h:panelGroup rendered="#{courseServiceBean.courses.size() == 
                 0}"> 
                  <h3>No courses found</h3> 
                </h:panelGroup> 
              
                <c:if test="#{courseServiceBean.courses.size() > 0}"> 
                  <b>Total number of courses 
                    <h:outputText value="#{courseServiceBean.courses.size()}"/> 
                  </b> 
                </c:if> 
                <p/> 
                <h:button value="Add" outcome="addCourse"/> 
              </html> 
              

              由于空间限制,我们不会讨论如何添加删除/更新课程的功能,或者如何创建带有Teacher字段选中的课程。请下载本章讨论的示例的源代码,以查看完成的项目。

              摘要

              在本章中,我们学习了如何构建需要从关系型数据库访问数据的 Web 应用程序。首先,我们使用 JDBC 和 JSTL 构建了一个简单的课程管理应用程序,然后,使用 JPA 和 JSF 构建了相同的应用程序。

              与 JDBC 相比,JPA 更受欢迎,因为最终你写的代码要少得多。映射对象数据到关系型数据的代码由 JPA 实现为你创建。然而,JDBC 仍然被许多 Web 应用程序使用,因为它更简单易用。尽管 JPA 的学习曲线适中,但 Eclipse EE 中的 JPA 工具可以使使用 JPA API 变得更容易一些,尤其是在配置实体、关系和persistence.xml方面。

              在下一章中,我们将稍微偏离我们对 JEE 的讨论,看看如何为 Java 应用程序编写和运行单元测试。我们还将了解如何在运行单元测试后测量代码覆盖率。

第五章:单元测试

在上一章中,我们学习了如何创建使用数据库的 Web 应用程序。在本章中,我们将学习如何在 Eclipse 中为 JEE 应用程序编写和执行单元测试。本章将涵盖以下主题:

  • 使用 Eclipse JEE 创建和执行单元测试

  • 从 Eclipse IDE 执行单元测试

  • 为单元测试模拟外部依赖

  • 计算单元测试覆盖率

测试你开发的软件是整个软件开发周期中非常重要的一个部分。测试的类型有很多;每一种都有其特定的目的,并且范围各不相同。测试的例子包括功能测试、集成测试、场景测试和单元测试。

在所有这些类型中,单元测试的范围最窄,通常由开发者编码和执行。每个单元测试旨在测试特定的、小块的功能(通常是一个类中的方法),并且预期在没有外部依赖的情况下执行。以下是你应该编写高效单元测试的一些原因:

  • 为了尽早捕捉到错误。如果在功能或集成测试中发现错误,这些测试的范围要广泛得多,那么可能很难隔离导致错误的代码。在单元测试中捕捉和修复错误要容易得多,因为单元测试,按照定义,在一个更窄的范围内工作,如果测试失败,你将确切知道去哪里修复问题。

  • 单元测试可以帮助你捕捉到在编辑代码时可能引入的任何回归。有很好的工具和库可用于自动化单元测试的执行。例如,使用构建工具如 Ant 和 Maven,你可以在构建成功后执行单元测试,这样你就可以立即发现你所做的更改是否破坏了之前正常工作的代码。

如前所述,编写单元测试并执行它们通常是开发者的责任。因此,大多数 IDE 都提供了良好的内置支持来编写和执行单元测试。Eclipse JEE 也不例外。它内置了对 JUnit 的支持,JUnit 是一个流行的 Java 单元测试框架。

在本章中,我们将了解如何为我们在第四章中构建的课程管理网络应用程序编写和执行 JUnit 测试,创建 JEE 数据库应用程序。然而,首先,这里有一个 JUnit 的快速介绍。

介绍 JUnit

JUnit 测试类是与你要测试的类分开的 Java 类。每个测试类可以包含许多测试用例,这些测试用例只是标记为在执行 JUnit 测试时执行的方法。测试套件是一组测试类。

习惯上,将测试类命名为你想要测试的类的相同名称,并在该名称后附加 Test。例如,如果你想要测试上一章中的 Course 类,那么你会创建一个 JUnit 测试类,并将其命名为 CourseTest。测试用例(方法)名称以 test 开头,后跟你要测试的类中的方法名称;例如,如果你想要测试 Course 类中的 validate 方法,那么你会在 CourseTest 类中创建 testValidate 方法。测试类也创建在与要测试的类所在的包相同的包中。在 Maven 项目中,测试类通常位于 src/test/java 文件夹下。习惯上,在 test 文件夹中创建与 src/main/java 文件夹相同的包结构。

JUnit 支持使用注解标记单元测试和测试套件。以下是对 Course 类的简单测试用例:

/** 
* Test for {@link Course} 
*/ 
Class CourseTest { 
  @Test 
  public void testValidate() { 
    Course course = new Course(); 
    Assert.assertFalse(course.validate()); 
    course.setName("course1") 
    Assert.assetFalse(course.validate()); 
    Course.setCredits(-5); 
    Assert.assetFalse(course.validate()); 
    course.setCredits(5); 
    Assert.assertTrue(course.validate()); 
  } 
} 

假设 validate 方法检查课程 name 是否为非空,以及 credits 是否大于零。

前面的测试用例被标记为 @Test 注解。它创建了一个 Course 类的实例,然后调用 Assert.assertFalse 方法来确保 validate 方法返回 false,因为 namecredits 没有设置,它们将具有默认值,分别是 null0Assert 是由 JUnit 库提供的一个类,它有许多断言方法来测试多种条件(更多信息请参见 junit.sourceforge.net/javadoc/org/junit/Assert.html)。

然后,测试用例仅设置名称,并再次执行相同的验证,期望 validate 方法返回 false,因为学分仍然是零。最后,测试用例设置名称和学分,并调用 Assert.assertTrue 方法来确保 course.validate() 返回 true。如果任何断言失败,则测试用例失败。

除了 @Test,你还可以使用 JUnit 提供的以下注解:

  • @Before@After:带有这些注解的方法在每个测试前后执行。你可能想在 @Before 中初始化资源,并在 @After 中释放它们。

  • @BeforeClass@AfterClass:与 @Before@After 类似,但它们不是针对每个测试调用,而是在每个测试类中只调用一次。带有 @BeforeClass 注解的方法在该类中的任何测试用例执行之前被调用,而带有 @AfterClass 注解的方法在所有测试用例执行之后被调用。

你可以在 junit.org/junit4/javadoc/latest/org/junit/package-summary.html 找到更多关于 JUnit 的注解。

使用 Eclipse JEE 创建和执行单元测试

要了解如何编写单元测试,让我们以我们在第四章,“创建 JEE 数据库应用程序”中开发的课程管理应用程序的 JDBC 版本为例。让我们从一个简单的测试用例开始,用于验证课程。以下是Course.java的源代码:

package packt.book.jee.eclipse.ch5.bean; 

import java.sql.SQLException; 
import java.util.List; 

import packt.book.jee.eclipse.ch5.dao.CourseDAO; 

public class Course { 
  private int id; 
  private String name; 
  private int credits; 
  private Teacher teacher; 
  private int teacherId; 
  private CourseDAO courseDAO = new CourseDAO(); 

  public int getId() { 
    return id; 
  } 
  public void setId(int id) { 
    this.id = id; 
  } 
  public String getName() { 
    return name; 
  } 
  public void setName(String name) { 
    this.name = name; 
  } 
  public int getCredits() { 
    return credits; 
  } 
  public void setCredits(int credits) { 
    this.credits = credits; 
  } 
  public boolean isValidCourse() { 
    return name != null && credits != 0; 
  } 
  public Teacher getTeacher() { 
    return teacher; 
  } 
  public void setTeacher(Teacher teacher) { 
    this.teacher = teacher; 
  } 
  public void addCourse() throws SQLException { 
    courseDAO.addCourse(this); 
  } 
  public List<Course> getCourses() throws SQLException { 
    return courseDAO.getCourses(); 
  } 
  public int getTeacherId() { 
    return teacherId; 
  } 
  public void setTeacherId(int teacherId) { 
    this.teacherId = teacherId; 
  } 
} 

创建单元测试用例

Maven 项目遵循某些约定;Maven 项目的整个应用程序源代码位于src/main/java文件夹中,单元测试预期位于src/test/java文件夹中。实际上,当你使用 Eclipse 创建 Maven 项目时,它会为你创建src/test/java文件夹。我们将在该文件夹中创建我们的测试用例。我们将为测试类创建与应用程序源相同的包结构;也就是说,为了测试packt.book.jee.eclipse.ch5.bean.Course类,我们将在src/test/java文件夹下创建packt.book.jee.eclipse.ch5.bean包,然后创建一个名为CourseTest的 JUnit 测试类,如下所示:

  1. 在 Eclipse 的包资源管理器中,右键单击src/test/java文件夹,选择新建 | JUnit 测试用例(如果你在菜单中找不到此选项,请选择新建 | 其他,并在过滤器文本框中输入junit。然后,选择 JUnit 测试用例选项)。

  2. 输入包名为packt.book.jee.eclipse.ch5.bean,类名为CourseTest

  3. 点击“测试用例下的类”旁边的浏览...按钮。在过滤器文本框中输入course,并选择Course类:图片 3

    图 5.1:JUnit 测试用例向导

    1. 点击下一步。页面显示我们想要为它们创建测试用例的类(Course)中的方法。选择你想要为它们创建测试用例的方法。

    2. 我们不想测试 getter 和 setter,因为它们是简单的方法,除了获取或设置成员变量之外,没有做太多其他的事情。目前,我们将只创建一个方法的测试用例:isValidTestCase。选择此方法的复选框:图片 2

    图 5.2:选择测试用例的方法

    1. 点击完成。Eclipse 会检查 JUnit 库是否包含在你的项目中,如果没有,会提示你包含它们:图片 1

      图 5.3:在项目中包含 JUnit 库

      1. 点击确定。Eclipse 创建包和测试类,包含一个名为testIsValidCourse的方法/测试用例。请注意,该方法使用@Test注解,表示它是一个 JUnit 测试用例。

      我们如何测试isValidCourse是否按预期工作?我们创建一个Course类的实例,设置一些我们知道是有效/无效的值,调用isValidateCourse方法,并将结果与预期结果进行比较。JUnit 在Assert类中提供了许多方法,用于比较通过调用测试方法获得的实际结果与预期结果。因此,让我们将测试代码添加到testIsValidCourse方法中:

      package packt.book.jee.eclipse.ch5.bean; 
      import org.junit.Assert; 
      import org.junit.Test; 
      public class CourseTest { 
      
        @Test 
        public void testIsValidCourse() { 
          Course course = new Course(); 
          //First validate without any values set 
          Assert.assertFalse(course.isValidCourse()); 
          //set  name 
          course.setName("course1"); 
          Assert.assertFalse(course.isValidCourse()); 
          //set zero credits 
          course.setCredits(0); 
          Assert.assertFalse(course.isValidCourse()); 
          //now set valid credits 
          course.setCredits(4); 
          Assert.assertTrue(course.isValidCourse()); 
        } 
      
      } 
      

      我们首先创建 Course 类的一个实例,并且不设置其任何值,然后调用 isValidCourse 方法。我们知道它不是一个有效课程,因为名称和学分是有效课程中必需的字段。因此,我们通过调用 Assert.assertFalse 方法来检查 isValidCourse 返回的值是否为 false。然后我们设置名称并再次检查,预期实例是一个无效课程。然后,我们为 Course 设置 0 学分值,最后,我们为 Course 设置 4 学分。现在,isValidCourse 预期返回 true,因为名称和学分都是有效的。我们通过调用 Assert.assertTrue 来验证这一点。

      运行单元测试用例

      让我们在 Eclipse 中运行这个测试用例。在文件上右键单击,或在包资源管理器中的项目中的任何位置,然后选择 Run As | JUnit Test 菜单。Eclipse 会找到项目中的所有单元测试,执行它们,并在 JUnit 视图中显示结果:

      图片

      图 5.4:JUnit 结果视图

      此视图显示了运行测试用例的摘要。在这种情况下,它已运行了一个测试用例,并且是成功的。绿色条表示所有测试用例都成功执行。

      现在,让我们在方法中添加一个额外的检查:

        @Test 
        public void testIsValidCourse() { 
          ... 
          //set empty course name 
          course.setName(""); 
          Assert.assertFalse(course.isValidCourse()); 
        } 
      

      然后,再次运行测试用例:

      图片

      图 5.5:显示失败测试的 JUnit 结果视图

      测试用例失败是因为当课程名称设置为空字符串时,course.isValidCourse() 返回了 true,而测试用例预期实例应该是一个无效课程。因此,我们需要修改 Course 类的 isValidCourse 方法以修复这个失败:

      public boolean isValidCourse() { 
        return name != null && credits != 0 && name.trim().length() > 0; 
      } 
      

      我们已经添加了检查 name 字段长度的条件。这应该可以修复测试用例失败。你可以再次运行测试用例以验证。

      使用 Maven 运行单元测试用例

      你也可以使用 Maven 运行单元测试。事实上,Maven 的 install 目标也会运行单元测试。然而,你可以只运行单元测试。为此,在包资源管理器中右键单击项目,然后选择 Run As | Maven test。

      你可能在控制台看到以下错误:

      java.lang.NoClassDefFoundError: org/junit/Assert 
        at packt.book.jee.eclipse.ch5.bean.CourseTest.testIsValidCourse 
      (CourseTest.java:10) 
      Caused by: java.lang.ClassNotFoundException: org.junit.Assert 
        at java.net.URLClassLoader$1.run(URLClassLoader.java:366) 
        at java.net.URLClassLoader$1.run(URLClassLoader.java:355) 
        at java.security.AccessController.doPrivileged(Native Method) 
      

      这个错误的原因是我们没有为我们的 Maven 项目添加 JUnit 依赖。在 pom.xml 中添加以下依赖项:

          <dependency> 
            <groupId>junit</groupId> 
            <artifactId>junit</artifactId> 
            <version>4.12</version> 
          </dependency> 
      

      请参考第二章 使用 Maven 进行项目管理 中的 创建简单的 JEE Web 应用程序 部分,了解如何向 Maven 项目添加依赖项。

      再次运行 Maven 测试;这次测试应该会通过。

      为单元测试模拟外部依赖

      单元测试旨在在没有任何外部依赖的情况下执行。我们当然可以编写细粒度的方法,使得核心业务逻辑方法与具有外部依赖的方法完全分离。然而,有时这并不实际,我们可能不得不为紧密依赖于访问外部系统的方法的代码编写单元测试。

      例如,假设我们必须在Course对象中添加一个方法来添加学生到课程中。我们还将强制规定课程对学生数量的上限,一旦达到这个上限,就不能再添加更多学生。让我们向我们的Course对象添加以下方法:

      public void addStudent (Student student) 
          throws EnrolmentFullException, SQLException { 
        //get current enrolement first 
        int currentEnrolment = courseDAO.getNumStudentsInCourse(id); 
        if (currentEnrolment >= getMaxStudents()) 
          throw new EnrolmentFullException("Course if full. Enrolment closed"); 
        courseDAO.enrolStudentInCourse(id, student.getId()); 
      } 
      

      addStudent方法首先找到课程当前的学生注册人数。为此,它使用CourseDAO类查询数据库。然后,它检查当前注册人数是否小于最大注册人数。然后,它调用CourseDAOenrollStudentInCourse方法。

      addStudent方法有一个外部依赖。它依赖于成功访问外部数据库。我们可以为这个函数编写以下单元测试:

      @Test 
      public void testAddStudent() { 
        //create course 
        Course course = new Course(); 
        course.setId(1); 
        course.setName("course1"); 
        course.setMaxStudents(2); 
        //create student 
        Student student = new Student(); 
        student.setFirstName("Student1"); 
        student.setId(1); 
        //now add student 
        try { 
          course.addStudent(student); 
        } catch (Exception e) { 
          Assert.fail(e.getMessage()); 
        } 
      } 
      

      testAddStudent方法旨在检查当所有外部依赖都满足时,addStudent方法是否运行正常;在这种情况下,这意味着数据库连接已建立,数据库服务器正在运行,并且表已正确配置。如果我们想通过考虑所有依赖项来验证在课程上注册学生的功能是否正常工作,那么我们应该编写一个功能测试。单元测试只需要检查不依赖于外部依赖的代码是否运行正常;在这种情况下,这是一个简单的检查,以验证总注册人数是否小于最大允许注册人数。这是一个简单的例子,但在实际应用中,你可能需要测试更多复杂的代码。

      之前单元测试的问题是我们可能会出现假失败,从单元测试的角度来看,因为数据库可能已关闭或可能没有正确配置。一个解决方案是模拟外部依赖;我们可以模拟对数据库的调用(在这种情况下,对CourseDAO的调用)。我们不需要对数据库进行实际调用,而是可以创建返回一些模拟数据或执行模拟操作的存根。例如,我们可以编写一个模拟函数,为CourseDAOgetNumStudentsInCourse方法返回一些硬编码的值。然而,我们不想修改应用程序源代码来添加模拟方法。幸运的是,有一些开源框架允许我们在单元测试中模拟依赖项。接下来,我们将看到如何使用名为 Mockito 的流行框架来模拟依赖项(mockito.org/)。

      使用 Mockito

      在非常高的层面上,我们可以使用 Mockito 做两件事:

      • 在应用程序类中提供依赖方法的包装器实现

      • 验证这些包装器实现是否被调用

      我们使用 Mockito 的静态方法指定包装器实现:

      Mockito.when(object_name.method_name(params)).thenReturn(return_value); 
      

      此外,我们通过调用 Mockito 的另一个静态方法来验证包装器方法是否被调用:

      Mockito.verify(object_name, Mockito.atLeastOnce()).method_name(params);
      

      要在我们的项目中使用 Mockito,我们需要在pom.xml中添加对其的依赖:

          <dependency> 
            <groupId>org.mockito</groupId> 
            <artifactId>mockito-core</artifactId> 
            <version>2.17.0</version> 
          </dependency> 
      

      在我们开始使用 Mockito 编写单元测试用例之前,我们将在Course类中进行一些小的修改。目前,Course类中的CourseDAO是私有的,并且没有为其提供 setter 方法。在Course类中添加 setter 方法(setCourseDAO):

      public void setCourseDAO(CourseDAO courseDAO) { 
        this.courseDAO = courseDAO; 
      } 
      

      现在,让我们使用 Mockito 重写我们的测试用例。

      首先,我们需要告诉 Mockito 我们想要模拟哪些方法调用以及模拟函数中应该采取什么操作(例如,返回特定值)。在我们的例子中,我们希望模拟从Course.addStudent方法中调用的CourseDAO中的方法,因为CourseDAO中的方法访问数据库,而我们希望我们的单元测试独立于数据访问代码。因此,我们使用 Mockito 创建了一个模拟的(包装)CourseDAO实例:

      CourseDAO courseDAO = Mockito.mock(CourseDAO.class); 
      

      然后,我们告诉 Mockito 在这个对象中要模拟哪些具体方法。我们想要模拟getNumStudentsInCoursegetNumStudentsInCourse如下:

      try { 
      Mockito.when(courseDAO.getNumStudentsInCourse(1)).thenReturn(60); 
      Mockito.doNothing().when(courseDAO).enrollStudentInCourse(1, 1); 
      } catch (SQLException e) { 
        Assert.fail(e.getMessage()); 
      } 
      

      这段代码位于try...catch块中,因为getNumStudentsInCoursegetNumStudentsInCourse方法会抛出SQLException。由于模拟的方法不会调用任何 SQL 代码,所以这不会发生。然而,由于这些方法的签名表明SQLException可以从这些方法中抛出,我们必须在try...catch中调用它们以避免编译器错误。

      try块中的第一个语句告诉 Mockito,当在courseDAO对象上调用getNumStudentsInCourse方法,并传递参数1(课程 ID)时,它应该从模拟的方法中返回60

      第二个语句告诉 Mockito,当在courseDAO对象上调用enrollStudentInCourse方法,并传递参数1(课程 ID)和1(学生 ID)时,它应该不执行任何操作。我们并不想在单元测试代码中真正地将任何记录插入到数据库中。

      现在,我们将创建CourseStudent对象,并调用CourseaddStudent方法。这段代码与我们在前面的测试用例中写的类似:

      Course course = new Course(); 
      course.setCourseDAO(courseDAO); 
      
      course.setId(1); 
      course.setName("course1"); 
      course.setMaxStudents(60); 
      //create student 
      Student student = new Student(); 
      student.setFirstName("Student1"); 
      student.setId(1); 
      //now add student 
      course.addStudent(student); 
      

      注意,我们在创建CourseStudent对象时使用的课程 ID 和学生 ID 应该与我们模拟方法时传递给getNumStudentsInCourseenrollStudentInCourse的参数相匹配。

      我们已经将允许在这个课程中学习的最大学生人数设置为60。当模拟getNumStudentsInCourse时,我们要求 Mockito 也返回60。因此,addStudent方法应该抛出一个异常,因为课程已满。我们将在稍后添加@Test注解来验证这一点。

      测试结束时,我们想要验证模拟的方法实际上被调用了:

      try { 
        Mockito.verify(courseDAO, Mockito.atLeastOnce()).getNumStudentsInCourse(1); 
      } catch (SQLException e) { 
        Assert.fail(e.getMessage()); 
      } 
      

      前面的代码验证了当运行这个测试时,Mockito 至少调用了一次courseDAOgetNumStudentsInCourse方法。

      以下是完整的测试用例,包括@Test注解属性,以确保函数抛出异常:

        @Test (expected = EnrollmentFullException.class) 
        public void testAddStudentWithEnrollmentFull() throws Exception 
         { 
          CourseDAO courseDAO = Mockito.mock(CourseDAO.class); 
          try { 
      Mockito.when(courseDAO.getNumStudentsInCourse(1)).thenReturn(60); 
      Mockito.doNothing().when(courseDAO).enrollStudentInCourse(1, 1); 
          } catch (SQLException e) { 
            Assert.fail(e.getMessage()); 
          } 
          Course course = new Course(); 
          course.setCourseDAO(courseDAO); 
      
          course.setId(1); 
          course.setName("course1"); 
          course.setMaxStudents(60); 
          //create student 
          Student student = new Student(); 
          student.setFirstName("Student1"); 
          student.setId(1); 
          //now add student 
          course.addStudent(student); 
      
          try { 
            Mockito.verify(courseDAO, 
             Mockito.atLeastOnce()).getNumStudentsInCourse(1); 
          } catch (SQLException e) { 
            Assert.fail(e.getMessage()); 
          } 
      
          //If no exception was thrown then the test case was successful 
          //No need of Assert here 
        } 
      

      现在,运行单元测试。所有测试都应该通过。

      这里有一个类似的测试用例,它使 Mockito 返回当前的注册人数为59,并确保学生成功注册:

        @Test 
        public void testAddStudentWithEnrollmentOpen() throws Exception 
         { 
          CourseDAO courseDAO = Mockito.mock(CourseDAO.class); 
          try { 
      Mockito.when(courseDAO.getNumStudentsInCourse(1)).thenReturn(59); 
      Mockito.doNothing().when(courseDAO).enrollStudentInCourse(1, 1); 
          } catch (SQLException e) { 
            Assert.fail(e.getMessage()); 
          } 
          Course course = new Course(); 
          course.setCourseDAO(courseDAO); 
      
          course.setId(1); 
          course.setName("course1"); 
          course.setMaxStudents(60); 
          //create student 
          Student student = new Student(); 
          student.setFirstName("Student1"); 
          student.setId(1); 
          //now add student 
          course.addStudent(student); 
      
          try { 
            Mockito.verify(courseDAO, 
             Mockito.atLeastOnce()).getNumStudentsInCourse(1);      Mockito.verify(courseDAO, 
             Mockito.atLeastOnce()).enrollStudentInCourse(1,1); 
          } catch (SQLException e) { 
            Assert.fail(e.getMessage()); 
          } 
      
          //If no exception was thrown then the test case was successful 
          //No need of Assert here 
        } 
      

      注意,这个测试用例不期望抛出任何异常(如果抛出异常,则测试用例失败)。我们还可以验证被模拟的enrollStudentInCourse方法是否被调用。我们之前没有验证这一点,因为在Course.addStudent方法调用此方法之前抛出了异常。

      在本节中,我们还没有涵盖 JUnit 的许多主题。我们鼓励您阅读 JUnit 文档github.com/junit-team/junit4/wiki。特别是,以下主题可能对您感兴趣:

      计算单元测试覆盖率

      单元测试可以告诉您您的应用程序代码是否按预期行为。单元测试对于维护代码质量和在开发周期早期捕获错误非常重要。然而,如果您没有编写足够的单元测试来测试您的应用程序代码,或者测试用例中没有测试所有可能的输入条件以及异常路径,那么这个目标就处于风险之中。为了衡量测试用例的质量和充分性,您需要计算测试用例的覆盖率。简单来说,覆盖率告诉您在运行单元测试时,您的应用程序代码中有多少百分比被触及。有不同方法来计算覆盖率:

      • 覆盖的行数

      • 覆盖的分支数量(使用ifelseelseifswitchtry/catch语句创建)

      • 覆盖的函数数量

      这三个指标共同为您单元测试的质量提供了一个公正的衡量。Java 有许多代码覆盖率工具。在本章中,我们将探讨一个名为 JaCoCo 的开源代码覆盖率工具www.eclemma.org/jacoco/。JaCoCo 还有一个 Eclipse 插件www.eclemma.org/,我们可以在 Eclipse 中直接测量代码覆盖率。

      您可以使用更新 URL([update.eclemma.org/](http://update.eclemma.org/))或从 Eclipse Marketplace 安装 JaCoCo 插件。要使用更新站点安装它,选择帮助 | 安装新软件...菜单。点击添加按钮并输入更新站点的名称(您可以给出任何名称)和更新 URL:

      图 5.6:为 JaCoCo 添加更新站点

      然后,按照说明安装插件。

      或者,您也可以从市场安装它。选择帮助 | Eclipse Marketplace...菜单。在查找文本框中输入EclEmma,然后点击 Go 按钮:

      图 5.7:从 Eclipse Marketplace 安装 EclEmma 代码覆盖率插件

      点击安装按钮并按照说明操作。

      要验证插件是否正确安装,打开窗口 | 显示视图 | 其他。在过滤器文本框中输入coverage并确保在 Java 类别下的覆盖率(Coverage)视图可用。打开该视图。

      要运行带有覆盖率单元测试,在包资源管理器中右键单击项目并选择覆盖率作为 | JUnit Test。测试运行后,覆盖率信息将在覆盖率视图中显示:

      图 5.8:覆盖率结果

      您如何解释这些结果?总体而言,在项目级别,覆盖率是 24.2%。这意味着在我们为这个应用程序编写的所有代码中,我们的单元测试用例只接触了 24.2%。然后,还有包级别和类级别的覆盖率百分比。

      在覆盖率视图中双击Course.java以查看此文件中哪些行被覆盖。以下截图显示了文件的一部分,其中红色行表示未覆盖的代码,绿色行表示覆盖的代码:

      图 5.9:行覆盖率详情

      我们为addStudent编写了单元测试,这个类的覆盖率是 100%,这是好的。我们没有在我们的单元测试中使用所有的getterssetters,因此其中一些没有被覆盖。

      如您所见,覆盖率结果可以帮助您了解代码中未编写单元测试或部分被单元测试覆盖的地方。基于这些数据,您可以添加未覆盖的代码的单元测试。当然,如果代码非常简单,例如前面类中的获取器和设置器,您可能不希望所有行都被覆盖。

      图 5.8中,观察一下覆盖率工具也分析了测试类。通常,我们不想对测试类进行覆盖率测量;我们希望通过运行测试类来测量应用程序代码的覆盖率。要排除测试类从这次分析中,在项目上右键单击并选择覆盖率作为 | 覆盖率配置...。点击覆盖率选项卡并仅选择 CourseManagementJDBC - src/main/java:

      图 5.10:覆盖率配置

      点击覆盖率以使用新设置运行覆盖率。您将在覆盖率视图中看到测试类没有出现在报告中,并且项目的整体测试覆盖率也有所下降。

      如果您想使用 Maven 运行覆盖率,请参阅www.eclemma.org/jacoco/trunk/doc/maven.html。具体来说,查看pom.xml(jacoco.org/jacoco/trunk/doc/examples/build/pom-it.xml),它为 JUnit 和 JaCoCo 覆盖率创建报告。

      摘要

      编写单元测试是应用程序开发过程中的重要部分。单元测试帮助您在非常早期阶段捕捉到应用程序中的错误;它们还帮助您捕捉到由于后续代码更改而引起的任何回归。JUnit 和 Eclipse 提供了一种简单的方法将单元测试集成到您的开发工作流程中。Eclipse 还在 JUnit 视图中创建了一个漂亮的报告,这使得识别失败的测试和跳转到测试失败的代码行变得容易。

      单元测试旨在在没有任何外部依赖的情况下执行。Mockito 等库可以帮助您模拟任何外部依赖。

      使用 JaCoCo 等覆盖率工具来检查您所编写的单元测试的质量。覆盖率工具会告诉您应用程序代码中有多少百分比被单元测试覆盖。您还可以查看每个类中哪些行被单元测试覆盖,哪些没有被覆盖。这样的报告可以帮助您决定是否需要编写更多的单元测试用例,或者修改现有的单元测试用例以覆盖单元测试未测试的重要代码。

      在下一章中,我们将看到如何从 Eclipse 调试 Java 应用程序。这一章还将解释如何连接到远程 JEE 服务器进行调试。

第六章:调试 JEE 应用程序

在上一章中,我们学习了如何使用 Eclipse 和 JUnit 为 Java 应用程序编写和运行单元测试。在本章中,我们将学习如何使用 Eclipse 调试 JEE 应用程序。调试是应用程序开发中不可避免的一部分。除非应用程序非常简单,否则它可能不会在第一次尝试时就按预期工作,你将花费一些时间来找出原因。在非常复杂的应用程序中,应用程序开发者可能最终会花费比编写应用程序代码更多的时间来调试。问题可能不一定存在于你的代码中,但可能存在于你的应用程序所依赖的外部系统中。调试复杂软件需要技能,这些技能可以通过经验来培养。然而,这也需要应用程序运行时和 IDE 的良好支持。

调试应用程序有不同的方法。你可以在代码中添加 System.out.println() 语句并打印变量的值,或者只是打印一条消息,说明应用程序的执行已达到某个点。如果应用程序很小或简单,这可能有效,但在调试大型和复杂的应用程序时,这可能不是一个好主意。你还需要记住在将代码移动到预发布或生产环境之前删除这些调试语句。如果你编写了单元测试,并且其中一些单元测试失败,那么这可能给你一些关于代码中问题的线索。然而,在许多情况下,你可能希望监控代码的行级或函数级执行,并检查该行或该函数中的变量值。这需要语言运行时的支持和一款能够帮助你可视化和控制调试过程的良好 IDE。幸运的是,Java 拥有出色的调试器,Eclipse JEE 为调试 Java 代码提供了极大的支持。

本章将学习如何使用 Eclipse JEE 调试 JEE 应用程序。我们将使用我们在第四章,“创建 JEE 数据库应用程序”中构建的相同的 课程管理 应用程序进行调试。本章中描述的调试技术可以应用于远程调试任何 Java 应用程序,并不一定仅限于 JEE 应用程序。

本章将涵盖以下主题:

  • 将 Eclipse 配置为远程调试 JEE 应用程序

  • 理解如何执行不同的调试操作,例如设置断点、检查变量和表达式,以及逐步执行代码

  • 将 Eclipse 的调试器连接到外部运行的 JEE 应用程序服务器

调试远程 Java 应用程序

你可能已经从 Eclipse 调试过独立的 Java 应用程序。你在代码中设置断点,从 Eclipse 以调试模式运行应用程序,然后通过单步执行代码来调试应用程序。调试远程 Java 应用程序略有不同,尤其是在如何启动调试器方面。在本地应用程序的情况下,调试器启动应用程序。在远程应用程序的情况下,它已经启动,你需要将调试器连接到它。一般来说,如果你想允许应用程序进行远程调试,你需要使用以下参数运行应用程序:

-Xdebug -Xrunjdwp:transport=dt_socket,address=9001,server=y,suspend=n

  • Xdebug 启用调试

  • Xrunjdwp 运行 Java 调试线协议(JDWP)的调试实现

除了 -Xdebug -Xrunjdwp,你也可以为 JDK 1.5 及以上版本使用 -agentlib:jdwp,例如:

 -agentlib:jdwp=transport= dt_socket,address=9001,server=y,suspend=n

让我们详细了解一下这里使用的参数:

  • transport=dt_socket: 这将在address=9001(这可以是任何空闲端口)启动一个套接字服务器,以接收调试命令并发送响应。

  • server=y: 这告诉 JVM 在调试通信的上下文中,应用程序是服务器还是客户端。对于远程应用程序,使用 y 值。

  • suspend=n: 这告诉 JVM 不要等待调试客户端连接到它。如果值为 y,则 JVM 将在执行主类之前等待,直到调试客户端连接到它。为该选项设置 y 值可能在你想调试,例如,在 Web 容器启动时加载的 servlet 的初始化代码的情况下很有用。在这种情况下,如果你不选择在调试器连接到它之前挂起应用程序,你想要调试的代码可能会在调试器客户端连接到它之前执行。

使用 Eclipse EE 中的 Tomcat 调试 Web 应用程序

我们已经学习了如何在 Eclipse EE 中配置 Tomcat 并从 Eclipse 中部署 Web 应用程序(请参阅第二章[part0037.html#1394Q0-d43a3a5ee6dd4ebc9d7c7e1cc8d7df55]中的“配置 Tomcat 在 Eclipse”和“在 Tomcat 中运行 JSP”部分,以及第四章[part0037.html#1394Q0-d43a3a5ee6dd4ebc9d7c7e1cc8d7df55]中的“创建简单的 JEE Web 应用程序”)。我们将使用我们在第四章[part0037.html#1394Q0-d43a3a5ee6dd4ebc9d7c7e1cc8d7df55]中创建的“课程管理”应用程序(JDBC 版本)进行调试。

以调试模式启动 Tomcat

如果你想调试远程 Java 进程,你需要使用调试参数启动该进程。然而,如果你已经在 Eclipse EE 中配置了 Tomcat,你不需要手动这样做。Eclipse 会负责以调试模式启动 Tomcat。要启动 Tomcat 以调试模式,请在“服务器视图”中选择服务器并点击“调试”按钮。或者,右键单击服务器并从菜单中选择调试。确保你想要调试的项目已经添加到 Tomcat 中;在这种情况下,项目是 CourseManagementJDBC

图片

图 6.1:以调试模式启动 Tomcat

一旦 Tomcat 以调试模式启动,其状态将变为调试模式:

图片

图 6.2:Tomcat 以调试模式运行

设置断点

现在,在我们启动CourseManagement应用程序之前,让我们在代码中设置断点。从CourseManagementJDBC项目打开CourseDAO,并在getCourses方法的第一行左侧边缘双击:

图片

图 6.3:设置断点

在行上设置断点的另一种方法是右键单击左侧边缘并选择切换断点:

图片

图 6.4:使用菜单切换断点

你也可以在方法级别设置断点。只需将光标放在任何方法内部,然后选择运行 | 切换方法断点菜单。这相当于在方法的第一行设置断点。当你总是想在方法开始时停止时,这比在方法的第一行设置断点更受欢迎。即使你后来在方法开头插入代码,调试器也会始终在方法的第一条语句处停止。

另一个有用的断点选项是在程序执行期间发生任何异常时设置它。通常,你可能不想在特定位置设置断点,但可能想调查异常发生的原因。如果你无法访问异常的堆栈跟踪,你只需为异常设置断点并再次运行程序。下次,执行将在异常发生的代码位置停止。这使得调试异常变得容易。要为异常设置断点,请选择运行 | 添加 Java 异常断点...并从列表中选择Exception类:

图片

图 6.5:在异常处设置断点

以调试模式运行应用程序

现在,让我们以调试模式运行listCourse.jsp页面:

  1. 在项目导航器中,转到src/main/webapp/listCourse.jsp,在文件上右键单击。选择调试为 | 服务器调试。Eclipse 可能会提示你使用现有的调试服务器:

图片

图 6.6:选择现有的调试服务器

  1. 点击完成。Eclipse 会询问你是否想要切换到调试视角(有关 Eclipse 视角的讨论,请参阅第一章,介绍 JEE 和 Eclipse):

图片

图 6.7:自动切换到调试视角

  1. 选择“记住我的决定”选项并点击“是”按钮。Eclipse 将切换到调试视角。Eclipse 会尝试在内部 Eclipse 浏览器中打开页面,但不会立即显示页面。回想一下listCourse.jsp调用Course.getCourses(),它反过来调用CourseDAO.getCourses()。我们在CourseDAO.getCourses()方法中设置了断点,因此页面的执行在此处停止:

图片

图 6.8:调试器在断点处暂停

执行步骤操作和检查变量

您现在可以使用顶部的工具栏图标或使用键盘快捷键执行不同的步骤操作(步骤跳过、步骤进入和步骤退出)。打开运行菜单的下拉菜单,了解调试的菜单和工具栏快捷键。通常,您会检查变量或执行步骤操作以验证执行流程是否正确,然后通过单击“继续”按钮或使用菜单/键盘快捷键继续执行。

在调试选项卡中(参见图 6.8),当调试器暂停时,您可以看到所有线程并检查每个线程的堆栈帧。线程的堆栈帧显示了程序在该线程中的执行路径,直到调试器在遇到断点或由于步骤操作而暂停的点。在多线程应用程序中,例如 Tomcat Web 容器,可能同时暂停多个线程,并且每个线程可能具有不同的堆栈帧。在调试多线程应用程序时,请确保在选择步骤操作/进入/退出或继续选项之前,您已经在调试选项卡中选择了所需的线程。

通常,您会进入一个方法,发现值不是您预期的,您想重新运行当前方法中的语句来调查它们。在这种情况下,您可以退回到任何之前的堆栈帧并重新开始。

例如,假设在前面的例子中我们进入DatabaseConnectionFactory.getConnectionFactory().getConnection方法。当我们进入时,调试器首先进入getConnectionFactory方法,然后在下一步进入操作中,它进入getConnection方法。假设当我们处于getConnection方法中时,我们想返回并检查getConnectionFactory方法中可能之前错过的事情(尽管在这个简单的例子中,getConnectionFactory方法中并没有发生太多;它应该只作为一个例子)。我们可以返回到getCourses方法,并重新开始getConnectionFactorygetConnection的执行。在调试选项卡中,右键单击CourseDAO.getCourses()堆栈帧,并选择“退到帧”,如图所示:

图片 2

图 6.9 退到帧

调试器丢弃所选帧以上的所有堆栈帧,执行退回到所选帧;在这种情况下,在CourseDAO类的getCourses方法中。然后您可以再次进入getConnection方法。请注意,当您退到帧时,只会丢弃堆栈变量及其值。对不在堆栈上的引用对象所做的任何更改都不会回滚。

检查变量值

现在让我们跳过几个语句,直到进入while循环,从结果集返回的数据中创建课程对象。在右上角的窗口中,您将找到变量视图,它显示在执行该点时适用的变量:

图片 1

图 6.10:调试器在断点处暂停

您也可以通过更改调试选项卡中的选择来检查上一个方法调用中的变量:点击任何先前的方法调用(堆栈帧),变量视图将显示所选方法的变量。您可以更改任何变量的值,包括对象的成员变量的值。例如,在图 6.8中,我们可以将课程名称从"Machine Learning"更改为"Machine Learning - Part1"。要更改变量值,请在变量视图中右键单击变量并选择更改值:

图片 2

图 6.11:在调试过程中更改变量的值

您不必每次都去变量视图检查变量的值。有一个快速的方法:只需将光标悬停在编辑器中的变量上,Eclipse 就会弹出一个显示变量值的窗口:

图片 3

图 6.12:检查变量

您也可以右键单击变量并选择检查选项来查看变量的值。然而,当您选择检查选项时,无法更改值。

如果您想经常查看变量的值(例如,循环中的变量),可以将变量添加到监视列表。这是一个比在变量视图中搜索变量更方便的选项。右键单击变量并从菜单中选择监视选项。监视选项将变量添加到表达式视图(其默认位置在右上角的断点视图旁边)并显示其值:

图片 1

图 6.13:检查变量

表达式视图的使用不仅限于监视变量值。您可以监视任何有效的 Java 表达式,例如算术表达式,甚至方法调用。在表达式视图中单击加号图标并添加一个表达式。

在外部配置的 Tomcat 中调试应用程序

到目前为止,我们已经使用在 Eclipse 中配置的 Tomcat 调试我们的应用程序。当我们以调试模式启动 Tomcat 时,Eclipse 负责将调试 JVM 参数添加到 Tomcat 启动脚本中。在本节中,我们将了解如何启动一个外部(相对于 Eclipse)的 Tomcat 实例,并从 Eclipse 连接到它。尽管我们将调试远程 Tomcat 实例,但本节中的信息也可以用于连接到任何以调试模式启动的远程 Java 程序。我们已经看到了在调试模式下启动远程应用程序时传递的调试参数。

在调试模式下外部启动 Tomcat 并不太难。Tomcat 启动脚本已经有一个选项可以以调试模式启动服务器;您只需要传递适当的参数。从命令提示符中,选择<TOMCAT_HOME>/bin文件夹,在 Windows 中输入以下命令:

>catalina.bat jpda start 

在 Mac OSX 和 Linux 中以调试模式启动 Tomcat:

$./catalina.sh jpda start  

通过传递jpda参数,将所有所需的调试参数设置为默认值。默认调试端口是 8000。如果您想更改它,可以修改catalin.bat/catalin.sh或按以下方式设置环境变量JPDA_ADDRESS

在 Windows 中设置JPDA_ADDRESS环境变量:

>set JPDA_ADDRESS=9001  

在 OSX 和 Linux 中设置JPDA_ADDRESS环境变量:

$export JPDA_ADDRESS=9001  

同样,您可以将JPDA_SUSPEND设置为yn来控制调试器在执行main类之前是否应该等待客户端连接。

要从 Eclipse 连接到远程实例,请选择“运行 | 调试配置...”菜单。在左侧的列表视图中右键单击“远程 Java 应用程序”节点,然后选择“新建”:

图 6.14:检查变量

设置适当的项目和端口(与您在调试模式下启动 Tomcat 时选择的相同,即默认:8000)并点击“调试”。如果调试器连接成功,Eclipse 将切换到调试视角。从现在开始,调试的过程与之前解释的相同。

使用调试器了解程序执行状态

我们已经看到如何使用调试器来验证程序的执行流程(使用单步操作)以及检查变量。您还可以使用调试器来了解正在运行的程序的状态。例如,一个 Web 请求耗时过长,您想知道执行确实卡在了哪里。您可以使用调试器来找到这一点。这类似于获取正在运行的程序的线程转储,但比获取线程转储的方法要简单得多。让我们假设我们的CourseDAO.getCourses方法执行时间过长。我们可以通过使用几个Thread.sleep调用来模拟这种情况,如下面的代码片段所示:

public List<Course> getCourses () throws SQLException { 
  //get connection from connection pool 
  Connection con = 
 DatabaseConnectionFactory.getConnectionFactory().getConnection(); 

  try { 
    Thread.sleep(5000); 
  } catch (InterruptedException e) {} 

  List<Course> courses = new ArrayList<Course>(); 
  Statement stmt = null; 
  ResultSet rs = null; 
  try { 
    stmt = con.createStatement(); 

    StringBuilder sb = new StringBuilder("select course.id as 
     courseId, course.name as courseName,")      .append("course.credits as credits, Teacher.id as teacherId, 
       Teacher.first_name as firstName, ")      .append("Teacher.last_name as lastName, Teacher.designation 
       designation ") 
      .append("from Course left outer join Teacher on ") 
      .append("course.Teacher_id = Teacher.id ") 
      .append("order by course.name"); 

    rs = stmt.executeQuery(sb.toString()); 

    while (rs.next()) { 
      Course course = new Course(); 
      course.setId(rs.getInt("courseId")); 
      course.setName(rs.getString("courseName")); 
      course.setCredits(rs.getInt("credits")); 
      courses.add(course); 

      int teacherId = rs.getInt("teacherId"); 
      if (rs.wasNull()) //no teacher set for this course. 
        continue; 
      Teacher teacher = new Teacher(); 
      teacher.setId(teacherId); 
      teacher.setFirstName(rs.getString("firstName")); 
      teacher.setLastName(rs.getString("lastName")); 
      teacher.setDesignation(rs.getString("designation")); 
      course.setTeacher(teacher); 
    } 

    try { 
      Thread.sleep(5000); 
    } catch (InterruptedException e) {} 

    return courses; 
  } finally { 
    try {if (rs != null) rs.close();} catch (SQLException e) {} 
    try {if (stmt != null) stmt.close();} catch (SQLException e) 
 {} 
    try {con.close();} catch (SQLException e) {} 
  } 
} 

以调试模式启动 Tomcat,并以调试模式运行listCourses.jsp。因为我们插入了Thread.sleep语句,所以请求将需要时间。转到“调试”视图,这是显示线程和调用栈的地方。在 Tomcat 调试配置节点下的第一个节点上单击,并选择“暂停”选项,如下面的屏幕截图所示:

图 6.15:暂停程序执行

调试器暂停程序中所有线程的执行。然后您可以通过展开线程节点来查看每个线程的状态。您将找到一个线程正在执行CourseDAO.getCourse方法,以及它在暂停之前正在执行的语句:

图 6.16:暂停线程的状态

从前面的屏幕截图中,您可以看到线程的执行在Thread.sleep语句的CourseDAO.getCourses方法中被暂停。当程序暂停时,您甚至可以在每个调用栈帧中检查变量。通过暂停程序并检查线程和调用栈的状态,您可能能够找到应用程序中的瓶颈。

摘要

语言运行时和 IDE 对调试的良好支持可以显著减少调试所花费的时间。Java 运行时和 Eclipse 为本地和远程应用程序的调试提供了出色的支持。要调试远程应用程序,请使用 JVM 调试参数启动它,并将 Eclipse 调试器连接到它。然后,你可以像调试本地应用程序一样调试远程应用程序,即设置断点、执行步骤操作和检查变量。你还可以在应用程序执行暂停时更改变量值。

在下一章中,我们将了解如何使用 EJBs 开发 JEE 应用程序并使用 GlassFish 服务器。尽管本章解释了在 Tomcat 部署的 JEE 应用程序的调试,但你也可以在 GlassFish 服务器中使用相同的技巧。

第七章:使用 EJB 创建 JEE 应用程序

在上一章中,我们学习了从 Eclipse 调试 JEE 应用程序的一些技术。在本章中,我们将将我们的重点重新转向 JEE 应用程序开发,并学习如何创建和使用 企业 JavaBeansEJB)。如果你还记得 第四章,“创建 JEE 数据库应用程序”中的数据库应用程序架构,我们使用了 JSP 或 JSF 页面调用 JSP Bean 或托管 Bean。然后,Bean 调用 DAO 执行数据访问代码。这样,用户界面、业务逻辑和数据库的代码就很好地分离了。这对于小型或中型应用程序来说可能适用,但在大型企业应用程序中可能会成为瓶颈;应用程序可能扩展性不好。如果业务逻辑的处理耗时较长,那么将其分布在不同服务器上以获得更好的可扩展性和弹性会更有意义。如果用户界面、业务逻辑和数据访问的代码都在同一台机器上,那么它可能会影响应用程序的可扩展性;也就是说,在负载下可能表现不佳。

在需要将处理业务逻辑的组件分布在不同服务器上的场景中,使用 EJB 实现业务逻辑是理想的。然而,这只是 EJB 的优势之一。即使你在与 Web 应用程序相同的服务器上使用 EJB,你也可以从 EJB 容器提供的众多服务中获益;你可以声明性地(使用注解)指定调用 EJB 方法的安全约束,并可以使用注解轻松指定事务边界(指定一个事务的一部分的方法调用集合)。此外,容器处理 EJB 的生命周期,包括某些类型 EJB 对象的池化,以便在应用程序负载增加时可以创建更多对象。

在 第四章,“创建 JEE 数据库应用程序”,我们使用简单的 JavaBeans 创建了一个 课程管理 网络应用程序。在本章中,我们将使用 EJB 创建相同的应用程序并将其部署到 GlassFish 服务器上。然而,在此之前,我们需要了解一些 EJB 的基本概念。

我们将涵盖以下广泛的主题:

  • 理解不同类型的 EJB 以及它们如何从不同的客户端部署场景中访问

  • 在 Eclipse 中配置 GlassFish 服务器以测试 EJB 应用程序

  • 使用和未使用 Maven 从 Eclipse 创建和测试 EJB 项目

EJB 类型

根据 EJB3 规范,EJB 可以有以下类型:

  • 会话 Bean:

    • 有状态会话 Bean

    • 无状态会话 Bean

    • 单例会话 Bean

  • 消息驱动 Bean

当我们学习到 JEE 应用程序中的异步请求处理时,我们将在第十章 Chapter 10,“使用 JMS 的异步编程”,将详细讨论消息驱动 BeanMDB)。在这一章中,我们将专注于会话 Bean。

会话 Bean

通常,会话 Bean 旨在包含执行企业应用程序主要业务逻辑的方法。任何普通 Java 对象POJO)都可以通过适当的 EJB3 特定注解来注解,使其成为一个会话 Bean。会话 Bean 有三种类型。

有状态会话 Bean

一个有状态会话 Bean 只为一个客户端提供服务。有状态会话 Bean 与客户端之间存在一对一的映射。因此,有状态 Bean 可以在多个方法调用之间保留客户端的状态数据。在我们的课程管理应用程序中,我们可以在学生登录后使用一个有状态 Bean 来保存学生数据(学生资料和她/他选择的课程)。当服务器重启或会话超时时,由有状态 Bean 维护的状态将丢失。由于每个客户端都有一个有状态 Bean,使用有状态 Bean 可能会影响应用程序的可伸缩性。

我们在类上使用@Stateful注解来标记它为一个有状态会话 Bean。

无状态会话 Bean

无状态会话 Bean 不保留任何客户端的状态信息。因此,一个会话 Bean 可以被多个客户端共享。EJB 容器维护着一组无状态 Bean 的池,当客户端请求到来时,它会从池中取出一个 Bean,执行方法,然后将 Bean 返回池中。无状态会话 Bean 提供了卓越的可伸缩性,因为它们可以被共享,并且不需要为每个客户端创建。

我们在类上使用@Stateless注解来标记它为一个无状态会话 Bean。

单例会话 Bean

如其名所示,在 EJB 容器中只有一个单例 Bean 类的实例(这在集群环境中也是正确的;每个 EJB 容器将有一个单例 Bean 的实例)。这意味着它们被多个客户端共享,并且不会被 EJB 容器池化(因为只能有一个实例)。由于单例会话 Bean 是一个共享资源,我们需要在其中管理并发。Java EE 为单例会话 Bean 提供了两种并发管理选项,即容器管理并发和 Bean 管理并发。容器管理并发可以通过注解轻松指定。

有关在单例会话 Bean 中管理并发的更多信息,请参阅javaee.github.io/tutorial/ejb-basicexamples003.html#GIPSZ

如果存在资源竞争,单例 Bean 的使用可能会影响应用程序的可伸缩性。

我们在类上使用@Singleton注解来标记它为一个单例会话 Bean。

从客户端访问会话 Bean

会话 Bean 可以被设计为本地访问(客户端和 Bean 在同一个应用程序中),远程访问(从运行在不同应用程序或 JVM 中的客户端),或两者兼而有之。在远程访问的情况下,会话 Bean 必须实现一个远程接口。对于本地访问,会话 Bean 可以实现一个本地接口或实现无接口(会话 Bean 的无接口视图)。会话 Bean 实现的远程和本地接口有时也被称为业务接口,因为它们通常暴露主要业务功能。

创建无接口会话 Bean

要创建具有无接口视图的会话 Bean,创建一个 POJO,并使用适当的 EJB 注解类型和@LocalBean对其进行注解。例如,我们可以创建一个本地状态 ful 的Student Bean 如下:

import javax.ejb.LocalBean; 
import javax.ejb.Singleton; 

@Singleton 
@LocalBean 
public class Student { 
... 
} 

使用依赖注入访问会话 Bean

您可以通过使用@EJB注解(该注解将 Bean 注入客户端类)或通过执行Java 命名和目录接口JNDI)查找来访问会话 Bean。EJB 容器必须使 EJB 的 JNDI URL 对客户端可用。

使用@EJB注入仅适用于受管理组件,即由 EJB 容器管理生命周期的应用程序组件。当组件由容器管理时,它由容器创建(实例化)和销毁。您不使用new运算符创建受管理组件。支持直接注入 EJB 的 JEE 受管理组件包括 servlet、JSF 页面的受管理 Bean 以及 EJB 本身(一个 EJB 可以注入另一个 EJB)。不幸的是,您不能让 Web 容器在 JSP 或 JSP Bean 中注入 EJB。此外,您不能将 EJB 注入您创建并使用new运算符实例化的任何自定义类。在本章的后面部分,我们将看到如何使用 JNDI 从不受容器管理的对象访问 EJB。

我们可以从 JSF 的受管理 Bean 中使用之前创建的学生 Bean 如下:

import javax.ejb.EJB; 
import javax.faces.bean.ManagedBean; 

@ManagedBean 
public class StudentJSFBean { 
  @EJB 
  private Student studentEJB; 
} 

注意,如果您创建了一个无接口视图的 EJB,那么该 EJB 中的所有public方法都将暴露给客户端。如果您想控制客户端可以调用的方法,那么您应该实现一个业务接口。

使用本地业务接口创建会话 Bean

EJB 的业务接口是一个简单的 Java 接口,用@Remote@Local进行注解。因此,我们可以创建一个学生 Bean 的本地接口如下:

import java.util.List; 
import javax.ejb.Local; 

@Local 
public interface StudentLocal { 
  public List<Course> getCourses(); 
} 

此外,我们可以如下实现会话 Bean:

import java.util.List; 
import javax.ejb.Local; 
import javax.ejb.Stateful; 

@Stateful 
@Local 
public class Student implements StudentLocal { 
  @Override 
  public List<CourseDTO> getCourses() { 
    //get courses are return 
... 
  } 
} 

客户端只能通过本地接口访问Student EJB:

import javax.ejb.EJB; 
import javax.faces.bean.ManagedBean; 

@ManagedBean 
public class StudentJSFBean { 
  @EJB 
  private StudentLocal student; 
} 

会话 Bean 可以实现多个业务接口。

使用 JNDI 查找访问会话 Bean

虽然使用依赖注入访问 EJB 是最简单的方法,但它仅当容器管理访问 EJB 的类时才有效。如果你想从一个不是管理 bean 的 POJO 中访问 EJB,则依赖注入将不起作用。依赖注入不起作用的另一个场景是当 EJB 部署在单独的 JVM(可能是在远程服务器上)时。在这种情况下,你必须使用 JNDI 查找来访问 EJB(有关 JNDI 的更多信息,请访问docs.oracle.com/javase/tutorial/jndi/)。

JEE 应用程序可以打包成企业应用程序存档EAR),其中包含 EJB 的.jar文件和 Web 应用程序的.war文件(以及包含两个都需要的库的lib文件夹)。例如,如果 EAR 文件的名称是CourseManagement.ear,其中 EJB 的.jar文件名称是CourseManagementEJBs.jar,则应用程序的名称是CourseManagement(EAR 文件的名称),模块名称是CourseManagementEJBs。EJB 容器使用这些名称来创建查找 EJB 的 JNDI URL。EJB 的全局 JNDI URL 创建如下:

"java:global/<application_name>/<module_name>/<bean_name>![<bean_interface>]" 

让我们看看前面代码片段中使用的不同参数:

  • java:global: 这表示它是一个全局 JNDI URL。

  • <application_name>: 这通常是 EAR 文件的名称。

  • <module_name>: 这是 EJB JAR 的名称。

  • <bean_name>: 这是 EJB bean 类的名称。

  • <bean_interface>: 如果 EJB 有一个无接口视图,或者如果 EJB 只实现了一个业务接口,则此属性是可选的。否则,它是业务接口的完全限定名称。

EJB 容器必须为每个 EJB 发布两个 JNDI URL 的变体。这些不是全局 URL,这意味着它们不能用于从不在同一 JEE 应用程序(同一 EAR)中的客户端访问 EJB:

  • java:app/[<module_name>]/<bean_name>![<bean_interface>]

  • java:module/<bean_name>![<bean_interface>]

如果 EJB 客户端在同一应用程序中,则可以使用第一个 URL;如果客户端在同一模块中(与 EJB 相同的.jar文件),则可以使用第二个 URL。

在你查找 JNDI 服务器中的任何 URL 之前,你需要创建InitialContext,这包括其他信息,例如 JNDI 服务器的主机名和它运行的端口。如果你在同一服务器中创建InitialContext,那么不需要指定这些属性:

InitialContext initCtx  = new InitialContext(); 
Object obj = initCtx.lookup("jndi_url"); 

我们可以使用以下 JNDI URL 来访问无接口(LocalBean)的Student EJB(假设 EAR 文件的名称是CourseManagement,EJB 的.jar文件名称是CourseManagementEJBs):

URL 何时使用
java:global/CourseManagement/ CourseManagementEJBs/Student 客户端可以位于 EAR 文件中的任何位置,因为我们使用了全局 URL。请注意,我们没有指定接口名称,因为我们假设在这个示例中学生 Bean 提供了一个无接口视图。
java:app/CourseManagementEJBs/Student 客户端可以位于 EAR 中的任何位置。我们跳过了应用程序名称,因为客户端预期位于同一应用程序中,因为 URL 的命名空间是 java:app
java:module/Student 客户端必须在与 EJB 相同的 .jar 文件中。

我们可以使用以下 JNDI URL 来访问实现了名为 StudentLocal 的本地接口的 Student EJB:

URL 何时使用
java:global/CourseManagement/ CourseManagementEJBs/Student!packt.jee.book.ch6.StudentLocal 客户端可以位于 EAR 文件中的任何位置,因为我们使用了全局 URL。
java:global/CourseManagement/ CourseManagementEJBs/Student 客户端可以位于 EAR 中的任何位置。我们跳过了接口名称,因为 Bean 只实现了单个业务接口。请注意,从这个调用返回的对象将是 StudentLocal 类型,而不是 Student 类型。
java:app/CourseManagementEJBs/Studentjava:app/CourseManagementEJBs/Student!packt.jee.book.ch6.StudentLocal 客户端可以位于 EAR 中的任何位置。我们跳过了应用程序名称,因为 JNDI 命名空间是 java:app
java:module/Studentjava:module/Student!packt.jee.book.ch6.StudentLocal 客户端必须在与 EJB 相同的 EAR 中。

以下是从我们的 Web 应用程序中的非 Web 容器管理的对象(之一)调用具有本地业务接口的学生 Bean 的示例:

InitialContext ctx = new InitialContext(); 
StudentLocal student = (StudentLocal) ctx.loopup 
 ("java:app/CourseManagementEJBs/Student"); 
return student.getCourses(id) ; //get courses from Student EJB 

使用远程业务接口创建会话 Bean

如果您创建的会话 Bean 将由不在 Bean 所在 JVM 中的客户端对象访问,则该 Bean 需要实现远程业务接口。您可以通过在类上使用 @Remote 注解来创建远程业务接口:

import java.util.List; 
import javax.ejb.Remote; 

@Remote 
public interface StudentRemote { 
  public List<CourseDTO> getCourses(); 
} 

实现远程接口的 EJB 也用 @Remote 注解:

@Stateful 
@Remote 
public class Student implements StudentRemote { 
  @Override 
  public List<CourseDTO> getCourses() { 
    //get courses are return 
... 
  } 
} 

远程 EJB 可以使用 @EJB 注解注入到同一应用程序中的管理对象中。例如,一个 JSF Bean 可以如下访问之前提到的学生 Bean(位于同一应用程序中):

import javax.ejb.EJB; 
import javax.faces.bean.ManagedBean; 

@ManagedBean 
public class StudentJSFBean { 
  @EJB 
  private StudentRemote student; 
} 

访问远程会话 Bean

要访问远程 Student EJB,我们可以使用以下 JNDI URL:

URL 何时使用
java:global/CourseManagement/ CourseManagementEJBs/Student!packt.jee.book.ch6.StudentRemote 客户端可以在同一应用程序或远程位置。对于远程客户端,我们需要设置适当的 InitialContext 参数。
java:global/CourseManagement/CourseManagementEJBs/Student 客户端可以在同一应用程序或远程位置。我们跳过了接口名称,因为 Bean 只实现了单个业务接口。
java:app/CourseManagementEJBs/Studentjava:app/CourseManagementEJBs/Student!packt.jee.book.ch6.StudentRemote 客户端可以在 EAR 的任何位置。我们跳过了应用程序名称,因为 JNDI 命名空间是java:app
java:module/Studentjava:module/Student!packt.jee.book.ch6.StudentRemote 客户端必须在与 EJB 相同的 EAR 中。

要从远程客户端访问 EJB,您需要使用 JNDI 查找方法。此外,您需要设置具有某些属性的InitialContext;其中一些属性是 JEE 应用服务器特定的。如果远程 EJB 和客户端都部署在 GlassFish 中(不同的 GlassFish 实例),那么您可以按以下方式查找远程 EJB:

Properties jndiProperties = new Properties(); 
jndiProperties.setProperty("org.omg.CORBA.ORBInitialHost", 
 "<remote_host>"); 
//target ORB port. default is 3700 in GlassFish 
jndiProperties.setProperty("org.omg.CORBA.ORBInitialPort", 
 "3700"); 

InitialContext ctx = new InitialContext(jndiProperties); 
StudentRemote student = 
 (StudentRemote)ctx.lookup("java:app/CourseManagementEJBs/Student"); 
return student.getCourses(); 

在 Eclipse 中配置 GlassFish 服务器

我们将在本章中使用 GlassFish 应用服务器。我们已经在第一章的“安装 GlassFish 服务器”部分中看到了如何安装 GlassFish。

我们将首先在 Eclipse JEE 中配置 GlassFish 服务器:

  1. 要在 Eclipse EE 中配置 GlassFish 服务器,请确保您处于 Eclipse 的 Java EE 视图中。右键单击服务器视图并选择新建 | 服务器。如果您在服务器类型列表中看不到 GlassFish 服务器组,请展开 Oracle 节点并选择和安装 GlassFish 工具:

图 7.1:安装 GlassFish 工具

  1. 如果您已经安装了 GlassFish 工具,或者 GlassFish 服务器类型在列表中可用,那么展开它并选择 GlassFish 选项:

图 7.2:在 Eclipse 中创建 GlassFish 服务器实例

  1. 点击下一步。在“域路径”字段中输入您本地机器上的 GlassFish 服务器路径。如果适用,输入管理员名称和密码,然后点击下一步:

图 7.3:定义 GlassFish 服务器属性

  1. 下一页允许您在 GlassFish 中部署现有的 Java EE 项目。目前我们没有要添加的项目,所以只需点击完成。

  2. 服务器已添加到服务器视图。右键单击服务器并选择启动。如果服务器已正确安装和配置,则服务器状态应更改为已启动。

  3. 要打开服务器的管理页面,右键单击服务器并选择 GlassFish | 查看管理控制台。管理页面在内置的 Eclipse 浏览器中打开。您可以通过打开http://localhost:8080 URL 来浏览服务器主页。8080是 GlassFish 的默认端口。

使用 EJB 创建课程管理应用程序

现在我们来创建我们在第四章中创建的课程管理应用程序,这次使用 EJB。在第四章中,我们创建了用于编写业务逻辑的服务类(它们是 POJO)。我们将用 EJB 来替换它们。我们将首先创建 EJB 的 Eclipse 项目。

在 Eclipse 中创建 EJB 项目

EJB 打包在一个 JAR 文件中。Web 应用程序打包在一个Web 应用程序存档WAR)中。如果 EJB 需要远程访问,则客户端需要访问业务接口。因此,EJB 业务接口和共享对象(在 EJB 和客户端之间共享)打包在一个单独的 JAR 中,称为 EJB 客户端 JAR。此外,如果 EJB 和 Web 应用程序要作为一个单一应用程序部署,那么它们需要打包在一个 EAR 中。

因此,在大多数情况下,带有 EJB 的应用程序不是一个单一的项目,而是四个不同的项目:

  • 创建 EJB JAR 的 EJB 项目

  • 包含业务类和共享(在 EJB 和客户端之间)类的 EJB 客户端项目

  • 生成 WAR 的 Web 项目

  • 生成包含 EJB JAR、EJB 客户端 JAR 和 WAR 的 EAR 项目

您可以独立创建这些项目并将它们集成。然而,Eclipse 提供了使用一个向导创建 EJB 项目、EJB 客户端项目和 EAR 项目的选项:

  1. 选择文件 | 新建 | EJB 项目。在项目名称文本框中输入CourseManagementEJBs

图片 4

图 7.4:新建 EJB 项目向导

确保目标运行时为 GlassFish 5,EJB 模块版本为 3.2 或更高。从配置下拉列表中选择 GlassFish 5 的默认配置。在 EAR 成员组中,勾选将项目添加到 EAR 的框。

  1. 选择下一步。在下一页上,指定类的源文件夹和输出文件夹。在此页上保持默认设置不变:

图片 1

图 7.5:选择源文件夹和输出文件夹

  1. 此项目的源 Java 文件将创建在ejbModule文件夹中。点击下一步:

图片 2

图 7.6:创建 EJB 客户端项目

  1. Eclipse 提供了创建 EJB 客户端项目的选项。选择该选项并点击完成。

  2. 由于我们正在构建一个 Web 应用程序,我们将创建一个 Web 项目。选择文件 | 动态 Web 项目。将项目名称设置为CourseManagementWeb

图片 3

图 7.7:新建动态 Web 项目

  1. 选择将项目添加到 EAR 的复选框。由于我们在工作空间中只有一个 EAR 项目,Eclipse 会从下拉列表中选择此项目。点击完成。

现在我们工作空间中有以下四个项目:

图片 5

图 7.8:课程管理项目

在课程管理应用程序中,我们将创建一个无状态的 EJB,称为 CourseBean。我们将使用 Java 持久性 API(JPA)进行数据访问并创建一个 Course 实体。有关使用 JPAs 的详细信息,请参阅第四章,创建 JEE 数据库应用程序CourseManagementEJBClient 项目将包含 EJB 业务接口和共享类。在 CourseManagementWeb 中,我们将创建一个 JSF 页面和一个管理 Bean,该 Bean 将访问 CourseManagementEJBs 项目中的 Course EJB 以获取课程列表。

配置 GlassFish 中的数据源

在第四章,创建 JEE 数据库应用程序中,我们在应用程序本地创建了 JDBC 数据源。在本章中,我们将在 GlassFish 中创建 JDBC 数据源。GlassFish 服务器没有打包 MySQL 的 JDBC 驱动程序。因此,我们需要将 MySQLDriver.jar 文件放置在 GlassFish 可以找到它的路径中。您可以将此类外部库放置在您想要部署应用程序的 GlassFish 域的 lib/ext 文件夹中。对于本例,我们将复制 JAR 文件到 <glassfish_home>/glassfish/domains/domain1/lib/ext

如果您没有 MySQL JDBC 驱动程序,您可以从以下网址下载它:http://dev.mysql.com/downloads/connector/j/

  1. 打开 GlassFish 管理控制台,可以通过在“服务器视图”中右键单击服务器并选择 GlassFish | 查看管理控制台(这将在 Eclipse 内打开管理控制台)或浏览到 http://localhost:48484848 是 GlassFish 管理控制台应用程序默认监听的端口号)。在管理控制台中,选择资源 | JDBC | JDBC 连接池。在 JDBC 连接池页面上单击“新建”按钮:

图 7.9:在 GlassFish 中创建 JDBC 连接池

  1. 将池名称设置为 MySQLconnectionPool 并选择 javax.sql.DataSource 作为资源类型。从数据库驱动程序供应商列表中选择 MySql 并单击下一步。在下一页上,选择正确的数据源类名(com.mysql.jdbc.jdbc2.optional.MysqlDatasource):

图 7.10:GlassFish 中的 JDBC 连接池设置

  1. 我们需要设置 MySQL 的主机名、端口号、用户名和密码。在管理页面上,向下滚动到“附加属性”部分,并设置以下属性:
属性
端口/端口号 3306
数据库名称 <schemaname_of_coursemanagement>, 例如,course_management。有关创建 Course Management 数据库的 MySQL 模式的详细信息,请参阅第四章,创建 JEE 数据库应用程序
密码 MySQL 数据库密码。
URL/URL jdbc:mysql://:3306/<database_name>,例如,jdbc:mysql://:3306/course_management
服务器名称 localhost
用户 MySQL 用户名
  1. 点击完成。新的连接池已添加到左侧窗格中的列表中。点击新添加的连接池。在“常规”选项卡中,点击“Ping”按钮并确保 ping 操作成功:

图片

图 7.11:在 GlassFish 中测试 JDBC 连接池

  1. 接下来,我们需要为这个连接池创建一个 JNDI 资源,以便它可以从客户端应用程序访问。在左侧窗格中选择“资源”|“JDBC”|“JDBC 资源”节点。点击“新建”按钮创建一个新的 JDBC 资源:

图片

图 7.12:在 GlassFish 中测试 JDBC 连接池

  1. 将 JNDI 名称设置为jdbc/CourseManagement。从“池名称”下拉列表中选择我们为 MySQL 创建的连接池,即MySQLconnectionPool。点击保存。

在 Eclipse 项目中配置 JPA

现在我们将配置我们的 EJB 项目以使用 JPA 来访问 MySQL 数据库。我们已经在第四章的创建 JEE 数据库应用程序部分中学习了如何为 Eclipse 项目启用 JPA。然而,我们将在下面再次简要介绍这些步骤:

  1. 在项目探索器中右键单击CourseManagementEJBs项目,选择“配置”|“转换为 JPA 项目”。Eclipse 打开“项目特性”窗口:

图片

图 7.13:Eclipse 项目特性

  1. 点击下一步进入“JPA 特性”页面:

图片

图 7.14:JPA 特性

保持默认值不变,然后点击完成。Eclipse 会将 JPA 所需的persistence.xml文件添加到项目探索器中的“JPA 内容”组下的项目中。我们需要在persistence.xml中配置 JPA 数据源。打开persistence.xml并点击“连接”选项卡。将事务类型设置为JTA。在 JTA 数据源文本框中,输入我们在上一节中为 MySQL 数据库设置的 JNDI 名称,即jdbc/CourseManagement。保存文件。请注意,persistence.xml的实际位置是ejbModule/META-INF

现在我们将在 Eclipse 中创建一个数据库连接,并将其与项目的 JPA 属性链接起来,以便我们可以从数据库表中创建 JPA 实体。在CourseManagementEJBs项目上右键单击并选择“属性”。这会打开“项目属性”窗口。点击“JPA”节点以查看详细信息页面。在连接下拉框下方点击“添加连接”链接。我们已经在第四章的“使用 Eclipse 数据源探索器”部分中看到了如何设置数据库连接,创建 JEE 数据库应用程序。然而,我们将在下面简要回顾这些步骤:

  1. 在“连接配置”窗口中,选择 MySQL:

图片

图 7.15:新的数据库连接配置文件

  1. 在名称文本框中输入CourseManagementDBConnection并点击下一步。在“新连接配置”窗口中,点击新连接配置按钮(位于“驱动程序”下拉框旁边的圆圈)以打开“新驱动程序定义”窗口。选择适当的 MySQL JDBC 驱动程序版本,并点击“JAR 列表”标签。如果出现任何错误,删除任何现有的.jar文件,并点击“添加 JAR/ZIP”按钮。浏览到我们在<glassfish_home>/glassfish/domains/domain1/lib/ext文件夹中保存的 MySQL JDBC 驱动程序 JAR 文件。点击确定。回到“新连接配置”窗口,输入数据库名称,修改连接 URL,并输入用户名和密码:

图 7.16:配置 MySQL 数据库连接

  1. 选择“保存密码”复选框。点击“测试连接”按钮并确保测试成功。点击完成按钮。回到 JPA 属性页面,新的连接被添加,并选择了适当的模式:

图 7.17:添加到 JPA 项目属性的连接

  1. 点击确定以保存更改。

创建 JPA 实体

我们现在将使用 Eclipse JPA 工具为Course创建实体类:

  1. 右键单击CourseManagementEJBs项目,并选择 JPA 工具 | 从表生成实体:

图 7.18:从表创建实体

  1. 选择课程表并点击下一步。在“表关联”窗口中点击下一步。在下一页上,选择identity作为键生成器:

图 7.19:自定义 JPA 实体细节

  1. 输入包名。我们不想在下一页上更改任何内容,因此点击完成。注意,向导为我们的类创建了一个findAll查询,我们可以使用它来获取所有课程:
@Entity 
@NamedQuery(name="Course.findAll", query="SELECT c FROM 
 Course c") 
public class Course implements Serializable { ...} 

创建无状态 EJB

我们现在将为我们的应用程序创建无状态 EJB:

  1. 在项目资源管理器中右键单击CourseManagementEJBs项目的ejbModule文件夹,并选择新建 | 会话 Bean(3.x)。在 Java 包文本框中输入packt.book.jee.eclipse.ch7.ejb,在类名中输入CourseBean。选择远程复选框:

图 7.20:创建无状态会话 Bean

  1. 点击下一步。在下一页上不需要进行任何更改:

图 7.21:无状态会话 Bean 信息

  1. 点击完成。一个带有@Stateless@Localbean注解的CourseBean类被创建。该类还实现了在CourseManagementEJBClient项目中定义的CourseBeanRemote接口,这是一个共享接口(调用 EJB 的客户需要访问此接口):
@Stateless 
@LocalBean 
public class CourseBean implements CourseBeanRemote { 
    public CourseBean() { 
    } 
} 

接口被注解为@Remote

@Remote 
public interface CourseBeanRemote { 

} 

现在,问题是我们是怎样从我们的 EJB 返回 Course 信息的?EJB 将调用 JPA API 来获取 Course 实体的实例,但我们希望 EJB 返回 Course 实体的实例,还是应该返回轻量级的数据传输对象(DTO)的实例?每种方法都有其自身的优点。如果我们返回一个 Course 实体,那么我们就不需要在不同对象之间传输数据;这在 DTO 的情况下是必须做的(从实体传输数据到相应的 DTO)。然而,如果 EJB 客户端不在同一应用程序中,那么在层之间传递实体可能不是一个好主意,你可能不希望将你的数据模型暴露给外部应用程序。此外,通过返回 JPA 实体,你正在迫使客户端应用程序在其实现中依赖 JPA 库。

DTOs 轻量级,并且你可以仅暴露那些你希望客户端使用的字段。然而,你将不得不在实体和 DTO 之间传输数据。

如果你的 EJB 将被同一应用程序中的客户端使用,那么从 EJB 传输实体到客户端可能更容易。然而,如果你的客户端不是同一 EJB 应用程序的一部分,或者当你想将 EJB 作为 Web 服务(我们将在 第九章,创建 Web 服务)暴露时,你可能需要使用 DTO。

在我们的应用程序中,我们将看到两种方法的示例,即 EJB 方法返回 JPA 实体以及 DTO。记住,我们已经创建了 CourseBean 作为远程以及本地 Bean(无接口视图)。远程接口方法的实现将返回 DTO,而本地方法的实现将返回 JPA 实体。

现在,让我们将 getCourses 方法添加到 EJB 中。我们将创建 CourseDTO,一个数据传输对象,它是一个 POJO,并且从 getCourses 方法返回 DTO 的实例。此 DTO 需要位于 CourseManagementEJBsClient 项目中,因为它将在 EJB 和其客户端之间共享。

CourseManagementEJBsClient 项目的 packt.book.jee.eclipse.ch7.dto 包中创建以下类:

package packt.book.jee.eclipse.ch7.dto; 

public class CourseDTO { 
  private int id; 
  private int credits; 
  private String name; 
  public int getId() { 
    return id; 
  } 
  public void setId(int id) { 
    this.id = id; 
  } 
  public int getCredits() { 
    return credits; 
  } 
  public void setCredits(int credits) { 
    this.credits = credits; 
  } 
  public String getName() { 
    return name; 
  } 
  public void setName(String name) { 
    this.name = name; 
  } 
} 

将以下方法添加到 CourseBeanRemote

public List<CourseDTO> getCourses(); 

我们需要在 CourseBean EJB 中实现此方法。要从数据库获取课程,EJB 需要先获取一个 EntityManager 实例。回想一下,在 第四章,创建 JEE 数据库应用程序,我们创建了 EntityManagerFactory 并从其中获取了一个 EntityManager 实例。然后,我们将该实例传递给服务类,该类实际上使用 JPA API 从数据库中获取数据。

JEE 应用服务器使注入EntityManager变得非常简单。你只需在 EJB 类中创建EntityManager字段,并用@PersistenceContext(unitName="<name_as_specified_in_persistence.xml>")注解它。如果persistence.xml中只定义了一个持久化单元,则unitName属性是可选的。打开CourseBean类,并添加以下声明:

@PersistenceContext 
EntityManager entityManager; 

EJB 是受管理的对象,EJB 创建后,EJB 容器会注入EntityManager

对象的自动注入是 JEE 特性的一部分,称为上下文和依赖注入CDI)。有关 CDI 的信息,请参阅javaee.github.io/tutorial/cdi-basic.html#GIWHB

现在我们给CourseBean EJB 添加一个方法,该方法将返回一个Course实体的列表。我们将把这个方法命名为getCourseEntities。这个方法将由同一 EJB 中的getCourses方法调用,然后将其转换成 DTO 列表。getCourseEntities方法也可以由任何 Web 应用程序调用,因为 EJB 公开了无接口视图(使用@LocalBean注解):

public List<Course> getCourseEntities() { 
//Use named query created in Course entity using @NameQuery 
 annotation.      TypedQuery<Course> courseQuery = 
 entityManager.createNamedQuery("Course.findAll", Course.class); 
      return courseQuery.getResultList(); 
} 

在实现getCourses方法(定义在我们的远程业务接口CourseBeanRemote中)之后,我们得到CourseBean,如下所示:

@Stateless 
@LocalBean 
public class CourseBean implements CourseBeanRemote { 
  @PersistenceContext 
  EntityManager entityManager; 

    public CourseBean() { 
    } 

    public List<Course> getCourseEntities() { 
      //Use named query created in Course entity using @NameQuery 
       annotation.      TypedQuery<Course> courseQuery = 
 entityManager.createNamedQuery("Course.findAll", Course.class); 
      return courseQuery.getResultList(); 
    } 

  @Override 
  public List<CourseDTO> getCourses() { 
    //get course entities first 
    List<Course> courseEntities = getCourseEntities(); 

    //create list of course DTOs. This is the result we will 
     return 
    List<CourseDTO> courses = new ArrayList<CourseDTO>(); 

    for (Course courseEntity : courseEntities) { 
      //Create CourseDTO from Course entity 
      CourseDTO course = new CourseDTO(); 
      course.setId(courseEntity.getId()); 
      course.setName(courseEntity.getName()); 
      course.setCredits(course.getCredits()); 
      courses.add(course); 
    } 
    return courses; 
  } 
} 

创建 JSF 和管理 Bean

我们现在将在CourseManagementWeb项目中创建一个 JSF 页面来显示课程。我们还将创建一个管理 Bean 来调用CourseEJBgetCourses方法。有关 JSF 的详细信息,请参阅第二章的Java 服务器页面部分,创建一个简单的 JEE Web 应用程序

如第二章中所述,创建一个简单的 JEE Web 应用程序,我们需要在web.xml中添加 JSF Servlet 和映射。从CourseManagementWeb项目打开web.xml。你可以通过双击项目资源管理器中的“部署描述符:CourseManagementWeb”节点(在项目下)或从WebContent/Web-INF文件夹(再次在项目下)打开此文件。在web-app节点内添加以下 Servlet 声明和映射:

<servlet> 
  <servlet-name>JSFServlet</servlet-name> 
  <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> 
  <load-on-startup>1</load-on-startup> 
</servlet> 

<servlet-mapping> 
  <servlet-name>JSFServlet</servlet-name> 
  <url-pattern>*.xhtml</url-pattern> 
</servlet-mapping> 

CourseManagementWeb项目需要访问 EJB 的业务接口,该接口位于CourseManagementEJBsClient中。因此,我们需要将CourseManagementEJBsClient的引用添加到CourseManagementWeb中。打开CourseManagementWeb项目的项目属性(在CourseManagementWeb项目上右键单击并选择属性),然后选择 Java 构建路径。单击“项目”选项卡,然后单击“添加...”按钮。从列表中选择CourseManagementEJBsClient并单击确定:

图 7.22:添加项目引用

现在,让我们为稍后要创建的 JSF 创建一个托管豆。在 CourseManagementWeb 项目的 packt.book.jee.eclipse.ch7.web.bean 包中创建一个 CourseJSFBean 类(Java 源文件位于 Java 资源组下的 src 文件夹中):

import java.util.List; 
import javax.ejb.EJB; 
import javax.faces.bean.ManagedBean; 
import packt.book.jee.eclipse.ch7.dto.CourseDTO; 
import packt.book.jee.eclipse.ch7.ejb.CourseBeanRemote; 

@ManagedBean(name="Course") 
public class CourseJSFBean { 
  @EJB 
  CourseBeanRemote courseBean; 

  public List<CourseDTO> getCourses() { 
    return courseBean.getCourses(); 
  } 
} 

JSF 豆是托管豆,因此我们可以使用 @EJB 注解让容器注入 EJB。在前面的代码中,我们使用其远程接口 CourseBeanRemote 引用了 CourseBean。然后我们创建了一个名为 getCourses 的方法,该方法调用 Course EJB 上相同名称的方法,并返回 CourseDTO 对象的列表。

注意,我们在 @ManagedBean 注解中设置了 name 属性。这个托管豆将通过 JSF 作为变量 Course 访问。

我们现在将创建 JSF 页面 course.xhtml。在 CourseManagementWeb 项目的 WebContent 组中右键单击,选择新建 | 文件。创建 courses.xhtml 并包含以下内容:

<html  

 > 

<head> 
  <title>Courses</title> 
</head> 
<body> 
  <h2>Courses</h2> 
  <h:dataTable value="#{Course.courses}" var="course"> 
      <h:column> 
      <f:facet name="header">Name</f:facet> 
      #{course.name} 
    </h:column> 
      <h:column> 
      <f:facet name="header">Credits</f:facet> 
      #{course.credits} 
    </h:column> 
  </h:dataTable> 
</body> 
</html> 

该页面使用 dataTable 标签 (docs.oracle.com/javaee/7/javaserver-faces-2-2/vdldocs-jsp/h/dataTable.html),它从 Course 托管豆(实际上是 CourseJSFBean 类)接收数据以填充。表达式语言语法中的 Course.coursesCourse.getCourses() 的简写形式。这导致调用 CourseJSFBean 类的 getCourses 方法。

Course.courses 返回的列表的每个元素,即 CourseDTOList,都由 course 变量(在 var 属性值中)表示。然后我们使用 column 子标签在表中显示每门课程的名字和学分。

运行示例

在我们可以运行示例之前,我们需要启动 GlassFish 服务器并将我们的 JEE 应用程序部署到其中:

  1. 启动 GlassFish 服务器。

  2. 一旦启动,在服务器视图中右键单击 GlassFish 服务器,并选择添加和移除...菜单选项:

图片 1.75

图 7.23:将项目添加到 GlassFish 以进行部署

  1. 选择 EAR 项目并点击添加按钮。然后,点击完成。

    选定的 EAR 应用程序将在服务器中部署:

图片 1.76

图 7.24:在 GlassFish 中部署的应用程序

  1. 要运行 JSF 页面 course.xhtml,在项目资源管理器中右键单击它。

    然后选择运行方式 | 在服务器上运行。页面将在内部 Eclipse 浏览器中打开,MySQL 数据库中的课程将显示在页面上。

注意,我们可以在 CourseJSFBean 中使用 CourseBean(EJB)作为本地豆,因为它们位于同一应用程序中,在相同的服务器上部署。为此,在 CourseManagementWeb 的构建路径中添加 CourseManagementEJBs 项目的引用(打开 CourseManagementWeb 的项目属性,选择 Java 构建路径,选择项目标签,然后点击添加...按钮。选择 CourseManagementEJBs 项目并添加其引用)。

然后,在 CourseJSFBean 类中,删除 CourseBeanRemote 的声明并添加一个 CourseBean

  //@EJB 
  //CourseBeanRemote courseBean; 

  @EJB 
  CourseBean courseBean; 

当您对代码进行任何更改时,需要重新部署 EAR 项目到 GlassFish 服务器。在服务器视图中,您可以通过检查服务器状态来查看是否需要重新部署。如果状态是[已启动,同步],则不需要重新部署。然而,如果状态是[已启动,重新发布],则需要重新部署。只需单击服务器节点并选择发布菜单选项。

在 Eclipse 外部创建 EAR 文件进行部署

在上一节中,我们学习了如何从 Eclipse 部署应用程序到 GlassFish。这在开发过程中运行良好,但最终您需要创建 EAR 文件以部署到外部服务器。要从项目创建 EAR 文件,请右键单击 EAR 项目(在我们的示例中,它是 CourseManagementEJBsEAR),然后选择导出 | EAR 文件:

图 7.25:导出到 EAR 文件

选择目标文件夹并单击完成。然后,可以使用管理控制台或将其复制到 GlassFish 的 autodeploy 文件夹来部署此文件。

使用 Maven 创建 JEE 项目

在本节中,我们将学习如何使用 Maven 创建带有 EJB 的 JEE 项目。创建 Maven 项目可能比 Eclipse JEE 项目更可取,因为构建可以自动化。我们在上一节中看到了创建 EJB、JPA 实体和其他类的许多细节,所以这里不会重复所有这些信息。我们还学习了如何在第二章,“创建简单的 JEE Web 应用程序”和第三章,“Eclipse 中的源代码管理”中创建 Maven 项目,所以创建 Maven 项目的详细信息也不会重复。我们将主要关注如何使用 Maven 创建 EJB 项目。我们将创建以下项目:

  • CourseManagementMavenEJBs:该项目包含 EJB

  • CourseManagementMavenEJBClient:该项目包含 EJB 项目和客户端项目之间的共享接口和对象

  • CourseManagementMavenWAR:这是一个包含 JSF 页面和管理 Bean 的 Web 项目

  • CourseManagementMavenEAR:该项目创建可以部署到 GlassFish 的 EAR 文件

  • CourseManagement:该项目是所有之前提到的项目的整体父项目,构建所有这些项目

我们仍然从 CourseManagementMavenEJBs 开始。该项目应生成 EJB JAR 文件。让我们创建一个具有以下详细信息的 Maven 项目:

字段
组 ID packt.book.jee.eclipse.ch7.maven
艺术品 ID CourseManagementMavenEJBClient
版本 1
打包 jar

我们需要将 JEE API 的依赖项添加到我们的 EJB 项目中。让我们将javax.javaee-api的依赖项添加到pom.xml中。由于我们打算在带有自己的 JEE 实现和库的 GlassFish 中部署此项目,我们将此依赖项的范围设置为提供。在pom.xml中添加以下内容:

  <dependencies> 
    <dependency> 
      <groupId>javax</groupId> 
      <artifactId>javaee-api</artifactId> 
      <version>8.0</version> 
      <scope>provided</scope> 
    </dependency> 
  </dependencies> 

当我们在本项目中创建 EJB 时,需要在共享项目(客户端项目)中创建本地或远程业务接口。因此,我们将创建CourseManagementMavenEJBClient,以下为详细信息:

字段
组 ID packt.book.jee.eclipse.ch7.maven
艺术品 ID CourseManagementMavenEJBs
版本 1
打包 jar

此共享项目还需要访问 EJB 注解。因此,将之前添加到pom.xml文件的javax.javaee-api依赖项添加到CourseManagementMavenEJBClient项目的pom.xml文件中。

我们将在本项目中创建一个packt.book.jee.eclipse.ch7.ejb包,并创建一个远程接口。创建一个CourseBeanRemote接口(就像我们在本章创建无状态 EJB部分的创建无状态 EJB 中创建的那样)。此外,在packt.book.jee.eclipse.ch7.dto包中创建一个CourseDTO类。此类与我们创建的创建无状态 EJB部分中的类相同。

我们将在CourseManagementMavenEJBs项目中创建一个Course JPA 实体。在我们这样做之前,让我们将此项目转换为 JPA 项目。在包资源管理器中右键单击项目,选择配置 | 转换为 JPA 项目。在 JPA 配置向导中,选择以下 JPA 特性详细信息:

字段
平台 Generic 2.1
JPA 实现 禁用库配置

JPA 向导在项目的src文件夹中创建一个META-INF文件夹,并创建persistence.xml文件。打开persistence.xml文件,点击连接选项卡。我们已经在 GlassFish 中创建了 MySQL 数据源(参见配置 GlassFish 中的数据源部分)。在 JTA 数据源字段中输入数据源的 JNDI 名称,jdbc/CourseManagement

packt.book.jee.eclipse.ch7.jpa中创建一个Course实体,如创建 JPA 实体部分所述。在我们创建本项目的 EJB 之前,让我们给本项目添加一个 EJB 特性。在项目上右键单击,选择属性。点击项目特性组,并选择 EJB 模块复选框。将版本设置为最新版本(撰写本文时,最新版本为 3.2)。我们现在将创建之前创建的远程会话 bean 接口的实现类。在CourseManagementMavenEJBs项目上右键单击,并选择新建 | 会话 bean 菜单。创建 EJB 类,以下为详细信息:

字段
Java 包 packt.book.jee.eclipse.ch7.ejb
类名 CourseBean
状态类型 无状态

不要选择任何业务接口,因为我们已经在 CourseManagementMavenEJBClient 项目中创建了业务接口。点击“下一步”。在下一页,选择 CourseBeanRemote。此时 Eclipse 将显示错误,因为 CourseManagementMavenEJBs 不了解 CourseManagementMavenEJBClient,它包含 CourseBeanRemote 接口,该接口在 EJB 项目中的 CourseBean 中使用。在 CourseManagementMavenEJBs 中添加 CourseManagementMavenEJBClient 的 Maven 依赖项(在 pom.xml 中)并在 EJB 类中实现 getCourses 方法应该可以修复编译错误。现在按照本章 创建无状态 EJB 部分的描述完成 CourseBean 类的实现。确保将 EJB 标记为 Remote

@Stateless 
@Remote 
public class CourseBean implements CourseBeanRemote { 
... 
} 

让我们使用 Maven 创建一个用于课程管理的 Web 应用程序项目。创建一个具有以下详细信息的 Maven 项目:

字段
组 ID packt.book.jee.eclipse.ch7.maven
艺术品 ID CourseManagementMavenWebApp
版本 1
打包 war

要在此项目中创建 web.xml,请右键单击项目并选择 Java EE

工具 | 生成部署描述符占位符。web.xml 文件将在 src/main/webapp/WEB-INF 文件夹中创建。打开 web.xml 并添加 JSF 的 servlet 定义和映射(参见本章的 创建 JSF 和托管 Bean 部分)。在 CourseManagementMavenWebApp 项目的 pom.xml 中添加 CourseManagementMavenEJBClient 项目和 javax.javaee-api 的依赖项,以便 Web 项目能够访问共享项目中声明的 EJB 业务接口以及 EJB 注解。

现在,让我们在 Web 项目中创建一个 CourseJSFBean 类,如本章 创建 JSF 和托管 Bean 部分所述。请注意,这将引用 EJB 的远程接口,如下所示:

@ManagedBean(name="Course") 
public class CourseJSFBean { 
  @EJB 
  CourseBeanRemote courseBean; 

  public List<CourseDTO> getCourses() { 
    return courseBean.getCourses(); 
  } 
} 

按照本章 创建 JSF 和托管 Bean 部分的描述,在 webapp 文件夹中创建 course.xhtml

现在,让我们创建一个具有以下详细信息的 CourseManagementMavenEAR 项目:

字段
组 ID packt.book.jee.eclipse.ch7.maven
艺术品 ID CourseManagementMavenEAR
版本 1
打包 ear

您必须在打包文件中键入 ear;下拉列表中没有 ear 选项。将 webejb 和客户端项目的依赖项添加到 pom.xml 中,如下所示:

  <dependencies> 
    <dependency> 
      <groupId>packt.book.jee.eclipse.ch7.maven</groupId> 
      <artifactId>CourseManagementMavenEJBClient</artifactId> 
      <version>1</version> 
    <type>jar</type> 
    </dependency> 
    <dependency> 
      <groupId>packt.book.jee.eclipse.ch7.maven</groupId> 
      <artifactId>CourseManagementMavenEJBs</artifactId> 
      <version>1</version> 
      <type>ejb</type> 
    </dependency> 
    <dependency> 
      <groupId>packt.book.jee.eclipse.ch7.maven</groupId> 
      <artifactId>CourseManagementMavenWebApp</artifactId> 
      <version>1</version> 
      <type>war</type> 
    </dependency> 
  </dependencies> 

确保正确设置每个依赖项的 <type>。您还需要更新任何名称更改的 JNDI URL。

Maven 没有内置支持来打包 EAR。然而,有一个针对 EAR 的 Maven 插件。您可以在maven.apache.org/plugins/maven-ear-plugin/maven.apache.org/plugins/maven-ear-plugin/modules.html找到此插件的详细信息。我们需要将其添加到我们的pom.xml中并配置其参数。我们的 EAR 文件将包含 EJB 项目的 JAR 文件、客户端项目以及 Web 项目的 WAR 文件。右键点击 EAR 项目的pom.xml,选择 Maven | 添加插件。在过滤器框中输入ear,并在 maven-ear-plugin 下选择最新插件版本。确保您还安装了 maven-acr-plugin 插件。在pom.xml的详细信息中配置 EAR 插件,如下所示:

<build> 
  <plugins> 
    <plugin> 
       <groupId>org.apache.maven.plugins</groupId> 
       <artifactId>maven-acr-plugin</artifactId> 
       <version>1.0</version> 
       <extensions>true</extensions> 
    </plugin> 

    <plugin> 
      <groupId>org.apache.maven.plugins</groupId> 
      <artifactId>maven-ear-plugin</artifactId> 
      <version>2.10</version> 
      <configuration> 
         <version>6</version> 
      <defaultLibBundleDir>lib</defaultLibBundleDir> 
      <modules> 
      <webModule> 
      <groupId>packt.book.jee.eclipse.ch7.maven</groupId> 
      <artifactId>CourseManagementMavenWebApp</artifactId> 
      </webModule> 
      <ejbModule> 
      <groupId>packt.book.jee.eclipse.ch7.maven</groupId> 
      <artifactId>CourseManagementMavenEJBs</artifactId> 
      </ejbModule> 
      < jarModule > 
      <groupId>packt.book.jee.eclipse.ch7.maven</groupId> 
      <artifactId>CourseManagementMavenEJBClient</artifactId> 
      </ jarModule > 
      </modules> 
      </configuration> 
    </plugin> 
    </plugins> 
  </build> 

修改pom.xml后,有时 Eclipse 可能会显示以下错误:

Project configuration is not up-to-date with pom.xml. Run Maven->Update Project or use Quick Fix... 

在这种情况下,右键点击项目并选择 Maven | 更新项目。

在本节中我们创建的最后一个项目是CourseManagement,它将成为所有其他 EJB 项目的容器项目。当此项目安装时,它应该构建并安装所有包含的项目。创建一个具有以下详细信息的 Maven 项目:

字段
组 ID packt.book.jee.eclipse.ch7.maven
艺术品 ID CourseManagement
版本 1
打包 Pom

打开pom.xml并点击“概览”选项卡。展开“模块”组,并将所有其他项目作为模块添加。以下模块应列在pom.xml中:

  <modules> 
    <module>../CourseManagementMavenEAR</module> 
    <module>../CourseManagementMavenEJBClient</module> 
    <module>../CourseManagementMavenEJBs</module> 
    <module>../CourseManagementMavenWebApp</module> 
  </modules> 

右键点击CourseManagement项目,选择运行方式 | Maven 安装。这将构建所有 EJB 项目,并在CourseManagementMavenEAR项目的目标文件夹中创建一个 EAR 文件。您可以从 GlassFish 的管理控制台部署此 EAR 文件,或者您可以在 Eclipse 的“服务器视图”中右键点击配置的 GlassFish 服务器,选择“添加和移除...”选项,并在 Eclipse 内部部署 EAR 项目。浏览到http://localhost:8080/CourseManagementMavenWebApp/course.xhtml以查看由course.xhtml JSF 页面显示的课程列表。

摘要

EJB 非常适合在 Web 应用程序中编写业务逻辑。它们可以作为 JSF、servlet 或 JSP 等 Web 界面组件与 JDO 等数据访问对象之间的完美桥梁。EJB 可以跨多个 JEE 应用程序服务器分布(这可以提高应用程序的可伸缩性),并且其生命周期由容器管理。EJB 可以轻松注入到管理对象中,或者可以使用 JNDI 查找。

Eclipse JEE 使得创建和消费 EJB 非常容易。就像我们看到的如何在 Eclipse 中配置和管理 Tomcat 一样,JEE 应用程序服务器,如 GlassFish,也可以在 Eclipse 中管理。

在下一章中,我们将学习如何使用 Spring MVC 创建 Web 应用程序。尽管 Spring 不是 JEE 的一部分,但它是一个流行的框架,用于在 JEE Web 应用程序中实现 MVC 模式。Spring 还可以与许多 JEE 规范协同工作。

第八章:使用 Spring MVC 创建 Web 应用程序

在上一章中,我们学习了如何使用 EJBs 创建 JEE 应用程序。在这一章中,我们将稍微偏离核心 JEE 规范,学习 Spring MVC。

虽然这本书是关于 JEE 和 Eclipse 的,Spring MVC 不是 JEE 的一部分,但了解 Spring MVC 框架是值得的。Spring MVC 是一个非常流行的用于创建 Web 应用的框架,并且可以与其他 JEE 技术(如 servlet、JSP、JPA 和 EJBs)一起使用。

JEE 默认支持 MVC,如果你使用 JSF。有关详细信息,请参阅第二章中的Java 服务器端面创建一个简单的 JEE Web 应用程序。然而,JSF 和 Spring MVC 的设计有所不同。JSF 是一个基于组件的 MVC 框架。它被设计成用户界面设计师可以通过组装由 JSF 提供或自定义开发的可重用组件来创建页面。Spring MVC 是一个基于请求-响应的 MVC 框架。如果你熟悉编写 JSP 或 servlet,那么 Spring MVC 将比 JSF 更容易使用。你可以在 Ed Burns 的www.oracle.com/technetwork/articles/java/mvc-2280472.html上找到关于基于组件的 MVC(由 JSF 实现)和基于请求-响应的 MVC(由 Spring MVC 实现)的良好描述。MVC 的 JSR 371 原本应该是 JEE 8 的一部分,但这个 JSR 后来从 JEE 8 规范中撤回。你可以在www.mvc-spec.org/上找到更多关于 JSR 371(也称为 MVC 1.0)的信息。

在我们了解 Spring MVC 是如何工作之前,我们需要了解 MVC 框架是什么。MVC代表模型-视图-控制器。我们将在 Java Web 应用程序的上下文中引用 MVC 框架,尽管应该在这里提到,MVC 模式也常用于桌面应用程序:

  • 模型:模型包含视图用于创建输出的数据。在我们这本书中跟随的示例中,课程管理应用程序,如果你有一个包含要在页面上显示的课程信息的Course类,那么这个Course对象可以被称为模型。

    MVC 的一些定义也包括在模型层实现业务逻辑的类。例如,一个CourseService类,它接受一个Course对象并调用CourseDAOCourse保存到数据库中,也可以被认为是模型层的一部分。

  • 视图:视图是显示给用户的页面。一个显示课程列表的 JSP 可以被认为是视图层的一部分。视图持有模型对象的引用,并使用它包含的数据来创建用户在浏览器中看到的页面。

  • 控制器:控制器是模型和视图之间的粘合剂。它处理来自网络客户端(例如,浏览器)的请求/操作,调用模型来处理业务逻辑,并将模型对象提供给视图以创建要返回给客户端的页面(用户界面)。控制器可以是 servlet,如 JSF 的情况,或者可以是 POJO(如 Spring MVC 的情况)。当控制器是 POJO 时,通常由DispatcherServlet调用。DispatcherServlet是一个接收请求并将其调度到配置中的一个控制器上的 servlet。我们将在本章后面看到这个示例。

MVC 提供了关注点的分离;也就是说,用户界面和业务逻辑的代码是分开的。正因为如此,UI 和业务层可以很大程度上独立地进行修改。当然,由于 UI 通常显示由业务层生成的数据,因此可能并不总是能够独立于其他层对每一层进行修改。具备适当技能的开发者可以独立地对每一层进行工作。UI 专家无需过分担心业务层的实现方式,反之亦然。

在本章中,我们将涵盖以下主题:

  • Spring 依赖注入简介

  • 配置 Spring Bean 并将它们注入到应用程序中

  • 使用 Eclipse 插件和 JEE 规范(如 JDBC、JPA 和 JSP)创建 Spring MVC 应用程序

依赖注入

Spring MVC 是 Spring 框架整体的一部分。Spring 框架的核心特性是依赖注入DI)。Spring 框架的几乎所有其他特性都使用了 DI。由依赖注入框架管理的对象不是直接在代码中实例化的(例如,使用new运算符)。我们可以称它们为管理对象。这些对象是由 DI 框架(如 Spring)创建的。因为这些对象是由框架创建的,所以框架在决定如何设置对象中的值以及从哪里获取它们方面具有更多的灵活性。例如,你的数据访问对象DAO)类可能需要一个数据库连接工厂对象的实例。然而,你不需要在 DAO 类中实例化它,你只需告诉 DI 框架,当它实例化 DAO 时,它必须设置连接池工厂成员变量的值。当然,连接池工厂的参数必须在某处进行配置,并且为 DI 框架所知。

当一个类实例化另一个类时,它们之间存在紧密的依赖关系。如果你希望独立于其他类测试类,这种设计可能会成为问题。例如,你可能想测试一个具有业务逻辑的类,但它也引用了一个 DAO,而 DAO 又依赖于一个 JDBC 连接对象。当你测试第一个类时,你必须实例化 DAO 并配置连接池。正如我们在第五章“单元测试”中看到的,单元测试应该能够在没有任何外部依赖的情况下运行。实现这一目标的一种方法是通过使用 DI。我们不是实例化 DAO 类,而是让我们的类引用由 DAO 实现的接口,并在运行时由 DI 框架注入该接口的实现。当你对这个类进行单元测试时,可以配置 DI 框架注入一个实现了所需接口的模拟对象。因此,DI 使得对象之间能够实现松耦合。

Spring 中的依赖注入

因为依赖注入(DI)是 Spring 框架的核心,让我们花些时间来了解它在 Spring 中的工作方式。为此,我们将创建一个独立的应用程序。创建一个简单的 Maven 项目。为 Spring 框架添加以下依赖项:

    <dependency> 
      <groupId>org.springframework</groupId> 
      <artifactId>spring-context</artifactId> 
      <version>5.0.5.RELEASE</version> 
    </dependency> 

将前面的版本号替换为 Spring 的最新版本。由 Spring 的 DI 容器管理的类被称为 bean 或组件。你可以要么在 XML 文件中声明 bean,要么在类上使用注解。在本章中,我们将使用注解。然而,尽管我们使用了注解,我们仍需要在 XML 文件中指定最小配置。因此,在你的项目src/main/resource文件夹中创建一个 XML 文件,并将其命名为context.xml。我们在src/main.resource文件夹中创建此文件的原因是,此文件夹中的文件将在类路径中可用。接下来,将以下内容添加到context.xml文件中:

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context" 
   xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd"> 
        <context:component-scan base-package="packt.jee.eclipse.spring"/> 
</beans> 

通过使用<context:component-scan>标签,我们告诉 Spring 框架扫描base-package文件夹,然后查找带有@Component注解的类,并将它们识别为受管理的类,以便在注入依赖项时可以使用。在前面的例子中,packt.jee.eclipse.spring包(及其子包)中的所有类都将被扫描以识别组件。

从配置文件中读取的信息必须保存到一个对象中。在 Spring 中,它被保存在ApplicationContext接口的一个实例中。ApplicationContext有不同的实现方式。我们将使用ClassPathXmlApplicationContext类,它在类路径中查找配置 XML 文件。

我们现在将创建两个 Spring 组件。第一个是CourseDAO,第二个是CourseService。尽管我们不会在这些类中编写任何业务逻辑(此示例的目的是了解 Spring 中的 DI 如何工作),但假设CourseDAO可能有访问数据库的代码,而CourseService调用CourseDAO来执行数据库操作。因此,CourseService依赖于CourseDAO。为了使代码简单,我们不会为CourseDAO创建任何接口,而是将直接依赖。按照以下方式创建CourseDAO类:

package packt.jee.eclipse.spring; 

import org.springframework.stereotype.Component; 

@Component 
public class CourseDAO { 

} 

CourseDAO中,我们将没有方法,但如前所述,它可能有访问数据库的方法。@Component将此类标记为由 Spring 管理。现在,创建CourseService类。此类需要一个CourseDAO实例:

package packt.jee.eclipse.spring; 

import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.stereotype.Component; 

@Component 
public class CourseService { 

  @Autowired 
  private CourseDAO courseDAO; 

  public CourseDAO getCourseDAO() { 
    return courseDAO; 
  } 
} 

我们已声明一个名为courseDAO的成员变量,并使用@Autowired进行了注解。这告诉 Spring 在其上下文中查找(CourseDAO类型的)组件并将其分配给courseDAO成员。

我们现在将创建主类。它创建ApplicationContext,获取CourseService豆/组件,调用getCourseDAO方法,然后检查它是否被正确注入。创建SpringMain类:

package packt.jee.eclipse.spring; 

import org.springframework.context.ApplicationContext; 
import org.springframework.context.support.ClassPathXmlApplicationContext; 

public class SpringMain { 

  public static void main (String[] args) { 
    //create ApplicationContext 
    ApplicationContext ctx = new 
     ClassPathXmlApplicationContext("context.xml"); 
    //Get bean 
    CourseService courseService = (CourseService) 
     ctx.getBean("courseService"); 
    //Get and print CourseDAO. It should not be null 
    System.out.println("CourseDAO = " + 
     courseService.getCourseDAO()); 
  } 
} 

我们首先创建一个ClassPathXmlApplicationContext实例。配置 XML 文件作为参数传递给构造函数。然后我们获取courseService豆/组件。注意指定豆名称时的命名约定;它是类名,首字母小写。然后我们获取并打印CourseDAO的值。值不会显示任何有意义的信

在前面的代码中,我们看到了在成员声明处注入对象的一个示例(这也可以称为属性注入)。我们也可以在构造函数中注入此对象:

@Component 
public class CourseService { 

  private CourseDAO courseDAO; 

  @Autowired 
  public CourseService (CourseDAO courseDAO) { 
    this.courseDAO = coueseDAO; 
  } 

  public CourseDAO getCourseDAO() { 
    return courseDAO; 
  } 
} 

注意,@Autowired注解已移动到构造函数中,并且单个构造函数参数是自动注入的。您也可以在设置器中注入对象:

@Component 
public class CourseService { 

  private CourseDAO courseDAO; 

  @Autowired 
  public void setCourseDAO (CourseDAO courseDAO) { 
    this.courseDAO = courseDAO; 
  } 

  public CourseDAO getCourseDAO() { 
    return courseDAO; 
  } 
} 

组件作用域

您可以在 Spring MVC 中指定组件的作用域。默认作用域是单例,这意味着在上下文中将只有一个组件实例。对每个此组件的请求都将使用相同的实例。其他作用域如下:

  • 原型:为每个组件请求提供该类的新实例。

  • 请求:适用于 Web 应用程序。为每个 HTTP 请求创建的组件类的单个实例。

  • 会话:为每个 HTTP 会话创建的组件类的单个实例。用于 Web 应用程序。

  • 全局会话:为全局 HTTP 会话创建的组件类的单个实例。用于 portlet 应用程序。

  • 应用程序:在 Web 应用程序中组件类的单个实例。该实例由该应用程序中的所有会话共享。

有关 Spring 中组件作用域的更多信息,请访问docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-scopes

如果在请求组件时该组件尚未实例化,那么 Spring 将创建该组件的一个实例。在先前的例子中,我们没有指定CourseDAO组件的作用域,因此如果有另一个请求注入CourseDAO,则将注入相同的实例。你可以在@Component注解中指定作用域。如果你想覆盖 Spring 给组件提供的默认名称,也可以指定组件名称。

要查看在没有指定作用域的情况下是否注入了组件的单个实例,让我们更改SpringMain类中的main方法并调用两次getBean方法:

  public static void main (String[] args) { 
    //create ApplicationContext 
    ApplicationContext ctx = new 
     ClassPathXmlApplicationContext("context.xml"); 
    //call and print ctx.getBean first time 
    System.out.println("Course Service 1 - " + 
     ctx.getBean("courseService"));    System.out.println("Course Service 2 - " + 
     ctx.getBean("courseService")); 
  } 

运行应用程序,你应该会看到打印出相同的courseService bean 实例。现在让我们改变CourseService组件的作用域:

@Component 
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 
public class CourseService { 
  //content remains the same 
} 

再次运行应用程序;这次,你应该会看到CourseService组件的不同实例。

当 Spring 遇到@Autowire注解时,它会尝试通过类型查找组件。在先前的例子中,courseDAO被注解为@Autowire。Spring 会尝试找到一个CourseDAO类型的组件;它找到一个CourseDAO的实例并将其注入。但如果上下文中存在该类的多个实例呢?在这种情况下,我们可以使用@Qualifier注解来唯一标识组件。现在让我们创建ICourseDAO接口,该接口将由两个组件实现,即CourseDAOCourseDAO1

public interface ICourseDAO { 
} 

CourseDAO实现了ICourseDAO,并且被唯一限定为"courseDAO"

@Component 
@Qualifier("courseDAO") 
public class CourseDAO implements ICourseDAO { 
} 

CourseDAO1实现了ICourseDAO,并且被唯一限定为"courseDAO1"

@Component 
@Qualifier("courseDAO1") 
public class CourseDAO1 implements ICourseDAO { 
} 

CourseService类中,我们将使用限定符来唯一标识我们想要注入CourseDAO还是CourseDAO1

@Component 
public class CourseService { 

  @Autowired 
  private @Qualifier("courseDAO1") ICourseDAO courseDAO; 

  public ICourseDAO getCourseDAO() { 
    return courseDAO; 
  } 
} 

限定符也可以在方法参数中指定,例如:

@Autowired 
public void setCourseDAO (@Qualifier("courseDAO1") ICourseDAO 
 courseDAO) { 
  this.courseDAO = courseDAO; 
} 

现在运行应用程序。你应该会在控制台看到打印出一个CourseDAO1实例。

我们已经介绍了 Spring 中依赖注入的基本知识。然而,Spring 提供的依赖注入选项和功能比我们在这里介绍的要多得多。在本章中,我们将根据需要看到更多的 DI 功能。

有关 Spring 中依赖注入的更多信息,请访问docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-dependencies

安装 Spring Tool Suite

Spring Tool SuiteSTS)是 Eclipse 中用于创建 Spring 应用的一组工具。它既可以作为 Eclipse JEE 现有安装的插件安装,也可以独立安装。STS 的独立版本也包含在 Eclipse EE 中,因此 STS 也提供了所有 Eclipse Java EE 开发功能。您可以从spring.io/tools下载 STS。由于我们已安装 Eclipse EE,我们将以插件的形式安装 STS。在 Eclipse Marketplace 中安装 STS 插件的最简单方法是选择“帮助 | Eclipse Marketplace...”菜单。

在查找框中输入Spring Tool Suite,然后点击“Go”按钮:

图 8.1:在 Eclipse Marketplace 中搜索 STS

点击“安装”。下一页将显示将要安装的 STS 功能。点击“确认”以安装所选功能。

创建 Spring MVC 应用

Spring MVC 可用于创建 Web 应用。它提供了一个简单的框架,将传入的 Web 请求映射到处理类(控制器)并创建动态 HTML 输出。它是 MVC 模式的实现。控制器和模型作为 POJO 创建,视图可以使用 JSP、JSTL、XSLT 甚至 JSF 创建。然而,在本章中,我们将专注于使用 JSP 和 JSTL 创建视图。

您可以在docs.spring.io/spring/docs/current/spring-framework-reference/web.html找到 Spring 网络文档。

Spring MVC 通过四层处理网络请求:

  • 前端控制器:这是一个配置在web.xml中的 Spring servlet。根据请求 URL 模式,它将请求传递到控制器。

  • 控制器:这些是带有@Controller注解的 POJO。对于您编写的每个控制器,您需要指定控制器预期处理的 URL 模式。子 URL 模式也可以在方法级别指定。我们将在稍后看到这方面的示例。控制器可以访问模型以及 HTTP 请求和响应对象。控制器可以将请求的处理委托给其他业务处理对象,获取结果,并填充模型对象,该对象由 Spring MVC 提供给视图。

  • 模型:这些是数据对象。控制器和视图层可以设置和获取模型对象中的数据。

  • 视图:这些通常是 JSP 页面,但 Spring MVC 也支持其他类型的视图。请参阅 Spring 文档中的视图技术docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-view

我们将通过示例学习本章的 Spring MVC,就像我们在本书的一些其他章节中学习的那样。我们将使用 Spring MVC 创建同一课程管理应用的一部分。该应用将显示课程列表,并提供添加、删除和修改课程的功能。

创建 Spring 项目

首先,确保你在 Eclipse EE 中安装了 STS。从 Eclipse 菜单中选择 File | New | Other,然后选择 Spring | Spring Legacy Project 选项。输入项目名称并选择 Spring MVC Project 模板:

图 8.2:选择 Spring MVC 项目模板

点击下一步。页面将要求你输入顶级包名:

图 8.3:输入顶级包名

无论你输入什么作为顶级包,向导都会将第三个子包作为应用程序名称。当应用程序在服务器上部署时,应用程序名称变为上下文名称。例如,如果你输入的包名为packt.jee.course_management,那么course_management将成为应用程序名称,本地机器上应用程序的基本 URL 将是http://localhost:8080/course_management/

点击完成。这将创建一个包含所需库的 Maven 项目,用于 Spring MVC。

理解 Spring MVC 项目模板创建的文件

让我们检查模板创建的一些文件:

  • src/main/webapp/WEB-INF/web.xml: 在这里声明了一个前端控制器 servlet 以及其他配置:
<!-- Processes application requests --> 
<servlet> 
  <servlet-name>appServlet</servlet-name> 
  <servlet- 
 class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 
  <init-param> 
    <param-name>contextConfigLocation</param-name> 
    <param-value>/WEB-INF/spring/appServlet/servlet- 
     context.xml</param-value> 
  </init-param> 
  <load-on-startup>1</load-on-startup> 
</servlet> 

DispatcherServlet是前端控制器 servlet。它传递上下文(XML)文件的路径以配置 Spring DI。回想一下,在独立 Spring 应用程序中,我们创建了context.xml来配置依赖注入。DispatcherServlet servlet 被映射来处理对此 Web 应用程序的请求。

  • src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml:Spring DI 的上下文配置。此文件中的一些显著配置参数如下:
<annotation-driven /> 

这启用了在类级别配置依赖注入的注解:

<resources mapping="/resources/**" location="/resources/" /> 

静态文件,如 CSS、JavaScript 和图像,可以放置在resources文件夹中(src/main/webapp/resources):

<beans:bean 
 class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 
  <beans:property name="prefix" value="/WEB-INF/views/" /> 
  <beans:property name="suffix" value=".jsp" /> 
</beans:bean> 

这告诉 Spring 使用InternalResourceViewResolver类来解析视图。此 bean 的属性告诉InternalResourceViewResolver类在/WEB-INF/views文件夹中查找视图文件。此外,视图将是 JSP 文件,如后缀属性所示。我们的视图将是src/main/webapp/WEB-INF/views文件夹中的 JSP 文件:

<context:component-scan base-package="packt.jee.course_management" /> 

这告诉 Spring 扫描packt.jee.course_management包及其子包以搜索组件(由@Component注解)。

默认模板还创建了一个控制器和一个视图。控制器类是你在 Spring 项目向导中指定的包中的HomeController(在我们的例子中是packt.jee.course_management)。Spring MVC 中的控制器由调度器 servlet 调用。控制器通过@Controller注解。要映射请求路径到控制器,你使用@RequestMapping注解。让我们看看模板在HomeController类中生成的代码:

@Controller 
public class HomeController { 

  private static final Logger logger = 
   LoggerFactory.getLogger(HomeController.class); 

  /** 
   * Simply selects the home view to render by returning its name. 
   */ 
  @RequestMapping(value = "/", method = RequestMethod.GET) 
  public String home(Locale locale, Model model) { 
    logger.info("Welcome home! The client locale is {}.", locale); 

    Date date = new Date(); 
    DateFormat dateFormat = 
 DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale); 

    String formattedDate = dateFormat.format(date); 

    model.addAttribute("serverTime", formattedDate ); 

    return "home"; 
  } 
} 

home方法被@RequestMapping注解。映射的值是/,这告诉 Dispatcher Servlet 将所有传入的请求发送到这个方法。method属性告诉 Dispatcher 只对 HTTP 请求的GET类型调用home方法。home方法接受两个参数,即LocaleModel;这两个都是在运行时由 Spring 注入的。@RequestMapping注解还告诉 Spring 在调用home方法时插入任何依赖项,因此localemodel会自动注入。

该方法本身并没有做什么;它获取当前的日期和时间,并将其设置为 Model 的属性。在 Model 中设置的任何属性都对 View(JSP)可用。该方法返回一个字符串,"home"。这个值被 Spring MVC 用来解析要显示的 View。我们在之前的servlet-context.xml中看到的InternalResourceViewResolver将这个值解析为/WEB-INF/views文件夹中的home.jsphome.jsp<body>标签中有以下代码:

<P>  The time on the server is ${serverTime}. </P> 

serverTime变量来自HomeControllerhome方法中设置的 Model 对象。

要运行此项目,我们需要在 Eclipse 中配置一个服务器并将此项目添加到服务器中。请参考第二章中的在 Eclipse 中配置 Tomcat在 Tomcat 中运行 JSP部分,创建一个简单的 JEE Web 应用程序

一旦你配置了 Tomcat 并将项目添加到其中,启动服务器。然后,在项目上右键单击并选择 Run As | Run on Server。你应该在 Eclipse 的内部浏览器中看到一个带有时间戳的 hello 消息。浏览器地址栏中的 URL 应该是http://localhost:8080/course_management/,假设 Tomcat 部署在端口8080,上下文名称(从顶级包名派生)是course_management。如果你想更改默认的上下文名称或删除上下文,即以根上下文部署应用程序,那么打开项目属性(在项目上右键单击并选择 Properties)并转到 Web Project Settings。你可以从这个页面更改上下文根名称或删除它:

图片

图 8.4:上下文根设置

对于我们的课程管理应用程序,我们不需要HomeController类或home.jsp,所以你可以继续删除这些文件。

使用 JDBC 构建 Spring MVC 应用程序

在本节中,我们将使用 Spring MVC 和 JDBC 构建课程管理应用程序的一部分。该应用程序将显示课程列表以及添加、删除和修改课程的选项。我们将继续使用上一节中创建的项目。随着我们的进行,我们将学习使用 JDBC 作为数据访问的许多 Spring 特性。

首先,我们将配置我们的数据源。我们将使用与第四章,创建 JEE 数据库应用创建数据库模式部分创建的相同 MySQL 数据库。

配置数据源

在春季,您可以在 Java 代码或 XML 配置(上下文)文件中配置 JDBC 数据源。在我们看到如何配置数据源之前,我们需要在 Maven 中添加一些依赖项。在本章中,我们将使用 Apache 的 Commons DBCP 组件来实现连接池(回想一下,在第四章,创建 JEE 数据库应用中,我们选择了 Hikari 连接池)。有关 Apache DBCP 的详细信息,请访问commons.apache.org/proper/commons-dbcp/。除了添加 Apache DBCP 的依赖项之外,我们还需要添加 Spring JDBC 和 MySQL JDBC 驱动程序的依赖项。将以下依赖项添加到项目的pom.xml文件中:

<!-- Spring JDBC --> 
<dependency> 
  <groupId>org.springframework</groupId> 
  <artifactId>spring-jdbc</artifactId> 
  <version>${org.springframework-version}</version> 
</dependency> 

<!-- Apache DBCP --> 
<dependency> 
  <groupId>commons-dbcp</groupId> 
  <artifactId>commons-dbcp</artifactId> 
  <version>1.4</version> 
</dependency> 

<!-- MySQL --> 
<dependency> 
  <groupId>mysql</groupId> 
  <artifactId>mysql-connector-java</artifactId> 
  <version>8.0.9-rc</version> 
</dependency> 

如果您想在 Java 代码中创建数据源,可以按照以下方式操作:

DriverManagerDataSource dataSource = new 
 DriverManagerDataSource(); 
dataSource.setDriverClassName("com.mysql.jdbc.Driver"); 
dataSource.setUrl("jdbc:mysql://localhost:3306/course_management"); 
dataSource.setUsername("your_user_name"); 
dataSource.setPassword("your_password"); 

然而,我们将使用 XML 配置文件来配置数据源。打开servlet-context.xml(您可以在src/main/webapp/WEB-INF/spring/appServlet文件夹中找到它)并添加以下 bean:

<beans:bean id="dataSource" 
  class="org.apache.commons.dbcp.BasicDataSource" destroy- method="close"> 
    <beans:property name="driverClassName" 
 value="com.mysql.jdbc.Driver"/>  <beans:property name="url" 
 value="jdbc:mysql://localhost:3306/course_management" /> 
  <beans:property name="username" value="your_user_name"/> 
  <beans:property name="password" value="your_password"/> 
</beans:bean> 

如果您想知道bean是什么意思,它与我们在本章早期示例中创建的组件相同。我们到目前为止已经使用注解创建了一个组件,但组件和 bean 也可以在 XML 文件中声明。实际上,这是在早期版本中使用的做法,直到 Spring 添加了对注解的支持。在现实世界的应用程序中,您可能希望在配置文件中指定数据库密码之前对其进行加密。在将密码发送到数据库之前解密密码的一种方法是为数据源创建一个包装类(在前面的示例中,为org.apache.commons.dbcp.BasicDataSource创建一个包装器)并重写setPassword方法,在那里您可以解密密码。

如果您想将数据库连接参数与 Spring 配置分开,则可以使用一个properties文件。Spring 提供了一种一致的方式来访问资源,例如properties文件。就像您可以使用http协议前缀访问 Web URL 或使用file协议前缀访问文件 URL 一样,Spring 允许您使用classpath前缀访问类路径中的资源。例如,如果我们创建一个jdbc.properties文件并将其保存在类路径中的一个文件夹中,那么我们可以将其访问为classpath:jdbc.properties

访问 docs.spring.io/spring/docs/current/spring-framework-reference/core.html#resources 获取有关使用 Spring 访问资源的详细信息。Spring 资源 URL 格式可用于配置文件或期望资源位置的 Spring API 中。

Spring 还提供了一个方便的标签来在上下文配置 XML 中加载属性文件。您可以使用 ${property_name} 语法在配置 XML 中访问属性文件中的属性值。

在本例中,我们将数据库连接属性移动到一个文件中。在 src/main/resources 文件夹中创建 jdbc.properties。Maven 使此文件夹可用于类路径,因此我们可以使用 XML 配置文件中的 Spring 资源格式来访问它:

jdbc.driverClassName=com.mysql.jdbc.Driver 
jdbc.url=jdbc:mysql://localhost:3306/course_management 
jdbc.username=your_user_name 
jdbc.password=your_password 

我们将使用 property-placeholder 标签从 servlet-context.xml 中加载此 properties 文件:

<context:property-placeholder location="classpath:jdbc.properties"/> 

注意,property 文件的存放位置是使用 Spring 资源格式指定的。在这种情况下,我们要求 Spring 在类路径中查找 jdbc.properties 文件。此外,因为 src/main/resources 文件夹位于类路径中(我们在这里保存了 jdbc.properties),它应该由 Spring 加载。

现在让我们修改 servlet-context.xml 中的 datasource bean 声明,以使用属性值:

<beans:bean id="dataSource" 
  class="org.apache.commons.dbcp.BasicDataSource" destroy- method="close"> 
    <beans:property name="driverClassName" 
 value="${jdbc.driverClassName}"/> 
  <beans:property name="url" value="${jdbc.url}" /> 
  <beans:property name="username" value="${jdbc.username}"/> 
  <beans:property name="password" value="${jdbc.password}"/> 
</beans:bean> 

注意,property-placeholder 标签的顺序以及属性的使用位置并不重要。Spring 在将属性引用替换为其值之前,会加载整个 XML 配置文件。

使用 Spring JDBCTemplate 类

Spring 提供了一个名为 JDBCTemplate 的实用工具类,它使得使用 JDBC 执行许多操作变得容易。它提供了执行 SQL 语句、将查询结果映射到对象(使用 RowMapper 类)、在数据库操作结束时关闭数据库连接等方便的方法。

访问 docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#jdbc 获取有关 JDBCTemplate 的更多信息。

在我们编写任何数据访问代码之前,我们将创建一个 数据传输对象DTO),名为 CourseDTO,它将仅包含描述一个 Course 的成员以及它们的设置和获取器。在 packt.jee.course_management.dto 包中创建 CourseDTO。此类实例将用于在不同层之间传输我们的应用程序中的数据:

public class CourseDTO { 
  private int id; 
  private int credits; 
  private String name; 

  //skipped setters and getters to save space 
} 

我们现在将创建一个简单的 DAO,它将使用 JdbcTemplate 类执行查询以获取所有课程。在 packt.jee.course_management.dao 包中创建 CourseDAO 类。使用 @Repository 注解 CourseDAO 类。类似于 @Component@Repository 注解将类标记为 Spring DI 容器管理的类。

根据 Spring 文档(docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-stereotype-annotations),@Component 是一个通用的注解,用于标记一个类为 Spring 容器管理,而 @Repository@Controller 是更具体的注解。更具体的注解有助于识别用于特定处理的类。建议为 DAO 使用 @Repository 注解。

CourseDAO 需要一个 JdbcTemplate 类的实例来执行查询和其他 SQL 语句。JdbcTemplate 在使用之前需要一个 DataSource 对象。我们将在 CourseDAO 的一个方法中注入 DataSource

@Repository 
public class CourseDAO { 

  private JdbcTemplate jdbcTemplate; 

  @Autowired 
  public void setDatasource (DataSource dataSource) { 
    jdbcTemplate = new JdbcTemplate(dataSource); 
  } 
} 

我们在 servlet-context.xml 中配置的 datasource 将在创建 CourseDAO 对象时由 Spring 注入。

我们现在将编写一个获取所有课程的方法。JdbcTemplate 类有一个 query 方法,允许你指定 RowMapper,其中你可以将查询中的每一行映射到一个 Java 对象:

public List<CourseDTO> getCourses() { 
  List<CourseDTO> courses = jdbcTemplate.query("select * from 
  course", 
    new CourseRowMapper()); 

  return courses; 
} 

public static final class CourseRowMapper implements 
 RowMapper<CourseDTO> { 
  @Override 
  public CourseDTO mapRow(ResultSet rs, int rowNum) throws 
   SQLException { 
    CourseDTO course = new CourseDTO(); 
    course.setId(rs.getInt("id")); 
    course.setName(rs.getString("name")); 
    course.setCredits(rs.getInt("credits")); 
    return course; 
  } 
} 

getCourses 方法中,我们将执行一个静态查询。稍后,我们将看到如何执行参数化查询。JDBCTemplate.query 方法的第二个参数是 RowMapper 接口的一个实例。我们创建了一个静态内部类 CourseRowMapper,它实现了 RowMapper 接口。我们重写了 mapRow 方法,该方法在 ResultSet 的每一行上被调用,然后我们从传入的参数中的 ResultSet 创建/映射 CourseDTO 对象。该方法返回一个 CourseDTO 对象。JdbcTemplate.query 的结果是 CourseDTO 对象的列表。请注意,query 方法还可以返回其他 Java 集合对象,例如 Map

现在,让我们编写一个方法来向表中添加课程:

public void addCourse (final CourseDTO course) { 
  KeyHolder keyHolder = new GeneratedKeyHolder(); 
  jdbcTemplate.update(new PreparedStatementCreator() { 

    @Override 
    public PreparedStatement createPreparedStatement(Connection 
     con) 
        throws SQLException { 
      String sql = "insert into Course (name, credits) values 
       (?,?)";      PreparedStatement stmt = con.prepareStatement(sql, new 
       String[] {"id"}); 
      stmt.setString(1, course.getName()); 
      stmt.setInt(2, course.getCredits()); 
      return stmt; 
    } 
  }, keyHolder); 

  course.setId(keyHolder.getKey().intValue()); 
} 

当我们添加或插入一个新的课程时,我们希望获取新记录的 ID,它是自动生成的。此外,我们希望使用预处理语句来执行 SQL。因此,首先我们为自动生成的字段创建 KeyHolderJdbcTemplateupdate 方法有多个重载版本。我们使用一个接受 PreparedStatementCreatorKeyHolder 的版本。我们创建一个 PreparedStatementCreator 的实例并重写 createPreparedStatement 方法。在这个方法中,我们创建一个 JDBC PreparedStatement 并返回它。一旦更新方法成功执行,我们通过调用 KeyHoldergetKey 方法来检索自动生成的值。

更新或删除课程的这些方法类似:

public void updateCourse (final CourseDTO course) { 
  jdbcTemplate.update(new PreparedStatementCreator() { 
    @Override 
    public PreparedStatement createPreparedStatement(Connection 
     con) 
        throws SQLException { 
      String sql = "update Course set name = ?, credits = ? where 
       id = ?"; 
      PreparedStatement stmt = con.prepareStatement(sql); 
      stmt.setString(1, course.getName()); 
      stmt.setInt(2, course.getCredits()); 
      stmt.setInt(3, course.getId()); 
      return stmt; 
    } 
  }); 
} 

public void deleteCourse(final int id) { 
  jdbcTemplate.update(new PreparedStatementCreator() { 
    @Override 
    public PreparedStatement createPreparedStatement(Connection 
     con) 
        throws SQLException { 
      String sql = "delete from Course where id = ?"; 
      PreparedStatement stmt = con.prepareStatement(sql); 
      stmt.setInt(1, id); 
      return stmt; 
    } 
  }); 
} 

我们需要向 CourseDAO 添加一个额外的方法,以便根据 ID 获取课程的详细信息:

public CourseDTO getCourse (int id) { 
  String sql = "select * from course where id = ?"; 
  CourseDTO course = jdbcTemplate.queryForObject(sql, new 
   CourseRowMapper(), id); 
  return course; 
}

queryForObject方法针对给定查询返回单个对象。在这里我们使用参数化查询,并将参数作为queryForObject方法的最后一个参数传递。此外,我们使用CourseRowMapper将此查询返回的单行映射到CourseDTO。请注意,您可以向queryForObject方法传递可变数量的参数,尽管在这种情况下,我们传递了一个单一值,即 ID。

我们现在在CourseDAO类中拥有了访问Course数据的所有方法。

有关在 Spring 中使用 JDBC 进行数据访问的详细讨论,请参阅docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#jdbc

创建 Spring MVC 控制器

我们现在将创建Controller类。在 Spring MVC 中,控制器映射到请求 URL 并处理与 URL 模式匹配的请求。在控制器中,在方法级别指定用于匹配传入请求的请求 URL。然而,可以在Controller类级别指定更通用的请求映射,并且可以在方法级别指定相对于类级别的特定 URL。

packt.jee.course_management.controller包中创建一个名为CourseController的类。使用@Controller注解它。@Controller注解是@Component类型,允许 Spring 框架特别识别该类为控制器。在CourseController中添加获取课程的方法:

@Controller 
public class CourseController { 
  @Autowired 
  CourseDAO courseDAO; 

  @RequestMapping("/courses") 
  public String getCourses (Model model) { 
    model.addAttribute("courses", courseDAO.getCourses()); 
    return "courses"; 
  } 
} 

CourseDAO实例是自动装配的;也就是说,它将由 Spring 注入。我们添加了getCourses方法,该方法接受一个 Spring Model 对象。可以使用此 Model 对象在视图和控制器之间共享数据。因此,我们在 Model 中添加了一个名为courses的属性,并将通过调用courseDAO.getCourses获得的课程列表分配给该属性。这个列表可以在视图 JSP 中作为courses变量使用。我们使用@RequestMapping注解了这个方法。此注解将传入请求 URL 映射到控制器方法。在这种情况下,我们表示任何以/courses开头的相对根请求都应该由这个控制器中的getCourses方法处理。我们将在CourseController中添加更多方法,并讨论我们可以传递给@RequestMapping注解的一些参数,但首先让我们创建一个视图来显示课程列表。

创建视图

我们已经为Course创建了一个数据访问对象和一个控制器。让我们看看我们如何从视图中调用它们。Spring 中的视图通常是 JSP。在src/main/webapp/WEB-INF/views文件夹中创建一个 JSP(命名为courses.jsp)。这是我们在servlet-context.xml中配置的文件夹,用于存放 Spring 视图文件。

courses.jsp中添加 JSTL 标签库:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 

显示课程的标记代码非常简单;我们使用courses变量,该变量由CourseController.getCourses方法在模型中提供,并使用 JSTL 表达式显示值:

<table> 
  <tr> 
    <th>Id</th> 
    <th>Name</th> 
    <th>Credits</th> 
    <th></th> 
  </tr> 
  <c:forEach items="${courses}" var="course"> 
    <tr> 
      <td>${course.id}</td> 
      <td>${course.name}</td> 
      <td>${course.credits}</td> 
    </tr> 
  </c:forEach> 
</table> 

回想一下,courses是一个CourseDTO类型的对象列表。CourseDTO的成员在forEach标签中访问以显示实际值。

不幸的是,我们无法像本书中之前那样从 Eclipse 中运行这个页面,即通过在项目或页面上右键单击并选择“运行”|“在服务器上运行”。如果你尝试运行项目(在项目上右键单击并选择运行菜单),那么 Eclipse 将尝试打开http://localhost:8080/course_management/ URL,因为我们没有任何起始页面(index.htmlindex.jsp),我们将得到 HTTP 404 错误。我们无法通过右键单击并选择运行选项来运行页面的原因是 Eclipse 试图打开http://localhost:8080/course_management/WEB-INF/views/courses.jsp,这失败了,因为WEB-INF中的文件无法从服务器外部访问。另一个原因,或者更确切地说,主要原因,是这个 URL 不会工作是因为在web.xml中,我们已经将所有请求映射到由 Spring 框架的DispatcherServlet处理的请求,并且它找不到适合请求 URL 的映射。要运行应用程序,请在浏览器中打开 URLhttp://localhost:8080/course_management/courses

使用@ModelAttribute 映射数据

在本节中,我们将实现插入新课程的功能。在这个过程中,我们将更多地了解将请求映射到方法和将请求参数映射到方法参数。

在上一节中,我们使用一个方法getCourses实现了CourseController。现在我们将添加插入新课程的方法。要添加一门课程,我们首先需要显示一个包含用户输入表单的视图。当用户实际提交表单时,表单数据应该被发送到处理将数据插入数据库的 URL。因此,这里涉及两个请求:第一个是显示添加课程表单,第二个是处理从表单发送的数据。我们将第一个请求称为addCourse,第二个请求称为doAddCourse。让我们首先创建用户界面。创建一个新的 JSP 页面并命名为addCourse.jsp。将以下标记添加到页面的body部分(跳过了 JSTL 和其他头部声明以节省空间):

  <h2>Add Course</h2> 
  <c:if test="${not empty error}"> 
    <span style="color:red;"> 
      ${error}<br> 
    </span> 
  </c:if> 

  <c:set var="actionPath" 
   value="${pageContext.request.contextPath}/doAddCourse"/> 
  <form method="post" action="${actionPath}"> 
    <table> 
      <tr> 
        <td>Course Name:</td> 
        <td><input type="text" name="name" value="${course.name}"> 
         </td> 
      </tr> 
      <tr> 
        <td>Credits:</td> 
        <td><input type="text" name="credits" 
         value="${course.credits}"> </td> 
      </tr> 
      <tr> 
        <td colspan="2"> 
        <button type="submit">Submit</button> 
        </td> 
      </tr> 
    </table> 
    <input type="hidden" name="id" value="${course.id}"> 
  </form> 

页面期望控制器提供course变量。在表单体中,它将课程值分配到适当的输入字段;例如,${course.name}值分配给课程名称的文本输入。表单将数据发送到"${pageContext.request.contextPath}/doAddCourse" URL。请注意,由于我们的应用程序没有部署在根上下文,我们需要在 URL 中包含上下文名称。

现在让我们添加控制器方法来处理两个添加请求:addCoursedoAddCourse。当发起 addCourse 请求时,我们希望提供显示输入表单的页面。当用户点击提交按钮时,我们希望使用 doAddCourse 请求发送表单数据。打开 CourseController 类并添加以下方法:

  @RequestMapping("/addCourse") 
  public String addCourse (@ModelAttribute("course") CourseDTO 
   course, Model model) { 
    if (course == null) 
      course = new CourseDTO(); 
    model.addAttribute("course", course); 
    return "addCourse"; 
} 

addCourse 方法使用 @RequestMapping 注解配置,以处理以(相对于上下文根)以 "/addCourse" 开头的请求 URL。如果之前已将 course 属性添加到 Model 中,那么我们希望将此对象作为参数传递给此函数。使用 @ModelAttribute,我们告诉 Spring 框架如果存在,则注入名为 course 的 Model 属性并将其分配给名为 course 的参数;否则,传递 null。在第一次请求的情况下,Model 不会有 course 属性,因此它将是 null。在随后的请求中,例如,当用户在表单中输入的数据(用于添加课程)无效并且我们想要重新显示页面时,Model 将具有 course 属性。

我们现在将为 '/doAddCourse' 请求创建一个处理器方法。这是一个当用户在 addCourse.jsp 中提交表单时发送的 POST 请求(参考之前讨论的表单及其 POST 属性):

  @RequestMapping("/doAddCourse")  
  public String doAddCourse (@ModelAttribute("course") CourseDTO 
   course,  Model model) { 
    try { 
      coursesDAO.addCourse(course); 
    } catch (Throwable th) { 
      model.addAttribute("error", th.getLocalizedMessage()); 
      return "addCourse"; 
    } 
    return "redirect:courses"; 
  } 

doAddCourse 方法还要求 Spring 将名为 course 的 Model 属性作为第一个参数注入。然后,它使用 CourseDAO 将课程添加到数据库中。如果发生错误,它将返回 addCourse 字符串,Spring MVC 将再次显示 addCourse.jsp。如果课程成功添加,则请求将被重定向到 courses,这告诉 Spring 处理并显示 courses.jsp。回想一下,在 servlet-context.xml(位于 src/main/webapp/WEB-INF/spring/appServlet 文件夹中的 Spring 上下文配置文件),我们配置了一个具有 org.springframework.web.servlet.view.InternalResourceViewResolver 类的 bean。这个类扩展了 UrlBasedViewResolver,它理解如何处理带有 redirectforward 前缀的 URL。因此,在 doAddCourse 中,我们将在数据库中保存数据,如果成功,我们将请求重定向到 courses,它将在处理 courses.jsp 后显示课程列表。

到目前为止,如果您想测试应用程序,请浏览到 http://localhost:8080/course_management/addCourse。输入课程名称和学分,然后点击提交。这应该会带您到课程页面并显示课程列表。

注意,Spring MVC 在将表单字段名称映射到 Model(在这种情况下,CourseDTO)中的对象属性时,会查看 Model 中的对象属性。例如,表单字段 name 被映射到 CourseDTO.name 属性。因此,请确保表单字段的名称和类(其对象被添加到 Model 中)中的属性名称相同。

使用 @RequestMapping 参数

我们已经看到了如何使用@RequestMapping注解将传入的请求映射到 Controller 方法。到目前为止,我们已经在@RequestMapping中映射了静态 URL 模式。然而,使用@RequestMapping也可以映射参数化 URL(如 REST 中使用的;参见spring.io/understanding/REST)。参数在{}内指定。

让我们添加更新现有课程的功能。在这里,我们只讨论如何为这个功能编写 Controller 方法。完整的代码在您下载本章的示例时可用。

让我们在CourseController中添加以下方法:

  @RequestMapping("/course/update/{id}") 
  public String updateCourse (@PathVariable int id, Model model) { 
    //TODO: Error handling 
    CourseDTO course = coursesDAO.getCourse(id); 
    model.addAttribute("course", course); 
    model.addAttribute("title", "Update Course"); 
    return "updateCourse"; 
  } 

在这里,我们将updateCourse方法映射到以下 URL 模式的请求:/course/update/{id},其中{id}可以是任何现有课程的 ID(数字),或者换句话说,任何整数。为了访问此参数的值,我们在参数中使用了@PathVariable注解。

使用 Spring 拦截器

Spring 拦截器可以在请求到达控制器之前处理任何请求。例如,这些可以用来实现安全功能(认证和授权)。与请求映射器一样,拦截器也可以为特定的 URL 模式声明。让我们将登录页面添加到我们的应用程序中,如果用户尚未登录,它应该显示在应用程序中的任何其他页面之前。

我们首先将在packt.jee.course_management.dto包中创建UserDTO。这个类包含用户名、密码以及要在登录页面上显示的任何消息,例如,认证错误:

public class UserDTO { 
  private String userName; 
  private String password; 
  private String message; 

  public boolean messageExists() { 
    return message != null && message.trim().length() > 0; 
  } 

  //skipped setters and getters follow 
} 

现在,让我们创建将处理登录请求的UserController。一旦用户成功登录,我们希望将此信息保留在会话中。可以通过检查会话中是否存在此对象来使用此对象的存在来检查用户是否已经登录。在packt.jee.course_management.controller包中创建UserController类:

@Controller 
public class UserController { 
} 

为登录页面的GET请求添加一个处理方法:

  @RequestMapping (value="/login", method=RequestMethod.GET) 
  public String login (Model model) {
    UserDTO  user = new UserDTO(); 
    model.addAttribute("user", user); 
    return "login"; 
  } 

注意,我们在@RequestMapping注解中指定了方法属性。当请求 URL 是/login且 HTTP 请求类型是GET时,才会调用login方法。如果客户端发送POST请求,则不会调用此方法。在login方法中,我们创建一个UserDTO实例并将其添加到 Model 中,以便它对 View 可访问。

我们将添加一个方法来处理来自登录页面的POST请求。我们将保持相同的 URL,即/login

  @RequestMapping (value="/login", method=RequestMethod.POST) 
  public String doLogin (@ModelAttribute ("user") UserDTO user, 
  Model model) { 

    //Hard-coded validation of user name and 
//password to keep this example simple 
    //But validation could be done against database or 
//any other means here. 
    if (user.getUserName().equals("admin") && 
         user.getPassword().equals("admin")) 
      return "redirect:courses"; 

    user.setMessage("Invalid user name or password. Please try 
     again"); 
    return "login"; 
  } 

我们现在在UserController中有两种方法来处理请求 URL /login。然而,登录方法处理GET请求,而doLogin处理POST请求。如果在doLogin方法中认证成功,那么我们将重定向到课程(列表)页面。否则,我们将设置错误信息并返回到登录页面。

让我们在登录方法中创建的用户对象保存在 HTTP 会话中。这可以通过简单的 @SessionAttributes 注解来完成。您可以指定需要保存在会话中的 Model 中的属性列表。此外,我们还想将 Model 的 user 属性保存在会话中。因此,我们将向 UserController 类添加以下注解:

@Controller 
@SessionAttributes("user") 

public class UserController { 
} 

现在,让我们创建登录页面。在 views 文件夹中创建 login.jsp 并在 HTML <body> 中添加以下代码:

<c:if test="${user.messageExists()}"> 
  <span style="color:red;"> 
    ${user.message}<br> 
  </span> 
</c:if> 

<form id="loginForm" method="POST"> 
  User Id : <input type="text" name="userName" required="required" 
   value="${user.userName}"><br> 
  Password : <input type="password" name="password"><br> 
  <button type="submit">Submit</button> 
</form> 

页面期望 userUserDTO 的实例)可用。这是通过 UserController 通过 Model 提供的。

我们现在有了登录页面和 UserController 来处理认证,但如何确保当用户未登录时,此页面会显示在每个请求中?这就是我们可以使用 Spring 拦截器的地方。我们将在 Spring 上下文配置文件 servlet-context.xml 中配置一个拦截器。将以下代码添加到 servlet-context.xml

<interceptors> 
  <interceptor> 
    <mapping path="/**"/> 
      <beans:bean 
 class="packt.jee.course_management.interceptor.LoginInterceptor"/> 
  </interceptor> 
</interceptors> 

在此配置中,我们正在告诉 Spring 在执行任何请求之前调用 LoginInterceptor(由 mapping path = "/**" 表示)。

现在让我们实现 LoginInterceptor。拦截器必须实现 HandlerInterceptor。我们将使 LoginInterceptor 继承 HandlerInterceptorAdapter,它实现了 HandlerInterceptor

packt.jee.course_management.interceptor 包中创建 LoginInterceptor

@Component 
public class LoginInterceptor extends HandlerInterceptorAdapter { 

  public boolean preHandle(HttpServletRequest request, 
   HttpServletResponse response, Object handler) 
        throws Exception { 

    //get session from request 
    HttpSession session = request.getSession(); 
    UserDTO user = (UserDTO) session.getAttribute("user"); 

    //Check if the current request is for /login. In that case 
    //do nothing, else we will execute the request in loop 
    //Intercept only if request is not /login 
    String context = request.getContextPath(); 
    if (!request.getRequestURI().equals(context + "/login") && 
      (user == null || user.getUserName() == null)) { 
      //User is not logged in. Redirect to /login 
      response.sendRedirect(request.getContextPath() + "/login"); 
      //do not process this request further 
      return false; 
    } 

    return true; 
  } 

} 

拦截器的 preHandle 方法在 Spring 执行任何请求之前被调用。如果我们从该方法返回 true,则请求将被进一步处理;否则,它将被终止。在 preHandle 中,我们首先检查会话中是否存在 user 对象。user 对象的存在意味着用户已经登录。在这种情况下,我们在这个拦截器中不做任何更多的事情,并返回 true。如果用户未登录,则我们将重定向到登录页面并返回 false,这样 Spring 就不会进一步处理此请求。

浏览到 http://localhost:8080/course_management/courses 以测试登录页面。如果您尚未登录,登录页面应该会显示。

使用 JPA 的 Spring MVC 应用程序

在上一节中,我们学习了如何使用 Spring 和 JDBC 创建一个网络应用程序。在本节中,我们将快速查看如何使用 JPAJava Persistence API)与 Spring 一起使用。我们已经在 第四章 和 第七章 中学习了如何使用 JPA,分别是在 创建 JEE 数据库应用程序使用 EJB 创建 JEE 应用程序 中,因此我们不会详细介绍如何为 JPA 设置 Eclipse 项目。然而,在本节中,我们将详细讨论如何与 Spring 一起使用 JPA。

我们将为这个示例创建一个单独的项目。按照本章中创建 Spring 项目部分所述创建一个 Spring MVC 项目。在项目向导的第二页,当被要求输入顶级包名时,输入packt.jee.course_management_jpa。回想一下,包名的最后一部分也用作 Web 应用程序上下文。

配置 JPA

我们将在本项目中使用 EclipseLink JPA 提供者和 MySQL 数据库驱动程序。因此,在项目的pom.xml文件中添加它们的 Maven 依赖项:

  <!-- JPA --> 
  <dependency> 
        <groupId>org.eclipse.persistence</groupId> 
        <artifactId>eclipselink</artifactId> 
        <version>2.7.1</version> 
      </dependency> 
      <dependency> 
        <groupId>mysql</groupId> 
        <artifactId>mysql-connector-java</artifactId> 
        <version>8.0.9-rc</version> 
  </dependency> 

现在我们将为 JPA 配置项目。在项目上右键单击并选择配置 | 转换为 JPA 项目。这打开项目特性页面,其中 JPA 被选为特性之一:

图 8.5:项目特性

点击下一步按钮以配置 JPA 特性:

图 8.6:JPA 特性

在上一页中选择 EclipseLink 平台。我们还将禁用库配置(从类型字段的下拉列表中选择)。配置 MySQL 连接(命名为 CourseMgmtDBConnection),如第七章中配置 JPA部分所述第七章,使用 EJB 创建 JEE 应用程序

点击完成。Persistence.xml文件将在项目资源管理器中的 JPA 内容组下创建(该文件的实际位置是src/main/resources/META-INF/persistence.xml)。我们将在其中配置 MySQL JDBC 连接的属性。打开文件并点击“连接”选项卡:

图 8.7:在 persistence.xml 中配置连接

选择事务类型为资源局部。然后输入 JDBC 驱动程序详细信息。保存文件。

创建课程实体

现在让我们创建Course实体。在项目上右键单击并选择 JPA 工具 | 从实体生成表菜单:

图 8.8:生成课程实体

确保已选择CourseMgmtDBConnection(请参阅第七章中配置 JPA部分第七章,使用 EJB 创建 JEE 应用程序),并且已选中在persistence.xml中列出生成的类。在此页面上点击下一步,然后在下一页上。在自定义默认值页面上,选择 identity 作为键生成器,并将包名设置为packt.jee.course_management_jpa.entity

图 8.9:自定义 JPA 实体默认值

点击下一步。验证实体类名称和其他详细信息:

图 8.10:自定义 JPA 实体详细信息

点击完成。Course实体类将创建在所选的包中:

//skipped imports 
@Entity 
@Table(name="COURSE") 
@NamedQuery(name="Course.findAll", query="SELECT c FROM Course c") 
public class Course implements Serializable { 
  private static final long serialVersionUID = 1L; 

  @Id 
  @GeneratedValue(strategy=GenerationType.IDENTITY) 
  private int id; 

  private int credits; 

  private String name; 

  @Column(name="teacher_id") 
  private int teacherId; 

  //skipped setter and getters 
} 

注意,向导还创建了一个命名查询,用于从表中获取所有课程。

我们现在需要创建 EntityManagerFactory,以便可以从它创建 EntityManager(参考第四章 JPA 概念 部分,创建 JEE 数据库应用程序)。我们将创建一个 Spring bean/component 来创建和存储 EntityManagerFactory。此外,我们将将其注入(自动连接)到 DAO 类中。

packt.jee.course_management_jpa.entity 包中创建 JPAEntityFactoryBean 类:

//skipped imports 

@Component 
public class JPAEntityFactoryBean { 

  EntityManagerFactory entityManagerFactory; 

  @PostConstruct 
  public void init() { 
    entityManagerFactory = 
Persistence.createEntityManagerFactory("CourseManagementSpringMVCJPA"); 
  } 

  public EntityManagerFactory getEntityManagerFactory() { 
    return entityManagerFactory; 
  } 
} 

在类的构造函数中,我们创建 EntityManagerFactorycreateEntityManagerFactory 的参数是 persistence.xml 中指定的持久化单元的名称。

创建 CourseDAO 和 Controller

现在让我们创建 CourseDAO 类。我们将在这个类中注入(自动连接)JPAEntityFactoryBean 的一个实例。在 packt.jee.course_management_jpa.dao 包中创建 CourseDAO 类:

@Component 
public class CourseDAO { 

  @Autowired 
  JPAEntityFactoryBean entityFactoryBean; 

  public List<Course> getCourses() { 
    //Get entity manager 
    EntityManagerFactory emf = 
     entityFactoryBean.getEntityManagerFactory(); 
    EntityManager em = emf.createEntityManager(); 

    //Execute Query 
    TypedQuery<Course> courseQuery = 
     em.createNamedQuery("Course.findAll", Course.class); 
      List<Course> courses = courseQuery.getResultList(); 
      em.close(); 

      return courses; 
  } 
} 

getCourses 方法中,我们首先创建 EntityManager(来自 JPAEntityFactoryBean)并执行命名查询。一旦我们得到结果,我们就关闭 EntityManager

CourseController 类将自动注入(自动连接)CourseDAO。在 packt.jee.course_management_jpa.controller 包中创建 CourseController

//skipped imports 
@Controller 
public class CourseController { 
  @Autowired 
  CourseDAO courseDAO; 

  @RequestMapping("/courses") 
  public String getCourses(Model model) { 
    model.addAttribute("courses", courseDAO.getCourses()); 
    return "courses"; 
  } 
} 

正如我们在之前为 JDBC 应用程序创建的 CourseController 中所看到的,我们从数据库中获取课程并将课程列表添加到以 courses 为键名的模型中。这个变量将可用于显示课程列表的视图页面。

创建课程列表视图

我们现在有了获取课程所需的所有类。现在让我们创建一个 JSP 来显示课程列表。在 src/main/webapp/WEB-INF/views 文件夹中创建 courses.jsp。在页面的 HTML body 标签中添加以下内容:

<h2>Courses:</h2> 

<table> 
  <tr> 
    <th>Id</th> 
    <th>Name</th> 
    <th>Credits</th> 
    <th></th> 
  </tr> 
  <c:forEach items="${courses}" var="course"> 
    <tr> 
      <td>${course.id}</td> 
      <td>${course.name}</td> 
      <td>${course.credits}</td> 
    </tr> 
  </c:forEach> 
</table> 

视图页面使用 JSTL 标签遍历课程(使用控制器通过模型提供的变量)并显示它们。

我们在这里不会构建整个应用程序。我们的想法是了解如何使用 JPA 与 Spring MVC,这是我们在这个部分学到的。浏览到 http://localhost:8080/course_management_jpa/courses 来运行应用程序。

概述

在本章中,我们学习了如何使用 Spring MVC 创建 Web 应用程序。正如其名称所示,Spring MVC 实现了 MVC 设计模式,这有助于清晰地分离用户界面代码和业务逻辑代码。

使用 Spring 框架的依赖注入功能,我们可以轻松地管理应用程序中不同对象的依赖关系。我们还学习了如何使用 JDBC 和 JPA 与 Spring MVC 一起创建数据驱动的 Web 应用程序。

在下一章中,我们将看到如何在 JEE 应用程序中创建和消费 Web 服务。我们将探讨基于 SOAP 和 RESTful 的 Web 服务。

第九章:创建 Web 服务

在上一章中,我们学习了如何使用 MVC 框架在 Java 中创建 Web 应用程序。在本章中,我们将学习如何在 Java 中实现 Web 服务。

我们将涵盖以下主题:

  • 使用 JAXB 和 JSON-B 进行 Java 对象绑定和序列化

  • 实现和消费 RESTful Web 服务

  • 实现和消费 SOAP Web 服务

什么是 Web 服务?

在第七章,“使用 EJB 创建 JEE 应用程序”中,我们学习了 EJB 可以用来创建分布式应用程序。EJB 可以作为粘合剂,帮助企业中的不同 JEE 应用程序相互通信。然而,如果企业希望其合作伙伴或客户使用某些应用程序功能,会怎样呢?例如,航空公司可能希望让其合作伙伴进行在线预订。

一种选择是让合作伙伴将其客户重定向到航空公司网站,但这不会为用户提供统一的使用体验。更好的处理方式是航空公司将其预订 API 暴露给合作伙伴,合作伙伴可以将这些 API 集成到自己的应用程序中,从而提供统一的使用体验。这是一个分布式应用程序的例子,可以使用 EJB 来实现。

然而,为了在 API 调用跨越企业边界的情况下使 EJBs 工作,API 的客户端也需要用 Java 实现。正如我们所知,这并不实用。在这个例子中,一些航空合作伙伴可能使用不同的编程平台实现其应用程序,例如.NET 和 PHP。

在这里提到的情况下,Web 服务非常有用。Web 服务是基于开放标准的自包含 API,且与平台无关。它们广泛用于不同系统之间的通信。主要有两种 Web 服务实现方式:

  • 基于简单对象访问协议(SOAP)

  • 表现性状态转移(RESTful)服务

多年来,基于 SOAP 的 Web 服务相当流行,但最近,由于实现和消费的简单性,RESTful 服务已经开始占据优势。

Web 服务提供了一个通用的集成平台,并提供了面向服务的架构SOA),其中某些组件暴露服务供其他组件或应用程序消费。此类服务的消费者可以通过组装多个此类松散耦合的服务来创建整个应用程序,这些服务可能来自不同的来源。

在本章中,我们将看到如何使用 JEE 和 Eclipse 开发和消费 SOAP 和 RESTful 服务。然而,首先了解如何将 Java 对象转换为 XML 和 JSON,以及反向操作将是有用的,因为 REST 和 SOAP 网络服务实现都需要执行这些操作。首先,我们将探讨 JAXB(Java XML 绑定),使用它可以绑定 Java 对象到 XML 和 JSON。然后我们将探讨 JSON-B(JEE 8 中添加的新规范)用于 Java JSON 绑定。

JAXB

JAXB 提供了一种简单的方法将数据的 XML 或 JSON 表示转换为 Java 对象,反之亦然。使用简单的注解,你可以让 JAXB 实现从 Java 对象创建 XML 或 JSON 数据,或者从 XML 或 JSON 创建 Java 对象。

要了解 Java 数据类型如何在 JAXB 中映射到 XML 架构类型,请参阅 docs.oracle.com/javase/tutorial/jaxb/intro/bind.html

以下是一些重要的 JAXB 注解:

  • @XmlRootElement: 这个注解指定了 XML 文档的根元素,通常用于类级别。

  • @XmlElement: 这个注解指定了一个非根元素的 XML 元素。当类被 @XmlRootElement 注解时,Java 类成员可以被标记为 XMLElement

  • @XmlAttribute: 这个注解将 Java 类的成员标记为父 XML 元素的属性。

  • @XmlAccessorType: 这个注解在类级别指定。它允许你控制类字段如何序列化为 XML 或 JSON。有效值包括 XmlAccessType.FIELD(每个非静态和非 @XmlTransient 字段都会被序列化)、XmlAccessType.PROPERTY(每个未用 @XmlTransient 注解的 getter/setter 对都会被序列化)、XmlAccessType.NONE(没有字段会被序列化,除非特定的字段被注解为序列化)、以及 XmlAccessType.PUBLIC_MEMBER(所有公共 getter/setter 对都会被序列化,除非它们被 @XmlTransient 注解)。

  • @XMLTransient: 这个注解指定了一个成员或 getter/setter 对,这些成员不应该被序列化。

对于 JAXB 注解的完整列表,请参阅 jaxb.java.net/tutorial/section_6_1-JAXB-Annotations.html#JAXB

JAXB 示例

让我们创建一个 Maven 项目来尝试使用 JAXB API。选择 File | Maven Project 菜单:

图 9.1:为 JAXB 示例创建 Maven 项目

确保项目配置为使用 JRE 1.7 或更高版本。现在让我们创建两个类,CourseTeacher。我们希望将这些类的实例序列化为 XML 并反向操作。在 packt.jee.eclipse.jaxb.example 包中创建这些类。以下是 Course 类的源代码:

package packt.jee.eclipse.jaxb.example; 
//Skipped imports 

@XmlRootElement 
@XmlAccessorType(XmlAccessType.FIELD) 
public class Course { 
  @XmlAttribute 
  private int id; 
  @XmlElement(namespace="http://packt.jee.eclipse.jaxb.example") 
  private String name; 
  private int credits; 
  @XmlElement(name="course_teacher") 
  private Teacher teacher; 

  public Course() {} 

  public Course (int id, String name, int credits) { 
    this.id = id; 
    this.name = name; 
    this.credits = credits; 
  } 

  //Getters and setters follow 
} 

当一个 Course 被序列化为 XML 文档时,我们希望 course 元素作为根。因此,该类被注解为 @XmlRootElement

序列化是将数据(通常是对象)写入格式(如 XML 或 JSON)的过程。反序列化是从格式中读取数据并创建对象的过程。

你可以通过指定name属性来为根元素指定不同的名称(除了类名),例如:

@XmlRootElement(name="school_course") 

id字段被标记为根元素的一个属性。如果这些字段有公共的 getter/setter 方法,你不需要特别将字段标记为元素。然而,如果你想设置额外的属性,那么你需要用@XmlElement注解它们。例如,我们为name字段指定了一个命名空间。credits字段没有被注解,但它仍然会被作为 XML 元素进行序列化。

这是Teacher类的源代码:

package packt.jee.eclipse.jaxb.example; 

public class Teacher { 
  private int id; 
  private String name; 

  public Teacher() {} 

  public Teacher (int id, String name) { 
    this.id = id; 
    this.name = name; 
  } 

  //Getters and setters follow 
} 

我们没有为Teacher类添加 JAXB 注解,因为我们不打算直接序列化它。它将在Course实例被序列化时由 JAXB 进行序列化。

让我们创建带有main方法的JAXBExample类:

package packt.jee.eclipse.jaxb.example; 

//Skipped imports 

public class JAXBExample { 

  public static void main(String[] args) throws Exception { 
    doJAXBXml(); 

  } 

  //Create XML from Java object and then vice versa 
  public static void doJAXBXml() throws Exception { 
    Course course = new Course(1,"Course-1", 5); 
    course.setTeacher(new Teacher(1, "Teacher-1")); 

    JAXBContext context = JAXBContext.newInstance(Course.class); 

    //Marshall Java object to XML 
    Marshaller marshaller = context.createMarshaller(); 
    //Set option to format generated XML 
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, 
     true); 
    StringWriter stringWriter = new StringWriter(); 
    //Marshal Course object and write to the StringWriter 
    marshaller.marshal(course, stringWriter); 
    //Get String from the StringWriter 
    String courseXml = stringWriter.getBuffer().toString(); 
    stringWriter.close(); 
    //Print course XML 
    System.out.println(courseXml); 

    //Now unmarshall courseXML to create Course object 
    Unmarshaller unmarshaller = context.createUnmarshaller(); 
    //Create StringReader from courseXml 
    StringReader stringReader = new StringReader(courseXml); 
    //Create StreamSource which will be used by JAXB unmarshaller 
    StreamSource streamSource = new StreamSource(stringReader); 
    Course unmarshalledCourse = 
     unmarshaller.unmarshal(streamSource, Course.class).getValue();    
     System.out.println("-----------------nUnmarshalled course name - " 
        + unmarshalledCourse.getName()); 
    stringReader.close(); 
   } 
} 

要使用 JAXB 进行序列化或反序列化,我们首先创建JAXBContext,传递给它需要处理的 Java 类。然后,我们创建 marshaller 或 unmarshaller,设置相关属性,并执行操作。代码相当简单。我们首先将Course实例序列化为 XML,然后使用相同的 XML 输出将其反序列化回Course实例。在类上右键单击并选择 Run As | Java Application。你应该在控制台看到以下输出:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 
<course id="1" > 
    <ns2:name>Course-1</ns2:name> 
    <credits>5</credits> 
    <course_teacher> 
        <id>1</id> 
        <name>Teacher-1</name> 
    </course_teacher> 
</course> 

----------------- 
Unmarshalled course name - Course-1 

现在我们来看一下如何将 Java 对象序列化为 JSON 然后再反序列化。JAXB 在 JDK 中默认不支持 JSON。我们将不得不使用一个支持 JAXB API 和 JSON 的外部库。这样的库之一是 EclipseLink MOXy (eclipse.org/eclipselink/#moxy)。我们将使用这个库将Course实例序列化为 JSON。

打开pom.xml并添加对 EclipseLink 的依赖项:

  <dependencies> 
    <dependency> 
      <groupId>org.eclipse.persistence</groupId> 
      <artifactId>eclipselink</artifactId> 
      <version>2.6.1-RC1</version> 
    </dependency> 
  </dependencies> 

我们还需要设置javax.xml.bind.context.factory属性,以便 JAXB 实现使用 EclipseLink 的JAXBContextFactory。在要序列化的实例的类所在的包中创建jaxb.properties文件。在这种情况下,在packt.jee.eclipse.jaxb.example包中创建该文件。在这个文件中设置以下属性:

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory 

这非常重要。如果你不设置这个属性,那么示例将无法工作。接下来,打开JAXBExample.java并添加以下方法:

  //Create JSON from Java object and then vice versa 
  public static void doJAXBJson() throws Exception { 

    Course course = new Course(1,"Course-1", 5); 
    course.setTeacher(new Teacher(1, "Teacher-1")); 

    JAXBContext context = JAXBContext.newInstance(Course.class); 

    //Marshal Java object to JSON 
    Marshaller marshaller = context.createMarshaller(); 
    //Set option to format generated JSON 
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, 
     true);    marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, 
     "application/json");    marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, 
     true); 

    StringWriter stringWriter = new StringWriter(); 
    //Marshal Course object and write to the StringWriter 
    marshaller.marshal(course, stringWriter); 
    //Get String from the StringWriter 
    String courseJson = stringWriter.getBuffer().toString(); 
    stringWriter.close(); 
    //Print course JSON 
    System.out.println(courseJson); 

    //Now, unmarshall courseJson to create Course object 
    Unmarshaller unmarshler = context.createUnmarshaller(); 
    unmarshler.setProperty(MarshallerProperties.MEDIA_TYPE, 
     "application/json");    unmarshler.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, 
     true); 

    //Create StringReader from courseJson 
    StringReader stringReader = new StringReader(courseJson); 
    //Create StreamSource which will be used by JAXB unmarshaller 
    StreamSource streamSource = new StreamSource(stringReader); 
    Course unmarshalledCourse = unmarshler.unmarshal(streamSource, 
     Course.class).getValue();    
    System.out.println("-----------------nUnmarshalled course name - " + unmarshalledCourse.getName()); 
    stringReader.close(); 
  } 

大部分代码与doJAXBXml方法中的代码相同。具体更改如下:

  • 我们设置了marshaller属性以生成 JSON 输出(application/json

  • 我们设置另一个marshaller属性以在输出中包含 JSON 根元素

  • 我们在unmarshaller上设置了相应的属性

修改主方法以调用doJAXBJson,而不是doJAXBXml。当你运行应用程序时,你应该看到以下输出:

{ 
   "course" : { 
      "id" : 1, 
      "name" : "Course-1", 
      "credits" : 5, 
      "course_teacher" : { 
         "id" : 1, 
         "name" : "Teacher-1" 
      } 
   } 
} 
----------------- 
Unmarshalled course name - Course-1 

在本章中,我们已经介绍了 JAXB 的基础知识。有关 JAXB 的详细教程,请参阅 docs.oracle.com/javase/tutorial/jaxb/intro/index.html

JSON-B

JSON-B 是 JEE 8 中包含的新规范。使用简单的注解,您可以转换 Java 对象到 JSON,反之亦然。JSON-B 有一个重要的注解,@JsonProperty。为类成员指定此注解,将其标记为序列化到或从 JSON。

JSON-B 提供了 JsonbBuilder 类,使用它可以执行实际的序列化。让我们通过一个简单的应用程序来学习如何使用 JSON-B。

一个 JSON-B 示例

让我们创建一个 Maven 项目,组 ID 为 JAXBExample,工件 ID 为 JSONBExampleProject。JSON-B 不是 JDK 的一部分,因此我们需要添加提供 JSON-B API 和其实现的库的 Maven 依赖项。在这个例子中,我们将使用 Eclipse 的 Yasson (projects.eclipse.org/projects/ee4j.yasson) 对 JSON-B 的实现。我们将在 pom.xml 中添加以下依赖项:

   <dependency>
    <groupId>javax.json.bind</groupId>
    <artifactId>javax.json.bind-api</artifactId>
    <version>1.0</version>
   </dependency>

   <dependency>
    <groupId>org.eclipse</groupId>
    <artifactId>yasson</artifactId>
    <version>1.0.1</version>
   </dependency>

   <dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.json</artifactId>
    <version>1.1.2</version>
   </dependency>
  </dependencies>

由于 yasson 实现依赖于其 JSON-P 实现,因此添加了对 javax.json 的依赖。

现在我们将创建 CourseTeacher 类,就像我们在上一节中为 JAXB 创建它们一样,但这次使用 JSON-B 注解。在 packt.jee.eclipse.jsonb.example 包中创建这两个类。以下是 Course 类的源代码:

package packt.jee.eclipse.jsonb.example;
import javax.json.bind.annotation.JsonbProperty;

public class Course {
 @JsonbProperty
 private int id;

 @JsonbProperty
 private String name;

 @JsonbProperty
 private int credits;

 @JsonbProperty("course_teacher")
 private Teacher teacher;

 //skipped constructors, getters and setters to save space
}

我们已经使用 @JsonbProperty 注解了 Course 类的成员。如果您想更改 JSON 中字段的名称,可以将它指定为 @JsonbProperty 的参数;例如,在上面的代码中,我们将 teacher 字段映射到 JSON 中的 course_teacher 名称。

Teacher 类与我们在 JAXB 部分创建的类相同。现在让我们创建主应用程序类,称为 JSONBExample,在其中我们将 Course 类的实例转换为 String,然后再从 String 转换回 Course 对象的实例:

package packt.jee.eclipse.jsonb.example;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
public class JSONBExample {
  public static void main(String[] args) throws Exception {
    Course course = new Course(1,"Course-1", 5);
    course.setTeacher(new Teacher(1, "Teacher-1"));

    // Serialize to JSON string
    Jsonb jsonb = JsonbBuilder.create();
    String courseJson = jsonb.toJson(course, Course.class);
    System.out.println(courseJson);

    // De-serialize fromd JSON string
    Course deserializedCourse = jsonb.fromJson(courseJson, Course.class);
    System.out.println(deserializedCourse.getName());
  }
}

要序列化 Course 类的实例,我们首先创建一个 JsonBuilder 实例,然后在该实例上调用 toJson 方法。要从 Course 类的 JSON 表示形式反序列化字符串,我们在同一个 JsonBuilder 实例上调用 fromJson。如果您运行应用程序,应该会看到我们创建的课程对象的 JSON 字符串。

关于 JSON-B 的更多详细信息,请参阅 json-b.net/index.html.

RESTful 网络服务

我们将从 RESTful 服务开始学习 Web 服务,因为它们被广泛使用且易于实现。REST 不一定是一个协议,而是一种架构风格,通常基于 HTTP。RESTful Web 服务在服务器上对资源进行操作,操作基于 HTTP 方法(GetPostPutDelete)。资源的状态以 XML 或 JSON 格式通过 HTTP 传输,尽管 JSON 更受欢迎。服务器上的资源通过 URL 进行标识。例如,要获取 ID 为10的课程详细信息,你可以使用以下 URL 的 HTTP GET方法:http://<server_address>:<port>/course/10。请注意,参数是基本 URL 的一部分。要添加新的Course或修改Course,你可以使用POSTPUT方法。此外,可以使用DELETE方法通过相同的 URL 删除Course,即用于获取课程的 URL,即http://<server_address>:<port>/course/10

RESTful Web 服务中的资源 URL 也可以嵌套;例如,要获取特定部门(例如 ID 为20)的所有课程,REST URL 可以是以下形式:http://<server_address>:<port>/department/20/courses

有关 RESTful Web 服务的属性和用于在服务器上操作 REST 资源的 HTTP 方法的更多详细信息,请参阅en.wikipedia.org/wiki/Representational_state_transfer

用于处理 RESTful Web 服务的 Java 规范称为 JAX-RS,即 Java API for RESTful services (jax-rs-spec.java.net/)。Project Jersey (jersey.java.net/)是该规范的参考实现。我们将使用这个参考实现在本章中。

使用 Jersey 创建 RESTful Web 服务

我们将为本书中一直在开发的课程管理示例创建一个 Web 服务。该 Web 服务将包含获取所有课程和创建新课程的方法。为了使示例简单,我们不会编写数据访问代码(你可以使用我们在前几章中学到的 JDBC 或 JDO API),而是将数据硬编码。

首先,创建一个 Maven Web 项目。选择 File | New | Maven Project。在向导的第一页上选择 Create a Simple Project 复选框,然后点击 Next:

图 9.2:为 RESTful Web 服务创建 Maven 项目

输入项目配置详细信息并点击 Finish。确保打包方式为war

由于我们将使用Jersey库来实现 JAX-RS,我们将将其 Maven 依赖项添加到项目中。打开pom.xml文件,并添加以下依赖项:

  <dependencies> 
    <dependency> 
      <groupId>org.glassfish.jersey.containers</groupId> 
      <artifactId>jersey-container-servlet</artifactId> 
      <version>2.26</version> 
    </dependency> 
  </dependencies> 

使用 JAX-RS 的 @Path 注解,我们可以将任何 Java 类转换为 REST 资源。传递给 @Path 注解的值是该资源的相对 URI。实现类中的方法,用于执行不同的 HTTP 方法,被以下注解之一标注:@GET@PUT@POST@DELETE@Path 注解也可以用于方法级别,用于子资源路径(主资源或根资源路径在类级别,再次使用 @Path 注解)。我们还可以通过使用 @Produces@Consumes 注解来分别指定先前方法产生的/消费的 MIME 类型。

在我们创建网络服务实现类之前,让我们创建一些实用类,更具体地说,在这个案例中是 DTOs。

packt.jee.eclipse.rest.ws.dto 包中创建 CourseTeacher 类。我们还将它们标注为 JAXB 注解。以下是 Teacher 类的源代码:

package packt.jee.eclipse.rest.ws.dto; 

import javax.xml.bind.annotation.XmlAccessType; 
import javax.xml.bind.annotation.XmlAccessorType; 
import javax.xml.bind.annotation.XmlAttribute; 
import javax.xml.bind.annotation.XmlElement; 
import javax.xml.bind.annotation.XmlRootElement; 

@XmlRootElement 
@XmlAccessorType(XmlAccessType.FIELD) 
public class Teacher { 

  @XmlAttribute 
  private int id; 

  @XmlElement(name="teacher_name") 
  private String name; 

  //constructors 
  public Course() {} 

  public Course (int id, String name, int credits, Teacher 
   teacher) { 
    this.id = id; 
    this.name = name; 
    this.credits = credits; 
    this.teacher = teacher; 
  } 

  //Getters and setters follow 
} 

以下是为 Course 类的源代码,我们将在后续章节中将它用于序列化为 XML 和 JSON:

package packt.jee.eclipse.rest.ws.dto; 

import javax.xml.bind.annotation.XmlAccessType; 
import javax.xml.bind.annotation.XmlAccessorType; 
import javax.xml.bind.annotation.XmlAttribute; 
import javax.xml.bind.annotation.XmlElement; 
import javax.xml.bind.annotation.XmlRootElement; 

@XmlRootElement 
@XmlAccessorType(XmlAccessType.FIELD) 
public class Course { 

  @XmlAttribute 
  private int id; 

  @XmlElement(name="course_name") 
  private String name; 

  private int credits; 

  private Teacher teacher; 

  //constructors 
  public Teacher() {} 

  public Teacher (int id, String name) { 
    this.id = id; 
    this.name = name; 
  } 

  //Getters and setters follow 
} 

我们在两个类中的 id 字段上标注了 @XMLAttribute。如果这些类的对象被序列化(从 Java 对象转换为 XML),则 Course idTeacher id 将是根元素(分别对应 CourseTeacher)的属性(而不是元素)。如果没有指定字段注解,并且存在属性的公共 getter/setter,则它被视为具有相同名称的 XML 元素。

我们特别使用了 @XMLElement 注解来标注 name 字段,因为我们希望在将它们序列化为 XML 时将它们重命名为 course_nameteacher_name

实现 REST GET 请求

现在我们来实现 RESTful 网络服务类。在 packt.jee.eclipse.rest.ws.services 包中创建 CourseService 类:

package packt.jee.eclipse.rest.ws.services; 

import javax.ws.rs.GET; 
import javax.ws.rs.Path; 
import javax.ws.rs.PathParam; 
import javax.ws.rs.Produces; 
import javax.ws.rs.core.MediaType; 

import packt.jee.eclipse.rest.ws.dto.Course; 
import packt.jee.eclipse.rest.ws.dto.Teacher; 

@Path("/course") 
public class CourseService { 

  @GET 
  @Produces (MediaType.APPLICATION_XML) 
  @Path("get/{courseId}") 
  public Course getCourse (@PathParam("courseId") int id) { 

    //To keep the example simple, we will return 
    //hardcoded values here. However, you could get 
    //data from database using, for example, JDO or JDBC 

    return new Course(id,"Course-" + id, 5, new Teacher(2, 
     "Teacher1")); 
  } 
} 

@Path 注解指定了由此类提供的资源可以通过相对 URI "/course" 访问。

getCourse 方法有许多注解。让我们逐一讨论它们。

@GET 注解指定,当使用 HTTP GET 方法调用由 CourseService 类上的 @Path 指定的相对 URI "/course" 时,将调用此方法。

@Produces (MediaType.APPLICATION_JSON) 指定此方法生成 JSON 输出。如果客户端指定了接受的 MIME 类型,则此注解将用于解析要调用的方法,如果多个方法都注有 @GET(或任何其他 HTTP 方法注解)。例如,如果我们有一个名为 getCourseJSON 的方法,它注有 @GET 并生成具有不同 MIME 类型(由 @Produces 指定)的数据,则将根据客户端请求的 MIME 类型选择适当的方法。@Produces 注解中的 MIME 类型还告诉 JAX-RS 实现当将返回给该方法的 Java 对象进行序列化时,要创建的响应的 MIME 类型。例如,在 getCourse 方法中,我们返回 Course 类的实例,@Produces 中指定的 MIME 类型告诉 Jersey 生成此实例的 XML 表示形式。

@Path 注解也可以在方法级别上使用,以指定子资源。在方法级别上指定的 @Path 值相对于类级别上指定的路径值是相对的。具有 ID 20 的资源(在这种情况下,Course)可以通过 /course/get/20 访问。完整的 URL 可以是 http://<server-address>:<port>/<app-name>/course/get/10。路径值中的参数名称在注解中以 {} 包围。

需要通过使用 @PathParam 注解和参数名称作为其值来在方法参数中标识路径参数。JAX-RS 实现框架将路径参数与匹配 @PathParam 注解的参数进行匹配,并适当地将参数值传递给方法。

为了使示例简单并保持对 RESTful 网络服务实现的关注,我们不会在这个方法中实现任何业务逻辑。我们可以通过使用 JDO 或 JDBC API(我们在前面的章节中已经看到了如何使用这些 API 的示例)从数据库中获取数据,但我们只是将返回一些硬编码的数据。该方法返回 Course 类的实例。当数据最终返回给客户端时,JAX-RS 实现将使用 JAXB 将此对象转换为 XML 表示形式。

我们需要告诉 Jersey 框架它需要扫描哪些包以查找 REST 资源。有两种方法可以做到这一点:

我们将使用第二种选项来创建 Application 的子类。然而,我们不会直接继承 Application,而是会继承 Jersey 的 ResourceConfig 类,它反过来又扩展了 Application

packt.jee.eclipse.rest.ws 包中创建 CourseMgmtRESTApplication 类:

package packt.jee.eclipse.rest.ws; 

import javax.ws.rs.ApplicationPath; 

import org.glassfish.jersey.server.ResourceConfig; 

@ApplicationPath("services") 
public class CourseMgmtRESTApplication extends ResourceConfig { 

  public CourseMgmtRESTApplication () { 
    packages("packt.jee.eclipse.rest.ws.services"); 
  } 

} 

我们使用了 @ApplicationPath 注解来指定使用 JAX-RS 实现的 REST 服务的 URL 映射。资源实现类上的所有 @Path URI 都将相对于此路径。例如,我们为 CourseService 类指定的 "/course" URI 将相对于在 @ApplicationPath 注解中指定的 "services" 路径。

在部署应用程序并测试我们的服务之前,我们需要生成 web.xml。在项目资源管理器中右键单击项目,选择 Java EE Tools | Generate Deployment Descriptor Stub。这将创建 WEB-INF 文件夹中的 web.xml。对于此示例,我们不需要修改它。

按照第一章 安装 Tomcat 部分中 介绍 JEE 和 Eclipse 的说明,以及第二章 在 Eclipse 中配置 Tomcat 部分中的说明配置 Eclipse 中的 Tomcat。要部署 Web 应用程序,在 Servers 视图中右键单击配置的 Tomcat 服务器,并选择 Add and Remove 选项。添加当前项目。

通过在 Servers 视图中右键单击配置的服务并选择启动来启动 Tomcat 服务器。

在浏览器中测试 REST GET 请求

在本节中,我们将测试上一节创建的 Web 服务。要测试 Web 服务,请浏览到 http://localhost:8080/CourseManagementREST/services/course/get/10

你应该在浏览器中看到以下 XML:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 
<course id="10"> 
  <course_name>Course-10</course_name> 
  <credits>5</credits> 
  <teacher id="2"> 
    <teacher_name>Teacher1</teacher_name> 
  </teacher> 
</course>

我们不想生成 XML 响应,而是想创建一个 JSON 响应,因为从网页中的 JavaScript 消费 JSON 响应比 XML 响应要容易得多。要创建 JSON 响应,我们需要更改 CourseService 类中 @Produces 注解的值。目前,它设置为 MediaType.APPLICATION_XML,我们希望将其设置为 MediaType.APPLICATION_JSON

public class CourseService { 

  @GET 
  @Produces (MediaType.APPLICATION_JSON) 
  @Path("get/{courseId}") 
  public Course getCourse (@PathParam("courseId") int id) { 
... 
} 
} 

我们还需要添加库来创建 JSON 响应。打开项目的 pom.xml 文件并添加以下依赖项:

    <dependency> 
      <groupId>org.glassfish.jersey.media</groupId> 
      <artifactId>jersey-media-json-jackson</artifactId> 
      <version>2.18</version> 
    </dependency> 

重新启动 Tomcat 服务器,再次浏览到 http://localhost:8080/CourseManagementREST/services/course/get/10 URL。这次,你应该看到一个 JSON 响应:

{ 
    id: 10, 
    credits: 5, 
    teacher: { 
        id: 2, 
        teacher_name: "Teacher1" 
    }, 
    course_name: "Course-10" 
} 

让我们创建 getCourse 方法的两个版本,一个生成 XML,另一个生成 JSON。用以下代码替换 getCourse 函数:

  @GET 
  @Produces (MediaType.APPLICATION_JSON) 
  @Path("get/{courseId}") 
  public Course getCourseJSON (@PathParam("courseId") int id) { 

    return createDummyCourse(id); 

  } 

  @GET 
  @Produces (MediaType.APPLICATION_XML) 
  @Path("get/{courseId}") 
  public Course getCourseXML (@PathParam("courseId") int id) { 

    return createDummyCourse(id); 

  } 

  private Course createDummyCourse (int id) { 
    //To keep the example simple, we will return 
    //hardcoded value here. However, you could get 
    //data from database using, for example, JDO or JDBC 

    return new Course(id,"Course-" + id, 5, new Teacher(2, 
     "Teacher1")); 
  } 

我们添加了 createDummyCourse 方法,该方法与我们之前在 getCourse 方法中使用的代码相同。现在我们有两个版本的 getCourse 方法:getCourseXMLgetCourseJSON,分别生成 XML 和 JSON 响应。

为 REST GET 网络服务创建 Java 客户端

现在让我们创建一个 Java 客户端应用程序,该应用程序调用先前的 Web 服务。创建一个简单的 Maven 项目,并将其命名为 CourseManagementRESTClient

图片 2

图 9.3:创建 JAX-RS 客户端项目

打开 pom.xml 并添加对 Jersey 客户端模块的依赖项:

  <dependencies> 
    <dependency> 
      <groupId>org.glassfish.jersey.core</groupId> 
      <artifactId>jersey-client</artifactId> 
      <version>2.18</version> 
    </dependency> 
  </dependencies>

packt.jee.eclipse.rest.ws.client 包中创建一个名为 CourseManagementRESTClient 的 Java 类:

图片 1

图 9.4:创建 REST 客户端主类

你可以使用 java.net.HttpURLConnection 或其他外部 HTTP 客户端库来调用 RESTful Web 服务。但 JAX-RS 客户端 API 使这项任务变得容易得多,如下面的代码所示:

package packt.jee.eclipse.rest.ws.client; 

import javax.ws.rs.client.Client; 
import javax.ws.rs.client.ClientBuilder; 
import javax.ws.rs.client.WebTarget; 
import javax.ws.rs.core.MediaType; 
import javax.ws.rs.core.Response; 

/** 
 * This is a simple test class for invoking RESTful web service 
 * using JAX-RS client APIs 
 */ 
public class CourseManagementClient { 

  public static void main(String[] args) { 

testGetCoursesJSON(); 

  } 

  //Test getCourse method (XML or JSON) of CourseService 
  public static void testGetCoursesJSON() { 
    //Create JAX-RS client 
    Client client = ClientBuilder.newClient(); 
    //Get WebTarget for a URL 
    WebTarget webTarget = 
 client.target("http://localhost:8080/CourseManagementREST/services/course"); 
    //Add paths to URL 
    webTarget = webTarget.path("get").path("10"); 

    //We could also have create webTarget in one call with the full URL - 
    //WebTarget webTarget = 
 client.target("http://localhost:8080/CourseManagementREST/services/course/get/10"); 

    //Execute HTTP get method 
    Response response = 
     webTarget.request(MediaType.APPLICATION_JSON).get(); 

    //Check response code. 200 is OK 
    if (response.getStatus() != 200) { 
      System.out.println("Error invoking REST web service - " + 
       response.getStatusInfo().getReasonPhrase()); 
      return; 
    } 

    //REST call was successful. Print the response 
    System.out.println(response.readEntity(String.class)); 
  } 
} 

有关如何使用 JAX-RS 客户端 API 的详细说明,请参阅 jersey.java.net/documentation/latest/client.html

实现 REST POST 请求

我们看到了如何使用 JAX-RS 实现 HTTP GET 请求的示例。现在让我们实现一个 POST 请求。我们将在 CourseService 类中实现一个添加课程的方法,这是我们的 CourseManagementREST 项目中的 Web 服务实现类。

就像 getCourse 方法的情况一样,我们实际上不会访问数据库,而只是简单地编写一个虚拟方法来保存数据。再次强调,我们的目的是保持示例简单,并仅关注 JAX-RS API 和实现。打开 CourseService.java 并添加以下方法:

  @POST 
  @Consumes (MediaType.APPLICATION_JSON) 
  @Produces (MediaType.APPLICATION_JSON) 
  @Path("add") 
  public Course addCourse (Course course) { 

    int courseId = dummyAddCourse(course.getName(), 
     course.getCredits()); 

    course.setId(courseId); 

    return course; 
  } 

  private int dummyAddCourse (String courseName, int credits) { 

    //To keep the example simple, we will just print 
    //parameters we received in this method to console and not 
    //actually save data to database. 
    System.out.println("Adding course " + courseName + ", credits 
 = " + credits); 

    //TODO: Add course to database table 

    //return hard-coded id 
    return 10; 
  } 

addCourse 方法生成并消费 JSON 数据。当资源路径(Web 服务端点 URL)具有以下相对路径时调用它:"/course/add"。回想一下,CourseService 类注解了以下路径:"/course"。因此,addCourse 方法的相对路径成为类级别和方法级别(在这种情况下是 "add")指定的路径。我们从 addCourse 返回 Course 的新实例。Jersey 根据类 Course 中的 JAXB 注解创建此类的适当 JSON 表示形式。我们已经在项目中添加了对处理 JSON 格式的 Jersey 模块的依赖项(在 pom.xml 中,我们添加了对 jersey-media-json-jackson 的依赖项)。

重新启动 Tomcat 服务器以使这些更改生效。

编写 REST POST Web 服务客户端

我们现在将在 CourseManagementClient 类中添加一个测试方法,在 CourseManagementRESTClient 项目中:

  //Test addCourse method (JSON version) of CourseService 
  public static void testAddCourseJSON() { 

    //Create JAX-RS client 
    Client client = ClientBuilder.newClient(); 

    //Get WebTarget for a URL 
    WebTarget webTarget = 
 client.target("http://localhost:8600/CourseManagementREST/services/course/add"); 

    //Create JSON representation of Course, 
    //with course_name and credits fields. Instead of creating 
    //JSON manually, you could also use JAXB to create JSON from 
    //Java object. 
    String courseJSON = "{"course_name":"Course-4", 
     "credits":5}"; 

    //Execute HTTP post method 
    Response response = webTarget.request(). 
        post(Entity.entity(courseJSON, 
         MediaType.APPLICATION_JSON_TYPE)); 

    //Check response code. 200 is OK 
    if (response.getStatus() != 200) { 
      //Print error message 
      System.out.println("Error invoking REST Web Service - " + 
       response.getStatusInfo().getReasonPhrase() + 
          ", Error Code : " + response.getStatus()); 
      //Also dump content of response message 
      System.out.println(response.readEntity(String.class)); 
      return; 
    } 

    //REST call was successful. Print the response 
    System.out.println(response.readEntity(String.class)); 
  } 

我们需要以 JSON 格式发送输入数据(Course 信息)。尽管我们在示例中硬编码了 JSON,但你也可以使用 JAXB 或其他任何将 Java 对象转换为 JSON 的库。

注意,我们正在使用 HTTP POST 方法执行请求 webTarget.request().post(...)。我们还已将请求的内容类型设置为 "application/JSON",因为我们的 Web 服务添加 Course 消费 JSON 格式。我们通过创建实体并设置其内容类型为 JSON 来完成此操作:

//Execute HTTP post method 
Response response = 
 webTarget.request().post(Entity.entity(courseJSON, 
       MediaType.APPLICATION_JSON_TYPE)); 

现在,修改 CourseManagementClient 类的 main 方法以调用 testAddCourseJSON 方法。在类上右键单击并选择 Run As | Java Application。你应该在控制台看到以 JSON 格式打印的 Course 信息。同时,检查 Eclipse 中的 Tomcat 控制台。在那里,你应该看到我们在 CourseService.dummyAddCourse 方法中打印的控制台消息。

从 JavaScript 调用 POST RESTful 网络服务

下面是一个如何从 JavaScript 调用我们的 RESTful 网络服务以添加课程的简单示例:

<!DOCTYPE html> 
<html> 
<head> 
<meta charset="UTF-8"> 
<title>Add Course - JSON</title> 

<script type="text/javascript"> 

  function testAddCourseJSON() { 

    //Hardcoded course information to keep example simple. 
    //This could be passed as arguments to this function 
    //We could also use HTML form to get this information from 
     users 
    var courseName = "Course-4"; 
    var credits = 4; 

    //Create XMLHttpRequest 
    var req = new XMLHttpRequest(); 

    //Set callback function, because we will have XMLHttpRequest 
    //make asynchronous call to our web service 
    req.onreadystatechange = function () { 
      if (req.readyState == 4 && req.status == 200) { 
        //HTTP call was successful. Display response 
        document.getElementById("responseSpan").innerHTML = 
         req.responseText; 
      } 
    }; 

    //Open request to our REST service. Call is going to be asyc 
    req.open("POST", 
 "http://localhost:8080/CourseManagementREST/services/course/add", 
 true); 
    //Set request content type as JSON 
    req.setRequestHeader("Content-type", "application/JSON"); 

    //Create Course object and then stringify it to create JSON 
     string 
    var course = { 
      "course_name": courseName, 
      "credits" : credits 
    }; 

    //Send request. 
    req.send(JSON.stringify(course)); 
  } 
</script> 

</head> 
<body> 
  <button type="submit" onclick="return testAddCourseJSON();">Add 
   Course using JSON</button> 
  <p/> 
  <span id="responseSpan"></span> 
</body> 
</html> 

如果你想测试这段代码,请创建一个 HTML 文件,例如 addCourseJSON.html,在 CourseManagementREST 项目的 src/main/webapp 文件夹中。然后,浏览到 http://localhost:8080/CourseManagementREST/addCourseJSON.html。点击“使用 JSON 添加课程”按钮。响应将在同一页面上显示。

使用表单 POST 创建 RESTful 网络服务

我们迄今为止已经创建了使用 HTTP GETPOST 方法的 RESTful 网络服务。使用 POST 方法的网络服务以 JSON 格式接收输入。我们也可以让网络服务的 POST 方法从 HTML 表单元素接收输入。让我们创建一个处理从 HTML 表单提交的数据的方法。打开 CourseManagementREST 项目的 CourseService.java 文件。添加以下方法:

@POST 
@Consumes (MediaType.APPLICATION_FORM_URLENCODED) 
@Path("add") 
public Response addCourseFromForm (@FormParam("name") String courseName, 
    @FormParam("credits") int credits) throws URISyntaxException { 

  dummyAddCourse(courseName, credits); 

  return Response.seeOther(new 
 URI("../addCourseSuccess.html")).build(); 
} 

该方法通过指定具有以下值的 @Consume 注解来标记处理表单数据:"application/x-www-form-urlencoded"。正如我们在 getCourse 方法中使用 @PathParam 映射路径参数一样,我们使用 @FormParam 注解将表单字段映射到方法参数。最后,一旦我们成功保存课程,我们希望客户端重定向到 addCourseSuccess.html。我们通过调用 Response.seeOther 方法来实现这一点。addCourseFromForm 方法返回 Response 对象。

有关如何配置网络服务中的 Response 的更多信息,请参阅 jersey.java.net/documentation/latest/representations.html

我们需要创建 addCourseSuccess.html 以完成此示例。在 CourseManagementREST 项目的 src/main/webapp 文件夹中创建此文件,并包含以下内容:

<h3>Course added successfully</h3> 

为表单编码的 RESTful 网络服务创建 Java 客户端

现在,让我们为调用之前消耗表单编码数据的网络服务创建一个测试方法。打开 CourseManagementRESTClient 项目的 CourseManagementClient.java 文件并添加以下方法:

  //Test addCourse method (Form-Encoded version) of CourseService 
  public static void testAddCourseForm() { 

    //create JAX-RS client 
    Client client = ClientBuilder.newClient(); 

    //Get WebTarget for a URL 
    WebTarget webTarget = 
 client.target("http://localhost:8600/CourseManagementREST/services/course/add"); 

    //Create Form object and populate fields 
    Form form = new Form(); 
    form.param("name", "Course-5"); 
    form.param("credits", "5"); 

    //Execute HTTP post method 
    Response response = webTarget.request(). 
        post(Entity.entity(form, 
         MediaType.APPLICATION_FORM_URLENCODED)); 

    //check response code. 200 is OK 
    if (response.getStatus() != 200) { 
      //Print error message 
      System.out.println("Error invoking REST Web Service - " + 
       response.getStatusInfo().getReasonPhrase() + 
          ", Error Code : " + response.getStatus()); 
      //Also dump content of response message 
      System.out.println(response.readEntity(String.class)); 
      return; 
    } 

    //REST call was successful. Print the response 
    System.out.println(response.readEntity(String.class)); 
  } 

注意,表单数据是通过创建 Form 对象的实例并设置其参数来创建的。POST 请求使用 MediaType.APPLICATION_FORM_URLENCODED 编码,其值为 "application/x-www-form-urlencoded"

现在,修改main方法以调用testAddCourseForm。然后,通过右键单击类并选择“运行方式”|“Java 应用程序”来运行应用程序。你应该在控制台中看到成功消息(来自addCourseSuccess.html)被打印出来。

使用 JSON-B 的 RESTful 网络服务

在上一节中,我们使用 JAXB 实现了 RESTful 网络服务。如前所述,JEE 8 增加了一个新的 JSON 绑定规范,称为 JSON-B。在本节中,我们将学习如何修改我们的网络服务以使用 JSON-B。

从 JAXB 切换到 JSON-B,实际上我们不需要在代码中做太多更改。我们需要使用 JSON-B 的@JsonbProperty注解在Course类中指定字段绑定,而不是使用 JAXB 的@XmlAttribute注解。然后,我们需要添加 Maven 依赖项以包含提供 JSON-B API 及其实现的库。将 pom.xml 中的依赖项部分替换为以下内容:

  <dependencies>
   <dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet</artifactId>
    <version>2.26</version>
   </dependency>
   <dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-binding</artifactId>
    <version>2.26</version>
   </dependency>
   <dependency>
    <groupId>org.glassfish.jersey.inject</groupId>
    <artifactId>jersey-hk2</artifactId>
    <version>2.26</version>
   </dependency>
  </dependencies> 

jersey-container-servlet的依赖没有改变。然而,我们已经用jersey-media-json-bindingjersey-hk2替换了对jersey-media-json-jackson的依赖。当网络服务方法被注解时,Jersey 框架会自动处理 Java 对象到 JSON 的转换:

@Produces (MediaType.APPLICATION_JSON)

这在CourseService类中进行了指定。

在本章的配套源代码中,为这一节创建了一个名为CourseManagementREST-JSONB的独立项目。

SOAP 网络服务

简单对象访问协议SOAP)是来自万维网联盟W3C)的规范(www.w3.org/TR/2007/REC-soap12-part0-20070427/)。尽管我们在这里讨论的是基于 SOAP 的网络服务,但 SOAP 是实现基于 XML 的网络服务所使用的规范之一。还有几个其他规范是实现 SOAP 网络服务所必需的,我们将在后面看到。SOAP 网络服务的一个前提是服务的动态发现和调用。例如,一个应用程序可以从中央目录中查找服务并动态调用它。然而,在实践中,很少有企业愿意在没有测试的情况下动态调用服务,因此 SOAP 网络服务的这一方面使用较少。

W3C 为 SOAP 网络服务定义了许多规范,例如消息、自动发现、安全和服务编排的规范。然而,在开发 SOAP 网络服务之前,我们至少需要了解以下规范。

SOAP

SOAP 定义了网络服务提供者和消费者之间消息交换的格式:

图片

图 9.5:SOAP 消息结构

SOAP 消息的顶级元素是SOAP Envelope。它包含一个可选的SOAP Header和一个SOAP Body。实际上,SOAP Body包含消息有效负载(供消费者处理)以及可选的SOAP Fault(可选),如果有任何错误。

SOAP 头部为 SOAP 消息提供了扩展性。它可以包含用户凭据、事务管理和消息路由等信息。

WSDL

如其名所示,Web 服务描述语言WSDL)描述 Web 服务;特别是,它描述了使用的数据类型(模式)、输入和输出消息、操作(方法)以及绑定和服务端点:

图 9.6:WSDL 结构

虽然在 Java 中创建 Web 服务时您不一定需要了解 WSDL 的详细信息,但了解 WSDL 的基本结构是很好的。WSDL 通常旨在由程序生成和处理,开发者不期望手动编写它们。以下是 WSDL 中的一些元素:

  • 定义: 这是 WSDL 的根元素。

  • 导入: 此元素允许您从外部文件导入元素。这样,您可以使 WSDL 文件模块化。

  • 类型: 此元素定义了 WSDL 中使用的不同数据类型的模式。

  • 消息: 此元素定义了 Web 服务和客户端之间交换的输入和输出消息的格式。

  • PortType: 此定义了 Web 服务支持的方法或操作。PortType中的每个操作都可以声明请求和响应消息。PortType中的操作引用在消息元素中定义的消息。

尽管在图 9.6中,绑定元素看起来与PortType相同,但它实际上指定了绑定到操作和消息类型(远程过程调用文档类型)以及消息的编码(编码或文本)的传输协议,这些操作和消息类型是在PortType中声明的。典型的传输协议是 HTTP,但它也可能是其他协议,如 JMS 和 SMTP。RPC 和文档类型之间的区别在于,RPC 消息类型包含消息中的远程方法名称,而文档类型不包含方法名称。处理文档类型消息中的有效负载的方法名称要么是从端点 URL 中派生的,要么是从头部信息中获取的。然而,还有一种称为文档包装的类型,它确实包含方法名称作为实际消息有效负载的封装元素。

服务元素包含每个 Web 服务端点的实际位置。

UDDI

通用描述、发现与集成UDDI)是一个 Web 服务目录,您可以在其中发布自己的 Web 服务或搜索现有的 Web 服务。该目录可以是全球性的,也可以是企业本地的。UDDI 也是一个支持发布和搜索内容的 Web 服务。

本书将不会重点介绍 UDDI,但您可以访问docs.oracle.com/cd/E14571_01/web.1111/e13734/uddi.htm#WSADV226获取更多信息。

在 Java 中开发 Web 服务

在 Java 中开发网络服务有许多框架。随着规范的改变,新的框架不断涌现。多年来,Java 开发网络服务的流行框架包括 Apache Axis (axis.apache.org/axis/)、Apache Axis2 (axis.apache.org/axis2/java/core/)、Apache CFX (cxf.apache.org/) 和 GlassFish Metro (metro.java.net/)。

早期网络服务框架的实现基于 JAX-RPC基于 XML 的 RPC 的 Java API)规范 (www.oracle.com/technetwork/java/docs-142876.html)。JAX-RPC 在 JEE 5 中被 Java API for XML Web ServicesJAX-WS)所取代。JAX-WS 通过支持注解使网络服务的开发变得更加容易。在本章中,我们将学习如何使用 JAX-WS 创建和消费网络服务。继续使用本书中一直跟踪的示例(课程管理),我们将创建网络服务以获取所有课程并添加新课程。

首先,让我们创建一个 Maven 网络项目。选择 File | New | Maven Project。选择创建一个简单项目的选项:

图 9.7:新 Maven 项目

点击“下一步”。在下一页中输入 Group Id、Artifact id 和 Version。选择 war 打包方式:

图 9.8:输入工件详情

点击“完成”以完成向导。

创建网络服务实现类

JAX-WS 注解是在 Java EE 5.0 中添加的。使用这些注解,我们可以将任何 Java 类(包括 POJOs)转换为网络服务。使用 @Webservice 注解将任何 Java 类转换为网络服务。这个注解可以用于接口或 Java 类上。如果一个 Java 类被 @Webservice 注解,那么该类中所有的公共方法都会在网络服务中暴露。如果一个 Java 接口被 @Webservice 注解,那么实现类仍然需要被 @Webservice 注解,并且需要 endpointInterface 属性及其值为接口名称。

在我们创建网络服务实现类之前,让我们创建几个辅助类。第一个是 Course 数据传输对象。这是我们在前几章中创建的相同类。在 packt.jee.eclipse.ws.soap 包中创建 Course 类:

package packt.jee.eclipse.ws.soap; 

public class Course { 
  private int id; 
  private String name; 
  private int credits; 

  //Setters and getters follow here 
} 

现在,让我们在 packt.jee.eclipse.ws.soap 包中创建网络服务实现类 CourseManagementService

package packt.jee.eclipse.ws.soap; 

import java.util.ArrayList; 
import java.util.List; 

import javax.jws.WebService; 

@WebService 
public class CourseManagementService { 

  public List<Course> getCourses() { 
    //Here courses could be fetched from database using, 
    //for example, JDBC or JDO. However, to keep this example 
    //simple, we will return hardcoded list of courses 

    List<Course> courses = new ArrayList<Course>(); 

    courses.add(new Course(1, "Course-1", 4)); 
    courses.add(new Course(2, "Course-2", 3)); 

    return courses; 
  } 

  public Course getCourse(int courseId) { 
    //Here again, we could get course details from database using 
    //JDBC or JDO. However, to keep this example 
    //simple, we will return hardcoded course 

    return new Course(1,"Course-1",4); 
  } 
} 

CourseManagementService有两个方法:getCoursesgetCourse。为了使示例简单,我们硬编码了值,但你完全可以从数据库中获取数据,例如使用我们在这本书前面讨论过的 JDBC 或 JDO API。该类使用@WebService注解,这告诉 JAX-WS 实现将此类视为网络服务。这个类中的所有方法都将公开作为网络服务操作。如果你想公开特定的方法,可以使用@WebMethod

使用 JAX-WS 参考实现(Glassfish Metro)

仅使用@WebService注解一个类不足以实现网络服务。我们需要一个实现 JAX-WS 规范的库。有多个 JAX-WS 框架可供选择,例如 Axis2、Apache CFX 和 Glassfish Metro。在本章中,我们将使用 Glassfish Metro 实现,它也是 Oracle 提供的 JAX-WS 参考实现(jax-ws.java.net/)。

让我们添加 JAX-WS 框架的 Maven 依赖项。打开pom.xml并添加以下依赖项:

  <dependencies> 
    <dependency> 
      <groupId>com.sun.xml.ws</groupId> 
      <artifactId>jaxws-rt</artifactId> 
      <version>2.2.10</version> 
    </dependency> 
  </dependencies> 

将之前的版本号替换为框架的最新版本。Metro 框架还要求你在名为sun-jaxws.xml的配置文件中声明网络服务端点。在src/main/webapp/WEB-INF文件夹中创建sun-jaxws.xml文件,并按如下方式添加端点:

<?xml version="1.0" encoding="UTF-8"?> 
<endpoints  
 version="2.0"> 
  <endpoint name="CourseService" implementation="packt.jee.eclipse.ws.soap.CourseManagementService" 
        url-pattern="/courseService" /> 
</endpoints> 

端点实现是我们网络服务实现类的完全限定名称。url-pattern就像你在web.xml中指定的 servlet 映射。在这种情况下,任何以/courseService开头的相对 URL 都会导致调用我们的网络服务。

检查 WSDL

我们已经完成了网络服务的实现。正如你所看到的,JAX-WS 确实使得开发网络服务变得非常容易。现在让我们检查一下我们网络服务的 WSDL。按照第一章中“安装 Tomcat”部分和第二章中“在 Eclipse 中配置 Tomcat”部分的描述,在 Eclipse 中配置 Tomcat。要部署网络应用程序,在服务器视图中右键单击配置的 Tomcat 服务器,并选择添加和移除选项:

图 9.9:将项目添加到 Tomcat

添加项目并点击完成。

通过在服务器视图中右键单击配置的服务并选择启动来启动 Tomcat 服务器。

要检查我们网络服务的 WSDL,请浏览到http://localhost:8080/CourseMgmtWSProject/courseService?wsdl(假设 Tomcat 运行在端口8080)。以下 WSDL 应该被生成(参见 WSDL 部分中图 9.6之后的描述,以了解此处生成的 WSDL 的结构):

<definitions 

 targetNamespace="http://soap.ws.eclipse.jee.packt/" 
  name="CourseManagementServiceService"> 
  <types> 
    <xsd:schema> 
      <xsd:import namespace="http://soap.ws.eclipse.jee.packt/" 
schemaLocation="http://localhost:8080/CourseMgmtWSProject/courseService?xsd=1" /> 
    </xsd:schema> 
  </types> 
  <message name="getCourses"> 
    <part name="parameters" element="tns:getCourses" /> 
  </message> 
  <message name="getCoursesResponse"> 
    <part name="parameters" element="tns:getCoursesResponse" /> 
  </message> 
  <message name="getCourse"> 
    <part name="parameters" element="tns:getCourse" /> 
  </message> 
  <message name="getCourseResponse"> 
    <part name="parameters" element="tns:getCourseResponse" /> 
  </message> 
  <portType name="CourseManagementService"> 
    <operation name="getCourses"> 
      <input 
wsam:Action="http://soap.ws.eclipse.jee.packt/CourseManagementService/getCoursesRequest" 
        message="tns:getCourses" /> 
      <output 
wsam:Action="http://soap.ws.eclipse.jee.packt/CourseManagementService/getCoursesResponse" 
        message="tns:getCoursesResponse" /> 
    </operation> 
    <operation name="getCourse"> 
      <input 
wsam:Action="http://soap.ws.eclipse.jee.packt/CourseManagementService/getCourseRequest" 
        message="tns:getCourse" /> 
      <output 
wsam:Action="http://soap.ws.eclipse.jee.packt/CourseManagementService/getCourseResponse" 
        message="tns:getCourseResponse" /> 
    </operation> 
  </portType> 
  <binding name="CourseManagementServicePortBinding" 
   type="tns:CourseManagementService"> 
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" 
      style="document" /> 
    <operation name="getCourses"> 
      <soap:operation soapAction="" /> 
      <input> 
        <soap:body use="literal" /> 
      </input> 
      <output> 
        <soap:body use="literal" /> 
      </output> 
    </operation> 
    <operation name="getCourse"> 
      <soap:operation soapAction="" /> 
      <input> 
        <soap:body use="literal" /> 
      </input> 
      <output> 
        <soap:body use="literal" /> 
      </output> 
    </operation> 
  </binding> 
  <service name="CourseManagementServiceService"> 
    <port name="CourseManagementServicePort" 
     binding="tns:CourseManagementServicePortBinding"> 
      <soap:address 
location="http://localhost:8080/CourseMgmtWSProject/courseService" 
 /> 
    </port> 
  </service> 
</definitions> 

注意,此 Web 服务的模式(参见/types/xsd:schemas元素的定义)已导入到之前的 WSDL 中。您可以在http://localhost:8080/CourseMgmtWSProject/courseService?xsd=1中看到生成的模式:

<xs:schema  
   version="1.0" 
  targetNamespace="http://soap.ws.eclipse.jee.packt/"> 

  <xs:element name="getCourse" type="tns:getCourse" /> 
  <xs:element name="getCourseResponse" 
   type="tns:getCourseResponse" /> 
  <xs:element name="getCourses" type="tns:getCourses" /> 
  <xs:element name="getCoursesResponse" 
 type="tns:getCoursesResponse" /> 

  <xs:complexType name="getCourses"> 
    <xs:sequence /> 
  </xs:complexType> 
  <xs:complexType name="getCoursesResponse"> 
    <xs:sequence> 
      <xs:element name="return" type="tns:course" minOccurs="0" 
        maxOccurs="unbounded" /> 
    </xs:sequence> 
  </xs:complexType> 
  <xs:complexType name="course"> 
    <xs:sequence> 
      <xs:element name="credits" type="xs:int" /> 
      <xs:element name="id" type="xs:int" /> 
      <xs:element name="name" type="xs:string" minOccurs="0" /> 
    </xs:sequence> 
  </xs:complexType> 
  <xs:complexType name="getCourse"> 
    <xs:sequence> 
      <xs:element name="arg0" type="xs:int" /> 
    </xs:sequence> 
  </xs:complexType> 
  <xs:complexType name="getCourseResponse"> 
    <xs:sequence> 
      <xs:element name="return" type="tns:course" minOccurs="0" /> 
    </xs:sequence> 
  </xs:complexType> 
</xs:schema>

模式文档定义了getCoursegetCourses方法及其响应(getCoursesResponsegetCourseResponse)以及Course类的数据类型。它还声明了Course数据类型的成员(idcreditsname)。请注意,getCourse数据类型有一个子元素(它是CourseManagementService中对getCourse方法的调用参数),称为arg0,实际上是int类型的课程 ID。此外,请注意getCoursesResponse的定义。在我们的实现类中,getCourses返回List<Course>,在 WSDL(或 WSDL 中的类型)中将其转换为课程类型的序列。

在之前的 WSDL 中定义了以下四个消息:getCoursesgetCoursesResponsegetCoursegetCourseResponse。每个消息都包含一个部分元素,该元素引用在types(或 schema)中声明的数据类型。

PortType名称与名为CourseManagementService的 Web 服务实现类相同,端口的操作与类的公共方法相同。每个操作的输入和输出都引用 WSDL 中已定义的消息。

绑定定义了网络传输类型,在这个例子中是 HTTP,以及 SOAP 体中的消息样式,它是文档类型。在我们的 Web 服务实现中,我们没有定义任何消息类型,但 JAX-WS 参考实现(Glassfish Metro)已将默认消息类型设置为document。绑定还定义了每个操作的输入和输出消息的消息编码类型。

最后,Service元素指定了端口的地址,即我们访问以调用 Web 服务的 URL。

使用接口实现 Web 服务

在我们的 Web 服务实现类CourseManagementService中声明的所有方法都作为 Web 服务操作公开。然而,如果你想只公开 Web 服务实现类中的有限方法集,则可以使用 Java 接口。例如,如果我们只想公开getCourses方法作为 Web 服务操作,则可以创建一个接口,比如ICourseManagementService

package packt.jee.eclipse.ws.soap; 

import java.util.List; 

import javax.jws.WebService; 

@WebService 
public interface ICourseManagementService { 
  public List<Course> getCourses(); 
} 

实现类也需要使用@WebService注解,并将endpointInterface属性设置为接口名称:

package packt.jee.eclipse.ws.soap; 

import java.util.ArrayList; 
import java.util.List; 

import javax.jws.WebService; 

@WebService 
 (endpointInterface="packt.jee.eclipse.ws.soap.ICourseManagementService") 
public class CourseManagementService implements ICourseManagementService { 

  //getCourses and getCourse methods follow here 
} 

现在,重新启动 Tomcat 并检查 WSDL。你会注意到在 WSDL 中只定义了getCourses操作。

使用 JAX-WS 消费 Web 服务

现在让我们创建一个简单的 Java 控制台应用程序来消费我们之前创建的 Web 服务。选择 File | New | Maven Project。在第一页上选择“Create a simple project”选项并点击 Next。输入以下配置详细信息:

图片

图 9.10:为网络服务客户端创建 Maven 项目

确保打包类型为jar。点击完成。

现在我们将在客户端生成一个存根和支持类以调用网络服务。我们将使用wsimport工具生成客户端类。我们将使用-p选项指定生成类的包,并使用 WSDL 位置生成客户端类。wsimport工具是 JDK 的一部分,如果使用 JDK 1.7 或更高版本,它应该位于<JDK_HOME>/bin文件夹中。

将文件夹更改为<project_home>/src//main/java并运行以下命令:

wsimport -keep -p packt.jee.eclipse.ws.soap.client http://localhost:8080/CourseMgmtWSProject/courseService?wsdl

-keep标志指示wsimport保留生成的文件,而不是删除它。

-p选项指定生成类的包名。

最后一个参数是网络服务的 WSDL 位置。在 Eclipse 的包资源管理器或项目资源管理器中,刷新客户端项目以查看生成的文件。这些文件应在packt.jee.eclipse.ws.soap.client包中。

wsimport为模式中定义的每个类型(在 WSDL 的类型元素中)生成客户端类。因此,您将找到CourseGetCourseGetCourseResponseGetCoursesGetCoursesResponse类。此外,它还为 WSDL 的portTypeCourseManagementService)和serviceCourseManagementServiceService)元素生成类。此外,它还创建了一个ObjectFactory类,该类使用 JAXB 从 XML 创建 Java 对象。

现在我们来编写实际调用网络服务的代码。在packt.jee.eclipse.ws.soap.client.test包中创建CourseMgmtWSClient类:

package packt.jee.eclipse.ws.soap.client.test; 

import packt.jee.eclipse.ws.soap.client.Course; 
import packt.jee.eclipse.ws.soap.client.CourseManagementService; 
import packt.jee.eclipse.ws.soap.client.CourseManagementServiceService; 

public class CourseMgmtWSClient { 

  public static void main(String[] args) { 
    CourseManagementServiceService service = new 
 CourseManagementServiceService();    CourseManagementService port = 
     service.getCourseManagementServicePort(); 

    Course course = port.getCourse(1); 
    System.out.println("Course name = " + course.getName()); 
  } 

} 

我们首先创建Service对象,然后从中获取端口。port对象定义了网络服务的操作。然后我们在port对象上调用实际的网络服务方法。在类上右键单击并选择运行方式 | Java 应用程序。输出应该是我们在网络服务实现中硬编码的课程名称,即Course-1

在网络服务操作中指定参数名称

如前所述,当为我们的Course网络服务创建 WSDL 时,getCourse操作名称的参数被创建为arg0。您可以通过浏览到http://localhost:8080/CourseMgmtWSProject/courseService?xsd=1并检查getCourse类型来验证这一点:

<xs:complexType name="getCourse"> 
     <xs:sequence> 
         <xs:element name="arg0" type="xs:int"/> 
     </xs:sequence> 
</xs:complexType> 

因此,客户端生成的代码(由wsimport生成)在CourseManagementService.getCourse中也命名参数为arg0。给参数起一个有意义的名称会很好。这可以通过在我们的网络服务实现类CourseManagementService中添加@WSParam注解轻松完成:

public Course getCourse(@WebParam(name="courseId") int courseId) {...} 

在此更改后重新启动 Tomcat,并再次浏览到 WSDL 模式 URL(http://localhost:8080/CourseMgmtWSProject/courseService?xsd=1)。现在您应该能在getCourse类型中看到正确的参数名称:

<xs:complexType name="getCourse"> 
     <xs:sequence> 
         <xs:element name="courseId" type="xs:int"/> 
     </xs:sequence> 
</xs:complexType> 

再次使用wsimport生成客户端代码,你会看到getCourse方法的参数被命名为courseId

检查 SOAP 消息

虽然你不必一定理解 Web 服务和客户端之间传递的 SOAP 消息,但有时检查两者之间交换的 SOAP 消息可以帮助调试一些问题。

当运行客户端时,你可以通过设置以下系统属性轻松地打印请求和响应 SOAP 消息:

com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump=true

在 Eclipse 中,右键单击CourseMgmtWSClient类,选择“运行”|“运行配置”。单击“参数”选项卡,并指定以下 VM 参数:

Dcom.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump=true

图片

图 9.11:设置 VM 参数

点击运行。你将在 Eclipse 的控制台窗口中看到请求和响应 SOAP 消息被打印出来。在格式化请求消息后,这是请求 SOAP 消息的外观:

<?xml version="1.0" ?> 
<S:Envelope > 
  <S:Body> 
    <ns2:getCourse > 
      <courseId>1</courseId> 
    </ns2:getCourse> 
  </S:Body> 
</S:Envelope> 

响应如下:

<?xml version='1.0' encoding='UTF-8'?> 
<S:Envelope > 
  <S:Body> 
    <ns2:getCourseResponse 
     > 
      <return> 
        <credits>4</credits> 
        <id>1</id> 
        <name>Course-1</name> 
      </return> 
    </ns2:getCourseResponse> 
  </S:Body> 
</S:Envelope> 

RPC 样式 Web 服务中的接口处理

回想一下,我们 Web 服务实现类的消息风格是Document,编码是literal。让我们将风格更改为 RPC。打开CourseManagementService.java,将 SOAP 绑定的风格从Style.DOCUMENT更改为Style.RPC

@WebService 
@SOAPBinding(style=Style.RPC, use=Use.LITERAL) 
public class CourseManagementService {...} 

重新启动 Tomcat。在 Tomcat 控制台中,你可能会看到以下错误:

    Caused by: com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
    java.util.List is an interface, and JAXB can't handle interfaces.
      this problem is related to the following location:
        at java.util.List

这个问题是由CourseManagementService类中的以下方法定义引起的:

  public List<Course> getCourses() {...} 

在 RPC 样式的 SOAP 绑定中,JAX-WS 使用 JAXB,而 JAXB 不能很好地序列化接口。一篇博客文章community.oracle.com/blogs/kohsuke/2006/06/06/jaxb-and-interfaces试图解释这一原因。解决方案是为List创建一个包装器,并用@XMLElement注解它。因此,在同一个包中创建一个名为Courses的新类:

package packt.jee.eclipse.ws.soap; 

import java.util.List; 

import javax.xml.bind.annotation.XmlAnyElement; 
import javax.xml.bind.annotation.XmlRootElement; 

@XmlRootElement 
public class Courses { 
  @XmlAnyElement 
  public List<Course> courseList; 

  public Courses() { 

  } 

  public Courses (List<Course> courseList) { 
    this.courseList = courseList; 
  } 
} 

然后,将CourseManagementServicegetCourses方法修改为返回Courses对象而不是List<Course>

  public Courses getCourses() { 
    //Here, courses could be fetched from database using, 
    //for example, JDBC or JDO. However, to keep this example 
    //simple, we will return hardcoded list of courses 

    List<Course> courses = new ArrayList<Course>(); 

    courses.add(new Course(1, "Course-1", 4)); 
    courses.add(new Course(2, "Course-2", 3)); 

    return new Courses(courses); 
  } 

重新启动 Tomcat。这次,应用程序应该在 Tomcat 中无错误地部署。使用wsimport重新生成客户端类,运行客户端应用程序,并验证结果。

异常处理

在 JAX-WS 中,当 XML 有效载荷发送到客户端时,从 Web 服务抛出的 Java 异常被映射为 SOAP 故障。在客户端,JAX-WS 将 SOAP 故障映射为SOAPFaultException或映射为特定于应用程序的异常。客户端代码可以在try...catch块中包装 Web 服务调用以处理 Web 服务抛出的异常。

关于如何在 JAX-WS 中处理 SOAP 异常的详细描述,请参阅docs.oracle.com/cd/E24329_01/web.1211/e24965/faults.htm#WSADV624

摘要

Web 服务是企业应用集成的一种非常有用的技术。它们允许不同的系统相互通信。Web 服务 API 通常是自包含且轻量级的。

Web 服务大致分为两种类型:基于 SOAP 的和 RESTful 的。基于 SOAP 的 Web 服务是基于 XML 的,并提供许多功能,如安全性、附件和事务。RESTful Web 服务可以通过使用 XML 或 JSON 交换数据。RESTful JSON Web 服务非常受欢迎,因为它们可以从 JavaScript 代码中轻松消费。

在本章中,我们学习了如何使用最新的 Java 规范 JAX-RS 和 JAX-WS 来开发和消费基于 SOAP 和 RESTful 的 Web 服务。

在下一章中,我们将探讨另一种应用集成技术:使用Java 消息服务JMS)的异步编程。

第十章:使用 JMS 进行异步编程

在上一章中,我们学习了如何在 JEE 中创建 Web 服务。我们学习了如何创建基于 RESTful 和 SOAP 的 Web 服务。在本章中,我们将学习如何在 JEE 中与消息系统一起工作。到目前为止,我们已经看到了客户端向 JEE 服务器发出请求并等待服务器发送响应的示例。这是编程的同步模型。当服务器处理请求需要很长时间时,这种编程模型可能不适用。在这种情况下,客户端可能希望向服务器发送请求并立即返回,而不必等待响应。服务器将处理请求,并以某种方式将结果提供给客户端。在这种情况下,请求和响应是通过消息发送的。此外,还有一个消息代理确保消息被发送到适当的接收者。这也被称为面向消息的架构。以下是采用面向消息的架构的一些优点:

  • 它可以极大地提高应用程序的可伸缩性。请求被放入一端的队列中,而在另一端可能有多个处理程序正在监听队列并处理请求。随着负载的增加,可以添加更多的处理程序,而当负载减少时,可以移除一些处理程序。

  • 消息系统可以作为不同软件应用程序之间的粘合剂。使用 PHP 开发的应用程序可以将 JSON 或 XML 消息放入消息系统中,该消息可以被 JEE 应用程序处理。

  • 它可以用来实现事件驱动的程序。事件可以作为消息放入消息系统中,并且任何数量的监听器都可以在另一端处理事件。

  • 它可以减少应用程序中系统故障的影响,因为消息在处理之前是持久化的。

企业级消息系统有很多,例如 Apache ActiveMQ (activemq.apache.org/)、RabbitMQ (www.rabbitmq.com/) 和 MSMQ (msdn.microsoft.com/en-us/library/ms711472(v=vs.85).aspx)。Java 消息服务(JMS)规范为与许多不同的消息系统工作提供了一个统一的接口。JMS 也是整体 Java EE 规范的一部分。有关 JMS API 的概述,请参阅 javaee.github.io/tutorial/jms-concepts.html#BNCDQ

任何消息系统都有两种类型的消息容器:

  • 队列:这是用于点对点消息的。一个消息生产者将消息放入队列,只有一个消息消费者接收该消息。可以为队列设置多个监听器,但只有一个监听器接收消息。然而,同一个监听器不一定能接收到所有消息。

  • 主题:这在发布-订阅类型的场景中使用。一个消息生产者将消息放入一个主题,许多订阅者接收该消息。主题对于广播消息很有用。

我们将涵盖以下主题:

  • 使用 JMS API 在队列和主题之间发送和接收消息

  • 使用 JSP、JSF 和 CDI Bean 创建 JMS 应用程序

  • 使用消息驱动 Bean(MDB)消费消息

我们将在本章中看到如何使用队列和主题的示例。我们将使用具有内置 JMS 提供程序的 GlassFish 服务器。我们将使用 JMS API 在课程管理应用程序中实现一个用例,这是我们在这本书的其他章节中一直在构建的应用程序。

使用 JMS 发送和接收消息的步骤

然而,在我们开始使用 JMS API 之前,让我们看看使用它们所涉及的通用步骤。以下步骤展示了如何向队列发送消息并接收它。尽管步骤集中在队列上,但主题的步骤类似,但需要使用适当的主题相关类:

  1. 使用 JNDI 查找ConnectionFactory
InitialContext ctx = new InitialContext(); 
QueueConnectionFactory connectionFactory = (QueueConnectionFactory)initCtx.lookup("jndi_name_of_connection_factory"); 
  1. 创建一个 JMS 连接并启动它:
QueueConnection con = connectionFactory.createQueueConnection(); 
con.start(); 
  1. 创建一个 JMS 会话:
QueueSession session = con.createQueueSession(false, Session.AUTO_ACKNOWLEDGE); 
  1. 查找 JMS Queue/Topic
Queue queue = (Queue)initCtx.lookup("jndi_queue_name"); 
  1. 对于发送消息,执行以下步骤:

    1. 创建一个发送者:
QueueSender sender = session.createSender(queue); 
    1. 创建消息。它可以以下列任何类型:TextMessage/ObjectMessage/MapMessage/BytesMessage/StreamMessage
TextMessage textMessage = session.createTextMessage("Test Message");
    1. 发送消息:
sender.send(textMessage); 
    1. 当不再需要时关闭连接:
con.close();
  1. 对于接收消息,执行以下步骤:

    1. 创建一个接收者:
//create a new session before creating the receiver. 
QueueReceiver receiver = session.createReceiver(queue); 
    1. 注册消息监听器或调用receive方法:
receiver.setMessageListener(new MessageListener() { 
    @Override 
    public void onMessage(Message message) { 
        try { 
            String messageTxt = 
             ((TextMessage)message).getText(); 
            //process message 
        } catch (JMSException e) { 
            //handle exception 
        } 
    } 
}); 
    1. 或者,你可以使用接收方法的任何变体:
Message message = receiver.receive(); //this blocks the thread till a message is received 
    1. 或者你可以使用以下方法:
Message message = receiver.receive(timeout); // with timeout
    1. 或者你可以使用以下方法:
Message message = receiver.receiveNoWait(); //returns null if no message is available. 

在使用 EJB 的 JEE 应用程序中,建议使用 MDB。我们将在本章后面看到 MDB 的示例。

  1. 完成后,关闭连接。这也会停止消息监听器:
con.close(); 

当使用 JMS 注解或使用 MDB 接收消息时,可以跳过一些步骤。我们将在稍后看到示例。

现在,让我们创建一个使用 JMS 发送和接收消息的工作示例。确保你已经安装了 GlassFish 应用程序服务器(参考第一章的安装 GlassFish 服务器部分,介绍 JEE 和 Eclipse),并在 Eclipse JEE 中配置了它(参考第七章的在 Eclipse 中配置 GlassFish 服务器部分,使用 EJB 创建 JEE 应用程序)。在这个示例中,我们将实现添加新课程的使用案例。尽管这不是异步处理的一个强用例,但我们将假设这个操作需要很长时间,并且需要异步处理。

在 GlassFish 中创建队列和主题

让我们在 GlassFish 中创建一个队列和一个主题。确保 GlassFish 服务器正在运行。打开 GlassFish 管理控制台。您可以在 Eclipse(在“服务器视图”)中配置的 GlassFish 服务器实例上右键单击,并选择 GlassFish | 查看管理控制台。这将在内置的 Eclipse 浏览器中打开管理控制台。如果您想在外部 Eclipse 中打开它,在浏览器中,则浏览到http://localhost:4848/(假设默认的 GlassFish 安装)。

我们首先将创建一个 JMS 连接工厂。在管理控制台中,转到“资源”|“JMS 资源”|“连接工厂”页面。点击“新建”按钮来创建一个新的连接工厂:

图片 1

图 10.1:创建 JMS 连接工厂

输入工厂的 JNDI 名称为jms/CourseManagementCF,并选择 javax.jms.ConnectionFactory 作为资源类型。保留池设置的默认值。点击“确定”。

要创建队列和主题,转到“资源”|“JMS 资源”|“目的地资源”页面。点击“新建”按钮:

图片 4

图 10.2:创建 JMS 队列

输入队列的 JNDI 名称为jms/courseManagementQueue,物理目的地名称为CourseManagementQueue,并选择 javax.jms.Queue 作为资源类型。点击“确定”以创建队列。

类似地,通过输入 JNDI 名称为jms/courseManagementTopic,物理目的地名称为CourseManagementTopic,并选择 javax.jms.Topic 作为资源类型来创建主题。

现在,你应该已经在目的地资源页面中配置了一个队列和一个主题:

图片 2

图 10.3:在 GlassFish 中创建的队列和主题

为 JMS 应用程序创建 JEE 项目

我们将看到使用 JMS API 的三种不同方式的示例。

在第一个示例中,我们将创建一个简单的addCourse.jsp页面,一个 JSP Bean 和一个实际执行 JMS 任务的Service类。

在第二个示例中,我们将使用 JSF 和托管 Bean。我们将在托管 Bean 中使用 JMS API。我们还将看到如何在 JSF 托管 Bean 中使用 JMS 注解。

在最后一个示例中,我们将使用 MDB 来消费 JMS 消息。

让我们从第一个示例开始,该示例使用 JSP、Bean 和 JMS API。通过选择“文件”|“新建”|“动态 Web 项目”或“文件”|“新建”|“其他”然后“Web”|“动态 Web 项目”来创建一个 Web 项目:

图片 3

图 10.4:为 JMS 应用程序创建动态 Web 项目

输入项目名称为CourseManagementJMSWeb。确保目标运行时为 GlassFish。点击“下一步”,接受所有默认选项。点击“完成”以创建项目。

使用 JSP 和 JSP Bean 创建 JMS 应用程序

让我们先创建一个 JSP 页面,用于显示输入课程详情的表单。我们还将创建一个 JSP Bean 来处理表单数据。在项目资源管理器视图下的项目中的WebContent文件夹上右键单击,选择“新建”|“JSP 文件”。创建名为addCourse.jsp的 JSP 文件。

我们现在将创建CourseDTO和名为CourseJSPBean的 JSP bean。在packt.jee.eclipse.jms.dto包中创建CourseDTO类。添加idnamecredits属性,以及它们的 getter 和 setter 方法:

import java.io.Serializable; 
public class CourseDTO implements Serializable { 
  private static final long serialVersionUID = 1L; 
  private int id; 
  private String name; 
  private int credits; 

  //getters and setters follow 
} 

packt.jee.eclipse.jms.jsp.beans包中创建CourseJSPBean

import packt.jee.eclipse.jms.dto.CourseDTO; 

public class CourseJSPBean { 

  private CourseDTO course = new CourseDTO(); 

  public void setId(int id) { 
    course.setId(id); 
  } 
  public String getName() { 
    return course.getName(); 
  } 
  public void setName(String name) { 
    course.setName(name); 
  } 
  public int getCredits() { 
    return course.getCredits(); 
  } 
  public void setCredits(int credits) { 
    course.setCredits(credits); 
  } 
  public void addCourse() { 
    //TODO: send CourseDTO object to a JMS queue 
  } 
} 

我们将在addCourse方法中稍后实现发送CourseDTO对象的代码。现在,将以下代码添加到addCourse.jsp中:

<%@ page language="java" contentType="text/html; charset=UTF-8" 
    pageEncoding="UTF-8"%> 
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
 "http://www.w3.org/TR/html4/loose.dtd"> 
<html> 
<head> 
<meta http-equiv="Content-Type" content="text/html; charset=UTF- 
 8"> 
<title>Add Course</title> 
</head> 
<body> 
  <!-- Check if form is posted --> 
  <c:if test="${"POST".equalsIgnoreCase(pageContext.request.method) 
      && pageContext.request.getParameter("submit") != null}"> 

    <!-- Create CourseJSPBean -->   
    <jsp:useBean id="courseService" class="packt.jee.eclipse.jms.jsp_beans.CourseJSPBean" 
 scope="page"></jsp:useBean> 

    <!-- Set Bean properties with values from form submission --> 
    <jsp:setProperty property="name" name="courseService" 
     param="course_name"/>    <jsp:setProperty property="credits" name="courseService" 
     param="course_credits"/> 

    <!-- Call addCourse method of the bean --> 
    ${courseService.addCourse()} 
    <b>Course detailed are sent to a JMS Queue. It will be 
     processed later</b> 
  </c:if> 

  <h2>New Course:</h2> 

  <!-- Course data input form --> 
  <form method="post"> 
    <table> 
      <tr> 
        <td>Name:</td> 
        <td> 
          <input type="text" name="course_name"> 
        </td> 
      </tr> 
      <tr> 
        <td>Credits:</td> 
        <td> 
          <input type="text" name="course_credits"> 
        </td> 
      </tr> 
      <tr> 
        <td colspan="2"> 
          <button type="submit" name="submit">Add</button> 
        </td> 
      </tr> 
    </table> 
  </form> 

</body> 
</html> 

在 JSP 文件的顶部,我们检查表单是否已提交。如果是,我们则创建一个CourseJSPBean实例,并使用表单提交的值设置其属性。然后,我们调用 bean 的addCourse方法。

执行 addCourse.jsp

我们还没有添加任何代码将Course对象放入 JMS 队列。然而,如果你想测试 JSP 和 bean,请将项目添加到 Eclipse 中配置的 GlassFish 服务器。为此,在 Eclipse 的“服务器视图”中右键单击配置的服务器,然后选择“添加和移除...”选项。选择我们创建的 Web 项目,然后单击“完成”。确保服务器已启动,状态为[已启动,同步]:

img/00203.jpeg

图 10.5:添加 Web 项目后 GlassFish 的状态

如果状态是重新发布,则右键单击服务器并选择“发布”选项。如果状态是重启,则右键单击服务器并选择“重启”选项。你可能不需要在添加项目后立即执行此操作,但在我们修改代码后,你可能需要重新发布或重启服务器,或者两者都要。所以,在你在 Eclipse 中执行代码之前,请密切关注服务器状态。

要执行addCourse.jsp,在项目资源管理器或编辑器中右键单击文件,然后选择“运行方式 | 在服务器上运行”选项。这将打开内置的 Eclipse 浏览器并在其中打开 JSP。你应该看到添加课程详情的表单。如果你点击“提交”按钮,你应该看到我们在 JSP 中添加的消息,当表单提交时。

现在我们添加一个类来将课程详情发送到 JMS 队列。

实现 JMS 队列发送类

让我们在packt.jee.eclipse.jms包中创建名为CourseQueueSender的类,内容如下:

package packt.jee.eclipse.jms; 

//skipped imports 

public class CourseQueueSender { 
  private QueueConnection connection; 
  private QueueSession session; 
  private Queue queue; 

  public CourseQueueSender() throws Exception { 
    //Create JMS Connection, session, and queue objects 
    InitialContext initCtx = new InitialContext(); 
    QueueConnectionFactory connectionFactory = 
     (QueueConnectionFactory)initCtx. 
          lookup("jms/CourseManagemenCF"); 
    connection = connectionFactory.createQueueConnection(); 
    connection.start(); 
    session = connection.createQueueSession(false, 
     Session.AUTO_ACKNOWLEDGE); 
    queue = (Queue)initCtx.lookup("jms/courseManagementQueue"); 

  } 

  public void close() { 
    if (connection != null) { 
      try { 
        connection.close(); 
      } catch (JMSException e) { 
        e.printStackTrace(); 
      } 
    } 
  } 
  @Override 
  protected void finalize() throws Throwable { 
    close(); //clean up 
    super.finalize(); 
  } 

  public void sendAddCourseMessage (CourseDTO course) throws 
   Exception { 
    //Send CourseDTO object to JMS Queue 
    QueueSender sender = session.createSender(queue); 
    ObjectMessage objMessage = 
     session.createObjectMessage(course); 
    sender.send(objMessage); 
  } 
} 

在构造函数中,我们查找 JMS 连接工厂并创建连接。然后我们创建一个 JMS 会话,并使用我们在前面的部分中创建队列时使用的 JNDI 名称查找队列。

注意,我们在构建InitialContext时没有指定任何配置属性。这是因为代码是在托管 JMS 提供者的 GlassFish Server 的同一实例中执行的。如果你正在连接到托管在另一个 GlassFish Server 中的 JMS 提供者,那么你将不得不指定配置属性,特别是对于远程主机,例如:

Properties jndiProperties = new Properties(); 
jndiProperties.setProperty("org.omg.CORBA.ORBInitialHost", 
 "<remote_host>"); 
//target ORB port. default is 3700 in GlassFish 
jndiProperties.setProperty("org.omg.CORBA.ORBInitialPort", 
 "3700"); 

InitialContext ctx = new InitialContext(jndiProperties); 

CourseQueueSender.sendAddcourseMessage 方法创建了 QueueSenderObjectMessage 的实例。由于本例中的消息生产者和消费者都是 Java,我们使用了 ObjectMessage。然而,如果你需要向一个消息系统发送消息,而该消息将由非 Java 消费者消费,那么你可以从 Java 对象创建 JSON 或 XML,并发送 TextMessage。我们已经在 第九章,创建 Web 服务 中看到了如何使用 JAXB 和 JSON-B 将 Java 对象序列化为 JSON 和 XML。

现在,让我们修改 CourseJSPBean 中的 addCourse 方法,以使用 CourseQueueSender 类发送 JMS 消息。请注意,我们可以在 CouseJSPBean 的类中创建 CourseQueueSender 的实例,但每次请求页面时都会创建一个 bean。因此,CourseQueueSender 将频繁创建,JMS 连接工厂和队列的查找也将频繁执行,这是不必要的。因此,我们将创建一个 CourseQueueSender 的实例并将其保存在 HTTP 会话中。然后,我们将修改 addCourse 方法以接受 HttpServletRequest 作为参数。我们还将从请求中获取 HttpSession 对象:

  public void addCourse(HttpServletRequest request) throws 
   Exception { 
    //get HTTP session 
    HttpSession session = request.getSession(true); 

    //look for instance of CourseQueueSender in Session 
    CourseQueueSender courseQueueSender = 
     (CourseQueueSender)session 
                        getAttribute("CourseQueueSender"); 
    if (courseQueueSender == null) { 
      //Create instance of CourseQueueSender and save in Session 
      courseQueueSender = new CourseQueueSender(); 
      session.setAttribute("CourseQueueSender", 
       courseQueueSender); 
    } 

    //TODO: perform input validation 
    if (courseQueueSender != null) { 
      try { 
        courseQueueSender.sendAddCourseMessage(course); 
      } catch (Exception e) { 
        e.printStackTrace(); 
        //TODO: log exception 
      } 
    } 
  } 

如果我们在会话中找不到 CourseQueueSender 对象,那么我们将创建一个并将其保存在会话中。

我们需要修改 addcourse.jsp 中对 addCourse 方法的调用。目前,我们没有向该方法传递任何参数。然而,随着对 addCourse 方法的先前更改,我们需要向其中传递 HttpServletRequest 对象。JSP 有一个内置属性 pageContext,它提供了对 HttpServletRequest 对象的访问。因此,修改 addCourse.jsp 中调用 courseService.addCourse 的代码如下:

<!-- Call addCourse method of the bean --> 
${courseService.addCourse(pageContext.request)} 

我们现在可以测试我们的代码,尽管消息已发送到队列,但我们还没有实现任何消费者来从队列中接收它们。因此,让我们为我们的 Course 队列实现一个 JMS 队列消费者。

实现 JMS 队列接收类

让我们在 packt.jee.eclipse.jms 包中创建 CourseQueueReceiver 类,其内容如下:

public class CourseQueueReceiver { 

  private QueueConnection connection; 
  private QueueSession session; 
  private Queue queue; 

  private String receiverName; 

  public CourseQueueReceiver(String name) throws Exception{ 

    //save receiver name 
    this.receiverName = name; 

    //look up JMS connection factory 
    InitialContext initCtx = new InitialContext(); 
    QueueConnectionFactory connectionFactory = 
 (QueueConnectionFactory)initCtx.lookup("jms/CourseManagemenCF"); 

    //create JMS connection 
    connection = connectionFactory.createQueueConnection(); 
    connection.start(); 

    //create JMS session 
    session = connection.createQueueSession(false, 
     Session.AUTO_ACKNOWLEDGE); 
    //look up queue 
    queue = (Queue)initCtx.lookup("jms/courseManagementQueue"); 

    topicPublisher = new CourseTopicPublisher(); 

    QueueReceiver receiver = session.createReceiver(queue); 
    //register message listener 
    receiver.setMessageListener(new MessageListener() { 

      @Override 
      public void onMessage(Message message) { 
        //we expect ObjectMessage here; of type CourseDTO 
        //skipping validation 
        try { 
          CourseDTO course = (CourseDTO) 
           ((ObjectMessage)message).getObject();          //process addCourse action. For example, save it in the 
           database          System.out.println("Received addCourse message for Course name - " + 
               course.getName() + " in Receiver " + receiverName); 

        } catch (Exception e) { 
          e.printStackTrace(); 
          //TODO: handle and log exception 
        } 
      } 
    }); 
  } 

  public void stop() { 
    if (connection != null) { 
      try { 
        connection.close(); 
      } catch (JMSException e) { 
        e.printStackTrace(); 
        //TODO: log exception 
      } 
    } 
  } 
}

查找连接工厂和队列的代码与CourseQueueSender中的代码类似。注意构造函数接受一个name参数。我们实际上并不需要使用 JMS API,但我们将使用它作为CourseQueueReceiver类实例的标识符。我们在构造函数中注册一个消息监听器,并在监听器类的onMessage方法中从消息中获取CourseDTO对象并将消息打印到控制台。当我们执行代码时,这个消息将出现在 Eclipse 中的 GlassFish 控制台中。为了使示例简单,我们没有实现将Course信息保存到数据库的代码,但你可以使用我们在第四章,“创建 JEE 数据库应用程序”中学到的 JDBC 或 JDO API 来实现。

我们需要在应用程序启动时实例化CourseQueueReceiver类,以便它开始监听消息。实现这一种方法是在一个启动时加载的 servlet 中。

让我们在packt.jee.eclipse.jms.servlet包中创建JMSReceiverInitServlet类。我们将使用注解标记这个 servlet 在启动时加载,并在init方法中实例化CourseQueueReceiver

package packt.jee.eclipse.jms.servlet; 

//skipped imports 

@WebServlet(urlPatterns="/JMSReceiverInitServlet", loadOnStartup=1) 
public class JMSReceiverInitServlet extends HttpServlet { 
  private static final long serialVersionUID = 1L; 

  private CourseQueueReceiver courseQueueReceiver = null; 

    public JMSReceiverInitServlet() { 
        super(); 
    } 

    @Override 
    public void init(ServletConfig config) throws ServletException 
 { 
      super.init(config); 
      try { 
      courseQueueReceiver = new CourseQueueReceiver("Receiver1"); 
    } catch (Exception e) { 
      log("Error creating CourseQueueReceiver", e); 
    } 
    } 

    @Override 
    public void destroy() { 
      if (courseQueueReceiver != null) 
        courseQueueReceiver.stop(); 
      super.destroy(); 
    } 
} 

再次在服务器上发布项目并执行addCourse.jsp(参见本章的执行 addCourse.jsp部分)。切换到 Eclipse 中的控制台视图。你应该会看到我们在CourseQueueReceiver中的onMessage方法中打印的消息:

图 10.6:JMS 接收器类的控制台消息示例

添加多个队列监听器

队列用于点对点通信,但这并不意味着一个队列不能有多个监听器。然而,只有一个监听器会接收到消息。此外,也不能保证同一个监听器每次都会接收到消息。如果你想测试这一点,在JMSReceiverInitServlet中添加一个CourseQueueReceiver的更多实例。让我们添加第二个实例,并给它一个不同的名字,比如Receiver2

@WebServlet(urlPatterns="/JMSReceiverInitServlet", loadOnStartup=1) 
public class JMSReceiverInitServlet extends HttpServlet { 
  private CourseQueueReceiver courseQueueReceiver = null; 
  private CourseQueueReceiver courseQueueReceiver1 = null; 

    @Override 
    public void init(ServletConfig config) throws ServletException 
{ 
      super.init(config); 
      try { 
        //first instance of CourseQueueReceiver 
      courseQueueReceiver = new CourseQueueReceiver("Receiver1"); 
      //create another instance of CourseQueueReceiver with a 
       different name 
      courseQueueReceiver1 = new CourseQueueReceiver("Receiver2"); 

    } catch (Exception e) { 
      log("Error creating CourseQueueReceiver", e); 
    } 
    } 

    @Override 
    public void destroy() { 
      if (courseQueueReceiver != null) 
        courseQueueReceiver.stop(); 
      if (courseQueueReceiver1 != null) 
        courseQueueReceiver1.stop(); 
      super.destroy(); 
    } 

    //rest of the code remains the same 
} 

重新发布项目,执行addCourse.jsp,并添加一些课程。检查控制台消息。你可能看到一些消息被Receiver1接收,而其他则被Receiver2接收:

图 10.7:控制台输出显示多个 JMS 接收器正在监听 JMS 队列

实现 JMS 主题发布者

假设我们想在添加新课程时通知一组应用程序。这样的用例最好通过一个JMS 主题来实现。一个主题可以有多个订阅者。当消息添加到主题时,所有订阅者都会收到相同的消息。这与队列不同,在队列中只有一个队列监听器会收到消息。

发布消息到主题和订阅消息的步骤与队列的步骤非常相似,只是类不同,在某些情况下,方法名也不同。

让我们实现一个主题发布者,当在CourseQueueReceiver类中实现的监听器的onMessage方法成功处理添加课程的消息时,我们将使用它。

packt.jee.eclipse.jms包中创建名为CourseTopicPublisher的类,内容如下:

package packt.jee.eclipse.jms; 

//skipped imports 

public class CourseTopicPublisher { 
  private TopicConnection connection; 
  private TopicSession session; 
  private Topic topic; 

  public CourseTopicPublisher() throws Exception { 
    InitialContext initCtx = new InitialContext(); 
    TopicConnectionFactory connectionFactory = 
     (TopicConnectionFactory)initCtx. 
        lookup("jms/CourseManagemenCF"); 
    connection = connectionFactory.createTopicConnection(); 
    connection.start(); 
    session = connection.createTopicSession(false, 
     Session.AUTO_ACKNOWLEDGE); 
    topic = (Topic)initCtx.lookup("jms/courseManagementTopic"); 
  } 

  public void close() { 
    if (connection != null) { 
      try { 
        connection.close(); 
      } catch (JMSException e) { 
        e.printStackTrace();. 
      } 
    } 
  } 

  public void publishAddCourseMessage (CourseDTO course) throws 
   Exception { 
    TopicPublisher sender = session.createPublisher(topic); 
    ObjectMessage objMessage = 
     session.createObjectMessage(course); 
    sender.send(objMessage); 
  } 
} 

代码相当简单且易于理解。现在让我们修改我们实现的队列接收器类CourseQueueReceiver,在队列中的消息成功处理后,从onMessage方法向主题发布消息:

public class CourseQueueReceiver { 

  private CourseTopicPublisher topicPublisher; 

  public CourseQueueReceiver(String name) throws Exception{ 

    //code to lookup connection factory, create session, 
    //and look up queue remains unchanged. Skipping this code 

    //create topic publisher 
    topicPublisher = new CourseTopicPublisher(); 

    QueueReceiver receiver = session.createReceiver(queue); 
    //register message listener 
    receiver.setMessageListener(new MessageListener() { 

      @Override 
      public void onMessage(Message message) { 
        //we expect ObjectMessage here; of type CourseDTO 
        //Skipping validation 
        try { 
          //code to process message is unchanged. Skipping it 

          //publish message to topic 
          if (topicPublisher != null) 
            topicPublisher.publishAddCourseMessage(course); 

        } catch (Exception e) { 
          e.printStackTrace(); 
          //TODO: handle and log exception 
        } 
      } 
    }); 
  } 

  //remaining code is unchanged. Skipping it 
} 

实现 JMS 主题订阅者

我们现在将实现一个主题订阅者类来接收发布到我们之前创建的主题的消息。在packt.jee.eclipse.jms包中创建名为CourseTopicSubscriber的类,内容如下:

package packt.jee.eclipse.jms; 
//skipping imports 
public class CourseTopicSubscriber { 

  private TopicConnection connection; 
  private TopicSession session; 
  private Topic topic; 

  private String subscriberName; 

  public CourseTopicSubscriber(String name) throws Exception{ 

    this.subscriberName = name; 

    InitialContext initCtx = new InitialContext(); 
    TopicConnectionFactory connectionFactory = 
 (TopicConnectionFactory)initCtx.lookup("jms/CourseManagemenCF"); 
    connection = connectionFactory.createTopicConnection(); 
    connection.start(); 
    session = connection.createTopicSession(false, 
     Session.AUTO_ACKNOWLEDGE); 
    topic = (Topic)initCtx.lookup("jms/courseManagementTopic"); 

    TopicSubscriber subscriber = session.createSubscriber(topic); 
    subscriber.setMessageListener(new MessageListener() { 

      @Override 
      public void onMessage(Message message) { 
        //we expect ObjectMessage here; of type CourseDTO 
        //skipping validation 
        try { 
          CourseDTO course = (CourseDTO) 
           ((ObjectMessage)message).getObject();          //process addCourse action. For example, save it in 
           database          System.out.println("Received addCourse notification for 
           Course name - "              + course.getName() + " in Subscriber " + 
               subscriberName); 

        } catch (JMSException e) { 
          e.printStackTrace(); 
          //TODO: handle and log exception 
        } 
      } 
    }); 
  } 

  public void stop() { 
    if (connection != null) { 
      try { 
        connection.close(); 
      } catch (JMSException e) { 
        e.printStackTrace(); 
        //TODO: log exception 
      } 
    } 
  } 
} 

再次,订阅主题的 JMS API 与CourseQueueReceiver中的类似,但类名和方法名不同。我们还通过名称标识订阅者,以便我们知道哪个类的实例接收了消息。

在前面的示例中,我们通过调用TopicSession.createSubscriber创建了主题订阅者。在这种情况下,订阅者将接收主题的消息,只要订阅者处于活动状态。如果订阅者变得不活跃然后再次活跃,它将丢失在该期间发布的主题消息。如果您想确保订阅者接收所有消息,您需要使用TopicSession.createDurableSubscriber创建一个持久订阅。除了主题名称外,此方法还接受订阅者名称作为第二个参数。有关更多信息,请参阅javaee.github.io/javaee-spec/javadocs/javax/jms/TopicSession.html#createDurableSubscriber-javax.jms.Topic-java.lang.String-

我们将在JMSReceiverInitServlet中创建CourseTopicSubscriber类的两个实例(因此将有两个主题订阅者)。这两个实例将在应用程序启动时开始监听消息(servlet 在启动时加载):

@WebServlet(urlPatterns="/JMSReceiverInitServlet", loadOnStartup=1) 
public class JMSReceiverInitServlet extends HttpServlet { 
  private CourseQueueReceiver courseQueueReceiver = null; 
  private CourseTopicSubscriber courseTopicSubscriber = null; 
  private CourseQueueReceiver courseQueueReceiver1 = null; 
  private CourseTopicSubscriber courseTopicSubscriber1 = null; 

    @Override 
    public void init(ServletConfig config) throws ServletException 
 { 
      super.init(config); 
      try { 
      courseQueueReceiver = new CourseQueueReceiver("Receiver1"); 
      courseQueueReceiver1 = new CourseQueueReceiver("Receiver2"); 
      courseTopicSubscriber = new 
       CourseTopicSubscriber("Subscriber1");      courseTopicSubscriber1 = new 
       CourseTopicSubscriber("Subscriber2"); 

    } catch (Exception e) { 
      log("Error creating CourseQueueReceiver", e); 
    } 
    } 

    //remaining code is unchanged. Skipping it 
} 

当应用程序启动时,我们现在有两个队列监听器和两个主题监听器就绪。重新发布项目,执行addCourse.jsp,并添加一门课程。检查 Eclipse 的控制台视图中的消息。您将看到发布在主题中的消息被所有订阅者接收,但发布在队列中的相同消息只被一个接收器接收:

图片

图 10.8:控制台输出显示多个 JMS 接收器正在监听 JMS 队列和主题

使用 JSF 和 CDI 豆创建 JMS 应用程序

在本节中,我们将看到如何使用 JSF 和组件依赖注入CDI)组件创建一个 JMS 应用程序。使用 CDI 组件,我们可以减少使用 JMS API 编写的代码,因为我们可以使用注解来注入诸如 JMS 连接工厂、队列和主题等对象。一旦我们获得了这些对象的引用,发送或接收数据的步骤与上一节中讨论的相同。因此,本节中的示例没有列出完整的代码。对于完整的源代码,请下载本章的源代码。

为了使我们的项目能够使用 JSF,我们需要创建web.xml并在其中添加 JSF servlet 定义和映射。在项目上右键单击并选择 Java EE Tools | Generate Deployment Descriptor Stub 选项。这将在WebContent/WEB-INF文件夹中创建web.xml。在web.xml中添加以下 servlet 定义和映射(在web-app标签内):

  <servlet> 
    <servlet-name>JSFServelt</servlet-name> 
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> 
    <load-on-startup>1</load-on-startup> 
  </servlet> 

  <servlet-mapping> 
    <servlet-name>JSFServelt</servlet-name> 
    <url-pattern>*.xhtml</url-pattern> 
  </servlet-mapping> 

为了使 CDI(Contexts and Dependency Injection)组件工作,我们需要在META-INF文件夹中创建一个beans.xml文件。你可以在 Eclipse 项目中WebContent文件夹下找到META-INF文件夹。让我们在META-INF中创建一个包含以下内容的bean.xml文件:

<beans xmlns="http://java.sun.com/xml/ns/javaee" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
    http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

我们现在将为 JSF 页面创建两个 CDI(Contexts and Dependency Injection)组件。第一个是CourseManagedMsgSenderBean。第二个是CourseJSFBean,它将在 JSF 页面中被引用。

packt.jee.eclipse.jms.jsf_bean包中创建CourseManagedMsgSenderBean类,内容如下:

package packt.jee.eclipse.jms.jsf_bean;

import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
//skipped other imports 

@Named("courseMessageSender")
@SessionScoped
public class CourseManagedMsgSenderBean implements Serializable {

  @Resource(name = "jms/CourseManagementCF")
  private QueueConnectionFactory connectionFactory;
  @Resource(lookup = "jms/courseManagementQueue")
  private Queue queue;

  QueueConnection connection;
  QueueSession session;
  Exception initException = null;

  @PostConstruct
  public void init() {
    try {
      connection = connectionFactory.createQueueConnection();
      connection.start();
      session = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
    } catch (Exception e) {
      initException = e;
    }
  }

  @PreDestroy
  public void cleanup() {
    if (connection != null) {
      try {
        connection.close();
      } catch (JMSException e) {
        e.printStackTrace();
        //TODO: log exception
      }
    }
  }

  public void addCourse(CourseDTO courseDTO) throws Exception {

    if (initException != null)
      throw initException;

    QueueSender sender = session.createSender(queue);
    ObjectMessage objMessage = session.createObjectMessage(courseDTO);
    sender.send(objMessage);
  }
}

注意,JMS 连接工厂和队列对象是通过@Resource注解注入的。我们使用了@PostConstruct注解来创建一个 JMS 连接和会话,以及@PreDestroy注解来进行清理操作。addCourse方法与我们在上一节中实现的CourseQueueSender类中的代码类似。

现在让我们在packt.jee.eclipse.jms.jsf_bean包中创建CourseJSFBean类,内容如下:

package packt.jee.eclipse.jms.jsf_bean;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;

import packt.jee.eclipse.jms.dto.CourseDTO;;

@Named("course")
@RequestScoped
public class CourseJSFBean {
  private CourseDTO courseDTO = new CourseDTO();

  @Inject
  private CourseManagedMsgSenderBean courseMessageSender;

  public String getName() {
    return this.courseDTO.getName();
  }
  public void setName(String name) {
    this.courseDTO.setName(name); 
  }
  public int getCredits() {
    return this.courseDTO.getCredits();
  }
  public void setCredits(int credits) {
    this.courseDTO.setCredits(credits);;
  }

  public void addCourse() throws Exception {
    //skipping validation
    //TODO: handle exception properly and show error message
    courseMessageSender.addCourse(courseDTO);
  }
}

使用@Inject注解将CourseManagedMsgSenderBean的实例注入到CourseJSFBean中。addCourse方法简单地调用相同名称的方法在CourseManagedMsgSenderBean中。

最后,让我们在WebContents文件夹中创建addCourse.xhtml,内容如下:

<html  

 > 

<head> 
  <title>Add Course</title> 
</head> 

 <body> 
  <h2>Course Details</h2> 

  <h:form> 
    <table> 
      <tr> 
        <td>Name:</td> 
        <td> 
          <h:inputText id="course_name" value="#{course.name}"/> 
        </td> 
      </tr> 
      <tr> 
        <td>Credits:</td> 
        <td> 
          <h:inputText id="course_credits" 
           value="#{course.credits}"/> 
        </td> 
      </tr> 
      <tr> 
        <td colspan="2"> 
            <h:commandButton value="Submit" 
             action="#{course.addCourse}"/> 
        </td> 
      </tr> 
    </table> 
  </h:form> 

</body> 

</html> 

表单字段绑定到CourseJSFBean中的字段。当点击提交按钮时,将调用相同 bean 中的addCourse方法,该方法将消息放入 JMS 队列。

重新发布项目并通过右键单击addCourse.xhtml选择运行方式 | 在服务器上运行来执行它。添加一门课程并查看在 Eclipse 的 GlassFish 控制台视图中打印的消息。

使用消息驱动 Bean(MDBs)消费 JMS 消息

消息驱动 Bean(MDB)使消费 JMS 消息变得容易得多。只需使用几个注解并实现onMessage方法,你就可以使任何 Java 对象成为 JMS 消息的消费者。在本节中,我们将实现一个 MDB 来从Course队列中消费消息。要实现 MDB,我们需要创建一个 EJB 项目。从主菜单中选择“文件 | 新建 | EJB 项目”:

图片

图 10.9:创建一个 EJB 项目以实现消息驱动 Bean(MDB)

将项目名称输入为CourseManagementEJB。点击“下一步”。在随后的页面上接受默认值,并在最后一页点击“完成”。

右键单击项目,选择“新建 | 消息驱动 Bean”选项。这会打开 MDB 创建向导:

图片

图 10.10:MDB 创建向导 – 类文件信息

packt.jee.eclipse.jms.mdb作为 Java 包名,将CourseMDB作为类名。保持目标类型为队列。

目标名称是在创建队列时指定的物理目标名称,而不是 JNDI 名称:

图片

图 10.11:GlassFish 管理控制台中的 JMS 队列物理目标名称

将目标类型输入为CourseManagementQueue。点击“下一步”。在第二页接受默认值并点击“完成”。向导生成以下代码:

@MessageDriven( 
    activationConfig = { 
      @ActivationConfigProperty(propertyName = "destinationType", 
            propertyValue = "javax.jms.Queue"), 
      @ActivationConfigProperty(propertyName = "destination", 
            propertyValue = "CourseManagementQueue") 
    }, 
    mappedName = "jms/courseManagementQueue") 
public class CourseMDB implements MessageListener { 

    /** 
     * Default constructor. 
     */ 
    public CourseMDB() { 
        // TODO Auto-generated constructor stub 
    } 

  /** 
     * @see MessageListener#onMessage(Message) 
     */ 
    public void onMessage(Message message) { 
        System.out.println("addCourse message received in 
         CourseMDB"); 

    } 

} 

该类使用@MessageDriven注解,并在向导中指定了activationConfig和 JMS 目标参数。它还创建了onMessage方法。在这个方法中,我们只是打印出 MDB 接收到的用于添加课程的消息。为了处理本类中的ObjectMessage,我们需要将CourseDTO类重构为一个共享的.jar文件,在 EJB 和 Web 项目之间。这留作读者的练习。

JEE 容器为单个 MDB 类创建一个 MDB 对象池。传入的消息可以由池中的任何一个 MDB 实例处理。这有助于构建可扩展的消息处理应用程序。

如果你想测试 MDB,请将项目添加到在 Eclipse 中配置的 GlassFish 服务器。为此,在 Eclipse 的“服务器”视图中右键单击配置的服务器,并选择“添加和移除...”选项。选择我们创建的CourseManagementEJB项目并点击“完成”。确保服务器已启动且状态为[已启动,同步]。你还需要将CourseManagementJMSWeb项目添加到服务器,因为我们有 JSF 和 JSP 页面在该项目中添加课程。从CourseManagementJMSWeb项目运行addCourse.xhtmladdCourse.jsp,添加课程,并在 Eclipse 中的 GlassFish 控制台检查从消息接收器和本节中创建的 MDB 打印的消息。然而,请注意,无论是 MDB 还是我们在CourseManagementJMSWeb中开发的队列监听器之一,都将接收消息,而不是所有接收器。

摘要

消息系统可以是一个强大的工具,用于整合不同的应用程序。它提供了一个异步的编程模型。客户端不需要等待服务器的响应,服务器也不一定会在客户端发送请求的同时处理这些请求。消息系统对于构建可扩展的应用程序和批量处理也非常有用。JMS 提供了统一的 API 来访问不同的消息系统。

在本章中,我们学习了如何从队列发送和接收消息,以及如何从主题发布和订阅消息。使用 JMS API 有众多不同的方式。我们首先从基本的 JMS API 开始,然后学习了注解如何帮助减少一些代码。我们还学习了如何使用消息驱动 Bean(MDB)来消费消息。

在下一章中,我们将看到一些用于分析 Java 应用程序 CPU 和内存使用情况的技巧和工具。

第十一章:Java CPU 分析器和内存跟踪

在上一章中,我们学习了如何使用 JMS(Java 消息服务)API 编写异步应用程序。在本章中,我们将了解一些用于分析 Java 应用程序的技术和工具。企业应用程序往往相当复杂且庞大。可能存在应用程序不符合你的要求或预期的情况。例如,应用程序中执行的一些操作可能花费的时间过长或消耗的内存比你预期的要多。此外,调试性能和内存问题有时可能变得非常困难。

幸运的是,JDK 和 Eclipse 都提供了工具来帮助我们调试这些问题。JDK 6(更新 7)及以上版本捆绑了jVisualVM应用程序,该程序可以连接到远程或本地应用程序。你可以在<JDK_HOME>/bin文件夹中找到这个工具。jVisualVM 可以帮助你分析内存和 CPU 使用情况。它还可以配置为在从 Eclipse 运行应用程序时从 Eclipse 启动。我们将在本章中学习如何使用 VisualVM 来分析 Java 应用程序。你可以在visualvm.github.io/找到有关 jVisualVM/VisualVM 的详细信息。

我们将创建一个独立的 Java 应用程序来模拟性能和内存问题,并了解如何使用 VisualVM 进行故障排除。虽然你可能想要调试的实际应用程序可能要复杂得多,但我们将在本章中学到的技术也可以用于复杂的应用程序。

在本章中,我们将涵盖以下主题:

  • 使用 VisualVM 进行 CPU 和内存分析

  • 检测内存泄漏和死锁的技术

  • 使用 Eclipse 内存分析器分析由 VisualVM 创建的堆转储

创建用于性能分析的示例 Java 项目

我们将创建一个简单的独立 Java 应用程序,这样你就可以轻松学习如何使用 VisualVM 进行性能分析。虽然它将是一个独立的应用程序,但我们将创建一些类,这些类与我们在前几章中为CourseManagement Web 应用程序创建的类相似,特别是CourseDTOCourseBean(JSP Bean)、CourseService(服务 Bean)和CourseDAO(用于数据库访问)。

  1. 在 Eclipse 中创建一个名为CourseManagementStandalone的标准 Java 项目。在packt.jee.eclipse.profile.dto包中创建CourseDTO类:
package packt.jee.eclipse.profile.dto; 

public class CourseDTO { 
  private int id; 
  private String name; 
  private int credits; 

  //skipped Getters and Setters 
} 
  1. packt.jee.eclopse.profile.dao包中创建CourseDAO类:
//skipped imports 
public class CourseDAO { 

  public List<CourseDTO> getCourses() { 
    //No real database access takes place here 
    //We will just simulate a long-running database operation 

    try { 
      Thread.sleep(2000); //wait 2 seconds 
    } catch (InterruptedException e) { 
      e.printStackTrace(); 
    } 

    //return dummy/empty list 
    return new ArrayList<>(); 
  } 
} 

我们通过使线程休眠几秒钟来在getCourses方法中模拟了一个长时间运行的数据库操作。

  1. packt.jee.eclipse.profile.service包中创建CourseService类:
//skipped imports 
public class CourseService { 

  private CourseDAO courseDAO = new CourseDAO(); 

  public List<CourseDTO> getCourses() { 
    return courseDAO.getCourses(); 
  } 
} 

CourseService.getCourses将调用委托给CourseDAO

  1. packt.jee.eclipse.profile.bean包中创建CourseBean类:
//skipped imports 
public class CourseBean { 
  private CourseService courseService = new CourseService(); 

  public List<CourseDTO> getCourses() { 
    return courseService.getCourses(); 
  } 
} 

CourseBean.getcourses将调用委托给CourseService

  1. 最后,在packt.jee.eclipse.profile包中创建CourseManagement类。此类包含main方法,并在读取标准输入的任何字符后重复调用getCourses方法:
//skipped imports 
public class CourseManagement { 

  public static void main(String[] args) throws IOException { 

    CourseBean courseBean = new CourseBean(); 

    System.out.println("Type any character to get courses. Type q 
     to quit."); 

    int ch; 
    while ((ch = System.in.read()) != -1) { 
      if (ch != 10 && ch != 13) { //ignore new lines 
        if (ch == 'q') //quit if user types q 
          break; 

        System.out.println("Getting courses"); 
        List<CourseDTO> courses = courseBean.getCourses(); 
        System.out.println("Got courses"); 

        System.out.println("Type any character to get courses. 
         Type q to quit."); 
      } 
    } 

    System.out.println("Quitting ..."); 
  } 
} 
  1. 运行应用程序(右键单击文件并选择运行方式 | Java 应用程序)。

    在控制台窗口中,输入任何字符并按Enter。您应该看到“获取课程”和“获取课程”的消息。

分析 Java 应用程序

  1. <JDK_HOME>/bin文件夹运行 jvisualvm:

图 11.1:Java VisualVM 分析器

VisualVM 列出了本地节点下可以由它分析的本地所有 Java 进程。您可以看到 VisualVM 本身以及 Eclipse 被列出。

  1. 一旦运行了CourseManagement应用程序,该进程也应该在本地显示:

图 11.2:可用于分析的 CourseManagement 应用程序

  1. 双击进程(或右键单击并选择打开)。然后,转到 Profile 选项卡并单击 CPU 按钮:

图 11.3:VisualVM 分析器选项卡

您应该看到状态设置为分析运行。

  1. 在开始 CPU 分析后,如果您遇到错误,例如“重新定义失败,错误代码 62”,请尝试使用-XVerify:none参数运行应用程序。在 Eclipse 中,选择 Run | Run Configurations 菜单,然后在 Java Application 组下选择 CourseManagement 应用程序。转到 Arguments 选项卡,并将-Xverify:none添加到 VM arguments。再次运行应用程序。

  2. 在 VisualVM 分析器页面,单击设置复选框以查看用于分析的包。请注意,VisualVM 会自动选择这些包:

图 11.4:VisualVM 分析器设置

  1. 您必须停止 CPU 分析才能编辑设置。然而,我们将保留默认设置。取消选中设置框以隐藏设置。

  2. 点击 Monitor 表以查看分析活动的概述:

图 11.5:分析活动概述

  1. 现在,让我们在我们的应用程序中执行getCourse方法。转到 Eclipse 的控制台视图,其中我们的应用程序正在运行,输入一个字符(除了q),然后按Enter。转到 VisualVM 的 Profiler 选项卡以查看分析数据:

图 11.6:CourseManagement 的 CPU 分析

观察 Self time 列。这表示 CPU 时间或执行相应方法所花费的流逝时间,不包括执行从该方法调用的其他方法的时间。在我们的案例中,CourseDAO.getCourses花费了最长时间,因此它位于列表的顶部。这份报告可以帮助您识别应用程序中的瓶颈。

识别资源竞争

在多线程应用程序中,线程锁定或等待锁是很常见的。线程转储可用于识别资源竞争。让我们通过修改CourseManagement类的main方法来在我们的应用程序中模拟此场景,以在单独的线程中调用courseBean.getCourses

public class CourseManagement { 

  public static void main(String[] args) throws IOException { 

    final CourseBean courseBean = new CourseBean(); 

    System.out.println("Type any character to get courses. Type q 
     to quit."); 

    int ch, threadIndex = 0; 
    while ((ch = System.in.read()) != -1) { 
      if (ch != 10 && ch != 13) { //ignore new lines 
        if (ch == 'q') //quit if user types q 
          break; 

        threadIndex++; //used for naming the thread 
        Thread getCourseThread = new Thread("getCourseThread" + 
         threadIndex) 
{ 

          @Override 
          public void run() { 
            System.out.println("Getting courses"); 
            courseBean.getCourses(); 
            System.out.println("Got courses"); 
          } 
        }; 

        //Set this thread as Daemon so that the application can exit 
        //immediately when user enters 'q' 
        getCourseThread.setDaemon(true); 

        getCourseThread.start();  

        System.out.println("Type any character to get courses. 
         Type q to quit."); 
      } 
    } 

    System.out.println("Quitting ..."); 
  } 
} 

注意我们在while循环中创建一个新的Thread对象,并在线程的run方法中调用courseBean.getCourseswhile循环不等待getCourses返回结果,可以立即处理下一个用户输入。这将允许我们模拟资源竞争。

要实际引起资源竞争,让我们同步CourseService.getCourses:

public class CourseService { 

  private CourseDAO courseDAO = new CourseDAO(); 

  public synchronized List<CourseDTO> getCourses() { 
    return courseDAO.getCourses(); 
  } 
 } 

同步的getCourses方法将导致只有一个线程在CourseService类的实例中执行此方法。现在我们可以通过在控制台中输入字符来同时触发多个getCourses调用,而不必等待对getCourse方法的先前调用返回。为了给我们更多的时间获取线程转储,让我们将CourseDAO.getCourses中的线程睡眠时间增加到,比如说,30 秒:

public class CourseDAO { 

  public List<CourseDTO> getCourses() { 
    //No real database access takes place here. 
    //We will just simulate a long-running database operation 

    try { 
      Thread.sleep(30000); //wait 30 seconds 
    } catch (InterruptedException e) { 
      e.printStackTrace(); 
    } 

    //return dummy/empty list 
    return new ArrayList<>(); 
  } 
} 

运行应用程序,并在 VisualVM 中开始监控此进程。在 Eclipse 中运行应用程序的控制台窗口中,输入一个字符并按Enter。重复一次。现在,将触发两个getCourses调用。在 VisualVM 中,转到线程选项卡并单击 ThreadDump 按钮。一个新的线程转储将保存在进程节点下,并在新选项卡中显示。查找以getCourseThread为前缀的线程。以下是两个getCourseThreads的示例线程转储:

"getCourseThread2" daemon prio=6 tid=0x000000001085b800 nid=0x34f8 waiting for monitor entry [0x0000000013aef000] 
   java.lang.Thread.State: BLOCKED (on object monitor) 
  at 
 packt.jee.eclipse.profile.service.CourseService.getCourses(CourseService.java:13)  - waiting to lock <0x00000007aaf57a80> (a 
 packt.jee.eclipse.profile.service.CourseService) 
  at packt.jee.eclipse.profile.bean.CourseBean.getCourses(CourseBean.java:12) 
  at packt.jee.eclipse.profile.CourseManagement$1.run(CourseManagement.java:27) 

   Locked ownable synchronizers: 
  - None 

"getCourseThread1" daemon prio=6 tid=0x000000001085a800 nid=0x2738 waiting on condition [0x000000001398f000] 
   java.lang.Thread.State: TIMED_WAITING (sleeping) 
  at java.lang.Thread.sleep(Native Method) 
  at packt.jee.eclipse.profile.dao.CourseDAO.getCourses(CourseDAO.java:15) 
  at packt.jee.eclipse.profile.service.CourseService.getCourses(CourseService.java:13) 
  - locked <0x00000007aaf57a80> (a packt.jee.eclipse.profile.service.CourseService) 
  at packt.jee.eclipse.profile.bean.CourseBean.getCourses(CourseBean.java:12) 
  at packt.jee.eclipse.profile.CourseManagement$1.run(CourseManagement.java:27) 

   Locked ownable synchronizers: 
  - None 

从前面的线程转储中可以看出,getCourseThread2正在等待(to lock <0x00000007aaf57a80>)而getCourseThread1正在持有对该对象的锁(locked <0x00000007aaf57a80>)。

使用相同的技巧(检查锁),你还可以在应用程序中检测死锁。实际上,VisualVM 可以检测死锁并明确指出死锁的线程。让我们修改CourseManagement类的main方法来引起死锁。我们将创建两个线程并使它们以相反的顺序锁定两个对象:

警告

以下代码将使应用程序挂起。你必须杀死进程才能退出。

  public static void main(String[] args) throws IOException { 

    System.out.println("Type any character and Enter to cause 
     deadlock - "); 
    System.in.read(); 

    final Object obj1 = new Object(), obj2 = new Object(); 

    Thread th1 = new Thread("MyThread1") { 
      public void run() { 
        synchronized (obj1) { 
          try { 
            sleep(2000); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 

          synchronized (obj2) { 
            //do nothing 
          } 
        } 
      } 
    }; 

    Thread th2 = new Thread("MyThread2") { 
      public void run() { 
        synchronized (obj2) { 
          try { 
            sleep(2000); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 

          synchronized (obj1) { 

          } 
        } 
      } 
    }; 

    th1.start(); 
    th2.start(); 

MyThread1首先锁定obj1然后尝试锁定obj2,而MyThread2

首先锁定obj2然后尝试锁定obj1。当你使用 VisualVM 监控此应用程序并切换到线程选项卡时,你会看到“检测到死锁!”的消息:

图片

图 11.7:使用 VisualVM 检测死锁

如果你获取线程转储,它将特别显示死锁的原因:

Found one Java-level deadlock: 
============================= 
"MyThread2": 
  waiting to lock monitor 0x000000000f6f71a8 (object 
 0x00000007aaf56538, a java.lang.Object), 
  which is held by "MyThread1" 
"MyThread1": 
  waiting to lock monitor 0x000000000f6f4a78 (object 
 0x00000007aaf56548, a java.lang.Object), 
  which is held by "MyThread2" 

内存跟踪

VisualVM 可以用来监控内存分配并检测可能的内存泄漏。让我们修改我们的应用程序来模拟一个未释放的大内存分配。我们将修改CourseService类:

public class CourseService { 

  private CourseDAO courseDAO = new CourseDAO(); 

  //Dummy cached data used only to simulate large 
  //memory allocation 
  private byte[] cachedData = null; 

  public synchronized List<CourseDTO> getCourses() { 

    //To simulate large memory allocation, 
    //let's assume we are reading serialized cached data 
    //and storing it in the cachedData member 
    try { 
      this.cachedData = generateDummyCachedData(); 
    } catch (IOException e) { 
      //ignore 
    } 

    return courseDAO.getCourses(); 
  } 

  private byte[] generateDummyCachedData() throws IOException { 
    ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 
    byte[] dummyData = "Dummy cached data".getBytes(); 

    //write 100000 times 
    for (int i = 0; i < 100000; i++) 
      byteStream.write(dummyData); 

    byte[] result = byteStream.toByteArray(); 
    byteStream.close(); 
    return result; 
  } 
 } 

getCourses方法中,我们将创建一个大的字节数组并将其存储在一个成员变量中。分配给数组的内存将不会释放,直到CourseService实例不被垃圾回收。现在,让我们看看这个内存分配如何在 VisualVM 中显示出来。开始监控进程并转到 Profiler 标签页。单击 Memory 按钮开始监控内存。现在,回到 Eclipse 的控制台窗口并输入一个字符以触发getCourses方法。转到 VisualVM 以检查内存分析报告:

图 11.8:使用 VisualVM 进行内存监控

此报告显示了应用程序中不同对象消耗的内存的实时状态。然而,如果您想分析和找到确切的分配位置,那么请进行堆转储。转到 Monitor 标签页并单击 Heap Dump 按钮。堆转储报告保存在进程节点下。在堆转储报告中单击 Classes 按钮,然后单击 Size 列按内存消耗量降序排序对象:

图 11.9:堆转储报告中的类

根据报告,byte[]在我们的应用程序中占用最大内存。要找到内存分配的位置,双击包含byte[]的行:

图 11.10:堆转储中的对象实例报告

右下角的引用窗口显示了在左上窗口中选中实例持有引用的对象。如图所示,byte[]的引用由CourseServecachedData字段持有。此外,CourseService的引用由CourseBean持有。

大量内存分配并不一定意味着内存泄漏。您可能想在应用程序中保留对大对象的引用。然而,堆转储可以帮助您找到内存分配的位置以及该实例是否打算保留在内存中。如果不是,您可以在适当的位置找到内存分配并释放它。

我们所取的堆转储如果重启 VisualVM 将会丢失。因此,请将其保存到磁盘上;为此,右键单击堆转储节点并选择另存为。我们将在下一节中使用这个堆转储在 Eclipse 内存分析器中。

Eclipse 内存分析插件

Eclipse 内存分析器(eclipse.org/mat/)可以用来分析 VisualVM 创建的堆转储。它提供了额外的功能,如自动内存泄漏检测。此外,通过将其作为 Eclipse 插件使用,您可以从堆转储报告中快速跳转到源代码。您可以使用此工具作为独立应用程序或作为 Eclipse 插件。我们将在本节中看到如何将其用作 Eclipse 插件。

要安装内存分析器插件并分析内存转储,请执行以下步骤:

  1. 打开 Eclipse Marketplace(选择 Help | Eclipse Marketplace 菜单)。搜索Memory Analyzer并安装插件:

图片

图 11.11:在 Eclipse Marketplace 中搜索内存分析器插件

  1. 打开上一节中保存的堆转储文件。选择“文件”|“打开文件”菜单,并选择由 VisualVM 保存的.hprof文件。内存分析器将提示您选择报告类型:

图片

图 11.12:Eclipse 内存分析器:入门向导

  1. 选择“泄漏嫌疑报告”并点击“完成”。Eclipse 内存分析器创建带有几个问题嫌疑人的泄漏嫌疑报告:

图片

图 11.13:Eclipse 内存分析器:泄漏嫌疑报告

  1. 在第一个问题嫌疑人的“详细信息”链接上点击

图片

图 11.14:Eclipse 内存分析器:问题嫌疑详情

报告清楚地将CourseService中的cachedData识别为泄漏嫌疑。要打开源文件,请点击节点并选择“打开源文件”选项。

内存分析器还提供了许多其他有用的报告。有关更多详细信息,请参阅help.eclipse.org/oxygen/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html

摘要

随 JDK 6 及以上版本一起提供的 VisualVM 工具对于检测性能瓶颈和内存泄漏很有用。

在本章中,我们学习了如何在简单的 Java 应用程序中使用此工具。然而,这项技术也可以用于大型应用程序。可以使用 Eclipse 内存分析器快速从堆转储中检测内存泄漏。在下一章中,我们将学习如何在 JEE 中开发微服务。

第十二章:微服务

在上一章中,我们学习了如何对 Java 应用程序进行性能分析,以便解决性能问题。

在本章中,我们将学习如何使用 Eclipse 开发 JEE 微服务。我们还将学习如何在 Docker 容器中部署微服务。我们将为我们的课程管理用例开发简单的微服务。

我们将涵盖以下主题:

  • 微服务和 Eclipse MicroProfile 简介

  • 使用 WildFly Swarm 和 Spring Boot 框架开发 JEE 微服务

  • Docker 和 Docker Compose 简介

  • 在 Docker 容器中部署微服务

什么是微服务?

微服务是一个设计得很好的小应用,用于执行特定的业务任务。微服务通常实现为 RESTful Web 服务。以下是一些微服务的特点:

  • 尺寸较小(与单一应用相比),专注于单一业务任务/模块

  • 它拥有自己的数据库,与所有业务功能使用单一数据库的单一应用形成对比

  • 通常是一个独立的应用程序,其中包含捆绑的 Web 容器

通过组装较小的微服务可以构建大型业务应用。与大型单一应用相比,微服务架构提供了以下好处:

  • 它们易于部署。在单一应用中,由于应用的复杂性,部署可能相当繁琐。微服务体积小,可以轻松部署到服务器上。

  • 微服务是松散耦合的,因此一个微服务的变更可以与应用程序中其他服务的变更隔离。此外,为每个服务拥有单独的数据库可以进一步隔离主应用程序和其他服务,防止数据库模式变更带来的影响。

为了理解单一应用架构和微服务架构之间的对比,让我们看一个例子。在整个书中,我们一直在跟随课程管理的例子。假设这个模块是更大型的大学管理系统的一部分,该系统包含更多模块。这个应用的单一架构可以看作如下:

图片

图 12.1:单一应用架构

我们有一个大型应用,即大学管理系统,包含多个模块和单一数据库。

同一应用程序可以使用微服务按以下方式架构:

图片

图 12.2:微服务应用架构

在微服务架构中,大学管理系统由许多微服务组成,每个微服务都有自己的数据库。

Eclipse MicroProfile

可以使用当前的 JEE 规范(JEE 8)构建微服务。然而,JEE 中存在一些对开发微服务更为重要的规范,例如 JAX-RS(用于 RESTful Web 服务)和 JSON-P(用于处理 JSON 数据)。因此,一些组织联合起来制定了一系列用于开发和运行微服务的规范,这些规范被归类为 MicroProfile。MicroProfile 下的许多规范已经是 JEE 规范的一部分(如 JAX-RS 和 JSON-P),但也有一些是新的规范,例如用于配置和监控微服务的规范。

该小组到目前为止已经提出了两个配置文件。每个符合 MicroProfile 的实现都应实现所支持配置文件中的每个规范。这确保了使用特定配置文件创建的微服务可以在支持该配置文件的所有 Microprofile 实现上运行。在撰写本章时,该小组已经提出了两个配置文件。以下是 MicroProfiles 及其包含的规范列表:

  • MicroProfile 1.0(2016 年 9 月发布):

    • CDI 1.2

    • JSON-P 1.0

    • JAX-RS 2.0

  • MicroProfile 1.1(2017 年 8 月发布):

    • Config 1.0

    • CDI 1.2

    • JSON-P 1.0

    • JAX-RS 2.0

预计 MicroProfile 2.0 将于 2018 年 6 月发布,并将包括对 JEE 8 规范的一些更新。MicroProfile 的一些实现包括 WildFly Swarm (wildfly-swarm.io/)、WebSphere Liberty (developer.ibm.com/wasdev/websphere-liberty/)、Payara (www.payara.fish/)和 Apache TomEE (tomee.apache.org/)。有关 MicroProfile 的更多信息,请访问其官方网站microprofile.io/

在下一节中,我们将看到如何使用两种解决方案实现我们的课程管理用例的微服务:

  • 使用 MicroProfile 实现(WildFly Swarm)

  • 使用 Spring Boot,它不是 MicroProfile 的一部分

接下来,我们将看到如何将微服务部署到 Docker 容器中。

要跟随本章中的代码示例,您需要熟悉 JPA 和 REST API。请参考第四章,创建 JEE 数据库应用程序,了解 JPA 概念,以及第九章,创建 Web 服务,了解 RESTful Web 服务。

为微服务项目设置数据库

我们将实现一个微服务来获取课程列表。我们将使用与本书中相同的 MySQL 数据库,course_management。如果您需要有关如何安装和设置 MySQL 的信息,请参阅第一章 介绍 JEE 和 Eclipse 中的 安装 MySQL 部分。如果您尚未创建 course_management 模式,请参阅第四章 创建 JEE 数据库应用程序 中的 创建数据库模式 部分。第四章。此时,我们将假设 MySQL 数据库正在运行,并且包含 CourseCourse_StudentStudentTeacher 表的 course_management 模式存在。

我们将使用 JPA 访问此数据库。如果您不熟悉 JPA,请参阅第四章 使用 JPA 创建数据库应用程序 部分中的 创建 JEE 数据库应用程序,第四章。我们将使用 EclipseLink 作为 JPA 提供者。

使用 WildFly Swarm 实现微服务

WildFly Swarm (wildfly-swarm.io/) 是 Red Hat 的 MicroProfile 实现。它允许您仅使用所需的规范来组装应用程序容器以运行微服务。

创建 WildFly Swarm 项目

让我们在 wildfly-swarm.io/generator/ 使用 WildFly Swarm 项目生成器选择我们想要包含在我们的应用程序中的规范,并创建起始项目:

图片

图 12.3:WildFly Swarm 项目生成器

输入组 ID 和工件 ID,如前一张截图所示。在依赖项文本框中,开始输入如 JPA 或 JAX-RS 等功能,然后从自动建议的选项中选择它们。确保已选择 JPA EclipseLink、JAX-RS 和 CDI 作为依赖项。如果您想查看所有可用的依赖项并从中选择,请点击查看所有可用依赖项链接。

点击生成项目按钮以创建项目和下载 ZIP 文件。这是一个 Maven 项目。将文件解压到一个文件夹中,并在 Eclipse 中将其作为 Maven 项目导入(通过选择菜单选项文件 | 导入,然后在 Maven 类别中选择现有 Maven 项目)。

右键点击 Eclipse 项目资源管理器,选择运行方式 | Maven 构建。在配置窗口中,在目标字段中输入 wildfly-swarm:run

图片

图 12.4:创建 WildFly Swarm 应用程序的 Maven 构建配置

点击运行。Maven 将下载和安装依赖项,然后运行应用程序(当应用程序准备就绪时,您将在控制台中看到 Wildfly Swarm is Ready 消息)。打开 http://localhost:8080/hello 以测试应用程序生成器创建的默认端点。您应该看到 hello 消息。

如果你查看项目的目标文件夹,你会看到 demo-swarm.jardemo.war。当我们执行 wildfly-swarm:run 目标时,Maven 启动 JBoss 容器并部署 WAR 文件。微服务也可以通过执行单个 JAR 文件 demo-swarm.jar 来运行。这个 JAR 包含了所有包,包括运行微服务所需的应用服务器。只需从命令行运行它即可:

java –jar demo-swarm.jar

要将输出文件名从 demo 改为,例如,coursemanagement,请更改 <build> 标签下的 <filename> 中的名称。

配置 JPA

现在,让我们在项目中添加 MySQL 的依赖项。参考 第四章 中 创建 JEE 数据库应用程序 部分的 图 4.11,添加 Maven 依赖项以使用 MySQL JDBC 驱动程序,或者简单地将以下依赖项添加到 pom.xml 中:

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.8-dmr</version>
</dependency>

将项目转换为 JPA 项目,以便我们可以使用 Eclipse 提供的 JPA 工具。在项目资源管理器中右键单击项目,选择 Configure | Convert to JPA Project 选项。确保选中以下项目特性,包括默认特性:

  • 动态 Web 模块

  • JAX-RS(RESTful Web 服务)

  • JPA

点击下一步按钮(参考 第四章 中 创建 JEE 数据库应用程序 部分的 图 4.20 向项目中添加 JPA 特性),并按 图 4.21 中所示配置 JPA 特性。点击完成。

现在我们来配置 persistence.xml 中的 JDBC 连接。按照 第四章 中 将项目转换为 JPA 项目 部分的步骤 7 到 9 进行,创建 JEE 数据库应用程序。现在你的 persistence.xml 应该包含以下持久化单元:

<persistence-unit name="coursemanagement" transaction-type="RESOURCE_LOCAL">
  <properties>
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
       <class>packt.book.jeeeclipse.wildflyswarm.coursemanagement.rest.Course</class>
    <property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/course_management"/>
    <property name="javax.persistence.jdbc.user" value="<enter_your_user_name> "/>
    <property name="javax.persistence.jdbc.password" value="<enter_your_password> "/>
  </properties>
</persistence-unit>

在之前的 XML 文件中,我们指定 org.eclipse.persistence.jpa.PersistenceProvider 类作为我们的 JPA 提供程序,然后设置连接到 MySQL 数据库的属性。

接下来,在 src/main 下创建名为 resources/META-INF 的文件夹,并将 persistence.xml 复制到 src/main/resources 文件夹中。如果 Eclipse 在 JPA 配置中显示错误,请在项目资源管理器中右键单击项目名称,选择 Maven | 更新项目。这样做的原因是 Maven 预期你想要复制到 classes 文件夹的文件位于 src/main/resources 文件夹中。我们需要在 classes 文件夹中拥有 META-INF/persistence.xml,以便 JPA 提供程序可以加载它。

创建课程实体和 JPA 工厂

如果你不太熟悉 JPA,请参考 第四章 中 创建 JEE 数据库应用程序 部分的 JPA 概念 部分。

我们现在将在 packt.book.jeeeclipse.wildflyswarm.coursemanagement.rest 包中创建 Course.java

package packt.book.jeeeclipse.wildflyswarm.coursemanagement.rest;

// skipping imports to save space

@Entity
@Table(name="\"Course\"")
@NamedQuery(name="Course.findAll", query="SELECT c FROM Course c")
public class Course implements Serializable {
  private static final long serialVersionUID = 2550281519279297343L;

  @Id
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  @Column(name="id")
  private int id;

  @NotNull
  @Column(name="name")
  private String name;

  @Min(1)
  @Column(name="credits")
  private int credits;

  // skipping getter and setters to save space
}

这是一个简单的 JPA 实体类,带有适当的注解。我们需要告诉 JPA 这是一个受管理的 bean。为此,打开persistence.xml,并在编辑器的“常规”选项卡中,点击“受管理类”部分中的“添加”按钮。将Course实体类添加到列表中。

创建一个名为CourseManagementJPAFactory的 JPA EntityManagerFactory类:

package packt.book.jeeeclipse.wildflyswarm.coursemanagement.rest;

// skipping imports to save space

@ApplicationScoped
public class CourseManagementJPAFactory {
  private EntityManager _entityManager;

  public EntityManager getEntityManager() {
    if (_entityManager != null) return _entityManager;

    EntityManagerFactory factory = Persistence.createEntityManagerFactory("coursemanagement");

    _entityManager = factory.createEntityManager();

    return _entityManager;
 }
}

在这个类中,我们正在从EntityManagerFactory创建EntityManager的实例。请注意,传递给Persistence.createEntityManagerFactory方法的名称与我们指定的persistence.xml中的名称相同。

最后,我们将创建主类,称为CourseManagementEndpoint,以及处理/course_management/courses URL 路径的 REST 端点函数:

package packt.book.jeeeclipse.wildflyswarm.coursemanagement.rest;
// skipping imports to save space

@ApplicationScoped
@Path("/course_management")
public class CourseManagementEndpoint {
  @Inject
  private CourseManagementJPAFactory jpaFactory;

  @GET
  @Path("/courses")
  @Produces(MediaType.APPLICATION_JSON)
  public List<Course> doGet() {
    EntityManager entityManager = jpaFactory.getEntityManager();
    TypedQuery<Course> courseQuery = entityManager.createNamedQuery("Course.findAll", Course.class);
    List<Course> courses = courseQuery.getResultList();
    return courses;
  }
}

如果应用程序尚未运行,请在项目资源管理器中右键单击项目,然后选择运行 As | Maven 构建。在浏览器中打开http://localhost:8080/course_managment/courses,你应该会看到数据库中课程的 JSON 列表。

要将默认服务器端口从8080更改为任何其他端口号,例如8000,请设置swarm.http.port=8000环境变量。您可以在项目的运行配置中设置此变量(从主菜单中选择运行 | 运行配置,然后在 Maven 构建部分查找您项目的配置):

图 12.5:在运行配置中设置环境变量

点击环境标签并添加环境变量及其值。

使用 Spring Boot 实现微服务

微服务可以通过多种方式实现;在前一节中,我们看到了使用 WildFly Swarm 实现微服务的一种方法,WildFly Swarm 是一个 MicroProfile 实现。在本节中,我们将了解如何使用 Spring Boot 实现微服务,Spring Boot 不是一个 MicroProfile 实现,但是一个非常流行的框架。

Spring Boot (spring.io/projects/spring-boot/) 是一个用于创建独立 Spring 应用程序的框架。有关 Spring 和 Spring MVC 框架的更多信息,请参阅第八章,使用 Spring MVC 创建 Web 应用程序。与 WildFly Swarm 项目生成器类似,Spring Boot 也有一个用于创建 Spring Boot 入门应用程序的网页,您可以在其中选择要包含在应用程序中的 JEE 功能/规范。访问start.spring.io/

图 12.6:Spring Boot 项目生成器

选择 Web、JPA 和 Jersey(JAX-RS)依赖项。下载入门项目并将其解压缩到一个文件夹中。由于我们已将 JPA 选为应用程序的一个依赖项,Spring Boot 期望我们在src/main/resources中的application.properties文件中配置数据库连接属性。将以下属性添加到application.properties中:

spring.datasource.url = jdbc:mysql://localhost/course_management?autoReconnect=true&useSSL=false
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.username=<your_user_name>
spring.datasource.password=<your_passwod>
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

我们现在可以运行服务器了,但我们还没有定义任何 REST 端点。所以,让我们来做这件事。我们将使用上一节中为 WildFly Swarm 项目创建的Course实体 bean。因此,将相同的文件复制到这个项目中,在packt.book.jeeeclipse.springboot.coursemanagementspring包中。参见创建课程实体 bean 和 JPA 工厂部分中的Course类列表。

Spring 提供了一个名为CrudRepository的实用接口,它告诉框架为给定的实体/类创建 CRUD 样板代码。我们将创建一个扩展CrudRepository的仓库接口,并为Course类创建一个 CRUD 实现。有关CrudRepository的更多信息,请参阅docs.spring.io/spring-data/data-commons/docs/1.6.1.RELEASE/reference/html/repositories.html

package packt.book.jeeeclipse.springboot.coursemanagementspring;
import org.springframework.data.repository.CrudRepository;
public interface CourseRepository extends CrudRepository<Course, Long>{
}

这只是一个标记接口,用于告诉 Spring 框架为具有类型Long主键的Course类/实体创建 CRUD 代码。

在 Spring 中,通过创建一个控制器并实际上用@RestController注解类来创建 REST 端点。有关使用 Spring 创建 RESTful Web 服务的更多信息,请参阅spring.io/guides/gs/rest-service/。因此,让我们创建CourseController类:

package com.example.demo;
// skipping imports to save space

@RestController
public class CourseController {
  @Autowired
  private CourseRepository courseRepository;

  @RequestMapping(value = "/course_management/courses", method = RequestMethod.GET)
  public Iterable<Course> getCourses() {
    return courseRepository.findAll();
  }
}

在这个类中,我们将 GET HTTP 请求映射到/course_management/courses URL 的getCourses方法。

使用@Autowired注解,CourseRepository的实例会自动注入到这个类中。

我们现在准备好运行应用程序。通过在项目资源管理器中右键单击项目并选择“运行 As | Maven Build”来为此应用程序创建一个运行配置。然后,在 Goals 字段中输入spring-boot:run(参见图 12.4以获取参考),然后单击运行按钮。一旦服务器准备就绪,请浏览到http://localhost:8080/course_management/courses,你应该会看到 JSON 输出(对于Courses)。

要将默认服务器端口从8080更改为任何其他端口号,例如8000,请设置环境变量server.port=8000。参见图 12.5以获取参考。

请参阅docs.spring.io/spring-boot/docs/current/reference/htmlsingle/以获取 Spring Boot 的完整参考。

在 Docker 容器中部署微服务

在本节中,我们将学习如何在 Docker 容器中部署微服务,但首先让我们了解什么是 Docker。

什么是 Docker?

Docker 是容器管理软件。通常,软件容器允许你将应用程序及其所有依赖项,包括操作系统,打包在一个包中。你的应用程序在打包的容器中独立运行。这减少了在开发、测试和部署时环境差异。由于你的应用程序的所有依赖项都已经解决并与其打包,因此你通常不会遇到在开发/测试环境中应用程序运行良好,但在生产环境中失败的情况——可能是因为某些依赖项未满足。例如,即使你在相同的操作系统版本上开发和测试,在生产环境中,由于操作系统更新,某些依赖项可能会失败。

Docker 是目前最受欢迎的容器管理技术。Docker 使得将你的应用程序打包并运行在容器中变得非常容易。它通常与虚拟机进行比较。以下图表显示了它们之间的区别:

图 12.7:虚拟机技术与 Docker 容器技术之间的区别

在图 12.7 中,虚拟机是运行在虚拟化软件(虚拟机管理程序将客户操作系统与宿主操作系统隔离开来并管理它们)之上的客户操作系统。Docker 容器运行在 Docker 引擎和共享的操作系统内核(例如,Linux 或 Windows)之上。Docker 容器不是完整的操作系统;它们是具有隔离资源(如文件系统和网络)的进程。

与虚拟机相比,Docker 容器易于打包和部署,并且启动速度要快得多(因为它们只是进程,而不是完整的操作系统)。Docker 容器也比虚拟机占用更少的资源。因此,你可以在相同的环境中运行比虚拟机更多的 Docker 容器。

查看这个官方 Docker 链接,www.docker.com/what-docker,获取更多信息。

如何获取 Docker

从这里下载 Mac 版本的 Docker:docs.docker.com/docker-for-mac/install/

从这里下载 Windows 版本的 Docker:docs.docker.com/docker-for-windows/install/

从这里下载 Linux 版本的 Docker:docs.docker.com/engine/installation/

如何使用 Docker

在本节中,我们将简要介绍如何使用 Docker。要创建一个新的容器,你通常需要创建一个 Dockerfile。在这个文件中,你需要指定要扩展容器的基镜像,例如 Ubuntu 或 Debian 的基镜像。你可以将 Docker 镜像视为模板,容器则是这些模板的运行实例。Docker Hub,hub.docker.com/,是 Docker 镜像的存储库。

Dockerfile

您创建一个 Dockerfile 来创建自己的容器镜像。您可以指定容器的基镜像、设置容器时执行的命令、要公开的端口、要复制到容器的文件以及入口点(容器启动时运行的程序)。以下是 Dockerfile 中常用的一些指令:

  • FROM: 指定 Docker 容器的基镜像,例如,FROM Ubuntu

  • ADD: 从主机机器添加文件(s)到 Docker 容器。例如,将setup.sh文件从运行 Docker 命令的目录复制到容器中。例如,ADD ./setup.sh /setup.sh

  • RUN: 在容器中运行一个命令。例如,在将setup.sh文件复制到容器后使其可执行。例如,RUN chmod +x /setup.sh

  • ENTRYPOINT: Docker 容器旨在有一个主要的应用程序,当它停止运行时,容器也会停止。该主程序使用ENTRYPOINT指令指定。例如,在安装后(可能使用RUN命令)运行 Apache 服务器时,ENTRYPOINT apachectl start

  • CMD: 一个执行命令。在没有ENTRYPOINT的情况下,CMD指定容器中的主应用程序。如果与ENTRYPOINT一起指定,则CMD的值作为ENTRYPOINT中指定应用程序的参数传递。

  • EXPOSE: 告诉 Docker 容器在运行时监听指定的端口(s)。例如,如果 Apache 服务器在容器中监听端口80,则您将指定EXPOSE 80

  • ENV: 在容器中设置环境变量(s)。例如,ENV PATH=/some/path:$PATH

  • VOLUME: 创建一个可挂载的卷点。卷就像一个文件夹或虚拟文件夹。在容器内部,它可以像其他文件夹一样访问。卷可以用于在不同运行的容器之间共享文件夹。一个容器也可以从另一个容器导入卷。

这是一个 Dockerfile 中常用 Docker 指令的列表。有关所有指令的详细信息,请参阅docs.docker.com/engine/reference/builder/中的 Dockerfile 参考。

Docker 命令

这里是启动、停止和删除等操作的一些 Docker 命令的简短列表:

操作 命令

| 从镜像运行容器 | 语法如下:

docker run –name <container_name> <options> <base_image> <command_to_run>

例如,要从 Ubuntu 镜像运行容器,打开终端并使用以下命令执行 bash shell:

docker run -name my-ubuntu -it ubuntu bash

|

| 从 Dockerfile 创建镜像 | 语法如下:

docker build <options> <folder_of_dockerfile>

例如,要从当前文件夹中的 Dockerfile 创建my_image,运行以下 Docker 命令:

docker build -t image_name

|

列出当前运行的容器
docker ps

|

列出所有容器,包括停止的容器
docker ps -a

|

| 启动(停止的)容器 | 语法如下:

docker start -i <container>

-i选项保持stdin(标准输入)开启,并允许您在容器中运行命令。要识别容器,您可以使用容器名称或 ID。|

删除容器
docker rm <container>

|

| 在运行容器中执行命令 | 语法如下:

docker exec <options> <container> <command>

例如,要在名为my_container的运行容器中打开 bash shell,请执行以下命令:

docker exec –it my_container bash

|

列出所有镜像
docker images

|

| 删除镜像 | 在此命令中,镜像 ID 由空格分隔:

docker rmi <image_ids>

|

获取运行容器的信息
docker inspect <container>

|

查看docs.docker.com/engine/reference/commandline/docker/获取完整参考。

这是对 Docker 的简要介绍。本书范围之外还有许多关于 Docker 的详细信息。请参阅提供的链接以及 Docker 网站(www.docker.com/)获取更多信息。现在我们将专注于 Eclipse Docker 工具以及如何在 Docker 容器中部署微服务。

在 Eclipse 中设置 Docker 工具

有一个 Eclipse 的 Docker 插件,使用它可以执行许多提到的 Docker 任务。要在 Eclipse 中安装插件,从菜单中选择帮助 | Eclipse Marketplace.... 搜索Eclipse Docker Tooling并安装它:

图片

图 12.8:从 Eclipse Marketplace 安装 Eclipse Docker 工具

切换到 Docker 工具视角(要么点击编辑窗口右上角的“打开视角”工具栏按钮,要么从“窗口 | 视角 | 打开视角 | 其他”菜单中选择)。

现在我们将在 Eclipse 中添加一个 Docker 连接(确保 Docker 守护进程正在运行):

图片

图 12.9:添加 Docker 连接

在 Docker 资源管理器中点击“添加连接”工具栏按钮并创建连接,如图下截图所示:

图片

图 12.10:添加连接对话框

在 Windows 上,您需要选择 TCP 连接并指定 Docker 守护进程监听的 URI。您可以在 Docker 设置中的“常规”选项卡中找到 URI。请确保已选中“在...公开守护进程”选项。从该选项复制 TCP URI 并将其粘贴到对话框中图 12.10所示的“TCP 连接 | URI”文本框中。

一旦成功添加连接,您将看到您本地机器上现有容器和图像的列表,如果有。

创建 Docker 网络

我们将在同一台机器上的两个不同的 Docker 容器中部署两个服务器:一个 MySQL 数据库服务器和一个运行我们的微服务的应用服务器。应用服务器需要知道数据库服务器以便访问它。允许两个 Docker 容器相互访问的推荐方法是部署它们在同一个 Docker 网络中。关于 Docker 网络的完整讨论超出了本书的范围,因此鼓励读者阅读有关 Docker 网络的文档docs.docker.com/engine/userguide/networking

知道我们即将创建的两个容器需要在同一个 Docker 网络中运行,让我们通过运行以下命令来创建一个 Docker 网络:

docker network create --driver bridge coursemanagement

在此命令中,coursemanagment是我们正在创建的网络名称。

创建 MySQL 容器

在本书中,我们迄今为止一直在使用安装在主机机器上的 MySQL 服务器。现在我们将看到如何创建一个带有 MySQL 的 Docker 容器。如果你在你的主机操作系统(Docker 运行的操作系统)上运行 MySQL 实例,那么停止该实例或将 MySQL 配置为在 Docker 容器中不同于3306的端口上运行(我们很快就会看到如何做到这一点)。

我们将使用官方的 MySQL Docker 镜像;请参阅hub.docker.com/_/mysql/。运行以下命令:

docker run --name course-management-mysql -e MYSQL_ROOT_PASSWORD=your_password –p 3306:3306 --network=coursemanagement -d mysql

your_password替换为你想要设置的 root 密码。此命令将安装 MySQL 的最新版本。-d选项以分离/后台模式运行容器。此外,请注意,容器是在我们之前创建的coursemanagement网络中创建的。如果你想要使用 MySQL 的特定版本,那么标记该版本;例如,要安装 MySQL 版本 5.5.58,请使用以下命令:

docker run --name course-management-mysql -e MYSQL_ROOT_PASSWORD=your_password -d –p 3306:3306 --network=coursemanagement mysql:5.5.58

MySQL将在容器中运行于端口3306,并且容器会在主机机器上以相同的端口暴露服务。若要在主机机器上以不同的端口(例如端口3305)暴露此服务,请使用–p--publish选项:

docker run --name course-management-mysql -e MYSQL_ROOT_PASSWORD=your_password –p 3305:3306 --network=coursemanagement –d mysql

此命令中的-p选项将 Docker 容器中的端口3306映射到主机机器上的端口3305

一旦命令执行成功,你可以通过执行docker ps命令来验证容器正在运行。容器也将出现在 Eclipse 的 Docker Explorer 中。切换到 Eclipse 的 Docker Tooling 视角,并展开 Local connection 下的 Containers 组:

图片

图 12.11:Docker Explorer 列出容器和镜像

右键单击容器名称以显示对容器执行不同操作(如启动、停止和重启)的菜单选项。

执行 Shell 选项非常有用,可以在容器中打开 shell 并执行命令。例如,要从容器内部执行 MySQL 命令,请选择执行 Shell 选项并执行mysql -u root –p命令:

图片

图 12.12:在 Docker 容器中执行 Shell

假设您已将容器中的端口 3306 映射到主机机器上的相同端口,您可以从主机机器连接到容器中的 MySQL 实例,如下所示:

mysql -h 127.0.0.1 –u root -p

确保您指定了 -h--host 选项,否则它将尝试使用本地的 .sock 文件进行连接,这将失败。您也可以从 MySQL Workbench 连接到这个 MySQL 实例。

接下来,在数据库中创建 course_management 模式。有关详细信息,请参阅本章的 设置数据库 部分。

如果您不想输入长的 Docker 命令并记住选项,可以使用 Docker Explorer 的用户界面来创建容器。我们使用 Docker 的 run 命令通过 mysql 镜像运行 MySQL 容器。该命令首先检查所需的镜像是否已经下载到本地机器上,如果没有,则下载它。也可以使用 docker pull 命令显式下载 Docker 镜像。例如,我们首先可以通过执行以下命令下载 mysql 镜像:

docker pull mysql

下载镜像后,它将在 Docker 探索器中显示。右键单击镜像并选择运行:

图片

图 12.13:在 Docker 探索器中从镜像创建 Docker 容器

按照向导创建一个容器。您可以使用此选项从相同的镜像创建多个实例,例如,运行多个 MySQL 容器。

在此向导的最后一页,您可以指定容器的网络。

在 Docker 容器中部署微服务

现在,我们将在此章中创建的 CourseManagement 微服务(使用 WildFly Swarm)部署到 Docker 容器中。您可以选择复制项目并将其粘贴到 Eclipse 项目资源管理器中,使用不同的名称,或者使用相同的项目。示例代码中有一个名为 coursemanagement-docker 的项目用于本节。

我们需要在 persistence.xml 中进行一项更改。回想一下,在我们之前的示例中,此文件中的 JDBC URL 指的是 127.0.0.1 或本地主机。那时这可行,因为应用程序和数据库都在同一环境中运行。但现在我们的数据库和应用程序将分别在单独的 Docker 容器中运行,具有隔离的运行时环境。因此,我们不能再使用微服务中的 localhost URL 访问数据库。那么,我们如何访问在单独容器中运行的数据库呢?答案是使用容器名称,如果两个容器都在相同的 Docker 网络模式下运行。我们已将数据库容器的容器配置为在 coursemanagment 网络中运行,在本节的后面我们将对微服务容器做同样的操作。因此,我们需要将 persistence.xml 中的 JDBC URL 更改为指向运行数据库服务器的容器名称,该名称为 course-management-mysql

打开 persistence.xml 并将 JDBC URL 中的 IP 127.0.0.1 替换为 course-management-mysql

<property name="javax.persistence.jdbc.url" value="jdbc:mysql://course-management-mysql/course_management?autoReconnect=true&amp;useSSL=false"/>

接下来,在项目的根目录中创建一个名为 Dockerfile 的文件,内容如下:

FROM openjdk:8
ENV swarm.http.port 8080
RUN mkdir microservices
COPY ./target/coursemanagement-swarm.jar ./microservices
EXPOSE 8080
ENTRYPOINT java -jar -Djava.net.preferIPv4Stack=true ./microservices/coursemanagement-swarm.jar

我们将使用这个 Dockerfile 来创建我们的微服务容器的镜像。让我们理解这个文件中的每条指令:

  • FROM openjdk:8: 这个容器的基镜像是 OpenJDK,版本 8。

  • ENV swarm.http.port 8080: 我们正在设置容器中的swarm.http.port环境变量。在这个示例中,这实际上并不是必要的,因为 WildFly Swarm 服务器默认运行在端口8080。如果你想将服务器运行在不同的端口上,请更改端口号。

  • RUN mkdir microservices: 我们在容器中创建一个名为microservices的文件夹。

  • COPY ./target/coursemanagement-swarm.jar ./microservices: 我们正在将项目中的目标文件夹中的coursemanagement-swarm.jar复制到容器的microservices文件夹中。

  • EXPOSE 8080: 我们请求 Docker 引擎从容器中暴露端口8080。我们的应用程序服务器在容器中监听端口8080上的请求。

  • ENTRYPOINT java -jar -Djava.net.preferIPv4Stack=true ./microservices/coursemanagement-swarm.jar: 最后,我们指定在容器中执行的主应用程序,该应用程序运行独立的微服务应用程序。

我们需要构建应用程序以创建一个单一的 JAR 文件,我们将在 Docker 容器中运行它。如果你尝试通过运行 Maven 目标wildfly-swarm:run(我们之前这样做来运行应用程序)来构建应用程序,它将会失败,因为它也会尝试运行应用程序。这不会工作,因为我们修改了persistence.xml中的 JDBC URL,使用了 DB 容器的名称。因此,运行 Maven 目标仅打包应用程序,不运行测试。在项目资源管理器中右键单击项目,选择“运行方式”|“Maven 构建”:

图片

图 12.14:Eclipse 运行配置以打包 Docker-microservice 项目

在“目标”字段中输入package。选择跳过测试选项,然后点击运行以在目标文件夹中创建应用程序 JAR 文件。

现在让我们从我们创建的 Dockerfile 创建 Docker 镜像。在项目资源管理器中右键单击文件,然后选择“运行方式”|“Docker 镜像构建”菜单选项。

图片

图 12.15:从 Dockerfile 构建 Docker 镜像

这将创建一个名为coursemanagement-microservice的 Docker 镜像,并标记为 1.0 版本。切换到 Eclipse 中的 Docker 工具视图,你应该能看到这个镜像列出来。

我们将创建这个镜像的一个实例,也就是说,从这个镜像创建一个容器,这个容器将实际运行我们的微服务。在镜像上右键单击,然后选择“运行...”:

图片

图 12.16:从镜像创建容器

这将打开一个向导来配置容器:

图片

图 12.17:配置 Docker 容器

在向导的第一页中为容器指定一个名称。留空端点和命令;镜像已经通过 Dockerfile 中我们指定的 ENTRYPOINT 创建。你可以在这一页中覆盖它,但我们不会这么做。

确保未勾选“将所有公开端口发布到主机接口上的随机端口”选项。我们希望将容器的端口 8080 以相同的端口号发布到主机。点击下一步。在第二页上保留默认选项,然后再次点击下一步。

图片

图 12.18:设置 Docker 容器的网络模式

最后一页(见 图 12.18)允许你为容器指定一个网络。在这里,我们将指定我们之前创建的网络,coursemanagement。回想一下,我们还创建了一个具有相同网络名称的 MySQL 容器,这样微服务容器就可以通过容器名称访问 MySQL 容器。

一旦应用程序在微服务容器中启动,请浏览到 http://localhost:8080/course_management/courses,你应该能看到数据库中的课程列表。

使用 Spring Boot 创建的微服务的部署过程与我们在本节中看到的类似。主要区别在于,在 Spring Boot 项目中,你需要更新 application.properties 中的 JDBC URL,而不是本节中我们修改的 persistence.xml。供你参考,示例代码有一个名为 coursemanagementspring-docker 的项目。

使用 Docker Compose 运行容器

在前面的章节中,我们已经看到了如何通过运行命令行 Docker 命令(或从 Eclipse 插件)单独创建 Docker 容器。如果你想在主机机器上运行多个容器,你应该考虑使用 Docker Compose。它允许你在一个文件中配置多个 Docker 容器,并指定它们之间的依赖关系。一个 docker-compose 命令从 docker-compose.yml 读取配置/指令,创建并运行容器。.yml 文件需要在顶部指定 docker-compose 的版本号,然后是服务部分,其中列出容器定义——指定镜像或 Dockerfile 位置、要在容器中设置的环境变量、要公开并映射到主机 OS 的端口,以及许多其他配置。有关更多详细信息,请参阅 docs.docker.com/compose/overview/

在本节中,我们将使用 docker-compose 同时运行 MySQL 和我们的 webservice 容器。在后续章节中关于在云中部署 JEE 应用程序的部分,我们将使用此配置进行部署。首先,从 docs.docker.com/compose/install/ 安装 docker-compose

创建一个新的通用 Eclipse 项目(文件 | 新建 | 项目,然后通用 | 项目),并将其命名为 coursemanagement-docker-compose。我们不需要 JEE 项目,因为我们将使用上一节中创建的单个 JAR 文件作为我们的微服务,并将其部署在 Docker 容器中。因此,将 coursemanagementspring-docker/coursemanagementspring-0.0.1-SNAPSHOT.jar 复制到项目文件夹。

我们需要在容器中创建和初始化一个 MySQL 数据库。我们将使用包含数据定义语言(例如,CREATE)语句的 SQL 脚本来创建数据库模式和表。本节源代码项目 coursemanagement-docker-compose 有一个名为 course-management-db.sql 的文件,其中包含 DDL 语句。此脚本创建空表,不包含数据。

如果您还想从现有的数据库导出数据,则可以创建 MySQL Workbench 中的脚本。从 MySQL Workbench 中,选择服务器 | 数据导出。选择要导出的模式,course_management。从下拉选项中选择导出结构和数据。在导出选项中,选择导出到自包含文件,并指定文件路径,例如,<your_project_path>/course-management-db.sql。然后,单击开始导出按钮。

现在,让我们在项目中创建两个 Dockerfile:

  • course-management-db.dockerfile 用于 MySQL 容器

  • course-management-service.dockerfile 用于微服务容器

使用以下内容创建 course-management-db.dockerfile

 FROM mysql:5.7
 COPY ./course-management-db.sql /docker-entrypoint-initdb.d
 ENV MYSQL_ROOT_PASSWORD root

在此文件中的 COPY 语句中,我们将 course-management-db.sql 从项目文件夹复制到容器中的 docker-entrypoint-initdb.d 文件夹。此文件中的任何 SQL 脚本都将由基础 MySQL 镜像执行以初始化数据库。请参阅 hub.docker.com/_/mysql/ 中的 初始化新实例 部分。

使用以下内容创建 course-management-service.dockerfile

FROM openjdk:8
RUN mkdir microservices
COPY ./coursemanagementspring-0.0.1-SNAPSHOT.jar ./microservices
EXPOSE 8080
ENTRYPOINT java -jar -Djava.net.preferIPv4Stack=true ./microservices/coursemanagementspring-0.0.1-SNAPSHOT.jar

在此 Dockerfile 中,我们使用 openjdk:8 基础镜像创建容器。然后,我们在容器中创建一个名为 microservices 的文件夹,并将 coursemanagementspring-0.0.1-SNAPSHOT.jar 从项目文件夹复制到容器中的 microservices 文件夹。然后,我们使用执行复制的 JAR 文件的命令设置容器的 ENTRYPOINT

最后,创建 docker-compose.yml,内容如下:

version: "3"
services: 
  course-managemnt-db:
    build: 
      context: .
      dockerfile: course-management-db.dockerfile
    container_name: course-management-mysql
    ports:
      - 3306:3306
  course-management-service:
    build:
      context: .
      dockerfile: course-management-service.dockerfile
    container_name: course-management-service
    ports:
      - 80:8080
    depends_on:
      - course-managemnt-db  

在此文件中,我们创建了两个服务:course-managemnt-db 用于数据库容器和 course-management-service 用于微服务容器。两者都是基于单独的 Dockerfile 构建的。上下文字段指定包含 Dockerfile 的文件夹路径;在这种情况下,它是当前文件夹(即项目文件夹)。请注意,我们已指定 course-management-service 容器对 course-managemnt-db 的依赖。这导致数据库容器在微服务容器之前启动。

我们正在将微服务容器的端口8080映射到主机的端口80。原因是我们将稍后在云中部署这些服务,并使用默认的 Web 服务器在端口80上。

警告本章中 JEE 容器的部署仅用于开发和测试目的。它不适用于生产环境,也不遵循生产环境最佳实践。这属于 DevOps 的范畴,而不在本书的范围之内。

由于这两个服务都在同一个docker-compose.yml文件中,docker-compose创建了一个网络并将两个容器都添加到该网络中。因此,course-management-service容器可以通过其名称访问course-management-mysql容器。我们不需要像上一节那样创建一个单独的网络。

请参阅docs.docker.com/compose/compose-file/以获取更多配置选项的docker-compose文件参考。

要一起启动docker-compose.yml中配置的所有容器,请从命令提示符运行以下命令(确保端口80没有被其他进程占用,因为我们已经将微服务容器的端口8080映射到主机的端口80):

docker-compose up

一旦容器成功启动,请浏览到http://localhost/course_management/courses,你应该会看到课程列表,如果没有课程在数据库中,则显示空列表。

要以分离/后台模式运行容器,请运行以下命令:

docker-compose up –d

要停止使用docker-compose启动的容器,请运行以下命令:

docker-compose down

如果你修改了 Dockerfile 或docker-compose.yml,那么你需要重新构建镜像。运行以下命令来完成此操作:

docker-compose build

请参考docs.docker.com/compose/reference/overview/获取关于docker-compose命令行选项的详细信息。

摘要

微服务是一个为单个用例提供服务的应用程序。微服务通常是 REST 服务,可以快速部署。Docker 容器非常适合部署微服务,因为它们允许应用程序在隔离的环境中运行,在开发、测试和生产环境之间几乎没有差异。Docker 容器也可以非常快速地部署,并且可以很好地扩展。

在本章中,我们看到了如何使用 WildFly Swarm 和 Spring Boot 开发微服务。我们创建了一个简单的微服务来列出我们的课程管理应用程序的课程。我们学到的概念可以扩展到使用其他框架创建微服务。我们还学习了如何使用 Docker Tooling 的 Eclipse 插件将这些服务部署到 Docker 容器中。

在下一章中,我们将学习如何在云中部署 JEE 应用程序。

第十三章:云中部署 JEE 应用程序

在上一章中,我们学习了如何开发 JEE 微服务并将它们部署到 Docker 容器中。

在本章中,我们将学习如何在云中部署 JEE 应用程序,具体来说是在亚马逊网络服务 (AWS)云和谷歌云平台中,使用 Eclipse 工具。重点将更多地放在使用 Eclipse 工具部署 JEE 应用程序在云上,而不是学习特定的云平台。

在本章中,我们将涵盖以下主题:

  • 在 AWS EC2 实例中部署 JEE 应用程序

  • 在 AWS Beanstalk 中部署 REST 网络服务

  • 在谷歌计算引擎中部署 Docker 容器

  • 在谷歌应用引擎中部署 RESTful 网络服务

云中部署

在云中部署应用程序有许多优点,例如根据其负载扩展应用程序,以及无需维护自己的数据中心或物理机器的所有好处。除了托管应用程序和灵活性之外,大多数云平台还提供数据库、文件存储、消息传递等服务,这些服务可以轻松集成到您的应用程序中。

云平台提供的部署服务可以大致分为以下几类:

  • 基础设施即服务 (IaaS): 在这项服务中,您将获得完全控制的虚拟机 (VMs)。您可以在它们上安装任何软件并设置负载均衡、存储、网络和安全。这就像在云中拥有自己的数据中心一样。IaaS 的例子包括亚马逊弹性计算云 (EC2)和谷歌计算引擎。

  • 平台即服务 (PaaS): 在这项服务中,您将获得预装操作系统和服务器软件的虚拟机。负载均衡、安全、网络等服务也为您预先配置(或使配置变得非常简单)。因此,您可以专注于应用程序的部署。例如,您可以取一个 WAR 文件并直接在 PaaS 中部署它。PaaS 的例子包括亚马逊弹性豆茎和谷歌应用引擎。

虽然 IaaS 提供了更多的灵活性,但它的配置比 PaaS 更困难。

在以下章节中,我们将看到如何在 AWS 和谷歌云中部署上述类型的 JEE 应用程序。

注意

本书,尤其是本章,解释了开发和测试的部署,而不是生产部署。生产部署是一个庞大而复杂的话题,需要考虑许多因素,如安全性、扩展性等,这些都不在本书的范围之内。

您需要拥有您想要使用的云服务提供商的账户才能使用服务。根据您使用的服务和服务器负载,云部署可能会花费您很多钱。然而,几乎所有的云提供商都提供免费服务,以便您可以尝试它们一段时间。为了遵循本章中的示例,请确保您有 AWS 云(aws.amazon.com/)和谷歌云平台(cloud.google.com)的账户。

在 AWS 云中部署

我们将在 AWS 中首先创建一个用户组,并在其中创建一个用户。当您设置用户组的权限时,该组中的所有用户也将获得相同的权限。

创建用户组和用户

我们将执行以下步骤来创建一个用户组:

  1. 前往 AWS 管理控制台(console.aws.amazon.com/)并登录。

  2. 从顶部的菜单中选择“服务 | IAM(身份与访问管理)”。

  3. 从左侧的列表中选择“组”。

  4. 点击“创建新组”按钮。

  5. 按照向导指定组的名称并附加访问策略。让我们将组命名为aws_eclipse_users

  6. 选择组的“管理员访问策略”。

我们将执行以下步骤来创建一个用户:

  1. 从左侧的列表中选择“用户”,然后点击“添加用户”按钮。

  2. 将用户名设置为 aws_eclipse_user。

  3. 在访问类型选项中,选择 AWS 管理控制台访问选项。如果您希望,可以关闭“需要密码重置”选项。

  4. 点击“下一步:权限”按钮,然后选择我们之前创建的组,即 aws_eclipse_users。

  5. 按照页面上的步骤完成工作流程,最终将引导您创建用户。

现在,您应该有具有管理员访问权限的 aws_eclipse_users 组和该组中的 aws_eclipse_user 用户。

下一步是为用户创建访问密钥。转到列出所有用户的页面(点击页面左侧的“用户”),然后点击用户 aws_eclipse_user。点击“安全凭证”选项卡,然后点击“创建访问密钥”按钮。它创建了一个访问密钥,并显示了访问密钥 ID 和秘密访问密钥。为将来使用保存此信息。AWS 为您提供下载包含此信息的 CSV 文件选项。

注意

访问 AWS 服务从 Eclipse 需要访问密钥 ID 和秘密访问密钥。这是 AWS 唯一显示秘密访问密钥的地方。如果您丢失了这些信息,以后将无法找回,所以请确保保存这些信息。

接下来,我们将添加一个安全组并指定相同的入站流量规则。在 AWS 管理控制台中,转到 服务 | EC2 | 网络 & 安全 | 安全组页面。点击 创建安全组 按钮。输入 安全组名称 为 eclipse-dev。输入任何描述(这是一个必填字段)。然后创建以下入站规则:

类型 协议 端口范围
SSH TCP 22 任何地方 (见下表中的说明)
自定义 TCP TCP 8080 任何地方

注意

上述 SSH 入站规则将允许从任何 IP 地址访问您的 EC2 实例。如果您想限制访问,请不要选择“任何地方”作为源,而是设置特定的 IP 地址,选择自定义。

因为本章解释了如何在 xloud 中部署 JEE 应用程序以进行开发和测试,所以源被选择为“任何地方”(任何 IP)。

前面的安全组将为端口 22 上的任何外部 IP 提供 SSH 访问,并为端口 8080 提供 TCP 访问。

安装 Eclipse 的 AWS 工具包

在本节中,我们将学习如何在 Eclipse 中安装 AWS 工具包插件。转到 Eclipse 菜单的 帮助 | Eclipse 市场位置.... 搜索 AWS Toolkit

图 13.1:安装 Eclipse 的 AWS 工具包

安装插件。在本章的后面部分,我们将看到这个插件的多项功能。访问 docs.aws.amazon.com/toolkit-for-eclipse/v1/user-guide/welcome.html 获取完整文档。

我们需要使用上一节中创建的访问密钥 ID 和秘密访问密钥来配置插件。打开 Eclipse 首选项并转到 AWS 工具包首选项:

图 13.2:设置访问密钥 ID 和秘密访问密钥 AWS 工具包首选项

您可以使用默认配置文件或创建一个新的配置文件。输入访问密钥 ID 和秘密访问密钥,然后点击 应用和关闭 按钮。这些信息将被 Eclipse 插件用于访问 AWS 中您的配置信息。

一旦认证成功,您就可以在 Eclipse 中访问 AWS 控制台网页上大部分信息。切换到 AWS 管理视角(选择 窗口 | 视角 | 打开视角 或点击右上角的 打开视角 工具栏按钮):

图 13.3:AWS 管理视角

在 AWS 探索器中展开 亚马逊 EC2 类别,您将看到查看 EC2 AMI、EC2 实例等选项。亚马逊机器镜像AMI)可以被认为是一个模板,可以从它创建多个虚拟机实例。

启动 EC2 实例

现在,让我们从 AMI 创建一个 EC2 实例。如果 EC2 AMI 视图尚未在 Eclipse 的 AWS 管理视图中打开,请在 AWS 资源管理器中的 AMIs 节点上右键单击,并选择打开 EC2 AMIs 视图。此视图可能需要很长时间才能加载,因为有许多 AMI 可供选择。我们将选择一个在免费层(在您的试用期间)可用的 Linux AMI。不幸的是,在 Eclipse 视图中搜索此 AMI 并不容易,因为视图不显示或允许您根据描述搜索 AMI。这意味着您不能通过在搜索框中输入linux来搜索 AMI。令人惊讶的是,视图中的平台筛选选项也没有显示 Linux 选项,至少在撰写本书时是这样的。

我们将从 AMI ID ami-f63b1193创建一个实例(您可以在创建新实例时从 AWS 控制台网页中看到 AMI 列表的更好视图)。在搜索框中输入ami-f63b1193,您应该在视图中看到一个结果。右键单击 AMI 并选择启动选项:

图 13.4:从 AMI 启动实例

选择合适的实例类型。在本例中,我们将选择通用可扩展微实例类型。

选择可用区。请参阅docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions获取可用区的列表。

接下来,选择一个密钥对以从您的宿主机连接到实例。如果没有列出密钥对,请单击加号图标并添加一个新的密钥对。您只需指定密钥的名称和它将在您的机器上保存的位置。

接下来,选择一个新的实例的安全组。我们将选择eclipse-dev安全组(向导不允许您完成,除非您选择安全组和密钥对)。点击完成。新实例将被添加到 EC2 实例视图中的实例列表中。注意实例的状态。当您刚刚创建实例时,状态将是挂起。一旦实例成功启动,状态将变为运行。右键单击实例以查看实例上的可用菜单选项:

图 13.5:EC2 实例视图中的上下文菜单

要打开外壳以执行 OS 命令,从弹出菜单中选择打开外壳选项:

图 13.6:AWS 实例中打开的外壳

我们将使用此选项(开放外壳)来在我们的实例中执行命令。

在 EC2 实例中安装 CourseManagement EJB 应用程序

在第七章,“使用 EJB 创建 JEE 应用程序”中,我们为CourseManagement开发了一个 EJB 应用程序。我们将看到如何将此应用程序部署在上一节中创建的 EC2 实例中。我们需要在实例中安装 GlassFish 5 和 MySQL 服务器。虽然您可以在单独的实例上安装这些服务器(这在生产设置中是推荐的),但我们将把它们都安装在同一实例上,以便我们可以减少创建新实例的步骤数量。让我们首先安装 GlassFish 5 服务器。

安装 GlassFish 5 服务器

在编写本章时,AWS 创建的 Linux 实例已预装了 JDK 7。然而,我们在本书中使用的是 JDK 8。因此,我们将首先卸载 JDK 7 并安装 JDK 8。在 Eclipse 中打开实例的 shell(有关详细信息,请参阅前面的章节)并运行以下命令:

命令 描述

|

sudo yum remove java-1.7.0-openjdk -y
从实例中移除 JDK 7。

|

sudo yum install java-1.8.0 -y
安装 JDK 8。

|

wget http://download.oracle.com/glassfish/5.0/release/glassfish-5.0.zip
下载 GlassFish 5。

|

unzip glassfish-5.0.zip
解压下载的 GlassFish 5 ZIP 文件。

|

glassfish5/glassfish/bin/asadmin --host localhost --port 4848 change-admin-password
更改服务器的密码。默认安装包含一个名为 admin 的用户且未设置密码。我们需要为服务器的远程管理员设置一个密码,以便远程管理能够工作。请注意,用户 ID 是 admin,旧密码为空(没有密码)。设置一个新密码,例如,admin

|

glassfish5/glassfish/bin/startserv > /dev/null 2>&1 &
启动服务器。

|

curl localhost:8080
检查服务器是否启动。

|

glassfish5/glassfish/bin/asadmin --host localhost --port 4848 enable-secure-admin
启用 GlassFish 5 服务器的远程管理。 参见此表后面的说明。

|

sudo glassfish5/glassfish/bin/asadmin
 asadmin> create-service
创建一个服务,以便在 VM 实例启动时启动。在运行asadmin命令后,在asadmin>提示符下运行create-service命令。

|

glassfish5/glassfish/bin/stopserv glassfish5/glassfish/bin/startserv > /dev/null 2>&1 &
停止并启动服务器,以便使前面的更改生效。

注意

在编写本章和本书出版之间,如果与 JDK 版本 1.8.0.151 以上一起使用,GlassFish 5 的启用安全管理的功能会中断。远程访问 GlassFish 5 管理控制台会失败,并出现以下错误(记录在glassfish/domains/domain1logs/server.log):

java.lang.NoClassDefFoundError: sun/security/ssl/SupportedEllipticPointFormatsExtension

您可以参考 GlassFish 5 的 bug github.com/javaee/glassfish/issues/22407

现在,我们需要指示 AWS 允许在此实例上端口 4848(用于管理员)、8080(用于访问 Web 应用程序)和 3306(用于远程连接到 MySQL 服务器)上的 TCP 请求。我们将通过在此实例的安全组上设置入站规则来实现这一点。回想一下,在前一节中我们选择了 eclipse-dev 安全组。我们需要在此组上设置入站规则。不幸的是,我们无法从 Eclipse 插件(在撰写本书时)中完成此操作。登录到 AWS 控制台网页,转到服务 | EC2,然后转到网络与安全 | 安全组。右键单击安全组 eclipse-user,选择“编辑入站规则”选项。添加规则以允许来自您机器 IP(您将从该机器远程访问实例的地方)的 TCP 流量(您可以使用类似 www.whatismyip.com/what-is-my-public-ip-address/ 的网站来查找您机器的真实 IP 地址):

图 13.7:在安全组中设置入站规则

注意,如果您的机器接收动态 IP,那么您将不得不在上面的页面上更新它。

您现在可以浏览到您实例上的 GlassFish 5 管理站点——转到 https://<your-instance-public-address>:4848。您可以从 Eclipse 视图 EC2 实例或从 AWS 控制台在线找到公共地址。

安装 MySQL 服务器

如前所述,我们将在这个 EC2 实例上安装 MySQL 服务器。您也可以使用 AWS 的 RDS 实例,这是亚马逊推荐用于 MySQL 数据库的实例。RDS 有许多优点,但为了简洁起见,我们将在同一个 VM 实例上安装 MySQL。确保按照前面解释的那样,在实例上打开一个 shell,并执行以下命令:

命令 描述

|

sudo yum install mysql-server -y
安装 MySQL

|

sudo chkconfig mysqld on
激活 MySQL 服务

|

sudo service mysqld start
启动 MySQL 服务

|

mysqladmin -u root password [your_new_pwd]
设置密码

|

mysqladmin -u root -p create course_management
创建 course_management 数据库

|

create user 'eclipse-user'@'%' identified by ‘enter_password_for_new_user’
创建新用户

|

 mysql -u root -p
从命令行登录 MySQL

|

create user 'eclipse-user'@'%' identified by ‘password_for_eclipse_user’;
mysql> 提示符下执行此命令以创建名为 eclipse-user 的新用户

|

grant all privileges on *.* to 'eclipse-user'@'%' with grant option;
授予新用户权限

|

exit
退出 MySQL 控制台

您现在可以从您的宿主机连接到此 MySQL 服务器实例。但在尝试连接到服务器之前,请确保您已在 EC2 实例上设置了入站规则,允许从您的机器(IP)在端口 3306 上进行连接(见 图 13.7)。然后您可以从终端(命令行)连接,或者使用 MySQL Workbench(见 第一章,介绍 JEE 和 Eclipse, 了解有关安装 MySQL Workbench 的更多信息)。使用实例的公共 DNS 名称进行连接。

根据第四章创建 JEE 数据库应用程序中的说明在此数据库中创建表。或者,使用位于本章源代码CourseManagementEAR文件夹中的course_management.sql来导入表。在 MySQL Workbench 中,选择“服务器 | 数据导入”菜单。选择“从自包含文件导入”并输入course_management.sql的路径。将course_management作为默认目标模式。选择“结构和数据”。然后,点击“开始导入”按钮。

在 GlassFish 5 服务器中配置数据源

在 GlassFish 5 服务器中配置我们的数据源,首先需要下载 MySQL JDBC 驱动程序。您可以在dev.mysql.com/downloads/connector/j/找到下载驱动程序的链接。在为我们的 EC2 实例打开的 shell 中执行以下命令:

命令 描述

|

wget https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.45.zip
下载驱动程序

|

unzip mysql-connector-java-5.1.45.zip 
解压文件

|

cp mysql-connector-java-5.1.45/mysql-connector-java-5.1.45-bin.jar glassfish5/glassfish//domains/domain1//lib/ext/
将驱动程序 JAR 文件复制到 GlassFish 服务器类路径中的一个文件夹

重启 EC2 实例(这是必要的,以便 GlassFish 5 可以加载 MySQL JAR 文件)。然后,按照第七章中“配置 Glassfish 中的数据源”部分的说明操作,使用 EJB 创建 JEE 应用程序。确保在配置数据源时使用您的 EC2 实例域名而不是 localhost(特别是在设置 JDBC 连接池的附加属性时)。您可以通过浏览到https://<enter_domain_name_of_ec2_instance>:4848来访问 GlassFish 5 的管理控制台。

在 GlassFish 管理控制台中配置连接池和 JDBC 数据源后,部署CourseManagementMavenEAR-1.ear。这是我们第七章中“使用 EJB 创建 JEE 应用程序”部分在CourseManagementMavenEAR项目(和相关项目)中创建的相同 EAR 文件。为了您的方便,相同的 EAR 文件也包含在本章源代码的CourseManagementEAR文件夹中。在管理控制台中,从左侧菜单栏点击“应用程序”。然后,点击“部署”按钮。从CourseManagementEAR文件夹中选择CourseManagementMavenEAR-1.ear并部署应用程序。

应用程序部署成功后,您应该能够浏览到http://<ec2_instance_domain_name>:8080/CourseManagementMavenWebApp/course.xhtml并看到课程列表(如果没有数据,则显示空列表)。

您可以通过创建 AMI 并将未来实例基于该 AMI 创建来将前面的设置保存为模板。要从运行实例创建 AMI,请浏览到 AWS 控制台并选择“服务 | EC2”选项。然后,转到运行实例列表。右键单击您想要创建映像的实例,并选择“创建映像”。

使用 Elastic Beanstalk 安装 CourseManagmenet REST 服务

Elastic Beanstalk (EBS) 是 AWS([aws.amazon.com/elasticbeanstalk/](https://aws.amazon.com/elasticbeanstalk/))提供的 PaaS(平台即服务)产品。其理念是您主要关注应用程序的开发,并将服务器配置(包括所需软件的安装)、负载均衡、日志文件管理等留给了 PaaS 提供商。然而,在 Elastic Beanstalk 中,您对服务器的控制不如您在配置自己的 EC2 实例时那么大。

EBS 为不同平台提供预配置的托管解决方案,包括 Java 的一种。它提供预配置 Tomcat 的服务器。您只需上传您的 WAR 文件,应用程序就会被部署。在本节中,我们将学习如何在 EBS 中部署 RESTful Web 服务。

回想一下,我们在 第九章,“创建 Web 服务”中开发了 CourseManagmenetREST 服务。我们将使用 EBS 在 Tomcat EBS 平台上部署相同的服务。确保您已创建了 CourseManagmenetREST 项目的 WAR 文件——如果尚未导入项目,请在 Eclipse 中项目资源管理器中右键单击项目,选择运行 As | Maven 安装。这将创建 target 文件夹中的 CourseManagementREST-1.war 文件。我们将使用 EBS 在 Tomcat 服务器上部署此 WAR 文件。

从 Eclipse 创建 Elastic Beanstalk 应用程序

我们首先将在 Eclipse 中为 EBS Tomcat 平台创建一个服务器。转到 Eclipse 的服务器视图。在默认的 JEE 视图中,此视图位于底部的一个选项卡中,位于编辑器下方。右键单击它并选择新建 | 服务器:

图 13.8:在 Eclipse 中添加 Elastic Beanstalk 服务器

从 Amazon Web Services 组中选择 AWS Elastic Beanstalk for Tomcat 8,或可用的最新 Tomcat 配置。保持其他默认选项。点击下一步:

图 13.9:配置 EBS 应用程序和环境

选择创建新应用程序的选项。让我们将此应用程序命名为 CourseManagementREST,环境为 CourseManagementREST-env。由于我们正在部署一个 Web 应用程序,请从下拉框中选择单实例 Web 服务器环境或负载均衡 Web 服务器环境。第三种类型,工作环境,通常用于长时间运行的批处理应用程序。点击下一步。此时,插件可能会警告您 IAM 操作不允许。点击确定继续:

图 13.10:选择 EBS 应用程序的实例配置文件和服务角色

在权限页面保持默认值并点击下一步:

图 13.11:选择 EBS 部署的密钥对

选择“使用密钥对部署”选项并从列表中选择一个密钥。如果没有列出密钥对,请点击加号图标并添加一个新的密钥对。你只需要指定密钥的名称和它在你的机器上的保存位置。点击下一步。

如果你已经从第九章创建 Web 服务中导入 CourseManagementREST 项目到 Eclipse 工作区,那么它将作为一个可部署的应用程序出现。点击“添加”按钮将其移动到“配置”列表中:

图片

图 13.12:将应用程序添加到 EBS 部署

点击完成。我们刚刚添加的 EBS 服务器应该会出现在服务器视图中:

图片

图 13.13:添加了 EBS 服务器后的服务器视图

点击“开始”按钮(或右键单击服务器并选择“开始”选项)。Eclipse 会要求你输入部署的版本标签:

图片

图 13.14:设置 EBS 部署的版本标签

设置标签(或保留默认标签)并点击确定。一旦服务器启动(你可以在服务器视图中检查状态——确保状态为已启动),浏览到 http://<your-ebs-app-domain>/services/course/get/1。你应该会看到包含课程 ID 1 的详细信息的 XML 输出。

要找到你的 EBS 服务器域名,在服务器视图中双击服务器。这将在编辑器中显示服务器属性:

图片

图 13.15:EBS 服务器属性

你可以在环境 URL 链接中找到域名。点击其他选项卡以查看有关服务器配置的更多信息。点击“日志”选项卡将显示你的服务器日志,这对于故障排除问题非常有用。

如果你想要查看 AWS 为你的 Beanstalk 应用程序创建的 EC2 实例,请点击“环境资源”选项卡:

图片

图 13.16:EBS 中的 EC2 实例

右键单击实例行并选择打开外壳。这也可以用于故障排除应用程序。请注意,你对 EBS 中的 EC2 实例所做的任何更改(如安装软件)在部署新版本的应用程序时将会丢失。

你可以在 Eclipse 的 AWS 探索器中看到 EBS 应用程序和环境:

图片

图 13.17:在 AWS 探索器中浏览 EBS 应用程序和环境

登录 AWS 控制台并转到服务 | 弹性豆舱,以查看所有应用程序和环境,包括从 Eclipse 创建的:

图片

图 13.18:EBS 仪表板

如果你有一个要部署/更新的 WAR 文件,请点击“上传和部署”按钮并选择你想要部署的 WAR 文件。

你可以通过点击仪表板下方的“配置”链接来修改你环境的配置。配置页面中的选项允许你修改实例、容量、负载均衡器、安全设置等设置。如果你的应用程序使用数据库,你也可以进行配置。

您可以通过打开 <环境 URL 在图 13.15>/services/course/get/1 来浏览应用程序,CourseManagementREST

如果由于某种原因,应用程序从 Eclipse 无法正确部署,则可以通过点击 图 13.18 中的“上传和部署”按钮,从 AWS Console 重新部署应用程序,并浏览到项目 target 文件夹中创建的 WAR 文件(如果未创建 WAR 文件,则右键单击项目并选择 Run As | Maven Install)。

Elastic Beanstalk 可以显著节省将应用程序部署到云中的时间。它需要的设置和配置要少得多。

在 Google Cloud 中部署

在本节中,我们将了解如何在 Google Compute Engine(IaaS 提供服务)和 Google App Engine(PaaS 提供服务)中部署 JEE 应用程序。Compute Engine (cloud.google.com/compute/) 可以被认为是 AWS EC2 的对应物,而 App Engine (cloud.google.com/appengine ) 可以被认为是 Elastic Beanstalk 的对应物。您需要有一个 Google 账户才能登录到 console.cloud.google.com 的 Cloud Console。您需要在 Google Cloud 中创建至少一个项目才能部署应用程序。当您登录到 Cloud Console 时,如果没有可用的项目,它将提示您创建一个项目:

图 13.19:从仪表板创建 Google Cloud 项目

在创建项目页面中,您只需要输入项目的名称。项目 ID 将自动为您选择。您应该保留这个项目 ID,因为许多 SDK 命令需要一个项目 ID 作为它们的参数之一。

如果您已经有了项目,但想为这本书创建一个新的项目,请打开 Google Cloud Console 网页,转到 IAM & admin | 管理资源页面。点击页面上的“创建项目”链接。

设置 Google Cloud Tools

设置 Google Cloud Tools 需要多个步骤。让我们从安装 SDK 开始。

安装 Google Cloud SDK

cloud.google.com/sdk/ 下载 SDK。解压它,并从 bin 文件夹运行以下命令:

gcloud init

有关初始化 SDK 的更多选项,请参阅 cloud.google.com/sdk/docs/initializing

安装 App Engine SDK 的 Java 扩展

运行以下命令(确保已安装和配置 Cloud SDK):

gcloud components install app-engine-java

有关管理 Google Cloud 组件的详细信息,请参阅 cloud.google.com/sdk/docs/managing-components

接下来,设置 gcloud 命令的默认项目名称:

gcloud config set project <your-project-name-here>

安装 Google Cloud Tools for Eclipse

要在 Eclipse 中安装 Google Cloud 插件,请打开 Eclipse Marketplace(选择菜单 Help | Eclipse Marketplace...)。搜索 Google Cloud Tools:

图 13.20:从市场安装 Google Cloud Tools 插件

为 Google Cloud Tools 设置 Eclipse 首选项

打开 Eclipse 首选项并转到 Google Cloud Tools 首选项:

图 13.21:在 Google Cloud Tools 首选项中设置 SDK 路径

在 SDK 位置字段中输入您解压缩 SDK 的文件夹路径。

在 Google Compute Engine 中部署应用程序

在本节中,我们将在 Google Compute Engine 中创建一个虚拟机实例并在其中部署一个 JEE 应用程序。一旦我们创建了一个虚拟机,我们就可以遵循与在前面 在 EC2 实例中安装 CourseManagement EJB 应用程序 部分中安装 GlassFish 服务器和 Course Management 应用程序相同的步骤。但让我们在 Compute Engine 中部署一个不同的应用程序。在上一章中,我们看到了如何在 Docker 容器中部署 JEE 应用程序。所以,让我们在 Compute Engine 的虚拟机中安装 Docker 并部署 CourseManagement 服务。但首先,让我们创建一个虚拟机。不幸的是,在撰写本书时,Google Cloud Tools for Eclipse 并没有为与 Compute Engine 的工作提供很多支持。因此,我们将使用 Google Cloud Console 网页或主机机器上的终端。

在 Google Compute Engine 中创建虚拟机实例

登录到 Google Cloud 控制台 (console.cloud.google.com) 并转到 计算引擎 | 虚拟机实例 页面。点击 创建实例 链接。使用 Debian GNU/Linux 引导盘创建实例。请确保选择 允许 HTTP 流量 和 允许 HTTPS 流量 选项。

在虚拟机实例中安装 Docker

在虚拟机实例页面,选择您要使用的实例并下拉 SSH 选项(在表格的 连接 列中):

图 13.22:打开到虚拟机实例的 SSH 连接

选择在浏览器窗口中打开。此选项将在浏览器窗口中打开并打开虚拟机实例的 SSH shell。在 shell 中运行以下命令以安装 Docker:

命令 描述

|

sudo apt-get update
获取软件包和依赖项的最新版本

|

curl -fsSL get.docker.com -o get-docker.sh
下载 Docker 安装脚本

|

sudo sh get-docker.sh
运行安装脚本

有关在 Debian 发行版上安装 Docker 的更多信息,请参阅 docs.docker.com/install/linux/docker-ce/debian/

一旦安装了 Docker,我们需要执行几个命令,以便可以在不使用 sudo 的情况下调用 Docker 命令(Docker 以 root 身份运行):

命令 描述

|

sudo groupadd docker
创建 Docker 用户组。它可能已经存在。

|

sudo usermod -aG docker $USER
将当前用户添加到 Docker 组中。

有关更多详细信息,请参阅 docs.docker.com/install/linux/linux-postinstall/#manage-docker-as-a-non-root-user

从外壳注销并重新登录(关闭外壳窗口并打开一个新的外壳窗口)。如果所有前面的命令都已成功执行,那么您应该能够在没有sudo的情况下运行docker ps命令。

接下来,我们将在实例中安装docker-compose(请参阅docs.docker.com/compose/install/)。执行以下命令(安装docker-compose的命令行版本号可能不同):

命令 描述

|

sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
下载docker-compose

|

sudo chmod +x /usr/local/bin/docker-compose
使docker-compose可执行

我们在第十二章中创建了docker-compose部署配置,微服务。我们将在本节中创建的虚拟机实例中部署相同的配置。本章的源代码包括一个名为coursemanagement-docker-compose的文件夹。将此文件夹中的所有文件上传到虚拟机实例。您可以从浏览器外壳窗口上传,或者使用主机机器上的gcloud命令。在浏览器外壳中,点击右上角的设置图标,选择上传文件选项。上传coursemanagement-docker-compose文件夹中的所有文件。要从终端上传,在将文件夹更改为coursemanagement-docker-compose后,执行以下gcloud命令:

gcloud compute scp * <your-instance-name-here>:~/

此命令将当前文件夹(在我们的案例中是coursemanagement-docker-compose)中的所有文件复制到实例中用户的主文件夹。

无论您使用哪种方法上传文件,请确保您在虚拟机实例中有以下文件:

  • course-management-db.dockerfile

  • course-management-service.dockerfile

  • docker-compose.yml

  • course-management-db.sql

  • coursemanagementspring-0.0.1-SNAPSHOT.jar

在虚拟机实例的浏览器外壳中,执行以下命令以在 Docker 容器中设置数据库和 REST 服务:

docker-compose up -d

请参阅第十二章,微服务,以获取有关前面文件和命令的更多详细信息。一旦命令执行成功,浏览到http://<instance_external_ip>/course_management/courses。您将只看到一个空的 JSON 数组,因为没有数据在数据库中。您可以从“计算引擎 | 虚拟机实例”页面找到您实例的外部 IP。

执行docker-compose down命令以关闭容器。

在 Google App Engine 中部署应用程序

App Engine 是 Google 的平台即服务PaaS),类似于亚马逊的 Elastic Beanstalk。在《从 Eclipse 创建 Elastic Beanstalk 应用程序》这一节中,我们使用 Elastic Beanstalk 部署了CourseManagementREST服务。在本节中,我们将学习如何使用 Google App Engine 部署相同的服务。

让我们复制 CourseManagementREST 项目。在 Eclipse 项目资源管理器中右键单击项目,然后选择复制。在项目资源管理器中的任何位置右键单击,然后选择粘贴。Eclipse 将提示您命名项目。让我们将其命名为 CourseManagementREST-GAE。我们将使用 Google App Engine 部署此项目。

让我们配置我们的项目为 App Engine 项目。在项目资源管理器中右键单击 CourseManagementREST-GAE 项目,然后选择配置 | 转换为 App Engine 标准项目。

如果您正在创建一个用于部署到 Google App Engine 的新项目,请转到文件 | 新建 | Google App Engine 标准 Java 项目菜单。或者,从工具栏中的 Google Cloud Platform 图标下拉菜单中选择创建新项目 | Google App Engine 标准 Java 项目。

在我们部署项目之前,请从 src/main/webapp/WEB-INF 文件夹中删除 web.xml。Google App Engine 的 Java 平台使用 Jetty 服务器,并且在此部署中不需要 web.xml

在删除 web.xml 后,您可能会看到一个错误,表明 web.xml 缺失,并且 <failOnMissingWebXml> 设置为 true。为了抑制此错误,请在 pom.xml 中添加以下属性:

<properties>

    <failOnMissingWebXml>false</failOnMissingWebXml>   

</properties>

要在本地测试此应用程序,请转到服务器视图,右键单击它,然后选择新建 | 服务器。然后,展开 Google 组并选择 App Engine Standard:

图 13.23:创建本地 App Engine 服务器

点击下一步并添加用于部署的 CourseManagementREST-GAE 项目:

图 13.24:为部署添加 CourseManagementREST-GAE 项目

点击完成并从服务器视图启动服务器。然后,浏览到 http://localhost:8080/services/course/get/1 以验证应用程序是否已正确部署。

如果您在 pom.xml 中遇到有关 JDK 版本的错误,请在 pom.xml 中的依赖项部分上方添加以下部分:

<properties>

  <maven.compiler.source>1.8</maven.compiler.source>

  <maven.compiler.target>1.8</maven.compiler.target>

</properties>

在您将此项目部署到 Google App Engine 之前,您应该确保在 Google App Engine 中已创建应用程序。浏览到 console.cloud.google.com/appengine 并检查是否存在任何应用程序。如果不存在,您可以从该页面创建应用程序。或者,您可以在终端中运行以下命令:

gcloud app create

要将此项目部署到 Google App Engine,请在项目资源管理器中选择项目,并从 Google Cloud Platform 工具栏按钮的下拉菜单中选择:

图 13.25:将项目部署到 Google App Engine

选择部署到 App Engine 标准菜单:

图 13.26:设置 Google App Engine 的部署参数

从下拉菜单中选择你的 Google 账户,如果没有选择,然后选择你想要部署应用程序的 Google Cloud 项目。

一旦项目部署完成,请浏览到 https://<your_project_id>.appsport.com/services/course/get/1 以验证它。

要停止应用程序,你需要禁用应用程序——打开 console.cloud.google.com,转到 App Engine | 设置并点击“禁用应用程序”按钮。

摘要

在本章中,我们了解了亚马逊和谷歌提供的两种云部署服务类型。一种是 IaaS,另一种是 PaaS。PaaS 允许你在预配置的环境中部署你的应用程序,而 IaaS 则让你完全控制部署配置。亚马逊的 IaaS 提供服务称为 EC2,谷歌的称为 Compute Engine。亚马逊的 PaaS 提供服务称为 Elastic Beanstalk,谷歌的称为 App Engine。

我们在亚马逊 EC2 的一个实例中的 GlassFish 服务器上部署了 CourseManagement EJB 应用程序。然后我们在 Elastic Beanstalk 中部署了 CourseManagementREST 服务。

然后,我们在 Google Compute Engine 的一个实例中部署了一个带有 CourseManagement 服务的 Docker 容器。最后,我们在 Google App Engine 中部署了 CourseManagementREST 服务。

在下一章中,我们将学习如何确保 JEE 应用程序的安全。

第十四章:保护 JEE 应用程序

在上一章中,我们学习了如何在云中部署 JEE 应用程序。在本章中,我们将学习如何保护 JEE 应用程序——特别是如何执行身份验证和授权。

我们将涵盖以下主题:

  • 使用部署描述符保护 JEE 网络应用程序

  • 使用注解保护 JEE 网络应用程序

  • 保护网络服务

  • JEE 8 中的安全增强

JEE 中的身份验证和授权

身份验证是验证用户是否是他或她所声称的过程。这通常是通过要求用户提供用户名和密码来完成的。验证客户端身份的另一种方式是要求客户端证书。在本章中,我们将仅查看密码身份验证。

授权是确定用户是否被允许在应用程序中执行某些操作的过程。JEE 规范允许基于角色的授权。在应用程序中,您指定可以执行操作或访问资源的角色,然后将用户添加到这些角色中。

不幸的是,根据 JEE 规范,保护 JEE 应用程序并非完全独立于服务器。配置的部分是服务器通用的,而部分则是特定于服务器供应商的。常见的配置大多在 web.xml 中完成或通过使用注解。但是,服务器特定的配置因供应商而异。在本章中,我们将学习如何在 GlassFish 和 Tomcat 服务器中保护 JEE 应用程序。

但是,在我们学习有关保护应用程序的详细信息之前,我们需要了解在安全配置的上下文中常用的某些术语:

  • 用户:请求访问应用程序中受保护资源的客户端

  • :具有相似特征的用户的集合

  • 角色:确定用户或组可以访问应用程序中的哪些资源,具有该特定角色

  • :可以被视为一个安全域,拥有自己的用户、组和存储方法

修改数据库以保存身份验证信息

在本章中,我们将使用数据库来验证用户。其他用于存储安全信息的方法包括文件和 LDAP。我们需要更新我们的 course_management 数据库,添加存储用户和组信息的表。让我们创建三个表——UserGroupsUser_Group

图 14.1:新的身份验证表

User 表存储用户名和密码。Groups 表存储组名。我们稍后将直接将组名分组到角色中。User_Group 表是一个联合表,将 UserGroups 表连接起来。一个用户可以属于多个组,一个组也可以有多个用户。

为了简化在 JEE 服务器中配置领域时从前面表映射信息,我们将创建一个名为user_group_view的视图,使得所有前面表的信息在一个视图中可用。该视图的 DDL 脚本如下:

CREATE
VIEW `user_group_view` AS
    SELECT
        `user`.`user_name` AS `user_name`,
        `groups`.`group_name` AS `group_name`,
        `user`.`password` AS `password`
    FROM
        ((`user`
        JOIN `groups`)
        JOIN `user_group`)
    WHERE
        ((`user`.`id` = `user_group`.`user_id`)
            AND (`groups`.`id` = `user_group`.`group_id`))

如果你已经有之前章节中的course_management模式,那么在add_auth_tables.sql文件(该文件在本章源代码文件夹中)中运行脚本。如果你使用MySQLWorkbench,可以按照以下方式运行脚本:

  1. 确保默认模式为course_management;右键点击模式并选择设置为默认模式选项。

  2. 选择文件 | 打开 SQL 脚本菜单,然后选择add_auth_tables.sql文件。文件将在新标签页中打开。

  3. 点击工具栏中的执行图标以执行此脚本。

  4. 右键点击course_management模式并选择刷新所有选项。确保新表和视图已在该模式中创建。

为了测试目的,让我们在user表中插入以下数据**:

ID user_name password
1 user1 user1_pass
2 user2 user2_pass

**:

ID group_name
1 admin

用户组**:

user_ID group_ID
1 1

根据前面的数据,user1在 admin 组中,而user2不在任何组中。

GlassFish 中的应用程序安全

我们将使用在第七章创建 JEE 应用程序与 EJB 中开发的Course Management应用程序来添加安全功能。按照以下步骤导入项目:

  1. 为本章创建一个新的 Eclipse 工作空间。

  2. 将源代码文件夹中第七章,“使用 EJB 创建 JEE 应用程序”,with-maven文件夹内的所有项目复制到当前工作空间中。

  3. 将所有项目导入到新工作空间中(打开文件 | 导入菜单,然后选择 Maven | 已存在的 Maven 项目)。

你现在应该在你的 Eclipse 工作空间中有以下项目:CourseManagementMavenEARCourseManagementMavenEJBClientCourseManagementMavenEJBsCourseManagementMavenWebApp。现在让我们学习如何保护文件夹中 JSP 的访问。

保护 Web 应用程序中文件夹的访问

为了保护 Web 文件夹中的任何资源,你需要在web.xml中声明安全约束。在安全约束中,你可以声明要保护 URL,以及哪些角色可以访问受保护的 URL。在CourseManagementMavenWebApp项目中打开web.xml并添加以下声明在<web-app>标签内:

<security-constraint>
    <display-name>Admin resources</display-name>
    <web-resource-collection>
        <web-resource-name>admins</web-resource-name>
        <url-pattern>/admin/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>admin </role-name>
    </auth-constraint>
    <!--         
    <user-data-constraint>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
    -->
</security-constraint>
<security-role>
    <description>Admins</description>
    <role-name>admin</role-name>
</security-role>

在这里,我们声明所有通过/admin/* URL 访问的资源都是受保护的,并且只有admin角色的用户可以访问这些资源。我们还使用<security-role>标签声明了admin角色。如果您希望 URL 资源仅通过 SSL(使用 HTTPS)访问,则将<transport-guarantee>设置为CONFIDENTIAL。然而,您需要从证书颁发机构(如 Verisign)获取(购买)SSL 证书,并将其安装在服务器上。

有关 SSL 证书的详细信息,请参阅www.verisign.com/en_US/website-presence/website-optimization/ssl-certificates/index.xhtml

每个服务器安装证书的过程都不同。然而,我们不会在本书中讨论如何安装 SSL 证书。因此,前述代码中描述了<user-data-constraint>配置。

有关安全约束的更多详细信息,请参阅javaee.github.io/tutorial/security-webtier002.html#specifying-security-constraints

到目前为止,让我们看看应用程序是如何工作的。在 GlassFish 中部署应用程序之前,让我们创建一个受保护的资源。由于我们已经使用/admin/* URL 保护了所有访问的资源,请在src/main/webapp文件夹中创建一个名为admin的文件夹。在这个文件夹内,使用以下内容创建admin.jsp

<!DOCTYPE HTML>
<html>
<head>
<title>Course Management Admin</title>
</head>
<body>
       Welcome to Course Management Admin<br>
</body>
</html>

请参考第七章中的《在 Eclipse 中配置 GlassFish 服务器》部分,以了解如何将 GlassFish 5 服务器添加到您的 Eclipse 工作空间。

我们需要构建两个应用程序:CourseManagementMavenWebAppCourseManagementMavenEAR。EAR 项目只是一个容器项目;实际内容是由CourseManagementMavenWebApp提供的。因此,我们需要构建这两个项目。在 Eclipse 项目资源管理器中右键单击CourseManagementMavenWebApp,然后选择运行方式 | Maven 安装。对于CourseManagementMavenEAR项目也执行相同的操作。然后,从 GlassFish 5 的目标文件夹中部署CourseManagementMavenEAR-1.ear

要在 GlassFish 5 中部署应用程序,请浏览到http://localhost:4848并配置数据源,如第七章中《使用 EJB 创建 JEE 应用程序》的《在 GlassFish 中配置数据源》部分所述。然后,单击应用程序节点并部署CourseManagementMavenEAR-1.ear

一旦应用程序部署完成,请浏览到http://localhost:8080/CourseManagementMavenWebApp/course.xhtml并确保页面可以在不进行任何认证的情况下访问,因为这是一个未受保护的资源/页面。现在,尝试浏览到http://localhost:8080/CourseManagementMavenWebApp/admin/admin.jsp。由于我们已经将/admin/* URL 模式标记为受保护资源,浏览器会弹出此认证对话框:

图 14.2:浏览器认证对话框

我们尚未配置应用程序进行用户认证。因此,无论你输入什么用户名和密码,前面的对话框都会认证失败。让我们通过配置数据库在 GlassFish 中认证用户来解决这个问题。

在 GlassFish 中配置 JDBC 域

GlassFish 支持不同的域用于 JEE 认证;例如,文件、LDAP 和 JDBC 域。我们将创建一个 JDBC 域,它将使用存储在UserGroupsUser_Groups表中的信息(通过user_group_view公开)。

要在 GlassFish 中创建一个新的 JDBC 域,请浏览到 GlassFish 管理页面(http://localhost:4848),然后在左侧的导航菜单中,转到配置 | 服务器配置 | 安全 | 域。在域页面,点击新建按钮。

图 14.3:在 GlassFish 管理页面上创建 JDBC 域

将以下信息输入到表单中:

类名

字段名 备注
名称 courseManagementJDBCRealm
com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm 从下拉菜单中选择。
JAAS 上下文 jdbcRealm
JNDI jdbc/CourseManagement 我们创建的 JDBC 数据源。有关更多详细信息,请参阅第七章,使用 EJB 创建 JEE 应用程序
用户表 user_group_view 包含用户信息的表。我们指定了我们之前创建的视图。
用户名列 user_name 我们user_group_view中的用户名列。
密码列 password 我们user_group_view中的密码列。
组表 user_group_view 组数据也通过我们的user_group_view公开。
组表用户名列 user_name user_group_view中。
组名称列 group_name user_group_view中。
密码加密算法 AES 加密数据库中密码的算法。我们在应用程序外部预先填充密码。因此,这对我们的示例影响不大。
摘要算法 none 我们在表中输入的密码没有被哈希,所以在这里输入none

点击 OK 按钮创建域。

我们需要告诉我们的应用程序使用之前创建的 JDBC 域。这配置在应用程序的web.xml文件中的<login-config>标签中。<login-config>支持两种认证方法:基本认证和基于表单的认证。

在基本身份验证中,浏览器显示登录表单,就像在 图 14.2 中所示。实际上,这是默认的身份验证方法,因此在我们之前的 web.xml 中没有 <login-config> 标签的情况下,服务器默认为基本身份验证。

在基于表单的身份验证中,你可以指定登录页面。这给了你一个机会来自定义登录体验。

让我们先使用基本身份验证配置域。

在 GlassFish 中使用 JDBC 域的基本身份验证

我们将对在 保护对 web 应用程序文件夹的访问 部分中添加的标签进行一些更改。以下是更改内容:

  1. role-nameadmin 重命名为 admin-role

  2. 删除 <security-role> 标签

  3. 添加 <login-config> 标签

这里是更改后的声明应该看起来像什么:

<security-constraint>
    <display-name>Admin resources</display-name>
    <web-resource-collection>
        <web-resource-name>admins</web-resource-name>
        <url-pattern>/admin/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>admin-role</role-name>
    </auth-constraint>
</security-constraint>
<login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>courseManagementJDBCRealm</realm-name>
</login-config>

注意,我们在 <login-config> 标签中指定了我们配置的域的名称(在 GlassFish 管理页面上配置的)。我们删除了 <security-role>,因为角色现在保存在数据库中的 Groups 表中。然而,我们需要将 web.xml 中声明的角色映射到数据库中的组。这种映射是在 glassfish-web.xml 中完成的。在 web.xml 相同的文件夹中创建 glassfish-web.xml,即 src/main/webapp/WEB-INF,在 CourseManagementMavenWebApp 项目中。向其中添加以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
    <security-role-mapping>
       <role-name>admin-role</role-name>
       <group-name>admin</group-name>
    </security-role-mapping>
</glassfish-web-app>

这里,我们将我们在 web.xml 中声明的 admin-role 映射到数据库中 Groups 表的 admin 组。

现在,通过右键单击项目并选择 Run As | Maven Install 来构建 CourseManagementMavenWebAppCourseManagementMavenEAR 项目(按相同顺序),然后按照 保护对 web 应用程序文件夹的访问 部分中描述的方式在 GlassFish 中部署应用程序。

浏览到 http://localhost:8080/CourseManagementMavenWebApp/admin/admin.jsp。这次,一旦你输入有效的管理员凭据;即用户名为 user1,密码为 user1_pass,浏览器应该显示 admin.jsp 的内容。

在 GlassFish 中使用 JDBC 域的基于表单的身份验证

让我们将基本身份验证更改为基于表单的身份验证,这样我们就可以自定义登录页面。我们需要更新 web.xml 中的 <login-config>。将之前的 <login-config> 块替换为以下内容:

<login-config>
    <auth-method>FORM</auth-method>
    <realm-name>courseManagementJDBCRealm</realm-name>
    <form-login-config>
        <form-login-page>/login.jsp</form-login-page>
        <form-error-page>/login-error.jsp</form-error-page>
    </form-login-config>
</login-config>

我们已经将 <auth-method> 从 BASIC 更改为 FORM。对于基于表单的身份验证,我们需要指定 form-login-page,我们已指定为 login.jsp。form-error-page 是可选的,但我们已将其设置为 login-error.jsp

下一步是创建 login.jsplogin-error.jsp。在 src/main/webapp 文件夹中创建这两个文件,内容如下。

这里是 login.jsp 的源代码。我们已经将其配置为登录页面,如前面的代码块所示:

<!DOCTYPE HTML>
<html>
<head>
<title>Admin Login</title>
</head>
<body>
    <form method=post action="j_security_check">
        <table>
            <tr>
                <td>User Name: </td>
                <td><input type="text" name="j_username"></td>
            </tr>
            <tr>
               <td>Password: </td>
               <td><input type="password" name="j_password"></td>
            </tr>
            <tr>
                <td colspan="2"><input type="submit" value="Login"></td>
            </tr>
        </table>
    </form>
</body>
</html>

为了使基于表单的身份验证生效,有一些要求:

  1. 表单操作必须设置为 j_security_check

  2. 用户名输入字段必须命名为 j_username

  3. 密码输入字段必须命名为j_password

这里是login-error.jsp的源代码。我们已将其配置为<form-error-page>中的错误页面,如前一个代码块所示:

<!DOCTYPE HTML>
<html>
<head>
<title>Login Failed</title>
</head>
<body>
       Invalid user name or password<br>
       <a href="<%=request.getContextPath()%>/admin/admin.jsp">Try Again</a>
</body>
</html>

错误页面显示错误消息并显示重试链接。即使重试链接指向admin.jsp,因为它是一个受保护资源,用户将被重定向到login.jsp。如果登录成功,则将重定向到admin.jsp

在用户成功登录后提供注销选项将很棒。此选项可以添加到admin.jsp中。如下所示在admin.jsp中添加一个注销链接:

<!DOCTYPE HTML>
<html>
<head>
<title>Course Management Admin</title>
</head>
<body>
       Welcome to Course Management Admin<br>
       <a href="../logout.jsp">Logout</a>
</body>
</html>

在与login.jsp相同的文件夹中创建logout.jsp,内容如下:

<%@ page session="true"%>
Logged out <%=request.getRemoteUser()%>
<% session.invalidate(); %>

注销页面简单地调用session.invalidate()来注销用户。

要查看基于表单的认证的实际操作,通过右键单击项目并选择运行 As | Maven Install 来构建CourseManagementMavenWebAppCourseManagementMavenEAR项目(按相同顺序),然后按照保护 Web 应用程序中文件夹的访问部分所述在 GlassFish 中部署应用程序。

浏览到http://localhost:8080/CourseManagementMavenWebApp/admin/admin.jsp。这次,浏览器应该显示带有登录表单的login.jsp,而不是它自己的认证弹出窗口。

在 Tomcat 中保护应用程序

在本节中,我们将学习如何在 Tomcat 服务器中保护资源。为了使示例与我们在上一节中为 GlassFish 学习的示例保持一致,我们将保护admin文件夹中的所有页面。我们将使用我们在第四章,“创建 JEE 数据库应用程序”中创建的CourseManagementJDBC项目开始。回想一下,在第四章,“创建 JEE 数据库应用程序”中,我们在 Tomcat 服务器中部署了此项目。执行以下步骤将项目导入本章的新工作区并配置 Tomcat:

  1. CourseManagementJDBC项目从第七章,“使用 EJB 创建 JEE 应用程序”的项目文件夹复制到当前工作区。将项目导入新工作区(打开文件 | 导入菜单,然后选择 Maven | 已存在的 Maven 项目)。

  2. 如第一章“介绍 JEE 和 Eclipse”中的配置 Eclipse 中的 Tomcat部分所述配置 Tomcat。

  3. 确保应用程序已添加到服务器并按预期运行。请参阅第二章“创建简单的 JEE Web 应用程序”中的在 Tomcat 中运行 JSP部分。

  4. CourseManagementMavenWebApp(参见本章前面的上一节)中的admin文件夹复制到CourseManagementJDBC项目的src/main/webapp中。因此,保护 admin 文件夹的代码对于 GlassFish 和 Tomcat 中的项目都是相同的。

因此,现在你应该已经在 Eclipse 中配置了CourseManagementJDBC项目和 Tomcat。

我们现在将修改web.xml以添加安全约束,就像我们在上一节中为 GlassFish 所做的那样:

<security-constraint>
    <display-name>Admin resources</display-name>
    <web-resource-collection>
        <web-resource-name>admins</web-resource-name>
        <url-pattern>/admin/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>admin</role-name>
    </auth-constraint>
</security-constraint>
<login-config>
    <auth-method>FORM</auth-method>
    <form-login-config>
        <form-login-page>/login.jsp</form-login-page>
        <form-error-page>/login-error.jsp</form-error-page>
    </form-login-config>
</login-config> 

与 GlassFish 的配置相比,前面的配置有两个不同之处:

  • 没有必要像在 GlassFish 中那样将role-name映射到组名。因此,在<auth-constaint>中将角色名称从admin-role更改为仅admin

  • <login-config>中不需要<realm-name>标签。

现在我们通过在server.xml中添加<realm>标签来配置 Tomcat 中的 JDBC realm。如果你使用在 Eclipse 中配置的 Tomcat 来运行应用程序,那么你可以通过展开项目资源管理器中的“Servers”节点来访问server.xml

图片

图 14.4:在 Eclipse 配置的 Tomcat 服务器中访问 server.xml

如果你是在 Eclipse 之外运行 Tomcat,那么你将在$CATALINA_BASE/conf/server.xml中找到server.xml

server.xml中的<Engine defaultHost="localhost" name="Catalina">节点内添加以下 realm 标签:

<Realm  className="org.apache.catalina.realm.JDBCRealm"
    driverName="com.mysql.jdbc.Driver"
    connectionURL="jdbc:mysql://localhost:3306/course_management"
    connectionName="<your-db-username>"
    connectionPassword="<your-db-password>"
    userTable="user_group_view"
    userNameCol="user_name"
    userCredCol="password"
    userRoleTable="user_group_view"
    roleNameCol="group_name" />

Tomcat 的管理模块需要访问我们的 MySQL 数据库,因此我们需要使 MySQL JDBC 驱动程序对管理模块可用。将 MySQL JDBC 驱动程序复制到<tomcat-install-dir>/lib。如果你还没有这样做,可以从dev.mysql.com/downloads/connector/j/下载驱动程序。

这就是保护 Tomcat 中文件夹所需的所有内容。重新启动服务器,并浏览到http://localhost:8080/CourseManagementJDBC/admin/admin.jsp。你应该看到登录页面。

使用注解保护 servlet

到目前为止,我们已经看到了指定安全约束的声明性语法;即通过在web.xml中指定<security-constraint>。然而,安全约束也可以使用 Java 注解来指定,特别是对于 servlet。在本节中,我们将创建AdminServlet并使用注解来保护它。按照上一节中的步骤从Chapter09导入CourseManagementJDBC项目,但将其重命名为CourseManagementJDBC-SecureAnnotations,并将其导入工作区。然后,只在web.xml中添加<login-config>,但不指定<security-constraint>

  <login-config>
    <auth-method>FORM</auth-method>
    <form-login-config>
      <form-login-page>/login.jsp</form-login-page>
      <form-error-page>/login-error.jsp</form-error-page>
    </form-login-config>
  </login-config>

确保你已经按照上一节中的描述复制了login.jsplogin-error.jsp

现在在packt.book.jee.eclipse.ch4.servlet包中创建一个名为AdminServlet的 servlet,内容如下:

package packt.book.jee.eclipse.ch4.servlet;
// skipping imports to save space
@WebServlet("/AdminServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))
public class AdminServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    public AdminServlet() {
        super();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        try {
            request.authenticate(response);
            response.getWriter()
                    .append("Served at: ").append(request.getContextPath());
        } finally {
            response.getWriter().close();
        }      
     }

    protected void doPost(HttpServletRequest request, 
        HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))指定了 servlet 的安全约束。使用此注解,我们只允许具有 admin 角色的用户访问 servlet。如果你浏览到http://localhost:8080/CourseManagementJDBC-SecurityAnnotations/AdminServlet,你应该看到登录页面。

保护 Web 服务

保护 Web 服务的过程与保护 Web URL 的过程类似,我们已经在前面的章节中看到了两个例子。我们在web.xml中指定了<security-constraint><login-config>。让我们看看如何保护我们在第九章中开发的 REST Web 服务:创建 Web 服务

  1. CourseManagementRESTCourseManagementRESTClient项目从Chapter09复制并导入到本章的工作区。正如其名称所暗示的,第一个项目是 REST 服务,第二个项目是一个独立的客户端应用程序,它调用 Web 服务。

  2. 在 Tomcat 中部署CourseManagementREST项目(有关如何操作的详细信息,请参阅前节)。

  3. 确保从CourseManagementClient.java中的main方法调用testGetCoursesJSON方法,来自*CourseManagementRESTClient*项目。

  4. 运行应用程序(在项目资源管理器中右键单击文件并选择 Run As | Java Application),并验证服务是否运行正常。

要使用基本认证保护 Web 服务,请在web.xml中添加以下配置:

:<security-constraint>
    <display-name>Admin resources</display-name>
    <web-resource-collection>
        <web-resource-name>admins</web-resource-name>
            <url-pattern>/services/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
 </security-constraint>
<login-config>
    <auth-method>BASIC</auth-method>
</login-config>   

使用上述配置,我们正在保护包含/services/的任何 URL。我们还指定只有 admin 角色可以访问此 URL,并且认证方法是 BASIC。

现在,在 Tomcat 的server.xml中添加如前节所述的<Realm>配置。如果你此时运行CourseManagementClient.java,你会得到一个未授权错误。这是因为客户端应用程序没有在GET请求中发送认证信息——即用户名和密码。对于基本认证方法,这些信息应该通过authorization头传递。此头参数的值应设置为Basic,后跟 base64 编码的username:password字符串;例如,authorization: Basic dXNlcjE6dXNlcjFfcGFzcw==

在前面的头中,dXNlcjE6dXNlcjFfcGFzcw==user1:user1_pass字符串的 base64 编码格式。

现在,让我们修改CourseManagementClient.java中的testGetCoursesJSON方法,以传递前面的头信息。以下是你需要在检查响应状态之前添加的代码:

String userName = "user1";
String password = "user1_pass";
String authString = userName + ":" + password;
String encodedAuthStr =  Base64.*getEncoder*().encodeToString(authString.getBytes());
//Execute HTTP get method
Response response = webTarget.request(MediaType.*APPLICATION_JSON*).header(HttpHeaders.*AUTHORIZATION*, "Basic " + encodedAuthStr).get();

注意,java.util.Base64从 JDK 1.8 开始可用。如果你使用的是低于 1.8 的版本,你可以使用 Apache commons-codec中的org.apache.commons.codec.binary.Base64。在pom.xml中添加以下依赖项:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>

右键单击项目,选择 Run As | Maven Install。然后,通过调用以下方式编码String

encodedAuthStr =  new String(org.apache.commons.codec.binary.Base64.encodeBase64(authString.getBytes()));

当你现在运行应用程序时,Web 服务应该能够无错误地执行。

JEE 8 的安全性增强

JEE 8 集成了 Java EE Security API 1.0 (JSR 375, javaee.github.io/security-spec/)。这些 API 的增强被广泛分为四个类别:

我们在本章中不会涵盖所有上述增强功能,但我们将详细探讨前三个 API。

在本章的前几节中,我们已经看到安全配置在容器之间并不统一。具体来说,角色到组的映射并不统一。这个问题可以通过使用新的 JEE 8 安全 API 来解决。让我们通过开发一个应用程序来看看如何实现。有关源代码中的 CourseManagementMavenWebApp-jee8 项目,请参阅。

在 JEE 8 中实现可移植安全性

我们将在本节中修改 CourseManagementMavenWebApp,它来自 第七章,使用 EJB 创建 JEE 应用程序。这个项目是 EJB CourseManagementMavenEAR项目的部分,但在本节中,我们将独立地使用CourseManagementMavenWebApp。将CourseManagementMavenWebApp项目从Chapter07复制到本章节的 Eclipse 工作空间中,命名为CourseManagementMavenWebApp-jee8

我们将修改这个项目以提供以下功能:

  • AdminServlet是一个受保护的 servlet,需要登录。我们将实现基本认证。

  • 有三种可能的用户角色:管理员、经理和用户。

  • 只有管理员角色的用户才能看到由AdminServlet提供的管理页面。

  • 只有在管理员角色的用户才能看到由ManagementServlet提供的管理页面。

JEE 8 安全 API 需要启用应用程序中的上下文和依赖注入CDI)。我们只需要在src/main/webapp/WEB-INF文件夹中创建一个空的beans.xml文件来启用 CDI。

接下来,我们需要在pom.xml中添加以下 Maven 依赖项,以便在应用程序中提供 JEE 8 API:

  <dependency>
   <groupId>javax</groupId>
   <artifactId>javaee-api</artifactId>
   <version>8.0</version>
   <scope>provided</scope>
  </dependency>

让我们在packt.book.jee.eclipse.ch7.web.servlet包中创建一个名为ApplicationConfig的类(用于声明应用程序中允许的所有用户角色)。以下是ApplicationConfig类的源代码:

package packt.book.jee.eclipse.ch7.web.servlet;
import javax.annotation.security.DeclareRoles;
import javax.enterprise.context.ApplicationScoped;
@DeclareRoles({"admin", "user", "manager"})
@ApplicationScoped
public class ApplicationConfig {}

现在,让我们在packt.book.jee.eclipse.ch7.web.servlet包中创建两个 servlet,AdminServletManagementServlet。如果你使用 Eclipse 向导创建这些类,那么它会自动在web.xml中添加 servlet 声明和映射。如果你没有使用 Eclipse 向导,那么你可以手动添加声明和映射,或者在 servlet 类中添加@WebServlet注解。以下是AdminServlet的源代码:

package packt.book.jee.eclipse.ch7.web.servlet;

// skipped imports

@BasicAuthenticationMechanismDefinition()
@ServletSecurity(@HttpConstraint(rolesAllowed = {"admin", "manager"} ))
public class AdminServlet extends HttpServlet {

 private static final long serialVersionUID = 1L;

 @Inject
 private SecurityContext securityContext;

 public AdminServlet() {
 super();
 }

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 if (securityContext.isCallerInRole("manager")) {
 request.getRequestDispatcher("/ManagementServlet").forward(request, response);
 } else {
 response.getWriter().append("Welcome to Admin Page!");
 }
 }

 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 doGet(request, response);
 }
}

这个 servlet 使用@ServletSecurity注解允许只有管理员和经理角色访问,并通过@BasicAuthenticationMechanismDefinition指定基本认证类型。我们还要求 JEE 容器注入一个SecurityContext实例,该实例在doGet方法中使用以检查用户的角色。如果用户是经理角色,则请求被转发到ManagementServlet,否则,允许当前 servlet 的访问。注意调用securityContext.isCallerInRole来检查用户的角色。

下面是ManagementServlet的源代码:

package packt.book.jee.eclipse.ch7.web.servlet;
// skipped imports
@BasicAuthenticationMechanismDefinition(realmName="basic")
@ServletSecurity(@HttpConstraint(rolesAllowed = {"manager"} ))
public class ManagementServlet extends HttpServlet {
 private static final long serialVersionUID = 1L;

 public ManagementServlet() {
   super();
 }

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 response.getWriter().append("Welcome to Management Page!");
 }

 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 doGet(request, response);
 }
}

前面的 servlet 也使用了基本认证,并且只允许管理员角色的用户访问。

使用前面的注解,不需要在web.xml或任何自定义容器特定文件中进行声明性配置。但是,我们如何告诉安全 API 哪些是有效的用户和角色呢?我们通过实现IdentityStore接口来实现。在packt.book.jee.eclipse.ch7.web.servlet包中创建SimpleMapIdentityStore类。这个类应该实现IdentityStore接口:

package packt.book.jee.eclipse.ch7.web.servlet;

// skipped imports

@ApplicationScoped
public class SimpleMapIdentityStore implements IdentityStore {
 class UserInfo {
   String userName;
   String password;
   String role;

   public UserInfo(String userName, String password, String role) {
     this.userName = userName;
     this.password = password;
     this.role = role;
   }
 }

 private HashMap<String, UserInfo> store = new HashMap<>();

 public SimpleMapIdentityStore() {
   UserInfo user1 = new UserInfo("user1", "user1_pass", "admin");
   UserInfo user2 = new UserInfo("user2", "user2_pass", "user");
   UserInfo user3 = new UserInfo("user3", "user3_pass", "manager");
   store.put(user1.userName, user1);
   store.put(user2.userName, user2);
   store.put(user3.userName, user3);
 }

 public CredentialValidationResult validate(UsernamePasswordCredential usernamePasswordCredential) {
   String userName = usernamePasswordCredential.getCaller();
   String password = usernamePasswordCredential.getPasswordAsString(); 

   UserInfo userInfo = this.store.get(userName.toLowerCase());
   if (userInfo == null || !userInfo.password.equals(password)) {
     return INVALID_RESULT;
   }

   return new CredentialValidationResult(userInfo.userName, new HashSet<>(asList(userInfo.role)));
 } 
}

重要的是,前面的类需要注解为@ApplicationScoped,这样它就可以在整个应用程序中可用,并且 CDI 可以注入它。我们在前面的类中硬编码了用户和角色到一个HashMap中,但你也可以编写代码从任何来源获取用户和角色,例如数据库、LDAP 或文件。在应用程序中,可以有多个IdentityStore。容器将调用每个validate方法。在validate方法中,我们首先验证用户名和密码是否有效,然后返回一个带有用户角色的CredentialValidationResult实例。

构建应用程序(在项目上右键单击并选择 Run As | Maven Install),然后按照前面章节的描述将其部署到 GlassFish 5 服务器。确保应用程序的上下文设置为/CourseManagementMavenWebApp-jee8。你可以在 GlassFish 管理页面上通过编辑已部署的应用程序并验证上下文根字段的值来验证这一点。然后浏览到http://localhost:8080/CourseManagementMavenWebApp-jee8/AdminServlet。如果你使用 user1 凭据登录,则将显示管理页面。如果你以 user3 的身份登录,则将显示管理页面。其他所有用户的访问都被阻止。你需要关闭浏览器窗口以尝试使用不同的用户登录,因为一旦登录,用户凭据将被记住,直到会话被无效化。应用程序可以很容易地扩展以添加注销选项,就像我们在前面的章节中所做的那样。

在前面的示例中,我们已经创建了一个自定义的身份存储。你可以在这个地方实现任何代码来获取用户信息,无论是从数据库还是 LDAP。但是,JEE 安全 API 提供了内置的注解来访问数据库和 LDAP 作为身份存储;即@DatabaseIdentityStoreDefinition@LdapIdentityStoreDefinition。例如,我们可以修改ApplicationConfig类来声明一个数据库身份存储,如下所示:

package packt.book.jee.eclipse.ch7.web.servlet;

import javax.enterprise.context.ApplicationScoped;
import javax.security.enterprise.identitystore.DatabaseIdentityStoreDefinition;
import javax.security.enterprise.identitystore.PasswordHash;

@DatabaseIdentityStoreDefinition (
 dataSourceLookup = "jdbc/CourseManagement",
 callerQuery = "select password from user_group_view where user_name = ?",
 groupsQuery = "select group_name from user_group_view where user_name = ?",
 hashAlgorithm = PasswordHash.class,
 priority = 10 
)
@ApplicationScoped
public class ApplicationConfig {
}

我们需要传递 JDBC 资源的 JNDI 查找名称,即jdbc/CourseManagement,以及用于验证用户名和密码以及获取组的 SQL 查询。这些与我们在 GlassFish 管理页面上创建 Realm 时配置的 SQL 查询类似,但使用新的安全 API,使配置更加便携。有关IdentityStore注解的更多详细信息,请参阅javaee.github.io/security-spec/spec/jsr375-spec.html#_annotations_and_built_in_identitystore_beans

在前面的示例中,我们使用了基本的认证类型。但是,你可以使用基于表单的认证,通过使用@FormAuthenticationMechanismDefinition注解。例如,我们可以将@BasicAuthenticationMechanismDefinition替换为@FormAuthenticationMechanismDefinition,如下所示:

package packt.book.jee.eclipse.ch7.web.servlet;
// ...
@FormAuthenticationMechanismDefinition(
 loginToContinue = @LoginToContinue(
 loginPage = "/loginServlet",
 errorPage = "/loginErrorServlet"
 )
)
@DeclareRoles({"admin"})
@ServletSecurity(@HttpConstraint(rolesAllowed = "admin"))
public class AdminServlet extends HttpServlet {
 ...
}

此配置类似于我们在早期示例中在web.xml中配置的<form-login-config>

注意,新的安全 API 主要在 Java 类上工作,例如 servlets、EJBs 和 beans,但如果你想要保护 JSP 页面,那么你需要使用我们在前几节中学到的声明性配置。

JEE 中的安全是一个非常大的主题,无法在通用的书中涵盖。本章的范围仅限于使用用户名和密码保护 JEE 资源。有关 JEE 中安全性的详细信息,请参阅javaee.github.io/tutorial/security-intro.html

概述

在本章中,我们学习了如何在 JEE 应用程序中保护由 URL 表示的资源。在 JEE 中,声明性保护资源的过程并不完全通用;其中一部分在所有服务器中是通用的,特别是web.xml中的配置。声明性安全域的配置在不同服务器中有所不同。然而,JEE 8 添加了新的 Java EE 安全 API,使得基于注解的配置对 Java 类是可移植的。

我们学习了如何在 GlassFish 和 Tomcat 服务器中保护文件夹。我们还学习了如何保护 RESTful Web 服务,并在客户端应用程序中使用安全凭据调用它们。

posted @ 2025-09-10 15:08  绝不原创的飞龙  阅读(32)  评论(0)    收藏  举报