您的位置:首页 > 编程语言 > Java开发

《Spring3实战》摘要(11)为 Spring 添加 REST 功能(1)

2018-02-07 10:43 483 查看

第十一章 为 Spring 添加 REST 功能

近几年来,以信息为中心的表述性状态转移(Repressentational State Transfer,REST)已成为替换传统 SOAP(简单对象访问协议) Web服务的流行方案。为了帮助 Spring 开发人员使用 REST 架构模式,Spring 3.0 封装了对 REST 的良好支持。

Spring 对 REST 的支持是构建在 Spring MVC 之上的,在本章中,我们将基于Spring MVC知识来开发处理 RESTful 资源的控制器。

11.1 了解 REST

在软件开发中你可能会发现一种很流行的做法,那就是在推动 REST 代替 SOAP Web 服务的时候,会谈论到 SOAP 的不足。

对于许多应用程序而言,使用 SOAP 可能会有些大材小用了,而 REST 提供了一个更简单的可选方案。

11.1.1 REST 的基本原理

当谈论 REST 时,有一种常见的错误就是将其视为“基于 URL 的 Web 服务”—-将 REST 作为另一种类型的远程过程调用(Remote Procedure Call,RPC)机制,就像 SOAP 一样,只不过是通过简单的 HTTP URL 而不是 SOAP 的大量 XML 命名空间来触发。

恰好相反,REST 与 RPC 几乎没有任何关系。RPC 是面向服务的,并关注于行为和动作;而 REST 是面向资源的,强调描述应用程序的事物和名词。

为了理解 REST 是什么,我们将它的首字母缩写拆分为不同的组成部分。

表述性(Representational): REST 资源实际上可以用各种形式来进行表述,包括 XML、JSON(JavaScript Object Notation)甚至 HTML。

状态(State):当使用 REST 的时候,我们更关注资源的状态而不是对资源采取的行为。

转移(Transfer):REST 涉及转移资源数据,它以某一种表述性形式从一个应用转移到了一个应用。

更简洁地讲,REST 就是将资源的状态以最合适的形式从服务器端转移到客户端(或者反之)。

11.1.2 Spring 是如何支持 REST 的

Spring 很早就有导出 REST 资源的需求。Spring 3 对 Spring MVC 的一些增强功能为 REST 提供了良好的支持。现在,Spring 支持以下方式来开发 REST 资源。

控制器可以处理所有的 HTTP 方法,包含4个主要的 REST 方法:GET、PUT、 DELETE 以及 POST。

新的 @PathVariable 注解使得控制器能够处理参数化的 URL(将变量输入作为 URL 的一部分)

Spring 的表单绑定 JSP 标签库的
<form:form>
标签以及新的 HiddenHttpMethodFilter,使得通过 HTML 表单提交 PUT 和 DELETE 请求称为可能,即使在某些浏览器中不支持这些 HTTP 方法。

通过使用 Spring 的视图和视图解析器,资源可以以各种形式进行表说,包括将模型数据表现为 XML、JSON、Atom 和 RSS 的新视图实现。

可以使用新的 ContentNegotiatingViewResolver 来选择最适合客户端的表述。

基于视图的渲染可以使用新的 @ResponseBody 注解和各种 HttpMethodConverter 实现来达到。

类似地,新的 @ResponseBody 注解以及 HttpMethodConverter 实现可以将传入的 HTTP 数据传入控制器处理方法的 Java 对象。

RestTemplate 简化了客户端对 REST 资源的使用。

11.2 编写面向资源的控制器

编写 Spring MVC 控制器类的模型是相当灵活的。但是这种灵活性的副作用就是 Spring MVC 允许你开发出不符合 RESTful 资源的控制器。编写出的控制器很容易是 RESTless 的。

11.2.1 剖析 RESTless 的控制器

以下是一个 RESTless 控制器,DisplaySpittleController 的编写方式并没有严重的错误。但是,它并不是一个 RESTful 的控制器。它是面向行为的并关注于一个特殊的用例:以 HTML 的形式展现一个 Spittle 对象的详细信息。就连控制器的类名都说明了这一点。

/**
* 这是一个 RESTless 的控制器,是错误的演示例子
*/
@Controller
@RequestMapping("/displaySpittle.htm")
public class DisplaySpittleController {
private final SpitterService spitterService;

@Inject
public DisplaySpittleController(SpitterService spitterService){
this.spitterService = spitterService;
}

@RequestMapping(method=RequestMethod.GET)
public String showSpittle(@RequestParam("username") String username,Model model){
model.addAttribute(spitterService.getSpitter(username));
return "spittles/view";
}
}


