7.1 使用 RestTemplate 调用 REST 端点

从客户的角度来看,与 REST 资源进行交互需要做很多工作 —— 主要是单调乏味的样板文件。使用低级 HTTP 库,客户端需要创建一个客户端实例和一个请求对象,执行请求,解释响应,将响应映射到域对象,并处理过程中可能抛出的任何异常。不管发送什么 HTTP 请求,所有这些样板文件都会重复。

为了避免这样的样板代码,Spring 提供了 RestTemplate。正如 JDBCTemplate 处理使用 JDBC 糟糕的那部分一样,RestTemplate 使你不必为调用 REST 资源而做单调的工作。

RestTemplate 提供了 41 个与 REST 资源交互的方法。与其检查它提供的所有方法,不如只考虑 12 个惟一的操作,每个操作都有重载,以形成 41 个方法的完整集合。表 7.1 描述了 12 种操作。

表 7.1 RestTemplate 定义的 12 个唯一操作

方法 描述
delete(…) 对指定 URL 上的资源执行 HTTP DELETE请求
exchange(…) 对 URL 执行指定的 HTTP 方法,返回一个 ResponseEntity,其中包含从响应体映射的对象
execute(…) 对 URL 执行指定的 HTTP 方法,返回一个映射到响应体的对象
getForEntity(…) 发送 HTTP GET 请求,返回一个 ResponseEntity,其中包含从响应体映射的对象
getForObject(…) 发送 HTTP GET 请求,返回一个映射到响应体的对象
headForHeaders(…) 发送 HTTP HEAD 请求,返回指定资源 URL 的 HTTP 请求头
optionsForAllow(…) 发送 HTTP OPTIONS 请求,返回指定 URL 的 Allow 头信息
patchForObject(…) 发送 HTTP PATCH 请求,返回从响应主体映射的结果对象
postForEntity(…) 将数据 POST 到一个 URL,返回一个 ResponseEntity,其中包含从响应体映射而来的对象
postForLocation(…) 将数据 POST 到一个 URL,返回新创建资源的 URL
postForObject(…) 将数据 POST 到一个 URL,返回从响应主体映射的对象
put(…) 将资源数据 PUT 到指定的URL

除了 TRACE 之外,RestTemplate 对于每个标准 HTTP 方式至少有一个方法。此外,execute() 和 exchange() 为使用任何 HTTP 方式发送请求提供了低层的通用方法。表 7.1 中的大多数方法都被重载为三种方法形式:

  • 一种是接受一个 String 作为 URL 规范,在一个变量参数列表中指定 URL 参数。
  • 一种是接受一个 String 作为 URL 规范,其中的 URL 参数在 Map 中指定。
  • 一种是接受 java.net.URI 作为 URL 规范,不支持参数化 URL。

一旦了解了 RestTemplate 提供的 12 个操作以及每种变体的工作方式,就可以很好地编写调用资源的 REST 客户端了。

要使用 RestTemplate,需要创建一个实例:

  1. RestTemplate rest = new RestTemplate();

或是将它声明为一个 bean,在需要它的时候将其注入:

  1. @Bean
  2. public RestTemplate restTemplate() {
  3. return new RestTemplate();
  4. }

让我们通过查看支持四种主要 HTTP 方法(GET、PUT、DELETE 和 POST)的操作来探寻 RestTemplate 的操作。我们将从 getForObject() 和 getForEntity() —— GET 方法开始。

7.1.1 请求 GET 资源

假设想从 Taco Cloud API 获取一个 Ingredient 数据。假设 API 没有启用 HATEOAS,需要使用 getForObject() 来获取 Ingredient。例如,下面的代码使用 RestTemplate 获取一个 Ingredient 对象的 ID:

  1. public Ingredient getIngredientById(String ingredientId) {
  2. return rest.getForObject("http://localhost:8080/ingredients/{id}",
  3. Ingredient.class, ingredientId);
  4. }

这里使用的是 getForObject() 变量,它接受一个字符串 URL 并为 URL 变量使用一个变量列表。传递给 getForObject() 的 ingredientId 参数用于填充给定 URL 中的 {id} 占位符。虽然在本例中只有一个 URL 变量,但重要的是要知道变量参数是按给定的顺序分配给占位符的。

getForObject() 的第二个参数是响应应该绑定的类型。在这种情况下,应该将响应数据(可能是 JSON 格式)反序列化为将要返回的 Ingredient 对象。

或者,可以使用映射来指定 URL 变量:

  1. public Ingredient getIngredientById(String ingredientId) {
  2. Map<String, String> urlVariables = new HashMap<>();
  3. urlVariables.put("id", ingredientId);
  4. return rest.getForObject("http://localhost:8080/ingredient/{id}",
  5. Ingredient.class, urlVariables);
  6. }

在这个例子中,ingredientId 的值被映射到 id 键上,当发出请求时,{id} 占位符被键为 id 的映射条目替换。

使用 URI 参数稍微复杂一些,需要在调用 getForObject() 之前构造一个 URI 对象,它类似于其他两中形式:

  1. public Ingredient getIngredientById(String ingredientId) {
  2. Map<String,String> urlVariables = new HashMap<>();
  3. urlVariables.put("id", ingredientId);
  4. URI url = UriComponentsBuilder
  5. .fromHttpUrl("http://localhost:8080/ingredients/{id}")
  6. .build(urlVariables);
  7. return rest.getForObject(url, Ingredient.class);
  8. }

