7.1 使用 RestTemplate 调用 REST 端点
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<String, String> 中指定。
- 一种是接受 java.net.URI 作为 URL 规范,不支持参数化 URL。
一旦了解了 RestTemplate 提供的 12 个操作以及每种变体的工作方式,就可以很好地编写调用资源的 REST 客户端了。
要使用 RestTemplate,需要创建一个实例:
RestTemplate rest = new RestTemplate();
或是将它声明为一个 bean,在需要它的时候将其注入:
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
让我们通过查看支持四种主要 HTTP 方法(GET、PUT、DELETE 和 POST)的操作来探寻 RestTemplate 的操作。我们将从 getForObject() 和 getForEntity() —— GET 方法开始。
7.1.1 请求 GET 资源
假设想从 Taco Cloud API 获取一个 Ingredient 数据。假设 API 没有启用 HATEOAS,需要使用 getForObject() 来获取 Ingredient。例如,下面的代码使用 RestTemplate 获取一个 Ingredient 对象的 ID:
public Ingredient getIngredientById(String ingredientId) {
return rest.getForObject("http://localhost:8080/ingredients/{id}",
Ingredient.class, ingredientId);
}
这里使用的是 getForObject() 变量,它接受一个字符串 URL 并为 URL 变量使用一个变量列表。传递给 getForObject() 的 ingredientId 参数用于填充给定 URL 中的 {id}
占位符。虽然在本例中只有一个 URL 变量,但重要的是要知道变量参数是按给定的顺序分配给占位符的。
getForObject() 的第二个参数是响应应该绑定的类型。在这种情况下,应该将响应数据(可能是 JSON 格式)反序列化为将要返回的 Ingredient 对象。
或者,可以使用映射来指定 URL 变量:
public Ingredient getIngredientById(String ingredientId) {
Map<String, String> urlVariables = new HashMap<>();
urlVariables.put("id", ingredientId);
return rest.getForObject("http://localhost:8080/ingredient/{id}",
Ingredient.class, urlVariables);
}
在这个例子中,ingredientId 的值被映射到 id 键上,当发出请求时,{id}
占位符被键为 id 的映射条目替换。
使用 URI 参数稍微复杂一些,需要在调用 getForObject() 之前构造一个 URI 对象,它类似于其他两中形式:
public Ingredient getIngredientById(String ingredientId) {
Map<String,String> urlVariables = new HashMap<>();
urlVariables.put("id", ingredientId);
URI url = UriComponentsBuilder
.fromHttpUrl("http://localhost:8080/ingredients/{id}")
.build(urlVariables);
return rest.getForObject(url, Ingredient.class);
}
这里的 URI 对象是根据字符串规范定义的,其占位符是根据映射中的条目填充的,这与前面的 getForObject() 形式非常相似。getForObject() 方法是获取资源的一种有效方法。但是,如果客户端需要的不仅仅是有效负载,可能需要考虑使用 getForEntity()。
getForEntity() 的工作方式与 getForObject() 非常相似,但它返回的不是表示响应有效负载的域对象,而是包装该域对象的 ResponseEntity 对象。ResponseEntity 允许访问附加的响应细节,比如响应头。
例如,假设除了 Ingredient 数据之外,还希望检查响应中的 Date 头信息,有了 getForEntity(),事情就简单多了:
public Ingredient getIngredientById(String ingredientId) {
ResponseEntity<Ingredient> responseEntity =
rest.getForEntity("http://localhost:8080/ingredients/{id}",
Ingredient.class, ingredientId);
log.info("Fetched time: " +
responseEntity.getHeaders().getDate());
return responseEntity.getBody();
}
getForEntity() 方法使用与 getForObject() 相同的重载参数,因此可以将 URL 变量作为变量列表参数,或者使用 URI 对象调用 getForEntity()。
7.1.2 请求 PUT 资源
对于发送 HTTP PUT 请求,RestTemplate 提供 put() 方法。put() 的所有三个重载方法都接受一个将被序列化并发送到给定 URL 的对象。至于 URL 本身,可以将其指定为 URI 对象或 String。与 getForObject() 和 getForEntity() 类似,URL 变量可以作为变量参数列表或 Map 提供。
假设想要用来自一个新的 Ingredient 对象的数据来替换配料资源。下面的代码应该可以做到这一点:
public void updateIngredient(Ingredient ingredient) {
rest.put("http://localhost:8080/ingredients/{id}",
ingredient,
ingredient.getId());
}
这里 URL 以 String 的形式给出,并有一个占位符,该占位符由给定的 Ingredient 对象的 id 属性替换。要发送的数据是 Ingredient 对象本身。put() 方法返回 void,因此不需要处理返回值。
7.1.3 请求 DELETE 资源
假设 Taco Cloud 不再提供一种配料,并希望将其作为一种选项完全删除。要做到这一点,可以从 RestTemplate 中调用 delete() 方法:
public void deleteIngredient(Ingredient ingredient) {
rest.delete("http://localhost:8080/ingredients/{id}",
ingredient.getId());
}
在本例中,仅将 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():
public Ingredient createIngredient(Ingredient ingredient) {
return rest.postForObject("http://localhost:8080/ingredients",
ingredient,
Ingredient.class);
}
postForObject() 方法的这种形式采用 String 作为 URL 规范,要发送到服务器的对象以及响应主体应该绑定到的域类型。虽然在本例中没有利用它,但第四个参数可以是 URL 变量值的 Map 或要替换到 URL 中的参数的变量列表。
如果客户对新创建的资源的位置有更多的需求,那么可以调用 postForLocation():
public URI createIngredient(Ingredient ingredient) {
return rest.postForLocation("http://localhost:8080/ingredients",
ingredient);
}
注意,postForLocation() 的工作方式与 postForObject() 非常相似,只是它返回的是新创建资源的 URI,而不是资源对象本身。返回的 URI 派生自响应的 Location 头信息。如果同时需要位置和响应负载,可以调用 postForEntity():
public Ingredient createIngredient(Ingredient ingredient) {
ResponseEntity<Ingredient> responseEntity =
rest.postForEntity("http://localhost:8080/ingredients",
ingredient,
Ingredient.class);
log.info("New resource created at " +
responseEntity.getHeaders().getLocation());
return responseEntity.getBody();
}
虽然 RestTemplate 方法的用途不同,但是它们的使用方式非常相似。这使得你很容易精通 RestTemplate 并在客户端代码中使用它。
另一方面,如果使用的 API 在其响应中包含超链接,那么 RestTemplate 就没有那么有用了。当然可以使用 RestTemplate 获取更详细的资源数据,并处理其中包含的内容和链接,但是这样做并不简单。在使用 RestTemplate 调用超媒体 API 时,与其挣扎,不如将注意力转移到为这类事情创建的客户端库 —— Traverson。