启用数据后端服务
正如我们在第3章所看到的,Spring Data有一种特殊的能力,能够基于我们定义的接口自动创建存储库实现。但是Spring Data还有另外一项技巧,能够帮助我们定义应用的API。
Spring Data REST是Spring Data家族中另外一个成员,它会为Spring Data创建的存储库自动生成REST API。只需要将Spring Data REST添加到构建文件中,就能得到一套API,它的操作与我们定义存储库接口是一致的。
为了使用Spring Data REST,需要将如下的依赖添加到构建文件中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
不管你是否相信,对于已经使用Spring Data自动生成存储库的项目,只需要完成这一步就能对外暴露REST API了。将Spring Data REST starter依赖添加到构建文件中之后,应用的自动配置功能会为Spring Data(包括Spring Data JPA、Spring Data Mongo等)创建的所有存储库自动创建REST API。
Spring Data REST所创建的端点和我们自己创建的端点一样好(甚至比我们创建的端点更好一些)。所以,可以做一些移除操作,在进行下一步之前将我们已经创建的带有@RestController注解的类移除。
为了尝试Spring Data REST提供的端点,可以启动应用并测试一些URL。基于为Taco Cloud已经定义的存储库,我们可以对taco、配料、订单和用户执行一些GET请求。
举例来说,我们可以向“/ingredients”发送GET请求以获取所有配料的列表。借助curl,我们得到的响应大致如下所示(有删减,只显示第一种配料):
$ curl localhost:8080/ingredients
{
"_embedded" : {
"ingredients" : [ {
"name" : "Flour Tortilla",
"type" : "WRAP",
"_links" : {
"self" : {
"href" : "http://localhost:8080/ingredients/FLTO"
},
"ingredient" : {
"href" : "http://localhost:8080/ingredients/FLTO"
}
}
},
...
]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/ingredients"
},
"profile" : {
"href" : "http://localhost:8080/profile/ingredients"
}
}
}
太棒了!将一项依赖添加到了构建文件中,不仅能得到针对配料的端点,而且返回的资源中还包含了超链接。这些超链接是通过超媒体作为应用状态引擎(Hypermedia As The Engine Of Application State, HATEOAS)实现的。消费这个API的客户端可以使用这些超链接作为指南,以便于导航API并执行后续的请求。
Spring HATEOAS项目为在Spring MVC控制器的响应中添加超链接提供了通用的支持。但是,Spring Data REST会在生成的API中自动向响应中添加这些链接。
我们可以假装成这个API的客户端,使用curl继续访问self链接以获取面粉薄饼(flour tortilla)的详情:
$ curl http://localhost:8080/ingredients/FLTO
{
"name" : "Flour Tortilla",
"type" : "WRAP",
"_links" : {
"self" : {
"href" : "http://localhost:8080/ingredients/FLTO"
},
"ingredient" : {
"href" : "http://localhost:8080/ingredients/FLTO"
}
}
}
为了避免分散注意力,在本书中,我们不再浪费时间深入探究Spring Data REST所创建的每个端点和可选项。但是,我们需要知道,它还支持端点的POST、PUT和DELETE方法。也就是说,你可以发送POST请求至“/ingredients”来创建新的配料,也可以发送DELETE请求到“/ingredients/FLTO”以便于从菜单中删除面粉薄饼。
我们想做的另外一件事可能就是为API设置一个基础路径,使它们具有不同的端点,避免与我们所编写的控制器产生冲突。为了调整API的基础路径,可以设置spring.data.rest.base-path属性:
spring:
data:
rest:
base-path: /data-api
这项配置会将Spring Data REST端点的基础路径设置为“/data-api”。尽管我们可以将基础路径设置为任意喜欢的值,但是在这里选择使用“/data-api”能够避免Spring Data REST暴露出来的端点与其他控制器的端点冲突,包括我们本章前面所创建的以“/api”路径开头的端点。现在,配料端点将会变成“/data-api/ingredients”。我们通过请求taco列表来验证一下这个新的基础路径:
$ curl http://localhost:8080/data-api/tacos
{
"timestamp": "2018-02-11T16:22:12.381 + 0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/tacos"
}
很遗憾,它并没有按照预期的方式运行。有了Ingredient实体和IngredientRepository接口之后,Spring Data REST就会暴露“data-api/ingredients”端点。我们也有Taco实体和TacoRepository接口,为什么Spring Data REST没有为我们生成“/data-api/tacos”端点呢?
调整资源路径和关系名称
实际上,Spring Data确实为我们提供了处理taco的端点。Spring Data REST虽然非常聪明,但是在暴露taco端点时,还是出现了一点问题。
当为Spring Data存储库创建端点时,Spring Data REST会尝试使用相关实体类的复数形式。对于Ingredient实体,端点将会是“/data-api/ingredients”;对于TacoOrder实体,端点将会是“/data-api/orders”。到目前为止,一切运行良好。
但有些场景下,比如遇到“taco”的情况,它获取到这个单词之后,为其生成的复数形式就不太正确了。实际上,Spring Data REST将“taco”的复数形式计算成了“tacoes”,所以,为了向taco发送请求,我们可以将错就错,请求“/data-api/tacoes”地址:
$ curl localhost:8080/data-api/tacoes
{
"_embedded" : {
"tacoes" : [ {
"name" : "Carnivore",
"createdAt" : "2018-02-11T17:01:32.999 + 0000",
"_links" : {
"self" : {
"href" : "http://localhost:8080/data-api/tacoes/2"
},
"taco" : {
"href" : "http://localhost:8080/data-api/tacoes/2"
},
"ingredients" : {
"href" : "http://localhost:8080/data-api/tacoes/2/ingredients"
}
}
}]
},
"page" : {
"size" : 20,
"totalElements" : 3,
"totalPages" : 1,
"number" : 0
}
}
你肯定会想,我是怎么知道“taco”的复数形式被错误计算成了“tacoes”呢。实际上,Spring Data REST还暴露了一个主资源(home resource),这个资源包含了所有端点的链接。只需要向API的基础路径发送GET请求,就能得到它的结果:
$ curl localhost:8080/api
{
"_links" : {
"orders" : {
"href" : "http://localhost:8080/data-api/orders"
},
"ingredients" : {
"href" : "http://localhost:8080/data-api/ingredients"
},
"tacoes" : {
"href" : "http://localhost:8080/data-api/tacoes{?page,size,sort}",
"templated" : true
},
"users" : {
"href" : "http://localhost:8080/data-api/users"
},
"profile" : {
"href" : "http://localhost:8080/data-api/profile"
}
}
}
可以看到,这个主资源显示了所有实体的链接。除了tacoes链接之外,其他都很正常,在这里关系名和URL地址上都是错误的复数形式“tacoes”。
好消息是,我们并非必须接受Spring Data REST的这个小错误。通过为Taco添加一个简单的注解,我们就能调整关系名和路径:
@Data
@Entity
@RestResource(rel = "tacos", path = "tacos")
public class Taco {
...
}
@RestResource注解能够为实体提供任何我们想要的关系名和路径。在本例中,我们将它们都设置成了“tacos”。现在,我们请求主资源的时候,会看到taco的正确复数形式“tacos”:
"tacos" : {
"href" : "http://localhost:8080/data-api/tacos{?page,size,sort}",
"templated" : true
},
这样我们就整理好了端点路径,现在就可以向“/data-api/tacos”发送请求来操作taco资源了。
接下来我们看一下如何对Spring Data REST端点的结果进行排序。
分页和排序
你可能已经发现,主资源上的所有链接都提供了可选的page、size和sort参数。默认情况下,请求集合资源(比如“/data-api/tacos”)都会返回首页的20个条目。但是,可以通过在请求中指定page和size参数调整具体的页数和每页的数量。
例如,如果我们想要请求首页的taco,但是仅希望结果包含5个条目,可以发送如下的GET请求(使用curl):
$ curl "localhost:8080/data-api/tacos?size = 5"
如果taco的数量超过了5个,可以使用page参数获取次页的taco:
$ curl "localhost:8080/data-api/tacos?size = 5&page = 1"
注意,page参数是从0开始计算的,也就是说page值为1的时候,会请求次页的数据。(你可能会发现,很多命令行shell遇到请求中的&符号会出错,所以我们在前面的curl命令中,为整个URL使用了引号)。
sort参数允许我们根据实体的某个属性对结果排序。例如,想要获取最近创建的12条taco进行UI展示,可以混合使用分页和排序参数实现:
$ curl "localhost:8080/data-api/tacos?sort = createdAt,desc&page = 0&size = 12"
在这里,sort 参数指定我们要按照 createdAt 属性排序,并且要按照降序排列(所以最新的 taco 会放在最前面)。page 和 size 参数指定我们想要获取首页的12个taco。
这恰好是UI展现最近创建的taco所需要的数据。它与我们在本章前文TacoController定义的“/api/tacos?recent”端点大致相同。
现在,我们调转方向,看一下如何编写客户端代码来消费我们创建的API端点。