Spring 3 中的 REST:@MVC

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

在過去的幾年裡,REST 已成為 SOAP/WSDL/WS-*-based 分散式架構的一個引人注目的替代方案。因此,當我們開始規劃 Spring 的下一個主要版本——3.0 的工作時,我們非常清楚必須專注於簡化“RESTful”Web 服務和應用程式的開發。現在,什麼是“RESTful”以及什麼不是“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 開發人員來說都將是令人滿意的。此外,已經有三個 JAX-RS 實現提供 Spring 支援(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抽象讓 @Controller 決定為給定請求渲染哪個檢視。在 RESTful 場景中,通常透過AcceptHTTP 頭讓客戶端決定可接受的表示形式。伺服器透過Content-Type頭響應交付的表示形式。這個過程被稱為內容協商

關於Accept頭的一個問題是,在 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 提要,
  • MarshallingView,它可用於返回 XML 表示。此檢視基於物件/XML 對映模組,該模組已從 Spring Web Services 專案複製。此模組包裝了 JAXB、Castor、JiBX 等 XML 編組技術,並使得在 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 過濾器。因此,它可以與任何 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 過濾器,因此可以與任何 Web 框架結合使用。顧名思義,此過濾器建立所謂的淺層 ETag(與深層 ETag 相反,稍後將詳細介紹)。它的工作方式非常簡單:過濾器只是快取渲染的 JSP(或其他內容)的內容,生成其 MD5 雜湊,並將其作為響應中的ETag頭返回。下次客戶端傳送同一資源的請求時,它將使用該雜湊作為If-None-Match值。過濾器會注意到這一點,再次渲染檢視,並比較兩個雜湊。如果它們相等,則返回 304。需要注意的是,此過濾器不會節省處理能力,因為檢視仍然會渲染。它唯一節省的是頻寬,因為渲染的響應不會透過網路傳送回來。

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

以及更多!

在後續的帖子中,我將結束我的 RESTful 之旅,並談論RestTemplate,它也在 Spring 3.0 M2 中引入。此類以類似於JdbcTemplate, JmsTemplate等的方式為您提供客戶端訪問 RESTful 資源的功能。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

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

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有