《Spring3實戰》摘要(11)爲 Spring 添加 REST 功能(1)

第十一章 爲 Spring 添加 REST 功能

近幾年來,以信息爲中心的表述性狀態轉移(Repressentational State Transfer,REST)已成爲替換傳統 SOAP(簡單對象訪問協議) Web服務的流行方案。爲了幫助 Spring 開發人員使用 REST 架構模式,Spring 3.0 封裝了對 REST 的良好支持。

Spring 對 REST 的支持是構建在 Spring MVC 之上的,在本章中,我們將基於Spring MVC知識來開發處理 RESTful 資源的控制器。

11.1 瞭解 REST

在軟件開發中你可能會發現一種很流行的做法,那就是在推動 REST 代替 SOAP Web 服務的時候,會談論到 SOAP 的不足。

對於許多應用程序而言,使用 SOAP 可能會有些大材小用了,而 REST 提供了一個更簡單的可選方案。

11.1.1 REST 的基本原理

當談論 REST 時,有一種常見的錯誤就是將其視爲“基於 URL 的 Web 服務”—-將 REST 作爲另一種類型的遠程過程調用(Remote Procedure Call,RPC)機制,就像 SOAP 一樣,只不過是通過簡單的 HTTP URL 而不是 SOAP 的大量 XML 命名空間來觸發。

恰好相反,REST 與 RPC 幾乎沒有任何關係。RPC 是面向服務的,並關注於行爲和動作;而 REST 是面向資源的,強調描述應用程序的事物和名詞。

爲了理解 REST 是什麼,我們將它的首字母縮寫拆分爲不同的組成部分。

  • 表述性(Representational): REST 資源實際上可以用各種形式來進行表述,包括 XML、JSON(JavaScript Object Notation)甚至 HTML。
  • 狀態(State):當使用 REST 的時候,我們更關注資源的狀態而不是對資源採取的行爲。
  • 轉移(Transfer):REST 涉及轉移資源數據,它以某一種表述性形式從一個應用轉移到了一個應用。

更簡潔地講,REST 就是將資源的狀態以最合適的形式從服務器端轉移到客戶端(或者反之)。

11.1.2 Spring 是如何支持 REST 的

Spring 很早就有導出 REST 資源的需求。Spring 3 對 Spring MVC 的一些增強功能爲 REST 提供了良好的支持。現在,Spring 支持以下方式來開發 REST 資源。

  • 控制器可以處理所有的 HTTP 方法,包含4個主要的 REST 方法:GET、PUT、 DELETE 以及 POST。
  • 新的 @PathVariable 註解使得控制器能夠處理參數化的 URL(將變量輸入作爲 URL 的一部分)
  • Spring 的表單綁定 JSP 標籤庫的<form:form>標籤以及新的 HiddenHttpMethodFilter,使得通過 HTML 表單提交 PUT 和 DELETE 請求稱爲可能,即使在某些瀏覽器中不支持這些 HTTP 方法。
  • 通過使用 Spring 的視圖和視圖解析器,資源可以以各種形式進行表說,包括將模型數據表現爲 XML、JSON、Atom 和 RSS 的新視圖實現。
  • 可以使用新的 ContentNegotiatingViewResolver 來選擇最適合客戶端的表述。
  • 基於視圖的渲染可以使用新的 @ResponseBody 註解和各種 HttpMethodConverter 實現來達到。
  • 類似地,新的 @ResponseBody 註解以及 HttpMethodConverter 實現可以將傳入的 HTTP 數據傳入控制器處理方法的 Java 對象。
  • RestTemplate 簡化了客戶端對 REST 資源的使用。

11.2 編寫面向資源的控制器

編寫 Spring MVC 控制器類的模型是相當靈活的。但是這種靈活性的副作用就是 Spring MVC 允許你開發出不符合 RESTful 資源的控制器。編寫出的控制器很容易是 RESTless 的。

11.2.1 剖析 RESTless 的控制器

