Spring 3 中的 REST:@MVC

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

在过去的几年里,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 支持(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 允许 @Controller 通过其View、视图名称和ViewResolver抽象来决定为给定请求呈现哪个视图。在 RESTful 场景中,通常允许客户端通过AcceptHTTP 标头来决定可接受的表示形式。服务器通过Content-Type标头来响应传递的表示形式。此过程称为内容协商

关于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.

视图

我们还向 Spring MVC 添加了一些新的视图,特别是
  • AbstractAtomFeedViewAbstractRssFeedView,它们可用于返回 Atom 和 RSS Feed,
  • MarshallingView,它可用于返回 XML 表示形式。此视图基于对象/XML 映射模块,该模块已从 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,或者只需使用“真实”方法作为附加参数(在 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;
}

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

查看全部