1.REST的基础知识
当谈论REST时,有一种常见的错误就是将其视为“基于URL的Web服务”——将REST作为另一种类型的远程过程调用(remote procedurecall,RPC)机制,就像SOAP一样,只不过是通过简单的HTTP URL来触发,而不是使用SOAP大量的XML命名空间。恰好相反,REST与RPC几乎没有任何关系。RPC是面向服务的,并关注于行为和动作;而REST是面向资源的,强调描述应用程序的事物和名词。为了理解REST是什么,我们将它的首字母缩写拆分为不同的构成部分:
表述性(Representational):REST资源实际上可以用各种形式来进行表述,包括XML、JSON(JavaScript Object Notation)甚至HTML——最适合资源使用者的任意形式;
状态(State):当使用REST的时候,我们更关注资源的状态而不是对资源采取的行为;
转移(Transfer):REST涉及到转移资源数据,它以某种表述性形式从一个应用转移到另一个应用。更简洁地讲,REST就是将资源的状态以最适合客户端或服务端的形式从服务器端转移到客户端(或者反过来)。在REST中,资源通过URL进行识别和定位。至于RESTful URL的结构并没有严格的规则,但是URL应该能够识别资源,而不是简单的发一条命令到服务器上。再次强调,关注的核心是事物,而不是行为
REST中会有行为,它们是通过HTTP方法来定义的。具体来讲,也就是GET、POST、PUT、DELETE、PATCH以及其他的HTTP方法构成了
REST中的动作。这些HTTP方法通常会匹配为如下的CRUD动作:
Create:POSTRead:GET
Update:PUT或PATCH
Delete:DELETE
尽管通常来讲,HTTP方法会映射为CRUD动作,但这并不是严格的限制。有时候,PUT可以用来创建新资源,POST可以用来更新资源。实际上,POST请求非幂等性(non-idempotent)的特点使其成为一个非常灵活的方法,对于无法适应其他HTTP方法语义的操作,它都能够胜任。
2.Spring是如何支持REST的
当前的4.0版本中,Spring支持以下方式来创建REST资源:
控制器可以处理所有的HTTP方法,包含四个主要的REST方法:GET、PUT、DELETE以及POST。Spring 3.2及以上版本还支持PATCH方法;
借助@PathVariable注解,控制器能够处理参数化的URL(将变量输入作为URL的一部分);借助Spring的视图和视图解析器,资源能够以多种方式进行表述,包括将模型数据渲染为XML、JSON、Atom以及RSS的View实现;可以使用ContentNegotiatingViewResolver来选择最适合客户端的表述;借助@ResponseBody注解和各种HttpMethodConverter实现,能够替换基于视图的渲染方式;类似地,@RequestBody注解以及HttpMethodConverter实现可以将传入的HTTP数据转化为传入控制器处理方法的Java对象;借助RestTemplate,Spring应用能够方便地使用REST资源。
3.创建第一个REST端点
首先,我们会在名为SpittleApiController的新控制器中创建第一个REST端点。如下的程序清单展现了这个新REST控制器起始的样子,它会提供Spittle资源。这是一个很简单的开始,但是在本章中,随着不断学习Spring REST编程模型的细节,我们将会不断构建这个控制器
对于非人类用户的使用者,比如其他的应用或调用REST端点的代码,资源表述的首选应该是XML和JSON。借助Spring同时支持这两种方案非常简单,所以没有必要做一个非此即彼的选择。按照我的意见,我推荐至少要支持JSON。JSON使用起来至少会像XML一样简单(很多人会说JSON会更加简单),并且如果客户端是JavaScript(最近一段时间以来,这种做法越来越常见)的话,JSON更是会成为优胜者,因为在JavaScript中使用JSON数据根本就不需要编排和解排(marshaling/demarshaling)
需要了解的是控制器本身通常并不关心资源如何表述。控制器以Java对象的方式来处理资源。控制器完成了它的工作之后,资源才会被转化成最适合客户端的形式。Spring提供了两种方法将资源的Java表述形式转换为发送给客户端的表述形式:
内容协商(Content negotiation):选择一个视图,它能够将模型
渲染为呈现给客户端的表述形式;
消息转换器(Message conversion):通过一个消息转换器将控
制器所返回的对象转换为呈现给客户端的表述形式。
协商资源表述
你可以回忆一下在第5章中(以及图5.1所示),当控制器的处理方法完成时,通常会返回一个逻辑视图名。如果方法不直接返回逻辑视图名(例如方法返回void),那么逻辑视图名会根据请求的URL判断得出。DispatcherServlet接下来会将视图的名字传递给一个视图解析器,要求它来帮助确定应该用哪个视图来渲染请求结果。在面向人类访问的Web应用程序中,选择的视图通常来讲都会渲染为HTML。视图解析方案是个简单的一维活动。如果根据视图名匹配上了视图,那这就是我们要用的视图了。当要将视图名解析为能够产生资源表述的视图时,我们就有另外一个维度需要考虑了。视图不仅要匹配视图名,而且所选择的视图要适合客户端。如果客户端想要JSON,那么渲染HTML的视图就不行了——尽管视图名可能匹配。
Spring的ContentNegotiatingViewResolver是一个特殊的视图解析器,它考虑到了客户端所需要的内容类型。按照其最简单的形式ContentNegotiatingViewResolver可以按照下述形式进行配置:
在这个简单的bean声明背后会涉及到很多事情。要理解ContentNegotiating-ViewResolver是如何工作的,这涉及内容协商的两个步骤
1.确定请求的媒体类型;
2.找到适合请求媒体类型的最佳视图。
确定请求的媒体类型
在内容协商两步骤中,第一步是确定客户端想要什么类型的内容表述。表面上看,这似乎是一个很简单的事情。难道请求的Accept头部信息不是已经很清楚地表明要发送什么样的表述给客户端吗?遗憾的是,Accept头部信息并不总是可靠的。如果客户端是Web浏览器,那并不能保证客户端需要的类型就是浏览器在Accept头部所发送的值。Web浏览器一般只接受对人类用户友好的内容类型(如text/html),所以没有办法(除了面向开发人员的浏览器插件)指定不同的内容类型。
ContentNegotiatingViewResolver将会考虑到Accept头部信息并使用它所请求的媒体类型,但是它会首先查看URL的文件扩展名。如果URL在结尾处有文件扩展名的话,ContentNegotiatingViewResolver将会基于该扩展名确定所需的类型。如果扩展名是“.json”的话,那么所需的内容类型必须是“application/json”。如果扩展名是“.xml”,那么客户端请求的就是“application/xml”。当然,“.html”扩展名表明客户端所需的资源表述为HTML(text/html)。如果根据文件扩展名不能得到任何媒体类型的话,那就会考虑请求中的Accept头部信息。在这种情况下,Accept头部信息中的值就表明了客户端想要的MIME类型,没有必要再去查找了。最后,如果没有Accept头部信息,并且扩展名也无法提供帮助的话,ContentNegotiatingViewResolver将会使用“/”作为默认的内容类型,这就意味着客户端必须要接收服务器发送的任何形式的表述。一旦内容类型确定之后,ContentNegotiatingViewResolver就该将逻辑视图名解析为渲染模型的View。
与Spring的其他视图解析器不同,ContentNegotiatingViewResolver本身不会解析视图。而是委托给其他的视图解析器,让它们来解析视图。ContentNegotiatingViewResolver要求其他的视图解析器将逻辑视图名解析为视图。解析得到的每个视图都会放到一个列表中。这个列表装配完成后,ContentNegotiatingViewResolver会循环客户端请求的所有媒体类型,在候选的视图中查找能够产生对应内容类型的视图。第一个匹配的视图会用来渲染模型。
影响媒体类型的选择
在上述的选择过程中,我们阐述了确定所请求媒体类型的默认策略。但是通过为其设置一个ContentNegotiationManager,我们能够改变它的行为。借助Content-NegotiationManager我们所能做到的事情如下所示:
1.指定默认的内容类型,如果根据请求无法得到内容类型的话,将会使用默认值;
2.通过请求参数指定内容类型;
3.忽视请求的Accept头部信息;
4.将请求的扩展名映射为特定的媒体类型;
5.将JAF(Java Activation Framework)作为根据扩展名查找媒体类
型的备用方案
有三种配置ContentNegotiationManager的方法:
直接声明一个ContentNegotiationManager类型的bean;
通过ContentNegotiationManagerFactoryBean间接创建bean;
重载WebMvcConfigurerAdapter的configureContentNegotiation()方法。
直接创建ContentNegotiationManager有一些复杂,除非有充分的原因,否则我们不会愿意这样做。后两种方案能够让创建ContentNegotiationManager更加简单。
一般而言,如果我们使用XML配置ContentNegotiationManager的话,那最有用的将会是ContentNegotiationManagerFactoryBean。例如,我们可能希望在XML中配置ContentNegotiationManager使
用“application/json”作为默认的内容类型:
因为ContentNegotiationManagerFactoryBean是FactoryBean的实现,所以它会创建一个ContentNegotiationManagerbean。这个ContentNegotiationManager能够注入到ContentNegotiatingViewResolver的contentNegotiationManager属性中。
如果使用Java配置的话,获得ContentNegotiationManager的最简便方法就是扩展WebMvcConfigurerAdapter并重载configureContentNegotiation()方法。在创建Spring MVC应用的时候,我们很可能已经扩展了WebMvcConfigurerAdapter。例如,在Spittr应用中,我们已经有了WebMvcConfigurerAdapter的扩展类,名为WebConfig,所以需要做的就是重载configureContentNegotiation()方法。如下就是configureContentNegotiation()的一个实现,它设置了默认的内容类型:
我们可以看到,configureContentNegotiation()方法给定了一个Content-NegotiationConfigurer对象。ContentNegotiationConfigurer中的一些方法对应于ContentNegotiationManager的Setter方法,这样我们就能在ContentNegotiation-Manager创建时,设置任意内容协商相关的属性。在本例中,我们调用defaultContentType()方法将默认的内容类型设置为“application/json”。
配置ContentNegotiationManager有很多的细节,在这里无法对它们进行一一介绍。如下的程序清单是一个非常简单的配置样例,当我使用ContentNegotiating-ViewResolver的时候,通常会采用这种用法:它默认会使用HTML视图,但是对特定的视图名称将会渲染为JSON输出。
除了程序清单16.2中的内容以外,还应该有一个能够处理HTML的视图解析器(如InternalResourceViewResolver或TilesViewResolver)。在大多数场景下,ContentNegotiatingViewResolver会假设客户端需要HTML,如ContentNegotiationManager配置所示。但是,如果客户端指定了它想要JSON(通过在请求路径上使用“.json”扩展名或Accept头部信息)的话,那么ContentNegotiatingViewResolver将会查找能够处理JSON视图的视图解析器。如果逻辑视图的名称为“spittles”,那么我们所配置的BeanNameViewResolver将会解析spittles()方法中所声明的View。这是因为bean名称匹配逻辑视图的名称。如果没有匹配的View的话ContentNegotiatingViewResolver将会采用默认的行为,将其输出为HTML。ContentNegotiatingViewResolver一旦能够确定客户端想要什么样的媒体类型,接下来就是查找渲染这种内容的视图
5.使用HTTP信息转换器
消息转换(message conversion)提供了一种更为直接的方式,它能够将控制器产生的数据转换为服务于客户端的表述形式。当使用消息转换功能时,DispatcherServlet不再需要那么麻烦地将模型数据传送到视图中。实际上,这里根本就没有模型,也没有视图,只有控制器产生的数据,以及消息转换器(message converter)转换数据之后所产生的资源表述。
例如,假设客户端通过请求的Accept头信息表明它能接受“application/json”,并且Jackson JSON在类路径下,那么处理方法返回的对象将交给MappingJacksonHttp-MessageConverter,并由它转换为返回客户端的JSON表述形式。另一方面,如果请求的头信息表明客户端想要“text/xml”格式,那么Jaxb2RootElementHttpMessage-Converter将会为客户端产生XML响应。
注意,表16.2中的HTTP信息转换器除了其中的五个以外都是自动注册的,所以要使用它们的话,不需要Spring配置。但是为了支持它们,你需要添加一些库到应用程序的类路径下。例如,如果你想使用MappingJacksonHttpMessageConverter来实现JSON消息和Java对象的互相转换,那么需要将Jackson JSON Processor库添加到类路径中。类似地,如果你想使用Jaxb2RootElementHttpMessageConverter来实现XML消息和Java对象的互相转换,那么需要JAXB库。如果信息是Atom或RSS格式的话,那么Atom-FeedHttpMessageConverter和RssChannelHttpMessageConverter会需要Rome库
你可能已经猜到了,为了支持消息转换,我们需要对Spring MVC的编程模型进行一些小调整。在响应体中返回资源状态正常情况下,当处理方法返回Java对象(除String外或View的实现以外)时,这个对象会放在模型中并在视图中渲染使用。但是,如果使用了消息转换功能的话,我们需要告诉Spring跳过正常的模型/视图流程,并使用消息转换器。有不少方式都能做到这一点,但是最简单的方法是为控制器方法添加@ResponseBody注解。
在请求体中接收资源状态
到目前为止,我们只关注了REST端点如何为客户端提供资源。但是
REST并不是只读的,REST API也可以接受来自客户端的资源表述。
如果要让控制器将客户端发送的JSON和XML转换为它所使用的Java
对象,那是非常不方便的。在处理逻辑离开控制器的时候,Spring的
消息转换器能够将对象转换为表述——它们能不能在表述传入的时候
完成相同的任务呢?
@ResponseBody能够告诉Spring在把数据发送给客户端的时候,要
使用某一个消息器,与之类似,@RequestBody也能告诉Spring查找
一个消息转换器,将来自客户端的资源表述转换为对象。例如,假设
我们需要一种方式将客户端提交的新Spittle保存起来。我们可以
按照如下的方式编写控制器方法来处理这种请求
@ResponseBody注解会告知Spring,我们要将返回的对象作为资源发送给客户端,并将其转换为客户端可接受的表述形式。更具体地讲,DispatcherServlet将会考虑到请求中Accept头部信息,并查找能够为客户端提供所需表述形式的消息转换器。举例来讲,假设客户端的Accept头部信息表明它接受“application/json”,并且Jackson JSON库位于应用的类路径下,那么将会选择MappingJacksonHttpMessage-Converter或MappingJackson2HttpMessageConverter(这取决于类路径下是哪个版本的Jackson)。消息转换器会将控制器返回的Spittle列表转换为JSON文档,并将其写入到响应体中。响应大致会如下所示
在请求体中接收资源状态到目前为止,我们只关注了REST端点如何为客户端提供资源。但是REST并不是只读的,REST API也可以接受来自客户端的资源表述。如果要让控制器将客户端发送的JSON和XML转换为它所使用的Java对象,那是非常不方便的。在处理逻辑离开控制器的时候,Spring的消息转换器能够将对象转换为表述——它们能不能在表述传入的时候完成相同的任务呢?
@ResponseBody能够告诉Spring在把数据发送给客户端的时候,要使用某一个消息器,与之类似,@RequestBody也能告诉Spring查找一个消息转换器,将来自客户端的资源表述转换为对象。例如,假设我们需要一种方式将客户端提交的新Spittle保存起来。我们可以按照如下的方式编写控制器方法来处理这种请求
@RequestBody,所以Spring将会查看请求中的Content-Type头部信息,并查找能够将请求体转换为Spittle的消息转换器。例如,如果客户端发送的Spittle数据是JSON表述形式,那么Content-Type头部信息可能就会是“application/json”。在这种情况下,DispatcherServlet会查找能够将JSON转换为Java对象的消息转换器。如果Jackson 2库在类路径中,那么MappingJackson2HttpMessageConverter将会担此重任,将JSON表述转换为Spittle,然后传递到saveSpittle()方法中。这个方法还使用了@ResponseBody注解,因此方法返回的Spittle对象将会转换为某种资源表述,发送给客户端。注意,@RequestMapping有一个consumes属性,我们将其设置为“application/ json”。consumes属性的工作方式类似于produces,不过它会关注请求的Content-Type头部信息。它会告诉Spring这个方法只会处理对“/spittles”的POST请求,并且要求请求的Content-Type头部信息为“application/json”。如果无法满足这些条件的话,会由其他方法(如果存在合适的方法的话)来处理请求。
为控制器默认设置消息转换
当处理请求时,@ResponseBody和@RequestBody是启用消息转换的一种简洁和强大方式。但是,如果你所编写的控制器有多个方法,并且每个方法都需要信息转换功能的话,那么这些注解就会带来一定程度的重复性。Spring 4.0引入了@RestController注解,能够在这个方面给我们提供帮助。如果在控制器类上使用@RestController来代替@Controller的话,Spring将会为该控制器的所有处理方法应用消息转换功能。我们不必为每个方法都添加@ResponseBody了。我们所定义的SpittleController可能就会如下所示:
我们看到了如何使用Spring MVC编程模型将RESTful资源发布到响应体之中。但是响应除了负载以外还会有其他的内容。头部信息和状态码也能够为客户端提供响应的有用信息。接下来,我们
看一下在提供资源的时候,如何填充头部信息和设置状态码