以下是一個 RESTless 控制器,DisplaySpittleController 的編寫方式並沒有嚴重的錯誤。但是,它並不是一個 RESTful 的控制器。它是面向行爲的並關注於一個特殊的用例:以 HTML 的形式展現一個 Spittle 對象的詳細信息。就連控制器的類名都說明了這一點。

/**
 * 這是一個 RESTless 的控制器,是錯誤的演示例子
 */
@Controller
@RequestMapping("/displaySpittle.htm")
public class DisplaySpittleController {
    private final SpitterService spitterService;

    @Inject
    public DisplaySpittleController(SpitterService spitterService){
        this.spitterService = spitterService;
    }

    @RequestMapping(method=RequestMethod.GET)
    public String showSpittle(@RequestParam("username") String username,Model model){
        model.addAttribute(spitterService.getSpitter(username));
        return "spittles/view";
    }
}

11.2.2 處理 RESTful URL

URL 是統一資源定位符(Uniform Resource Locator)的縮寫。按照這個名字,URL 本意是用於定位資源的。此外,所有的 URL 同時也都是 URI 或 統一資源標識符(Uniform Resource Identifier)。如果這樣的話,我們可以認爲任何給定的 URL 不僅可以定位一個資源還可以用於標識一個資源。

11.2.1 中的控制器處理的 URL 是 http://localost:8080/Spitter/displaySpittle.htm?username=123,這個 URL 並沒有定位或標識資源。它要求服務器展現一個 Spittle。URL 中唯一的標識就是 id 查詢參數。 URL 的基礎部分是面向動作的,這就是說它是一個 RESTless 的 URL。

11.2.2.1 RESTful URL 的特點

不同於 RESTful URL,RESTful URL 完全承認 HTTP 用於標識資源的。例如下圖展示了我們應該如何重構 RESTless URL 使其更加面向資源。
這裏寫圖片描述

這個 URL 不僅定位資源,還可以唯一標識這個資源—-它不僅是 URL 也是 URI。這裏使用完整的基本 URL 來標識資源,而不是使用查詢參數標識資源。

實際上,新的 URL 根本沒有查詢參數。儘管使用查詢參數往服務器發送信息仍然是一種合法的方式,但是這應當用於爲服務器創建資源提供指導。查詢參數不應該用於幫助標識資源。

有關 RESTful URL 還有最後一個關注點:它們是有層級的。如果從左到右讀,你會經歷從抽象到具體的過程。在我們的示例中,URL 有多個層級,每層都可以用於標識一個資源。

有趣的是,RESTful URL 的路徑是參數化的。RESTless URL 使用查詢參數作爲輸入,而 RESTful URL 的輸入是 URL 路徑的一部分。爲了處理這種類型的URL,我們需要一種能夠從 URL 路徑中獲取輸入的方式來編寫控制器處理方法。

11.2.2.2 在 URL 中嵌入參數

爲了使用參數化的 URL 路徑,Spring 3 引入了新的 @PathVariable 註解。

/**
 * 這是標準的 RESTful 控制器示例
 */
@Controller
@RequestMapping("/spittles")
public class SpittleController {
    private SpitterService spitterService;

    @Inject
    public SpittleController(SpitterService spitterService){
        this.spitterService = spitterService;
    }

    //使用路徑中的佔位符變量
    @RequestMapping(value="/{username}",method=RequestMethod.GET)
    public String getSpittle(@PathVariable("username") String username,Model model){
        model.addAttribute(spitterService.getSpitter(username));
        return "spittles/view";
    }
}

不管你是不是通過名字來明確指定路徑變量,@PathVariable 可以讓你編寫控制器方法來處理標識資源的 URL 而不是描述某些行爲的 URL。RESTful 請求的另一方面就是用於 URL 的 HTTP 方法。

11.2.3 執行 REST 動作

REST 是關於資源狀態轉移的。因此,我們需要一些動作(verb)來應用於這些資源—-轉移資源狀態的動作。對於任意給定的資源,最常見的操作是在服務器上對資源進行創建、檢索、更新和刪除。

