Spring应用除了提供对外API
之外,同时要对另一个应用的API发起请求。实际上,在微服务领域,这正变得越来越普遍。因此,花点时间研究一下如何使用Spring与RESTAPI交互是非常值得的。
Spring应用可以采用多种方式来消费RESTAPI
。
RestTemplatc
:由Spring核心框架提供的简单、同步REST客户端。Traverson
:对 Spring RestTemplate 的包装,由Spring HATEOAS 提供的支持超链接、同步的 REST客户端,其灵感来源于同名的 JavaScript库。WebClient
:反应式、异步REST的客户端。现在,我们主要关注使用 RestTemplate
创建客户端。
从客户端的角度来看,与 REST
资源进行交互涉及很多工作,而且大多数都是很单调乏味的样板式代码。如果使用较低层级HTTP
库,客户端需要创建一个客户端实例和请求对象,执行请求,解析响应,将响应映射为领域对象,还要处理这个过程中可能会抛出的所有异常。不管发送什么样的 HTTP
请求,这种样板代码都要不断重复。
为了避免这种样板代码,Spring
提供了 RestTemplate
。就像JDBCTemplate
能够处理JDBC
中丑陋的那部分代码一样,RestTemplate
也能够将你从消费 REST
资源所面临的单调工作中解放出来。
RestTemplat
提供了 41个与 REST
资源交互的方法。我不会介绍它所提供的所有方法,而是只考虑独立的12个操作,这些操作的重载形式组成了完整的41个方法。
方法 | 描述 |
---|---|
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
操作
表中的大多数操作都以如下的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()
开始。
假设我们现在想要通过 Taco Cloud API
(访问该RestAPI可以得到对应的数据)获取某个配料。为了实现这一点,我们可以使用RestTemplate
的getForObject()
方法来获取配料。例如如下的代码使用RestTemplate
来根据ID
来获Ingredient
对象:
public Ingredient getIngredientById(string ingredientId){
return rest.getForObject("http://localhost;8080/ingredients/{id}",
Ingredient.class, ingredientId);
}
在这里,我们使用了gctForObject()
的变种形式,它接收一个String
类型的URL
并使用可变列表来指定URL
变量。传递给getForObject()
的 IngredientId
参数会用来填充给定URL
的{id}
占位符。尽管在本例中只有一个URL
变量,但是有很重要的一点需要我们注意:变量参数会按照它们出现的顺序被设置到占位符中。
getForObject()
方法的第二个参数是响应应该绑定的类型。在本例中,响应数据(很可能是JSON格式)应该被反序列化为要返回的Igredient
对象。另外一种替代方案是使用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);
在本例中,ingredicntId
的值会映射到名为 id
的 key
上。当发起请求的时候,{id}
占位符将会被替换成key
为id
的Map
条目。
使用 URI
参数要稍微复杂一些,这种方式需要我们在调用 getForObject()
之前构建URI
对象。在其他方面,它与另外两个变种非常类似:
public Ingred1ent 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()
。
为了发送HTTP
PUT
请求,RestTemplate
提供了 put()
方法。put()
方法的3个变种形式都会接收一个会被序列化并发送至给定 URL
的 Object
。就URL
本身来讲,它可以按照URI
对象或 String
的形式来指定。与 getForObject()
和 getForEntity()
类似,URL
变量能够以可变参数列表或Map
的形式提供。
假设我们想要使用一个新 Igredient
对象的数据来替换某个配料资源,那么如下的代码片段就能做到这一点:
public void updateIngredient(Ingredient ingredient) {
rest.put("http://localhost:8080/ingredients/{id}",ingredient,ingredient.getId());
}
在这里,URL
是以 String
的形式指定的,该 URL
包含一个占位符,它会被给定Ingredient
的id
属性所替换。要发送的数据是 Ingredient
对象本身。put()
方法返回 void
所以我们没有必要处理返回值。
假设 Taco Cloud
不想再提供某种配料,因此我们要从可选列表中将其完全删除。为了实现这一点,可以使用 RestTemplate
来调用 delete()
方法:
public void deleteIngredlent(Ingredient ingredient) {
rest.delete("http://1ocalhost;8080/ingredients/{id})",ingredient.getId())
}
在本例中,我们只为 delete()
提供了 URL
(以 String 的形式指定)和 URL
变量值但是,和其他的 RestTemplate
方法类似,URL
能够以URI
对象的方式来指定,URL
参数也能够以 Map
的方式来声明。
现在,我们假设要添加新的配料到 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
,并将其用到客户端代码中。