Spring REST 教程(一)
原文:Spring REST
一、REST 简介
在本章中,我们将学习以下内容:
-
休止符基础
-
REST 资源及其表示
-
HTTP 方法和状态代码
-
理查森的成熟度模型
今天,网络已经成为我们生活中不可或缺的一部分——从在脸书上查看状态到在线订购产品,再到通过电子邮件交流。Web 的成功和无处不在导致组织将 Web 的架构原则应用于构建分布式应用。在这一章中,我们将深入 REST,一种形式化这些原则的架构风格。
什么是休息?
REST 代表表述性状态转移,是一种用于设计分布式网络应用的架构风格。罗伊·菲尔丁在他的博士论文 1 中创造了术语 REST,并提出以下六个约束或原则作为其基础:
-
客户端-服务器——客户端和服务器之间的关注点应该分开。这使得客户端和服务器组件能够独立发展,从而允许系统伸缩。
-
无状态—客户端和服务器之间的通信应该是无状态的。服务器不需要记住客户端的状态。相反,客户端必须在请求中包含所有必要的信息,以便服务器能够理解和处理它。
-
分层系统—客户端和服务器之间可以存在多个分层结构,如网关、防火墙和代理。可以透明地添加、修改、重新排序或删除层,以提高可伸缩性。
-
缓存—来自服务器的响应必须声明为可缓存或不可缓存。这将允许客户机或其中间组件缓存响应,并在以后的请求中重用它们。这降低了服务器的负载,有助于提高性能。
-
统一接口—客户端、服务器和中介组件之间的所有交互都基于其接口的统一性。这简化了整体架构,因为只要组件实现了商定的契约,它们就可以独立发展。统一接口约束进一步分解为四个子约束:资源标识、资源表示、自描述消息和作为应用状态引擎的超媒体,或 HATEOAS。我们将在本章的后面几节研究其中的一些指导原则。
-
按需代码—客户端可以通过按需下载和执行代码来扩展其功能。例子包括 JavaScript 脚本、Java 小程序、Silverlight 等等。这是一个可选约束。
遵守这些约束的应用被认为是 RESTful 的。正如您可能已经注意到的,这些约束并不决定用于开发应用的实际技术。相反,遵循这些指导方针和最佳实践将使应用可伸缩、可见、可移植、可靠,并且能够更好地执行。理论上,可以使用任何网络基础设施或传输协议构建 RESTful 应用。实际上,RESTful 应用利用了 Web 的特性和功能,并使用 HTTP 作为传输协议。
统一接口约束是 REST 应用区别于其他基于网络的应用的关键特性。REST 应用中的统一接口是通过资源、表示、URIs 和 HTTP 方法等抽象来实现的。在接下来的部分中,我们将研究这些重要的 REST 抽象。
了解资源
REST 中信息的关键抽象是资源。
—罗伊·菲尔丁
REST 的基础是资源的概念。资源是任何可以被访问或操作的东西。资源的例子包括“视频”、“博客条目”、“用户配置文件”、“图像”,甚至是有形的东西,如人或设备。资源通常与其他资源相关。例如,在电子商务应用中,客户可以订购任意数量的产品。在这个场景中,产品资源与相应的订单资源相关。也可以将资源分组到集合中。使用相同的电子商务示例,“订单”是单个“订单”资源的集合。
识别资源
在我们能够交互和使用资源之前,我们必须能够识别它。Web 提供了统一资源标识符或 URI,用于唯一地标识资源。URI 的语法是
scheme:scheme-specific-part
使用分号分隔scheme和scheme-specific-part。方案的例子包括http或ftp或mailto,并且用于定义 URI 其余部分的语义和解释。拿 URI 的例子来说——http://www.apress.com/9781484208427。示例的http部分是方案;它表示应该使用 HTTP 方案来解释 URI 的其余部分。HTTP 方案被定义为 RFC 7230 的一部分, 2 表示由我们的示例 URI 标识的资源位于主机名为apress.com的机器上。
表 1-1 显示了 URIs 的例子以及它们所代表的不同资源。
表 1-1
URI 和资源描述
|上呼吸道感染
|
资源描述
|
| --- | --- |
| http://blog.example.com/posts | 表示博客文章资源的集合。 |
| http://blog.example.com/posts/1 | 表示标识符为“1”的博客文章资源;这种资源被称为单体资源。 |
| http://blog.example.com/posts/1/comments | 表示与由“1”标识的博客条目相关联的评论集合;像这样驻留在资源下的集合称为子集合。 |
| http://blog.example.com/posts/1/comments/245 | 表示由“245”标识的注释资源 |
即使一个 URI 唯一地标识一个资源,一个资源也可能有多个 URI。例如,可以使用 URIs https://www.facebook.com 和 https://www.fb.com 访问脸书。术语 URI 别名用于指代标识相同资源的这种 URIs。URI 别名提供了灵活性和额外的便利,例如必须键入更少的字符才能找到资源。
URI 模板
当使用 REST 和 REST API 时,有时您需要表示 URI 的结构,而不是 URI 本身。例如,在一个博客应用中,URI http://blog.example.com/2014/posts将检索 2014 年创建的所有博客帖子。类似地,URIs http://blog.example.com/2013/posts、http://blog.example.com/2012/posts、,等将返回对应于 2013 年、2012 年等年份的博客帖子。在这种情况下,对于消费客户来说,知道描述 URIs 范围而不是单个 URIs 的 URI 结构http://blog.example.com/ 年 /posts会很方便。
RFC 6570 ( http://tools.ietf.org/html/rfc6570)中定义的 URI 模板提供了描述 URI 结构的标准化机制。这种情况下的标准化 URI 模板可以是
http://blog.example.com/{year}/posts
花括号{}表示模板的年份部分是一个变量,通常称为路径变量。消费客户端可以将这个 URI 模板作为输入,用正确的值替换 year 变量,并检索相应年份的博客文章。在服务器端,URL 模板允许服务器代码轻松地解析和检索变量值或 URI 的选定部分。
表示
RESTful 资源是抽象的实体。构成 RESTful 资源的数据和元数据需要在发送到客户机之前序列化成一个表示。这种表示可以被视为在给定时间点资源状态的快照。考虑一个电子商务应用中的数据库表,它存储所有可用产品的信息。当一个在线购物者使用他们的浏览器购买一个产品并请求它的详细信息时,应用会以 HTML 网页的形式提供产品的详细信息。现在,当编写原生移动应用的开发人员请求产品细节时,电子商务应用可能会以 XML 或 JSON 格式返回这些细节。在这两个场景中,客户端都没有与实际的资源(保存产品详细信息的数据库记录)进行交互。相反,他们处理其代表性。
Note
REST 组件通过来回传输资源的表示来与资源交互。他们从不直接与资源互动。
正如这个产品示例中所提到的,同一个资源可以有多个表示。这些表示形式从基于文本的 HTML、XML 和 JSON 格式到 pdf、JPEGs 和 MP4s 等二进制格式。客户端可以请求特定的表示,这个过程被称为内容协商。以下是两种可能的内容协商策略:
-
用期望的表示对 URI 进行后置处理——在这个策略中,请求 JSON 格式的产品细节的客户将使用 URI
http://www.example.com/products/143.json。不同的客户端可能使用 URIhttp://www.example.com/products/143.xml来获取 XML 格式的产品细节。 -
使用
Accept头——客户端可以用所需的表示填充 HTTPAccept头,并随请求一起发送。处理资源的应用将使用Accept头值来序列化请求的表示。RFC 2616 3 提供了一组详细的规则,用于指定一种或多种格式及其优先级。
Note
JSON 已经成为 REST 服务事实上的标准。本书中的所有例子都使用 JSON 作为请求和响应的数据格式。
HTTP 方法
“统一接口”约束通过一些标准化操作或动词来限制客户机和服务器之间的交互。在 Web 上,HTTP 标准 4 提供了八种 HTTP 方法,允许客户端交互和操纵资源。一些常用的方法有 GET、POST、PUT 和 DELETE。在我们深入研究 HTTP 方法之前,让我们回顾一下它们的两个重要特征——安全性和幂等性。
Note
HTTP 规范使用术语“方法”来表示 HTTP 操作,如 GET、PUT 和 POST。然而,术语 HTTP 动词也可以互换使用。
安全
如果 HTTP 方法不会对服务器状态造成任何改变,那么它就是安全的。考虑 GET 或 HEAD 之类的方法,它们用于从服务器检索信息/资源。这些请求通常被实现为只读操作,不会对服务器的状态造成任何改变,因此被认为是安全的。
安全的方法用于检索资源。然而,安全性并不意味着该方法每次都必须返回相同的值。例如,检索 Google 股票的 GET 请求可能会导致每次调用的值不同。但是只要它没有改变任何状态,它仍然被认为是安全的。
在现实世界的实现中,安全操作仍然可能有副作用。考虑这样一个实现,其中每个股票价格请求都记录在一个数据库中。从纯粹主义者的角度来看,我们正在改变整个系统的状态。然而,从实际的角度来看,因为这些副作用是服务器实现的唯一责任,所以该操作仍然被认为是安全的。
幂等性
如果一个操作产生相同的服务器状态,无论我们应用它一次还是多次,它都被认为是幂等的。诸如 GET、HEAD(也是安全的)、PUT 和 DELETE 之类的 HTTP 方法被认为是等幂的,这保证了客户端可以重复请求,并期望得到与发出一次请求相同的效果。第二个和随后的请求使资源状态保持与第一个请求完全相同的状态。
考虑您在电子商务应用中删除订单的场景。请求成功完成后,订单不再存在于服务器上。因此,将来任何删除该订单的请求仍然会导致相同的服务器状态。相比之下,考虑使用 POST 请求创建订单的场景。成功完成请求后,会创建一个新订单。如果您要重新“发布”相同的请求,服务器只需接受该请求并创建一个新订单。因为重复的 POST 请求会导致不可预见的副作用,所以 POST 不被认为是等幂的。
得到
GET 方法用于检索资源的表示形式。例如,URI http://blog.example.com/posts/1上的 GET 返回由 1 标识的博客文章的表示。相比之下,URI 上的 GEThttp://blog.example.com/posts检索一组博客文章。因为 GET 请求不修改服务器状态,所以它们被认为是安全的和等幂的。
这里显示了对http://blog.example.com/posts/1的一个假设的 GET 请求和相应的响应。
GET /posts/1 HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.5
Connection: keep-alive
Host: blog.example.com
Content-Type: text/html; charset=UTF-8
Date: Sat, 10 Jan 2015 20:16:58 GMT
Server: Apache
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns:="http://www.w3.org/1999/xhtml">
<head>
<title>First Post</title>
</head>
<body>
<h3>Hello World!!</h3>
</body>
</html>
除了表示之外,对 GET 请求的响应还包括与资源相关联的元数据。这种元数据被表示为一系列称为 HTTP 头的键值对。Content-Type和Server是您在这个响应中看到的头的例子。因为 GET 方法是安全的,所以可以缓存对 GET 请求的响应。
GET 方法的简单性经常被滥用,它被用来执行诸如删除或更新资源表示的操作。这种用法违反了标准的 HTTP 语义,是非常不鼓励的。
头
有时,客户端希望检查特定的资源是否存在,并不真正关心实际的表示。在另一个场景中,客户端希望在下载之前知道是否有更新版本的资源可用。在这两种情况下,就带宽和资源而言,GET 请求可能是“重量级”的。取而代之的是头的方法更合适。
HEAD 方法允许客户端仅检索与资源相关联的元数据。没有资源表示被发送到客户端。表示为 HTTP 头的元数据将与响应 GET 请求而发送的信息相同。客户端使用这些元数据来确定资源的可访问性和最近的修改。下面是一个假设的 HEAD 请求和响应。
HEAD /posts/1 HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.5
Connection: keep-alive
Host: blog.example.com
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
Date: Sat, 10 Jan 2015 20:16:58 GMT
Server: Apache
像 GET 一样,HEAD 方法也是安全和幂等的,响应可以缓存在客户机上。
删除
DELETE 方法,顾名思义,请求一个资源被删除。收到请求后,服务器删除资源。对于可能需要很长时间才能删除的资源,服务器通常会发送一条确认消息,表明它已收到请求并将处理它。根据服务实现,资源可能会也可能不会被物理删除。
成功删除后,未来对该资源的 GET 请求将通过 HTTP 状态代码 404 产生“Resource Not Found”错误。我们将在一分钟内介绍状态代码。
在这个例子中,客户端请求删除由 1 标识的帖子。完成后,服务器可以返回状态代码 200 (OK)或 204 (No Content ),表明请求已成功处理。
Delete /posts/1 HTTP/1.1
Content-Length: 0
Content-Type: application/json
Host: blog.example.com
类似地,在这个例子中,与帖子#2 相关联的所有评论都被删除。
Delete /posts/2/comments HTTP/1.1
Content-Length: 0
Content-Type: application/json
Host: blog.example.com
因为 DELETE 方法修改系统的状态,所以它被认为是不安全的。但是,DELETE 方法被认为是幂等的;后续的删除请求仍然会使资源和系统处于相同的状态。
放
PUT 方法允许客户端修改资源状态。客户端修改资源的状态,并使用 PUT 方法将更新后的表示发送给服务器。收到请求后,服务器用新状态替换资源的状态。
在这个例子中,我们发送一个 PUT 请求来更新由 1 标识的帖子。该请求包含更新后的博文正文以及组成博文的所有其他字段。成功处理后,服务器将返回一个状态代码 200,表明请求已被成功处理。
PUT /posts/1 HTTP/1.1
Accept: */*
Content-Type: application/json
Content-Length: 65
Host: blog.example.com
BODY
{"title": "First Post","body": "Updated Hello World!!"}
考虑我们只想更新博客文章标题的情况。HTTP 语义规定,作为 PUT 请求的一部分,我们发送完整的资源表示,其中包括更新后的标题以及其他属性,如 blog post body 等,它们没有发生变化。然而,这种方法要求客户端具有完整的资源表示,如果资源非常大或者有很多关系,这可能是不可能的。此外,这将需要更高的数据传输带宽。因此,出于实际原因,设计倾向于接受部分表示作为 PUT 请求的一部分的 API 是可以接受的。
Note
为了支持部分更新,RFC 5789 ( http://www.ietf.org/rfc/rfc5789.txt )中增加了一个名为 PATCH 的新方法。我们将在本章的后面讨论补丁方法。
客户端也可以使用 PUT 方法创建新的资源。然而,只有当客户端知道新资源的 URI 时,这才是可能的。例如,在博客应用中,客户端可以上传与博客帖子相关联的图像。在这种情况下,客户端决定图像的 URL,如下例所示:
PUT http://blog.example.com/postsimg/author.jpg
PUT 不是一个安全的操作,因为它会改变系统状态。但是,它被认为是等幂的,因为将同一资源放置一次或多次会产生相同的结果。
邮政
POST 方法用于创建资源。通常,它用于在子集合(存在于父资源下的资源集合)下创建资源。例如,POST 方法可用于在博客应用中创建新的博客条目。这里,“posts”是位于博客父资源下的博客帖子资源的子集合。
POST /posts HTTP/1.1
Accept: */*
Content-Type: application/json
Content-Length: 63
Host: blog.example.com
BODY
{"title": "Second Post","body": "Another Blog Post."}
Content-Type: application/json
Location: posts/12345
Server: Apache
与 PUT 不同,POST 请求不需要知道资源的 URI。服务器负责为资源分配一个 ID,并决定资源将要驻留的 URI。在前面的例子中,博客应用将处理 POST 请求并在http://blog.example.com/posts/12345下创建一个新资源,其中“12345”是服务器生成的 id。响应中的Location头包含新创建的资源的 URL。
POST 方法非常灵活,通常在没有其他合适的 HTTP 方法时使用。考虑您想要为 JPEG 或 PNG 图像生成缩略图的场景。这里我们要求服务器对我们提交的图像二进制数据执行一个操作。像 GET 和 PUT 这样的 HTTP 方法并不适合这里,因为我们正在处理一个 RPC 风格的操作。这种情况使用 POST 方法处理。
Note
术语“控制器资源”被用来描述接受输入、执行某些动作并返回输出的可执行资源。尽管这些类型的资源不符合真正的 REST 资源定义,但是它们非常方便地公开复杂的操作。
POST 方法被认为是不安全的,因为它会改变系统状态。此外,多次 POST 调用会导致生成多个资源,这使它变得不可靠。
修补
正如我们前面讨论的,HTTP 规范要求客户机将整个资源表示作为 PUT 请求的一部分发送。作为 RFC 5789 ( http://tools.ietf.org/html/rfc5789 )的一部分提出的补丁方法用于执行部分资源更新。它既不安全也不幂等。下面是一个使用补丁方法更新博客文章标题的例子。
PATCH /posts/1 HTTP/1.1
Accept: */*
Content-Type: application/json
Content-Length: 59
Host: blog.example.com
BODY
{"replace": "title","value": "New Awesome title"}
请求正文包含对需要在资源上执行的更改的描述。在示例中,请求主体使用"replace"命令来指示需要替换"title"字段的值。
作为补丁请求的一部分,没有标准化的格式来描述对服务器的更改。不同的实现可能使用以下格式来描述相同的更改:
{"change" : "name", "from" : "Post Title", "to" : "New Awesome Title"}
目前,正在为 JSON 定义补丁格式( http://tools.ietf.org/html/draft-ietf-appsawg-json-patch )。缺乏标准导致实现以更简单的格式描述变更集,如下所示:
{"name" : "New Awesome Title"}
Crud and HTTP Verbs
数据驱动的应用通常使用术语 CRUD 来表示四种基本的持久性功能——创建、读取、更新和删除。一些构建 REST 应用的开发人员错误地将四个流行的 HTTP 动词 GET、POST、PUT 和 DELETE 与 CRUD 语义相关联。常见的典型联想是
Create -> POST
Update -> PUT
Read -> GET
Delete -> DELETE
这些相关性适用于读取和删除操作。但是,对于创建/更新和发布/上传来说,就不那么简单了。正如您在本章前面所看到的,只要满足幂等性约束,PUT 就可以用来创建资源。同理,如果 POST 用于更新( http://roy.gbiv.com/untangled/2009/it-is-okay-to-use-post ),也不会被认为是非 RESTful 的。客户端也可以使用补丁来更新资源。
因此,对于 API 设计者来说,为给定的操作使用正确的动词比简单地使用与 CRUD 的一对一映射更重要。
HTTP 状态代码
HTTP 状态代码允许服务器传达处理客户端请求的结果。这些状态代码分为以下几类:
-
信息代码—指示服务器已收到请求但尚未完成处理的状态代码。这些中间响应代码属于 100 系列。
-
成功代码—指示请求已被成功接收和处理的状态代码。这些代码属于 200 系列。
-
重定向代码—状态代码,表示请求已被处理,但客户端必须执行额外的操作才能完成请求。这些操作通常涉及重定向到不同的位置以获取资源。这些代码属于 300 系列。
-
客户端错误代码—表示客户端请求存在错误或问题的状态代码。这些代码属于 400 系列。
-
服务器错误代码—表示服务器在处理客户端请求时出错的状态代码。这些代码属于 500 系列。
HTTP 状态代码在 REST API 设计中起着重要的作用,因为有意义的代码有助于传达正确的状态,使客户端能够做出适当的反应。表 1-2 显示了一些您通常会遇到的重要状态代码。
表 1-2
HTTP 状态代码及其描述
|状态代码
|
描述
| | --- | --- | | 100(续) | 表示服务器已经收到请求的第一部分,请求的其余部分应该发送出去。 | | 200(好的) | 表明请求一切顺利。 | | 201(已创建) | 表示请求已完成,新资源已创建。 | | 202(已接受) | 表示请求已被接受,但仍在处理中。 | | 204(无内容) | 指示服务器已完成请求,并且没有要发送到客户端的实体主体。 | | 301(永久移动) | 指示请求的资源已移动到新位置,需要使用新的 URI 来访问该资源。 | | 400(错误请求) | 表示请求格式不正确,服务器无法理解该请求。 | | 401(未经授权) | 指示客户端需要在访问资源之前进行身份验证。如果请求已经包含客户端的凭证,则 401 指示无效的凭证(例如,错误的密码)。 | | 403(禁止) | 表示服务器理解请求,但拒绝执行请求。这可能是因为正在从黑名单中的 IP 地址或在批准的时间窗口之外访问资源。 | | 404(未找到) | 指示请求的 URI 处的资源不存在。 | | 406(不可接受) | 指示服务器能够处理该请求;但是,客户端可能不接受生成的响应。当客户端对 accept 头过于挑剔时,就会发生这种情况。 | | 500(内部服务器错误) | 表示处理请求时服务器出错,请求无法完成。 | | 503(服务不可用) | 表示请求无法完成,因为服务器过载或正在进行定期维护。 |
理查森的成熟度模型
由 Leonard Richardson 开发的 Richardson 成熟度模型(RMM)根据基于 REST 的 web 服务遵守 REST 原则的程度对它们进行分类。图 1-1 显示了这种分类的四个级别。
图 1-1
RMM 级别
RMM 对于理解不同风格的 web 服务以及它们的设计、好处和权衡是有价值的。
零级
这是服务最基本的成熟度级别。此级别的服务使用 HTTP 作为传输机制,并在单个 URI 上执行远程过程调用。通常,POST 或 GET HTTP 方法用于服务调用。基于 SOAP 和 XML-RPC 的 web 服务属于这一级。
一级
下一个级别更加严格地遵循 REST 原则,并引入了多个 URIs,每个资源一个。大型服务端点的复杂功能被分解成多个资源。然而,这一层中的服务使用一个 HTTP 动词(通常是 POST)来执行所有的操作。
第二层
这一级别的服务利用 HTTP 协议,并正确使用协议中可用的 HTTP 动词和状态代码。实现 CRUD 操作的 Web 服务是二级服务的好例子。
第三层
这是服务最成熟的层次,围绕超媒体作为应用状态引擎(HATEOAS)的概念构建。这个级别的服务通过提供包含其他相关资源和控件的链接的响应,告诉客户端下一步做什么,从而允许发现。
构建 RESTful API
设计和实现一个漂亮的 RESTful API 不亚于一门艺术,它需要时间、精力和多次迭代。一个设计良好的 RESTful API 允许您的最终用户轻松地使用该 API,并使其更容易被采用。概括地说,下面是构建 RESTful API 的步骤:
-
识别资源 REST 的核心是资源。我们开始对消费者感兴趣的不同资源进行建模。通常,这些资源可以是应用的域或实体。然而,并不总是需要一对一的映射。
-
确定端点—下一步是设计将资源映射到端点的 URIs。在第四章中,我们将探讨设计和命名端点的最佳实践。
-
识别操作—识别可用于对资源执行操作的 HTTP 方法。
-
识别响应—识别请求和响应的支持资源表示,以及要返回的正确状态代码。
在本书的其余部分,我们将探讨设计 RESTful API 并使用 Spring 技术实现它的最佳实践。
摘要
REST 已经成为当今建筑服务的事实标准。在这一章中,我们介绍了 REST 和抽象的基础知识,如资源、表示、URIs 和 HTTP 方法,它们构成了 REST 的统一接口。我们还查看了 RMM,它提供了 REST 服务的分类。
在下一章,我们将深入探究 Spring 及其简化 REST 服务开发的相关技术。
Footnotes 1www.ics.uci.edu/%257Efielding/pubs/dissertation/top.htm
2
http://tools.ietf.org/html/rfc7230 。
3
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1??。
4
https://www.ietf.org/rfc/rfc2616.txt 。
二、Spring Web MVC 入门
在本章中,我们将讨论以下内容:
-
Spring 及其特征
-
模型视图控制器模式
-
Spring Web MVC 及其组件
Java 生态系统中充满了诸如 Jersey 和 RESTEasy 之类的框架,它们允许您开发 REST 应用。Spring web MVC 就是这样一个流行的 Web 框架,它简化了 Web 和 REST 应用的开发。我们从 Spring 框架的概述开始这一章,并深入探究 Spring Web MVC 及其组件。
Note
这本书没有给出 Spring 和 Spring Web MVC 的全面概述。参考 Pro Spring 和 Pro Spring MVC 和 WebFlux (均由 Apress 出版)对这些概念的详细处理。
春季概览
Spring 框架已经成为构建基于 Java/Java EE 的企业应用的事实标准。Spring 框架最初由 Rod Johnson 在 2002 年编写,是 Pivotal Software Inc .(http://spring.io)拥有和维护的项目套件之一。除此之外,Spring 框架还提供了一个依赖注入模型 1 来减少应用开发的管道代码,支持面向方面编程(AOP)来实现横切关注点,并使其易于与其他框架和技术集成。Spring 框架由不同的模块组成,这些模块提供数据访问、工具、消息传递、测试和 web 集成等服务。不同的弹簧框架模块及其分组如图 2-1 所示。
图 2-1
Spring 框架模块
作为开发人员,您不必被迫使用 Spring 框架提供的所有东西。Spring 框架的模块化允许您根据您的应用需求挑选模块。在本书中,我们将重点介绍用于开发 REST 服务的 web 模块。此外,我们将使用一些其他的 Spring portfolio 项目,比如 Spring Data、Spring Security 和 Spring Boot。这些项目建立在 Spring 框架模块提供的基础设施之上,旨在简化数据访问、认证/授权和 Spring 应用创建。
开发基于 Spring 的应用需要彻底理解两个核心概念——依赖注入和面向方面编程。
依赖注入
Spring 框架的核心是依赖注入(DI)。顾名思义,依赖注入允许依赖被注入到需要它们的组件中。这使得这些组件不必创建或定位它们的依赖关系,从而允许它们松散耦合。
为了更好地理解 DI,考虑在在线零售店购买产品的场景。完成购买通常是使用一个组件来实现的,比如 OrderService。OrderService 本身将与一个 OrderRepository 交互,这个 order repository 将在数据库中创建订单详细信息,并与一个 NotificationComponent 交互,这个 notification component 将向客户发送订单确认。在传统实现中,OrderService 创建(通常在其构造函数中)OrderRepository 和 NotificationComponent 的实例并使用它们。尽管这种方法没有任何问题,但它会导致难以维护、难以测试和高度耦合的代码。
相反,DI 允许我们在处理依赖关系时采取不同的方法。使用 DI,您可以让 Spring 之类的外部进程创建依赖项,管理依赖项,并将这些依赖项注入到需要它们的对象中。因此,使用 DI,Spring 将创建 OrderRepository 和 NotificationComponent,然后将这些依赖项交给 OrderService。这使得 OrderService 不必处理 order repository/notification component 的创建,从而更容易测试。它允许每个组件独立发展,使得开发和维护更加容易。此外,它使不同的实现交换这些依赖关系或在不同的上下文中使用这些组件变得更加容易。
面向方面编程
面向方面编程(AOP)是一种实现横切逻辑或关注点的编程模型。日志、事务、度量和安全性是跨越(横切)应用不同部分的关注点的一些例子。这些关注点不处理业务逻辑,并且经常在应用中重复。AOP 提供了一种被称为方面的标准化机制,用于将这样的关注封装在一个单独的位置。然后这些方面被编织到其他对象中,这样横切逻辑就会自动应用到整个应用中。
Spring 通过它的 Spring AOP 模块提供了一个纯基于 Java 的 AOP 实现。Spring AOP 不需要任何特殊的编译,也不需要改变类装入器的层次结构。相反,Spring AOP 使用代理在运行时将方面编织到 Spring beans 中。图 2-2 展示了这种行为。当目标 bean 上的某个方法被调用时,代理会截获该调用。然后,它应用方面逻辑并调用目标 bean 方法。
图 2-2
Spring AOP 代理
Spring 提供了双代理实现——JDK 动态代理和 CGLIB 代理。如果目标 bean 实现了一个接口,Spring 将使用 JDK 动态代理来创建 AOP 代理。如果这个类没有实现接口,Spring 使用 CGLIB 来创建一个代理。
你可以在官方文档中了解更多关于 JDK 动态代理: https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
Spring Web MVC 概述
Spring web MVC 是 Spring 框架的 Web 模块的一部分,是构建基于 Web 的应用的流行技术。它基于模型-视图-控制器架构,并提供了丰富的注释和组件集。多年来,该框架不断演变;它目前提供了一组丰富的配置注释和特性,比如灵活的视图分辨率和强大的数据绑定。
模型视图控制器模式
模型视图控制器(MVC)是一种用于构建解耦 web 应用的架构模式。该模式将 UI 层分解为以下三个组件:
-
模型—模型代表数据或状态。在基于 web 的银行应用中,代表帐户、事务和报表的信息是该模型的示例。
-
视图(view )-提供模型的可视化表示。这是用户通过提供输入和查看输出进行交互的内容。在我们的银行应用中,显示账户和事务的网页就是视图的例子。
-
控制器——控制器负责处理用户操作,如按钮点击。然后,它与服务或存储库交互来准备模型,并将准备好的模型交给适当的视图。
每个组件都有特定的职责。它们之间的相互作用如图 2-3 所示。交互从控制器准备模型并选择要呈现的适当视图开始。视图使用模型中的数据进行渲染。与视图的进一步交互被发送到控制器,控制器重新开始这个过程。
图 2-3
模型视图控制器交互
Spring Web MVC 架构
Spring 的 Web MVC 实现围绕 dispatcher servlet——front controller 模式 2 的一个实现,作为处理请求的入口点。Spring Web MVC 的架构如图 2-4 所示。
图 2-4
Spring Web MVC 的架构
图 2-4 中的不同组件及其相互作用如下:
-
交互从 DispatcherServlet 接收来自客户机的请求开始。
-
DispatcherServlet 查询一个或多个 HandlerMapping,以找出可以为请求提供服务的处理程序。处理程序是对 Spring Web MVC 支持的控制器和其他基于 HTTP 的端点进行寻址的通用方式。
-
HandlerMapping 组件使用请求路径来确定正确的处理程序,并将其传递给 DispatcherServlet。HandlerMapping 还确定了在处理程序执行之前(前)和之后(后)需要执行的拦截器列表。
-
然后,DispatcherServlet 执行适当的预处理拦截器,并将控制传递给处理程序。
-
处理程序与任何需要的服务进行交互,并准备模型。
-
该处理程序还确定需要在输出中呈现的视图的名称,并将其发送给 DispatcherServlet。然后执行后进程拦截器。
-
接下来是 DispatcherServlet 将逻辑视图名传递给 ViewResolver,后者确定并传递实际的视图实现。
-
DispatcherServlet 然后将控制和模型传递给视图,视图生成响应。这个 ViewResolver 和视图抽象允许 DispatcherServlet 从特定的视图实现中分离出来。
-
DispatcherServlet 将生成的响应返回给客户端。
Spring Web MVC 组件
在上一节中,向您介绍了 Spring Web MVC 组件,如 HandlerMapping 和 ViewResolver。在这一节中,我们将更深入地了解这些以及其他的 Spring Web MVC 组件。
Note
在本书中,我们将使用 Java 配置来创建 Spring beans。与基于 XML 的配置相反,Java 配置提供了编译时安全性、灵活性和额外的功能/控制。
控制器
Spring Web MVC 中的控制器是使用原型org.springframework.stereotype.Controller声明的。Spring 中的原型指定了类或接口的角色或职责。清单 2-1 显示了一个基本的控制器。
@Controller
public class HomeController {
@GetMapping("/home.html")
public String showHomePage() {
return "home";
}
}
Listing 2-1HomeController Implementation
@Controller注释将HomeController类指定为 MVC 控制器。@GetMapping 是一个复合注释,充当@ request mapping(method = request method)的快捷方式。获取)。@GetMapping 批注将 web 请求映射到处理程序类和处理程序方法。在这种情况下,@GetMapping 指示当发出对home.html的请求时,应该执行showHomePage方法。showHomePage方法有一个很小的实现,只是返回逻辑视图名home。在这个例子中,这个控制器没有准备任何模型。
模型
Spring 提供了org.springframework.ui.Model接口,作为模型属性的持有者。清单 2-2 显示了带有可用方法的Model接口。顾名思义,addAttribute和addAttributes方法可以用来给模型对象添加属性。
public interface Model {
Model addAttribute(String attributeName, Object attributeValue);
Model addAttribute(Object attributeValue);
Model addAllAttributes(Collection<?> attributeValues);
Model addAllAttributes(Map<String, ?> attributes);
Model mergeAttributes(Map<String, ?> attributes);
boolean containsAttribute(String attributeName);
Map<String, Object> asMap();
Object getAttribute(String attributeName);
}
Listing 2-2Model Interface
控制器处理模型对象最简单的方法是将其声明为方法参数。清单 2-3 显示了带有模型参数的showHomePage方法。在方法实现中,我们向模型对象添加了currentDate属性。
@GetMapping("/home.html")
public String showHomePage(Model model) {
model.addAttribute("currentDate", new Date());
return "home";
}
Listing 2-3showHomePage with Model Attribute
Spring 框架努力将我们的应用从框架的类中分离出来。因此,处理模型对象的一种流行方法是使用清单 2-4 中所示的java.util.Map实例。Spring 将使用传入的 Map 参数实例来丰富暴露给视图的模型。
@GetMapping("/home.html")
public String showHomePage(Map model) {
model.put("currentDate", new Date());
return "home";
}
Listing 2-4showHomePage with Map Attribute
视角
Spring Web MVC 支持多种视图技术,比如 JSP、Velocity、FreeMarker 和 XSLT。Spring Web MVC 使用org.springframework.web.servlet.View接口来完成这个任务。View接口有两个方法,如清单 2-5 所示。
public interface View
{
String getContentType();
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
Listing 2-5View Interface API
接口的具体实现负责呈现响应。这是通过覆盖render方法来实现的。getContentType方法返回生成的视图的内容类型。表 2-1 展示了 Spring Web MVC 提供的开箱即用的重要View实现。您会注意到所有这些实现都驻留在org.springframework.web.servlet.view包中。
表 2-1
Spring Web MVC 视图实现
|类别名
|
描述
|
| --- | --- |
| org.springframework.web.servlet.view.json.MappingJackson2JsonView | 查看编码模型属性并返回 JSON 的实现 |
| org.springframework.web.servlet.view.xslt.XsltView | 查看执行 XSLT 转换并返回响应的实现 |
| org.springframework.web.servlet.view.InternalResourceView | 将请求委托给 web 应用内部的 JSP 页面的视图实现 |
| org.springframework.web.servlet.view.tiles2.TilesView | 使用 Apache Tiles 配置进行图块定义和渲染的视图实现 |
| org.springframework.web.servlet.view.JstlView | 使用 JSTL 支持 JSP 页面的InternalResourceView的专门实现 |
| org.springframework.web.servlet.view.RedirectView | 查看重定向到不同(绝对或相对)URL 的实现 |
清单 2-6 显示了我们之前看到的HomeController的重新实现。这里我们创建了一个JstlView的实例,并设置了需要呈现的 JSP 页面。
@Controller
public class HomeController {
@RequestMapping("/home.html")
public View showHomePage() {
JstlView view = new JstlView();
view.setUrl("/WEB-INF/pages/home.jsp");
return view;
}
}
Listing 2-6HomeController View Implementation
控制器实现通常不处理视图实例。相反,它们返回逻辑视图名,如清单 2-1 所示,并让视图解析器确定和创建视图实例。这将控制器与特定的视图实现解耦,并使交换视图实现变得容易。此外,控制器不再需要知道错综复杂的情况,如视图的位置。
@RequestParam(请求参数)
@RequestParam注释用于将 Servlet 请求参数绑定到处理程序/控制器方法参数。使用类型转换将请求参数值自动转换为指定的方法参数类型。清单 2-7 展示了@RequestParam的两种用法。在第一种用法中,Spring 查找名为query的请求参数,并将其值映射到方法参数query。在第二种用法中,Spring 查找名为page的请求参数,将其值转换为整数,并将其映射到pageNumber方法参数。
@GetMapping("/search.html")
public String search(@RequestParam String query, @RequestParam("page") int pageNumber) {
model.put("currentDate", new Date());
return "home";
}
Listing 2-7RequestParam Usage
当使用@RequestParam注释方法参数时,指定的请求参数必须在客户机请求中可用。如果参数丢失,Spring 将抛出一个MissingServletRequestParameterException异常。解决这个问题的一个方法是将required属性设置为false,如清单 2-8 所示。另一个选项是使用defaultValue属性来指定一个默认值。
@GetMapping("/search.html")
public String search(@RequestParam String query, @RequestParam(value="page", required=false) int pageNumber, @RequestParam(value="size", defaultValue="10") int pageSize) {
model.put("currentDate", new Date());
return "home";
}
Listing 2-8Making a Request Parameter Not Required
@RequestMapping
正如我们在“控制器”一节中了解到的,@RequestMapping注释用于将 web 请求映射到处理程序类或处理程序方法。@RequestMapping提供了几个属性,可以用来缩小这些映射的范围。表 2-2 显示了不同的元素及其描述。
表 2-2
请求映射元素
|元素名称
|
描述
| | --- | --- | | 方法 | 将映射限制为特定的 HTTP 方法,如 GET、POST、HEAD、OPTIONS、PUT、PATCH、DELETE、TRACE | | 生产 | 将映射缩小到由该方法生成的媒体类型 | | 消耗 | 将映射缩小到方法使用者的媒体类型 | | 头球 | 将映射缩小到应该出现的标题 | | 名字 | 允许您为映射指定名称 | | 参数 | 将映射限制为所提供的参数名和值 | | 价值 | 特定处理程序方法的缩小路径(如果没有任何元素,缺省值是 main 元素) | | 小路 | 特定处理程序方法的缩小路径(值的别名) |
@RequestMapping映射的默认 HTTP 方法是 GET。使用清单 2-9 中所示的method元素可以改变这种行为。只有在执行 POST 操作时,Spring 才会调用saveUser方法。对saveUser的 GET 请求将导致抛出异常。Spring 提供了一个方便的RequestMethod枚举,列出了可用的 HTTP 方法。
@RequestMapping(value="/saveuser.html", method=RequestMethod.POST)
public String saveUser(@RequestParam String username, @RequestParam String password) {
// Save User logic
return "success";
}
Listing 2-9POST Method Example
@RequestMapping 快捷方式批注
您可以对@RequestMapping 使用“快捷方式注释”。
它看起来可读性更好,因为您可以使用“快捷方式注释”来代替@RequestMapping。
所有快捷方式批注都从@RequestMapping 继承所有元素,不使用方法,因为方法已经在批注的标题中。
比如@GetMapping 和@RequestMapping(method = RequestMethod.GET)完全一样。
表 2-3
@RequestMapping 的快捷批注
|注释
|
更换
|
| --- | --- |
| @GetMapping | @RequestMapping(method = RequestMethod.GET) |
| @PostMapping | @RequestMapping(method = RequestMethod.POST) |
| @PutMapping | @RequestMapping(method = RequestMethod.PUT) |
| @DeleteMapping | @RequestMapping(method = RequestMethod.DELETE) |
| @补丁映射 | @RequestMapping(method = RequestMethod.PATCH) |
produces元素表示映射方法产生的媒体类型,比如 JSON 或 XML 或 HTML。produces元素可以将一个或多个媒体类型作为它的值。清单 2-10 显示了添加了produces元素的search方法。MediaType.TEXT_HTML值表示当在搜索.html上执行 GET 请求时,该方法返回一个 HTML 响应。
@GetMapping(value="/search.html", produces="MediaType.TEXT_HTML")
public String search(@RequestParam String query, @RequestParam(value="page", required=false) int pageNumber) {
model.put("currentDate", new Date());
return "home";
}
Listing 2-10Produces Element Example
客户端可能在/search.html上执行 GET 请求,但是发送一个带有值application/JSON的Accept报头。在这种情况下,Spring 不会调用search方法。相反,它将返回 404 错误。produces元素提供了一种方便的方式来限制控制器可以服务的内容类型的映射。同样,consumes元素用于指示带注释的方法所使用的媒体类型。
Accept and Content-Type Header
正如在第一章中所讨论的,REST 资源可以有多种表现形式。REST 客户端通常使用Accept和Content-Type头来处理这些表示。
REST 客户端使用Accept头来表示它们接受的表示。HTTP 规范允许客户端发送不同媒体类型的优先列表,它将接受这些媒体类型作为响应。收到请求后,服务器将发送优先级最高的表示。为了理解这一点,考虑 Firefox 浏览器的默认Accept头:
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
q参数,也称为相对质量参数,表示偏好程度,取值范围从 0 到 1。从字符串中,我们可以推断出 HTML 和 XHTML 的优先级为 1,因为它们没有关联的q值。XML 媒体类型的优先级为 0.9,其余表示的优先级为 0.8。在收到这个请求时,服务器会尝试发送一个 HTML/XHTML 表示,因为它具有最高的优先级。
以类似的方式,REST 客户机使用Content-Type头来指示发送到服务器的请求的媒体类型。这允许服务器正确地解释请求并正确地解析内容。如果服务器无法解析内容,它将发送 415 不支持的媒体类型错误状态代码。
Spring Web MVC 允许对用@RequestMapping注释的方法进行灵活的签名。这包括可变的方法参数和方法返回类型。表 2-4 列出了允许的重要参数。关于允许参数的详细列表,请参考 Spring 的 Javadocs at http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html 。
表 2-4
方法参数和描述
|方法参数
|
描述
|
| --- | --- |
| HttpServletRequest/HttpServletResponse | HTTP Servlet 请求和响应对象。允许原始访问客户端数据,如请求参数和头。 |
| HttpSession | 实例,表示用户的 HTTP 会话。 |
| Command object | Spring 用用户提交的数据填充/绑定的 POJO 或模型对象。命令对象可以用@ModelAttribute注释。 |
| BindingResult | 实例,表示命令对象的验证和绑定。此参数必须紧接在命令对象之前。 |
| HttpEntity<?> | 表示 HTTP 请求的实例。每个HttpEntity由请求体和一组头组成。 |
| Principal | 一个代表经过身份验证的用户的java.security.Principal实例。 |
表 2-5 显示了用@RequestMapping标注的方法支持的不同返回类型。
表 2-5
退货类型和说明
|返回类型
|
描述
|
| --- | --- |
| String | 表示逻辑视图名称。使用注册的视图解析器来解析物理视图,并生成响应。 |
| View | 表示视图的实例。在这种情况下,不执行视图解析,视图对象负责生成响应。例子包括JstlView、VelocityView、RedirectView等等。 |
| HttpEntity<?> | 表示 HTTP 响应的实例。每个HttpEntity由响应体和一组头组成。 |
| HttpHeaders | 实例捕获要返回的标头。响应将有一个空体。 |
| Pojo | 被认为是模型属性的 Java 对象。专门的RequestToViewNameTranslator用于确定适当的逻辑视图名称。 |
路径变量
@RequestMapping注释通过 URI 模板支持动态 URIs。如第一章所述,URI 模板是带有占位符或变量的 URIs。@PathVariable注释允许您通过方法参数访问和使用这些占位符。清单 2-11 给出了一个@PathVariable的例子。在这个场景中,getUser方法被设计为提供与路径变量{username}相关的用户信息。客户端将在 URL /users/jdoe上执行 GET,以检索与用户名jdoe相关联的用户信息。
@RequestMapping("/users/{username}")
public User getUser(@PathVariable("username") String username) {
User user = null;
// Code to construct user object using username
return user;
}
Listing 2-11PathVariable Example
解析视图
如前所述,Spring Web MVC 控制器可以返回一个org.springframework.web.servlet.View实例或一个逻辑视图名。当返回一个逻辑视图名时,使用一个ViewResolver将视图解析为一个View实现。如果这个过程由于某种原因失败,就会抛出一个javax.servlet.ServletException。ViewResolver接口只有一个方法,如清单 2-12 所示。
public interface ViewResolver
{
View resolveViewName(String viewName, Locale locale) throws Exception;
}
Listing 2-12ViewResolver Interface
表 2-6 列出了 Spring Web MVC 提供的一些ViewResolver实现。
你可能已经注意到,表 2-6 中不同的视图解析器模拟了我们之前看到的不同类型的视图。清单 2-13 显示了创建InternalViewResolver所需的代码。
同样,清单 2-13 显示了@Bean 注释——简而言之,在由@Configuration 注释的类中,由@Bean 定义的所有方法都将返回对象,这由 Spring Framework 控制,在帮助下,我们可以为一些对象定义行为,并在需要时使用@Inject 或@Autowired 注释调用任何地方的对象。默认情况下,Spring Framework 创建的每个对象都将被定义为一个@Bean。
表 2-6
ViewResolver 实现和描述
|返回类型
|
描述
|
| --- | --- |
| BeanNameViewResolver | ViewResolver实现,查找 id 与ApplicationContext中的逻辑视图名称相匹配的 bean。如果它在ApplicationContext中没有找到 bean,则返回一个null。 |
| InternalResourceViewResolver | 寻找具有逻辑视图名称的内部资源。内部资源的位置通常是通过在逻辑名称前加前缀和后缀路径和扩展名信息来计算的。 |
| ContentNegotiatingViewResolver | 将视图解析委托给其他视图解析器。视图解析器的选择基于所请求的媒体类型,而媒体类型本身是使用 Accept 头、文件扩展名或 URL 参数来确定的。 |
| TilesViewResolver | ViewResolver在瓦片配置中寻找与逻辑视图名称匹配的模板。 |
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
Listing 2-13InternalViewResolver Example
异常处理程序
异常是任何应用的一部分,Spring 提供了HandlerExceptionResolver机制来处理那些意外的异常。HandlerExceptionResolver抽象类似于ViewResolver,用于解决错误视图的异常。清单 2-14 显示了HandlerExceptionResolver API。
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex);
}
Listing 2-14HandlerExceptionResolver API
Spring 提供了几个现成的HandlerExceptionResolver实现,如表 2-7 所示。
表 2-7
HandlerExceptionResolver 实现和描述
|解析器实现
|
描述
|
| --- | --- |
| org.springframework.web.servlet.handler.SimpleMappingExceptionResolver | 将异常类名映射到视图名的异常解析器实现。 |
| org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver | 异常解析器实现,将标准的 Spring 异常转换为 HTTP 状态代码。 |
| org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver | Spring 应用中的自定义异常可以用@ResponseStatus进行注释,它将一个 HTTP 状态代码作为其值。这个异常解析器将异常转换为其映射的 HTTP 状态代码。 |
| org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver | 使用带注释的@ExceptionHandler方法解决异常的异常解决器实现。 |
SimpleMappingExceptionResolver已经存在了很长时间。Spring 3 引入了一种使用@ExceptionHandler策略处理异常的新方法。这为处理基于 REST 的服务中的错误提供了一种机制,在这种情况下,实际上没有视图可显示,而是返回数据。清单 2-15 显示了一个带有异常处理程序的控制器。任何现在在HomeController中抛出一个SQLException的方法都将在handleSQLException方法中得到处理。handleSQLException只是创建一个ResponseEntity实例并返回它。但是,可以执行额外的操作,如日志记录、返回额外的诊断数据等。
@Controller
public class HomeController {
@ExceptionHandler(SQLException.class)
public ResponseEntity handleSQLException() {
ResponseEntity responseEntity = new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
return responseEntity;
}
@GetMapping("/stream")
public void streamMovie(HttpServletResponse response) throws SQLException {
}
}
Listing 2-15ExceptionHandler Example
带注释的方法只能处理发生在控制器或其子类中的异常。因此,如果我们需要在其他控制器中处理 SQL 异常,那么我们需要在所有这些控制器中复制并粘贴handleSQLException方法。这种方法有严重的局限性,因为异常处理确实是一个横切关注点,应该是集中的。
为了解决这个问题,Spring 提供了@ControllerAdvice注释。用@ControllerAdvice标注的类中的方法应用于所有的@RequestMapping方法。清单 2-16 显示了使用handleSQLException方法的GlobalExceptionHandler。正如您所看到的,GlobalExceptionHandler扩展了 Spring 的ResponseEntityExceptionHandler,它将默认的 Spring Web MVC 异常转换为带有 HTTP 状态代码的ResponseEntity。
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(SQLException.class)
public ResponseEntity handleSQLException() {
ResponseEntity responseEntity = new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
return responseEntity;
}
}
Listing 2-16GlobalExceptionHandler Example
截击机
Spring Web MVC 提供了拦截器的概念来实现横切不同处理程序的关注点。考虑这样一个场景,您希望阻止对一组控制器的未经身份验证的访问。拦截器允许您集中这种访问逻辑,而不必在每个控制器中复制和粘贴代码。顾名思义,拦截器拦截一个请求;他们这样做是基于以下三点:
-
在控制器被执行之前。这允许拦截器决定它是需要继续执行链还是返回一个异常或自定义响应。
-
在控制器执行之后,但在响应发出之前。这允许拦截器向视图提供任何额外的模型对象。
-
在响应发出后,允许任何资源清理。
Note
Spring Web MVC 拦截器类似于 HTTP servlet 过滤器。两者都可以用来拦截请求和执行常见的问题。但是,它们之间有一些值得注意的差异。过滤器能够包装甚至交换HttpServletRequest和HttpServletResponse对象。拦截器不能修饰或交换那些对象。拦截器是 Spring 管理的 bean,我们可以很容易地在其中注入其他 spring beans。筛选器是容器管理的实例;它们没有提供注入 Spring 管理的 beans 的直接机制。
Spring Web MVC 为实现拦截器提供了HandlerInterceptor接口。清单 2-17 给出了HandlerInterceptor接口。如您所见,这三个方法对应于我们刚刚讨论的三个拦截器特性。
public interface HandlerInterceptor{
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView);
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler);
}
Listing 2-17HandlerInterceptor API
清单 2-18 给出了一个简单的拦截器实现。如您所见,SimpleInterceptor类扩展了HandlerInterceptorAdapter。HandlerInterceptorAdapter是一个方便的抽象类,它实现了HandlerInterceptor接口并提供了其方法的默认实现。
public class SimpleInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = Logger.getLogger(SimpleInterceptor.class);
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("Inside the prehandle");
return false;
}
}
Listing 2-18Spring Web MVC Interceptor Example
拦截器可以使用InterceptorRegistry策略在 Spring Web 应用中注册。当使用 Java 配置时,这通常是通过创建一个扩展WebMvcConfigurerAdapter的配置类来实现的。Spring Web MVC 的WebMvcConfigurerAdapter类提供了用于访问InterceptorRegistry的addInterceptors方法。清单 2-19 显示了注册两个拦截器的代码:LocalInterceptor,它与 Spring 和我们的SimpleInterceptor一起出现。
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.springrest.web" })
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
registry.addInterceptor(new SimpleInterceptor()).addPathPatterns("/auth/**");
}
}
Listing 2-19Example Registering Interceptors
当拦截器被添加到拦截器注册中心时,拦截器被应用到所有的处理程序映射。因此,清单 2-19 中的LocaleChangeInterceptor应用于所有的处理程序映射。然而,也可以将拦截器限制在某些 URL 上。这在清单 2-19 中使用addPathPatterns方法进行了演示。这里我们指出SimpleInterceptor应该只应用于在auth路径下的 URL。
摘要
在这一章中,我们已经了解了 Spring 框架的基础和 Spring Web MVC 的不同组件。在下一章,我们将把所有的东西放在一起,看看如何使用 Spring Boot 构建我们的第一个 RESTful 应用。
Footnotes 1martinfowler.com/articles/injection.html
2
www.oracle.com/technetwork/java/frontcontroller-135648.html
三、REST Spring
在本章中,我们将讨论以下内容:
-
Spring Boot 的基本情况
-
构建 Hello World REST 应用
-
访问 REST 应用的工具
Spring 框架的目标之一是减少管道代码,以便开发人员可以集中精力实现核心业务逻辑。然而,随着 Spring 框架的发展,并向其投资组合中添加了几个子项目,开发人员最终花费了相当多的时间来设置项目、寻找项目依赖关系以及编写模板代码和配置。
Spring Boot 是一个 Spring portfolio 项目,旨在通过提供一组 starter 项目模板来简化 Spring 应用的引导。这些将根据项目能力提取所有需要的适当的依赖关系。例如,如果您启用 JPA 功能,它会自动包含所有相关的 JPA、Hibernate 和 Spring JAR 文件。
Spring Boot 还采用了一种固执己见的方法,并提供了默认配置,这大大简化了应用的开发。例如,如果 Spring Boot 在类路径中找到 JPA 和 MySQL JARs,它会自动配置一个 JPA 持久性单元。它还支持使用嵌入式 Jetty/Tomcat 服务器创建独立的 Spring 应用,使它们易于部署在任何只安装了 Java 的机器上。此外,它还提供了生产就绪功能,如指标和运行状况检查。通过这本书,我们将探索和学习 Spring Boot 的这些和其他特点。
Note
Spring Roo 是另一个 Spring portfolio 项目,试图提供快速的 Spring 应用开发。它提供了一个命令行工具,支持简单的项目引导,并为 JPA 实体、web 控制器、测试脚本和必要的配置等组件生成代码。尽管最初对这个项目有很多兴趣,但 Spring Roo 从未真正成为主流。AspectJ 代码生成和陡峭的学习曲线,再加上它试图接管你的项目,是它没有被采用的一些原因。相比之下,Spring Boot 采取了不同的方法;它侧重于启动项目并提供聪明、合理的默认配置。Spring Boot 不会生成任何代码来简化项目管理。
生成 Spring Boot 项目
从头开始创建 Spring Boot 项目是可能的。但是,Spring Boot 提供了以下选项来生成新项目:
-
使用 Spring Boot 的入门网站(
http://start.spring.io)。 -
使用 Spring 工具套件(STS) IDE。
-
使用引导命令行界面(CLI)。
我们将在本章探讨所有三种选择。然而,对于本书的其余部分,我们将选择 Boot CLI 来生成新项目。在我们开始项目生成之前,在您的机器上安装 Java 是很重要的。Spring Boot 要求您安装 Java SDK 1.8 或更高版本。在本书中,我们将使用 Java 1.8。
安装构建工具
Spring Boot 支持两种最流行的构建系统:Maven 和 Gradle。在本书中,我们将使用 Maven 作为我们的构建工具。Spring Boot 需要 Maven 版本 3.5 或更高版本。这里给出了在 Windows 机器上下载和配置 Maven 的步骤。Mac 和其他操作系统的类似说明可以在 Maven 的安装页面上找到( https://maven.apache.org/install.html ):
-
从
https://maven.apache.org/download.cgi下载最新的 Maven 二进制。在写这本书的时候,Maven 的当前版本是 3.8.1。对于 Windows,下载apache-maven-3.8.1-bin.zip文件。 -
解压
C:\tools\maven下 zip 文件的内容。 -
添加一个值为
C:\tools\maven\apache-maven-3.8.1的环境变量M2_HOME。这告诉 Maven 和其他工具 Maven 安装在哪里。还要确保JAVA_HOME变量指向已安装的 JDK。 -
将值
%M2_HOME%\bin附加到Path环境变量中。这允许您从命令行运行 Maven 命令。 -
打开一个新命令行,键入以下内容:
mvn - v
您应该会看到类似图 3-1 的输出,表明 Maven 已经成功安装。
图 3-1
Maven 安装验证
Note
要了解更多关于 Maven 的信息,请参考 Apress ( http://www.apress.com/9781484208427 )出版的介绍 Maven 。
使用 start.spring.io 生成项目
Spring Boot 在 http://start.spring.io 托管一个初始化应用。Initializr 提供了一个 web 界面,允许您输入项目信息并选择项目所需的功能,瞧——它将项目生成为一个 zip 文件。按照以下步骤生成我们的 Hello World REST 应用:
图 3-2
start.spring.io 网站
-
在浏览器中启动
http://start.spring.io网站,输入如图 3-2 所示的信息。 -
在 Dependencies ➤ Web 下,选择选项“Web ”,并指明您希望 Spring Boot 包含 Web 项目基础结构和依赖项。
-
然后点击“生成项目”按钮。这将开始下载
hello-rest.zip文件。
下载完成后,解压缩 zip 文件的内容。您将看到生成的hello-rest文件夹。图 3-3 显示生成的文件夹内容。
图 3-3
hello-rest 应用内容
快速浏览一下hello-rest的内容,可以看到我们有一个标准的基于 Maven 的 Java 项目布局。我们有src\main\java文件夹,存放 Java 源代码;src\main\resources,其中包含属性文件;静态内容,如 HTML\CSS\JS 文件;和包含测试用例的src\test\java文件夹。在运行 Maven 构建时,这个项目会生成一个 JAR 工件。现在,对于第一次使用 WAR 工件来部署 web 应用的人来说,这可能有点令人困惑。默认情况下,Spring Boot 创建独立的应用,其中所有的东西都打包到一个 JAR 文件中。这些应用将嵌入诸如 Tomcat 之类的 servlet 容器,并使用一种古老的main()方法来执行。
Note
Spring Boot 还允许您使用 WAR 工件,其中包含 html、css、js 和其他开发 web 应用所需的文件,这些应用可以部署到外部 Web 和应用容器中。
清单 3-1 给出了hello-rest应用的pom.xml文件的内容。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/>
</parent>
<groupId>com.appress</groupId>
<artifactId>hello-rest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Hello World REST</name>
<description>Hello World REST Application Using Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
</build>
</project>
Listing 3-1hello-rest pom.xml file Contents
pom.xml文件中的groupId、artifactId和version元素对应于 Maven 描述我们项目的标准 GAV 坐标。parent标签表明我们将从spring-boot-starter-parent POM 继承。这确保了我们的项目继承了 Spring Boot 的默认依赖项和版本。元素列出了两个 POM 文件依赖关系:spring-boot-starter-web和spring-boot-starter-test。Spring Boot 使用术语starter POM来描述这样的 POM 文件。
这些 starter POMs 用于提取其他依赖项,实际上不包含任何自己的代码。例如,spring-boot-starter-web提取 Spring MVC 依赖项、Tomcat 嵌入式容器依赖项和用于 JSON 处理的 Jackson 依赖项。这些 starter 模块在提供所需的依赖项和将应用的 POM 文件简化为几行代码方面起着重要的作用。表 3-1 列出了一些常用的启动器模块。
表 3-1
Spring Boot 起动机模块
|Starter POM 依赖项
|
使用
|
| --- | --- |
| spring-boot-starter | 入门产品,引入了自动配置支持和日志记录等功能所必需的核心依赖项 |
| spring-boot-starter-aop | 引入面向方面编程和 AspectJ 支持的入门工具 |
| spring-boot-starter-test | Starter 引入了测试所需的依赖项,如 JUnit、Mockito 和spring-test |
| spring-boot-starter-web | 引入 MVC 依赖关系(spring-webmvc)和嵌入式 servlet 容器支持的启动程序 |
| spring-boot-starter-data-jpa | Starter 通过引入spring-data-jpa、spring-orm,和 Hibernate 依赖项来增加 Java 持久性 API 支持 |
| spring-boot-starter-data-rest | 引入spring-data-rest-webmvc将存储库公开为 REST API 的启动程序 |
| spring-boot-starter-hateoas | 为 HATEOAS REST 服务带来spring-hateoas依赖性的启动器 |
| spring-boot-starter-jdbc | 支持 JDBC 数据库的入门产品 |
最后,spring-boot-maven-plugin包含将应用打包成可执行的 JAR/WAR 并运行它的目标。
HelloWorldRestApplication.java类是我们应用的主类,包含了main()方法。清单 3-2 显示了HelloWorldRestApplication.java类的内容。@SpringBootApplication注释是一个方便的注释,相当于声明以下三个注释:
-
@Configuration—将带注释的类标记为包含一个或多个 Spring bean 声明。Spring 处理这些类来创建 bean 定义和实例。 -
@ComponentScan—这个类告诉 Spring 扫描并寻找用@Component, @Service, @Repository, @Controller, @RestController, and @Configuration标注的类。默认情况下,Spring 会扫描包中@ComponentScan注释类所在的所有类。为了覆盖默认行为,我们可以在 configuration 类中设置这个注释,并将 basePackages 参数定义为包的名称。 -
@EnableAutoConfiguration—启用 Spring Boot 的自动配置行为。基于在类路径中找到的依赖项和配置,Spring Boot 智能地猜测并创建 bean 配置。
典型的 Spring Boot 应用总是使用这三种注释。除了在这些场景中提供一个很好的选择外,@SpringBootApplication注释正确地表明了类的意图。
package com.apress.hellorest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HelloWorldRestApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldRestApplication.class, args);
}
}
Listing 3-2HelloWorldRestApplication Contents
main()方法只是将应用引导委托给SpringApplication的run()方法。run()将一个HelloWorldRestApplication.class作为它的参数,并指示 Spring 从HelloWorldRestApplication读取注释元数据,并从中填充ApplicationContext。
现在我们已经查看了生成的项目,让我们创建一个 REST 端点,它只返回“Hello REST”理想情况下,我们应该在一个单独的控制器 Java 类中创建这个端点。然而,为了简单起见,我们将在HelloWorldRestApplication中创建端点,如清单 3-3 所示。我们从添加@RestController开始,表示HelloWorldRestApplication有可能的 REST 端点。然后我们创建了helloGreeting()方法,它简单地返回问候“Hello REST”最后,我们使用RequestMapping注释将对“/greet”路径的 web 请求映射到helloGreeting()处理程序方法。
package com.apress.hellorest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@SpringBootApplication
@RestController
public class HelloWorldRestApplication {
public static void main(String args) {
SpringApplication.run(HelloWorldRestApplication.class, args);
}
@GetMapping("/greet")
public String helloGreeting() {
return "Hello REST";
}
}
Listing 3-3Hello REST Endpoint
下一步是启动并运行我们的应用。为此,打开命令行,导航到hello-rest文件夹,并运行以下命令:
mvn spring-boot:run
您将看到 Maven 下载必要的插件和依赖项,然后它将启动应用,如下所示:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
[32m :: Spring Boot :: [39m [2m (v2.5.3)[0;39m
[2m2021-08-12 21:54:43.147[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main][0;39m [36mc.a.hellorest.HelloWorldRestApplication [0;39m [2m:[0;39m Starting HelloWorldRestApplication using Java 1.8 on DESKTOP-82GK4GP with PID 15012 (C:\Users\makus\OneDrive\Desktop\hello-rest\target\classes started by makus in C:\Users\makus\OneDrive\Desktop\hello-rest)
[2m2021-08-12 21:54:43.149[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main][0;39m [36mc.a.hellorest.HelloWorldRestApplication [0;39m [2m:[0;39m No active profile set, falling back to default profiles: default
[2m2021-08-12 21:54:43.843[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main][0;39m [36mo.s.b.w.embedded.tomcat.TomcatWebServer [0;39m [2m:[0;39m Tomcat initialized with port(s): 8080 (http)
[2m2021-08-12 21:54:43.851[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main][0;39m [36mo.apache.catalina.core.StandardService [0;39m [2m:[0;39m Starting service [Tomcat]
[2m2021-08-12 21:54:43.851[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main][0;39m [36morg.apache.catalina.core.StandardEngine [0;39m [2m:[0;39m Starting Servlet engine: [Apache Tomcat/9.0.50]
[2m2021-08-12 21:54:43.917[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main][0;39m [36mo.a.c.c.C.[Tomcat].[localhost]. [0;39m [2m:[0;39m Initializing Spring embedded WebApplicationContext
[2m2021-08-12 21:54:43.917[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main][0;39m [36mw.s.c.ServletWebServerApplicationContext[0;39m [2m:[0;39m Root WebApplicationContext: initialization completed in 734 ms
[2m2021-08-12 21:54:44.286[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main][0;39m [36mo.s.b.w.embedded.tomcat.TomcatWebServer [0;39m [2m:[0;39m Tomcat started on port(s): 8080 (http) with context path ''
[2m2021-08-12 21:54:44.297[0;39m [32m INFO[0;39m [35m15012[0;39m [2m---[0;39m [2m[ main]0;39m [36mc.a.hellorest.HelloWorldRestApplication [0;39m [2m:[0;39m Started HelloWorldRestApplication in 1.445 seconds (JVM running for 2.055)
为了测试我们正在运行的应用,启动浏览器并导航到http://localhost:8080/greet。注意,Spring Boot 作为Root上下文而不是hello-world上下文启动应用。你应该会看到一个类似于图 [3-4 的屏幕。
图 3-4
你好休息问候
Spring Initializr
位于 http://start.spring.io 的 Spring Initializr 应用本身就是使用 Spring Boot 构建的。你可以在 GitHub 的 https://github.com/spring-io/initializr 找到这个应用的源代码。您也可以构建并托管自己的 Initializr 应用实例。
除了提供 web 接口之外,Initializr 还提供了一个 HTTP 端点,该端点提供了类似的项目生成功能。事实上,Spring Boot 的 CLI 和诸如 STS 之类的 ide 在幕后使用这个 HTTP 端点来生成项目。
也可以使用 curl 从命令行调用 HTTP 端点。例如,下面的命令将使用 curl 生成hello-rest项目 zip 文件。–d选项用于提供作为请求参数传递的数据:
curl https://start.spring.io/starter.zip -d style=web -d name=hello-rest
使用 STS 生成项目
Spring Tool Suite 或 STS 是一个免费的基于 Eclipse 的开发环境,它为开发基于 Spring 的应用提供了强大的工具支持。您可以从 Pivotal 的网站 https://spring.io/tools 下载并安装最新版本的 STS。在写这本书的时候,STS 的当前版本是 4.11.0。
STS 提供了一个类似于 Initializr 的 web 界面的用户界面,用于生成 Boot starter 项目。以下是生成 Spring Boot 项目的步骤:
图 3-5
STS 春季启动项目
- 如果还没有启动 STS,请启动 STS。进入文件➤新建,点击 Spring Starter 项目,如图 3-5 所示。
图 3-6
起始项目选项
- 在以下屏幕中,输入如图 3-6 所示的信息。输入 Maven 的 GAV 信息。点击下一个。
图 3-7
起始项目选项
- 在以下屏幕中,输入如图 3-7 所示的信息。选择 web starter 选项。点击下一个。
图 3-8
起始项目位置
- 在接下来的屏幕上,更改您想要存储项目的位置。“完整 Url”区域显示 HTTP REST 端点以及您选择的选项(参见图 3-8 )。
图 3-9
STS Spring starter 项目资源
- 点击 Finish 按钮,您将看到在 STS 中创建了新的项目。项目的内容类似于我们之前创建的项目(参见图 3-9 )。
STS 的 starter 项目向导提供了一种生成新的 Spring Boot 项目的便捷方式。新创建的项目会自动导入到 IDE 中,并且可以立即用于开发。
使用 CLI 生成项目
Spring Boot 提供了一个命令行界面(CLI ),用于生成项目、构建原型和运行 Groovy 脚本。在开始使用 CLI 之前,我们需要安装它。以下是在 Windows 计算机上安装引导 CLI 的步骤:
图 3-10
Spring Boot CLI 目录
-
从 Spring 的网站
https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started.installing.cli下载最新版本的 CLI ZIP 发行版。在写这本书的时候,CLI 的当前版本是 2.5.3。这个版本可以直接从https://repo.spring.io/release/org/springframework/boot/spring-boot-cli/2.5.3/spring-boot-cli-2.5.3-bin.zip下载。 -
解压 zip 文件,将其内容(文件夹如
bin、lib)放在C:\tools\springbootcli下,如图 3-10 所示。 -
添加一个值为
c:\tools\springbootcli的新环境变量SPRING_HOME。 -
编辑
Path环境变量,并在其末尾添加%SPRING_HOME%/bin值。 -
打开一个新命令行,运行以下命令验证安装:
spring --version
您应该会看到类似于图 3-10 所示的输出。
图 3-11
Spring Boot CLI 安装
现在我们已经安装了 Boot CLI,生成新项目只需在命令行运行以下命令:
spring init --dependencies web rest-cli
该命令创建一个新的具有 web 功能的rest-cli项目。运行该命令的输出如清单 3-4 所示。
C:\test>spring init --dependencies web rest-cli
Using service at https://start.spring.io
Project extracted to 'C:\test\rest-cli'
Listing 3-4Boot CLI Output
访问 REST 应用
有几个免费的和商业的工具允许您访问和试验 REST API/应用。在这一节中,我们将看看一些流行的工具,它们允许您快速测试请求并检查响应。
邮递员
Postman 是一个 Chrome 浏览器扩展,用于发出 HTTP 请求。它提供了大量的特性,使得开发、测试和记录 REST API 变得容易。还提供了一个 Chrome 应用版本的 Postman,它提供了浏览器扩展中不提供的额外功能,如批量上传。
可以从 Chrome 网上商店下载并安装 Postman。要安装 Postman,只需启动 Chrome 浏览器并导航至 https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop 。你可能会被要求登录你的谷歌浏览器账户,并使用“新应用”安装对话框进行确认。安装完成后,您应该能够使用书签栏中的“应用图标”或通过键入chrome://apps/shortcut来定位并启动 Postman。图 3-10 显示了在 Chrome 浏览器中启动的 Postman。
Postman 提供了一个简洁直观的用户界面,用于编写 HTTP 请求、将其发送到服务器以及查看 HTTP 响应。它还会自动保存请求,以便将来运行时使用。图 3-12 显示了向我们的问候服务发出的 HTTP GET 请求及其响应。您还可以在左侧边栏的 History 部分看到保存的请求。
图 3-12
邮递员浏览器扩展
Postman 很容易将相关的 API 调用逻辑分组到集合中,如图 3-12 所示。一个集合下可能有请求的子集合。
图 3-13
邮递员收藏
剩馀客户端
RESTClient 是一个 Firefox 扩展,用于访问 REST APIs 和应用。与 Postman 不同,RESTClient 没有太多花哨的功能,但它提供了快速测试 REST API 的基本功能。要安装 RESTClient,启动 Firefox 浏览器并导航到 URL https://addons.mozilla.org/en-US/firefox/addon/restclient/ 。然后点击“+添加到 Firefox”按钮,在下面的“软件安装”对话框中,点击“立即安装”按钮。
安装完成后,您可以使用浏览器右上角的 RESTClient 图标启动 RESTClient。图 3-14 显示了 RESTClient 应用对我们的 Greet 服务的请求以及相应的响应。
图 3-14
剩馀客户端
摘要
Spring Boot 提供了一种自以为是的方法来构建基于 Spring 的应用。在这一章中,我们研究了 Spring Boot 的特性,并用它构建了一个 Hello World REST 应用。我们还研究了用于测试和探索 REST API 的 Postman 和 RESTClient 工具。
在下一章,我们将开始开发一个更复杂的 REST 应用,并讨论识别和设计资源的过程。
四、开始实现快速投票应用
在本章中,我们将讨论以下内容:
-
分析快速投票的要求
-
识别快速投票资源
-
设计展示
-
实施快速投票
到目前为止,我们已经了解了 REST 的基础知识,并回顾了我们的技术实现选择——Spring MVC。现在是时候开发一个更复杂的应用了。在这一章中,我们将向你介绍一个应用的开端,我们将在整本书中使用它。我们称之为快速投票。我们将经历分析需求、识别资源、设计它们的表示,以及最后提供一个特性子集的实现的过程。在接下来的章节中,我们将通过添加新的特性、文档、安全性和版本来继续我们的设计和实现。
快速投票简介
如今,在许多网站上,民意调查已经成为征求社区观点和意见的一个流行选项。在线调查之间有一些差异,但是一个调查通常有一个问题和一个答案列表,如图 4-1 所示。
图 4-1
Web 投票示例
参与者通过选择一个或多个答案来投票并表达他们的观点。很多民调还允许参与者查看民调结果,如图 4-2 所示。
图 4-2
网络投票结果
想象一下成为 QuickPoll Inc .的一部分,这是一家新兴的软件即服务(或 SaaS)提供商,允许用户创建、操作和投票。我们计划向一小部分受众推出我们的服务,但我们打算成为一家全球性企业。除了网络之外,QuickPoll 还希望瞄准本地 iOS 和 Android 平台。为了实现这些崇高的目标,我们选择使用 REST 原则和 web 技术来实现我们的应用。
我们通过分析和理解需求来开始开发过程。我们的 QuickPoll 应用有以下要求:
-
用户与 QuickPoll 服务交互以创建新的投票。
-
每个投票都包含一组在投票创建过程中提供的选项。
-
投票中的选项可以在以后更新。
-
为了简单起见,QuickPoll 限制了对单一选项的投票。
-
参与者可以投任意数量的票。
-
任何人都可以查看投票结果。
我们从快速投票的一组简单要求开始。与任何其他应用一样,这些需求会不断发展和变化。我们将在接下来的章节中讨论这些变化。
设计快速投票
正如第一章所讨论的,设计一个 RESTful 应用通常包括以下步骤:
-
资源标识
-
资源表示
-
端点标识
-
动词/动作识别
资源标识
我们通过分析需求和提取名词开始资源识别过程。在高层次上,QuickPoll 应用有用户,他们创建投票并与之交互。从前面的语句中,您可以将 User 和 Poll 标识为名词,并将它们归类为资源。类似地,用户可以对投票进行投票并查看投票结果,从而使投票成为另一种资源。这个资源建模过程类似于数据库建模,因为它用于标识实体或标识域对象的面向对象的设计。
重要的是要记住,并非所有被识别的名词都需要作为资源公开。例如,一个投票包含几个选项,使选项成为另一个候选资源。使轮询选项成为一个资源需要一个客户机发出两个 GET 请求。第一个请求将获得一个投票表示;第二个请求将获得相关的选项表示。然而,这种方法使 API 变得冗长,并可能使服务器过载。另一种方法是在投票表示中包含选项,从而将选项作为资源隐藏起来。这将使 poll 成为粗粒度的资源,但是客户机将在一次调用中获得与 Poll 相关的数据。此外,第二种方法可以实施业务规则,例如要求至少有两个选项才能创建投票。
这种名词的方法让我们能够识别馆藏资源。现在,考虑这样一个场景,您想要检索给定投票的所有投票。要处理这个问题,您需要一个“投票”集合资源。您可以执行 GET 请求并获取整个集合。类似地,我们需要一个“投票”集合资源,它允许我们查询投票组并创建新的投票组。
最后,我们需要处理这样一个场景,在这个场景中,我们对一次投票的所有投票进行计数,并将计算结果返回给客户端。这包括循环一次投票的所有投票,根据选项对这些投票进行分组,然后对它们进行计数。这种处理操作通常使用“控制器”资源来实现,我们在第一章中介绍过。在这种情况下,我们为执行计数操作的计算机结果资源建模。表 4-1 显示了已识别的资源及其集合资源对应项。
表 4-1
快速投票应用的资源
|资源
|
描述
| | --- | --- | | 用户 | 单一用户资源 | | 用户 | 集合用户资源 | | 投票 | 单一轮询资源 | | 民意调查 | 收集轮询资源 | | 投票 | 单一投票资源 | | 投票 | 集合投票资源 | | 计算机结果 | 计数处理资源 |
资源表示
REST API 设计过程的下一步是定义资源表示和表示格式。REST APIs 通常支持多种格式,比如 HTML、JSON 和 XML。格式的选择很大程度上取决于 API 的受众。例如,公司内部的 REST 服务可能只支持 JSON 格式,而公共 REST API 可能支持 XML 和 JSON 格式。在本章和本书的其余部分,JSON 将是我们操作的首选格式。
JSON Format
JavaScript Object Notation(JSON)是一种用于交换信息的轻量级格式。JSON 中的信息是围绕两种结构组织的:对象和数组。
JSON 对象是名称/值对的集合。每个名称/值对由双引号中的字段名、冒号(:)和字段值组成。JSON 支持几种类型的值,如布尔值(true 或 false)、数字(int 或 float)、字符串、null、数组和对象。名称/值对的示例包括
"country" : "US"
"age" : 31
"isRequired" : true
"email" : null
JSON 对象用大括号({})括起来,每个名称/值对用逗号(,)分隔。下面是一个 person JSON 对象的例子:
{ "firstName": "John", "lastName": "Doe", "age" : 26, "active" : true }
另一个 JSON 结构是数组,是值的有序集合。每个数组都用方括号([ ])括起来,值之间用逗号分隔。以下是位置数组的示例:
[ "Salt Lake City", "New York", "Las Vegas", "Dallas"]
JSON 数组也可以包含对象作为它们的值:
[
{ "firstName": "Jojn", "lastName": "Doe", "age": 26, "active": true },
{ "firstName": "Jane", "lastName": "Doe", "age": 22, "active": true },
{ "firstName": "Jonnie", "lastName": "Doe", "age": 30, "active": false }
]
资源由一组属性组成,可以使用类似于面向对象设计的过程来识别这些属性。例如,投票资源有一个包含投票问题的问题属性和一个唯一标识投票的 id 属性。它还包含一组选项;每个选项都由一个值和一个 id 组成。清单 4-1 显示了一个带有样本数据的投票的表示。
{
"id": 2,
"question": "How will win SuperBowl this year?",
"options": [{"id": 45, "value": "New England Patriots"}, {"id": 49,
"value": "Seattle Seahawks"}, {"id": 51, "value": "Green Bay Packers"},
{"id": 54, "value": "Denver Broncos"}]
}
Listing 4-1Poll Representation
Note
在本章中,我们有意将用户排除在投票代表之外。在第八章中,我们将讨论用户表示及其与投票和投票资源的关联。
轮询集合资源的表示包含单个轮询的集合。清单 4-2 给出了带有虚拟数据的轮询收集资源的表示。
[
{
"id": 5,
"question": "q1",
"options": [
{"id": 6, "value": "X"}, {"id": 9, "value": "Y"},
{"id": 10, "value": "Z"}]
},
{
"id": 2,
"question": "q10",
"options": [{"id": 15, "value": "Yes"}, {"id": 16, "value": "No"}]
}
.......
]
Listing 4-2List of Polls Representation
投票资源包含投票的选项和唯一标识符。清单 4-3 显示了带有虚拟数据的投票资源表示。
{
"id": 245,
"option": {"id": 45, "value": "New England Patriots"}
}
Listing 4-3Vote Representation
清单 4-4 给出了带有虚拟数据的投票集合资源表示。
{
"id": 245,
"option": {"id": 5, "value": "X"}
},
{
"id": 110,
"option": {"id": 7, "value": "Y"}
},
............
Listing 4-4List of Votes Representation
ComputeResult 资源表示应该包括投票和投票选项的总数,以及与每个选项相关联的投票计数。清单 [4-5 用样本数据展示了这种表示。我们使用 totalVotes 属性保存投票,使用 results 属性保存选项 id 和相关的投票。
{
totalVotes: 100,
"results" : [
{ "id" : 1, "count" : 10 },
{ "id" : 2, "count" : 8 },
{ "id" : 3, "count" : 6 },
{ "id" : 4, "count" : 4 }
]
}
Listing 4-5ComputeResult Representation
既然我们已经定义了我们的资源表示,我们将继续为这些资源识别端点。
端点标识
REST 资源使用 URI 端点来标识。设计良好的 REST APIs 应该具有易于理解、直观且易于使用的端点。请记住,我们构建 REST APIs 是为了让消费者使用。因此,我们为端点选择的名称和层次结构对消费者来说应该是明确的。
我们使用行业中广泛使用的最佳实践和惯例为我们的服务设计端点。第一个约定是为我们的 REST 服务使用一个基本 URI。基本 URI 提供了访问 REST API 的入口点。公共 REST API 提供者通常使用子域如 http://api.domain.com 或 http://dev.domain.com 作为他们的基本 URI。流行的例子有 GitHub 的 https://api.github.com 和 Twitter 的 https://api.twitter.com 。通过创建单独的子域,您可以防止与网页发生任何可能的域名冲突。它还允许您实施不同于常规网站的安全策略。为了简单起见,在本书中我们将使用http://localhost:8080作为我们的基地 URI。
第二个约定是使用复数名词命名资源端点。在我们的 QuickPoll 应用中,这将导致一个端点http://localhost:8080/polls用于访问轮询收集资源。将使用诸如http://localhost:8080/polls/1234和http://localhost:8080/polls/3456之类的 URI 来访问各个投票资源。我们可以使用 URI 模板http://localhost:8080/polls/{pollId}来概括对个人投票资源的访问。类似地,端点http://localhost:8080/users和http://localhost:8080/users/{userId}用于访问集合和个人用户资源。
第三个约定建议使用 URI 层次结构来表示彼此相关的资源。在我们的 QuickPoll 应用中,每个投票资源都与一个投票资源相关。因为我们通常为投票投票,所以建议使用分层端点http://localhost:8080/polls/{pollId}/votes来获取或操作与给定投票相关的所有投票。同样,端点http://localhost:8080/polls/{pollId}/votes/{voteId}将返回投票的个人投票。
最后,端点http://localhost:8080/computeresult可以用来访问 ComputeResult 资源。为了使该资源正常工作并计算投票数,需要一个投票 id。因为ComputeResult与Vote、Poll和Option资源一起工作,所以我们不能使用第三种方法来设计本质上是分层的 URI。对于需要数据来执行计算的用例,第四个约定推荐使用查询参数。例如,客户机可以调用端点http://localhost:8080/computeresult?pollId=1234来计算 id 为 1234 的投票的所有票数。查询参数是向资源提供附加信息的极好工具。
在本节中,我们已经确定了 QuickPoll 应用中资源的端点。下一步是确定这些资源上允许的操作,以及预期的响应。
动作识别
HTTP 动词允许客户端使用其端点进行交互和访问资源。在我们的 QuickPoll 应用中,客户端必须能够对资源(如 Poll 和 Vote)执行一个或多个 CRUD 操作。分析“快速轮询简介”部分的用例,表 4-2 显示了轮询/轮询收集资源上允许的操作以及成功和错误响应。注意,在轮询收集资源上,我们允许 GET 和 POST 操作,但拒绝 PUT 和 Delete 操作。集合资源上的 POST 允许客户端创建新的投票。类似地,我们允许对给定的轮询资源执行 GET、PUT 和 Delete 操作,但拒绝 POST 操作。对于不存在的轮询资源上的任何 GET、PUT 和 DELETE 操作,该服务返回 404 状态代码。类似地,任何服务器错误都会导致向客户端发送状态代码 500。
表 4-2
轮询资源上允许的操作
|超文本传送协议
方法
|
资源
端点
|
投入
|
成功响应
|
错误响应
|
描述
|
| --- | --- | --- | --- | --- | --- |
| 得到 | /polls | 主体:空的 | 状态:200 正文:投票列表 | 现状:500 | 检索所有可用的投票 |
| 邮政 | /polls | 正文:新的民意调查数据 | 现状:201 正文:新创建的投票 id | 现状:500 | 创建新的投票 |
| 放 | /polls | 不适用的 | 不适用的 | 现状:400 | 禁止动作 |
| 删除 | /polls | 不适用的 | 不适用的 | 现状:400 | 禁止动作 |
| 得到 | /polls/{pollId} | 主体:空的 | 状态:200 正文:民意测验数据 | 状态:404 或 500 | 检索到现有的投票 |
| 邮政 | /polls/{pollId} | 不适用的 | 不适用的 | 现状:400 | 被禁止的 |
| 放 | /polls/{pollId} | 正文:带更新的轮询数据 | 状态:200 主体:空的 | 状态:404 或 500 | 更新现有投票 |
| 删除 | /polls/{pollId} | 主体:空的 | 状态:200 | 状态:404 或 500 | 删除现有的投票 |
同样,表 4-3 显示了投票/选票收集资源上允许的操作。
表 4-3
投票资源上允许的操作
|超文本传送协议
方法
|
资源
端点
|
投入
|
成功响应
|
错误响应
|
描述
|
| --- | --- | --- | --- | --- | --- |
| 得到 | /polls/{pollId}/votes | 主体:空的 | 状态:200 正文:投票列表 | 现状:500 | 检索给定投票的所有可用投票 |
| 邮政 | /polls/{pollId}/votes | 正文:新投票 | 现状:201 正文:新创建的投票 id | 现状:500 | 创建新投票 |
| 放 | /polls/{pollId}/votes | 不适用的 | 不适用的 | 现状:400 | 禁止动作 |
| 删除 | /polls/{pollId}/votes | 不适用的 | 不适用的 | 现状:400 | 禁止动作 |
| 得到 | /polls/{pollId}/votes/{voteId} | 主体:空的 | 状态:200 正文:投票数据 | 状态:404 或 500 | 检索现有投票 |
| 邮政 | /polls/{pollId}/votes/{voteId} | 不适用的 | 不适用的 | 现状:400 | 被禁止的 |
| 放 | /polls/{pollId}/votes/{voteId} | 不适用的 | 不适用的 | 现状:400 | 禁止投票不能根据我们的要求更新 |
| 删除 | /polls/{pollId}/votes/{voteId} | 不适用的 | 不适用的 | 现状:400 | 根据我们的要求,禁止投票不能被删除 |
最后,表 4-4 显示了 ComputeResult 资源上允许的操作。
表 4-4
计算机上允许的操作结果资源
|超文本传送协议
方法
|
资源
端点
|
投入
|
成功响应
|
错误响应
|
描述
|
| --- | --- | --- | --- | --- | --- |
| 得到 | /computeresult | 主体:空的参数:pollId | 状态:200 正文:投票计数 | 现状:500 | 返回给定投票的投票数 |
QuickPoll REST 服务的设计到此结束。在我们开始实施之前,我们将回顾一下 QuickPoll 的高级架构。
快速轮询架构
QuickPoll 应用将由一个 web 或 REST API 层和一个存储库层组成,一个域层(Web API 和存储库之间的层)横切这两个层,如图 4-3 所示。分层方法提供了清晰的关注点分离,使得应用易于构建和维护。每一层都使用定义明确的契约与下一层进行交互。只要契约得到维护,就有可能交换底层实现,而不会对整个系统产生任何影响。
图 4-3
快速轮询架构
Web API 层负责接收客户端请求、验证用户输入、与服务或存储库层交互以及生成响应。使用 HTTP 协议,在客户端和 Web API 层之间交换资源表示。这一层包含控制器/处理程序,通常非常轻量级,因为它将大部分工作委托给它下面的层。
领域层被认为是应用的“心脏”。这一层中的域对象包含业务规则和业务数据。这些对象是以系统中的名词为模型的。例如,我们的 QuickPoll 应用中的 Poll 对象将被视为域对象。
存储库或数据访问层负责与数据存储(如数据库、LDAP 或遗留系统)进行交互。它通常提供 CRUD 操作,用于在数据存储中存储和检索对象。
Note
细心的读者会注意到 QuickPoll 架构缺少一个服务层。服务层通常位于 API/表示层和存储库层之间。它包含粗粒度的 API,其方法满足一个或多个用例。它还负责管理事务和其他横切关注点,如安全性。
因为在本书中,我们不会处理 QuickPoll 应用的任何复杂用例,所以我们不会在我们的架构中引入服务层。
实施快速投票
我们通过使用 STS 生成一个 Spring Boot 项目来开始 QuickPoll 的实现。按照第三章的“使用 STS 生成项目”一节中讨论的步骤,创建一个名为 quick-poll 的项目。图 4-4 和 4-5 给出了项目生成过程中使用的配置信息。请注意,我们已经选择了“JPA”和“Web”选项。
图 4-5
QuickPoll Spring starter 项目依赖项
图 4-4
QuickPoll 春季启动项目
或者,您可以从本书的下载源代码中将 QuickPoll 项目导入您的 STS IDE。下载的源代码包含多个名为ChapterX的文件夹,其中X代表对应的章节号。每个ChapterX文件夹进一步包含两个子文件夹:一个starter文件夹和一个final文件夹。starter文件夹中有一个 QuickPoll 项目,您可以使用它来跟踪本章中描述的解决方案。
即使每一章都建立在前一章的基础上,starter 项目也允许你在书中跳来跳去。例如,如果您对了解安全性感兴趣,您可以简单地在Chapter8\starter文件夹下加载 QuickPoll 应用,并按照第八章中描述的解决方案进行操作。
顾名思义,final文件夹包含每章的完整解决方案/代码。为了尽量减少本章文本中的代码,我在一些代码清单中省略了 getter/setter 方法、导入和包声明。请参考final文件夹下的快速轮询代码,获取完整的代码列表。
默认情况下,Spring Boot 应用在端口 8080 上运行。因此,如果您打算运行两个版本的 QuickPoll,只需使用命令行选项-Dserver.port:
mvn spring-boot:run -Dserver.port=8181
Note
Java 持久性 API(JPA)是一种基于标准的 API,用于访问、存储和管理 Java 对象和关系数据库之间的数据。像 JDBC 一样,JPA 纯粹是一个规范,许多商业和开源产品如 Hibernate 和 TopLink 都提供 JPA 实现。对 JPA 的正式概述超出了本书的范围。详情请参考 Pro JPA 2 ( http://www.apress.com/9781430219569/ )了解。
域实现
域对象通常充当任何应用的主干。因此,我们实现过程的下一步是创建域对象。图 4-5 显示了一个 UML 类图,它代表了 QuickPoll 应用中的三个域对象以及它们之间的关系。
图 4-6
快速轮询域对象
在 quick-poll 项目中,在/src/main/java文件夹下创建一个com.apress.domain子包,并创建对应于我们识别的域对象的 Java 类。清单 4-6 给出了Option类的实现。如您所见,Option类有两个字段:id,用于保存身份;和value,对应选项值。此外,您将看到我们已经用 JPA 注释对该类进行了注释,例如@Entity和@Id。这允许使用 JPA 技术轻松持久化和检索Option类的实例。
package com.apress.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Option {
@Id
@GeneratedValue
@Column(name="OPTION_ID")
private Long id;
@Column(name="OPTION_VALUE")
private String value;
// Getters and Setters omitted for brevity
}
Listing 4-6Option Class
接下来,我们创建一个Poll类,如清单 4-7 所示,以及相应的 JPA 注释。Poll类有一个问题字段来存储投票问题。@OneToMany注释,顾名思义,表示一个Poll实例可以包含零个或多个Option实例。CascadeType.All表示任何数据库操作,比如对一个Poll实例的持久化、删除或合并,都需要传播到所有相关的Option实例。例如,当一个Poll实例被删除时,所有相关的Option实例都将从数据库中删除。
package com.apress.domain;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy ;
@Entity
public class Poll {
@Id
@GeneratedValue
@Column(name="POLL_ID")
private Long id;
@Column(name="QUESTION")
private String question;
@OneToMany(cascade=CascadeType.ALL)
@JoinColumn(name="POLL_ID")
@OrderBy
private Set<Option> options;
// Getters and Setters omitted for brevity
}
Listing 4-7Poll Class
最后,我们创建了Vote类,如清单 4-8 所示。@ManyToOne注释表明一个Option实例可以有零个或多个Vote实例与之相关联。
package com.apress.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
@Entity
public class Vote {
@Id
@GeneratedValue
@Column(name="VOTE_ID")
private Long id;
@ManyToOne
@JoinColumn(name="OPTION_ID")
private Option option;
// Getters and Setters omitted for brevity
}
Listing 4-8Vote Class
知识库实现
存储库或数据访问对象(DAO)提供了与数据存储交互的抽象。传统上,存储库包括一个接口,该接口提供一组 finder 方法,例如用于检索数据的findById、findAll,以及保存和删除数据的方法。存储库还包括一个使用特定于数据存储的技术实现该接口的类。例如,处理数据库的存储库使用 JDBC 或 JPA 等技术,处理 LDAP 的存储库使用 JNDI。通常每个域对象都有一个存储库。
尽管这是一种流行的方法,但是在每个存储库实现中都有许多重复的样板代码。开发人员试图将通用功能抽象成通用接口和通用实现( http ://www.ibm.com/developerworks/library/j-genericdao/ )。然而,他们仍然需要为每个域对象创建一对存储库接口和类。通常这些接口和类是空的,只会导致更多的维护。
Spring Data 项目旨在通过完全消除编写任何存储库实现的需要来解决这个问题。使用 Spring 数据,您需要的只是一个存储库接口,在运行时自动生成它的实现。唯一的要求是应用存储库接口应该扩展许多可用的 Spring 数据标记接口中的一个。因为我们将使用 JPA 将 QuickPoll 域对象持久化到关系数据库中,所以我们将使用 Spring Data JPA 子项目的org.springframework.data.repository.CrudRepository标记接口。从清单 4-9 中可以看出,CrudRepository接口将它所操作的域对象的类型和域对象的标识符字段的类型作为其通用参数T和ID。
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S var1);
<S extends T> Iterable<S> saveAll(Iterable<S> var1);
Optional<T> findById(ID var1);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> var1);
void deleteById(ID var1);
void delete(T var1);
void deleteAllById(Iterable<? extends ID> var1);
void deleteAll(Iterable<? extends T> var1);
void deleteAll();
// Utility Methods
long count();
boolean existsById(ID var1);
}
Listing 4-9CrudRepository API
我们通过在src\main\java文件夹下创建一个com.apress.repository包来开始我们的存储库实现。然后,我们创建一个如清单 4-10 所示的OptionRepository接口。如前所述,OptionRepository扩展了 Spring Data 的CrudRepository,从而继承了它所有的 CRUD 方法。因为OptionRepository与Option域对象一起工作,所以它将Option和Long作为通用参数值传递。
package com.apress.repository;
import org.springframework.data.repository.CrudRepository;
import com.apress.domain.Option;
public interface OptionRepository extends CrudRepository<Option, Long> {
}
Listing 4-10OptionRepository Interface
采用同样的方法,我们然后创建PollRepository和VoteRepository接口,如清单 4-11 和 4-12 所示。
public interface VoteRepository extends CrudRepository<Vote, Long> {
}
Listing 4-12OptionRepository Interface
public interface PollRepository extends CrudRepository<Poll, Long> {
}
Listing 4-11PollRepository Interface
嵌入式数据库
在上一节中,我们创建了存储库,但是我们需要一个关系数据库来保存数据。关系数据库市场充满了各种选择,从 Oracle 和 SQL Server 等商业数据库到 MySQL 和 PostgreSQL 等开源数据库。为了加快 QuickPoll 应用的开发,我们将使用 HSQLDB,这是一个流行的内存数据库。内存数据库(也称为嵌入式数据库)不需要任何额外的安装,可以简单地作为 JVM 进程运行。它们的快速启动和关闭能力使它们成为原型和集成测试的理想选择。同时,它们通常不提供持久存储,应用需要在每次启动时播种数据库。
Spring Boot 为嵌入 HSQLDB、H2 和 Derby 的数据库提供了出色的支持。唯一的要求是在pom.xml文件中包含一个构建依赖项。Spring Boot 负责在部署期间启动数据库,并在应用关闭期间停止数据库。不需要提供任何数据库连接 URL 或用户名和密码。清单 4-13 显示了需要添加到 QuickPoll 的pom.xml文件中的依赖信息。
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
Listing 4-13HSQLDB POM.XML Dependency
API 实现
在这一节中,我们将创建 Spring MVC 控制器并实现我们的 REST API 端点。我们首先在src\main\java下创建com.apress.controller包来容纳所有的控制器。
PollController 实现
PollController提供了所有必要的端点来访问和操作轮询和轮询资源。清单 4-14 显示了一个基本的PollController类。
package com.apress.controller;
import javax.inject.Inject;
import org.springframework.web.bind.annotation.RestController;
import com.apress.repository.PollRepository;
@RestController
public class PollController {
@Inject
private PollRepository pollRepository;
}
Listing 4-14PollController Class
用一个@RestController注释对PollController类进行了注释。@RestController是一个方便而有意义的注释,与添加@Controller和@ResponseBody注释具有相同的效果。因为我们需要读取和存储Poll实例,所以我们使用@Inject注释将一个PollRepository实例注入到我们的控制器中。作为 Java EE 6 的一部分引入的javax.inject.Inject注释提供了声明依赖关系的标准机制。我们使用这个注释来支持 Spring 专有的@Autowired注释,以便更加兼容。为了使用@Inject注释,我们需要将清单 4-15 中所示的依赖项添加到pom.xml文件中。
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
Listing 4-15Inject Dependency in POM File
在/polls端点上的 GET 请求提供了 QuickPolls 应用中所有可用投票的集合。清单 4-16 显示了实现该功能的必要代码。shortcut注释声明了 URI 和允许的 HTTP 方法。getAllPolls方法使用ResponseEntity作为其返回类型,表明返回值是完整的 HTTP 响应。ResponseEntity让您完全控制 HTTP 响应,包括响应体和响应头。方法实现从使用PollRepository读取所有轮询开始。然后我们创建一个ResponseEntity的实例,并传入Poll数据和HttpStatus.OK状态值。Poll数据成为响应主体的一部分,OK(代码 200)成为响应状态代码。
@GetMapping("/polls")
public ResponseEntity<Iterable<Poll>> getAllPolls() {
Iterable<Poll> allPolls = pollRepository.findAll();
return new ResponseEntity<>(pollRepository.findAll(), HttpStatus.OK);
}
Listing 4-16GET Verb Implementation for /polls
让我们通过运行 QuickPoll 应用来快速测试我们的实现。在命令行中,导航到quick-poll项目目录并运行以下命令:
mvn spring-boot:run
在你的 Chrome 浏览器中启动 Postman 应用,输入网址http://localhost:8080/polls,如图 4-7 所示,点击发送。因为我们还没有创建任何投票,这个命令将导致一个空的集合。
图 4-7
获取所有投票请求
Note
下载的源代码包含一个导出的 Postman 集合,其中的请求可用于运行本章中的测试。只需将这个集合导入到您的 Postman 应用中,并开始使用它。
我们的下一站是实现向PollController添加新轮询的功能。我们通过实现后动词功能来实现这一点,如清单 4-17 所示。createPoll方法接受一个类型为Poll的参数。@RequestBody注释告诉 Spring 整个请求体需要转换成Poll的一个实例。Spring 使用传入的Content-Type头来标识一个合适的消息转换器,并将实际的转换委托给它。Spring Boot 附带了支持 JSON 和 XML 资源表示的消息转换器。在方法内部,我们简单地将Poll持久性委托给PollRepository的save方法。然后我们创建一个新的状态为CREATED (201)的ResponseEntity并返回它。
@PostMapping("/polls")
public ResponseEntity<?> createPoll(@RequestBody Poll poll) {
poll = pollRepository.save(poll);
return new ResponseEntity<>(null, HttpStatus.CREATED);
}
Listing 4-17Implementation to Create New Poll
虽然这个实现满足了请求,但是客户端无法知道新创建的Poll的 URI。例如,如果客户想要将新创建的Poll共享到一个社交网站,当前的实现是不够的。最佳实践是使用Location HTTP 头将 URI 传递给新创建的资源。构建 URI 需要我们检查HttpServletRequest对象,以获得诸如根 URI 和上下文之类的信息。Spring 通过其ServletUriComponentsBuilder实用程序类简化了 URI 生成过程:
URI newPollUri = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(poll.getId())
.toUri();
fromCurrentRequest方法通过从HttpServletRequest复制主机、模式、端口等信息来准备构建器。path方法将传入的路径参数附加到构建器中现有的路径上。在createPoll方法的情况下,这将导致http://localhost:8080/polls/{id}。buildAndExpand方法将构建一个UriComponents实例,并用传入的值替换任何路径变量(在我们的例子中是{id})。最后,我们调用UriComponents类上的toUri方法来生成最终的 URI。清单 4-18 显示了createPoll方法的完整实现。
@PostMapping("/polls")
public ResponseEntity<?> createPoll(@RequestBody Poll poll) {
poll = pollRepository.save(poll);
// Set the location header for the newly created resource
HttpHeaders responseHeaders = new HttpHeaders();
URI newPollUri = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(poll.getId())
.toUri();
responseHeaders.setLocation(newPollUri);
return new ResponseEntity<>(null, responseHeaders, HttpStatus.CREATED);
}
Listing 4-18Complete Implementation of Create Poll
要测试我们新添加的功能,请启动 QuickPoll 应用。如果您已经运行了应用,您需要终止该进程并重新启动它。如图 4-8 所示,在 Postman 中输入信息,点击发送。确保您已经添加了值为application/json的Content-Type标题。正文中使用的 JSON 如下所示:
图 4-8
创建投票邮递员示例
{
"question": "Who will win SuperBowl this year?",
"options": [
{"value": "New England Patriots"},
{"value": "Seattle Seahawks"},
{"value": "Green Bay Packers"},
{"value": "Denver Broncos"}]
}
请求完成后,您将看到状态 201 创建的消息和标题:
Content-Length ® 0
Date ® Mon, 23 Feb 2015 00:05:11 GMT
Location ® http://localhost:8080/polls/1
Server ® Apache-Coyote/1.1
现在让我们将注意力转向访问个人投票。清单 4-19 给出了必要的代码。shortcut annotations (@GetMapping, @PostMapping, etc.)中的值属性采用 URI 模板/polls/{pollId}。占位符{pollId}和@PathVarible注释允许 Spring 检查请求 URI 路径并提取 pollId 参数值。在这个方法中,我们使用PollRepository的findById finder 方法来读取投票,并将其作为ResponseEntity的一部分传递。
@GetMapping("/polls/{pollId}")
public ResponseEntity<?> getPoll(@PathVariable Long pollId) {
Optional<Poll> poll = pollRepository.findById(pollId);
if(!poll.isPresent()) {
throw new Exception("Pool not found");
}
return new ResponseEntity<>(poll.get(), HttpStatus.OK);
}
Listing 4-19Retrieving an Individual Poll
以同样的方式,我们实现了更新和删除一个Poll的功能,如清单 4-20 所示。
@PutMapping("/polls/{pollId}")
public ResponseEntity<?> updatePoll(@RequestBody Poll poll, @PathVariable Long pollId) {
// Save the entity
Poll newPoll = pollRepository.save(poll);
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/polls/{pollId}")
public ResponseEntity<?> deletePoll(@PathVariable Long pollId) {
pollRepository.deleteById(pollId);
return new ResponseEntity<>(HttpStatus.OK);
}
Listing 4-20Update and Delete a Poll
一旦将这段代码添加到PollController中,重启 QuickPoll 应用并执行 Postman 请求,如图 4-8 所示,以创建一个新的投票。然后输入图 4-9 中的信息,创建一个新的邮递员请求并更新轮询。请注意,PUT 请求包含整个Poll表示以及 id。
图 4-9
更新投票
这就结束了PostController的实现。
VoteController 实现
遵循用于创建 PollController 的原则,我们实现了VoteController类。清单 4-21 给出了VoteController类的代码以及创建投票的功能。VoteController使用VoteRepository的注入实例在Vote实例上执行 CRUD 操作。
@RestController
public class VoteController {
@Inject
private VoteRepository voteRepository;
@PostMapping("/polls/{pollId}/votes")
public ResponseEntity<?> createVote(@PathVariable Long pollId, @RequestBody Vote vote) {
vote = voteRepository.save(vote);
// Set the headers for the newly created resource
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation(ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(vote.getId()).toUri());
return new ResponseEntity<>(null, responseHeaders, HttpStatus.CREATED);
}
}
Listing 4-21VoteController Implementation
为了测试投票能力,向/polls/1/votes端点发送一个新的投票,在请求体中有一个选项,如图 4-10 所示。在成功执行请求时,您将看到一个值为http://localhost:8080/polls/1/votes/1的Location响应头。
图 4-10
投新的一票
接下来,我们看一下实现检索给定投票的所有投票的能力。VoteRepository中的findAll方法返回数据库中的所有投票。因为这不能满足我们的需求,我们需要向清单 4-22 所示的VoteRepository添加这个功能。
import org.springframework.data.jpa.repository.Query;
public interface VoteRepository extends CrudRepository<Vote, Long> {
@Query(value="select v.* from Option o, Vote v where o.POLL_ID = ?1 and
v.OPTION_ID = o.OPTION_ID", nativeQuery = true)
public Iterable<Vote> findByPoll(Long pollId);
}
Listing 4-22Modified VoteRepository Implementation
自定义查找器方法findVotesByPoll将Poll的 ID 作为其参数。这个方法上的@Query注释接受一个原生 SQL 查询以及设置为true的nativeQuery标志。在运行时,Spring Data JPA 用传入的pollId参数值替换?1占位符。接下来,我们在VoteController中实现/polls/{pollId}/votes端点,如清单 4-23 所示。
@GetMapping("/polls/{pollId}/votes")
public Iterable<Vote> getAllVotes(@PathVariable Long pollId) {
return voteRepository. findByPoll(pollId);
}
Listing 4-23GET All Votes Implementation
ComputeResultController 实现
我们剩下的最后一块工作是实现ComputeResult资源。因为我们没有任何域对象可以直接帮助生成这个资源表示,所以我们实现了两个数据传输对象或 dto—OptionCount和VoteResult。OptionCount DTO 包含选项的 ID 和为该选项投票的计数。VoteResult DTO 包含总投票数和一组OptionCount实例。这两个 dto 是在com.apress.dto包下创建的,它们的实现在清单 4-24 中给出。
package com.apress.dto;
public class OptionCount {
private Long optionId;
private int count;
// Getters and Setters omitted for brevity
}
package com.apress.dto;
import java.util.Collection;
public class VoteResult {
private int totalVotes;
private Collection<OptionCount> results;
// Getters and Setters omitted for brevity
}
Listing 4-24DTOs for ComputeResult Resources
遵循创建PollController和VoteController的原则,我们创建一个新的ComputeResultController类,如清单 4-25 所示。我们将VoteRepository的一个实例注入控制器,它用于检索给定投票的投票。computeResult方法将pollId作为其参数。@RequestParam注释指示 Spring 从 HTTP 查询参数中检索pollId值。使用新创建的ResponseEntity实例将计算结果发送给客户机。
package com.apress.controller;
@RestController
public class ComputeResultController {
@Inject
private VoteRepository voteRepository;
@GetMapping("/computeresult")
public ResponseEntity<?> computeResult(@RequestParam Long pollId) {
VoteResult voteResult = new VoteResult();
Iterable<Vote> allVotes = voteRepository.findByPoll(pollId);
// Algorithm to count votes
return new ResponseEntity<VoteResult>(voteResult, HttpStatus.OK);
}
}
Listing 4-25ComputeResultController implementation
有几种方法可以计算与每个选项相关的票数。该代码提供了一个这样的选项:
int totalVotes = 0;
Map<Long, OptionCount> tempMap = new HashMap<Long, OptionCount>();
for(Vote v : allVotes) {
totalVotes ++;
// Get the OptionCount corresponding to this Option
OptionCount optionCount = tempMap.get(v.getOption().getId());
if(optionCount == null) {
optionCount = new OptionCount();
optionCount.setOptionId(v.getOption().getId());
tempMap.put(v.getOption().getId(), optionCount);
}
optionCount.setCount(optionCount.getCount()+1);
}
voteResult.setTotalVotes(totalVotes);
voteResult.setResults(tempMap.values());
这就结束了ComputeResult控制器的实现。启动/重新启动快速轮询应用。使用先前的 Postman 请求,创建一个投票并对其选项进行投票。然后创建一个新的 Postman 请求,如图 4-11 所示,并提交它来测试我们的/computeresult端点。
图 4-11
计算机结果终点测试
成功完成后,您将看到类似如下的输出:
{
"totalVotes": 7,
"results": [
{
"optionId": 1,
"count": 4
},
{
"optionId": 2,
"count": 3
}
]
}
摘要
在本章中,我们学习了为 QuickPoll 应用创建 RESTful 服务。我们在这一章中的大多数例子都假设了一条“幸福之路”,在这条路上一切都按计划进行。然而,这在现实世界中很少发生。在下一章中,我们将着眼于处理错误、验证输入数据以及传达有意义的错误信息。