消费REST服务

你有没有过这样的经历:兴冲冲地跑去看电影,却发现自己是影厅中唯一的观众?这当然是一种很奇妙的经历,本质上来讲,这场电影变成了私人电影。你可以选择任意想要的座位,和屏幕上的角色交谈,甚至打开手机发推文,完全不用担心因为破坏了别人的观影体验而惹得别人生气。最棒的是,没有人会毁了你观看这部电影的心情。

对我来说,这样的事情并不常见。但是,遇到这种情况的时候,我会在想,如果我也不出现,会发生什么呢?工作人员还会播放这部影片吗?电影中的英雄还会拯救世界吗?电影播放结束后,工作人员还会打扫影院吗?

没有观众的电影就像没有客户端的API。这些API已经准备好接收和提供数据了,但是如果它们从来没有被调用过,那么它们还是API吗?这些API就像薛定谔的猫。在发起请求之前,我们并不知道这些API是否活跃,也不知道它们是否返回HTTP 404响应。

这种场景并不罕见:Spring应用除了提供对外API之外,同时要对另外一个应用的API发起请求。实际上,在微服务领域,这正变得越来越普遍。因此,花点时间研究一下如何使用Spring与REST API交互是非常值得的。

Spring应用可以采用多种方式来消费REST API。

  • RestTemplate:由Spring核心框架提供的简单、同步REST客户端。

  • Traverson:对Spring RestTemplate的包装,由Spring HATEOAS提供的支持超链接、同步的REST客户端,其灵感来源于同名的JavaScript库。

  • WebClient:反应式、异步REST的客户端。

现在,我们主要关注使用RestTemplate创建客户端。我将WebClient推迟到第12章介绍Spring的反应式Web框架时再讨论。如果你对编写支持超链接的客户端感兴趣,可以参阅Traverson的文档。

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

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

RestTemplat提供了41个与REST资源交互的方法。我不会详细介绍它所提供的所有方法,而是只考虑12个独立的操作,这些操作的重载形式组成了完整的41个方法。这12个操作如表7.2所示。

image 2024 03 13 18 59 44 636
Figure 1. 表7.2  RestTemplate定义了12个独立的操作,分别有若干重载,共计41个方法

除了TRACE,RestTemplate对每种标准的HTTP方法都提供了至少一个方法。除此之外,execute()和exchange()提供了较低层次的通用方法,以便使用任意的HTTP操作。

表7.2中的大多数操作都以如下的3种方法形式进行了重载:

  • 使用String作为URL格式,并使用可变参数列表指明URL参数;

  • 使用String作为URL格式,并使用Map<String,String>指明URL参数;

  • 使用java.net.URI作为URL格式,不支持参数化URL。

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

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

RestTemplate rest = new RestTemplate();

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

@Bean
public RestTemplate restTemplate() {
  return new RestTemplate();
}

我们从其支持的4个主要HTTP方法(也就是GET、PUT、DELETE和POST)入手,来研究RestTemplate的操作。不妨从GET方法的getForObject()和getForEntity()开始。

GET资源

假设我们现在想要通过Taco Cloud API获取某个配料。为了实现这一点,我们可以使用RestTemplate的getForObject()方法来获取配料。例如,如下的代码使用RestTemplate来根据ID来获取Ingredient对象:

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变量:

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条目。

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

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

例如,假设我们除了想要获取配料数据,还想要从响应中探查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()。

PUT资源

为了发送HTTP PUT请求,RestTemplate提供了put()方法。put()方法的3个变种形式都会接收一个会被序列化并发送至给定URL的Object。就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的形式指定的,该URL包含一个占位符,它会被给定Ingredient的id属性所替换。要发送的数据是Ingredient对象本身。put()方法返回void,所以我们没有必要处理返回值。

DELETE资源

假设Taco Cloud不想再提供某种配料,因此我们要从可选列表中将其完全删除。为了实现这一点,可以使用RestTemplate来调用delete()方法:

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

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

POST资源

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

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

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

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

public java.net.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,并将其用到客户端代码中。

小结

  • REST端点可以通过Spring MVC来创建,这里的控制器与面向浏览器的控制器遵循相同的编程模型。

  • 为了绕过视图和模型的逻辑,并将数据直接写入到响应体中,控制器处理方法可以添加@ResponseBody注解,也可以返回ResponseEntity对象。

  • @RestController注解简化了REST控制器,使用它时,处理器方法中不需要添加@ResponseBody注解。

  • 借助Spring Data REST,Spring Data存储库可以自动导出为REST API。