《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服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章