11.4 编写REST客户端
作为客户端,编写REST资源交互的代码可能比较乏味,并且所编写的代码都是样式的。
//REST客户端会涉及到模板代码和异常处理
public Spittle[] retrieveSpittlesForSpitter(String username){
try {
//创建HttpClient
HttpClient httpClient = new DefaultHttpClient();
//组建URL
String spittleUrl = "http://localhost:8080/Spitter/spitters"
+ username + "/spittles";
//创建对URL的请求
HttpGet getRequest = new HttpGet(spittleUrl);
getRequest.setHeader(new BasicHeader("Accept","application/json"));
//执行请求
HttpResponse response = httpClient.execute(getRequest);
//解析结果
HttpEntity entity = response.getEntity();
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(entity.getContent(),Spittle[].class);
}catch(IOException e){
//可能会抛出IOException异常。因为IOException是检查型异常,必须捕获或者抛出它。这里选择捕获它并重新抛出一个自定义的非检查型异常。
throw new SpitterClientException("Unable to retrieve Spittles", e);
}
}
可以看到,在使用REST资源的时候涉及很多代码。这里我甚至还偷懒使用了Jakarta Commons HTTP Client(http://hc.apache.org/httpcomponents-client/index.html)创建请求并使用Jackson JSON processor解析响应。
11.4.1 了解RestTemplate的操作
鉴于在资源使用上有如此多的样板代码,Spring 的 RestTemplate 将通用代码进行了封装。
在11.2.3中提到,HTTP规范定义了与RESTful资源交互的7个方法类型。这些方法类型提供了RESTful会话中的动作。
RestTemplate定义了33个与REST资源交互的方法,涵盖了HTTP动作的各种形式。其实,这里面只有11个独立的方法,而每一个方法都有3个重载的变种。
下表列出了RestTemplate定义的11个独立的操作,而每一个都有重载,这样一共是33个方法:
方法 | 描述 |
---|---|
delete() | 在特定的URL上对资源执行HTTP DELETE操作 |
exchange() | 在URL上执行特定的HTTP方法,返回包含对象的ResponseEntity,这个对象是从响应体中映射得到的 |
execute() | 在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象 |
getForEntity() | 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象 |
getForObject() | GET资源,返回的请求体将映射为一个对象 |
headForHeaders() | 发送HTTP HEAD请求,返回包含特定资源URL的HTTP头 |
optionsForAllow() | 发送HTTP OPTIONS请求,返回对特定URL的ALLOW头信息 |
postForEntity() | POST数据,返回包含一个对象的ResponseEntity,这个对象是从响应体中映射得到的 |
postForLocation() | POST数据,返回新资源的URL |
postForObject() | POST数据,返回的请求体将匹配为一个对象 |
put() | PUT资源到特定的URL |
除了TRACE,RestTemplate涵盖了所有的HTTP动作。除此之外,execute()和exchange()提供了较低层次的通用方法来使用任意的HTTP方法。
上表中列出的每个操作都以3种方法形式进行了重载:
- 一个使用java.net.URI作为URL格式,不支持参数化URL;
- 一个使用String作为URL格式,并使用Map指明URL参数;
- 一个使用String作为URL格式,并使用可变参数列表指明URL参数。
我们通过对4个主要HTTP方法的支持(也就是GET、PUT、DELETE和POST)来研究RestTemplate的操作。
11.4.2 GET资源
getForObject() 和 getForEntity() 执行GET请求。
//3个getForObject()方法签名如下:
<T> T getForObject(URI url, Class<T> responseType) throws RestClientException;
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
//3个getForEntity()方法签名如下:
<T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType) throws RestClientException;
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
除了返回类型,getForObject()方法和getForEntity()方法工作方式大同小异。唯一的区别在于getForObject()只返回所请求类型的对象,而getForEntity()方法会返回请求的对象以及相应相关的额外信息。
11.4.2.1 检索资源
getForObject()方法是检索资源的合适选择。你请求一个资源并以你所选择的Java类型接收该资源。作为getForObject()功能的一个简单示例,让我们看一下retrieveSpittlesForSpitter()的另一个实现:
//示例1:
public Spittle[] retrieveSpittlesForSpitter(String username){
return new RestTemplate().getForObject(
//RestTemplate可接受参数化URL,URL中的{spitter}占位符会用方法的username参数来填充
"http://localhost:8080/Spitter/spitters/{spitter}/spittles"
,Spittle[].class,username);
}
retrieveSpittlesForSpitter()首先构建了一个RestTemplate的实例(另一种可行的方式是通过注入实例来代替)。接下来,它调用了getForObject()来得到Spittle列表。为了做到这一点,它要求结果是Spittle对象的数组。在接收到这个数组后,它将其返回给调用者。
//示例2:
public Spittle[] retrieveSpittlesForSpitter(String username){
Map<String, String> urlVariables = new HashMap<String, String>();
urlVariables.put("spitter",username);
return new RestTemplate().getForObject(
//URL中的{spitter}占位符会用urlVariables参数中的spitter值来填充
"http://localhost:8080/Spitter/spitters/{spitter}/spittles"
,Spittle[].class,urlVariables);
}
这里没有任何形式的JSON解析和对象映射。在表面之下,getForObject()为我们将响应体转换为对象。它实现这些需要依赖11.3.2中所列出的HTTP信息转换器,与带有@ResponseBody注解的Spring MVC处理方法所使用的一样。
11.4.2.2 抽取响应的元数据
作为getForObject()的一个替代方案,RestTemplate还提供了getForEntity()。getForEntity()方法与getForObject()方法的工作很相似。getForObject()只返回资源(通过HTTP信息转换器将其转换为Java对象), getForEntity()在ResponseEntity中返回相同的对象。ResponseEntity还带有关于响应的额外信息,如HTTP状态码和响应头。
ResponseEntity的一个用途是获取响应头的一个值。例如,假设除了获取资源,你还想要知道资源的最后修改时间。假设服务端在Last-Modified头中提供了这个信息,可以这样像这样使用getHeaders()方法:
Date lastModified = new Date(response.getHeaders().getLastModified());
getHeaders()方法返回一个HttpHeaders对象,该对象提供了多个便利的方法来查询响应头,包括getLastModified(),它将返回从1970年1月1日开始的毫秒数。
除了getLastModified(),HttpHeaders还包含如下的方法来获取头信息:
public List<MediaType> getAccept();
public List<Charset> getAcceptCharset();
public Set<HttpMethod> getAllow();
public String getCacheControl();
public long getContentType();
public MediaType getContentType();
public long getDate();
public String getETag();
public long getExpires();
public long getIfNotModifiedSince();
public List<String> getIfNoneMatch();
public long getLastmodified();
public URI getlocation();
public String getPragma();
为了实现更通用的HTTP头信息访问,HttpHeaders提供了get()方法和getFirst()方法。两个方法都接受String参数来标识头信息。get()将会返回一个String值的列表,每个值都是赋给这个头信息的。getFirst()方法只会返回第一个头信息的值。
//示例3
public Spittle[] retrieveSpittlesForSpitter(String username){
ResponseEntity<Spittle[]> response = new RestTemplate().getForEntity(
"http://localhost:8080/Spitter/spitters/{spitter}/spittles"
, Spittle[].class,username
);
//getStatusCode()方法获取响应的HTTP状态码,如果服务器响应304状态,意味着服务器端的内容自从之前的请求之后再也没有修改过。
//在这种情况下,将会抛出自定义的NotModifiedException异常来表明客户端应该检查它的资源数据缓存。
if(response.getStatusCode() == HttpStatus.NOT_MODIFIED){
throw new NotModifiedException();
}
return response.getBody();
}
11.4.3 PUT 资源
为了对数据进行PUT操作,RestTemplate提供了3个方法。就像其他的RestTemplate方法一样,put()方法有3种形式:
void put(URI url, Object request) throws restClientException;
void put(String url, Object request, Object... uriVariables) throws RestClientException;
void put(String url, Object request, Map<String, ?> urlVariables) throws RestClientException;
在它最简单的形式中,put() 使用 java.net.URI 来标识(及定位)要发送到服务器的资源以及表述资源的一个java对象。
//示例1
public void updateSpittle(Spittle spittle) throws SpitterException {
try{
String url = "http://localhost:8080/Spitter/spittles/" + spittle.getId();
new RestTemplate().put(new URI(url), spittle);
}catch(URISyntaxException e){
//若将一个非URI传递给URI构造函数,会报URISyntaxException
throw new SpitterUpdateException("Unable to update Spittle", e);
}
}
使用基于String的put()方法能够减少大多数有关创建URI的麻烦,包括对异常的处理。此外,这些方法可以将URI指定为模板并对可变部分插入值。
//示例2,这个版本的put方法最后一个参数是大小可变的参数列表,每个值会按照顺序赋值给占位符变量
public void updateSpittle(Spittle spittle) throws SpitterException{
new RestTemplate().put("http://localhost:8080/Spitter/spittles/{id}", spittle, spittle.getId());
}
//示例3,还可以将模板参数作为Map传递进来
public void updateSpittle(Spittle spittle) throws SpitterException {
Map<String, String> params = new HashMap<String, String>();
params.put("id", spittle.getId());
restTemplate.put("http://localhost:8080/Spitter/spittles/{id}", spittle, params);
}
对象被转换成成什么内容类型很大程度上取决于传递给put()方法的类型。如果给定一个String的值,那么将会使用StringHttpMessageConverter,这个值直接被写到请求体中,内容类型设置为text/plain。如果给定一个MultiValueMap<String, String>
,那么这个Map中的值将会被FormHttpMessageConverter以application/x-www-form-urlencoded的格式写到请求体中。
因为上例中传递的是Spittle对象,所以需要一个能够处理任意对象的信息转换器。如果类路径下包含了Jackson JSON库,那么MappingJacksonHttpConverter将以application/json格式将Spittle写到请求中。如果Spittle类使用了JAXB序列化注解并且JAXB在类路径中,那么Spittle将会作为application/xml发送,并且以XML的格式写到请求体中。
11.4.4 DELETE资源
delete()方法的3个版本:
void delete(String url, Object... urivariables) throws RestClientException;
void delete(String url, Map urivariables) throws RestClientException;
void delete(URI url) throws RestClientException;
11.4.5 POST资源数据
RestTemplate有3个不同类型的方法来发送POST请求,当再乘上每个方法的3个不同的变种,那就是有9个方法来POST数据到服务器端。
11.4.5.1 在POST请求中获取响应对象
1、postForObject()
POST资源到服务端的一种方式是使用RestTemplate的postForObject()方法。3种postForObject()方法有着如下的签名:
<T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
示例:
/* 当POST新的Spitter资源到Spitter REST API时,它们应该发送到http://localhost:8080/Spitter/spitters
这里会有一个应对POST请求的处理方法来保存对象。
在响应中,它接收到一个Spitter对象并将其返回给调用者
*/
public Spitter postSpitterForObject(Spitter spitter){
RestTemplate rest = new RestTemplate();
String url = "http://localhost:8080/Spitter/spitters";
return rest.postForObject(url, spitter, Spitter.class);
}
2、postForEntity()
就像getForEntity()方法一样,你可能需要得到请求带回来的一些元数据。在这种情况下,postForEntity()是更适合的方法。
<T> T postForEntity(URI url, Object request, Class<T> responseType) throws RestClientException;
<T> T postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T postForEntity(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
示例:
/*假设除了要获取返回的Spitter资源,你还要查看响应中Location头信息的值。*/
public Spitter postSpitterForObject(Spitter spitter){
RestTemplate rest = new RestTemplate();
String url = "http://localhost:8080/Spitter/spitters";
ResponseEntity<Spitter> response = rest.postForEntity(url, spitter, Spitter.class);
Spitter spitter = response.getBody();
URI url = response.getHeaders().getLocation();
}
与getForEntity()方法一样,postForEntity()返回一个ResponseEntity<T>
对象。你可以调用这个对象的getBody()方法以获取资源对象(上例中是Spitter)。getHeaders()会给你一个HttpHeaders,通过它可以访问响应中返回的各种HTTP头信息。
11.4.5.2 在POST请求后获取资源位置
对于要同时接收所发送的资源和响应头来说,postForEntity()方法是很便利的。但通常并不需要将资源发送回来。如果你真正需要的是Location头信息的值,那么使用RestTemplate的postForLocation()方法会更简单。
URI postForLocation(String url, Object request, Object... uriVariables) throws RestClientException;
URI postForLocation(String url, Object request, Map<String,?> uriVariables) throws RestClientException;
URI postForLocation(URI url, Object request) throws RestClientException;
示例:
public String postSpitter(Spitter spitter) {
RestTemplate rest = new RestTemplate();
String url = "http://localhost:8080/Spitter/spitters";
return rest.postForLocation(url, spitter).toString();
}
11.4.6 交换资源
getForEntity()和postForEntity(),这两个方法将结果资源包含在一个ResponseEntity对象中,通过这个对象我们可以得到响应头和状态码。能够从响应中读取头信息是很有用的。
但是如果你想在发送给服务器的请求中设置头信息的话,怎么办呢?这就是RestTemplate的exchange()的用武之地。exchange()也重载为3个签名格式,如下所示:
<T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType) throws RestClientException;
<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... urlVariables) throws RestClientException;
<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Map<String,?> urlVariables) throws RestClientException;
exchange()方法使用HttpMethod参数来表明要使用的HTTP动作。根据这个参数的值,exchange()能够执行与其他RestTemplate方法一样的工作。
示例:
//从服务器端获取Spitter资源,与getForEntity()等价
RestTemplate<Spitter> response = rest.exchange(
"http://localhost:8080/Spitter/spitters/{spitter}"
, HttpMethod.GET, null, Spitter.class, spitterId);
Spitter spitter = response.getBody();
与getForEntity()不同的是,exchange()方法允许在请求中设置头信息。接下来,我们不再给enchange()传递null,而是传入带有请求头信息的HttpEntity。
如果不指明头信息,exchange()对Spitter的GET请求会带有如下的头信息:
GET /Spitter/spitters/habuma HTTP/1.1
Accept: application/xml, text/xml, application/*+xml, application/json
Content-Length: 0
User-Agent: Java/1.6.0_20
Host: localhost:8080
Connection: keep-alive
让我们看一下Accept头信息。Accept头信息表明它能够接受多种不同的XML内容类型以及application/json。这就为服务器端留有余地来决定采用哪种格式返回资源。假设我们希望服务端以JSON格式发送资源。在这种情况下,我们需要指明application/json是Accept头信息的唯一值。
设置请求头信息是很简单的,只需构造发送给exchange()方法的HttpEntity对象,HttpEntity含有承载头信息的MultiValueMap:
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
headers.add("Accept","application/json");
HttpEntity<Object> requestEntity = new HttpEntity<Object>(headers);
在这里,我们创建了一个LinkedMultiValueMap并添加值为aplication/json的Accept头信息。接下来,我们构建了一个HttpEntity(使用Object泛型类型),将MultiValueMap作为构造参数传入。如果这是一个PUT或POST请求,我们需要为HttpEntity设置在请求体中发送的对象——对于GET请求来说,这是没有必要的。
现在,可以传入HttpEntity来调用exchange():
String url = "http://localhost:8080/Spitter/spitters/{spitter}";
ResponseEntity<Spitter> response = rest.exchange(url, HttpMethod.GET, requestEntity, Spitter.class, spitterId);
从表面上看,结果是一样的。我们得到了请求的Spitter对象。但在表面之下,请求将会带有如下的头信息发送:
GET /Spitter/spitters/habuma HTTP/1.1
Accept: application/json
Content-Length: 0
User-Agent: Java/1.6.0_20
Host: localhost:8080
Connection: keep-alive
假设服务器端能够将Spitter序列化为JSON,响应体将会以JSON格式来进行表述。
11.5 提交RESTful表单
当Web浏览器请求REST资源时,需要考虑一些局限性因素——具体来讲就是浏览器支持的HTTP方法范围。非浏览器的客户端,如使用RestTemplate,在发送任意HTTP动作方面并没有什么问题。但是HTML 4官方在表单中只支持GET和POST,忽略了PUT、DELETE以及其他的HTTP方法。尽管HTML 5和一些新的浏览器支持所有的HTTP方法,但是你不能指望应用程序的用户都使用最新的浏览器。
规避HTML 4和较早浏览器缺陷的一个技巧是将PUT或DELETE请求伪装为POST请求。这种方式提交一个浏览器支持的POST请求,但是会有一个隐藏域带有实际HTTP方法的名字。当请求到达服务器端的时候,它会重写为隐藏域指定的请求类型。
Spring 通过两个特性来支持POST伪装:
- 通过使用 HiddenHttpMethodFilter来进行请求转换;
- 使用
<sf:form>
JSP标签渲染隐藏域。<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
11.5.1 在JSP中渲染隐藏的方法域
在7.4.1节中,介绍了如何使用Spring的表单绑定库渲染HTML表单。<sf:form>
标签为其他的表单绑定标签设置内容,它将渲染的表单与模型属性关联起来。
在HTML表单中,将PUT或DELETE请求伪装为POST请求的关键是创建一个带有隐藏域并且method为POST的表单。例如,以下的HTML片段展示了如何提交DELETE请求的表单:
<form method="post" >
<input type="hidden" name="_method" value="delete" />
...
</form>
可以看到,创建带有隐藏域的表单并不困难,这个隐藏域会指定真正的HTTP方法。你所要做的就是添加一个隐藏域并将这个域的值设置为期望的HTTP方法名,这个域的名字是需要表单和服务端进行协商并达成一致的。当这个表单提交时,会发送POST请求到服务器端。可以想象一下,服务器端将从_method域中得到真正要处理的方法类型(稍后,我们会看到如何配置服务器端这么做)。
当使用Spring的表单绑定库时,<sf:form>
会让其变得更简单。你可以将method属性设置为期望的HTTP方法,<sf:form>
将为你处理隐藏域:
<sf:form method="delete" modelAttribute="spitter">
...
</sf:form>
11.5.2 发布真正的请求
当浏览器以PUT或DELETE请求提交渲染所得的表单时,在各个方面它都是一个POST请求。
同时,控制器的处理方法使用@RequestMapping注解,在等待处理PUT和DELETE请求。HTTP方法的不匹配问题必须在DispatcherServlet查找控制器处理方法之前解决。这就是HiddenHttpMethodFilter所要做的事情。
HiddenHttpMethodFilter是一个Servlet过滤器,并要在web.xml中进行配置:
<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>
org.springframework.web.filter.HiddenHttpMethodFilter
</filter-class>
</filter>
…
<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
通过非浏览器客户端发送的请求,包括RestTemplate发送的请求,能够发送各种HTTP动作因此没有必要包装成POST的形式发送。所以,如果不涉及浏览器表单发送的PUT和DELETE请求,那么就没有必要使用HiddenHttpMethodFilter服务。