Spring 3 中的 REST:@MVC

工程 | Arjen Poutsma | 2009年03月08日 | ...

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

一些背景知識

好的,我說謊了:首先是一些背景知識。如果您真的想了解新功能,請隨時跳到下一節

對我來說,REST 的工作大約在兩年前開始,就在閱讀了 Leonard Richardson 和 Sam Ruby 的 O'Reilly 撰寫的強烈推薦書籍RESTful Web Services之後。最初,我正在考慮將 REST 支援添加到 Spring Web Services,但在原型上工作了幾個星期後,我很清楚這不是一個很好的選擇。 特別是,我發現我必須從 Spring-MVC 複製大部分邏輯DispatcherServlet到 Spring-WS。 顯然,這不是前進的方向。

大約在同一時間,我們引入了 Spring MVC 的基於註釋的模型。 該模型顯然是對以前基於繼承的模型的改進。 當時另一個有趣的發展是 JAX-RS 規範的發展。 我的下一次嘗試是嘗試合併這兩個模型:嘗試將 @MVC 註釋與 JAX-RS 註釋結合起來,並能夠在以下環境中運行 JAX-RS 應用程式DispatcherServlet。 雖然我確實從這項工作中獲得了一個可行的原型,但結果並不令人滿意。 有一些技術問題我不會讓您感到厭煩,但最重要的是,對於已經習慣於 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 中,我們透過以下方式引入了 URI 模板的使用@PathVariable註釋。 例如


@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標頭是在 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 專案複製過來。 該模組包裝了 XML 封送處理技術(例如 JAXB、Castor、JiBX 等),並使其更容易在 Spring 應用程式內容中配置這些技術,
  • theJacksonJsonView,用於模型中物件的 JSON 表示形式。 此檢視實際上是 Spring JavaScript 專案的一部分,我們將在以後的部落格文章中對此進行更多討論。
顯然,這些與ContentNegotiatingViewResolver!

HTTP 方法轉換

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

雖然 HTTP 定義了這四種方法,但 HTML 僅支援兩種方法:GET 和 POST。 幸運的是,有兩種可能的解決方法:您可以使用 JavaScript 執行 PUT 或 DELETE,或者只是使用 'real' 方法作為附加參數來執行 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,'real' 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 的更高版本中添加對深 ETag 的支援,方法是依賴 JPA 的 @Version 註釋或 AspectJ 切面實例。

還有更多!

在後續的文章中,我將結束我的 RESTful 之旅,並談論RestTemplate,它也是在 Spring 3.0 M2 中引入的。 此類使您可以像JdbcTemplate, JmsTemplate等一樣,從客戶端存取 RESTful 資源。

取得 Spring 電子報

透過 Spring 電子報保持聯繫

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

檢視全部