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请求,返回指定资源URLHTTP请求头
optionsForAllow(…) 发送HTTP OPTIONS请求,返回指定URLAllow头信息
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、DELETEPOST)的操作来探寻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());
}

这里URLString的形式给出,并有一个占位符,该占位符由给定的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。

下一页