返回介绍

16.2.1 协商资源表述

发布于 2024-08-17 00:45:49 字数 6698 浏览 0 评论 0 收藏 0

你可以回忆一下在第5章中(以及图5.1所示),当控制器的处理方法完成时,通常会返回一个逻辑视图名。如果方法不直接返回逻辑视图名(例如方法返回void),那么逻辑视图名会根据请求的URL判断得出。DispatcherServlet接下来会将视图的名字传递给一个视图解析器,要求它来帮助确定应该用哪个视图来渲染请求结果。

在面向人类访问的Web应用程序中,选择的视图通常来讲都会渲染为HTML。视图解析方案是个简单的一维活动。如果根据视图名匹配上了视图,那这就是我们要用的视图了。

当要将视图名解析为能够产生资源表述的视图时,我们就有另外一个维度需要考虑了。视图不仅要匹配视图名,而且所选择的视图要适合客户端。如果客户端想要JSON,那么渲染HTML的视图就不行了——尽管视图名可能匹配。

Spring的ContentNegotiatingViewResolver是一个特殊的视图解析器,它考虑到了客户端所需要的内容类型。按照其最简单的形式,ContentNegotiatingViewResolver可以按照下述形式进行配置:

在这个简单的bean声明背后会涉及到很多事情。要理解ContentNegotiating-ViewResolver是如何工作的,这涉及内容协商的两个步骤:

1.确定请求的媒体类型;

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

让我们深入了解每个步骤来了解ContentNegotiatingViewResolver是如何完成其任务的,首先从弄明白客户端需要什么类型的内容开始。

确定请求的媒体类型

在内容协商两步骤中,第一步是确定客户端想要什么类型的内容表述。表面上看,这似乎是一个很简单的事情。难道请求的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我们所能做到的事情如下所示:

指定默认的内容类型,如果根据请求无法得到内容类型的话,将会使用默认值;

通过请求参数指定内容类型;

忽视请求的Accept头部信息;

将请求的扩展名映射为特定的媒体类型;

将JAF(Java Activation Framework)作为根据扩展名查找媒体类型的备用方案。

有三种配置ContentNegotiationManager的方法:

直接声明一个ContentNegotiationManager类型的bean;

通过ContentNegotiationManagerFactoryBean间接创建bean;

重载WebMvcConfigurerAdapter的configureContentNegotiation()方法。

直接创建ContentNegotiationManager有一些复杂,除非有充分的原因,否则我们不会愿意这样做。后两种方案能够让创建ContentNegotiationManager更加简单。

ContentNegotiationManager是在Spring 3.2中加入的

ContentNegotiationManager是Spring中相对比较新的功能,是在Spring 3.2中引入的。在Spring 3.2之前,ContentNegotiatingViewResolver的很多行为都是通过直接设置ContentNegotiatingViewResolver的属性进行配置的。从Spring 3.2开始,Content-NegotiatingViewResolver的大多数Setter方法都废弃了,鼓励通过Content-NegotiationManager来进行配置。

尽管我不会在本章中介绍配置ContentNegotiatingViewResolver的旧方法,但是我们在创建ContentNegotiationManager所设置的很多属性,在ContentNegotiatingViewResolver中都有对应的属性。如果你使用较早版本的Spring的话,应该能够很容易地将新的配置方式对应到旧配置方式中。

一般而言,如果我们使用XML配置ContentNegotiationManager的话,那最有用的将会是ContentNegotiationManagerFactoryBean。例如,我们可能希望在XML中配置ContentNegotiationManager使用“application/json”作为默认的内容类型:

因为ContentNegotiationManagerFactoryBean是FactoryBean的实现,所以它会创建一个ContentNegotiationManager bean。这个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”。

现在,我们已经有了ContentNegotiationManagerbean,接下来就需要将它注入到ContentNegotiatingViewResolver的contentNegotiationManager属性中。这需要我们稍微修改一下之前声明ContentNegotiatingViewResolver的@Bean方法:

这个@Bean方法注入了ContentNegotiationManager,并使用它调用了setContentNegotiationManager()。这样的结果就是ContentNegotiatingView、Resolver将会使用ContentNegotiationManager所定义的行为。

配置ContentNegotiationManager有很多的细节,在这里无法对它们进行一一介绍。如下的程序清单是一个非常简单的配置样例,当我使用ContentNegotiating-ViewResolver的时候,通常会采用这种用法:它默认会使用HTML视图,但是对特定的视图名称将会渲染为JSON输出。

程序清单16.2 配置ContentNegotiationManager

除了程序清单16.2中的内容以外,还应该有一个能够处理HTML的视图解析器(如InternalResourceViewResolver或TilesViewResolver)。在大多数场景下,ContentNegotiatingViewResolver会假设客户端需要HTML,如ContentNegotiationManager配置所示。但是,如果客户端指定了它想要JSON(通过在请求路径上使用“.json”扩展名或Accept头部信息)的话,那么ContentNegotiatingViewResolver将会查找能够处理JSON视图的视图解析器。

如果逻辑视图的名称为“spittles”,那么我们所配置的BeanNameViewResolver将会解析spittles()方法中所声明的View。这是因为bean名称匹配逻辑视图的名称。如果没有匹配的View的话,ContentNegotiatingViewResolver将会采用默认的行为,将其输出为HTML。

ContentNegotiatingViewResolver一旦能够确定客户端想要什么样的媒体类型,接下来就是查找渲染这种内容的视图。

ContentNegotiatingViewResolver的优势与限制

ContentNegotiatingViewResolver最大的优势在于,它在Spring MVC之上构建了REST资源表述层,控制器代码无需修改。相同的一套控制器方法能够为面向人类的用户产生HTML内容,也能针对不是人类的客户端产生JSON或XML。

如果面向人类用户的接口与面向非人类客户端的接口之间有很多重叠的话,那么内容协商是一种很便利的方案。在实践中,面向人类用户的视图与REST API在细节上很少能够处于相同的级别。如果面向人类用户的接口与面向非人类客户端的接口之间没有太多重叠的话,那么ContentNegotiatingViewResolver的优势就体现不出来了。

ContentNegotiatingViewResolver还有一个严重的限制。作为ViewResolver的实现,它只能决定资源该如何渲染到客户端,并没有涉及到客户端要发送什么样的表述给控制器使用。如果客户端发送JSON或XML的话,那么ContentNegotiatingViewResolver就无法提供帮助了。

ContentNegotiatingViewResolver还有一个相关的小问题,所选中的View会渲染模型给客户端,而不是资源。这里有个细微但很重要的区别。当客户端请求JSON格式的Spittle对象列表时,客户端希望得到的响应可能如下所示:

而模型是key-value组成的Map,那么响应可能会如下所示:

尽管这不是很严重的问题,但确实可能不是客户端所预期的结果。

因为有这些限制,我通常建议不要使用ContentNegotiatingViewResolver。我更加倾向于使用Spring的消息转换功能来生成资源表述。接下来,我们看一下如何在控制器代码中使用Spring的消息转换器。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文