使用带有 JSON 视图的 HAL
了解如何使用 Grails 构建可发现的 API
作者:ZacharyKlein
Grails 版本 3.3.1
1 Grails 培训
Grails 培训 - 由创建并积极维护了 Grails 框架的人员设计并提供!。
2 开始使用
在本指南中,您将利用 JSON 视图(由 Grails Views 库提供),在 Grails 中探索 HAL(超文本应用程序语言)支持。
超文本应用程序语言 (HAL) 是一个 Internet 草案(一个“正在进行中工作”)标准惯例,用于在 JSON 或 XML 代码中定义超媒体,例如链接到外部资源。
HAL 的目的是使 API “可发现” - 它定义了一组惯例,使用这些惯例,您的 API 使用者可以访问资源之间的链接,并且为“探索”API 提供分页和其他便利功能。HAL 是实现 HATEOAS(超媒体作为应用程序状态引擎)架构的流行标准,它是基本 REST 架构的扩展。
如需了解 HAL 的全面简介,请查看以下 URL 中的概述和规范:http://stateless.co/hal_specification.html |
Grails 通过 JSON 视图来支持 HAL,这属于 Grails Views 库的一部分。您可以在现有应用程序中使用本库,它遵循了文档中的安装步骤,或者您也可以创建一个新应用程序,只需使用 `rest-api` 概要文件或其中一个前端概要文件(`angular`、`angular2` 和 `react`),它们都扩展了 `rest-api` 概要文件。
在此指南中,我们在 `initial` 项目中使用 `rest-api` 概要文件提供了一个基本的 Grails 3.3.1 应用程序。我们还包括了一些域类,它们可通过我们的 API 呈现。如果您希望,您可以生成您自己的项目(您需要将域类从 `initial/grails-app/domain/` 复制到您的项目中),或者只需使用 `initial` 项目,按照指南来执行操作。
2.1 您需要提供什么
完成本指南,您需要提供以下内容
-
在您手中的一些时间
-
不错的文本编辑器或 IDE
-
安装了 JDK 1.7 或更高版本且正确设置了 `JAVA_HOME`
2.2 如何完成指南
请按照以下步骤来开始操作
-
下载源代码并解压缩
或
-
克隆 Git 仓库
git clone https://github.com/grails-guides/using-hal-with-json-views.git
Grails 指南仓库包含两个文件夹
-
initial
初始项目。常常是简单的 Grails 应用程序,并增加了一些代码,以便您可以快速入门。 -
complete
一个完整的示例。这是根据指南提出的步骤执行操作并对 `initial` 文件夹应用这些更改后的结果。
若要完成此指南,请转到 `initial` 文件夹
-
在目录中输入
cd
后转到grails-guides/using-hal-with-json-views/initial
并按照后续部分中的说明操作。
如果您 cd 至 grails-guides/using-hal-with-json-views/complete 中,则可以直接转到完成的示例 |
3 运行应用程序
Grails 很好地支持 RESTful URL 映射,用于呈现域资源。在 `initial` 项目中,我们已经在我们的域类中注释了 `@Resource` 注释,这将生成 `RestfulController` 及关联的 URL 映射,将每个域类作为一个 restful 资源呈现。
package com.example
import grails.rest.*
@Resource(readOnly = true, uri='/api/customers') (1)
class Customer {
1 | @Resource 注释接受多个可选参数,包括 URL 终端和格式选项,以及 API 是否只能呈现读取终端 |
向我们的域类添加这些注释为我们快速创建我们的 API 做好了准备。有关 `@Resource` 的更多信息,请参见 Grails 文档
要运行应用程序,请使用命令 ./gradlew bootRun
,此命令将在端口 8080 上启动应用程序。
既然 Grails 应用程序正在运行,我们可以通过 @Resource
注解尝试 Grails 为我们生成的 API。发送 GET 请求至 /api/products
以检索产品列表。
curl -H "Accept: application/json" localhost:8080/api/products
[
{
"id": 1,
"category": {
"id": 1
},
"inventoryId": "CLOTH001",
"name": "Cargo Pants",
"price": 15.00
},
{
"id": 2,
"category": {
"id": 1
},
"inventoryId": "CLOTH002",
"name": "Sweater",
"price": 12.00
},
{
"id": 3,
"category": {
"id": 1
//...
]
在此,发送至 /api/orders/1
的 GET 请求返回 ID 为 1 的 Order
。
curl -H "Accept: application/json" localhost:8080/api/orders/1
{
"id" : 1,
"shippingAddress" : {
"id" : 1
},
"shippingCost" : 13.54,
"orderId" : "0A12321",
"orderPlaced" : "2017-02-08T09:10:36Z",
"products" : [
{
"id" : 11
},
{
"id" : 6
},
{
"id" : 1
}
],
"customer" : {
"id" : 1
}
}
由于我们在 @Resource 注解中指定了 readOnly = true ,Grails 不会为更新/创建/删除操作生成端点。这对本指南中的步骤来说已经足够,但你可以移除 readOnly 属性(或将其设为 false )以启用“写入”操作。 |
4 构建我们的 API
Grails 呈现的默认 JSON 有一个好的开端,但它不一定表示我们希望在公开 API 中表述的详细信息。JSON 视图 让我们利用 Groovy 的 StreamingJsonBuilder
使用静态编译的 Groovy 视图来呈现数据。JSON 视图提供了一个基于 DSL 的强大工具,用于表示 API 中的 JSON 输出。
4.1 引入 JSON 视图
这是一个 JSON 视图的小示例
json.message {
hello "world"
}
呈现时,此 JSON 视图将生成以下输出
{"message":{ "hello":"world"}}
JSON 视图还支持 model
,它引用传递到视图中的数据,如下所示
model {
String message
}
json.message {
hello message
}
JSON 视图是具有 gson
文件扩展名的 Groovy 文件,它们驻留在 grails-app/views
目录中,就像 GSP 视图一样。它们被解析到与视图目录同名的控制器中(同样与 GSP 视图一样,这是约定俗成的做法)。
4.2 自定义 API
让我们创建一个 JSON 视图来自定义来自 /api/orders/$id
端点的输出。现在,默认 JSON 呈现器包括所有关联对象的 ID。但是,我们不想公开 shippingAddress
属性的 id
(它是 Address
域类的一个实例) - 它不是 API 中公开的域资源,对于 API 用户而言,它仅作为 Order
或 Customer
的一部分具有相关性。理想情况下,我们希望将 shippingAddress
字段包含在 order API 的 JSON 输出中。
此外,我们希望将 orderId
属性表示为 order 的 id
,而不是数据库中的实际 id。
在 grails-app/views
下创建一个名为 order
的新目录
$ mkdir grails-app/views/order
如果你熟悉 Grails 的视图解析,你可能会认为我们需要创建一个 OrderController 才能从 order 目录中使用视图。我们确实可以这样做,然而,由于我们正在我们的域类上使用 @Resource 注解,Grails 将生成为我们生成一个关联的 OrderController (它又将从 RestfulController 继承)。因此,此时,我们不需要为 Order 类创建控制器。 |
创建名为 show.gson
的新 JSON 视图。它将解析为控制器中的 show
操作,就像 show.gsp
页面在常规 Grails 应用程序中所做的那样。使用以下内容编辑新视图
import com.example.Order
model {
Order order
}
json {
id order.orderId
shippingCost order.shippingCost
date order.orderPlaced.format('M-dd-yyy') (1)
shippingAddress { (2)
street order.shippingAddress.street
street2 order.shippingAddress.street2
city order.shippingAddress.city
state order.shippingAddress.state
zip order.shippingAddress.zip
}
products order.products.collect { [id: it.id] } (3)
customer {
id order.customer.id
}
}
1 | 请注意,我们正在 Date 类中使用 Groovy 的 format 方法自定义 orderPlaced 属性的格式 |
2 | 这里我们使用 Address 类的字段填充我们的 shippingAddress |
3 | 请注意,我们正在通过 collect 方法遍历集合 (order.products ) 并返回一个映射 - 这将创建一个对象的 JSON 数组 |
现在,如果您向 /api/orders/1
发出请求,您应该会看到以下输出
curl -H "Accept: application/json" localhost:8080/api/orders/1
{
id: "0A12321",
shippingCost: 13.54,
date: "2-08-2017",
shippingAddress: {
street: "321 Arrow Ln",
street2: null,
city: "Chicago",
state: "IL",
zip: 646465
},
products: [
{
id: 11
},
{
id: 1
},
{
id: 6
}
],
customer: {
id: 1
}
}
让我们为我们的 Customer
域类创建另一个 JSON 视图。在 grails-app/views
下创建一个名为 customer
的新目录,并创建一个新的 JSON 视图 show.gson
$ mkdir grails-app/views/customer
创建名为 show.gson
的新 JSON 视图。它将解析为控制器中的 show
操作,就像 show.gsp
页面在常规 Grails 应用程序中所做的那样。使用以下内容编辑新视图
import com.example.Customer
model {
Customer customer
}
json {
id customer.id
firstName customer.firstName
lastName customer.lastName
fullName "${customer.firstName} ${customer.lastName}"
address {
street customer.address.street
street2 customer.address.street2
city customer.address.city
state customer.address.state
zip customer.address.zip
}
orders customer.orders.collect { [id: it.id] }
}
5 通过 HAL 使我们的 API 可被发现
我们现在在某种程度上自定义了我们的 API,但我们有一些问题
-
由于为订单自定义了我们的
id
属性,因此对于客户端来说如何引用一个颗粒记录不再明确(因为我们的 API 仍然依赖于数据库 id) -
我们公开的三个域类有许多仅公开 id 的关联 - 这意味着客户端需要发出一个新请求来获取关联对象(例如,使用订单的客户端将需要发出单独的请求来获取其产品的字段)的详细信息
-
我们的 API 相当不透明 - 如果没有文档,我们的 API 用户必须猜测端点才能到达关联记录。即使有文档,客户端可能也需要使用自定义代码来浏览我们的 API,而没有一个一致的标准要遵循。
HAL+JSON 标准的惯例可以帮助我们解决这些问题,并且 JSON 视图为 HAL 提供了一流的支持 - 让我们看看如何使用它。
5.1 链接资源
链接是 HAL 标准中的关键。HAL 资源包括一个名为 _links
的特殊字段,其中包含一个 JSON 对象数组,定义到相关资源的链接。HAL 链接包含(至少)两条信息 - 一种关系和一个包含用于访问相关资源的 URL 的 href
。还可以包含其他元数据。
这里有一个 JSON 正文示例,其中包含一个 _links
字段和两个链接
{
title: "Groovy Recipes",
author: "Scott Davis",
pages: 100,
"_links": {
"self": {
"href": "http://localhost:8080/book/show/1", (1)
},
"author": {
"href": "http://localhost:8080/author/show/1", (2)
}
}
}
1 | self 是每个 HAL 资源都应该包含的一个特殊链接 - 它指定返回到当前资源的 URL。 |
2 | 这里我们定义了一个名为 author 的自定义链接,它指定 author 字段的 URL |
当客户端访问 HAL 资源时,可以在 _links
字段中表示关系处“浏览”而无需客户端知道 API 端点的确切组成。
//examples are using the fetch API: https://mdn.org.cn/en-US/docs/Web/API/Fetch_API
//retrieve a book instance from the API
fetch("http://localhost:8080/api/book/1").then(function(response) {
return response.json();
}).then(function(data) {
this.book = data;
});
//retrieve book's author using links
var author;
fetch(book._links.author.href).then(function(response) {
return response.json();
}).then(function(data) {
author = data;
});
JSON 视图实现了一个 Groovy 特性 HalView
,它公开了带有用于输出符合 HAL 标准的 JSON 的多个方法的 hal
帮助器。其中之一是 links
方法。
model {
Order order
}
json {
hal.links(order)
//...
}
在我们的域资源上调用 hal.links()
将产生以下 JSON 输出
{
_links: {
self: {
href: "http://localhost:8080/api/orders/1",
hreflang: "en_US",
type: "application/hal+json"
}
}
}
让我们编辑我们的 order/show.gson
视图,以包括一个 self
链接和到相关客户的链接
import com.example.Order
model {
Order order
}
json {
hal.links(self: order, customer: order.customer) (1)
id order.orderId
shippingCost order.shippingCost
date order.orderPlaced.format('M-dd-yyy')
shippingAddress {
street order.shippingAddress.street
street2 order.shippingAddress.street2
city order.shippingAddress.city
state order.shippingAddress.state
zip order.shippingAddress.zip
}
products order.products.collect { [id: it.id] }
customer {
id order.customer.id
}
}
1 | links 方法可以采用域资源实例,或要链接的链接名称和对象的映射。 |
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/orders?id=1",
hreflang: "en",
type: "application/hal+json"
},
customer: {
href: "http://localhost:${serverPort}/api/customers?id=1",
hreflang: "en",
type: "application/hal+json"
}
},
id: "0A12321",
shippingCost: 13.54,
date: "2-08-2017",
shippingAddress: {
street: "321 Arrow Ln",
street2: null,
city: "Chicago",
state: "IL",
zip: 646465
},
products: [
{
id: 11
},
{
id: 1
},
{
id: 6
}
],
customer: {
id: 1
}
}
//...
}
5.2 使用模板呈现集合
Order
包含与 Product
的一对多关系,现在我们的 API 返回一个简单的 ID 列表来表示订单中的产品。理想情况下,我们还希望包含到这些产品的链接,以便 API 的客户端可以通过跟踪我们提供的链接来检索每个产品的详细信息。我们可以使用 JSON 视图的模板功能。
在 grails-app/views/order
目录中,创建一个名称为 _product.gson
的新 JSON 模板
import com.example.Product
model {
Product product
}
json {
hal.links(product)
id product.id
}
现在在我们的 order/show.gson
视图中,我们可以使用我们的新 _product
模板将 order.products
传递至 tmpl
帮助器
import com.example.Order
model {
Order order
}
json {
hal.links(self: order, customer: order.customer)
id order.orderId
shippingCost order.shippingCost
date order.orderPlaced.format('M-dd-yyy')
shippingAddress {
street order.shippingAddress.street
street2 order.shippingAddress.street2
city order.shippingAddress.city
state order.shippingAddress.state
zip order.shippingAddress.zip
}
products tmpl.product(order.products) (1)
}
1 | tmpl 帮助器会将一个方法名称解析为当前视图目录中的一个模板 - 例如,tmpl.product 将解析为 /order/_product.gson 。如果你想访问当前目录之外的模板,可以使用一个绝对路径(相对于 views 目录)作为字符串:tmpl."/customer/order"() 将解析为 grails-app/views/customer/_order.gson 。 |
有关在 JSON 视图中使用模板的更多信息,请参阅 Grails 视图文档。 |
对 http://localhost:8080/api/orders/1
发出请求,你应该会看到 products
数组中每个产品的 _links
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/orders?id=1",
hreflang: "en",
type: "application/hal+json"
},
customer: {
href: "http://localhost:${serverPort}/api/customers?id=1",
hreflang: "en",
type: "application/hal+json"
}
},
id: "0A12321",
shippingCost: 13.54,
date: "2-08-2017",
shippingAddress: {
street: "321 Arrow Ln",
street2: null,
city: "Chicago",
state: "IL",
zip: 646465
},
products: [
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/products/11",
hreflang: "en",
type: "application/hal+json"
}
},
id: 11
},
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/products/6",
hreflang: "en",
type: "application/hal+json"
}
},
id: 6
},
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/products/1",
hreflang: "en",
type: "application/hal+json"
}
},
id: 1
}
]
}
让我们对 客户的订单 - 创建一个新模板在 `grails-app/views/customer/_order.gson`
中使用相同的技术
import com.example.Order
model {
Order order
}
json {
hal.links(order)
id order.id
}
编辑 customer/show.gson
视图以使用新的 _order
模板
让我们对 客户的订单 - 创建一个新模板在 `grails-app/views/customer/_order.gson`
中使用相同的技术
import com.example.Customer
model {
Customer customer
}
json {
id customer.id
firstName customer.firstName
lastName customer.lastName
fullName "${customer.firstName} ${customer.lastName}"
address {
street customer.address.street
street2 customer.address.street2
city customer.address.city
state customer.address.state
zip customer.address.zip
}
orders tmpl.order(customer.orders)
}
5.3 嵌入关联对象
我们来看一下我们的 Product
域资源。Product
具有与 Category
的 belongsTo
关系,它在我们的默认 JSON 输出中以一个包含类别 ID 的简单对象来表示
{
id: 1,
category: {
id: 1
},
inventoryId: "CLOTH001",
name: "Cargo Pants",
price: 15
}
我们再次希望让 API 使用者更容易获取产品的类别。我们有多个选项
-
我们可以在 JSON 视图中直接包含
category
详细信息:这种方法混淆了我们的 API 中Product
和Category
资源之间的界限 - 它给客户端(错误的)印象:认为category.name
是Product
的属性,而不是一个本身就是 API 资源。 -
我们可以提供到该类别的链接:此方法要求客户端发出新的请求来获取类别详细信息,并且很可能大多数客户端在同一请求中需要产品和类别详细信息。
您可能还记得,在订单的shippingAddress 的情况下,我们使用这两种方法中的第一种(在 JSON 视图中包括关联对象的详细信息)——这是因为我们的 API 中未将Address 公开为资源,因此就我们的 API 而言,Address 实际上是Order (shippingAddress )或Customer (address )的一部分。 |
HAL 指定一个_embedded
属性来以嵌套格式表示资源间的关联关系。使用嵌入方法,我们可以将Category
包含在同一个 HAL+JSON 响应中,但该类别将处于一个单独的元素中,以阐明这些是单独的资源。
JSON 视图提供了一个embedded
方法(通过hal
帮助程序)将在我们的 JSON 视图中生成一个_embedded
元素。它将包含嵌入对象中默认 JSON 输出以及每个对象的_self
链接。我们用这个在我们的产品输出中嵌入类别。
在grails-app/views
下创建一个名为product
的新目录
$ mkdir grails-app/views/product/
在此目录下创建一个名为show.gson
的新 JSON 视图
import com.example.Product
model {
Product product
}
json {
hal.links(product)
hal.embedded(category: product.category) (1)
id product.inventoryId
name product.name
price product.price
}
1 | 我们向embedded 方法传入一个元素名称(在本例中为category )到嵌入对象的映射(product.category ) |
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"category": {
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/categories/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Clothing",
"version": 0
}
},
"id": "CLOTH001",
"name": "Cargo Pants",
"price": 15.00
}
在上一个代码片段中,category
对象包括我们的Category
资源的默认 JSON 渲染程序输出,以及一个_self
链接,以便在需要时客户端可以直接请求类别。
让我们使用order/show.gson
视图上的embedded 方法,并嵌入 `order.customer` 资源。
import com.example.Order
model {
Order order
}
json {
hal.links(self: order, customer: order.customer)
hal.embedded(customer: order.customer) (1)
}
},
"_embedded": {
"customer": {
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/customers/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"firstName": "Peter",
"lastName": "River",
"version": 0
现在我们 API 的客户端可以访问嵌入资源的详细信息,而无需发出其他请求。
//examples are using the fetch API: https://mdn.org.cn/en-US/docs/Web/API/Fetch_API
//retrieve an order instance from the API
fetch("http://localhost:8080/api/orders/1").then(function(response) {
return response.json();
}).then(function(data) {
this.order = data;
});
var customer = this.order._embedded.customer;
console.log("Order ID: " + this.order.id);
console.log("Customer: " + customer.firstName + " " + customer.lastName);
//retrieve a product instance from the API
fetch("http://localhost:8080/api/products/1").then(function(response) {
return response.json();
}).then(function(data) {
this.product = data;
});
console.log("Product: " + this.product.name);
console.log("Category:" + this.order._embedded.category.name);
5.4 分页结果
HAL 标准指定的另一个约定是分页。在提供资源列表时,_links
元素可以提供first
、prev
、next
和last
链接,可用于浏览资源列表。
hal
帮助程序提供一个paginate
方法,该方法将生成这些链接并处理资源的分页。此方法要求我们 JSON 视图的model
中的更多信息,以便跟踪当前偏移量、每页的最大记录数以及资源总数。为了做到这一点,我们需要创建一个控制器,以便我们可以传入所需的模型参数。
让我们在我们的Product
资源上使用 HAL 分页链接。
因为我们将创建自己的 ProductController
来提供分页所需的参数,我们需要移除 Product
领域类中一直使用的 @Resource
注释。编辑 grails-app/domain/com/example/Product.groovy
package com.example
class Product {
String name
String inventoryId
BigDecimal price
static belongsTo = [ category : Category ]
}
在开发 RESTful API 时,你通常会发现 @Resource 生成的控制器和 URL 映射是入门的好方法,但有时你需要更多地控制 API - 这时候生成你自己的 RestfulController 是一个好的解决方案。 |
现在,使用 create-restful-controller
命令(由 rest-api
配置文件提供)创建一个新的 RestfulController
$ ./grailsw create-restful-controller com.example.ProductController
用如下内容编辑这个新控制器
package com.example
import grails.rest.RestfulController
class ProductController extends RestfulController<Product> {
static responseFormats = ['json']
ProductController() {
super(Product)
}
@Override
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
return [
productList : listAllResources(params), (1)
productCount: countResources(), (2)
max : params.max, (3)
offset : params.int("offset") ?: 0, (4)
sort : params.sort, (5)
order : params.order (6)
]
}
@Override
boolean getReadOnly() {
return true
}
}
1 | listAllResource() 由 RestfulController 提供,用于返回所有领域资源的列表 - 你可以覆盖此方法以控制此列表的生成方式 |
2 | countResources() 是另一个 RestfulController 方法 - 再次声明,你可以覆盖实现以适应你的 API |
3 | 每页结果总数 |
4 | 偏移量(用于计算当前页) |
5 | 排序属性 |
6 | 排序方向 |
最后,我们需要编辑 UrlMappings
以创建以前使用 @Resource
注释生成的 REST 端点。Grails 在 URL 映射中支持 resource
属性,它将自动生成这些 URL。编辑 UrlMappings.groovy
并将以下规则添加到 mappings
块
"/api/products"(resources: "product")
现在,我们可以使用分页创建新的 JSON 视图。在 grails-app/views/product
下创建以下视图和模板
import com.example.Product
model {
Iterable<Product> productList (1)
Integer productCount (2)
Integer max
Integer offset
String sort
String order
}
json {
hal.paginate(Product, productCount, offset, max, sort, order) (3)
products tmpl.product(productList ?: [])
count productCount
max max
offset offset
sort sort
order order
}
1 | 产品资源列表 |
2 | 来自我们控制器的分页参数 |
3 | 在这里,我们将分页参数传递给 paginate 方法,它将生成 HAL 分页链接 |
import com.example.Product
model {
Product product
}
json {
hal.links(product)
name product.name
id product.inventoryId
price product.price
category product.category.name
}
"""
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products?offset=0&max=10",
"hreflang": "en",
"type": "application/hal+json"
},
"first": {
"href": "http://localhost:${serverPort}/api/products?offset=0&max=10",
"hreflang": "en"
},
"next": {
"href": "http://localhost:${serverPort}/api/products?offset=10&max=10",
"hreflang": "en"
},
"last": {
"href": "http://localhost:${serverPort}/api/products?offset=10&max=10",
"hreflang": "en"
}
},
"products": [
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Cargo Pants",
"id": "CLOTH001",
"price": 15.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/2",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Sweater",
"id": "CLOTH002",
"price": 12.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/3",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Jeans",
"id": "CLOTH003",
"price": 15.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/4",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Blouse",
"id": "CLOTH004",
"price": 18.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/5",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "T-Shirt",
"id": "CLOTH005",
"price": 10.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/6",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Jacket",
"id": "CLOTH006",
"price": 20.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/7",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Bookcase",
"id": "FURN001",
"price": 40.00,
"category": "Furniture"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/8",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Coffee Table",
"id": "FURN002",
"price": 50.00,
"category": "Furniture"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/9",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Vanity",
"id": "FURN003",
"price": 90.00,
"category": "Furniture"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/10",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Table Saw",
"id": "TOOL001",
"price": 120.00,
"category": "Tools"
}
],
"count": 13,
"max": 10,
"offset": 0,
"sort": null,
"order": null
}
向 next
链接 http://localhost:8080/product/index?offset=10&max=10
发出请求,你将看到下一页结果。由于示例数据中的资源数量较少,因此只有 2 页 - 尝试将请求中的 max
参数更改为 4 - 现在你将检索其他页面以反映较小的页面大小。
如果你愿意,可以重复这些步骤为其他领域资源(如 Order
和 Customer
)启用分页。
5.5 自定义 MIME 类型
HAL 资源可以声明一个自定义的“MIME 类型”(或“内容类型”),以便客户端使用它与 API 交互。Grails 在默认的 application.yml
中包含两个通用的 HAL MIME 类型
accept:
header:
userAgents:
- Gecko
- WebKit
- Presto
- Trident
types:
json:
- application/json
- text/json
hal:
- application/hal+json
- application/hal+xml
xml:
- text/xml
- application/xml
如果您愿意,可以通过向此配置添加条目来为您的 API 指定自定义 MIME 类型
xml:
- text/xml
- application/xml
atom: application/atom+xml
css: text/css
csv: text/csv
js: text/javascript
rss: application/rss+xml
text: text/plain
all: '*/*'
inventory: "application/vnd.com.example.inventory+json" (1)
urlmapping:
cache:
maxsize: 1000
1 | 指定一个称为 inventory 的 MIME 类型,然后向它提供类型规范(根据惯例,vnd 表示“供应商”MIME 类型) |
现在,您可以在 JSON 视图中使用 type
帮助程序方法使用此自定义 MIME 类型
import com.example.Product
model {
Product product
}
json {
hal.links(product)
hal.embedded(category: product.category)
hal.type("inventory") (1)
id product.inventoryId
name product.name
price product.price
}
1 | hal.type() 方法接受一个字符串来识别 application.yml 文件中的自定义 MIME 类型,或接受一个作为字符串的显式 MIME 规范 |
向 http://localhost:8080/api/products/1
发出请求,以查看自定义内容类型
$ curl -i localhost:8080/product/1
HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/vnd.com.example.inventory+json;charset=UTF-8 (1)
Transfer-Encoding: chunked
Date: Sun, 05 Feb 2017 01:51:08 GMT
def result = render(view: "/product/show", model:[product: product])
then:
result.json._embedded
}
}
1 | 注意 Content-Type 标头中的新 MIME 类型 |
6 探索 API
HAL+JSON 是一种用于构建 API 输出的灵活且强大的规范。无论第三方整合、您自己的网络应用还是原生应用是 API 的客户端,它都可以以一种有效且一致的方式浏览您的资源。
有很多有用的工具用于开发和测试 HAL+JSON API。如果您使用 Google Chrome 浏览器,请尝试安装 JSONView 插件,然后使用浏览器导航至 http://localhost:8080/api/products
。此插件将以一种友好的格式呈现 JSON,并允许您在浏览器中关注资源之间的链接。
Postman 是一款基于 Google Chrome 的应用,支持 macOS、Windows、Linux 和 Chrome 操作系统,是用于测试 API 的另一个强大工具。