領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多在過去的幾年裡,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 實現(Jersey、RESTEasy 和 Restlet)。在這種情況下再新增第四個似乎不是明智地利用我們寶貴的時間。
在 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的請求,例如。
關於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.
雖然 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;
}
。在 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 切面。