这里的 URI 对象是根据字符串规范定义的,其占位符是根据映射中的条目填充的,这与前面的 getForObject() 形式非常相似。getForObject() 方法是获取资源的一种有效方法。但是,如果客户端需要的不仅仅是有效负载,可能需要考虑使用 getForEntity()。

getForEntity() 的工作方式与 getForObject() 非常相似,但它返回的不是表示响应有效负载的域对象,而是包装该域对象的 ResponseEntity 对象。ResponseEntity 允许访问附加的响应细节,比如响应头。

例如,假设除了 Ingredient 数据之外,还希望检查响应中的 Date 头信息,有了 getForEntity(),事情就简单多了:

  1. public Ingredient getIngredientById(String ingredientId) {
  2. ResponseEntity<Ingredient> responseEntity =
  3. rest.getForEntity("http://localhost:8080/ingredients/{id}",
  4. Ingredient.class, ingredientId);
  5. log.info("Fetched time: " +
  6. responseEntity.getHeaders().getDate());
  7. return responseEntity.getBody();
  8. }

getForEntity() 方法使用与 getForObject() 相同的重载参数,因此可以将 URL 变量作为变量列表参数,或者使用 URI 对象调用 getForEntity()。

7.1.2 请求 PUT 资源

对于发送 HTTP PUT 请求,RestTemplate 提供 put() 方法。put() 的所有三个重载方法都接受一个将被序列化并发送到给定 URL 的对象。至于 URL 本身,可以将其指定为 URI 对象或 String。与 getForObject() 和 getForEntity() 类似,URL 变量可以作为变量参数列表或 Map 提供。

假设想要用来自一个新的 Ingredient 对象的数据来替换配料资源。下面的代码应该可以做到这一点:

  1. public void updateIngredient(Ingredient ingredient) {
  2. rest.put("http://localhost:8080/ingredients/{id}",
  3. ingredient,
  4. ingredient.getId());
  5. }

这里 URL 以 String 的形式给出,并有一个占位符,该占位符由给定的 Ingredient 对象的 id 属性替换。要发送的数据是 Ingredient 对象本身。put() 方法返回 void,因此不需要处理返回值。

7.1.3 请求 DELETE 资源

假设 Taco Cloud 不再提供一种配料,并希望将其作为一种选项完全删除。要做到这一点,可以从 RestTemplate 中调用 delete() 方法:

  1. public void deleteIngredient(Ingredient ingredient) {
  2. rest.delete("http://localhost:8080/ingredients/{id}",
  3. ingredient.getId());
  4. }

在本例中,仅将 URL(指定为 String)和 URL 变量值赋给 delete()。但是,与其他 RestTemplate 方法一样,可以将 URL 指定为 URI 对象,或者将 URL 参数指定为 Map。

7.1.4 请求 POST 资源

现在,假设向 Taco Cloud 菜单添加了一种新 Ingredient。向 .../ingredients 端点发起 HTTP POST 请求就能实现添加,这个请求的请求体重需要包含 Ingredient 数据。RestTemplate 有三种发送 POST 请求的方法,每种方法都有相同的重载变量来指定 URL。如果想在 POST 请求后收到新创建的 Ingredient 资源,可以像这样使用 postForObject():

  1. public Ingredient createIngredient(Ingredient ingredient) {
  2. return rest.postForObject("http://localhost:8080/ingredients",
  3. ingredient,
  4. Ingredient.class);
  5. }

postForObject() 方法的这种形式采用 String 作为 URL 规范,要发送到服务器的对象以及响应主体应该绑定到的域类型。虽然在本例中没有利用它,但第四个参数可以是 URL 变量值的 Map 或要替换到 URL 中的参数的变量列表。

如果客户对新创建的资源的位置有更多的需求,那么可以调用 postForLocation():

  1. public URI createIngredient(Ingredient ingredient) {
  2. return rest.postForLocation("http://localhost:8080/ingredients",
  3. ingredient);
  4. }

注意,postForLocation() 的工作方式与 postForObject() 非常相似,只是它返回的是新创建资源的 URI,而不是资源对象本身。返回的 URI 派生自响应的 Location 头信息。如果同时需要位置和响应负载,可以调用 postForEntity():

  1. public Ingredient createIngredient(Ingredient ingredient) {
  2. ResponseEntity<Ingredient> responseEntity =
  3. rest.postForEntity("http://localhost:8080/ingredients",
  4. ingredient,
  5. Ingredient.class);
  6. log.info("New resource created at " +
  7. responseEntity.getHeaders().getLocation());
  8. return responseEntity.getBody();
  9. }

虽然 RestTemplate 方法的用途不同,但是它们的使用方式非常相似。这使得你很容易精通 RestTemplate 并在客户端代码中使用它。

另一方面,如果使用的 API 在其响应中包含超链接,那么 RestTemplate 就没有那么有用了。当然可以使用 RestTemplate 获取更详细的资源数据,并处理其中包含的内容和链接,但是这样做并不简单。在使用 RestTemplate 调用超媒体 API 时,与其挣扎,不如将注意力转移到为这类事情创建的客户端库 —— Traverson。