领先一步
VMware 提供培训和认证,助你加速进步。
了解更多使用 Spring MVC 生成输出有两种方式
@ResponseBody
方式和 HTTP 消息转换器,通常用于返回 JSON 或 XML 等数据格式。编程客户端、移动应用和启用 AJAX 的浏览器是常见的客户端。无论哪种情况,你需要处理由控制器返回的同一数据的多种表示形式(或视图)。确定要返回哪种数据格式称为 内容协商。
有三种情况下我们需要知道 HTTP 响应中要发送哪种数据格式
确定用户请求的格式依赖于 ContentNegotationStrategy
。开箱即用提供了默认实现,但你也可以根据需要实现自己的策略。
在这篇文章中,我想讨论如何配置和使用 Spring 进行内容协商,主要针对使用 HTTP 消息转换器的 RESTful Controller。在后来的 文章中,我将展示如何使用 Spring 的 ContentNegotiatingViewResolver
专门针对视图设置内容协商。
[caption id="attachment_13288" align="alignleft" width="200" caption="获取正确的内容"][/caption]
通过 HTTP 发送请求时,可以通过设置 Accept
头属性来指定你想要的响应类型。Web 浏览器预设为请求 HTML(以及其他内容)。事实上,如果你仔细查看,你会发现浏览器发送的 Accept 头非常混乱,这使得依赖它们变得不切实际。有关此问题的精彩讨论,请参阅 http://www.gethifi.com/blog/browser-rest-http-accept-headers。总结来说:Accept
头很混乱,并且你通常也无法更改它们(除非你使用 JavaScript 和 AJAX)。
因此,对于那些 Accept
头属性不适用或不希望使用的情况,Spring 提供了一些替代的约定来使用。(这是 Spring 3.2 中一项很好的改变,它使得灵活的内容选择策略不仅在使用视图时可用,而且在整个 Spring MVC 中都可用。) 你可以集中配置一次内容协商策略,它将应用于任何需要确定不同格式(媒体类型)的地方。
Spring 支持几种用于选择所需格式的约定:URL 后缀和/或 URL 参数。这些与使用 Accept
头一起工作。因此,可以通过以下三种方式之一请求内容类型。默认情况下,它们按此顺序检查:
http://myserver/myapp/accounts/list.html
之类的形式,则需要 HTML。对于电子表格,URL 应为 http://myserver/myapp/accounts/list.xls
。后缀到媒体类型的映射是通过 JavaBeans Activation Framework 或 JAF 自动定义的(因此 activation.jar
必须在类路径上)。http://myserver/myapp/accounts/list?format=xls
。参数名默认为 format
,但这可以更改。默认情况下禁用使用参数,但启用后,它会进行第二次检查。Accept
HTTP 头属性。这是 HTTP 实际定义的工作方式,但是,如前所述,使用它可能会有问题。设置此功能的 Java 配置如下所示。只需通过其配置器自定义预定义的内容协商管理器。请注意,MediaType
帮助类为大多数已知媒体类型提供了预定义常量。
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* Setup a simple strategy: use all the defaults and return XML by default when not sure.
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_XML);
}
}
使用 XML 配置时,内容协商策略最容易通过 ContentNegotiationManagerFactoryBean
进行设置
<!--
Setup a simple strategy:
1. Take all the defaults.
2. Return XML by default when not sure.
-->
<bean id="contentNegotiationManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="defaultContentType" value="application/xml" />
</bean>
<!-- Make this available across all of Spring MVC -->
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />
通过任一设置创建的 ContentNegotiationManager
是 ContentNegotationStrategy
的一个实现,该实现遵循上述的 PPA 策略(路径扩展,然后是参数,然后是 Accept 头)。
在 Java 配置中,可以通过配置器上的方法完全自定义策略
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* Total customization - see below for explanation.
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false).
favorParameter(true).
parameterName("mediaType").
ignoreAcceptHeader(true).
useJaf(false).
defaultContentType(MediaType.APPLICATION_JSON).
mediaType("xml", MediaType.APPLICATION_XML).
mediaType("json", MediaType.APPLICATION_JSON);
}
}
在 XML 中,可以通过工厂 Bean 上的方法配置策略
<!-- Total customization - see below for explanation. -->
<bean id="contentNegotiationManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="favorPathExtension" value="false" />
<property name="favorParameter" value="true" />
<property name="parameterName" value="mediaType" />
<property name="ignoreAcceptHeader" value="true"/>
<property name="useJaf" value="false"/>
<property name="defaultContentType" value="application/json" />
<property name="mediaTypes">
<map>
<entry key="json" value="application/json" />
<entry key="xml" value="application/xml" />
</map>
</property>
</bean>
我们在两种情况下所做的
format
,我们将改用 mediaType
。Accept
头。如果你的大多数客户端实际上是 Web 浏览器(通常通过 AJAX 进行 REST 调用),这通常是最好的方法。为了演示,我构建了一个简单的账户列表应用程序作为我们的示例——截图显示了 HTML 格式的典型账户列表。完整代码可以在 Github 上找到:https://github.com/paulc4/mvc-content-neg。
要以 JSON 或 XML 格式返回账户列表,我需要一个这样的 Controller。我们将暂时忽略生成 HTML 的方法。
@Controller
class AccountController {
@RequestMapping(value="/accounts", method=RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List<Account> list(Model model, Principal principal) {
return accountManager.getAccounts(principal) );
}
// Other methods ...
}
这是内容协商策略的设置
<!-- Simple strategy: only path extension is taken into account -->
<bean id="cnManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="favorPathExtension" value="true"/>
<property name="ignoreAcceptHeader" value="true" />
<property name="defaultContentType" value="text/html" />
<property name="useJaf" value="false"/>
<property name="mediaTypes">
<map>
<entry key="html" value="text/html" />
<entry key="json" value="application/json" />
<entry key="xml" value="application/xml" />
</map>
</property>
</bean>
或者,使用 Java 配置,代码如下所示
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) {
// Simple strategy: only path extension is taken into account
configurer.favorPathExtension(true).
ignoreAcceptHeader(true).
useJaf(false).
defaultContentType(MediaType.TEXT_HTML).
mediaType("html", MediaType.TEXT_HTML).
mediaType("xml", MediaType.APPLICATION_XML).
mediaType("json", MediaType.APPLICATION_JSON);
}
假设我的类路径上有 JAXB2 和 Jackson,Spring MVC 将自动设置必要的 HttpMessageConverters
。我的领域类也必须使用 JAXB2 和 Jackson 注解进行标记才能启用转换(否则消息转换器不知道该怎么做)。为了回应评论(见下文),带有注解的 Account
类显示在下方。
这是我们的 Accounts 应用程序的 JSON 输出(注意 URL 中的路径扩展)。
系统如何知道是转换为 XML 还是 JSON?这是因为内容协商——根据 ContentNegotiationManager
的配置方式,将使用上面讨论的三种(PPA 策略)选项中的任何一种。在这种情况下,URL 以 accounts.json
结尾,因为路径扩展是唯一启用的策略。
在示例代码中,你可以通过在 web.xml
中设置活动 profile 来在 MVC 的 XML 或 Java 配置之间切换。profile 分别是 “xml” 和 “javaconfig”。
Spring MVC 的 REST 支持基于现有的 MVC Controller 框架。因此,同一个 Web 应用程序可以同时以原始数据(如 JSON)和使用演示格式(如 HTML)返回信息。
这两种技术可以很容易地在同一个 Controller 中并行使用,如下所示
@Controller
class AccountController {
// RESTful method
@RequestMapping(value="/accounts", produces={"application/xml", "application/json"})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List<Account> listWithMarshalling(Principal principal) {
return accountManager.getAccounts(principal);
}
// View-based method
@RequestMapping("/accounts")
public String listWithView(Model model, Principal principal) {
// Call RESTful method to avoid repeating account lookup logic
model.addAttribute( listWithMarshalling(principal) );
// Return the view to use for rendering the response
return ¨accounts/list¨;
}
}
这里有一个简单的模式:@ResponseBody
方法处理所有数据访问以及与底层服务层的集成(即 AccountManager
)。第二个方法调用第一个方法,并在 Model 中设置响应供 View 使用。这避免了重复的逻辑。
为了确定选择两个 @RequestMapping
方法中的哪一个,我们再次使用了我们的 PPA 内容协商策略。它使得 produces
选项能够工作。以 accounts.xml
或 accounts.json
结尾的 URL 映射到第一个方法,以 accounts.anything
结尾的任何其他 URL 映射到第二个方法。
ContentNegotiatingViewResolver
发挥作用的地方,那将是我下一篇文章的主题。我想感谢 Rossen Stoyanchev 在撰写本文方面给予的帮助。任何错误均由我本人负责。
添加于 2013年6月2日.
由于有一些关于如何为 JAXB 注解类的问题,这里是 Account
类的一部分。为简洁起见,我省略了数据成员以及除带注解的 getter 方法之外的所有方法。如果愿意,我可以直接注解数据成员(实际上就像 JPA 注解一样)。请记住,Jackson 可以使用这些相同的注解将对象编组为 JSON。
/**
* Represents an account for a member of a financial institution. An account has
* zero or more {@link Transaction}s and belongs to a {@link Customer}. An aggregate entity.
*/
@Entity
@Table(name = "T_ACCOUNT")
@XmlRootElement
public class Account {
// data-members omitted ...
public Account(Customer owner, String number, String type) {
this.owner = owner;
this.number = number;
this.type = type;
}
/**
* Returns the number used to uniquely identify this account.
*/
@XmlAttribute
public String getNumber() {
return number;
}
/**
* Get the account type.
*
* @return One of "CREDIT", "SAVINGS", "CHECK".
*/
@XmlAttribute
public String getType() {
return type;
}
/**
* Get the credit-card, if any, associated with this account.
*
* @return The credit-card number or null if there isn't one.
*/
@XmlAttribute
public String getCreditCardNumber() {
return StringUtils.hasText(creditCardNumber) ? creditCardNumber : null;
}
/**
* Get the balance of this account in local currency.
*
* @return Current account balance.
*/
@XmlAttribute
public MonetaryAmount getBalance() {
return balance;
}
/**
* Returns a single account transaction. Callers should not attempt to hold
* on or modify the returned object. This method should only be used
* transitively; for example, called to facilitate reporting or testing.
*
* @param name
* the name of the transaction account e.g "Fred Smith"
* @return the beneficiary object
*/
@XmlElement // Make these a nested <transactions> element
public Set<Transaction> getTransactions() {
return transactions;
}
// Setters and other methods ...
}