11.2.2 处理 RESTful URL

URL 是统一资源定位符(Uniform Resource Locator)的缩写。按照这个名字,URL 本意是用于定位资源的。此外,所有的 URL 同时也都是 URI 或 统一资源标识符(Uniform Resource Identifier)。如果这样的话,我们可以认为任何给定的 URL 不仅可以定位一个资源还可以用于标识一个资源。

11.2.1 中的控制器处理的 URL 是 http://localost:8080/Spitter/displaySpittle.htm?username=123,这个 URL 并没有定位或标识资源。它要求服务器展现一个 Spittle。URL 中唯一的标识就是 id 查询参数。 URL 的基础部分是面向动作的,这就是说它是一个 RESTless 的 URL。

11.2.2.1 RESTful URL 的特点

不同于 RESTful URL,RESTful URL 完全承认 HTTP 用于标识资源的。例如下图展示了我们应该如何重构 RESTless URL 使其更加面向资源。



这个 URL 不仅定位资源,还可以唯一标识这个资源—-它不仅是 URL 也是 URI。这里使用完整的基本 URL 来标识资源,而不是使用查询参数标识资源。

实际上,新的 URL 根本没有查询参数。尽管使用查询参数往服务器发送信息仍然是一种合法的方式,但是这应当用于为服务器创建资源提供指导。查询参数不应该用于帮助标识资源。

有关 RESTful URL 还有最后一个关注点:它们是有层级的。如果从左到右读,你会经历从抽象到具体的过程。在我们的示例中,URL 有多个层级,每层都可以用于标识一个资源。

http://localhost:8080 标识了域和端口。

http://localhost:8080/Spitter 标识应用程序的 Servlet 上下文。

http://localhost:8080/Spitter/spittles 表明了一种资源,也就是 Spitter 应用程序中 Spittle 的对象列表。

http://localhost:8080/Spitter/spittles/123 是最精确的 URL,标识了一个特定的 Spittle 资源。

有趣的是,RESTful URL 的路径是参数化的。RESTless URL 使用查询参数作为输入,而 RESTful URL 的输入是 URL 路径的一部分。为了处理这种类型的URL,我们需要一种能够从 URL 路径中获取输入的方式来编写控制器处理方法。

11.2.2.2 在 URL 中嵌入参数

为了使用参数化的 URL 路径,Spring 3 引入了新的 @PathVariable 注解。

/**
* 这是标准的 RESTful 控制器示例
*/
@Controller
@RequestMapping("/spittles")
public class SpittleController {
private SpitterService spitterService;

@Inject
public SpittleController(SpitterService spitterService){
this.spitterService = spitterService;
}

//使用路径中的占位符变量
@RequestMapping(value="/{username}",method=RequestMethod.GET)
public String getSpittle(@PathVariable("username") String username,Model model){
model.addAttribute(spitterService.getSpitter(username));
return "spittles/view";
}
}


不管你是不是通过名字来明确指定路径变量,@PathVariable 可以让你编写控制器方法来处理标识资源的 URL 而不是描述某些行为的 URL。RESTful 请求的另一方面就是用于 URL 的 HTTP 方法。

11.2.3 执行 REST 动作

REST 是关于资源状态转移的。因此,我们需要一些动作(verb)来应用于这些资源—-转移资源状态的动作。对于任意给定的资源,最常见的操作是在服务器上对资源进行创建、检索、更新和删除。

我们关心的动作(post、get、put 以及 delete)直接对应于 HTTP 规范定义的4个方法。

方法描述是否安全是否幂等
GET从服务器上检索资源数据,资源通过请求的URL来进行标识
POST传送(POST)数据到服务器上,数据会由监听该请求URL的处理器来进行处理
PUT按照请求的URL,放置(Put)资源数据到服务器上
DELETE将请求URL标识的资源从服务器上删除(DELETE)
OPTIONS请求与服务器通信可用的选项
HEAD类似于GET,但只会返回头部信息–在响应体中不应该包含内容
TRACE将请求体的内容返回给客户
每个HTTP方法具有两个特性:安全性和幂等性。如果一个方法不改变资源的状态,就认为它是安全的。幂等的方法可能改变也可能不改变状态,但是一次请求和多次请求具有相同的作用。按照定义,所有安全的方法都必须是幂等的,但并不是所有幂等的方法都是安全的。

表中描述的4个HTTP方法通常会匹配到 CRUD(创建、读取、更新、删除)操作。GET 方法执行读取操作,而 DELETE 方法执行删除操作。尽管 PUT 和 POST 方法不仅仅能够用于更新和创建操作,但通常来讲它们就是应该这么使用的。

