《Spring3实战》摘要(11)为 Spring 添加 REST 功能(2)

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 deleteString url, Object... urivariables) throws RestClientException;

void deleteString 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服务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值