领先一步
VMware 提供培训和认证,助您加速进步。
了解更多在我之前的文章中,我介绍了内容协商的概念以及 Spring MVC 用于确定所请求内容的三个策略。
在这篇文章中,我想专门扩展这个概念,以使用 ContentNegotiatingViewResolver (或 CNVR) 支持不同内容类型的多个视图。
由于我们已经知道如何从之前的文章中设置内容协商,因此使用它在多个视图之间进行选择非常简单。只需像这样定义一个 CNVR
<!--
// View resolver that delegates to other view resolvers based on the
// content type
-->
<bean class="org.springframework.web.servlet.view.
ContentNegotiatingViewResolver">
<!-- All configuration now done by manager - since Spring V3.2 -->
<property name="contentNegotiationManager" ref="cnManager"/>
</bean>
<!--
// Setup a simple strategy:
// 1. Only path extension is taken into account, Accept headers
// are ignored.
// 2. Return HTML by default when not sure.
-->
<bean id="cnManager" class="org.springframework.web.accept.
ContentNegotiationManagerFactoryBean">
<property name="ignoreAcceptHeader" value="true"/>
<property name="defaultContentType" value="text/html" />
</bean>
对于每个请求,一个 @Controller 通常会返回一个 逻辑视图名称(或者 Spring MVC 会根据传入的 URL 约定来确定一个)。CNVR 将会查询配置中定义的所有其他视图解析器,以查看 1) 它是否有一个具有正确名称的视图,以及 2) 它是否有一个也能生成正确内容的视图——所有视图都“知道”它们返回什么内容类型。所需的内容类型以与上一篇文章中讨论的完全相同的方式确定。
有关等效的 Java 配置,请参阅此处。有关扩展配置,请参阅此处。Github 上有一个演示应用程序:https://github.com/paulc4/mvc-content-neg-views。
对于那些赶时间的人来说,这就是它的全部内容。
对于其他人,这篇文章展示了我们是如何做到这一点的。它讨论了 Spring MVC 中多视图的概念,并在此基础上定义了 CNVR 是什么、如何使用它以及它是如何工作的。它使用了与上一篇文章相同的 Accounts 应用程序,并将其构建为以 HTML、电子表格、JSON 和 XML 格式返回账户信息。所有这些都 只 使用视图。
MVC 模式的优点之一是能够为相同的数据拥有多个视图。在 Spring MVC 中,我们通过“内容协商”来实现这一点。我的上一篇文章总体讨论了内容协商,并展示了使用 HTTP 消息转换器的 RESTful 控制器的示例。但内容协商也可以与视图一起使用。
例如,假设我希望不仅将账户信息显示为网页,还希望将其作为电子表格提供。我可以为每个使用不同的 URL,在我的 Spring 控制器上放置两个方法,并让每个方法返回正确的视图类型。(顺便说一下,如果您不确定 Spring 如何创建电子表格,我稍后会向您展示)。
@Controller
class AccountController {
@RequestMapping("/accounts.htm")
public String listAsHtml(Model model, Principal principal) {
// Duplicated logic
model.addAttribute( accountManager.getAccounts(principal) );
return ¨accounts/list¨; // View determined by view-resolution
}
@RequestMapping("/accounts.xls")
public AccountsExcelView listAsXls(Model model, Principal principal) {
// Duplicated logic
model.addAttribute( accountManager.getAccounts(principal) );
return new AccountsExcelView(); // Return view explicitly
}
}
使用多个方法很不优雅,违背了 MVC 模式,而且如果我还想支持其他数据格式,如 PDF、CSV 等,会变得更糟糕。如果您还记得在上一篇文章中,我们遇到了一个类似的问题,希望一个方法能够返回 JSON 或 XML(我们通过返回单个 @RequestBody 对象并选择正确的 HTTP 消息转换器来解决)。
[caption id="attachment_13458" align="alignleft" width="380" caption="通过内容协商选择正确的视图。"]
[/caption]
现在我们需要一个“智能”视图解析器,它能从 多个 可能的视图中选择 正确 的视图。
Spring MVC 长期以来一直支持多个视图解析器,并依次咨询它们以查找视图。尽管可以指定视图解析器被咨询的顺序,但 Spring MVC 总是选择 第一个 提供的视图。而“内容协商视图解析器”(CNVR)则在 所有 视图解析器之间进行协商,以找到与所需格式 最匹配 的视图——这 就是 我们的“智能”视图解析器。
这是一个简单的账户列表应用程序,我们将它作为示例,用视图来列出 HTML 格式、电子表格格式以及(稍后)JSON 和 XML 格式的账户。
完整的代码可在 Github 上找到:https://github.com/paulc4/mvc-content-neg-views。它是上次我展示的应用程序的一个变体,该应用程序 仅 使用视图生成输出。注意:为了使下面的示例简单,我直接使用了 JSP 和 InternalResourceViewResolver。Github 项目使用 Tiles 和 JSP,因为它比原始 JSP 更容易。
账户列表 HTML 页面的截图显示了当前登录用户的所有账户。您稍后会看到电子表格和 JSON 输出的截图。
生成我们页面的 Spring MVC 控制器如下。请注意,HTML 输出是由逻辑视图 accounts/list 生成的。
@Controller
class AccountController {
@RequestMapping("/accounts")
public String list(Model model, Principal principal) {
model.addAttribute( accountManager.getAccounts(principal) );
return ¨accounts/list¨;
}
}
为了显示两种类型的视图,我们需要两种类型的视图解析器——一种用于 HTML,一种用于电子表格(为了简单起见,我将使用 JSP 作为 HTML 视图)。下面是 Java 配置。
@Configuration
@EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter {
@Autowired
ServletContext servletContext;
// Will map to bean called "accounts/list" in "spreadsheet-views.xml"
@Bean(name="excelViewResolver")
public ViewResolver getXmlViewResolver() {
XmlViewResolver resolver = new XmlViewResolver();
resolver.setLocation(new ServletContextResource(servletContext,
"/WEB-INF/spring/spreadsheet-views.xml"));
resolver.setOrder(1);
return resolver;
}
// Will map to the JSP page: "WEB-INF/views/accounts/list.jsp"
@Bean(name="jspViewResolver")
public ViewResolver getJspViewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("WEB-INF/views");
resolver.setSuffix(".jsp");
resolver.setOrder(2);
return resolver;
}
}
或者用 XML
<!-- Maps to a bean called "accounts/list" in "spreadsheet-views.xml" -->
<bean class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="order" value="1"/>
<property name="location" value="WEB-INF/spring/spreadsheet-views.xml"/>
</bean>
<!-- Maps to "WEB-INF/views/accounts/list.jsp" -->
<bean class="org.springframework.web.servlet.view.
InternalResourceViewResolver">
<property name="order" value="2"/>
<property name="prefix" value="WEB-INF/views"/>
<property name="suffix" value=".jsp"/>
</bean>
在 WEB-INF/spring/spreadsheet-beans.xml 中,你会发现
<bean id="accounts/list" class="rewardsonline.accounts.AccountExcelView"/>
生成的电子表格看起来像这样
下面是如何使用视图创建电子表格(这是一个简化版本,完整实现要长得多,但你明白了)
class AccountExcelView extends AbstractExcelView {
@Override
protected void buildExcelDocument(Map<String, Object> model,
HSSFWorkbook workbook, HttpServletRequest request,
HttpServletResponse response) throws Exception {
List<Account> accounts = (List<Account>) model.get("accountList");
HSSFCellStyle dateStyle = workbook.createCellStyle();
dateStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat("m/d/yy"));
HSSFSheet sheet = workbook.createSheet();
for (short i = 0; i < accounts.size(); i++) {
Account account = accounts.get(i);
HSSFRow row = sheet.createRow(i);
addStringCell(row, 0, account.getName());
addStringCell(row, 1, account.getNumber());
addDateCell(row, 2, account.getDateOfBirth(), dateStyle);
}
}
private HSSFCell addStringCell(HSSFRow row, int index, String value) {
HSSFCell cell = row.createCell((short) index);
cell.setCellValue(new HSSFRichTextString(value));
return cell;
}
private HSSFCell addDateCell(HSSFRow row, int index, Date date,
HSSFCellStyle dateStyle) {
HSSFCell cell = row.createCell((short) index);
cell.setCellValue(date);
cell.setCellStyle(dateStyle);
return cell;
}
}
就目前而言,此设置将始终返回电子表格,因为 XmlViewResolver 会首先被咨询(其 order 属性为 1),并且它总是返回 AccountExcelView。InternalResourceViewResolver 从未被咨询(其 order 为 2,我们从未达到那一步)。
这就是 CNVR 发挥作用的地方。让我们快速回顾一下上一篇文章中讨论的内容选择策略。请求的内容类型按以下顺序确定:
http://...accounts.json 表示 JSON 格式。format,例如 http://...accounts?format=json。Accept 头部属性(这实际上是 HTTP 的工作方式,但并不总是方便使用——尤其是在客户端是浏览器时)。在前两种情况下,后缀或参数值(xml、json ...)必须映射到正确的 MIME 类型。可以使用 JavaBeans Activation Framework,也可以显式指定映射。对于 Accept 头部属性,其值 就是 MIME 类型。
这是一个特殊的视图解析器,我们的策略就内置其中。这是 Java 配置
@Configuration
@EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter {
/**
* Setup a simple strategy:
* 1. Only path extension taken into account, Accept headers ignored.
* 2. Return HTML by default when not sure.
*/
@Override
public void configureContentNegotiation
(ContentNegotiationConfigurer configurer) {
configurer.ignoreAcceptHeader(true)
.defaultContentType(MediaType.TEXT_HTML);
}
/**
* Create the CNVR. Get Spring to inject the ContentNegotiationManager
* created by the configurer (see previous method).
*/
@Bean
public ViewResolver contentNegotiatingViewResolver(
ContentNegotiationManager manager) {
ContentNegotiatingViewResolver resolver =
new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(manager);
return resolver;
}
}
或者用 XML
<!--
// View resolver that delegates to other view resolvers based on the
// content type
-->
<bean class="org.springframework.web.servlet.view.
ContentNegotiatingViewResolver">
<!-- All configuration now done by manager - since Spring V3.2 -->
<property name="contentNegotiationManager" ref="cnManager"/>
</bean>
<!--
// Setup a simple strategy:
// 1. Only path extension taken into account, Accept headers ignored.
// 2. Return HTML by default when not sure.
-->
<bean id="cnManager" class="org.springframework.web.accept.
ContentNegotiationManagerFactoryBean">
<property name="ignoreAcceptHeader" value="true"/>
<property name="defaultContentType" value="text/html" />
</bean>
ContentNegotiationManager 正是我在上一篇文章中讨论过的那个 bean。
CNVR 会自动遍历 Spring 定义的 所有 其他视图解析器 bean,并请求它们提供一个与控制器返回的视图名称(在本例中为 accounts/list)相对应的 View 实例。每个 View 都“知道”它能生成什么类型的内容,因为它上面有一个 getContentType() 方法(继承自 View 接口)。JSP 页面由 JstlView 渲染(由 InternalResourceViewResolver 返回),其内容类型为 text/html,而 AccountExcelView 生成 application/vnd.ms-excel。
CNVR 实际的配置方式委托给 ContentNegotiationManager,后者又通过配置器(Java Configuration)或 Spring 的众多工厂 bean 之一(XML)创建。
谜题的最后一块是:CNVR 如何知道请求的是哪种内容类型?因为内容协商策略告诉它怎么做:要么识别 URL 后缀,要么识别 URL 参数,要么识别 Accept 头部。这与上一篇文章中描述的策略设置完全相同,并被 CNVR 重复利用。
请注意,当 Spring 3.0 引入内容协商策略时,它们仅适用于选择视图。自 3.2 以来,此功能已全面可用(如我之前的文章所述)。本文中的示例使用 Spring 3.2,可能与您之前见过的旧示例有所不同。特别是,配置内容协商策略的大多数属性现在都位于ContentNegotiationManagerFactoryBean上,而不是ContentNegotiatingViewResolver上。CNVR 上的属性现在已被弃用,取而代之的是管理器上的属性,但 CNVR 本身的工作方式与以往完全相同。
默认情况下,CNVR 会自动检测 Spring 定义的所有 ViewResolvers 并在它们之间进行协商。如果您愿意,CNVR 本身有一个 viewResolvers 属性,您可以 明确地 告诉它使用哪些视图解析器。这使得 CNVR 是主解析器,而其他解析器是其从属解析器变得很明显。请注意,不再需要 order 属性。
@Configuration
@EnableWebMvc
public class MvcConfiguration extends WebMvcConfigurerAdapter {
// .. Other methods/declarations
/**
* Create the CNVR. Specify the view resolvers to use explicitly.
* Get Spring to inject the ContentNegotiationManager created by the
* configurer (see previous method).
*/
@Bean
public ViewResolver contentNegotiatingViewResolver(
ContentNegotiationManager manager) {
// Define the view resolvers
List<ViewResolver> resolvers = new ArrayList<ViewResolver>();
XmlViewResolver r1 = new XmlViewResolver();
resolver.setLocation(new ServletContextResource(servletContext,
"/WEB-INF/spring/spreadsheet-views.xml"));
resolvers.add(r1);
InternalResourceViewResolver r2 = new InternalResourceViewResolver();
r2.setPrefix("WEB-INF/views");
r2.setSuffix(".jsp");
resolvers.add(r2);
// Create CNVR plugging in the resolvers & content-negotiation manager
ContentNegotiatingViewResolver resolver =
new ContentNegotiatingViewResolver();
resolver.setViewResolvers(resolvers);
resolver.setContentNegotiationManager(manager);
return resolver;
}
}
或者用 XML
<bean class="org.springframework.web.servlet.view.
ContentNegotiatingViewResolver">
<property name="contentNegotiationManager" ref="cnManager"/>
<!-- Define the view resolvers explicitly -->
<property name="viewResolvers">
<list>
<bean class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="location" value="spreadsheet-views.xml"/>
</bean>
<bean class="org.springframework.web.servlet.view.
InternalResourceViewResolver">
<property name="prefix" value="WEB-INF/views"/>
<property name="suffix" value=".jsp"/>
</bean>
</list>
</property>
</bean>
Github 演示项目使用两组 Spring 配置文件。web.xml 中可以分别指定 xml 或 javaconfig 用于 XML 或 Java 配置。对于它们中的任何一个,都可以指定 separate 或 combined。separate 配置文件将所有视图解析器定义为顶级 bean,并让 CNVR 扫描上下文以查找它们(如上一节所述)。在 combined 配置文件中,视图解析器明确定义,而不是作为 Spring bean,并通过其 viewResolvers 属性传递给 CNVR(如本节所示)。
MappingJacksonJsonView,它支持使用 Jackson Object-to-JSON 映射库从 Java 对象生成 JSON 数据。MappingJacksonJsonView 会自动将模型中找到的所有属性转换为 JSON。唯一的例外是它会忽略 BindingResult 对象,因为这些对象是 Spring MVC 表单处理的内部对象,不需要。需要一个合适的视图解析器,而 Spring 没有提供。幸运的是,自己编写一个非常简单:
public class JsonViewResolver implements ViewResolver {
/**
* Get the view to use.
*
* @return Always returns an instance of {@link MappingJacksonJsonView}.
*/
@Override
public View resolveViewName(String viewName, Locale locale)
throws Exception {
MappingJacksonJsonView view = new MappingJacksonJsonView();
view.setPrettyPrint(true); // Lay JSON out to be nicely readable
return view;
}
}
简单地将此视图解析器声明为 Spring bean 意味着可以返回 JSON 格式数据。JAF 已经将 json 映射到 application/json,所以我们完成了。像 http://myserver/myapp/accounts/list.json 这样的 URL 现在可以返回 JSON 格式的账户信息。这是我们的 Accounts 应用程序的输出:
有关此视图的更多信息,请参阅 Spring Javadoc。
有一个类似的类用于生成 XML 输出——MarshallingView。它会处理模型中第一个可以被编组的对象。您可以通过告知它要选择哪个模型属性(键)来可选地配置视图——请参阅 setModelKey()。
我们再次需要一个视图解析器。Spring 通过 Spring 的 对象到 XML 编组 (OXM) 抽象支持多种编组技术。让我们只使用 JAXB2,因为它内置于 JDK 中(自 JDK 6 起)。这是解析器:
/**
* View resolver for returning XML in a view-based system.
*/
public class MarshallingXmlViewResolver implements ViewResolver {
private Marshaller marshaller;
@Autowired
public MarshallingXmlViewResolver(Marshaller marshaller) {
this.marshaller = marshaller;
}
/**
* Get the view to use.
*
* @return Always returns an instance of {@link MappingJacksonJsonView}.
*/
@Override
public View resolveViewName(String viewName, Locale locale)
throws Exception {
MarshallingView view = new MarshallingView();
view.setMarshaller(marshaller);
return view;
}
}
我的类仍需要注解才能与 JAXB 配合使用(针对评论,我已在我上一篇文章的末尾添加了一个示例)。
使用 Java 配置将新的解析器配置为 Spring bean
@Bean(name = "marshallingXmlViewResolver")
public ViewResolver getMarshallingXmlViewResolver() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
// Define the classes to be marshalled - these must have
// @Xml... annotations on them
marshaller.setClassesToBeBound(Account.class,
Transaction.class, Customer.class);
return new MarshallingXmlViewResolver(marshaller);
}
或者我们可以在 XML 中做同样的事情——注意 oxm 命名空间的使用
<oxm:jaxb2-marshaller id="marshaller" >
<oxm:class-to-be-bound name="rewardsonline.accounts.Account"/>
<oxm:class-to-be-bound name="rewardsonline.accounts.Customer"/>
<oxm:class-to-be-bound name="rewardsonline.accounts.Transaction"/>
</oxm:jaxb2-marshaller>
<!-- View resolver that returns an XML Marshalling view. -->
<bean class="rewardsonline.accounts.MarshallingXmlViewResolver" >
<constructor-arg ref="marshaller"/>
</bean>
这是我们完成的系统
使用 @ResponseBody、@ResponseStatus 和其他与 REST 相关的 MVC 注解,可以完全支持 MVC 的 RESTful 方法。就像这样:
@RequestMapping(value="/accounts",
produces={"application/json", "application/xml"})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List<Account> list(Principal principal) {
return accountManager.getAccounts(principal);
}
为了使我们的 @RequestMapping 方法也能进行相同的内容协商,我们必须重用我们的内容协商管理器(这使得 produces 选项能够工作)。
<mvc:annotation-driven
content-negotiation-manager="contentNegotiationManager" />
然而,这会产生一种不同风格的 Controller 方法,其优点是功能更强大。那么,该选择哪种方式:Views 还是 @ResponseBody?
对于已经使用 Spring MVC 和视图的现有网站,MappingJacksonJsonView 和 MarshallingView 提供了一种简单的方法来扩展 Web 应用程序,以同时返回 JSON 和/或 XML。在许多情况下,这些是您唯一需要的数据格式,并且是支持只读移动应用程序和/或启用 AJAX 的网页的简单方法,其中 RESTful 请求仅用于 获取 数据。
对 REST 的全面支持,包括修改数据的能力,涉及结合 HTTP 消息转换器使用带注解的控制器方法。在这种情况下,使用视图没有意义,只需返回一个 @ResponseBody 对象,让转换器完成工作。