微信公众号:Java周刊
欢迎关注、一起学习、一起进步!
最近更新:2018-04-04
创建第一个REST站点
表述是REST中很重要的一个方面。它是关于客户端和服务器端针对某一资源是如何通信的。任何给定的资源都几乎可以用任意的形式来进行表述。
如果资源的使用者愿意使用JSON,那么资源就可以用JSON格式来表述。如果使用者喜欢尖括号,那相同的资源可以用XML来进行表述。同时,如果用户在浏览器中查看资源的话,可能更愿意以HTML的方式来展现(或者PDF、Excel及其他便于人类阅读的格式)。资源没有变化,只是它的表述方式变化了。
尽管Spring支持多种资源表述形式,但是在定义REST API的时候,不一定要全部使用它们。对于大多数客户端来说,用JSON和XML来进行表述就足够了。当然,如果内容要由人类用户来使用的话,那么我们可能需要支持HTML格式的资源。根据资源的特点和应用的需求,我们还可能选择使用PDF文档或Excel表格来展现资源。
对于非人类用户的使用者,比如其他的应用或调用REST端点的代码,资源表述的首选应该是XML和JSON。借助Spring同时支持这两种方案非常简单,所以没有必要做一个非此即彼的选择。
需要了解的是控制器本身通常并不关心资源如何表述。控制器以Java对象的方式来处理资源。控制器完成了它的工作之后,资源才会被转化成最适合客户端的形式。
Spring提供了两种方法将资源的Java表述形式转换为发送给客户端的表述形式:
1.内容协商(Content negotiation):选择一个视图,它能够将模型渲染为呈现给客户端的表述形式。
2.消息转换器(Message conversion):通过一个消息转换器将控制器所返回的对象转换为呈现给客户端的表述形式。
1. 协商资源表述
当控制器的处理方法完成时,通常会返回一个逻辑视图名。如果方法不直接返回逻辑视图名(例如方法返回void),那么逻辑视图名会根据请求的URL判断得出。DispatcherServlet接下来会将视图的名字传递给一个视图解析器,要求它来帮助确定应该用哪个视图来渲染请求结果。
在面向人类访问的Web应用程序中,选择的视图通常来讲都会渲染为HTML。视图解析方案是个简单的一维活动。如果根据视图名匹配上了视图,那这就是我们要用的视图了。
当要将视图名解析为能够产生资源表述的视图时,我们就有另外一个维度需要考虑了。视图不仅要匹配视图名,而且所选择的视图要适合客户端。如果客户端想要JSON,那么渲染HTML的视图就不行了,尽管视图名可能匹配。
Spring的ContentNegotiatingViewResolver是一个特殊的视图解析器,它考虑到了客户端所需要的内容类型。按照其最简单的形式,ContentNegotiatingViewResolver可以按照下述形式进行配置:
@Bean
public ViewResolver cnViewResolver(){
return new ContentNegotiatingViewResolver();
}
在这个简单的bean声明背后会涉及到很多事情。要理解ContentNegotiatingViewResolver是如何工作的,这涉及内容协商的两个步骤:
1.确定请求的媒体类型;
2.找到适合请求媒体类型的最佳视图。
1.1 确定请求的媒体类型
在内容协商两步骤中,第一步是确定客户端想要什么类型的内容表述。表面上看,这似乎是一个很简单的事情。难道请求的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会循环客户端请求的所有媒体类型,在候选的视图中查找能够产生对应内容类型的视图。第一个匹配的视图会用来渲染模型。
1.2 影响媒体类型的选择
在上述的选择过程中,我们阐述了确定所请求媒体类型的默认策略。但是通过为其设置一个ContentNegotiationManager,我们能够改变它的行为。
借助ContentNegotiationManager我们所能做到的事情如下所示:
1.指定默认的内容类型,如果根据请求无法得到内容类型的话,将会使用默认值;
2.通过请求参数指定内容类型;
3.忽视请求的Accept头部信息;
4.将请求的扩展名映射为特定的媒体类型;
5.将JAF(Java Activation Framework)作为根据扩展名查找媒体类型的备用方案。
有三种配置ContentNegotiationManager的方法:
1.直接声明一个ContentNegotiationManager类型的bean;
2.通过ContentNegotiationManagerFactoryBean间接创建bean;
3.重载WebMvcConfigurerAdapter的configureContentNegotiation()方法。
直接创建ContentNegotiationManager有一些复杂,除非有充分的原因,否则我们不会愿意这样做。后两种方案能够让创建ContentNegotiationManager更加简单。
ContentNegotiationManager是Spring中相对比较新的功能,是在Spring 3.2中引入的。在Spring 3.2之前,ContentNegotiatingViewResolver的很多行为都是通过直接设置ContentNegotiatingViewResolver的属性进行配置的。从Spring3.2开始,ContentNegotiatingViewResolver的大多数Setter方法都废弃了,鼓励通过ContentNegotiationManager来进行配置。
一般而言,如果我们使用XML配置ContentNegotiationManager的话,那最有用的将会是ContentNegotiationManagerFactoryBean。例如,我们可能希望在XML中配置ContentNegotiationManager使用“application/json”作为默认的内容类型:
<bean id="contentNegotiationManager"
class="org.springframework.http.ContentNegotiationManagerFactoryBean"
p:defaultContentType="application/josn"/>
因为ContentNegotiationManagerFactoryBean是FactoryBean的实现,所以它会创建一个ContentNegotiationManager bean。这个ContentNegotiationManager能够注入到ContentNegotiatingViewResolver的contentNegotiationManager属性中。
如果使用Java配置的话,获得ContentNegotiationManager的最简便方法就是扩展WebMvcConfigurerAdapter并重载configureContentNegotiation()方法。
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer){
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
我们可以看到,configureContentNegotiation()方法给定了一个ContentNegotiationConfigurer对象。
ContentNegotiationConfigurer中的一些方法对应于ContentNegotiationManager的Setter方法,这样我们就能在ContentNegotiationManager创建时,设置任意内容协商相关的属性。在本例中,我们调用defaultContentType()方法将默认的内容类型设置为“application/json”。
现在,我们已经有了ContentNegotiationManager bean,接下来就需要将它注入到ContentNegotiatingViewResolver的contentNegotiationManager属性中。这需要我们稍微修改一下之前声明ContentNegotiatingViewResolver的@Bean方法:
@Bean
public ViewResolver cnViewResolver(
ContentNegotiationManager cnm){
ContentNegotiatingViewResolver cnvr =
new ContentNegotiatingViewResolver();
cnvr.setContentNegotiatingViewResolver();
return cnvr;
}
这个@Bean方法注入了ContentNegotiationManager,并使用它调用了setContentNegotiationManager()。这样的结果就是ContentNegotiatingViewResolver将会使用ContentNegotiationManager所定义的行为。
1.3 ContentNegotiatingViewResolver的优势与限制
ContentNegotiatingViewResolver最大的优势在于,它在Spring MVC之上构建了REST资源表述层,控制器代码无需修改。相同的一套控制器方法能够为面向人类的用户产生HTML内容,也能针对不是人类的客户端产生JSON或XML。
如果面向人类用户的接口与面向非人类客户端的接口之间有很多重叠的话,那么内容协商是一种很便利的方案。在实践中,面向人类用户的视图与REST API在细节上很少能够处于相同的级别。
如果面向人类用户的接口与面向非人类客户端的接口之间没有太多重叠的话,那么ContentNegotiatingViewResolver的优势就体现不出来了。
ContentNegotiatingViewResolver还有一个严重的限制。作为ViewResolver的实现,它只能决定资源该如何渲染到客户端,并没有涉及到客户端要发送什么样的表述给控制器使用。如果客户端发送JSON或XML的话,那么ContentNegotiatingViewResolver就无法提供帮助了。
ContentNegotiatingViewResolver还有一个相关的小问题,所选中的View会渲染模型给客户端,而不是资源。这里有个细微但很重要的区别。当客户端请求JSON格式的User对象列表时,客户端希望得到的响应可能如下所示:
[
{
"id":1,
"message":"hello world"
},
{
"id":2,
"message":"stay hurry"
}
]
而模型是key-value组成的Map,那么响应可能会如下所示:
{
"resultList":[
{
"id":1,
"message":"hello world"
},
{
"id":2,
"message":"stay hurry"
}
]
}
尽管这不是很严重的问题,但确实可能不是客户端所预期的结果。因为有这些限制,通常建议不要使用ContentNegotiatingViewResolver。通常更加倾向于使用Spring的消息转换功能来生成资源表述。
2.使用HTTP信息转换器
消息转换(message conversion)提供了一种更为直接的方式,它能够将控制器产生的数据转换为服务于客户端的表述形式。
当使用消息转换功能时,DispatcherServlet不再需要那么麻烦地将模型数据传送到视图中。实际上,这里根本就没有模型,也没有视图,只有控制器产生的数据,以及消息转换器(message converter)转换数据之后所产生的资源表述。
Spring自带了各种各样的转换器,如下表所示,这些转换器满足了最常见的将对象转换为表述的需要。
因为格式原因,消息转换器表格见微信公众号。
注意,表中的HTTP信息转换器除了其中的五个以外都是自动注册的,所以要使用它们的话,不需要Spring配置。
但是为了支持它们,你需要添加一些库到应用程序的类路径下。例如,如果你想使用MappingJacksonHttpMessageConverter来实现JSON消息和Java对象的互相转换,那么需要将Jackson JSON Processor库添加到类路径中。类似地,如果你想使用Jaxb2RootElementHttpMessageConverter来实现XML消息和Java对象的互相转换,那么需要JAXB库。如果信息是Atom或RSS格式的话,那么AtomFeedHttpMessageConverter和RssChannelHttpMessageConverter会需要Rome库。
你可能已经猜到了,为了支持消息转换,我们需要对Spring MVC的编程模型进行一些小调整。
2.1 在响应体中返回资源状态
正常情况下,当处理方法返回Java对象(除String外或View的实现以外)时,这个对象会放在模型中并在视图中渲染使用。但是,如果使用了消息转换功能的话,我们需要告诉Spring跳过正常的模型/视图流程,并使用消息转换器。有不少方式都能做到这一点,但是最简单的方法是为控制器方法添加@ResponseBody注解。
例如查询列表数据的方法:
@ResponseBody
@RequestMapping(method=RequestMethod.GET,produces="application/josn")
public List<User> listAll(){
return userService.listAll();
}
@ResponseBody注解会告知Spring,我们要将返回的对象作为资源发送给客户端,并将其转换为客户端可接受的表述形式。更具体地讲,DispatcherServlet将会考虑到请求中Accept头部信息,并查找能够为客户端提供所需表述形式的消息转换器。
举例来讲,假设客户端的Accept头部信息表明它接受“application/json”,并且Jackson JSON库位于应用的类路径下,那么将会选择MappingJacksonHttpMessageConverter或MappingJackson2HttpMessageConverter(这取决于类路径下是哪个版本的Jackson)。消息转换器会将控制器返回的Spittle列表转换为JSON文档,并将其写入到响应体中。响应大致会如下所示:
[
{
"id":1,
"message":"hello world"
},
{
"id":2,
"message":"stay hurry"
}
]
2.2 Jackson默认会使用反射
注意在默认情况下,Jackson JSON库在将返回的对象转换为JSON资源表述时,会使用反射。对于简单的表述内容来讲,这没有什么问题。但是如果你重构了Java类型,比如添加、移除或重命名属性,那么所产生的JSON也将会发生变化(如果客户端依赖这些属性的话,那客户端有可能会出错)。
但是,我们可以在Java类型上使用Jackson的映射注解,从而改变产生JSON的行为。这样我们就能更多地控制所产生的JSON,从而防止它影响到API或客户端。
请注意这里的@RequestMapping注解。在这里,我使用了produces属性表明这个方法只处理预期输出为JSON的请求。也就是说,这个方法只会处理Accept头部信息包含“application/json”的请求。其他任何类型的请求,即使它的URL匹配指定的路径并且是GET请求也不会被这个方法处理。这样的请求会被其他的方法来进行处理(如果存在适当方法的话),或者返回客户端HTTP 406(Not Acceptable)响应。
2.3 在请求体中接收资源状态
到目前为止,我们只关注了REST端点如何为客户端提供资源。但是REST并不是只读的,REST API也可以接受来自客户端的资源表述。
@ResponseBody能够告诉Spring在把数据发送给客户端的时候,要使用某一个消息器,与之类似,@RequestBody也能告诉Spring查找一个消息转换器,将来自客户端的资源表述转换为对象。
例如一个保存用户信息的方法:
@RequestMapping(value="/user/save",method=RequestMethod.POST,produces="application/josn")
public void saveUser(@RequestBody User user){
userService.saveUser(user);
}
因为参数上使用了@RequestBody,所以Spring将会查看请求中的Content-Type头部信息,并查找能够将请求体转换为User的消息转换器。例如,如果客户端发送的User数据是JSON表述形式,那么Content-Type头部信息可能就会是“application/json”。在这种情况下,DispatcherServlet会查找能够将JSON转换为Java对象的消息转换器。如果Jackson 2库在类路径中,那么MappingJackson2HttpMessageConverter将会担此重任,将JSON表述转换为User,然后传递到saveUser()方法中。
注意,@RequestMapping有一个consumes属性,我们将其设置为“application/json”。consumes属性的工作方式类似于produces,不过它会关注请求的Content-Type头部信息。它会告诉Spring这个方法只会处理对“/user/save”的POST请求,并且要求请求的Content-Type头部信息为“application/json”。如果无法满足这些条件的话,会由其他方法(如果存在合适的方法的话)来处理请求。
2.4 为控制器默认设置消息转换
当处理请求时,@ResponseBody和@RequestBody是启用消息转换的一种简洁和强大方式。但是,如果你所编写的控制器有多个方法,并且每个方法都需要信息转换功能的话,那么这些注解就会带来一定程度的重复性。Spring 4.0引入了@RestController注解,能够在这个方面给我们提供帮助。如果在控制器类上使用@RestController来代替@Controller的话,Spring将会为该控制器的所有处理方法应用消息转换功能。我们不必为每个方法都添加@ResponseBody了。
参考资料:1.Spring In Action(FOURTH EDITION)
喜欢本文,欢迎关注《Java周刊》