11.2.3.1 使用 PUT 更新资源

GET 请求将资源的状态从服务器转移到客户端,而PUT将资源的状态从客户端转移到服务器上。

@RequestMapping(value="/{id}",method=RequestMethod.PUT)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void putSpittle(@PathVariable("id") long id,@Valid Spittle spittle){
spitterService.saveSpittle(spittle);
}


上例中,putSpittle() 方法使用了 @ResponseStatus 注解定义了 HTTP 状态,这个状态要设置在发往客户端的响应中。上例中,HttpStatus.NO_CONTENT 说明响应状态要设置为 HTTP 状态码 204。这个状态码以为着请求被成功处理了,但是在响应体中不包含任何返回信息。

12.2.3.2 处理 DELETE 请求

除了简单地更新资源,我们可能还希望将其完全清理掉。当你不再需要某条资源的时候,这就是 HTTP 的DELETE 方法发挥作用的时候。

@RequestMapping(value="/{id}",method=RequestMethod.DELETE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteSpittle(@PathVariable("id") long id){
spitterService.deleteSpittle(id);
}


12.2.3.3 使用 POST 创建资源

@RequestMapping(method=RequestMethod.POST)
//用HTTP 201 进行响应
@ResponseStatus(HttpStatus.CREATED)
public @ResponseBody Spittle createSpittle(@Vaild Spittle spittle,
BindingResult result, HttpServletResponse response)
throws BindException{
if(result.hasErrors()){
throw new BindException(result);
}
spitterService.saveSpittle(spittle);
//设置资源位置
response.setHeader("Location","/spittles/"+spittle.getId());
//返回 Spittle 资源
return spittle;
}


createSpittle() 将处理 URL 模式匹配”/spittles”的请求。这个方法使用了 @ResponseStatus 注解来设置 HTTP 状态码。这次,状态码被设置成了 201(Created)来表明一个资源被成功创建了。当一个 HTTP 201 响应发送到客户端,新资源的 URL 也会一同发送回来。所以,createSpittle() 方法最后要做的事情之一就是设置 Location 头信息来包含资源的 URL。

尽管这不是 HTTP 201 响应的强制要求,但可以在响应体中返回完整的实体表述。所以,与前面 GET 的处理方法 getSpittle() 类似,这个方法最终返回新建的 Spittle 对象。这个对象会被转化为客户端可用的表述形式。

11.3 表述资源

表述是 REST 中很重要的一个方面。它是关于客户端和服务端针对某一资源是如何通信的。任何给定的资源都几乎可以用任意的形式来进行表述。如果资源的使用者希望使用 JSON,那么资源就可以用 JSON 格式来表述。如果使用者习惯使用尖括号,那相同的资源可以用 XML 来进行表述。同时,如果用户在浏览器中查看资源的话,可能更愿意以 HTML 的方式来展现。

需要了解的是控制器本身并不关心资源如何表述。控制器以 Java 对象的方式来处理资源。直到控制器完成了它的工作之后,资源才会被转化成最适合客户端的形式。

Spring 提供了两种方法将资源的 Java 表述形式转换为发送给客户端的表述形式

基于视图渲染进行协商;

HTTP 消息转换器。

鉴于我们在第七章中讨论过视图解析器,并且已经熟悉了基于视图的渲染(第七章),我们会直接查看如何使用内容协商来选择视图或视图解析器,它们将资源渲染为客户端能够接受的形式。

11.3.1 协商资源表述

回顾第七章,当控制器处理方法完成时,通常会返回一个逻辑视图名。如果方法不直接返回逻辑视图名,那么逻辑视图名会来源于请求的 URL。DispatcherServlet 接下来会将视图的名字传递给一个视图解析器,要求它来帮助确定应该用哪个视图来渲染请求结果。

Spring 的 ContentNegotiatingViewResolver 是一个特殊的视图解析器,它考虑到了客户端所需要的内容类型。

<!-- 将ContentNegotiatingViewResolver视图解析器配置在 Spring 上下文中 -->
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<!-- 首先检查URL扩展名与设置的key匹配,如果没有匹配值,则会查找请求的 Accept 头信息来确定媒体类型 -->
<property name="mediaTypes">
<entry key="json" value="application/json" />
<entry key="xml" value="text/xml" />
<entry key="htm" value="text/html" />
</property>
<!-- 如果请求中不包含 Accept 头信息,将使用 defaultContentType 所设置的类型 -->
<property name="defaultContentType" value="text/html" />
</bean>


要理解 ContentNegotiatingViewResolver 是如何工作的,要涉及内容协商的两个步骤。

确定请求的媒体类型。

找到适合请求媒体类型的最佳视图。

12.3.1.1 确定请求的媒体类型

ContentNegotiatingViewResolver 将考虑到 浏览器 Accept 头部 信息并使用它请求的媒体类型,但它会首先查看 URL 的文件扩展名。如果 URL 在结尾处有文件扩展名的话,它将扩展名与 mediaTypes 中的条目进行匹配。mediaTypes 是一个 Map,它的 key 是文件扩展名而 value 是媒体类型。如果找到了匹配项,那么将会使用找到的媒体类型。通过这种方式,文件扩展名将覆盖 Accept 头信息中的任何媒体类型。

12.3.1.2影响如何选择媒体类型

以上介绍中,我们展现了在确定请求媒体类型时的默认选择策略。但是有几个选择项可以影响到这个行为。

将 favorPathExtension 属性设置为 false,将会使得 ContentNegotiatingView Resoler 忽略 URL 路径的扩展名。

将 JAF(Java Activation Framework)添加到类路径下将会使得 ContentNegotiatingViewResolver 除了使用 mediaTypes 属性中的条目以外,在由路径扩展名确定媒体类型时还会借助 JAF。

如果你将 favorParameter 属性设置为 true,并且请求中包含名为 format 参数,那么 format 参数的值将与 mediaTypes 属性来进行匹配(另外,参数名可以通过设置 parameterName 属性来进行选择 )。

将 ignoreAcceptHeader 设置为 true ,将忽略 Accept 信息。

<!-- 将 favorParameter 属性设置为 true后,只要请求的 format 参数被设置为json,即使请求的URL中没有文件扩展名也能匹配 application/json 媒体类型。 -->
<property name="favorParameter" value="true" />


12.3.1.3 查找视图

不像其他视图解析器那样,ContentnegotiatingViewResolver 并不会直接解析视图,而是委托其他的视图解析器来查找最适合客户端的视图。如果没有特别指明的话,它将使用应用程序中的所有视图解析器。但可以通过设置 viewResolvers 属性明确声明它委托的视图解析器列表。

ContentnegotiatingViewResolver 将会使用所有的视图解析器来将逻辑视图名解析为视图。每个解析得到的视图都会存放在一个待选视图列表中。此外,如果在 defaultView 属性中指定了某个视图的话,那么这个视图将被添加到候选视图列表的尾部。

当候选视图列表组装完成之后,ContentnegotiatingViewResolver 将会循环所有请求的媒体类型,并在候选视图中查找能产生匹配内容类型的视图。找到的第一个匹配项就是要使用的视图。

最后,如果 ContentnegotiatingViewResolver 没有找到合适的视图,那么它将返回 null 视图。或者,如果 useNotAcceptableStatusCode 属性被设置为 true,那么将返回带有 HTTP 状态码 406(Not Acceptable)的视图。

通过内容协商来为客户端渲染资源表述的方式与我们在第七章中开发应用程序的 Web 前端的方式是吻合的。对于 Spring MVC Web 应用程序已有的 HTML 表述方式,这是在它上面添加其他表述方式的好办法。

当定义机器使用的 RESTful 资源时,另一种开发控制器的方式可能更有意义,这种控制器产生的数据将会作为资源被其他的应用程序所使用。这就是 Spring 的 HTTP 消息转换器 和 @ResponseBody 注解发挥作用的地方了。

11.3.2 使用 HTTP 信息转换器

典型的 Spring MVC 控制器方法在结束时会将一些信息放在模型中,然后到达一个视图来为用户渲染这些数据。尽管有多种方式来填充数据和识别视图,但是到目前为止我们看到的控制器遵循的都是这种基本模式。

但是,当控制器的工作是产生资源表述的时候,有一种更直接的方法可以绕过模型和数据。在这种风格的 处理器方法中,控制器返回的对象将自动转化为适合客户端的表述形式。

要使用这项新的技术,首先要将 @ResponseBody 注解添加到控制器处理方法上。

11.3.2.1 在响应体中返回资源状态

如果在方法中使用了 @ResponseBody ,那表明 HTTP 信息转换器机制会发挥作用,并将返回的对象转换为客户端要的任意格式。

//示例
@RequestMapping(value="/{username}",method=Request.GET,headers={"Accept=text/xml,application/json"})
public @ResponseBody Spitter getSpitter(@PathVariable String username){
return spitterService.getSpitter(username);
}


@ResponseBody 注解会告知 Spring,我们要将返回的对象作为资源发送给客户端,并将其转换为客户端可接受的表述形式。更具体地讲,资源的格式需要满足请求中 Accept 头信息的要求。如果请求中没有包含 Accept 头部信息的话,那它就假设客户端能够接受任意的表述形式。

对于 Accept 头部信息,请注意 getSpitter() 的 @RequestMapping 注解。headers 属性表明这个方法只处理 Accept 头部信息为 text/xml 或 application/json 的请求。其他任何类型的请求,即使它的 URL 匹配指定的路径并且是 GET 请求也不会被这个方法处理。这样的请求会被其他的方法进行处理(如果存在适当方法的话),或者返回客户端 HTTP 406 (Not Acceptable)响应。

Spring HTTP 信息转换器的工作就是,将处理方法返回的 Java 对象转换为满足客户端要求的表述形式。Spring 自带了各种各样的转换器,这些转换器满足了最常见的将对象转换为表述的需要。

信息转换器描述
AtomFeedHttpMessageConverterRome Feed 对象和 Atom feed(媒体类型 application/atom+xml)之间的相互转换。如果 Rome 包在类路径下将会进行注册
BufferedImageHttpMessageConverterBufferedImages 与图片二进制数据之间互相转换
ByteArrayHttpMessageConverter读取/写入字节数组。从所有媒体类型(*/*)中读取,并以application/octet-steam格式写入。默认注册
FormHttpMessageConverter将application/x-www-form-urlencoded内容读入到
MultiValueMap<String,String>
中,也会将
MultiValueMap<String,String>
写入到application/x-www-form-urlencoded中或将
MultiValueMap<String,Object>
写入到multipart/porm-data
Jaxb2RootElementHttpMessageConverter在XML(text/xml或application/xml)和使用JAXB2注解的对象间互相读取和写入。如果JAXB v2库在类路径下,将进行注册
MappingJacksonHttpMessageConverter在JSON和类型化的对象或非类型化的HashMap间互相读取和写入。如果Jackson JSON库在类路径下,将进行注册
MarshallingHttpMessageConverter使用注入的marshaller和unmarshaller来读入和写入XML。支持的marshaller和unmarshaller包括Castor、JAXB2、JIBX、XML Beans 以及XStream
ResourceHttpMessageConverter读取或写入Resource,默认注册
RssChannelHttpMessageConverter在RSS feed和RomeChannel对象间相互读取或写入。如果Rome库在类路径下,将进行注册
SourceHttpMessageConverter在XML和javax.xml.transform.Source对象间互相读取和写入。默认注册
StringHttpMessageConverter将所有媒体类型(*/*)读取为String。将String写入为text/plain。默认注册
XmlAwareFormHttpMessageConverterFormHttpMessageConverter的扩展,使用SourceHttpMessageConverter来支持基于XML的部分。默认注册
例如,假设客户端通过请求的Accept头信息表明它能接受application/json,并且Jackson JSON在类路径下,那么处理方法返回的对象将交给Mapping-JacksonHttpMessageConverter,并由其转换为返回客户端的JSON表述形式。另一个方面,如果请求的头信息表明客户端想要text/xml格式,那么Jaxb2RootElementHttpMessageConverter将会为客户端产生XML响应。

11.3.2.2 在请求体中接收资源状态

在RESTful会话的另一端,客户端可能会以JSON、XML或其他内容格式给我们发送一个对象过来。如果需要控制器的处理方法以原始形式来接受数据并自行进行转换的话,这是很不方便的。幸好,就像@ResponseBody注解能够将发送给客户端的数据进行转换一样,@RequestBody也能够对客户端发过来的对象做相同的事情。

假设客户端提交了一个PUT请求,在请求体中包含了JSON格式表述的Spitter对象数据。为了以Spitter对象来接受信息,只需要在处理方法的Spitter参数上使用@RequestBody注解:

@RequestMapping(value="/{username}",method=RequestMethod.PUT,
headers="Content-Type=application/json")
//通过@ResponseStatus注解设定该方法返回给客户端的HTTP响应为204状态码(即表示无内容)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updateSpitter(@PathVariable String username,
@RequestBody Spitter spitter){
spitterService.saveSpitter(spitter);
}


当请求到达时,Spring MVC发现updateSpitter()能够处理这个请求。但是抵达的信息是JSON格式,而这个方法要求的是Spitter对象。在这种情况下,会选择MappingJacksonHttpMessageConverter来将JSON信息转换为Spitter对象。为了做到这一点,需要满足如下条件:

请求的Content-Type头信息必须是application/json;

Jackson JSON库必须包含在应用程序的类路径下。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: