启用数据后端服务

正如我们在第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中自动向响应中添加这些链接。

是否要使用HATEOAS?

HATEOAS的总体理念是让客户端像人类导航网站那样去导航API,也就是跟随链接进行导航。借助HATEOAS,我们不用在客户端中对API的细节进行编码并让客户端为每个请求构建URL。现在,客户端可以从超链接列表中按名称选择一个链接,并使用它来进行下一次请求。通过这种方式,客户端不需要编码来了解API的结构,而可以将API本身作为一个路线图来使用。

另一方面,超链接确实会在有效载荷中增加少量的额外数据。它也增加了一些复杂性,要求客户端知道如何使用这些超链接进行导航。这使得API开发者经常放弃使用HATEOAS,如果API中有超链接,客户端开发者经常会简单地忽略超链接。

除了Spring Data REST响应中自带的超链接外,我们将忽略HATEOAS,专注于简单的、非超媒体的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端点。