Spring 3 中的 REST:@MVC

工程 | Arjen Poutsma | 2009 年 3 月 8 日 | ...

在過去的幾年裡,REST 已成為 SOAP/WSDL/WS-*-based 分散式架構的一個引人注目的替代方案。因此,當我們開始規劃下一個 Spring 主要版本(3.0 版)的工作時,我們很清楚必須專注於簡化“RESTful” Web 服務和應用程式的開發。現在,“RESTful”的定義以及它是什麼和不是什麼本身就可以成為一個全新的博文主題;在這篇文章中,我將採取更實際的方法,重點介紹我們在 Spring MVC 的 @Controller 模型中新增的功能。

一些背景知識

好的,我說謊了:首先還是有一些背景介紹。如果您確實想了解新功能,請隨時跳到下一節

對我來說,對 REST 的工作大約始於兩年前,在我閱讀了 O'Reilly 出版社 Leonard Richardson 和 Sam Ruby 撰寫的強烈推薦的著作 RESTful Web Services 之後不久。最初,我考慮將 REST 支援新增到 Spring Web Services 中,但在原型上工作了幾周後,我清楚地意識到這並不是一個很好的契合點。特別是,我發現我不得不從 Spring MVC 的大部分邏輯複製過來DispatcherServlet到 Spring-WS 中。顯然,這不是前進的方向。

大約在同一時間,我們引入了 Spring MVC 的基於註解的模型。這個模型顯然是對之前基於繼承的模型的改進。當時的另一個有趣進展是 JAX-RS 規範的開發。我的下一次嘗試是試圖合併這兩個模型:嘗試將 @MVC 註解與 JAX-RS 註解結合起來,並能夠在DispatcherServlet中執行 JAX-RS 應用。雖然我透過這次努力得到了一個可工作的原型,但結果並不令人滿意。存在一些技術問題,我就不詳細介紹了,但最重要的是,對於已經習慣了 Spring MVC 2.5 的開發人員來說,這種方法感覺“笨拙”且不自然。

最後,我們決定將 RESTful 功能直接新增到 Spring MVC 本身的功能中。顯然,這意味著與 JAX-RS 會有一些重疊,但至少程式設計模型對於現有和新的 Spring MVC 開發人員來說會令人滿意。此外,已經有三個提供 Spring 支援的 JAX-RS 實現(JerseyRESTEasyRestlet)。在這種情況下再新增第四個似乎不是明智地利用我們寶貴的時間。

Spring MVC 3.0 中的 RESTful 功能

現在,背景介紹就到這裡,讓我們來看看這些功能吧!

URI 模板

URI 模板是一種類似 URI 的字串,包含一個或多個變數名。當這些變數被替換為值時,模板就變成了一個 URI。更多資訊請參見建議的 RFC 文件

在 Spring 3.0 M1 中,我們透過@PathVariable註解引入了 URI 模板的使用。例如


@RequestMapping("/hotels/{hotelId}")
public String getHotel(@PathVariable String hotelId, Model model) {
    List<Hotel> hotels = hotelService.getHotels();
    model.addAttribute("hotels", hotels);
    return "hotels";
}

當請求傳送到/hotels/1時,這個 1 將繫結到hotelId引數。您可以選擇指定引數繫結的變數名,但當您啟用除錯編譯程式碼時,這是不必要的:我們會從引數名推斷路徑變數名。

您也可以擁有多個路徑變數,如下所示


@RequestMapping(value="/hotels/{hotel}/bookings/{booking}", method=RequestMethod.GET)
public String getBooking(@PathVariable("hotel") long hotelId, @PathVariable("booking") long bookingId, Model model) {
    Hotel hotel = hotelService.getHotel(hotelId);
    Booking booking = hotel.getBooking(bookingId);
    model.addAttribute("booking", booking);
    return "booking";
}

這將匹配諸如/hotels/1/bookings/2的請求,例如。

您還可以結合使用 Ant 風格路徑和路徑變數,如下所示


@RequestMapping(value="/hotels/*/bookings/{booking}", method=RequestMethod.GET)
public String getBooking(@PathVariable("booking") long bookingId, Model model) {
    ...
}

您還可以使用資料繫結


@InitBinder
public void initBinder(WebDataBinder binder) {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}

@RequestMapping("/hotels/{hotel}/dates/{date}")
public void date(@PathVariable("hotel") String hotel, @PathVariable Date date) {
    ...
}

上面這個將匹配/hotels/1/dates/2008-12-18的請求,例如。

內容協商

在 2.5 版本中,Spring-MVC 透過其View、檢視名和ViewResolver抽象來決定為給定請求渲染哪個檢視。在 RESTful 場景中,通常透過AcceptHTTP 頭讓客戶端決定可接受的表示形式。伺服器透過Content-Type頭響應送達的表示形式。這個過程稱為內容協商

關於Accept頭的一個問題是無法在 Web 瀏覽器(HTML 中)更改它。例如,在 Firefox 中,它固定為 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 那麼如果您想連結到特定資源的 PDF 版本怎麼辦?檢視副檔名是一個很好的解決方法。例如,http://example.com/hotels.pdf檢索酒店列表的 PDF 檢視,就像http://example.com/hotels使用 Accept 頭application/pdf.