我們關心的動作(post、get、put 以及 delete)直接對應於 HTTP 規範定義的4個方法。

方法 描述 是否安全 是否冪等
GET 從服務器上檢索資源數據,資源通過請求的URL來進行標識
POST 傳送(POST)數據到服務器上,數據會由監聽該請求URL的處理器來進行處理
PUT 按照請求的URL,放置(Put)資源數據到服務器上
DELETE 將請求URL標識的資源從服務器上刪除(DELETE)
OPTIONS 請求與服務器通信可用的選項
HEAD 類似於GET,但只會返回頭部信息–在響應體中不應該包含內容
TRACE 將請求體的內容返回給客戶

每個HTTP方法具有兩個特性:安全性和冪等性。如果一個方法不改變資源的狀態,就認爲它是安全的。冪等的方法可能改變也可能不改變狀態,但是一次請求和多次請求具有相同的作用。按照定義,所有安全的方法都必須是冪等的,但並不是所有冪等的方法都是安全的。

表中描述的4個HTTP方法通常會匹配到 CRUD(創建、讀取、更新、刪除)操作。GET 方法執行讀取操作,而 DELETE 方法執行刪除操作。儘管 PUT 和 POST 方法不僅僅能夠用於更新和創建操作,但通常來講它們就是應該這麼使用的。

11.2.3.1 使用 PUT 更新資源

GET 請求將資源的狀態從服務器轉移到客戶端,而PUT將資源的狀態從客戶端轉移到服務器上。

@RequestMapping(value="/{id}",method=RequestMethod.PUT)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void putSpittle(@PathVariable("id") long id,@Valid Spittle spittle){
    spitterService.saveSpittle(spittle);
}

上例中,putSpittle() 方法使用了 @ResponseStatus 註解定義了 HTTP 狀態,這個狀態要設置在發往客戶端的響應中。上例中,HttpStatus.NO_CONTENT 說明響應狀態要設置爲 HTTP 狀態碼 204。這個狀態碼以爲着請求被成功處理了,但是在響應體中不包含任何返回信息。

12.2.3.2 處理 DELETE 請求

除了簡單地更新資源,我們可能還希望將其完全清理掉。當你不再需要某條資源的時候,這就是 HTTP 的DELETE 方法發揮作用的時候。

