Spring 3 中的 REST:@MVC

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

在过去的几年里,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 实现(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抽象来决定为给定请求渲染哪个视图。在 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 项目复制而来。该模块封装了 JAXB、Castor、JiBX 等 XML marshalling 技术,并使得在 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 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;
}

ETag 支持

ETag(实体标签)是 HTTP/1.1 兼容 Web 服务器返回的 HTTP 响应头,用于确定给定 URL 上内容的变化。它可以被视为更复杂的Last-Modified头的后续版本。当服务器返回带有 ETag 头的表示形式时,客户端可以在后续的 GET 请求中使用此头,作为If-None-Match头。如果内容没有改变,服务器将返回304: Not Modified.

。在 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 切面。

还有更多!

在下一篇文章中,我将结束我的 RESTful 之旅,并讨论RestTemplate,它也在 Spring 3.0 M2 中引入。这个类提供了客户端访问 RESTful 资源的功能,其方式类似于JdbcTemplate, JmsTemplate等。

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持连接

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持及二进制文件。

了解更多

即将到来的活动

查看 Spring 社区所有即将到来的活动。

查看全部