领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多在过去的几年里,REST 已成为基于 SOAP/WSDL/WS-* 的分布式架构的引人注目的替代方案。因此,当我们开始计划 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 开发人员(现有的和新的)来说都是令人满意的。此外,已经有三个 JAX-RS 实现提供了 Spring 支持(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标头的一个问题是,在 HTML 中,无法在 Web 浏览器中更改它。例如,在 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,或者只需使用“真实”方法作为附加参数(在 HTML 表单中建模为隐藏输入字段)执行 POST。后一种技巧就是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;
}
在 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 方面。