DropWizard REST Web 服务指南(全)
原文:
zh.annas-archive.org/md5/22f48e3528846c5b231430ad4c3787e1
译者:飞龙
协议:CC BY-NC-SA 4.0
前言
Dropwizard 是一个用于 RESTful Web 服务的 Java 开发框架。它最初由 Yammer 构建,用作其后端系统的基础。Dropwizard 是生产就绪的;它封装了您进行 RESTful 开发所需的一切。
Jersey、Jackson、jDBI 和 Hibernate 只是 Dropwizard 包含的一些库。基于 Dropwizard 构建的应用程序运行在嵌入的 Jetty 服务器上——您无需担心应用程序的部署位置或是否与您的目标容器兼容。
使用 Dropwizard,您将能够以最少的努力和时间高效地构建快速、安全且可扩展的 Web 服务应用程序。
Dropwizard 是开源的,其所有模块都可通过 Maven 仓库获取。这样,您只需在您的 pom.xml
文件中添加适当的依赖项条目,就能集成您想要的每个库——如果它尚未存在。需要具备 Maven 的基本知识和理解。
本书涵盖内容
第一章, Dropwizard 入门,将引导您了解 Dropwizard 的基础知识,帮助您熟悉其概念并准备开发环境。
第二章, 创建 Dropwizard 应用程序,将介绍 Maven 及其如何用于创建 Dropwizard 应用程序。这包括基于默认工件生成空应用程序的结构,以及启动构建 Dropwizard 应用程序所需的必要修改。
第三章, 配置应用程序,介绍了通过启用配置文件和配置类(该类负责获取、验证并使配置值在应用程序中可用)来外部化应用程序配置的方法。
第四章, 创建和添加 REST 资源,将指导您了解应用程序最重要的方面:资源类。您将学习如何将 URI 路径和 HTTP 动词映射到资源类的方 法,以及如何向 Dropwizard 应用程序添加新资源。
第五章, 表示形式 – RESTful 实体,讨论了将表示形式建模为实际 Java 类以及 Jackson 如何自动将 POJO 转换为 JSON 表示形式。
第六章, 使用数据库,展示了 jDBI 的集成和使用方法,如何从接口创建数据访问对象,以及如何使用 jDBI 的 SQL 对象 API 与数据库交互。本章还介绍了所需的附加配置修改。
第七章, 验证 Web 服务请求,介绍了如何使用 Hibernate Validator 在满足请求之前对来自 Web 服务客户端的请求进行验证。
第八章, Web 服务客户端,演示了如何创建一个由 Dropwizard 应用程序使用的托管 Jersey HTTP 客户端,以便通过WebResource
对象与 Web 服务进行交互。
第九章, 认证,介绍了 Web 服务认证的基础知识,并指导您实现基本的 HTTP 认证器,以及如何将其适配到您的资源类和应用程序的 HTTP 客户端。
第十章, 用户界面 – 视图,展示了如何使用 Dropwizard 视图包和 Mustache 模板引擎来为 Web 服务客户端创建 HTML 界面。
附录 A, 测试 Dropwizard 应用程序,展示了如何使用 Dropwizard 的测试模块创建自动化集成测试。本附录还涉及实现应用程序的运行时测试,这些测试被称为健康检查。您将指导实现一个健康检查,以确保您的 HTTP 客户端确实可以与 Web 服务进行交互。
附录 B, 部署 Dropwizard 应用程序,解释了您需要采取的必要步骤,以便通过使用单独的配置文件并保护应用程序的管理端口访问来将 Dropwizard 应用程序部署到 Web 服务器。
您需要为此书准备的内容
为了跟随书中提供的示例和代码片段,您需要一个安装有 Linux、Windows 或 OS X 操作系统的计算机。一个现代的 Java 代码编辑器/ IDE,如 Eclipse、Netbeans 或 IDEA,将真正帮助您。您还需要 Java 开发工具包(JDK)的版本 7 以及 Maven 和 MySQL 服务器。额外的依赖项将由 Maven 获取,因此您需要一个有效的互联网连接。
本书面向的对象
本书的目标读者是至少具备基本 Java 知识和 RESTful Web Services 基本理解的软件工程师和网络开发者。了解 SQL/MySQL 的使用和命令行脚本也可能是有必要的。
习惯用法
在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称的显示方式如下:"在Contact
类中添加一个名为#isValidPerson()
的新方法。"。
代码块设置如下:
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.util.ArrayList;
import javax.validation.Validator;
import javax.ws.rs.core.Response.Status;
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
private final ContactDAO contactDao; private final Validator validator;public ContactResource(DBI jdbi, Validator validator) {contactDao = jdbi.onDemand(ContactDAO.class); this.validator = validator;}
任何命令行输入或输出都应如下编写:
$> java -jar target/app.jar server conf.yaml
新术语和重要词汇将以粗体显示。你会在屏幕上看到,例如在菜单或对话框中的文字,将以这种方式显示:“在某个时候,你将被提示提供MySQL Root 密码。”
注意
警告或重要注意事项将以这种方式显示在框中。
小贴士
小技巧和技巧看起来像这样。
读者反馈
我们始终欢迎读者的反馈。请告诉我们你对这本书的看法——你喜欢什么或者可能不喜欢什么。读者的反馈对我们开发你真正能从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需发送电子邮件到<feedback@packtpub.com>
,并在邮件主题中提及书名。
如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com
。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support
并注册,以便直接将文件通过电子邮件发送给你。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果你发现任何错误清单,请通过访问www.packtpub.com/submit-errata
,选择你的书,点击错误清单提交表单链接,并输入你的错误清单详情。一旦你的错误清单得到验证,你的提交将被接受,错误清单将被上传到我们的网站,或添加到该标题的错误清单部分。任何现有的错误清单都可以通过从www.packtpub.com/support
中选择你的标题来查看。
盗版
在互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过 <copyright@packtpub.com>
联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。
问题和版权
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com>
联系我们,我们将尽力解决。
第一章. 开始使用 Dropwizard
Dropwizard 是一个开源的 Java 框架,用于快速开发 RESTful Web 服务,它将您所需的一切整合在一起。您可以使用一个生产就绪的应用程序,利用 Jetty、Jersey、Jackson、JDBI 和 Hibernate,以及 Dropwizard 包含的大量其他库,无论是核心库还是模块库。这解决了在从头开始构建 Web 服务应用程序时手动添加、配置和连接大量不同库的问题。可以这样想:您将需要 Jersey 来公开 Web 服务,一些其他库用于数据库交互,以及额外的库用于验证和身份验证,更不用说依赖项管理、打包和分发的开销了。
在本书的各个章节中,我们将使用 Dropwizard 和其组件来构建一个示例应用程序——即一个电话簿应用程序,它公开了一组 RESTful Web 服务,这些服务有助于存储和管理联系人。它的工作方式几乎与您手机内置的电话簿应用程序或任何其他联系人管理应用程序相同。
使用 Dropwizard 开发 Web 服务
我们将使用 Jersey 来构建我们的 Web 服务。Jersey 是 JAX-RS 标准(JSR 311)的参考实现,即 Java API for RESTful Web Services。JAX-RS 利用注解,简化了 Web 服务应用程序的开发。
我们将构建的 Web 服务将生成 JSON 输出。Dropwizard 包含 Jackson,这是一个快速、可配置的 JSON 处理器,并由 Jersey 用于将普通的 Java 对象转换为 JSON 表示形式。
我们的应用程序将使用数据库来存储数据。为了满足我们的数据库交互需求,我们将使用 JDBI。JDBI 是一个库,它将使我们能够轻松地创建 DAO 接口。数据访问对象将允许我们通过将 Java 方法映射到 SQL 查询和语句来执行数据库操作。JDBI 作为 Dropwizard 模块提供,使我们能够轻松快速地构建数据访问对象。
Dropwizard 包含验证、监控和测试模块,我们将使用这些模块来确保我们的服务在生产环境中表现正确。我们将集成 Dropwizard 的验证机制,确保在尝试提供服务之前,每个对我们的 Web 服务的请求都是有效的。
准备开发环境
在我们开始创建 Dropwizard 应用程序之前,我们需要设置我们的开发环境,这至少包括 Java (JDK 7)、Maven 和 MySQL。
准备工作
Maven 是 Java 项目的构建管理器。我们将使用它来创建和构建我们的项目。我们的应用程序的依赖项(对 Dropwizard 模块的依赖)将由 Maven 管理;我们只需在我们的项目配置文件中添加适当的条目即可。
我们需要一个数据库,因此我们将使用 MySQL 来满足本书的需求。MySQL 是最流行的开源关系型数据库管理系统——是网络应用程序的常见选择。在整个安装过程中,你将被提示创建或配置环境变量的值。这个过程因操作系统而异,并且超出了本书的范围。
如何操作…
我们将查看你需要下载和安装的所有组件。
下载和安装 Java
-
从
www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html
下载 Java 7 JDK。 -
由于有许多安装包可用,你需要根据你的操作系统和平台选择合适的安装包。
-
下载完成后,通过运行你下载的安装程序来安装 JDK,如下面的截图所示。目前不需要使用与默认设置不同的设置。经过几个步骤后,安装将完成。
-
安装成功后,将
JAVA_HOME
环境变量设置为 Java 安装路径。在 Windows 中,这可能类似于C:\Program Files\Java\jdk1.7.0_40\
。
下载和安装 Maven
-
Maven 安装相当简单。只需从
maven.apache.org/download.cgi
下载 Maven 二进制文件,并将包的内容提取到你的选择目录中。 -
修改
PATH
环境变量,添加以\bin
结尾的 Maven 目录,例如C:\apache-maven-3.0.5\bin
,这样在命令行或终端中使用时,mvn
可执行文件将在所有目录中可用。
下载和安装 MySQL
-
从
dev.mysql.com/downloads/mysql/#downloads
下载适用于你操作系统的MySQL Community Server安装程序。 -
运行安装程序并选择安装 MySQL。保持建议的默认安装设置。
-
在某个时候,你将被提示提供MySQL Root 密码。这是 root 用户的密码,具有完全访问权限。输入你选择的密码,然后点击下一步 >按钮。安装将很快完成。
-
请选择一个你容易记住的密码,因为你将在稍后阶段需要提供它。
它是如何工作的…
我们刚刚完成了构建 Dropwizard 应用程序所需软件包的安装。我们将使用 Maven 来创建应用程序的结构,该应用程序将使用 MySQL 作为其数据的持久存储。
我们将创建一个 Maven 项目,并在其 项目对象模型 (POM) 文件中包含应用将使用的 Dropwizard 组件的引用(依赖项)。Maven 将自动下载并使它们在整个项目中可用。
第二章。创建 Dropwizard 应用程序
让我们通过创建基于 Dropwizard 的新 RESTful Web 服务应用程序所需的过程。首先,我们需要创建应用程序的结构、文件和文件夹,并获取必要的库。幸运的是,Maven 将为我们处理这些任务。
一旦我们的应用程序结构准备就绪,我们将修改适当的文件,定义应用程序对 Dropwizard 模块的依赖关系,并配置应用程序的可执行包应该如何生成。之后,我们可以继续编写应用程序的代码。
生成基于 Maven 的项目
在我们开始编码之前,我们需要执行一些任务,以便正确创建项目结构。我们将使用 Maven 来生成一个默认的、空的项目,然后将其转换为 Dropwizard 应用程序。
准备工作
我们的项目将基于 maven-archetype-quickstart
架构。架构是 Maven 项目模板,通过使用 quick-start
架构,我们将在很短的时间内准备好项目结构(文件夹和文件)。
如何操作…
-
打开终端(Windows 的命令行)并导航到您想要创建应用程序的目录。
-
通过执行以下命令创建一个新的 Maven 项目(不带换行符):
$ mvn archetype:generate-DgroupId=com.dwbook.phonebook-DartifactId=dwbook-phonebook-DarchetypeArtifactId=maven-archetype-quickstart-DinteractiveMode=false
这将在 dwbook-phonebook
目录中创建一个空白的 Maven 项目。
小贴士
下载示例代码
您可以从您在 www.packtpub.com
的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
工作原理…
Dropwizard 是基于 Maven 的,因此我们在其中包含了 Dropwizard 的核心依赖项的新 Maven 项目中创建了一个新项目。
到目前为止,dwbook-phonebook
目录的结构如下所示:
src/
文件夹将包含我们的应用程序的主要类,而所有测试类都将放置在 test/
目录下。
注意 Maven 已经将 pom.xml
放在了应用程序的根目录下。项目对象模型(POM)是一个包含有关项目配置和依赖项重要信息的 XML 文件。这是我们需要编辑的文件,以便为我们的项目添加 Dropwizard 支持。
配置 Dropwizard 依赖项和构建配置
我们刚刚创建了一个示例应用程序概要。接下来,我们需要编辑项目的配置文件pom.xml
,并定义我们的应用程序所依赖的 Maven 模块。我们正在构建一个 Dropwizard 应用程序,而 Dropwizard 基于 Maven,所以我们需要的一切都在 Maven Central Repository 中。这意味着我们只需要提供模块的 ID,Maven 就会负责下载并将这些模块包含到我们的项目中。
接下来,我们需要为我们的项目添加构建和打包支持。我们将使用maven-shade
插件,这将允许我们将我们的项目及其依赖项完全打包成一个单一的独立 JAR 文件(胖 JAR),它可以直接分发和执行。
如何做到这一点...
执行以下步骤以配置 Dropwizard 依赖项并构建配置:
-
我们需要通过添加包含所有 Dropwizard 模块快照的 Maven 仓库来配置我们的 POM。然后,Maven 将能够在构建我们的项目时自动获取所需的模块。在
pom.xml
中的<dependencies>
部分找到并添加以下条目:<repositories><repository><id>sonatype-nexus-snapshots</id><name>Sonatype Nexus Snapshots</name><url>http://oss.sonatype.org/content/repositories/snapshots</url></repository></repositories>
-
要定义依赖项,在
<dependencies>
部分内添加以下代码:<dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-core</artifactId><version>0.7.0-SNAPSHOT</version></dependency>
-
要配置构建和打包过程,在
pom.xml
中的<project>
部分找到并插入以下条目:<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>1.7</source><target>1.7</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>1.6</version><configuration><filters><filter><artifact>*:*</artifact><excludes><exclude>META-INF/*.SF</exclude><exclude>META-INF/*.DSA</exclude><exclude>META-INF/*.RSA</exclude></excludes></filter></filters></configuration><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformerimplementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>com.dwbook.phonebook.App</mainClass></transformer></transformers></configuration></execution></executions></plugin></plugins></build>
它是如何工作的...
我们刚刚告诉 Maven 构建我们的应用程序所需知道的一切。Maven 将从 Maven Central Repository 获取 Dropwizard 核心模块,并在打包(由于mvn package
命令)应用程序时将其包含在构建路径中。
此外,我们使用maven-shade
插件添加了构建和打包支持,并指定了我们的应用程序的主类(pom.xml
中的<mainClass>
部分),这有助于将 Dropwizard 应用程序及其依赖项打包成一个单一的 JAR 文件。我们还指示maven-compiler-plugin
为 Java 1.7 版本构建应用程序(检查maven-compiler plugin
配置部分的 target 和 source 元素)。
排除数字签名
maven-shade
配置中的<excludes>
部分指示 Maven 排除所有引用的已签名 JAR 文件的数字签名。这是因为 Java 否则会在运行时将它们视为无效,从而阻止我们的应用程序执行。
使用 Dropwizard 的“Hello World”
我们的项目依赖项现在已设置在pom.xml
文件中,我们可以开始构建我们的应用程序。Maven 已经在我们App.java
文件中创建了应用程序的入口点类,即App
类。然而,其默认内容更适合于一个普通的 Java 应用程序,而不是基于 Dropwizard 的。
如何做到这一点...
让我们看看打印Hello World
消息使用 Dropwizard 所需的步骤:
-
在
App.java
文件中,添加以下导入语句:import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.dropwizard.Application; import io.dropwizard.Configuration; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment;
-
修改
App
类的定义,如下一步所示。这个类需要扩展Application <Configuration>
。 -
通过在
App
类的定义之后将其声明为静态最终成员,为我们的应用程序添加一个日志记录器:public class App extends Application<Configuration> {private static final Logger LOGGER =LoggerFactory.getLogger(App.class);
-
通过添加以下代码实现
Service
类的抽象方法initialize()
和run()
:@Overridepublic void initialize(Bootstrap<Configuration> b) {}@Overridepublic void run(Configuration c, Environment e) throwsException {LOGGER.info("Method App#run() called");System.out.println( "Hello world, by Dropwizard!" );}
-
最后,修改
main()
方法,添加必要的代码来实例化我们的 Dropwizard 服务:public static void main( String[] args ) throws Exception{new App().run(args);}
-
在
dwbook-phonebook
目录内,通过执行以下命令来构建应用程序:$ mvn package
此命令的输出将包含[INFO] BUILD SUCCESS
行,表明项目已成功构建,如下面的截图所示:
Maven 使用 shade 插件生成了可执行的 Fat JAR,它位于target/directory
目录下,名为dwbook-phonebook-1.0-SNAPSHOT.jar
。你可以像运行任何可执行 JAR 文件一样使用java -jar
命令来运行它,如下所示:
$ java -jar target/dwbook-phonebook-1.0-SNAPSHOT.jar server
通常,你会在终端看到很多条目,包括一个错误。第一行是我们包含的#run()
方法的提示信息。随后是一个警告信息,指出我们的应用程序没有配置健康检查,但这是我们将在本书后面处理的事情。
下一个记录的条目表明,嵌入在我们 Dropwizard 应用程序中的 Jetty 服务器正在启动并监听 8080 端口的传入请求。8081 端口也用于管理目的。你还会看到一个错误,指出找不到任何资源类(ResourceConfig
实例不包含任何根资源类),这是合理的,也是绝对正常的,因为我们还没有创建和配置任何 REST 资源。
它是如何工作的…
我们刚才所做的只是添加了在 Dropwizard 应用程序中所需的最小代码量。正如你所看到的,我们的应用程序的入口点类需要扩展io.dropwizard.Application
类,并实现initialize(Bootstrap<Configuration>)
和run(Configuration, Environment)
方法。initialize
方法负责引导,可能加载额外的组件,并通常准备应用程序的运行环境。
在这个阶段,我们只是打印一个Hello
消息,所以我们在run()
方法中只包含了一个println()
语句。
由mvn
包命令生成的 JAR 文件的执行导致 Dropwizard 打印了Hello World!问候语,因为public static void main
触发了public void run
方法中的相关代码。
还有更多…
为了执行 JAR 文件,我们在命令中添加了 server
参数。在 public static void main
中,我们调用了 public void run
方法,并将命令行参数传递给它。Dropwizard 只有一个预配置的命令(尽管我们能够配置额外的命令),即 server
命令,它启动内嵌的 HTTP 服务器(Jetty)以运行我们的服务。在我们的例子中,在 run()
方法中的代码执行之后,显示了一个带有异常的错误,因为 Jetty 找不到任何 REST 资源来提供服务。
日志记录
Dropwizard 由 Logback 支持,并为我们的日志记录提供了 SLF4J 实现。在 App.java
文件中,我们导入了必要的 Logger
和 LoggerFactory
类,以便构建一个我们可以用于日志记录需求的 Logger
实例。
默认 HTTP 端口
Dropwizard 的内嵌 Jetty 服务器默认会尝试绑定到端口 8080 和 8081。端口 8080 由服务器用于处理应用程序的传入 HTTP 请求,而 8081 由 Dropwizard 的管理界面使用。如果在您的系统上运行了其他使用这些端口中的任何一个的服务,当尝试运行此示例时,您将看到 java.net.BindException
异常。
之后,我们将看到如何配置您的应用程序以使用另一个端口来接收请求,但到目前为止,请确保这个端口可用。
第三章。配置应用程序
到目前为止,我们已经为 Dropwizard 应用程序创建了一个简单的模板。我们的应用程序在启动时会向终端打印一条消息。
通常,每个现代应用程序都依赖于一系列配置设置,这些设置定义了它的运行方式。例如,一旦我们的应用程序增长并需要与数据库交互,我们就应该以某种方式使用(至少)用户名和密码来建立数据库连接。当然,我们可以在应用程序内部硬编码这些设置,但这并不高效,因为即使是微小的更改也要求重新构建它。存储此类或类似信息的适当方式是使用外部配置文件。
将应用程序的配置外部化
使用配置文件需要适当的应用程序逻辑来加载和解析它。幸运的是,Dropwizard 内置了我们将要使用的功能,以便外部化我们的应用程序配置。
如何做…
-
在与
pom.xml
文件相同的目录下创建一个名为config.yaml
的新 YAML 文件。这将是我们的应用程序的配置文件。我们将添加两个配置参数:启动时打印的消息以及打印的次数。为了做到这一点,请将以下代码添加到config.yaml
中:message: This is a message defined in the configuration file config.yaml. messageRepetitions: 3
-
现在我们有了配置文件,但我们需要解析它。让我们在
com.dwbook.phonebook
包中创建一个新的类PhonebookConfiguration
,通过添加以下代码:package com.dwbook.phonebook;import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.Configuration;public class PhonebookConfiguration extends Configuration {@JsonPropertyprivate String message;@JsonPropertyprivate int messageRepetitions;public String getMessage() {return message;}public int getMessageRepetitions() {return messageRepetitions;} }
注意
如你所见,它是一个简单的类,包含两个成员属性,分别以我们的配置设置命名,以及它们的 getter 方法。
-
要使用此类作为我们的配置代理,修改我们的主
App
类的声明,使其扩展Application<PhonebookConfiguration>
类而不是Application<Configuration>
:public class App extends Application<PhonebookConfiguration> {
-
类似地,更新
configuration
到PhonebookConfiguration
在App#initialize()
方法的声明中:@Overridepublic void initialize(Bootstrap<PhonebookConfiguration> b) {}
-
App#run()
方法在其定义中也需要相同的修改,但我们还将进一步修改此方法,以便从配置类中检索要打印的消息:public void run(PhonebookConfiguration c, Environment e)throws Exception {LOGGER.info("Method App#run() called");for (int i=0; i < c.getMessageRepetitions(); i++) {System.out.println(c.getMessage());} }
-
打包(
mvn package
)并运行应用程序,并指定配置文件:$ java -jar target/dwbook-phonebook-1.0-SNAPSHOT.jar server config.yaml
在应用程序启动期间,你将在终端中看到消息打印了三次,如下截图所示:
此外,就像前面的例子一样,你还会看到一个异常,指出找不到资源类(ResourceConfig
实例不包含任何根资源类)。这是因为我们在应用程序中还没有注册任何 REST 资源。我们将在下一章中处理这个问题。
它是如何工作的…
你应该看到我们的配置文件被自动解析。实际上,PhonebookConfiguration
类是用配置文件中指定的值实例化的。
当配置文件作为命令行参数传递时,Dropwizard 会解析它并创建你的服务配置类的实例。我们将所需的配置参数作为PhonebookConfiguration
类的私有成员添加,并使用@JsonProperty
注解它们,以便 Dropwizard 可以解析它们。为了使这些属性对我们应用程序的服务类可访问,我们还需要为这些参数添加公共 getter 方法。
还有更多...
将应用程序的配置外部化有许多优点。使用 Dropwizard,你可以轻松地存储和读取你希望为应用程序使用的任何类型的属性(配置设置),只需将 YAML 属性映射到配置类的属性即可,无需花费太多精力。
Dropwizard 的配置参数
Dropwizard 提供了大量的配置参数,例如嵌入式 Jetty 监听的端口和日志级别。虽然这个列表相当长,无法在此详尽介绍,但它可以在官方 Dropwizard 网站上找到,链接为www.dropwizard.io/manual/core/#configuration-defaults
。
YAML
根据其官方网站的描述(www.yaml.org
),YAML 是一种人性化的数据序列化标准。它的语法相当简单,这也是 YAML 被广泛接受的原因。YAML 文件由扩展名.yaml
和.yml
标识;两者都是有效的,尽管.yml
似乎最近更受欢迎。
验证配置设置
虽然将应用程序的配置外部化是好事,但我们不应总是完全依赖它。Dropwizard 已经为我们提供了保障,并且我们有适当的工具来在应用程序启动时验证配置属性。这是因为我们可以为配置属性使用约束注解,例如包含在javax.validation.constraints
或org.hibernate.validator.constraints
包中的那些注解。
我们将限制消息重复的次数为 10;如果提供的数字大于 10,则输入被视为无效。
如何做到这一点...
让我们通过以下步骤来验证配置设置:
-
更新
PhonebookConfiguration
中messageRepetitions
属性的定义,使用@Max
注解标注该属性(你还需要导入javax.validation.constraints.Max
):@JsonProperty @Max(10) private int messageRepetitions;
-
以类似的方式,定义
message
属性不应为空,使用@NotEmpty (org.hibernate.validator.constraints.NotEmpty)
注解标注该属性:@JsonProperty @NotEmpty private String message;
-
编辑
Config.yaml
文件,并为messageRepetitions
属性指定一个大于 10 的值。 -
重新打包并再次运行应用程序。应用程序将拒绝启动,你将在终端上看到以下截图所示的错误信息:
它是如何工作的…
验证相关的注解强制 Dropwizard 验证我们配置文件中声明的每个属性的值。如果验证约束不满足,相关的错误信息将打印在终端上,并且应用程序将不会启动。
还有更多…
现在你有一个在应用程序启动时映射到配置对象上的工作配置文件。此外,除了检查配置参数的有效性之外,你还可以为每个参数提供一个默认值。
指定默认参数
你可以像在声明时初始化变量一样轻松地指定配置参数的默认值。这样,可选参数可以省略,并且在运行时可以具有默认值,即使它们没有包含在应用程序的配置文件中。
让我们添加一个额外的参数,我们将初始化它,命名为additionalMessage
,以及它的 getter 方法:
@JsonProperty
private String additionalMessage = "This is optional";
public String getAdditionalMessage() {return additionalMessage;
}
如果你运行应用程序并指定了一个不包含additionalMessage
属性的配置文件,那么当你尝试从代码的其他部分访问该属性时,将返回此属性的默认值,例如,如果你在App#run()
方法内部使用c.getAdditionalMessage()
。这样,你可以为你的应用程序提供可选参数。
第四章:创建和添加 REST 资源
到目前为止,我们的应用程序实际上并没有做很多事情。这是因为它缺少配置的 REST 资源。REST 资源是人们可以称之为实体的东西,在我们的案例中,是一组 URI 模板,具有共同的基 URL,人们可以使用常见的 HTTP 方法与之交互。
创建资源类
我们正在构建一个电话簿应用程序,因此我们需要实现存储和管理联系人的必要功能。我们将创建电话簿服务的资源类。此类将负责处理 HTTP 请求并生成 JSON 响应。资源类最初将提供检索、创建、更新和删除联系人的端点。
请注意,我们目前还没有处理结构化数据或与数据库交互,因此从我们的应用程序传输到和从应用程序传输的联系人相关信息不遵循特定的格式。
如何做到这一点...
创建资源类的以下步骤:
-
创建一个新的包,
com.dwbook.phonebook.resources,
并在其中添加一个ContactResource
类。 -
导入所需的包,
javax.ws.rs.*
和javax.ws.rs.core.*.wdasdasd
:import javax.ws.rs.*; import javax.ws.rs.core.*;
-
通过使用
@Path
注解指定资源的 URI 模板,并使用@Produces
注解指定响应Content-Type
头:@Path("/contact") @Produces(MediaType.APPLICATION_JSON) public class ContactResource {// code... }
-
为了添加一个将返回存储联系人信息的方法的步骤,创建
#getContact()
方法。此方法将返回一个javax.ws.rs.core.Response
对象,这是一种简单但有效的方式来操作发送给执行请求的客户端的实际 HTTP 响应。添加@GET
和@PATH
注解,如以下代码片段所示。这将使方法绑定到/contact/{id}
的 HTTP GET 请求。URI 的{id}
部分代表一个变量,并通过@PathParam
注解绑定到同一方法的int id
参数:@GET@Path("/{id}")public Response getContact(@PathParam("id") int id) {// retrieve information about the contact with theprovided id// ...return Response.ok("{contact_id: " + id + ", name: \"Dummy Name\",phone: \"+0123456789\" }").build();}
-
同样,我们需要实现创建、删除和更新联系人的适当方法。创建联系人的
#createContact()
方法将绑定到/contact
URI 的 HTTP POST 请求。由于我们的基本 URI 没有附加任何内容,因此此方法不需要使用@Path
注解。此方法也将返回一个Response
对象,就像我们所有资源的方法一样,表示已创建新的联系人:@POST public Response createContact(@FormParam("name") String name,@FormParam("phone") String phone) {// store the new contact // ...return Response.created(null).build(); }
-
对于删除现有联系人,HTTP 客户端需要向特定联系人的 URI 发送 HTTP DELETE 请求。因此,相应方法的 URI 将与检索单个联系人的 URI 完全相同。将
#deleteContact()
方法添加到我们的资源类中,如以下代码片段所示。我们还需要表明请求的 URI 不再包含内容:@DELETE @Path("/{id}") public Response deleteContact(@PathParam("id") int id) {// delete the contact with the provided id// ...return Response.noContent().build(); }
-
现有联系人的更新通常是通过向联系人的端点发送 HTTP PUT 请求来完成的。
#updateContact()
方法将处理此类请求并指示更新成功,返回适当的Response
对象:@PUT @Path("/{id}") public Response updateContact(@PathParam("id") int id,@FormParam("name") String name,@FormParam("phone") String phone) {// update the contact with the provided ID// ...return Response.ok("{contact_id: "+ id +", name: \""+ name +"\",phone: \""+ phone +"\" }").build();}
-
通过修改
App
类中的run
方法并通过JerseyEnvironment#register()
方法将实现的资源添加到我们的 Dropwizard 应用程序环境中,如以下代码所示。您还需要在App.java
文件顶部添加一个导入语句来导入ContactResource
类(导入com.dwbook.phonebook.resources.ContactResource
)。您还应该看到,为了访问我们的应用程序的 Jersey 环境,您可以使用Environment#jersey()
方法:public void run(PhonebookConfiguration c, Environment e) throws Exception {// ...// Add the resource to the environmente.jersey().register(new ContactResource());}
-
使用
mvn package
重建应用程序并运行java -jar target/dwbook-phonebook-1.0-SNAPSHOT.jar server config.yaml
。您将看到一条消息,指示我们的(基于 Jersey 的)Dropwizard 应用程序正在启动,以及配置的资源列表,在这种情况下,是我们在com.dwbook.phonebook.resources.ContactResource
类中定义的资源。 -
将您的浏览器指向
http://localhost:8080/contact/100
并查看结果;它将生成一个 ID 为 100 的虚拟 JSON 表示,这是您在 URL 中提供的(一个路径参数,它将适用于任何整数)。
服务正在运行并监听传入的请求。您可以通过在终端中按 Ctrl + C 来关闭它。几秒钟后,服务将停止。
它是如何工作的...
资源类是 RESTful Web 服务最重要的部分,因为它是定义您希望公开的资源及其 URI 的地方。
@Produces
注解定义了类方法生成的响应的内容类型。尽管它定义了 HTTP Content-Type
头的值,但它也用于指示 Jackson 将表示转换为适当的格式,在这种情况下是 JSON;因此有 MediaType.APPLICATION_JSON
的定义。如果我们想返回 XML 文档作为响应,我们应该使用 MediaType.APPLICATION_XML
。
我们使用 @Path
注解来定义 URI 模板。通过应用它并将其提升到类的级别,我们定义了资源的基 URI 将是 /contact
。我们还使用了此注解来指定 #getContact
方法,指定了 /{id}
模板。这导致触发 #getContact
执行的完整 URI 是 /contact/{id}
。
URI 的 {id}
部分是一个路径参数,我们使用 @PathParam
注解将其映射到 int id
参数。PathParam
使用路径参数的名称作为其参数,在这种情况下是 id
。
Jersey 将拦截每个传入的 HTTP 请求并尝试将其与定义的 URI 模板匹配,以找到要调用的资源类方法。
通常,在类级别定义基本 URI 是一个好习惯,并且还可以为每个方法定义更具体的 URI 模板。
为了配置我们的应用程序使用我们创建的资源,我们必须在 App
类的 #run()
方法初始化后将其添加到执行环境中。
更多内容…
表示是一个实体;可以引用的东西。表示可以创建、更新、删除和返回。REST 资源是接受此类操作的 HTTP 请求的端点。
我们为 #getContact()
方法使用了 @GET
注解。这意味着该方法绑定到,并且仅绑定到 HTTP GET 动词。我们使用这个动词是因为我们返回了关于实体的数据,但没有以任何方式修改它。
HTTP 动词 – RESTful 约定
通常,RESTful Web 服务使用四种基本的 HTTP 方法(动词)映射到 CRUD 操作:
-
POST 用于创建资源
-
PUT 用于更新资源
-
DELETE 用于删除资源
-
GET 用于返回资源的表示
GET 是一个幂等操作;如果给定相同的输入,它将在任何情况下返回相同的结果,而不会修改请求实体。
小贴士
您可以使用适当的注解(例如 @POST
、@PUT
、@DELETE
和 @GET
)将 HTTP 动词映射到资源方法(例如,#getContact()
)。
HTTP 响应代码
除了 CRUD 操作映射到特定的 HTTP 方法之外,另一个重要的 RESTful Web 服务设计原则是根据请求和触发的操作结果使用特定的响应代码。
根据这个约定,当新实体成功创建时,我们的应用程序会响应,表示 HTTP 响应状态代码为 201 Created
。
类似地,当实体成功删除时,我们的应用程序会发送 204 No Content
代码。204 No Content
代码也可以在其他情况下使用,当我们发送给客户端的响应不包含实体时,而不仅仅是在我们删除资源的情况下。
对于大多数情况,当我们的应用程序在响应 GET 请求时返回数据,200 OK
响应代码就足够了。
我们在我们的实现中使用了响应类,以便在我们的应用程序响应中包含特定的响应代码。
响应类
javax.ws.rs.Response
类,我们所有方法都返回其实例,提供了一套 ResponseBuilder
方法,我们可以使用这些方法来构建返回给执行 HTTP 请求到我们服务的客户端的数据。
Response#ok()
方法接受一个 Object
实例作为参数,然后根据我们的服务响应格式(由 @Produces
注解定义)相应地进行序列化。使用此方法将返回 HTTP 200 OK
响应代码给客户端。
Response#noContent()
方法返回一个 HTTP 204 No Content
响应代码给客户端,表示没有内容适用于此请求。
另一方面,Response#created()
方法用于发送一个201 Created
响应代码,并附带新创建资源的 URI。URI(或 null)可以作为参数传递给此方法,并将其用作响应的Location
头部的值。
Response
类有多个类似的有用方法,但它还允许我们设置自定义响应代码,而无需使用预定义方法之一。为此,你可以通过提供适当的响应代码来使用Response#status()
方法,如下例所示:
Response.status(Status.MOVED_PERMANENTLY);
此外,我们可以使用ResponseBuilder#entity()
方法来设置适当响应负载。#entity()
方法接受Object
作为参数,并以类似于Response#created()
方法的方式处理它:
Response.status(Status.MOVED_PERMANENTLY).entity(new Object());
应该注意的是,所有这些方法都返回一个ResponseBuilder
实例,并且也可以进行链式调用。为了构建Response
对象,我们必须使用ResponseBuilder#build()
方法。
第五章。表示 – RESTful 实体
我们的 Web 服务现在通过利用Response
类来响应产生输出的请求。我们注意到这个类有一些方法接受一个对象作为参数。
创建表示类
我们将创建由我们的应用程序的 REST 资源产生的表示。一个简单的 Java 类就是 Jersey 所需的一切,因此它将把该类视为 RESTful 表示。
由于我们的 Web 服务需要以 JSON 格式生成与联系人相关的信息,一个示例响应将类似于以下代码:
{ id: 1, firstName: "John", lastName: "Doe", phone: "+123-456-789" }
我们将围绕这个 JSON 字符串构建我们的表示类。该类将具有必要的属性(id
、firstName
、lastName
和phone
)以及它们的 getter 方法。
如何做到这一点…
执行以下步骤以创建表示类:
-
创建一个名为
com.dwbook.phonebook.representations
的新包,并在其中创建一个Contact
类。 -
将上述联系人属性作为 final 成员添加,并实现它们的 getter 和构造函数:
package com.dwbook.phonebook.representations;public class Contact {private final int id;private final String firstName;private final String lastName;private final String phone;public Contact() {this.id = 0;this.firstName = null;this.lastName = null;this.phone = null;}public Contact(int id, String firstName, String lastName,String phone) {this.id = id;this.firstName = firstName;this.lastName = lastName;this.phone = phone;}public int getId() {return id;}public String getFirstName() {return firstName;}public String getLastName() {return lastName;}public String getPhone() {return phone;} }
它是如何工作的…
联系人的表示类现在已准备就绪。所需的一切只是一个具有与我们要生成应用程序的 JSON 对象相同属性的普通 Java 类。然而,为了使其工作,需要适当的公共 getter 方法。
我们的属性被声明为 final,以便是不可变的,因此我们也创建了一个相应初始化属性的构造函数。
这个类的实例现在可以用作我们基于 Jersey 的 REST 资源的输出。Jackson 将透明地处理从 POJO 到 JSON 的转换。
更多内容…
任何 POJO 都可以用作表示。Jackson 根据每个类的 getter 方法和它们的返回类型递归地构建 JSON 字符串。
Jackson Java JSON 处理器
Jackson 是一个强大的开源 JSON 数据绑定/解析器/处理器,它简化了将普通 Java 对象转换为 JSON 格式以及相反的过程。Jersey 使用 Jackson 来满足其转换需求,并且是dropwizard-core
模块的一部分;因此,它已经包含在我们的项目设置中。
JSON 数组
任何java.util.List
类型的实例都将被转换为 JSON 数组。例如,如果我们想为联系人存储多个电话号码,我们将在表示类中声明private final List<String> phoneNumbers
(对类构造函数和 getter 的适当修改)。
这将导致以下格式的 JSON 表示:
{ id: 1, firstName: "John", lastName: "Doe", phoneNumbers: ["+123-456-789", "+234-567-890", "+345-678-901"] }
忽略属性
你可以通过在其 getter 上添加@JsonIgnore
注解来防止一个属性成为 JSON 表示的一部分。
这将导致 Jackson 忽略一个否则会被视为 JSON 属性的 getter 方法。
通过 Resource 类提供表示
考虑我们之前实现的ContactResource#getContact()
方法。我们使用Response#ok(Object entity)
方法来构建要发送给客户端的响应,并将其作为参数传递给String
,如下面的代码所示:
return Response.ok("{id: " + id + ", name: \"Dummy Name\", phone: \"+0123456789\" }").build();
现在,我们已经准备好了Representation
类,我们将利用它并将其实例传递给#ok()
方法。
如何做到这一点...
执行以下步骤以通过资源类学习表示的提供:
-
根据以下代码相应地更新
ContactResource#getContact()
方法,以便在#ok()
方法中传递Contact
对象而不是String
,您需要首先导入Contact
类(import com.dwbook.phonebook.representations.Contact
):@GET @Path("/{id}") public Response getContact(@PathParam("id") int id) {// retrieve information about the contact with the provided id// ...return Response.ok( new Contact( id, "John", "Doe", "+123456789") ).build(); }
-
接下来,修改方法的签名,将
name
变量拆分为firstName
和lastName
,以便与Contact
类保持一致:@PUT@Path("/{id}")public Response updateContact(@PathParam("id") int id,@FormParam("firstName") String firstName,@FormParam("lastName") String lastName,@FormParam("phone") String phone) {// update the contact with the provided ID// ...return Response.ok( new Contact(id, firstName, lastName, phone) ).build();}
-
重新构建(
mvn package
)并再次运行应用程序:$ java -jar target/dwbook-phonebook-1.0-SNAPSHOT.jar server config.yaml
-
导航到
http://localhost:8080/contact/123
或向同一 URL 执行 PUT 请求。您将看到服务器发送给我们的请求的响应是我们传递给Response#ok()
方法的对象的 JSON 表示。
它是如何工作的...
我们通过使用Response#ok()
方法定义发送给客户端的响应,该方法接受一个对象作为参数。到目前为止,我们一直直接传递 JSON 字符串。这不是一种高效的方式,因为我们的应用程序将处理实际的对象(Contact
实例),而且没有理由手动创建它们的 JSON 表示,当 Jackson 可以自动完成时。
还有更多...
我们现在正在使用我们的representation
类来将其属性映射到我们正在生成的响应。我们还可以使用相同的类来映射我们的输入参数。例如,我们可以修改ContactResource#updateContact()
和ContactResource#createContact()
方法,使其期望一个Contact
对象作为参数,而不是显式使用其每个属性。
使用 cURL 执行 HTTP 请求
使用您的浏览器,您只能执行 GET 请求。为了有效地测试我们的应用程序,我们需要一个能够使用 POST、PUT 和 DELETE 方法执行 HTTP 请求的工具。cURL (curl.haxx.se/
)是一个命令行工具,我们可以用它更好地理解示例。您可以从curl.haxx.se/download.html
下载它,选择与您的平台兼容的包。
执行 GET 请求与 cURL 一样简单。以下示例将调用#getContact()
方法:
$ curl http://localhost:8080/contact/123
您在第二行看到的 JSON 字符串是服务器的响应。
为了执行一个用于更新联系人的 PUT 请求,我们需要使用 -X
标志后跟方法名称(即 curl -X PUT
…)。为了在请求中向服务器发送数据,在这种情况下是联系人的信息,同时使用 -d
标志以及数据。请注意,由于 #updateContact()
方法的参数映射到请求参数(使用 @FormParam
),我们需要以 URL 编码的形式发送数据。请看下面的截图:
如果我们想看到包含请求和响应头部的详细输出,可以使用 -v
(长名称 --verbose)标志。此外,如果我们需要设置请求头部的值,可以使用 -H
(长名称 --header)标志,后跟头部信息:
$ curl --header "Content-Type: application/json" http://localhost:8080/contact/1
将请求数据映射到表示形式
当前通过在 #createContact()
和 #updateContact()
方法的签名中提及每个属性(注解)来读取 Web 服务属性的方式是可以的;然而,在大量输入数据的情况下,它并不高效。想象一下,如果我们需要在 Contact
类中添加几个额外的属性。我们还需要更新方法签名,使它们变得不那么易读,最终难以管理。通常,我们更喜欢直接将请求数据映射到表示形式。为了实现这一点,我们将相应地更新相关方法,删除属性并添加一个 contact
实例。Jackson 将处理其余部分。
如何操作...
执行以下步骤以映射请求数据:
-
更新
ContactResource#createContact()
方法,用单个contact
对象替换其参数:@POST public Response createContact(Contact contact) {// store the new contact // ...return Response.created(null).build(); }
-
更新
ContactResource#updateContact()
方法,用单个contact
对象替换其参数:@PUT @Path("/{id}") public Response updateContact(@PathParam("id") int id,Contact contact) {// update the contact with the provided ID// ...return Response.ok(new Contact(id, contact.getFirstName(), contact.getLastName(), contact.getPhone())).build(); }
-
重新构建并再次运行应用程序。现在,应用程序能够处理
/contact
和/contact/{id}
端点的 HTTP POST 和 PUT 请求,请求体中包含 JSON 字符串而不是命名参数。请注意,请求的Content-Type
头部将被设置为application/json
。
它是如何工作的...
通过在处理请求的方法(即带有 Jersey 注解绑定到 URI 的方法)上声明 Contact
实例作为参数,我们强制 Jersey 解析请求体并将其反序列化(使用 Jackson)为 Contact
对象。
在上一个示例中执行的 PUT 请求现在可以通过向服务器发送 JSON 数据并设置适当的头部来执行,如下面的代码行所示:
$ curl --header "Content-Type: application/json" -X PUT -d '{"firstName": "FOO", "lastName":"BAR", "phone":"987654321"}' http://localhost:8080/contact/123
如果在 http://localhost:8080/contact
上执行 POST 请求,请求体包含 JSON 数据 {"firstName": "Alexandros", "lastName": "Dallas", "phone": "+3012345678"}
,并且 Content-Type
报头为 application/json
,那么在 #createContact()
方法中的 contact
对象将根据这些属性进行初始化,这得益于 Jackson 以及其适当的 JAX-RS 实体提供者。实体提供者是处理包含在 HTTP 请求中的有效负载并将其转换为对象的组件。这与当 resource
方法返回一个对象并将其转换为 JSON 对象时发生的转换类似。
第六章。使用数据库
我们的应用程序正在稳步增长。我们现在需要一个地方来存储我们将要管理的联系信息,以及一个高效的方式来做到这一点。我们将使用 MySQL 服务器,其安装已在本书的第一章中概述,来满足我们的数据存储需求。Dropwizard 提供了我们与之交互所需的一切。
准备数据库
是时候使用我们的应用程序实际存储和检索数据了。我们将创建应用程序与 MySQL 数据库之间的连接。
我们需要一个实际的数据库来连接和查询。由于我们已经安装了 MySQL,我们也可以使用 mysql
命令行客户端来创建数据库并在其中创建一些表。
准备工作
通过在终端中执行以下命令来启动 mysql
客户端:
$ mysql -u root -p
如以下截图所示,MySQL 壳将提示您提供密码,这是您在安装 MySQL 时设置的 MySQL root 用户的密码:
如何操作...
让我们按照以下步骤准备我们应用程序的数据库:
-
通过运行以下查询来创建数据库 phonebook:
> CREATE DATABASE `phonebook`;
-
我们需要一个额外的 MySQL 用户,具有对新创建的数据库的完全访问权限。使用以下命令创建用户并授予适当的访问权限:
> CREATE USER 'phonebookuser'@'localhost' IDENTIFIED BY'phonebookpassword'; > GRANT ALL ON phonebook.* TO 'phonebookuser'@'localhost';
-
使用
USE
命令选择phonebook
数据库:> USE `phonebook`;
-
创建一个联系表来存储一些联系信息。
> CREATE TABLE IF NOT EXISTS `contact` (`id` int(11) NOT NULL AUTO_INCREMENT,`firstName` varchar(255) NOT NULL,`lastName` varchar(255) NOT NULL,`phone` varchar(30) NOT NULL,PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
-
在联系表中添加一些测试数据:
> INSERT INTO `contact` VALUES (NUL L, 'John', 'Doe', '+123456789'), (NULL, 'Jane', 'Doe', '+987654321');
它是如何工作的...
我们刚刚设置了数据库。通过我们运行的查询,我们创建了一个数据库、一个数据库用户以及一个用于存储联系信息的表。我们的应用程序将更新以存储和检索此表中的信息。
与数据库交互
现在我们已经有了数据库和数据。然而,为了能够连接到数据库,我们需要在项目中包含 mysql jdbc
连接器。此外,我们还需要 dropwizard-jdbi
模块,它将允许我们创建数据库连接和 数据访问对象 (DAO),通过它我们将查询数据库,利用 JDBI 项目提供的 API (jdbi.org/
)。
准备工作
让我们看看为了实现这一点需要什么。首先,在 pom.xml
的 <dependencies>
部分中添加以下依赖项:
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency><dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-jdbi</artifactId><version>0.7.0-SNAPSHOT</version></dependency>
我们现在已准备好继续并更新我们的应用程序。我们将使用 JDBI 的 SQL 对象 API 映射方法来预定义 SQL 语句。
如何操作...
让我们看看如何通过以下步骤通过我们的应用程序连接和交互数据库:
-
创建一个新的包,
com.dwbook.phonebook.dao
,并在其中创建一个ContactDAO
接口,代码如下:package com.dwbook.phonebook.dao;public interface ContactDAO { }
-
添加
#getContactById()
方法,这将允许我们查询数据库,并在给定 ID 时检索联系人列表或特定联系人。使用@SqlQuery
注解指定当方法被调用时将执行的 SQL 查询。你需要导入org.skife.jdbi.v2.sqlobject.*
和com.dwbook.phonebook.representations.Contact
。@SqlQuery("select * from contact where id = :id")Contact getContactById(@Bind("id") int id);
-
创建一个
com.dwbook.phonebook.dao.mappers
包和ContactMapper
类,该类实现了映射方法,如下面的代码片段所示。映射器类简化了将resultset
数据库行映射到对象的过程。你需要导入java.sql.ResultSet
、java.sql.SQLException
、org.skife.jdbi.v2.StatementContext
、org.skife.jdbi.v2.tweak.ResultSetMapper
和com.dwbook.phonebook.representations.Contact
。public class ContactMapper implements ResultSetMapper<Contact> {public Contact map(int index, ResultSet r,StatementContext ctx)throws SQLException {return new Contact(r.getInt("id"), r.getString("firstName"),r.getString("lastName"),r.getString("phone"));}}
-
在
ContactDAO
中,通过添加@Mapper
注解到#getContactById()
方法上(在@SqlQuery
注解之前),注册你的映射器。导入com.dwbook.phonebook.dao.mappers.ContactMapper
和org.skife.jdbi.v2.sqlobject.customizers.Mapper
类。@Mapper(ContactMapper.class)@SqlQuery("select * from contact where id = :id")Contact getContactById(@Bind("id") int id);
-
在
config.yaml
配置文件中,添加数据库部分,包含建立数据库连接所需的最小属性集(根据YAML
语法缩进)。database:driverClass: com.mysql.jdbc.Driveruser: phonebookuserpassword: phonebookpasswordurl: jdbc:mysql://localhost/phonebook
-
在
PhonebookConfiguration
类中添加数据库属性,并为其创建一个 getter 方法。首先导入io.dropwizard.db.DataSourceFactory
类。@JsonPropertyprivate DataSourceFactory database = new DataSourceFactory();public DataSourceFactory getDataSourceFactory() {return database;}
-
修改
App
类中的run
方法,以创建一个DBIFactory
类,该类将用于构建DBI
实例,然后我们将将其作为参数传递给ContactResource
。你需要导入org.skife.jdbi.v2.DBI
和io.dropwizard.jdbi.DBIFactory
。@Overridepublic void run(PhonebookConfiguration c, Environment e)throws Exception {LOGGER.info("Method App#run() called");for (int i=0; i < c.getMessageRepetitions(); i++) {System.out.println(c.getMessage());}System.out.println(c.getAdditionalMessage());// Create a DBI factory and build a JDBI instancefinal DBIFactory factory = new DBIFactory();final DBI jdbi = factory.build(e, c.getDataSourceFactory(), "mysql");// Add the resource to the environmente.jersey().register(new ContactResource(jdbi));}
-
在上一步中,我们将
jdbi
实例作为参数传递给ContactResource
构造函数。然而,构造函数ContactResource(DBI)
(目前)不存在,因此我们需要创建它。我们将在资源类中添加一个私有的final ContactDAO
成员,使用onDemand
方法,并使用 JDBI 来实例化它。你还需要添加DBI
和ContactDAO
的必要导入。private final ContactDAO contactDao;public ContactResource(DBI jdbi) {contactDao = jdbi.onDemand(ContactDAO.class);}
-
使用
contactDao
对象修改ContactResource#getContact()
方法类,使其从数据库返回实际的联系人。@GET@Path("/{id}")public Response getContact(@PathParam("id") int id) {// retrieve information about the contact with theprovided idContact contact = contactDao.getContactById(id);return Response.ok(contact).build();}
-
重新构建并运行应用程序,提供更新的配置文件作为参数。
-
打开你的浏览器并转到
http://localho``st:8080/contact/1
。你会看到我们插入到联系人表中的第一行的 JSON 表示,即id
等于1
的John Doe
。看看下面的截图,它概述了这一点:分别,以下截图显示了
http://localhost:8080/contact/2
的输出: -
现在,让我们在我们的 DAO 中添加创建、更新和删除联系人的方法。对于插入新条目,添加
#createContact()
方法。@GetGeneratedKeys@SqlUpdate("insert into contact (id, firstName, lastName, phone) values (NULL, :firstName, :lastName, :phone)")int createContact(@Bind("firstName") String firstName, @Bind("lastName") String lastName, @Bind("phone") String phone);
注意
注意,由于我们正在更新数据库而不是查询它(即检索信息),我们使用
@SqlUpdate
注解而不是在#getContact()
方法中使用的@SqlQuery
注解来为 SQL 查询进行注解。此外,使用@GetGeneratedKeys
注解来检索新插入行的主键值;在这种情况下,id
字段的值。 -
为了更新现有条目,添加
#updateContact()
方法:@SqlUpdate("update contact set firstName = :firstName, lastName = :lastName, phone = :phone where id = :id") void updateContact(@Bind("id") int id, @Bind("firstName") String firstName, @Bind("lastName") String lastName,@Bind("phone") String phone);
-
为了删除现有条目,添加
#deleteContact()
方法:@SqlUpdate("delete from contact where id = :id") void deleteContact(@Bind("id") int id);
-
现在我们已经设置了数据库方法,让我们在
Resource
类中使用它们,以便实际上可以插入、更新和删除联系人。修改ContactResource#createContact()
方法,以便在数据库中插入新的联系人,检索其id
,并使用它来构造其 URI,将其作为参数传递给Response#created()
方法。为此,我们首先需要导入java.net.URI
和java.net.URISyntaxException
:@POSTpublic Response createContact(Contact contact) throws URISyntaxException {// store the new contactint newContactId = contactDao.createContact(contact.getFirstName(), contact.getLastName(), contact.getPhone());return Response.created(new URI(String.valueOf(newContactId))).build();}
-
以类似的方式,更新
ContactResource#deleteContact()
方法,以便确实可以删除联系人:@DELETE@Path("/{id}")public Response deleteContact(@PathParam("id") int id) {// delete the contact with the provided idcontactDao.deleteContact(id);return Response.noContent().build();}
-
最后,让我们也更新
ContactResource#updateContact()
方法,以便我们的应用程序可以在处理相关 HTTP 请求的同时更新现有联系人:@PUT@Path("/{id}")public Response updateContact(@PathParam("id") int id, Contact contact) {// update the contact with the provided IDcontactDao.updateContact(id, contact.getFirstName(),contact.getLastName(), contact.getPhone());return Response.ok(new Contact(id, contact.getFirstName(), contact.getLastName(),contact.getPhone())).build();}
它是如何工作的…
多亏了 JDBI,我们的电话簿应用程序现在可以与数据库交互,检索、存储、更新和删除联系人。
通过执行带有curl
的 HTTP POST 请求来创建一个新的联系人:
$ curl --verbose --header "Content-Type: application/json" -X POST -d '{"firstName": "FOO", "lastName":"BAR
", "phone":"987654321"}'
http://localhost:8080/contact/
联系人被创建,插入行的主键值,即联系人id
,为174
,如以下截图所示(Location
响应头):
JDBI 的 SQL 对象 API 简化了DAO
的创建。我们在上面创建了DAO
接口,我们可以使用@SqlQuery
注解将普通的、参数化的 SQL 查询映射到特定的方法;请注意,除了对象映射器之外,不需要额外的实现。
由于我们从数据库中检索数据并返回一个Contact
实例,我们需要创建一个Mapper
类,这是一个实现了org.skife.jdbi.v2.tweak.ResultSetMapper<T>
接口的Contact
类的类。它的实现相当简单直接。我们使用#getLong()
和#getString()
方法以及列名从数据库ResultSet
对象中获取的值创建了一个Contact
对象。
我们使用jdbi
在资源类中使用DBI#onDemand()
方法创建我们的DAO
实例。然而,为了做到这一点,我们必须创建一个DBI
工厂并在注册资源之前构建DBI
实例。再次强调,这很简单,只需要在App#run()
方法中进行一些小的修改。
DBI
工厂需要数据库连接设置来构建DBI
实例。退一步说,我们的配置类已经更新,以读取和公开DatabaseConfiguration
设置,这些设置在应用程序配置文件的数据库部分声明,即config.yaml
。
还有更多...
JDBI 将自己标识为 Java 的 SQL 便捷库。我们使用了 JDBI SQL 对象 API,其中特定方法映射到特定的 SQL 语句。然而,这并不是使用 JDBI 与数据库交互的唯一方式。JDBI 还公开了另一个 API,即流畅风格 API。
JDBI 流畅风格 API
流畅风格 API 允许我们打开并使用数据库句柄,在需要时即时创建和执行 SQL 查询,而不是使用 SQL 对象 API 使用的预定义 SQL 语句。
通常,你应该使用的 API 类型取决于你的个人喜好,你甚至可以将两种 API 混合使用。
@MapResultAsBean
注解
在这个例子中,我们实现了一个映射器,并使用@Mapper
注解将 SQL 查询的结果映射到Contact
实例。另一种方法可以使用MapResultAsBean
注解。
@MapResultAsBean
@SqlQuery("select * from contact where id = :id")
Contact getContactById(@Bind("id") int id);
在这个例子中,通过注解#getContactById()
,我们将 SQL 查询的结果直接映射到Contact
实例,而不需要实现自定义映射器。不过,为了使这可行,Contact
类应该更新为设置器(即setFirstName(String firstName){ .. }
)。因此,必须从每个成员变量的声明中移除final
关键字。
第七章:验证 Web 服务请求
到目前为止,我们有一个生成 JSON 表示的 RESTful Web 服务,并且还能够存储和更新联系人信息。但在实际存储或更新联系人信息之前,我们需要确保提供的信息是有效且一致的。
添加验证约束
为了验证联系人,我们首先需要定义什么是有效的联系人。为此,我们将修改表示类,通过 Hibernate Validator 注解的形式为其成员添加约束。
如何做到这一点...
我们有Contact
类,其实例必须有一个名字、一个姓氏和一个电话号码才能被认为是有效的。此外,这些值的长度必须在特定的限制范围内。让我们按顺序逐步了解应用这些约束的必要步骤。
修改Contact
表示类,为其成员添加适当的注解(首先导入org.hibernate.validator.constraints.*
):
-
更新
firstName
变量的声明,添加必要的注解以指示这是一个必填属性(不应为空),其长度应在 2 到 255 个字符之间。@NotBlank @Length(min=2, max=255) private final String firstName;
-
以类似的方式,对
lastName
属性应用相同的约束。@NotBlank @Length(min=2, max=255) private final String lastName;
-
phone
字段不应超过 30 位数字,因此相应地修改相关注解的值。@NotBlank @Length(min=2, max=30) private final String phone;
它是如何工作的...
验证约束的声明是基于注解的。这使我们能够直接将我们想要的验证规则添加到表示类的成员中。
Hibernate Validator 是dropwizard-core
模块的一部分,因此我们不需要在pom.xml
中声明任何额外的依赖。
还有更多...
验证对象的标准方法是使用标准的Bean Validation API(JSR 303)。对于我们的验证需求,我们使用Hibernate Validator,它是Dropwizard-core
模块的一部分,也是 JSR 303 的参考实现。使用 Hibernate Validator,我们可以声明字段约束,如@NotBlank
和@Length
,甚至创建和使用我们自己的自定义约束以满足我们的需求(您可以参考 Hibernate Validator 的文档,链接为docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints
)。
约束注解列表
字段约束的完整列表可在 Hibernate Validator 包导航器中找到,链接为docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints
。
执行验证
我们刚刚定义了什么是有效的注解。现在,我们必须修改我们的资源类的代码,以验证每个 POST 和 PUT 请求是否包含一个有效的Contact
对象,基于此创建或更新联系信息。
如何操作…
让我们通过以下步骤查看我们的资源类需要修改什么:
-
首先,我们需要导入一些将帮助我们进行验证的类。
import java.util.Set; import javax.validation.ConstraintViolation; import javax.util.ArrayList; import javax.validation.Validator; import javax.ws.rs.core.Response.Status;
-
添加一个最终成员
validator
,并更新构造方法以初始化它。private final ContactDAO contactDao; private final Validator validator;public ContactResource(DBI jdbi, Validator validator) {contactDao = jdbi.onDemand(ContactDAO.class); this.validator = validator;}
-
在
App
类中,修改#run()
方法,以便在初始化ContactResource
时将环境的validator
作为参数传递,同时包含 jDBI。// … // Add the resource to the environmente.jersey().register(new ContactResource(jdbi, e.getValidator()));// …
-
更新
ContactResource#createContact()
方法,并在将其插入数据库之前检查联系信息是否有效。@POSTpublic Response createContact(Contact contact) throws URISyntaxException {// Validate the contact's dataSet<ConstraintViolation<Contact>> violations = validator.validate(contact);// Are there any constraint violations?if (violations.size() > 0) {// Validation errors occurredArrayList<String> validationMessages = new ArrayList<String>();for (ConstraintViolation<Contact> violation : violations) { validationMessages.add(violation.getPropertyPath().toString() +": " + violation.getMessage());}return Response.status(Status.BAD_REQUEST).entity(validationMessages).build();}else {// OK, no validation errors// Store the new contactint newContactId = contactDao.createContact(contact.getFirstName(),contact.getLastName(), contact.getPhone());return Response.created(new URI(String.valueOf(newContactId))).build();}}
-
类似地,更新
ContactResource#updateContact()
方法。@PUT@Path("/{id}")public Response updateContact(@PathParam("id") int id, Contact contact) {// Validate the updated dataSet<ConstraintViolation<Contact>> violations = validator.validate(contact);// Are there any constraint violations?if (violations.size() > 0) {// Validation errors occurredArrayList<String> validationMessages = new ArrayList<String>();for (ConstraintViolation<Contact> violation : violations) { validationMessages.add(violation.getPropertyPath().toString() +": " + violation.getMessage());}return Response.status(Status.BAD_REQUEST).entity(validationMessages).build();}else {// No errors// update the contact with the provided IDcontactDao.updateContact(id, contact.getFirstName(),contact.getLastName(), contact.getPhone());return Response.ok(new Contact(id, contact.getFirstName(), contact.getLastName(),contact.getPhone())).build();}}
-
从命令行构建并运行应用程序,以便对我们刚刚实现的验证机制进行一些测试。
-
使用
curl
,向http://localhost:8080/contact/
执行一个 HTTP POST 请求,发送将触发验证错误的联系信息,例如长度小于 2 个字符的firstName
和lastName
,以及一个空值的phone
字段,如下面的 JSON 字符串所示:{"firstName": "F", "lastName": "L", "phone": ""}. #> curl -v -X POST -d '{"firstName": "F", "lastName": "L", "phone": ""}' http://localhost:8080/contact/ --header "Content-Type: application/json"
你将看到响应是一个HTTP/1.1 400 错误请求错误,响应负载是一个包含以下错误消息的 JSON 数组:
< HTTP/1.1 400 Bad Request< Date: Tue, 28 Jan 2014 20:16:57 GMT< Content-Type: application/json< Transfer-Encoding: chunked< * Connection #0 to host localhost left intact* Closing connection #0["phone: length must be between 2 and 30","firstName: length must be between 2 and 255","lastName: length must be between 2 and 255","phone: may not be empty"]
工作原理…
在映射到/contact
URI 的 POST 请求的ContactResource#createContact()
方法中,我们使用了环境的javax.validation.Validator
实例来验证接收到的contact
对象。
验证器的#validate()
方法返回一个Set<ConstraintViolation<Contact>>
实例,其中包含发生的验证错误(如果有的话)。我们检查列表的大小以确定是否存在任何违规行为。如果有,我们将遍历它们,提取每个错误的验证消息,并将其添加到一个ArrayList
实例中,然后我们将作为响应返回这个实例,并附带HTTP 状态码 400 – 错误请求。
由于我们的资源类生成 JSON 输出(已在类级别上通过@Produces
注解声明),ArrayList
实例将因 Jackson 而转换为 JSON 数组。
更多内容…
如你所见,为了测试和展示我们创建的端点的 POST 请求,我们需要一个 HTTP 客户端。除了 cURL 之外,还有一些非常好的、有用的 HTTP 客户端工具可用(例如适用于 Google Chrome 的 Postman,可在chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm
),这些工具可以帮助我们,我们将在下一章中创建自己的工具。
@Valid
注解
我们可以用在 #createContact
方法上注解 contact
对象为 @Valid
来代替使用 validator
对象验证输入对象,如下代码所示:
public Response createContact(@Valid Contact contact)
当一个对象被注解为 @Valid
时,验证会递归地在该对象上执行。这将导致在方法被调用时立即触发验证。如果发现 contact
对象无效,则将自动生成默认的 HTTP 422 – Unprocessable entity 响应。虽然 validator
对象更强大且可定制,但使用 @Valid
注解是一种替代的简单直接的方式来验证传入的请求。这避免了需要向调用者返回一个更详细的自定义验证错误信息,而是发送一个通用的错误信息。
跨字段验证
有时候需要在对象的多个字段(属性)上执行验证。我们可以通过实现自定义验证注解并应用类级别约束来实现这一点。
幸运的是,有一个更简单的方法可以实现这一点。Dropwizard 提供了 io.dropwizard.validation.ValidationMethod
注解,我们可以在表示类的 boolean
方法中使用它。
如何实现…
以下是向 contact
对象添加跨字段验证所需的步骤。我们将检查联系人的全名不是 John Doe:
-
在
Contact
类中添加一个名为#isValidPerson()
的新方法。public boolean isValidPerson() {if (firstName.equals("John") && lastName.equals("Doe")) {return false;}else {return true;} }
-
然后,我们需要确保当这个方法被 Jackson 序列化时,其输出永远不会包含在输出中。为此,使用
@JsonIgnore
注解 (com.fasterxml.jackson.annotation.JsonIgnore
) 注解#isValidPerson()
方法。 -
最后,使用
@ValidationMethod
(io.dropwizard.validation.ValidationMethod
) 注解相同的验证方法,并在验证失败的情况下提供错误信息。@ValidationMethod(message="John Doe is not a valid person!")
它是如何工作的…
当触发验证时,#isValidPerson()
方法将与我们放置的自定义验证代码一起执行。如果方法返回 true,则表示隐含的约束得到满足。如果方法返回 false,则表示违反了约束,验证错误信息将是与 ValidationMethod
注解一起指定的那个。
你可以在类中创建任意数量的跨字段验证方法。然而,请注意,每个自定义验证方法都必须返回 boolean
类型,并且其名称必须以 is
开头。
第八章. 网络服务客户端
我们的服务已经准备好了并且功能正常,但我们需要一个接口来实际使用它。当然,通过使用网络浏览器,我们能够执行 HTTP GET 请求,但不能执行更复杂的请求,如 POST。我们需要创建一个 HTTP 客户端来处理这些请求。
此外,在许多情况下,你可能需要让你的网络服务调用其他网络服务,然后在将信息返回给调用者之前执行额外的处理。
为我们的应用程序构建客户端
Dropwizard 包含了 Jersey 和 Apache HTTP 客户端。我们将使用 Jersey 客户端来创建我们的网络服务客户端。
准备工作
将dropwizard-client
模块添加到你的pom.xml
文件的依赖项部分,以便为我们的项目添加网络服务客户端支持:
<dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-client</artifactId><version>0.7.0-SNAPSHOT</version></dependency>
如何操作...
我们将创建一个新的资源类,它将监听并接受来自我们的网络浏览器的 HTTP GET 请求,然后调用Contact
资源的适当方法并以人类友好的格式呈现响应。让我们看看实现这一目标所需的步骤:
-
在
com.dwbook.phonebook.resources
包中创建ClientResource
类。类似于ContactResource
类,我们首先应该导入所需的javax.ws.rs
注解、我们将要使用的表示类,以及如以下代码片段所示所需的 Jersey 客户端类。package com.dwbook.phonebook.resources;import javax.ws.rs.*;import javax.ws.rs.core.*;import com.dwbook.phonebook.representations.Contact;import com.sun.jersey.api.client.*;public class ClientResource { }
-
将客户端资源类的上下文路径设置为
/client/
,通过在新建的类中添加适当的注解来逻辑上分离客户端和服务的 URI。@Path("/client/") public class ClientResource { }
-
由于我们的客户端将被人类使用,我们需要一个对人类友好的响应类型,例如
text/plain
,因此我们将使用MediaType.TEXT_PLAIN
。通过在我们的类中添加@Produces
注解来定义它。@Produces(MediaType.TEXT_PLAIN)@Path("/client/")public class ClientResource { }
-
为了对其他网络服务(在这种情况下,我们的服务,
ContactResource
类)进行调用,我们需要在我们的资源类中有一个Client
实例作为成员。这将在初始化期间提供,因此我们需要一个适当的构造函数。private Client client;public ClientResource(Client client) {this.client = client;}
-
在我们的应用程序的入口类中实例化客户端,并通过在
App#run()
方法中添加几行代码将新的资源添加到环境中。当然,我们首先需要导入com.sun.jersey.api.client.Client
、io.dropwizard.client.JerseyClientBuilder
以及我们刚刚创建的com.dwbook.phonebook.resources.ClientResource
类。// build the client and add the resource to the environmentfinal Client client = new JerseyClientBuilder(e).build("REST Client");e.jersey().register(new ClientResource(client));
它是如何工作的...
现在我们已经有了准备好的客户端资源。这个资源包含一个 Jersey Client
对象作为成员,我们可以使用它通过构建WebResource
对象(使用Client#resource()
方法)并在它们之间进行交互来对特定的 URL 执行 HTTP 请求。
还有更多...
大多数时候,尤其是在大型应用中,客户端与后端服务是解耦的,形成一个独立的应用。后端服务通常执行更复杂和密集的任务,通常将它们独立于客户端进行管理和扩展是好的实践。
与我们的服务交互
我们将继续添加必要的 ClientResource
类方法,绑定到 GET 请求,以便它们可以很容易地通过浏览器触发。我们需要添加创建、更新、删除和检索联系人的方法,我们将通过执行适当的 HTTP 请求来触发它们。
如何做到这一点...
-
将
#showContact()
方法添加到ClientResource
类中,使用@QueryParam
注解将查询String
参数id
绑定为输入。@GET@Path("showContact")public String showContact(@QueryParam("id") int id) {WebResource contactResource = client.resource("http://localhost:8080/contact/"+id);Contact c = contactResource.get(Contact.class);String output = "ID: "+ id +"\nFirst name: " + c.getFirstName() + "\nLast name: " + c.getLastName() + "\nPhone: " + c.getPhone();return output;}
-
创建
#newContact()
方法。此方法将接受Contact
对象的属性作为参数,并通过对ContactResource
服务执行适当的 HTTP 请求来创建一个新的联系人。@GET@Path("newContact")public Response newContact(@QueryParam("firstName") String firstName, @QueryParam("lastName") String lastName, @QueryParam("phone") String phone) {WebResource contactResource = client.resource("http://localhost:8080/contact");ClientResponse response = contactResource.type(MediaType.APPLICATION_JSON).post(ClientResponse.class, new Contact(0, firstName, lastName, phone));if (response.getStatus() == 201) {// Createdreturn Response.status(302).entity("The contact was created successfully! The new contact can be found at " + response.getHeaders().getFirst("Location")).build();}else {// Other Status code, indicates an errorreturn Response.status(422).entity(response.getEntity(String.class)).build();}}
-
更新联系人的
#updateContact()
方法将与之前的类似。@GET@Path("updateContact")public Response updateContact(@QueryParam("id") int id, @QueryParam("firstName") String firstName, @QueryParam("lastName") String lastName, @QueryParam("phone") String phone) {WebResource contactResource = client.resource("http://localhost:8080/contact/" + id);ClientResponse response = contactResource.type(MediaType.APPLICATION_JSON).put(ClientResponse.class, new Contact(id, firstName, lastName, phone));if (response.getStatus() == 200) {// Createdreturn Response.status(302).entity("The contact was updated successfully!").build();}else {// Other Status code, indicates an errorreturn Response.status(422).entity(response.getEntity(String.class)).build();}}
-
以类似的方式,让我们添加删除联系人的方法,
#deleteContact()
。@GET@Path("deleteContact")public Response deleteContact(@QueryParam("id") int id) {WebResource contactResource = client.resource("http://localhost:8080/contact/"+id);contactResource.delete();return Response.noContent().entity("Contact was deleted!").build();}
-
现在,你可以构建并运行应用程序,以查看我们到目前为止所做的工作。
如何工作…
将你的浏览器指向 http://localhost:8080/client/showContact?id=1
。客户端将对 http://localhost:8080/contact/1
执行 HTTP GET 请求,解析联系人的 JSON 表示,并生成其纯文本摘要。
为了执行 HTTP 请求,我们必须首先使用客户端的 #resource(String)
方法创建一个 WebResource
实例(因为 RESTful Web 服务都是关于资源和 HTTP 动词)。将 WebResource
视为特定 Web 服务端点的代理。
WebResource
类的 #get()
方法接受我们将用于解析和映射响应的类作为参数,这也会是其返回类型。
对于 HTTP POST 请求,我们使用通用的 HTTP 响应类 ClientResponse
,我们可以使用 #getStatus()
方法提取响应的状态码。此外,我们还可以使用 #getHeaders()
方法提取其头信息。
注意,对于 POST 和 PUT 请求,我们也在设置请求数据的媒体类型(WebResource#type()
)。
如果你将你的网络浏览器指向 http://localhost:8080/client/newContact?firstName=Jane&lastName=Doe&phone=98765432
,我们的客户端会将这些数据发送到 ClientResource
,这将创建一个新的联系人并返回其位置给客户端。然后客户端会显示如下截图中的新联系人的 URL:
同样,我们可以通过客户端请求适当的 URL 来更新联系。URL http://localhost:8080/client/updateContact?id=1&firstName=Alex&lastName=Updated&phone=3210465
将触发对联系服务的 PUT 请求,最终更新 id
等于 1 的联系。
如您可能已经猜到的,URL http://localhost:8080/client/deleteContact?id=1
将发送相关的 HTTP DELETE 请求到联系服务,删除由给定 id
标识的联系。
还有更多…
注意,在创建新联系时出现验证错误的情况下,这些错误会通知客户端。我们的客户端检查 POST 请求的状态码,如果它不等于 201
(表示实体已被创建),则将响应解析为字符串并展示给用户。
例如,导航到 http://localhost:8080/client/newContact?firstName=J&lastName=D&phone=9
。由于我们已经设置了约束,指出 firstName
、lastName
和 phone
的长度应大于 2,因此您将看到以下截图中的验证错误:
第九章. 认证
认证是验证访问应用程序的用户确实是他/她所声称的那个人,并且他/她被允许访问和使用我们的应用程序的过程。在本章中,我们将了解如何使用认证机制来保护我们的网络服务。
构建基本的 HTTP 认证器
我们的网络服务现在具有允许任何人使用 HTTP 客户端创建和检索联系人的功能。我们需要以某种方式保护我们的网络服务并认证调用它的用户。最常用的认证方式是基本的 HTTP 认证,它需要一个基本的凭证集:用户名和密码。
准备工作
在我们继续保护我们的网络服务之前,我们需要将dropwizard-auth
依赖项添加到我们的项目中,将以下内容添加到我们的pom.xml
文件的依赖项部分:
<dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-auth</artifactId><version>0.7.0-SNAPSHOT</version>
</dependency>
如何操作…
让我们看看构建认证机制并保护我们的方法需要什么;执行以下步骤:
-
在
com.dwbook.phonebook
包中创建一个名为PhonebookAuthenticator
的新类;在这里,我们将构建我们服务的安全机制。该类需要实现Authenticator<C, P>
接口及其#authenticate()
方法。认证器的第一个参数是Authentication
方法,而第二个参数是#authenticate()
方法的返回类型。package com.dwbook.phonebook;import com.google.common.base.Optional;import io.dropwizard.auth.AuthenticationException;import io.dropwizard.auth.Authenticator;import io.dropwizard.auth.basic.BasicCredentials;public class PhonebookAuthenticator implementsAuthenticator<BasicCredentials, Boolean> {public Optional<Boolean> authenticate(BasicCredentials c) throws AuthenticationException {if (c.getUsername().equals("john_doe") && c.getPassword().equals("secret")) {return Optional.of(true);}return Optional.absent();}}
-
通过将您刚刚构建的认证器添加到 Dropwizard 环境中,并使用
JerseyEnvironment#register()
方法传递一个BasicAuthProvider
实例来启用您刚刚构建的认证器。BasicAuthProvider
的构造函数接受一个用于输入的认证器实例和认证域。您还需要导入io.dropwizard.auth.basic.BasicAuthProvider
。// Register the authenticator with the environmente.jersey().register(new BasicAuthProvider<Boolean>(new PhonebookAuthenticator(), "Web Service Realm"));
-
现在,您可以通过修改
ContactResource
类方法的声明,期望一个带有@Auth
注解(导入io.dropwizard.auth.Auth
)的Boolean
变量作为参数来保护网络服务端点。包含此注解参数将触发认证过程。public Response getContact(@PathParam("id") int id, @Auth Boolean isAuthenticated) { … }public Response createContact(Contact contact, @Auth Boolean isAuthenticated) throws URISyntaxException { … }public Response deleteContact(@PathParam("id") int id, @Auth Boolean isAuthenticated) { … } public Response updateContact(@PathParam("id") int id, Contact contact, @Auth Boolean isAuthenticated) { … }
-
构建并启动应用程序,然后尝试访问
ContactResource
类的任何端点,例如http://localhost:8080/contact/1
,尝试显示 ID 等于 1 的联系人。您将看到一个消息,表明服务器需要用户名和密码。
它是如何工作的…
dropwizard-auth
模块包含了我们保护服务所需的一切。我们只需要实现一个认证器并将其注册到 Dropwizard 环境中。
然后,当我们为一个方法的输入参数使用@Auth
注解时,我们表示访问我们服务的用户必须经过认证。每次对包含@Auth
注解的变量的方法执行 HTTP 请求时,认证提供者都会拦截它,请求用户名和密码。然后,这些凭证被传递给我们的认证器,认证器负责确定它们是否有效。无论认证结果如何,即#authenticate()
方法的返回值,它都会注入到被@Auth
注解的变量中。如果认证失败或没有提供凭证,请求将被阻止,响应是一个HTTP/1.1 401 Unauthorized错误。您可以在以下屏幕截图中看到执行 HTTP 请求后收到的响应,请求使用 cURL 执行,但没有提供凭证:
我们的认证器类需要是一个实现了Authenticator<C, P>
接口的类,其中C
是我们可能用来认证用户的凭证集合,而P
是认证结果类型。在我们的例子中,我们使用了BasicCredentials
作为凭证存储,这是BasicAuthProvider
提供的。在#authenticate()
方法中,我们执行所有必要的任务以认证用户。我们实现了检查用户名是否为john_doe
,这是通过密码secret
识别的。这是一个例子;下一个菜谱将说明如何认证用户,当他们的详细信息(用户名和密码)存储在数据库中时。
还有更多…
如您可能已经注意到的,我们的认证器的#authenticate()
方法的返回类型是Optional
。这是一个 Guava 类型,它允许我们防止空指针异常。在某些情况下,#authenticate()
方法应该返回空值,因此我们返回Optional.absent()
,而不是简单地返回 null(如果处理不当可能会引起问题)。
这样的情况是我们需要向我们要保护的方法提供一个经过认证的主体的实例(可能包含用户名、姓名、电子邮件等),而不是像在这个例子中那样只是一个boolean
参数。
设置客户端的凭证
我们已经保护了我们的网络服务,特别是ContactResource
类的端点。我们的客户端也需要更新,以便能够访问这些受保护的资源。
要做到这一点,我们需要修改App#run()
方法。在client
对象实例化后,使用#addFilter()
方法,添加HTTPBasicAuthFilter
(导入com.sun.jersey.api.client.filter.HTTPBasicAuthFilter
),并提供正确的用户名和密码。
final Client client = new JerseyClientBuilder().using(environment).build();client.addFilter(new HTTPBasicAuthFilter("john_doe", "secret"));
#addFilter()
方法用于向 client
对象添加额外的处理指令。也就是说,我们 Jersey 客户端执行的每一个请求都必须在我们添加的过滤器处理之后才能最终执行。在这种情况下,我们使用 #addFilter()
方法来为每个发出的 HTTP 请求添加适当的 BasicAuth
头部。
可选认证
有许多情况下认证应该是可选的。想想一个为用户提供个性化信息的服务,当没有用户登录时返回默认消息。为了声明可选认证,我们应该在 @Auth
注解中提供 required=false
参数,如下面的代码所示:
@Auth(required=false)
认证方案
我们在应用程序中使用了基本的 HTTP 认证;然而,这并不是唯一可用的认证方案。例如,一些 Web 服务使用 API 密钥认证。在这种情况下,认证器应该检查 HTTP 请求的头部,验证传输的 API 密钥的有效性。然而,这样做将需要使用自定义认证提供者。无论如何,使用哪种认证方法取决于您的应用程序需求。
使用数据库中存储的凭据验证用户
在前面的菜谱中,我们使用一组硬编码的用户名和密码来验证用户的身份。然而,在大多数现实世界的案例中,您需要使用存储在数据库中,或者更具体地说,在包含用户信息的表中存储的凭据来识别用户并验证他们的身份。
准备工作
让我们先在数据库中创建一个表,用于存储用户数据。
启动 MySQL 客户端,登录后,在电话簿数据库中执行以下查询:
CREATE TABLE IF NOT EXISTS `users` (`username` varchar(20) NOT NULL,`password` varchar(255) NOT NULL,PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
现在让我们通过运行以下查询将用户添加到数据库中:
INSERT INTO `users` VALUES ('wsuser', 'wspassword');
如何操作…
我们将修改我们的认证提供者,以便在数据库中检查当前用户的凭据。让我们看看如何:
-
由于我们将与数据库交互以验证用户,我们需要一个 DAO。因此,在
com.dwbook.phonebook.dao
包中创建UserDAO
接口。package com.dwbook.phonebook.dao;import org.skife.jdbi.v2.sqlobject.*;public interface UserDAO {@SqlQuery("select count(*) from users where username = :username and password = :password")int getUser(@Bind("username") String username, @Bind("password") String password);}
-
修改
PhonebookAuthenticator
,添加一个UserDAO
实例作为成员变量,创建一个构造函数使用jdbi
初始化 DAO 实例,并最终通过利用UserDAO
实例通过查询数据库来验证用户数据来修改认证方法。import org.skife.jdbi.v2.DBI;import com.dwbook.phonebook.dao.UserDAO;import com.google.common.base.Optional;import io.dropwizard.auth.AuthenticationException;import io.dropwizard.auth.Authenticator;import io.dropwizard.auth.basic.BasicCredentials;public class PhonebookAuthenticator implements Authenticator<BasicCredentials, Boolean> {private final UserDAO userDao;public PhonebookAuthenticator(DBI jdbi) {userDao = jdbi.onDemand(UserDAO.class);}public Optional<Boolean> authenticate(BasicCredentials c) throws AuthenticationException {boolean validUser = (userDao.getUser(c.getUsername(), c.getPassword()) == 1);if (validUser) {return Optional.of(true);}return Optional.absent();}}
-
在
App#run()
方法中,修改我们的认证器的注册,以便将其现有的jdbi
实例传递给其构造函数。// Register the authenticator with the environment e.jersey().register(new BasicAuthProvider<Boolean>(new PhonebookAuthenticator(jdbi), "Web Service Realm"));
您现在可以重新构建、运行并测试应用程序。这次,当请求时,您需要提供存储在数据库中的用户名和密码设置,而不是硬编码的。
它是如何工作的…
在对受保护资源执行每个请求时,我们的应用程序都会将用户的凭据与数据库进行比对。为此,我们创建了一个简单的 DAO,它只有一个查询,实际上会计算与提供的用户名和密码匹配的行数。当然,这可能是 0(当用户名/密码集不正确时)或 1(当提供了正确的凭据集时)。这就是我们在认证器的#authenticate()
方法中检查的内容。
更多内容...
在这个菜谱中,我们将密码以纯文本形式存储在数据库中。这通常不是正确的做法;密码应该始终加密或散列,而不是以明文形式存储,以最大限度地减少可能入侵或未经授权访问的影响。
缓存
为了提高我们应用程序的性能,我们可以缓存数据库凭据。Dropwizard 提供了CachingAuthenticator
类,我们可以用它来处理这个问题。这个概念很简单;我们使用CachingAuthenticator#wrap()
方法围绕我们的认证器构建一个包装器,并将其注册到环境中。我们还将定义一组缓存指令,例如,要缓存多少条记录以及缓存多长时间,使用 Guava 的CacheBuilderSpec
。对于这个例子,我们需要导入io.dropwizard.auth.CachingAuthenticator
和com.google.common.cache.CacheBuilderSpec
。
// Authenticator, with caching support (CachingAuthenticator)
CachingAuthenticator<BasicCredentials, Boolean> authenticator = new CachingAuthenticator<BasicCredentials, Boolean>(
e.metrics(),
new PhonebookAuthenticator(jdbi),
CacheBuilderSpec.parse("maximumSize=10000, expireAfterAccess=10m"));// Register the authenticator with the environment
e.jersey().register(new BasicAuthProvider<Boolean>(
authenticator, "Web Service Realm"));// Register the authenticator with the environment
e.jersey().register(new BasicAuthProvider<Boolean>(
authenticator, "Web Service Realm"));
前面的代码片段中的关键语句是CacheBuilderSpec.parse("maximumSize=10000, expireAfterAccess=10m"));
。通过这个语句,我们配置包装器缓存10000
个主体(maximumSize
属性),即用户名/密码集合,并且每个都缓存 10 分钟。CacheBuilderSpec#parse()
方法用于通过解析字符串来构建一个CacheBuilderSpec
实例。这是为了我们的方便,允许我们将缓存配置外部化,而不是解析一个静态字符串,我们可以解析配置设置文件中定义的属性。
第十章。用户界面 – 视图
我们的 web 服务客户端获取有关联系人的信息,并将其以纯文本形式呈现给用户。我们将使用 Mustache,这是一个作为 dropwizard-views-mustache
模块一部分的模板引擎,来创建 HTML 视图。
为 web 服务客户端构建用户界面
我们将为 web 服务客户端构建一个用户界面,该界面由一个用于在表格中渲染联系人详细信息的 HTML 页面组成。
准备工作
毫不奇怪,我们首先需要做的是在我们的 pom.xml
文件中添加 dropwizard-views
和 dropwizard-assets
依赖项:
<dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-views-mustache</artifactId><version>0.7.0-SNAPSHOT</version></dependency>
<dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-assets</artifactId><version>0.7.0-SNAPSHOT</version></dependency>
此外,我们还需要创建一个文件夹来存储我们的模板文件。根据以下截图创建 [ProjectRoot]/src/main/resources/views
文件夹:
如何做到这一点…
-
通过在
App
类的#initialize()
方法中将Views
包添加到应用程序的引导中,启用Views
包。在初始化阶段(即#initialize()
方法执行时),我们可以使用引导对象将额外的模块注册到我们的应用程序中,例如包或命令。这必须在服务实际启动之前完成(即在#run()
方法被调用之前)。您需要导入io.dropwizard.views.ViewBundle
:@Override public void initialize(Bootstrap<PhonebookConfiguration> b) {b.addBundle(new ViewBundle());}
-
创建一个名为
com.dwbook.phonebook.views
的新包,并在其中创建ContactView
类。该类必须扩展View
,其构造函数将期望一个Contact
实例。您还必须调用超类构造函数,指定此类的模板文件(在这种情况下,contact.mustache
,它存储在我们之前创建的目录中)。您可以使用绝对路径引用view
文件,其中根目录是[ProjectRoot]/src/main/resources/views
文件夹。需要一个获取联系人对象的 getter,以便它可以由模板引擎访问:package com.dwbook.phonebook.views;import com.dwbook.phonebook.representations.Contact;import io.dropwizard.views.View;public class ContactView extends View {private final Contact contact;public ContactView(Contact contact) {super("/views/contact.mustache");this.contact = contact;}public Contact getContact() {return contact;}}
-
现在,让我们创建我们的模板,
contact.moustache
,它将是一个纯 HTML 文件,用于渲染一个包含联系人详细信息的表格。请记住将其存储在我们最初创建的views
文件夹中。看看下面的代码片段:<html><head><title>Contact</title></head><body><table border="1"><tr><th colspan="2">Contact ({{contact.id}})</th></tr><tr><td>First Name</td><td>{{contact.firstName}}</td></tr><tr><td>Last Name</td><td>{{contact.lastName}}</td></tr><tr><td>Phone</td><td>{{contact.phone}}</td></tr></table></body></html>
Mustache 标签,即双大括号包裹的文本,将在运行时自动替换为联系人对象属性的值。Mustache 提供了许多可以在模板中使用的标签类型,例如
条件
和循环
。您可以参考mustache.github.io/mustache.5.html
获取关于 Mustache 标签类型和高级使用的详细信息。 -
让我们现在修改
ClientResource
类,通过更改@Produces
注解,使其使用View
类生成 HTML 而不是纯文本:@Produces(MediaType.TEXT_HTML)
-
修改
#showContact
方法,使其返回一个使用通过 Jersey 客户端获取的联系人表示初始化的ContactView
实例。首先导入com.dwbook.phonebook.views.ContactView
:@GET@Path("showContact")public ContactView showContact(@QueryParam("id") int id) {WebResource contactResource = client.resource("http://localhost:8080/contact/"+id);Contact c = contactResource.get(Contact.class);return new ContactView(c);}
它是如何工作的…
让我们测试 UI。重新构建应用程序,运行它,并将您的浏览器指向http://localhost:8080/client/showContact?id=2
。现在,我们看到的不是客户端的纯文本响应,而是一个 HTML 表格,显示了 ID 等于 2 的联系人详细信息,如下面的截图所示:
当我们访问客户端的 URL 时,它会通过调用适当的服务来获取数据。然后,数据作为Contact
实例传递给扩展了 View 的ContactView
类,该类使用模板引擎解析指定的模板文件contact.mustache
,并生成 HTML 标记。文件扩展名指示应使用的模板引擎。
更多内容…
Mustache 不是唯一由 Dropwizard 支持的模板引擎;还有 Freemarker。我们选择 Mustache 而不是 Freemarker 来展示 Dropwizard 的模板功能,因为 Mustache 是一种更无逻辑、更中立的编程语言,并且为许多编程语言提供了实现。
另一方面,Freemarker 与 Java 绑定,具有更多的编程能力,并且可以执行更复杂的任务,例如清理生成的输出。
如果我们在上一个示例中使用 Freemarker 而不是 Mustache,模板的主要表格将如下所示:
<table border="1">
<tr><th colspan="2">Contact (${contact.id})</th>
</tr>
<tr><td>First Name</td><td>${contact.firstName?html}</td>
</tr>
<tr><td>Last Name</td><td>{contact.lastName?html}</td>
</tr>
<tr><td>Phone</td><td>${contact.phone?html}</td>
</tr>
</table>
如您所见,这两个模板引擎的语法相似。请注意,虽然 Mustache 默认会转义变量,但在 Freemarker 中,您必须指示处理器通过在变量后添加?html
后缀来清理输出。
提供静态资源
有时候,除了基于 HTML 的视图外,您还需要提供静态资源,例如 CSS 样式表、JavaScript 文件或任何可能由您的应用程序使用的其他文件。
要这样做,您可以在#bootstrap()
方法上添加一个AssetsBundle
实例,指定可以提供静态文件的文件夹以及该文件夹将被映射到的 URI。我们首先需要导入io.dropwizard.assets.AssetsBundle
并相应地修改pom.xml
文件,声明对 dropwizard-assets 组件的依赖。
例如,如果你想提供名为stylesheet.css
的静态样式表文件,你必须将其存储在src/main/java/resources/assets
下。
b.addBundle(new AssetsBundle());
stylesheet.css
文件现在可以通过http://localhost:8080/assets/stylesheet.css
URL 访问。
附录 A. 测试 Dropwizard 应用程序
我们的应用程序已经准备好了。然而,如果我们尊重其稳定性,我们必须确保我们至少用单元测试覆盖了其最重要的方面。您可能熟悉单元测试和 JUnit,但 Dropwizard 在这方面走得更远。
dropwizard-testing
模块包含了您创建应用程序测试所需的一切,例如 JUnit 和 FEST 断言,从小的单元测试到更大的、完整的测试。
为应用程序创建完整的测试
让我们为我们的应用程序创建一个完整、完全自动化的集成测试。这个测试应该像手动测试一样启动我们的应用程序,并对应用程序的服务执行一些 HTTP 请求,以检查应用程序的响应。
准备工作
当我们首次使用 Maven 在第二章中创建我们的项目时,创建 Dropwizard 应用程序,JUnit 依赖项已自动添加到我们的 pom.xml
文件中。我们将用 Dropwizard 的测试模块替换它,所以让我们将其删除。在 pom.xml
文件中定位并删除以下依赖项:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
我们将需要 dropwizard-testing
和 hamcrest-all
模块,所以将它们两个都包含在您的 pom.xml
文件中:
<dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-testing</artifactId><version>0.7.0-SNAPSHOT</version></dependency><dependency><groupId>org.hamcrest</groupId><artifactId>hamcrest-all</artifactId><version>1.3</version></dependency>
如何操作…
您的项目已经有一个测试文件夹。在生成默认工件期间,Maven 创建了 src/main/java
(我们的应用程序源代码所在的位置)和 src/test/java
作为单元测试的占位符。让我们看看我们需要放置什么来构建我们的测试:
-
在
src/test/java/com/dwbook/phonebook
文件夹中创建一个新的测试类,ApplicationTest
,继承自ResourceTest
基类。这个类需要有两个方法;#setUp()
,在其中我们将准备我们的模拟对象并添加所需的资源和服务提供者到内存中的 Jersey 服务器,以及#createAndRetrieveContact()
,在其中我们将执行实际的测试:package com.dwbook.phonebook;import static org.fest.assertions.api.Assertions.assertThat;import javax.ws.rs.core.MediaType;import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import com.dwbook.phonebook.representations.Contact; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;import io.dropwizard.testing.junit.DropwizardAppRule;public class ApplicationTest {private Client client;private Contact contactForTest = new Contact(0, "Jane", "Doe", "+987654321");@ClassRulepublic static final DropwizardAppRule<PhonebookConfiguration> RULE =new DropwizardAppRule<PhonebookConfiguration>(App.class, "config.yaml");@Beforepublic void setUp() {client = new Client();// Set the credentials to be used by the clientclient.addFilter(new HTTPBasicAuthFilter("wsuser", "wsp1"));}@Testpublic void createAndRetrieveContact() {// Create a new contact by performing the appropriate http request (POST)WebResource contactResource = client.resource("http://localhost:8080/contact");ClientResponse response = contactResource.type(MediaType.APPLICATION_JSON).post(ClientResponse.class, contactForTest);// Check that the response has the appropriate response code (201)assertThat(response.getStatus()).isEqualTo(201);// Retrieve the newly created contactString newContactURL = response.getHeaders().get("Location").get(0);WebResource newContactResource = client.resource(newContactURL);Contact contact = newContactResource.get(Contact.class);// Check that it has the same properties as the initial oneassertThat(contact.getFirstName()).isEqualTo(contactForTest.getFirstName());assertThat(contact.getLastName()).isEqualTo(contactForTest.getLastName());assertThat(contact.getPhone()).isEqualTo(contactForTest.getPhone());} }
-
我们的测试将在我们发出
mvn
打包命令时运行,但它们也可以通过mvn
的test
命令按需执行。现在,让我们通过发出以下命令在干净的应用程序环境中运行测试:$ mvn clean test
您将看到 Maven 会清理我们的目标目录,启动应用程序,然后成功运行我们的测试。
它是如何工作的…
首先,我们定义了我们的测试数据;即我们打算创建的 Contact
实例。
我们初始化了一个 DropwizardAppRule<PhonebookConfiguration>
实例,该实例被描述为在测试类开始和结束时启动和停止应用程序的 JUnit 规则,允许测试框架以您通常进行手动测试的方式启动应用程序。为此,我们不仅需要指定我们应用程序的主类,还需要指定要使用的配置文件。
在#setUp()
方法中,我们实例化了一个 REST 客户端来帮助我们向应用程序发送 HTTP 请求,并且由于我们的网络服务需要认证,我们还应用了必要的 HTTP 基本认证过滤器。
#createAndRetrieveContact()
方法封装了实际的测试。使用 REST 客户端,我们执行一个 HTTP POST 请求来创建一个新的联系人。在这样一个请求之后,我们期望得到一个带有code 201 – Created
响应的 HTTP 响应。我们使用由Fixtures for Easy Software Testing(FEST)库提供的assertThat()
和isEqual()
辅助方法来测试响应代码是否是我们期望的。正如 FEST 项目主页上所述(code.google.com/p/fest/
)):
"FEST 是一组库,在 Apache 2.0 许可下发布,其使命是简化软件测试。它由各种模块组成,可以与 TestNG 或 JUnit 一起使用。"
还有更多…
我们刚刚展示了如何使用 Dropwizard 测试模块通过启动一个连接到实际数据库的实际服务器来执行集成测试。尽管这个模块不仅限于集成测试。它由 JUnit 支持,你可以用它来进行较小的(但关键的)到较大的单元测试,也可以用于测试实体的正确序列化和反序列化。
添加健康检查
健康检查是我们应用程序的运行时测试。我们将创建一个使用 Jersey 客户端测试创建新联系人的健康检查。
健康检查结果可以通过我们应用程序的管理端口访问,默认端口为 8081。
如何做…
要添加健康检查,请执行以下步骤:
-
创建一个名为
com.dwbook.phonebook.health
的新包,并在其中创建一个名为NewContactHealthCheck
的类:import javax.ws.rs.core.MediaType; import com.codahale.metrics.health.HealthCheck; import com.dwbook.phonebook.representations.Contact; import com.sun.jersey.api.client.*;public class NewContactHealthCheck extends HealthCheck {private final Client client;public NewContactHealthCheck(Client client) {super();this.client = client;}@Overrideprotected Result check() throws Exception {WebResource contactResource = client.resource("http://localhost:8080/contact");ClientResponse response = contactResource.type(MediaType.APPLICATION_JSON).post(ClientResponse.class,new Contact(0, "Health Check First Name","Health Check Last Name", "00000000"));if (response.getStatus() == 201) {return Result.healthy();} else {return Result.unhealthy("New Contact cannot be created!");}}}
-
通过在
App
类的#run()
方法中使用HealthCheckRegistry#register()
方法将健康检查注册到 Dropwizard 环境中。你首先需要导入com.dwbook.phonebook.health.NewContactHealthCheck
。可以通过Environment#healthChecks()
方法访问HealthCheckRegistry
:// Add health checkse.healthChecks().register("New Contact health check", new NewContactHealthCheck(client));
-
在构建并启动你的应用程序后,使用浏览器导航到
http://localhost:8081/healthcheck
:
定义的健康检查的结果以 JSON 格式呈现。如果刚刚创建的自定义健康检查或其他任何健康检查失败,它将被标记为"healthy": false
,让你知道你的应用程序面临运行时问题。
它是如何工作的…
我们使用与我们的client
类完全相同的代码来创建一个健康检查;也就是说,这是一个运行时测试,通过向ContactResource
类的适当端点执行 HTTP POST 请求来确认可以创建新的联系人。这个健康检查让我们对我们的网络服务功能有了所需的信心。
创建一个健康检查所需的一切就是一个扩展HealthCheck
类并实现#check()
方法的类。在类的构造函数中,我们调用父类的构造函数,指定我们的检查名称——即用于识别我们的健康检查的那个名称。
在#check()
方法中,我们实际上执行了一个检查。我们检查一切是否如预期那样。如果是这样,我们返回Result.healthy()
,否则我们返回Result.unhealthy()
,表示有问题发生。
附录 B. 部署 Dropwizard 应用程序
在整本书中,我们展示了并使用了 Dropwizard 项目最重要的部分。我们的应用程序现在已准备好,可以投入生产。它已准备好部署到服务器,从那里可以通过互联网被每个人访问。
准备应用程序以进行部署
如您所猜,我们的应用程序没有很多依赖项。只需检查您的pom.xml
文件,并查找声明maven-compiler-plugin
的部分。
<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>com.dwbook.phonebook</groupId><artifactId>dwbook-phonebook</artifactId><packaging>jar</packaging><version>1.0-SNAPSHOT</version><name>dwbook-phonebook</name><url>http://maven.apache.org</url><!-- Maven Repositories --><repositories><repository><id>sonatype-nexus-snapshots</id><name>Sonatype Nexus Snapshots</name>
<url>http://oss.sonatype.org/content/repositories/snapshots</url></repository></repositories><!-- Dependencies --><dependencies><dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-core</artifactId><version>0.7.0-SNAPSHOT</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency><dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-jdbi</artifactId><version>0.7.0-SNAPSHOT</version></dependency><dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-client</artifactId><version>0.7.0-SNAPSHOT</version></dependency><dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-auth</artifactId><version>0.7.0-SNAPSHOT</version></dependency><dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-views-mustache</artifactId><version>0.7.0-SNAPSHOT</version></dependency><dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-assets</artifactId><version>0.7.0-SNAPSHOT</version></dependency><dependency><groupId>io.dropwizard</groupId><artifactId>dropwizard-testing</artifactId><version>0.7.0-SNAPSHOT</version></dependency><dependency><groupId>org.hamcrest</groupId><artifactId>hamcrest-all</artifactId><version>1.3</version></dependency></dependencies><!-- Build Configuration --><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>1.7</source><target>1.7</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>1.6</version><configuration><filters><filter><artifact>*:*</artifact><excludes><exclude>META-INF/*.SF</exclude><exclude>META-INF/*.DSA</exclude><exclude>META-INF/*.RSA</exclude></excludes></filter></filters></configuration><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>com.dwbook.phonebook.App</mainClass></transformer></transformers></configuration></execution></executions></plugin></plugins></build>
</project>
服务器上应该只存在 Java 运行时环境,其版本等于或高于构建插件配置部分中<target>
元素指定的版本。
如何做到这一点…
一旦我们确认了我们的依赖项(Java 版本)满足要求,我们就可以通过 FTP 上传 JAR 文件,并以与我们之前相同的方式运行应用程序:
$ java -jar <applicationFilename.jar> server <configFileName.yaml>
它是如何工作的…
在我们的pom.xml
文件中,我们声明了所有必需的 Maven 参数,包括maven-shade-plugin
,这使得我们可以构建一个包含我们应用程序使用的所有第三方模块和库的单个 JAR 文件。只需记住,也要将您的配置文件上传到服务器,或者创建一个新的配置文件,可能包含不同的设置,例如数据库连接详情。
还有更多…
有许多很好的理由,您可能希望将应用程序的默认端口从 8080 更改为其他端口。
这可以通过对您的配置文件config.yaml
进行少量添加来实现。然而,为了使这些设置生效,我们需要在构建配置中添加 ServiceResourceTransformer,通过在 pom.xml 文件中的<transformers>
部分添加以下条目来实现:<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
。
添加server
部分,并按照以下代码配置其属性:
server:applicationConnectors:- type: http# The port the application will listen onport: 8181adminConnectors:- type: http# The admin portport: 8282
多个配置文件
一种良好的实践是为您的应用程序的每个环境维护不同的配置文件集(YAML)。例如,您可能会为测试和生产环境使用不同的数据库,并且最好将连接信息保存在不同的文件中。此外,您可能希望在开发或测试环境中的日志级别比生产环境更详细。根据您应用程序的性质和复杂性,肯定会有许多其他原因让您和您的应用程序从中受益。幸运的是,Dropwizard 提供了许多可以调整以匹配您应用程序需求的设置。