@RequestMapping(value="/{id}",method=RequestMethod.DELETE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteSpittle(@PathVariable("id") long id){
    spitterService.deleteSpittle(id);
}

12.2.3.3 使用 POST 創建資源

@RequestMapping(method=RequestMethod.POST)
//用HTTP 201 進行響應
@ResponseStatus(HttpStatus.CREATED)
public @ResponseBody Spittle createSpittle(@Vaild Spittle spittle,
        BindingResult result, HttpServletResponse response)
        throws BindException{
    if(result.hasErrors()){
        throw new BindException(result);
    }
    spitterService.saveSpittle(spittle);
    //設置資源位置
    response.setHeader("Location","/spittles/"+spittle.getId());
    //返回 Spittle 資源
    return spittle;
}

createSpittle() 將處理 URL 模式匹配”/spittles”的請求。這個方法使用了 @ResponseStatus 註解來設置 HTTP 狀態碼。這次,狀態碼被設置成了 201(Created)來表明一個資源被成功創建了。當一個 HTTP 201 響應發送到客戶端,新資源的 URL 也會一同發送回來。所以,createSpittle() 方法最後要做的事情之一就是設置 Location 頭信息來包含資源的 URL。

儘管這不是 HTTP 201 響應的強制要求,但可以在響應體中返回完整的實體表述。所以,與前面 GET 的處理方法 getSpittle() 類似,這個方法最終返回新建的 Spittle 對象。這個對象會被轉化爲客戶端可用的表述形式。

11.3 表述資源

表述是 REST 中很重要的一個方面。它是關於客戶端和服務端針對某一資源是如何通信的。任何給定的資源都幾乎可以用任意的形式來進行表述。如果資源的使用者希望使用 JSON,那麼資源就可以用 JSON 格式來表述。如果使用者習慣使用尖括號,那相同的資源可以用 XML 來進行表述。同時,如果用戶在瀏覽器中查看資源的話,可能更願意以 HTML 的方式來展現。

需要了解的是控制器本身並不關心資源如何表述。控制器以 Java 對象的方式來處理資源。直到控制器完成了它的工作之後,資源纔會被轉化成最適合客戶端的形式。

Spring 提供了兩種方法將資源的 Java 表述形式轉換爲發送給客戶端的表述形式

  • 基於視圖渲染進行協商;
  • HTTP 消息轉換器。

鑑於我們在第七章中討論過視圖解析器,並且已經熟悉了基於視圖的渲染(第七章),我們會直接查看如何使用內容協商來選擇視圖或視圖解析器,它們將資源渲染爲客戶端能夠接受的形式。

11.3.1 協商資源表述

回顧第七章,當控制器處理方法完成時,通常會返回一個邏輯視圖名。如果方法不直接返回邏輯視圖名,那麼邏輯視圖名會來源於請求的 URL。DispatcherServlet 接下來會將視圖的名字傳遞給一個視圖解析器,要求它來幫助確定應該用哪個視圖來渲染請求結果。

Spring 的 ContentNegotiatingViewResolver 是一個特殊的視圖解析器,它考慮到了客戶端所需要的內容類型。

<!-- 將ContentNegotiatingViewResolver視圖解析器配置在 Spring 上下文中 -->
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <!-- 首先檢查URL擴展名與設置的key匹配,如果沒有匹配值,則會查找請求的 Accept 頭信息來確定媒體類型 -->
    <property name="mediaTypes">
        <entry key="json" value="application/json" />
        <entry key="xml" value="text/xml" />
        <entry key="htm" value="text/html" />
    </property>
    <!-- 如果請求中不包含 Accept 頭信息,將使用 defaultContentType 所設置的類型 -->
    <property name="defaultContentType" value="text/html" />
</bean>

要理解 ContentNegotiatingViewResolver 是如何工作的,要涉及內容協商的兩個步驟。

  1. 確定請求的媒體類型。
  2. 找到適合請求媒體類型的最佳視圖。

12.3.1.1 確定請求的媒體類型

ContentNegotiatingViewResolver 將考慮到 瀏覽器 Accept 頭部 信息並使用它請求的媒體類型,但它會首先查看 URL 的文件擴展名。如果 URL 在結尾處有文件擴展名的話,它將擴展名與 mediaTypes 中的條目進行匹配。mediaTypes 是一個 Map,它的 key 是文件擴展名而 value 是媒體類型。如果找到了匹配項,那麼將會使用找到的媒體類型。通過這種方式,文件擴展名將覆蓋 Accept 頭信息中的任何媒體類型。

12.3.1.2影響如何選擇媒體類型

以上介紹中,我們展現了在確定請求媒體類型時的默認選擇策略。但是有幾個選擇項可以影響到這個行爲。

  • 將 favorPathExtension 屬性設置爲 false,將會使得 ContentNegotiatingView Resoler 忽略 URL 路徑的擴展名。
  • 將 JAF(Java Activation Framework)添加到類路徑下將會使得 ContentNegotiatingViewResolver 除了使用 mediaTypes 屬性中的條目以外,在由路徑擴展名確定媒體類型時還會藉助 JAF。
  • 如果你將 favorParameter 屬性設置爲 true,並且請求中包含名爲 format 參數,那麼 format 參數的值將與 mediaTypes 屬性來進行匹配(另外,參數名可以通過設置 parameterName 屬性來進行選擇 )。
  • 將 ignoreAcceptHeader 設置爲 true ,將忽略 Accept 信息。
<!-- 將 favorParameter 屬性設置爲 true後,只要請求的 format 參數被設置爲json,即使請求的URL中沒有文件擴展名也能匹配 application/json 媒體類型。 -->
<property name="favorParameter" value="true" />

12.3.1.3 查找視圖

不像其他視圖解析器那樣,ContentnegotiatingViewResolver 並不會直接解析視圖,而是委託其他的視圖解析器來查找最適合客戶端的視圖。如果沒有特別指明的話,它將使用應用程序中的所有視圖解析器。但可以通過設置 viewResolvers 屬性明確聲明它委託的視圖解析器列表。

ContentnegotiatingViewResolver 將會使用所有的視圖解析器來將邏輯視圖名解析爲視圖。每個解析得到的視圖都會存放在一個待選視圖列表中。此外,如果在 defaultView 屬性中指定了某個視圖的話,那麼這個視圖將被添加到候選視圖列表的尾部。

當候選視圖列表組裝完成之後,ContentnegotiatingViewResolver 將會循環所有請求的媒體類型,並在候選視圖中查找能產生匹配內容類型的視圖。找到的第一個匹配項就是要使用的視圖。

最後,如果 ContentnegotiatingViewResolver 沒有找到合適的視圖,那麼它將返回 null 視圖。或者,如果 useNotAcceptableStatusCode 屬性被設置爲 true,那麼將返回帶有 HTTP 狀態碼 406(Not Acceptable)的視圖。

通過內容協商來爲客戶端渲染資源表述的方式與我們在第七章中開發應用程序的 Web 前端的方式是吻合的。對於 Spring MVC Web 應用程序已有的 HTML 表述方式,這是在它上面添加其他表述方式的好辦法。

當定義機器使用的 RESTful 資源時,另一種開發控制器的方式可能更有意義,這種控制器產生的數據將會作爲資源被其他的應用程序所使用。這就是 Spring 的 HTTP 消息轉換器 和 @ResponseBody 註解發揮作用的地方了。

11.3.2 使用 HTTP 信息轉換器

典型的 Spring MVC 控制器方法在結束時會將一些信息放在模型中,然後到達一個視圖來爲用戶渲染這些數據。儘管有多種方式來填充數據和識別視圖,但是到目前爲止我們看到的控制器遵循的都是這種基本模式。

但是,當控制器的工作是產生資源表述的時候,有一種更直接的方法可以繞過模型和數據。在這種風格的 處理器方法中,控制器返回的對象將自動轉化爲適合客戶端的表述形式。

要使用這項新的技術,首先要將 @ResponseBody 註解添加到控制器處理方法上。

11.3.2.1 在響應體中返回資源狀態

如果在方法中使用了 @ResponseBody ,那表明 HTTP 信息轉換器機制會發揮作用,並將返回的對象轉換爲客戶端要的任意格式。

//示例
@RequestMapping(value="/{username}",method=Request.GET,headers={"Accept=text/xml,application/json"})
public @ResponseBody Spitter getSpitter(@PathVariable String username){
    return spitterService.getSpitter(username);
}

@ResponseBody 註解會告知 Spring,我們要將返回的對象作爲資源發送給客戶端,並將其轉換爲客戶端可接受的表述形式。更具體地講,資源的格式需要滿足請求中 Accept 頭信息的要求。如果請求中沒有包含 Accept 頭部信息的話,那它就假設客戶端能夠接受任意的表述形式。

對於 Accept 頭部信息,請注意 getSpitter() 的 @RequestMapping 註解。headers 屬性表明這個方法只處理 Accept 頭部信息爲 text/xml 或 application/json 的請求。其他任何類型的請求,即使它的 URL 匹配指定的路徑並且是 GET 請求也不會被這個方法處理。這樣的請求會被其他的方法進行處理(如果存在適當方法的話),或者返回客戶端 HTTP 406 (Not Acceptable)響應。

Spring HTTP 信息轉換器的工作就是,將處理方法返回的 Java 對象轉換爲滿足客戶端要求的表述形式。Spring 自帶了各種各樣的轉換器,這些轉換器滿足了最常見的將對象轉換爲表述的需要。

信息轉換器 描述
AtomFeedHttpMessageConverter Rome Feed 對象和 Atom feed(媒體類型 application/atom+xml)之間的相互轉換。如果 Rome 包在類路徑下將會進行註冊
BufferedImageHttpMessageConverter BufferedImages 與圖片二進制數據之間互相轉換
ByteArrayHttpMessageConverter 讀取/寫入字節數組。從所有媒體類型(*/*)中讀取,並以application/octet-steam格式寫入。默認註冊
FormHttpMessageConverter 將application/x-www-form-urlencoded內容讀入到MultiValueMap<String,String>中,也會將MultiValueMap<String,String>寫入到application/x-www-form-urlencoded中或將MultiValueMap<String,Object>寫入到multipart/porm-data
Jaxb2RootElementHttpMessageConverter 在XML(text/xml或application/xml)和使用JAXB2註解的對象間互相讀取和寫入。如果JAXB v2庫在類路徑下,將進行註冊
MappingJacksonHttpMessageConverter 在JSON和類型化的對象或非類型化的HashMap間互相讀取和寫入。如果Jackson JSON庫在類路徑下,將進行註冊
MarshallingHttpMessageConverter 使用注入的marshaller和unmarshaller來讀入和寫入XML。支持的marshaller和unmarshaller包括Castor、JAXB2、JIBX、XML Beans 以及XStream
ResourceHttpMessageConverter 讀取或寫入Resource,默認註冊
RssChannelHttpMessageConverter 在RSS feed和RomeChannel對象間相互讀取或寫入。如果Rome庫在類路徑下,將進行註冊
SourceHttpMessageConverter 在XML和javax.xml.transform.Source對象間互相讀取和寫入。默認註冊
StringHttpMessageConverter 將所有媒體類型(*/*)讀取爲String。將String寫入爲text/plain。默認註冊
XmlAwareFormHttpMessageConverter FormHttpMessageConverter的擴展,使用SourceHttpMessageConverter來支持基於XML的部分。默認註冊

例如,假設客戶端通過請求的Accept頭信息表明它能接受application/json,並且Jackson JSON在類路徑下,那麼處理方法返回的對象將交給Mapping-JacksonHttpMessageConverter,並由其轉換爲返回客戶端的JSON表述形式。另一個方面,如果請求的頭信息表明客戶端想要text/xml格式,那麼Jaxb2RootElementHttpMessageConverter將會爲客戶端產生XML響應。

11.3.2.2 在請求體中接收資源狀態

在RESTful會話的另一端,客戶端可能會以JSON、XML或其他內容格式給我們發送一個對象過來。如果需要控制器的處理方法以原始形式來接受數據並自行進行轉換的話,這是很不方便的。幸好,就像@ResponseBody註解能夠將發送給客戶端的數據進行轉換一樣,@RequestBody也能夠對客戶端發過來的對象做相同的事情。

假設客戶端提交了一個PUT請求,在請求體中包含了JSON格式表述的Spitter對象數據。爲了以Spitter對象來接受信息,只需要在處理方法的Spitter參數上使用@RequestBody註解:

@RequestMapping(value="/{username}",method=RequestMethod.PUT,
    headers="Content-Type=application/json")
//通過@ResponseStatus註解設定該方法返回給客戶端的HTTP響應爲204狀態碼(即表示無內容)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updateSpitter(@PathVariable String username,
    @RequestBody Spitter spitter){
    spitterService.saveSpitter(spitter);
}

當請求到達時,Spring MVC發現updateSpitter()能夠處理這個請求。但是抵達的信息是JSON格式,而這個方法要求的是Spitter對象。在這種情況下,會選擇MappingJacksonHttpMessageConverter來將JSON信息轉換爲Spitter對象。爲了做到這一點,需要滿足如下條件:

  • 請求的Content-Type頭信息必須是application/json;
  • Jackson JSON庫必須包含在應用程序的類路徑下。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章