- 内容提要
- 序
- 前言
- 第一部分 背景知识
- 第 1 章 Spring Data 项目
- 第 2 章 Repository:便利的数据访问层
- 第 3 章 使用 Querydsl 实现类型安全的查询
- 第二部分 关系型数据库
- 第 4 章 JPA Repository
- 第 5 章 借助 Querydsl SQL 实现类型安全的 JDBC 编程
- 第三部分 NoSQL
- 第 6 章 MongoDB: 文档存储
- 第 7 章 Neo4j:图数据库
- 第 8 章 Redis:键/值存储
- 第四部分 快速应用开发
- 第 9 章 使用 Spring Roo 实现持久层
- 第 10 章 REST Repository 导出器
- 第五部分 大数据
- 第 11 章 Spring for Apache Hadoop
- 第 12 章 使用 Hadoop 分析数据
- 第 13 章 使用 Spring Batch 和 Spring Integration 创建大数据管道
- 第六部分 数据网格
- 第 14 章 分布式数据网格:GemFire
- 关于封面
10.1 示例工程
运行示例应用的最简单方式是通过命令行使用 Maven 的 Jetty 插件。Jetty 是一个很小的 servlet 容器,可以运行 Servlet 3.0 的 Web 应用。Maven 插件可以从模块目录中启动应用,所使用的命令如示例 10-1 所示。
示例 10-1 从命令行运行示例应用
在这里需要注意的第一件事就是在执行命令中传入了 JVM 参数,也就是 spring.profiles.active。这会为内存数据库填充一些示例产品、客户以及订单,这样我们就会有一些用于进行交互的实际数据了。Maven 将一些通用的活动信息打印到了控制台上。下一个比较有意思的行在 17:58:10 那一行,它告诉我们 Jetty 已经发现了 WebApplicationInitializer - 具体来说也就是我们的 RestWebApplicationInitializer。WebApplicationInitializer 是等同于 web.xml 文件的 API,是在 Servlet API 3.0 中引入的。它可以让我们摆脱基于 XML 的方式来配置 Web 应用的基础设施,取而代之的是使用 API。我们的实现如示例 10-2 所示。
示例 10-2 RestWebApplicationInitializer
首先,搭建 AnnotationConfigWebApplicationContext 并注册了 ApplicationConfigJavaConfig 类,它稍后会作为 Spring 配置来使用。我们将包装到 ContextLoaderListener 之中的 ApplicationContext 注册到实际的 ServletContext 里面。这个上下文稍后会触发监听器,这将会启动 ApplicationContext。到目前为止,这段代码与在 web.xml 文件中注册一个 ContextLoaderListener 并指向一个 XML 配置文件是相同的,只不过不用处理 XML 以及基于 String 的文件地址,它是以类型安全的方式引用了配置。所配置的 ApplicationContext 现在会启动嵌入式的数据库、包含事务管理器在内的 JPA 基础设施并最终启用 Repository。这个过程已经在 4.3 小节“启动示例代码”中进行了介绍。
稍后,声明了一个 RepositoryRestExporterServlet,它负责真正有意思的部分。它会注册很多的 Spring MVC 基础设施组件并会探查到用于 Spring Data Repository 实例的根应用上下文,并会为那些实现了 CrudRepository 接口的所有 Repository 暴露 HTTP 资源。目前来说这是个限制,这个模块的后续版本会将其移除。我们将 servlet 映射到 servlet 根上,这样这个应用就可以通过 http://localhost:8080 进行访问了。
10.1.1 与 Rest 导出器进行交互
现在已经启动了应用,来看一下实际上是怎样使用它的。使用命令行工具 curl 来与系统进行交互,因为它提供了一种便利的方式来触发 HTTP 请求并且它显示响应的方式非常适合在书中进行展现。当然,可以使用其他能够触发 HTTP 请求的客户端:命令行工具(如 Windows 下的 wget)或者直接使用所选择 Web 浏览器。要注意的是,后者只允许通过 URL 地址栏触发 GET 请求。如果想使用更为高级的请求(POST、PUT、DELETE 等),建议使用浏览器插件,如针对 Google Chrome 的 Dev HTTP Client( http://bit.ly/PZ5lCt )。对于其他的浏览器来说,也有类似的工具可供使用。
让我们触发一些对这个应用的请求,如示例 10-3 所示。目前我们所知的就是其部署为监听 http://localhost:8080 ,因此看一下它实际提供的资源是什么。
示例 10-3 使用 curl 触发初始请求
这里要注意的第一件事就是触发 curl 命令时使用了-v 标记。这个标记会激活详细输出,列出所有的请求和响应头以及实际的响应数据。可以看到,默认情况下,服务器所返回数据的内容类型为 application/json。在真正的响应体中包含了一个链接的集合,可以按照它来对应用进行查看。它所提供的每个链接实际上都是由 ApplicationContext 中可用的 Spring Data Repository 衍生而来。我们具有 CustomerRepository、ProductRepository 以及 OrderRepository,因此关系类型(rel)属性就是 customer、product 和 order(Repository 名字的前半部分,并且首字符要小写)。资源的 URL 也使用默认规则衍生而来。如果要自定义这种行为,可以在 Repository 接口上使用 @RestResource 注解,它允许你明确定义 path(URI 部分)以及 rel(关系类型)。
链接(Link)
链接的表述通常衍生自 Atom RFC( http://tools.ietf.org/html/rfc4287 )中所定义的链接元素。基本上来讲,它包含了两个属性:关系类型(rel)和超文本引用(href)。前者定义了链接的实际语义(因此需要文档化和标准化),而后者实际上对客户端是不透明的。通常客户端会探查链接的响应体以获取关系类型,并访问它所感兴趣的关系类型和链接。所以,一般来讲,客户端会知道在链接后添加 order 类型的 rel 就能获取所有的订单。这种结构会使得服务器和客户端实现解耦,因为服务端会告知客户端去哪里获取数据。如果 URL 会发生变化或者服务器希望客户端指向不同的机器以实现请求的负载均衡,这就会特别有用。
继续来查看系统中可用的产品。我们知道产品会通过关系类型 product 来进行暴露,因此我们访问带有这个 rel 的链接。
10.1.2 访问 Product
示例 10-4 展现了如何访问系统中所有可用的产品。
示例 10-4 访问产品
触发这个访问所有产品的请求会返回 JSON 表述,它包含了两个主要的域。域 content 包含了所有可用的产品的集合并且直接在响应中进行展现。每个元素中包含了 Product 类的序列化属性以及一个非原生的 links 容器。这个容器包含了一个链接,其关系类型为 self。self 类型通常作为一种标识符,因为它指向了资源本身。所以,可以根据表述中具有 self 关系类型的链接直接访问 iPad 产品,如示例 10-5 所示。
示例 10-5 访问单个产品
要更新一个产品,只需要对这个资源发送一个 PUT 请求并提供新的内容即可,如示例 10-6 所示。
示例 10-6 更新一个产品
通过使用-X 参数将 HTTP 方法设置为 PUT 并且提供了 Content-Type 头信息以表明我们所发送的是 JSON。通过-d 参数提交了更新后的 price 和 name 属性。服务端返回 204 No Content 表明请求已经成功了。对这个产品的 URL 再次发起一个 GET 请求会返回更新后的内容,如示例 10-7 所示。
示例 10-7 更新后的产品
资源集合的 JSON 表述中还包含了 links 属性,它指向了一个通用的资源,据此我们可以探索 Repository 所暴露的查询方法。根据约定要使用集合资源的关系类型(在我们的例子中,也就是 product)再加上.search。让我们访问这个链接并查看一下实际上可执行的查询是什么,如示例 10-8 所示。
示例 10-8 访问 Product 可用的搜索功能
可以看到,Repository 导出器为 ProductRepository 接口中所声明的每一个查询方法均暴露了一个资源。同样的,这里的关系类型模式基于资源的关系类型并且要再加上查询方法的名字,但是我们可以通过在查询方法上使用 @RestResource 注解来进行自定义。令人遗憾的是,JVM 并不支持从接口方法上得到参数名,所以我们必须要在查询方法的参数上使用 @Param,并且在 findByAttributeAndValue(...) 的方法定义上,对手动定义的查询方法要使用命名参数,如示例 10-9 所示。
示例 10-9 ProductRepository 接口
现在,按照 product.findByAttributeAndValue 链接,发送带有匹配参数的 GET 请求到服务器来触发第二个查询方法。搜索一下 connector 属性为 plug 的产品,如示例 10-10 所示。
示例 10-10 搜索 connector 属性为 plug 的产品
10.1.3 访问 Customer
我们已经看到了如何对可用的产品进行导航以及如何执行 Repository 接口所暴露的查找方法,现在让我们调转方向看一下系统中的注册用户。我们初始对 http://localhost:8080 的请求暴露了一个 customer 连接,如示例 10-3 所示。在示例 10-11 中,按照这个链接看看能够得到什么样的 customer 结果。
示例 10-11 访问顾客(1/2)
呀,看起来情况并不妙。我们得到的是 500 服务器错误(500 Server Error)的响应,表明处理这个请求的时候出现了错误。终端输出可能更为详尽,但是重要的一行在示例 10-11 中,也就是在 HTTP 状态码下面。Jackson(Spring Data Rest 所采用的 JSON 编组技术)在序列化 EmailAddress 值对象时似乎被阻塞住了。这是因为我们并没有暴露任何的 getter 和 setter 方法,Jackson 要使用这样的方法来寻找要渲染到响应之中的属性。
实际上,我们并不想将 EmailAddress 作为嵌入式的对象来进行渲染,而是将其作为简单的 String 值。可以通过使用 Jackson 所提供的 @JsonSerialize 注解来自定义渲染以实现这一点。我们将其 using 属性配置为预先定义的 ToStringSerializer.class,它会调用这个对象的 toString() 方法来进行渲染。
那么,让我们再试一次,如示例 10-12 所示。
示例 10-12 访问顾客(2/2)
看起来并没有好到哪里去,但是我们至少又往前进了一步。这一次 Jackson 的渲染器提示 Address 类暴露了 copy 属性,这会导致递归。产生这个问题的原因在于 Address 类的 getCopy() 方法遵循了 Java Bean 属性的语义,而不是传统意义上的 getter 方法。实际上,它会返回 Address 对象的拷贝版本以便于我们很容易地复制 Address 实例,并将其指定到 Order 之中,从而避免了 Customer 的 Address 所发生的变化会影响到已有的订单(参见示例 4-7)。所以,在这里有两个可选方案:重命名这个方法使其不再匹配 Java Bean 的属性约定或者添加一个注解告知 Jackson 忽视掉这个属性。我们选择了后者,因为不想重构客户端的代码。因此,使用 @JsonIgnore 注解来将 copy 属性排除在渲染的属性之外,如示例 10-13 所示。
示例 10-13 将 Address 类的 copy 属性排除在渲染之外
做完这些改动之后,让我们重启服务器并再次发出请求,如示例 10-14 所示。
示例 10-14 在进行编组调整之后访问顾客
可以看到,实体已经可以正确地进行渲染了。也可以看到预期的 links 区域中指向了顾客类可用的查询方法。不过在这里,与前面的不同在于返回的 JSON 中设置了额外的 page 属性。它包含了目前的页数(number)、请求的分页大小(size,这里是默认的 20)、可用的总页数(totalPages)以及可用的总元素数量(totalElements)。
出现这些属性是因为 CustomerRepository 扩展了 PagingAndSortingRepository,因此允许逐页地访问顾客。要了解关于这一点的更多信息,可以参考 2.3 小节“定义 Repository”。这意味着在发起请求的时候,可以使用 page 和 limit 参数来限制返回的顾客数量。由于我们一共有 3 位顾客需要展现,所以可以手动设置每页的数量是一位顾客,如示例 10-15 所示。
示例 10-15 访问第一页的顾客信息
注意,现在得到了元数据信息而且变成了只有一条结果。totalPages 域表明有三页的数据,因为我们所选择的每页大小是 1。更好的一点在于,服务端提示我们可以按照 customer.next 链接来得到下一页的顾客信息。它已经包含了请求第二页数据的请求参数,所以客户端并不需要手动构造 URL。访问这个链接,看一下在结果集中进行导航时元数据是如何发生变化的,如示例 10-16 所示。
示例 10-16 访问第二页的顾客信息
注意,将示例 10-16 中的 URI 粘贴到控制台时,可能需要对&符进行转义。如果使用专用的 HTTP 客户端,那么就不需要进行转义了。
除了实际返回的数据,注意 number 属性提示我们已经到了第二页。除此之外,服务端会探测到有可用的上一页数据并且提供了 customer.prev 链接以便导航到那里。按照 customer.next 链接进行第二次访问时,所得到的表述中就不会再有 customer.next 链接了,因为我们已经到达了可用的最后一页。
10.1.4 访问 Order
最后要探讨的根链接关系就是 order。顾名思义,它允许我们访问系统中可用的 Order 信息。支撑这个资源的 Repository 接口是 OrderRepository。现在,访问这个资源,看看服务端返回了什么,如示例 10-17 所示。
示例 10-17 访问订单信息
响应中包含了许多已经讨论过的我们所熟知的模式,包括指向 OrderRepository 所暴露的查询方法的链接以及嵌套的 content 域,在这个域中包含了序列化的 Order 对象、内联的 Address 对象以及 LineItems。也可以看到分页的元数据信息,因为 OrderRepository 实现了 PagingAndSortingRepository。
在这里要注意的是,Order 对象中所持有的 Customer 实例并没有进行内联显示,而是通过链接指向了它。这是因为 Customer 是通过 Spring Data Repository 来进行管理的。因此它们表现为 Order 的从属资源,从而允许操作它们之间的所属关系。按照这个链接来访问触发这个 Order 的 Customer,如示例 10-18 所示。
示例 10-18 访问下订单的顾客信息
这个调用返回了关联 Customer 的详细信息并提供了两个链接。具有 order.Order.customer. Customer 关系类型的链接指向 Customer 的关联资源,而 self 链接指向了实际的 Customer 资源。这有什么区别吗?前者展现的是 Customer 与订单之间的分配关系。我们可以通过对这个 URI 提交一个 PUT 请求来修改分配关系,也可以触发一个 DELETE 请求解除这种分配关系。在我们的场景下,DELETE 调用将会产生 405 Method not allowed 的响应,因为 JPA 映射要求在 customer 属性的 @ManyToOne 注解上要通过 optional = false 标识来映射 Customer。如果这个关系是可选的,那么 DELETE 请求就能正常运行了。
假设最初并不是 Dave 下的订单,而是 Carter。该怎样更新这种分配关系呢?首先,所选的 HTTP 方法应该是 PUT,我们已经知道管理这个资源的 URI。由于我们想要告诉服务器端的是“这个已有的 Customer 才是下订单的人”,因此将实际的数据发送到服务端并没有什么实际意义。因为 Customer 是通过其 URI 来进行识别的,所以将其通过 PUT 请求发送到服务器端,并将 Content-Type 请求头设置为 text/uri-list,这样服务器端就能知道发送的是什么,如示例 10-19 所示。
示例 10-19 修改下订单的顾客
text/uri-list( http://www.rfc-editor.org/rfc/rfc2483.txt )是一种标准的媒体类型,用来定义所传输的一个或多个 URI 格式。注意,我们从服务器端得到的是 204 No Content,表明它已经接受了请求并完成了关系的重新分配。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论