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 社区所有即将举行的活动。

查看所有