-------------------------------笔记来自于《Spring IN ACTION》
1. 编写REST客户端
Spring支持提供一个Rest类型的服务端,又支持使用Rest客户端来访问Rest服务器端。Spring提供的访问REST API的方式有以下几种:
- RestTemplate——由Spring提供的简单的,同步的REST客户端。
- Traverson——由Spring HATEOAS提供的超链接感知(hyperlink-aware),同步的REST客户端。此方式灵感来于同名的JavaScript库。
- WebClient——Spring 5中引入的一个反应式,异步的REST客户端。
本文仅介绍使用RestTemplate
来访问Rest端点的部分。例如如下代码使用Apache HTTP Client
来获取FaceBook中的个人信息。仔细看下代码可以发现fetchFaceBookProfile()
方法中只有少量代码与获取FaceBook个人信息有关。如果编写另外一个方法来使用其他的Rest资源,很可能会有很多代码与fetchFaceBookProfile()
相同的。另外还有一些地方抛出了IOException
异常,因为IOException
是检查型异常,所以要么捕获它,要么抛出它。鉴于在请求Rest资源上包含很多的重复代码,最好的方式就是封装通用代码并参数化可变部分。这正是RestTemplate
所做的事情。
public Profile fetchFaceBookProfile(String id) {
try {
HttpClient client = HttpClients.createDefault();
HttpGet request = new HttpGet("http://graph.facebook.com/"+id);
request.setHeader("Accept","application/json");
HttpResponse response = client.execute(request);
HttpEntity entity = response.getEntity();
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(entity.getContent(), Profile.class);
}catch(IOException e) {
throw new RuntimeException(e);
}
}
从客户端的角度看,与Rest服务器端交互包含客户端创建客户端实例和请求对象,执行请求,解析响应,将响应映射到对象,并且处理可能抛出的任何异常等过程涉及很多的底层HTTP库。不管发送什么类型的Http请求,所有这些样板文件都是重复的。为了避免重复,Spring提供了RestTemplate
,如同JDBCTemplate
处理JDBC中重复部分一样。
2. RestTemplate操作
RestTemplate
定义了41
种与REST资源交互的方法,其中大多数方法对应HTTP的方法。其中有11
个方法有三种重载形式,另外一个方法重载了8
次,因此共有12
个重载方法组。除了TRACE
以外,RestTemplate
涵盖了所有的HTTP动作。其中execute()
和exchange()
方法提供了较低级别的通用方法来使用任何HTTP方法。表中大多数操作都以三种形式进行了重载:
- 一个使用java.net.URI作为URL格式,不支持参数化URL。
- 一个使用String作为URL格式,并使用Map指明URL参数。
- 一个使用String作为URL格式,并使用可变参数列表指明URL参数。
方法组 | 描述 |
---|---|
delete | 在特定的URL上对资源指定HTTP DELETE操作 |
exchange | 在URL上执行特定的HTTP方法,返回包含对象的ResponseEntity,这个对象是从响应体中映射得到的 |
execute | 在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象 |
getForEntity | 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象 |
getForObject | 发送一个HTTP GET请求,返回的请求体将映射为一个对象 |
headForHeaders | 发送HTTP HEAD请求,返回包含特定资源的URL的HTTP头,需要注意的是JDK HttpURLConnection不支持PATCH,Apache HttpComponents和其他组件支持 |
optionsForAllow | 发送HTTP OPTIONS请求,返回对特定URL的Allow头信息 |
patchForObject | 发送HTTP PATCH请求,返回一个从响应体映射得到的对象 |
postForEntity | post数据到一个URL,返回包含一个对象的ResponseEntity,这个对象是从响应体中映射得到的 |
postForLocation | post数据到一个URL,返回新创建资源的URL |
postForObject | post数据到一个URL,返回根据响应提匹配形成的对象 |
put | put资源到特定的URL |
为了使用RestTemplate
,可以在客户端创建一个RestTemplate
实例对象:
RestTemplate rest = new RestTemplate();
或者将RestTemplate
声明为一个Spring bean
,并将其注入来实现方法调用。
@bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
2.1 GET资源
指定GET请求的方式有两种:getForObject()
和getForEntity()
,每个方法又有三种形式的重载。getForObject()
方法的重载形式:
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException
public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException
public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException
getForEntity()
方法的重载形式:
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)throws RestClientException
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)throws RestClientException
public <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType) throws RestClientException
两种类型的方法,除了返回类型,getForEntity()
方法就是getForObject()
方法的镜像。实际上它们的工作方式大同小异。它们都是根据URL检索资源的GET请求,都将资源根据responseType
参数匹配为一定的类型。唯一的区别在于getForObject()
只返回所请求类型的对象,而getForEntity()
方法会返回请求的对象以及响应相关的额外信息。
————检索资源
getForObject()
方法适合检索资源。请求一个资源按照所选择的Java类型接收该资源。如下:
public Profile fetchFaceBookProfile(String id) {
RestTemplate rest = new RestTemplate();
return rest.getForObject("http://graph.facebook.com/{id}", Profile.class, id);
}
现在 获取FaceBook个人信息通过一两行代码可以实现,fetchFaceBookProfile()
方法首先创建了一个RestTemplate
的实例(另外的方法是注入实例),它调用了getForObject()
方法得到了FaceBook个人信息。为了做到这一点,它要求结果是Profile对象。接收到Profile对象后,该方法将其返回给调用者。
注意的是方法的URL并不是通过字符串连接来构建,而是利用了RestTemplate
可以接收参数化的URL这一功能。URL中的{id}
占位符最终将会用方法的id参数来填充。每个参数都会按出现顺序插入到指定的URL的占位符中。
另外一种替代方案是将id参数放到Map中,并以id作为key,然后将这个Map作为最后一个参数传递给getForObject()
。
public Profile fetchFaceBookProfile(String id) {
Map<String,String> urlVal = new HashMap<>();
urlVal.put("id", id);
RestTemplate rest = new RestTemplate();
return rest.getForObject("http://graph.facebook.com/{id}", Profile.class, urlVal);
}
这里没有任何形式的JSON解析和对象映射。getForObject()
将响应体转换为对象。它实现了需要格式转换的HTTP消息转换器,与带有@ResponseBody
注解的Spring MVC处理方法所使用的一样。
这个方法也没有使用任何的异常处理。这不是因为getForObject()
不能抛出异常,而是因为它抛出的异常都是非检查型的。如果在getForObject()
中有错误,将抛出非检查型RestClientException
异常(或者它的子类)。编译器不会强制捕获它,可以选择捕获。
————抽取响应的元数据
作为getForObject()
方法的替代方法,getForEntity()方法与getForObject()方法的工作很相似。getForObject()
只返回了资源(通过HTTP信息转换器将其转换为Java对象),getForEntity()
会在ResponseEntity中返回相同的对象,而且ResponseEntity包含了响应的额外信息,如HTTP状态码和响应头。
例如服务端在LastModified
头部信息中提供了资源最后修改时间,通过ResponseEntity
上调用getHeaders()
方法来获取此时间信息。
Date lastModified = new Date(response.getHeaders().getLastModified());
getHeaders()
方法返回一个HttpHeaders
对象,该对象提供了多个便利的方法来查询响应头,包括getLastModified()
,它将返回从1970年1月1日开始的毫秒数。HttpHeaders
包含以下的方法来获取头信息:
public List<MediaType> getAccept() {}
public List<Charset> getAcceptCharset() {}
public Set<HttpMethod> getAllow() {}
public String getCacheControl() {}
public List<String> getConnection() {}
public long getContentLength() {}
public long getDate() {}
public String getETag() {}
public long getExpires() {}
public long getIfModifiedSince() {}
public List<String> getIfNoneMatch() {}
public long getLastModified() {}
public URI getLocation() {}
public String getOrigin() {}
public String getPragma() {}
public String getUpgrade() {}
...//省略其他方法
为了实现更通用的HTTP头部信息访问,HttpHeaders
提供了get()
方法和getFirst()
方法。两个方法都接受String参数来标识所需要的头信息。get()
将会返回一个String值的列表,其中的每个值都是赋给该头部信息的,而getFirst()
方法只会返回第一个头信息的值。可以调用getStatusCode()
方法来获取HTTP状态码。如下:
public Spittle fetchSpittle(long id) {
RestTemplate rest = new RestTemplate();
ResponseEntity<Spittle> response = rest.getForEntity("http://localhost:8080/spittle/{id}", Spittle.class,id);
if(response.getStatusCode()==HttpStatus.NOT_MODIFIED) {
throw new NotModifiedException();
}
return response.getBody();
}
这里服务器响应NOT_MODIFIED(304)
状态,意味着服务器端的内容自从上一次请求之后再也没有修改。此时将会抛出一个自定义的NotModifiedException
异常表明客户端应该检查他的缓存来获取Spittle对象。
2.2 PUT资源
RestTemplate
提供了三种重载的put()
方法。
public void put(String url, @Nullable Object request, Object... uriVariables)throws RestClientException
public void put(String url, @Nullable Object request, Map<String, ?> uriVariables)throws RestClientException
public void put(URI url, @Nullable Object request) throws RestClientException
put()最简单的形式,接收一个java.net.URI
,用来标识(及定位)要将资源发送到服务器上,另外还接收一个对象,这代表了资源的Java表述。如下是展现了使用基于URI版本的put()
方法
public void updateSpittle(Spittle spittle) throws SpitterException{
RestTemplate rest = new RestTemplate();
rest.put("http:.//localhost:8080/spittles/{id}", spittle,spittle.getId());
}
现在URI使用简单的String模板来进行表示。当RestTemplate
发送PUT
请求时,URI模板将{id}
部分使用spittle.getId()方法返回值来进行替换。就像getForObject()
和getForEntity()
一样,这个版本的put()
方法最后一个参数是大小可变的参数列表,每个值会按照顺序复制给占位符变量。还可以将模板参数作为Map传递进来。
public void updateSpittle(Spittle spittle) throws SpitterException{
RestTemplate rest = new RestTemplate();
Map<String,String> params = new HashMap<>();
params.put("id", spittle.getId());
rest.put("http://localhost:8080/spittles/{id}", spittle,params);
}
当使用Map来传递模板参数时,Map映射中的每个key与URI模板中占位符变量的名字相同。所有版本的put()
中,第二个参数表示资源的java对象,它将按照指定的URI
发送到服务器端。本例中是一个Spittle对象。RestTemplate使用适合的Http消息转换器将spittle对象转换为一种表述形式,并在请求体中将其发送到服务器端。
对象将被转换成什么样的内容类型很大程序取决于传递给put()
方法的类型,如果给定一个String值,那么将会使用StringHttpMessageConverter
:这个值直接被写到请求体中,内容类型设置为"text/plain"
。如果给定一个MultiValueMap<String,String>
,那么这个Map中的值将会被FormHttpMessageConverter
以"application/x-www-form-urlencoded"
的格式写到请求体中。
因为传递进来的是Spittle对象,所以需要一个能够处理任意对象的信息转换器。如果在类路径下包含Jackson2
库,那么MappingJacksonHttpMessageConverter
将以application/json
格式将Spittle写到请求中。
2.3 DELETE资源
当不需要在服务端保留某个资源时,那么可能需要调用RestTemplate
的delete()
方法。delete()
的三个重载方法如下:
public void delete(String url, Object... uriVariables) throws RestClientException
public void delete(String url, Map<String, ?> uriVariables) throws RestClientException
public void delete(URI url) throws RestClientException
delete()
方法是RestTemplate
方法中最简单的。唯一要提供的是要删除资源的URI
。例如为了删除指定的id的Spittle,调用的delete()
方法可以如下:
public void deleteSpittle(long id) {
RestTemplate rest = new RestTemplate();
rest.delete(URI.create("http://localhost:8080/spittles/"+id));
}
上面URI
依赖了字符串的连接来创建的,为避免麻烦,同样可以采用占位符的形式:
public void deleteSpittle(long id) {
RestTemplate rest = new RestTemplate();
rest.delete(URI.create("http://localhost:8080/spittles/{id}",id));
}
2.4 POST资源
RestTemplate
中有三种不同类型的方法来发送的POST
请求。每类方法又有三种重载形式,因此就有九个方法可以POST
数据到服务器端。其中postForObject()
和postForEntity()
对POST请求的处理方式与发送GET
请求的getForObject()
和getForEntity()
方法是类似的。另外一个方法是getForLocation()
,是POST
请求所特有的。
————在POST请求中获取响应对象
假设使用RestTemplate
来POST
一个全新的Spitter对象到Spittr应用程序的REST API。因为这是一个全新的Spitter,服务器端还不知道它,因此它还不是真正的REST资源,也没有URL。另外在服务端创建之前,客户端并不知道Spitter的id。
POST资源到服务端的一种方式是使用RestTemplate
的postForObject()
方法。postForObject()
方法的三种重载形式如下:
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
Object... uriVariables) throws RestClientException
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
Map<String, ?> uriVariables) throws RestClientException
public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)
throws RestClientException
postForObject()
方法所有重载形式中,第一个参数都是资源要POST
的URL,第二个参数是要发送的对象,而第三个参数是预期返回的Java类型。在将URL
作为String类型的两个版本中,第四个参数指定了URL
变量(要么是可变参数列表,要么是一个Map)。
当POST
新的Spitter资源到Spitter REST API时,它们应该发送到http:/localhost:8080/spitters
,这里会有一个应对POST
请求处理请求方法来保存对象,所以可以使用任何版本的postForObject()
。但为了保存简单,我们可以使用如下调用:
public Spitter postSpitterForObject(Spitter spitter) {
RestTemplate rest = new RestTemplate();
return rest.postForObject("http://localhost:8080/spitters", spitter, Spitter.class);
}
postSpitterForObject()
方法给定一个新创建的Spitter对象,并使用postForObject()
将其发送到服务器端。在响应中,它接收一个Spitter对象并将其返回个调用。就像getForEntity()
方法一样,可能需要在请求中带回来一些元数据。在这种情况下,postForEntity()
是更合适的方法。postForEntity()
方法有着与postForObject()
几乎相同的方法重载形式:
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,
Class<T> responseType, Object... uriVariables)
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,
Class<T> responseType, Map<String, ?> uriVariables)
public <T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType)
throws RestClientException
假设除了要获取返回的Spitter资源,并要查看响应中Location
头信息的值。可以使用下面的方式来调用postForEntity()
方法:
public String postSpitterForObject(Spitter spiter) {
RestTemplate rest = new RestTemplate();
ResponseEntity<Spitter> response = rest.postForEntity("http://localhost:8080/spitters", spitter, Spitter.class);
Spitter spitter =response.getBody();
URI url= response.getHeaders().getLocation();
return url.toString();
}
与getForEntity()
方法一样,postForEntity()
返回一个ResponseEntity<T>
对象。可以调用这个对象的getBody()
方法来获取资源对象。getHeaders()
方法返回的是HttpHeaders
对象,通过它可以访问响应中返回的各种HTTP头信息,这里使用getLocation()
方法得到java.net.URI
形式的Location
头信息。
————在POST请求后获取资源位置
如果同时接收所发送的资源和响应头,postForEntity()
方法比较适合。但通常来说POST
请求是发送资源到服务器端,不需要再将资源从服务器端发送回来。仅仅为了得到Location
头部信息的值,那么使用postForLocation
会更加简单。
类似于其他的POST
方法,POSTForLocation()
会在POST
请求的请求体中发送一个资源到服务器端。但是响应中不再是相同资源的对象,postForLocation()
的响应是新创建资源的位置。它的三个重载方法如下:
public URI postForLocation(String url, @Nullable Object request, Object... uriVariables)throws RestClientException
public URI postForLocation(String url, @Nullable Object request, Map<String, ?> uriVariables)throws RestClientException
public URI postForLocation(URI url, @Nullable Object request) throws RestClientException
如下使用postForLocation()
来POST
一个Spitter对象,并且方法返回中包含一个资源的URL。
public String postSpitter(Spitter spitter) {
RestTemplate rest = new RestTemplate();
return rest.postForLocation("http://localhost:8080/spittles", spitter).toString();
}
这里以String的形式将目标的URL传递进来,还有要POST
的spitter对象。创建资源后,如果服务端在响应的Location头信息中返回新资源的URL,接下来postForLocation()
会以String的格式返回该URL。
2.5 交换资源
上面使用了RestTemplate
的各个方法来GET、PUT、DELETE
以及POST
资源。其中有个两个特殊的方法:getForEntity()
和postForEntity()
,这两个方法将结果资源包含一个ResponseEntity
对象中,通过这个对象可以响应头和状态码。
从响应中读取头信息是很有用的,但是如果想在发送给服务端的请求中设置头信息时,就需要使用RestTemplate
的exchange()
方法。像RestTemplate
的其他方法一样,exchange()
方法有八种重载形式。
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
@Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
throws RestClientException
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
@Nullable HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables)
throws RestClientException
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, @Nullable HttpEntity<?> requestEntity,
Class<T> responseType) throws RestClientException
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> responseType, Object... uriVariables) throws RestClientException
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> responseType, Map<String, ?> uriVariables) throws RestClientException
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, @Nullable HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> responseType) throws RestClientException
public <T> ResponseEntity<T> exchange(RequestEntity<?> requestEntity, Class<T> responseType)
throws RestClientException
public <T> ResponseEntity<T> exchange(RequestEntity<?> requestEntity, ParameterizedTypeReference<T> responseType)
throws RestClientException
其中参数HttpMethod
表明了要使用的HTTP动作。根据这个参数的值,exchange()
能够执行与其他RestTemplate
方法一样的工作。如从服务器端获取Spitter资源的一种方法是使用RestTemplate
的getForEntity()
方法,如下所示:
RestTemplate rest = new RestTemplate();
ResponseEntity<Spitter> response = rest.getForEntity("http://localhost:8080/spitters/{id}", Spitter.class,id);
Spitter spitter = response.getBody();
下面的代码片段使用exchange()
也可以完成这项任务。
RestTemplate rest = new RestTemplate();
ResponseEntity<Spitter> response = rest.exchange("http://localhost:8080/spitters/{id}",HttpMethod.GET,null,Spitter.class,id);
Spitter spitter = response.getBody();
通过传入HttpMethod.GET
作为HTTP动作,exchange()
会发送一个GET
请求。第三个参数用于请求中发送资源的,但因为这是一个GET
请求,它可以是null
。下一个参数表明将响应转换为Spitter对象。最后一个参数用于替换URL模板中{id}
占位符的值。
按这种方式,exchange()
与之前使用的getForEntity()
是几乎相同的。但是不同于getForEntity()
或者getForObject()
方法的是exchange()
方法允许在请求中设置头信息。接下来不再给exchange()
传递null
,而是传入带有请求信息的HttpEntity
。如果不指明头信息,exchange()
对Spitter的GET请求会带有如下的头信息。
GET/Spitter/spitters/habuma HTTP/1.1
Accept:application/xml,text/xml,application/*+xml,aplication/json
Content-Length:0
User-Agent:Java/1.6.0_20
Host:Localhost:8080
Connnection:keep-alive
Accept头信息表明它能够接受多种不同的XML内容类型以及application/json
。这样服务器端在决定采用哪种格式返回资源时,就有很大的可选空间。假设希望服务端以JSON
格式发送资源,需要将"application/json"
设置为Accept头信息的唯一值。设置请求头信息只需要构造发送给exchange()
方法的HttpEntity
对象即可,HttpEntity
中包含承载头信息的MultiValueMap
;如下创建一个LinkedMultiValueMap
并添加值为"application/json"
的Accept头信息。接下来构建一个HttpEntity
,将MultiValueMap
作为构造参数传入。如果这是一个PUT
或者POST
请求,需要将HttpEntity设置
在请求体中发送的对象。对于GET
请求来说,这个参数无意义。最后传入HttpEntity
来调用exchange()
方法。
MultiValueMap<String,String> headers = new LinkedHashMap<>();
headers.add("Accept", "application/json");
HttpEntity<Object> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Spitter> response = rest.exchange("http://localhost:8080/spitters/{id}",HttpMethod.GET,requestEntity,Spitter.class,id);
Spitter spitter = response.getBody();
此时的请求报文如下面,头信息发生变化。
GET/Spitter/spitters/habuma HTTP/1.1
Accept:application/json
Content-Length:0
User-Agent:Java/1.6.0_20
Host:Localhost:8080
Connnection:keep-alive
假设服务器端能够将Spitter序列化为JSON,响应体将会以JSON格式来进行表述。
3. 参考
《Spring IN ACTION》