一樣。這就是ContentNegotiatingViewResolver的作用:它封裝了一個或多個其他的ViewResolver,檢視Accept頭或副檔名,並解析出與這些對應的檢視。在即將釋出的博文中,Alef Arendsen 將向您展示如何使用ContentNegotiatingViewResolver.

檢視

我們還在 Spring MVC 中添加了一些新的檢視,特別是
  • AbstractAtomFeedViewAbstractRssFeedView,它們可以用於返回 Atom 和 RSS feed,
  • MarshallingView,它可以用於返回 XML 表示形式。這個檢視基於 Object/XML Mapping 模組,該模組從 Spring Web Services 專案複製而來。該模組封裝了 JAXB、Castor、JiBX 等 XML marshalling 技術,並使得在 Spring 應用上下文中配置這些技術更加容易,
  • 以及JacksonJsonView,用於表示模型中物件的 JSON 形式。這個檢視實際上是 Spring JavaScript 專案的一部分,我們將在未來的博文中詳細討論。
顯然,這些與ContentNegotiatingViewResolver!

HTTP 方法轉換

REST 的另一個關鍵原則是使用統一介面。基本上,這意味著所有資源(URL)都可以使用相同的四種 HTTP 方法進行操作:GET、PUT、POST 和 DELETE。對於每種方法,HTTP 規範都定義了精確的語義。例如,GET 應該始終是一個安全操作,意味著它沒有副作用;而 PUT 或 DELETE 應該冪等,意味著您可以重複這些操作多次,但最終結果應該相同。

雖然 HTTP 定義了這四種方法,但 HTML 只支援兩種:GET 和 POST。幸運的是,有兩種可能的變通方法:您可以使用 JavaScript 執行 PUT 或 DELETE,或者只需透過 POST 請求並在其中新增一個附加引數(在 HTML 表單中模擬為隱藏輸入欄位)來指定“真實”方法。後一種技巧正是HiddenHttpMethodFilter所做的。這個過濾器是在 Spring 3.0 M1 中引入的,它是一個普通的 Servlet Filter。因此,它可以與任何 Web 框架結合使用(不僅僅是 Spring MVC)。只需將此過濾器新增到您的web.xml中,帶有隱藏的_method引數的 POST 請求將被轉換為相應的 HTTP 方法請求。

作為額外的好處,我們還在 Spring MVC 表單標籤中添加了方法轉換支援。例如,以下摘自更新的 Petclinic 示例的程式碼片段


<form:form method="delete">
    <p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>

實際上將執行一個 HTTP POST 請求,其中“真實”的 DELETE 方法隱藏在請求引數後面,以便由HiddenHttpMethodFilter獲取。因此,對應的 @Controller 方法是


@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
    this.clinic.deletePet(petId);
    return "redirect:/owners/" + ownerId;
}

ETag 支援

ETag(實體標籤)是 HTTP/1.1 相容 Web 伺服器返回的 HTTP 響應頭,用於確定給定 URL 上內容的變化。它可以被視為更復雜的Last-Modified頭的後續版本。當伺服器返回帶有 ETag 頭的表示形式時,客戶端可以在後續的 GET 請求中使用此頭,作為If-None-Match頭。如果內容沒有改變,伺服器將返回304: Not Modified.

。在 Spring 3.0 M1 中,我們引入了ShallowEtagHeaderFilter。這是一個普通的 Servlet Filter,因此可以與任何 Web 框架結合使用。顧名思義,該過濾器建立所謂的淺層 ETag(與深層 ETag 相對,稍後會詳細介紹)。它的工作方式非常簡單:過濾器只是快取已渲染 JSP(或其他內容)的內容,生成其 MD5 雜湊值,並將其作為ETag頭在響應中返回。下次客戶端請求同一資源時,它將使用該雜湊值作為If-None-Match值。過濾器注意到這一點,再次渲染檢視,並比較這兩個雜湊值。如果它們相等,則返回 304。重要的是要注意,此過濾器不會節省處理能力,因為檢視仍然會被渲染。它唯一節省的是頻寬,因為已渲染的響應不會透過網路傳送回去。

深層 ETag 有點複雜。在這種情況下,ETag 基於底層域物件、RDMBS 表等。使用這種方法,除非底層資料發生變化,否則不會生成內容。不幸的是,以通用方式實現這種方法比淺層 ETag 困難得多。我們可能會在更高版本的 Spring 中新增對深層 ETag 的支援,例如透過依賴 JPA 的 @Version 註解或 AspectJ 切面。

還有更多!

在下一篇文章中,我將結束我的 RESTful 之旅,並討論RestTemplate,它也在 Spring 3.0 M2 中引入。這個類提供了客戶端訪問 RESTful 資源的功能,其方式類似於JdbcTemplate, JmsTemplate等。

獲取 Spring 新聞通訊

訂閱 Spring 新聞通訊,保持連線

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲取支援

Tanzu Spring 透過一個簡單的訂閱提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援及二進位制檔案。

瞭解更多

即將到來的活動

檢視 Spring 社群所有即將到來的活動。

檢視全部