7.1 使用RestTemplate消费REST端点

7.1 使用RestTemplate消费REST端点

从客户端的角度来看,与REST资源进行交互涉及很多工作,而且大多数都是很单调乏味的样板式代码。如果使用较低层级HTTP库,客户端就需要创建一个客户端实例和请求对象、执行请求、解析响应、将响应映射为领域对象,并且还要处理这个过程中可能会抛出的所有异常。不管发送什么样的HTTP请求,这种样板代码都要不断重复。

为了避免这种样板代码,Spring提供了RestTemplate。就像JDBCTemplate能够处理JDBC中丑陋的那部分代码一样,RestTemplate也能够将你从消费REST资源所面临的单调工作中解放出来。

RestTemplat提供了41个与REST资源交互的方法。我们不会详细介绍它所提供的所有方法,而是只考虑12个独立的操作(见表7.1),每种方法都有重载形式,它们组成了完整的41个方法。

表7.1 RestTemplate中12个独立的操作

  • 使用String作为URL格式,并使用可变参数列表指明URL参数。
  • 使用String作为URL格式,并使用Map<String,String>指明URL参数。
  • 使用java.net.URI作为URL格式,不支持参数化URL。

明确了RestTemplate所提供的12个操作以及各个变种如何工作之后,你就能以自己的方式编写消费REST资源的客户端了。

要使用RestTemplate,你可以在需要的地方创建一个实例:

1
RestTemplate rest = new RestTemplate();

也可以将其声明为一个bean并注入到需要的地方:

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

我们通过对4个主要HTTP方法(GET、PUT、DELETE和POST)的支持来研究RestTemplate的操作。下面我们从GET方法的getForObject()和getForEntity()开始。

7.1.1 GET资源

假设我们现在想要通过Taco Cloud API获取某个配料,并且API没有实现HATEOAS,那么我们可以使用getForObject()获取配料。例如,如下的代码将使用RestTemplate根据ID来获取Ingredient对象:

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

在这里,我们使用了getForObject()的变种形式,接收一个String类型的URL并使用可变列表来指定URL变量。传递给getForObject()的ingredientId参数会用来填充给定URL的{id}占位符。尽管在本例中只有一个URL变量,但是很重要的一点需要我们注意,变量参数会按照它们出现的顺序设置到占位符中。getForObject()方法的第二个参数是响应应该绑定的类型。在本例中,响应数据(很可能是JSON格式)应该被反序列化为要返回的Ingredient对象。

另外一种替代方案是使用Map来指定URL变量:

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

在本例中,ingredientId的值会被映射到名为id的key上。当发起请求的时候,{id}占位符将会被替换为key为id的Map条目上。

使用URL参数要稍微复杂一些,这种方式需要我们在调用getForObject()之前构建一个URI对象。在其他方面,它与另外两个变种非常类似:

1
2
3
4
5
6
7
8
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对象是通过String规范定义的,它的占位符会被Map中的条目所替换,与之前看到的getForObject()变种非常相似。getForObject()是获取资源的有效方式,但是如果客户端需要的不仅仅是载荷体,那么可以考虑使用getForEntity()。

getForEntity()的工作方式和getForObject()类似,但是它所返回的并不是代表响应载荷的领域对象,而是一个包裹领域对象的ResponseEntity对象。借助ResponseEntity对象能够访问很多的响应细节,比如响应头信息。

例如,我们除了想要获取配料数据之外,还想要从响应中探查Date头信息。借助getForEntity(),这个需求很容易实现:

1
2
3
4
5
6
7
8
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()方法的3个变种形式都会接收一个Object,它会被序列化并发送至给定的URL。就URL本身来讲,它可以以URI对象或String的形式来指定。与getForObject()和getForEntity()类似,URL变量能够以可变参数列表或Map的形式来提供。

假设我们想要使用一个新Ingredient对象的数据来替换某个配料资源,那么如下的代码片段就能做到这一点:

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

在这里,URL是以String的形式指定的。该URL包含一个占位符,它会被给定Ingredient的id属性所替换。要发送的数据是Ingredient对象本身。put()方法返回的是void,所以没有必要处理返回值。

7.1.3 DELETE资源

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

在本例中,我们只为delete()提供了URL(以String的形式指定)和URL变量值。但是,和其他的RestTemplate方法类似,URL能够以URI对象的方式来指定,并且URL参数也能够以Map的方式来声明。

7.1.4 POST资源

现在,假设要添加新的配料到Taco Cloud菜单中,我们可以向“…/ingredients”端点发送HTTP POST请求并将配料数据放到请求体中。RestTemplate有3种发送POST请求的方法,每种方法都有相同的重载变种来指定URL。如果你希望在POST请求之后得到新创建的Ingredient资源,那么可以按照如下方式使用postForObject():

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

postForObject()方法的这个变种形式接收一个String类型的URL规范、要提交给服务器端的对象以及响应体应该绑定的领域类型。尽管我们在这里没有用到,但是第4个参数可以是URL变量值的Map或者是可变参数的列表,它们能够替换到URL之中。

如果客户端还想要知道新创建资源的地址,那么我们可以调用postForLocation()方法:

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

注意,postForLocation()的工作方式与postForObject()类似,只不过它所返回的是新创建资源的URI,而不是资源对象本身。这里所返回的URI是从响应的Location头信息中派生出来的。如果你同时需要地址和响应载荷,那么可以使用postForEntity()方法:

1
2
3
4
5
6
7
8
9
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。