Spring MVC有两种方式生成output的方法:
在这两种情况下,你都需要处理由控制器返回的相同数据的多个表示(或视图)。计算出要返回的数据格式称为内容协商(Content Negotiation)。
有三种情况下,我们需要知道在HTTP响应中发送哪种类型的数据格式:
确定用户请求的格式依赖于ContentNegotationStrategy。有默认的实现可用,但如果你愿意,也可以实现自己的实现。
在这篇文章中,我想讨论如何配置和使用Spring的内容协商,主要是使用HTTP消息转换器的RESTful控制器。在另一篇博文中,我将展示如何设置专门为使用内容协商使用Spring的ContentNegotiatingViewResolver视图。
通过HTTP发出请求时,可以通过设置Accept header来指定你想要的响应类型。Web浏览器有这个预设来请求HTML(包括其他东西)。事实上,如果你看的话,你会发现浏览器实际上会发送非常混乱的Accept标头,这使得依赖它们变得不切实际。请参阅http://www.gethifi.com/blog/browser-rest-http-accept-headers的一个很好的讨论这个问题。Accept-header是混乱的,你也不能正常地改变它们(除非你使用JavaScript和AJAX)。
因此,针对Accept-header这种不是令人满意的情况,Spring提供了一些可使用的约定。(这是Spring 3.2的一个很好的变化,它使一个灵活的内容选择策略在所有Spring MVC中都可以使用,而不仅仅是在使用视图的时候)。你可以集中地配置一次内容协商策略,并在需要确定不同格式(媒体类型)的地方应用它。
Spring支持两种约定来选择所需格式:URL后缀或者URL参数。这些工作与使用Accept header一起工作。因此,可以以三种方式请求内容类型。默认情况下,按以下顺序进行检查:
设置这个的Java配置,看起来是这样的。只需通过它的配置器定制预定义的内容协商管理器。注意,MediaType助手类为大多数著名的media-types提供了预定义的常量。
@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设置:
<bean id="contentNegotiationManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="defaultContentType" value="application/xml" />
bean>
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />
ContentNegotiationManage通过设置一个实现了ContentNegotationStrategy来实现上面提到的PPA策略 (path extension, then parameter, then Accept header)。
在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上的方法来配置策略:
<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>
我们在这两种情况下都做了什么:
为了演示,我将一个简单的帐户清单应用程序作为我们的工作示例—屏幕截图显示了HTML中一个典型的帐户列表。完整的代码可以在Github上找到。
要返回JSON或XML的帐户列表,我需要这样的控制器。现在,我们将忽略HTML生成方法。
@Controller
class AccountController {
@RequestMapping(value="/accounts", method=RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List list(Model model, Principal principal) {
return accountManager.getAccounts(principal) );
}
// Other methods ...
}
下面是内容协商策略的设置:
<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将自动设置必要的httpmessageconverter。我的domain class还必须使用JAXB2和Jackson注释来进行转换(否则消息转换器不知道该做什么)。为了响应注释(下面),注释的Account类如下所示。
/**
* 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 element
public Set getTransactions() {
return transactions;
}
// Setters and other methods ...
}
下面是我们的帐户应用程序的JSON输出(请注意URL中的路径扩展)。
系统如何知道要转换为XML还是JSON?因为内容协商(PPA策略)的任何一个选项将使用上面讨论取决于ContentNegotiationManager是如何配置的。在本例中,URL以帐户结束。json因为路径扩展是唯一启用的策略。
在示例代码中,您可以通过在web.XML中设置一个活动概要文件,在XML或Java配置之间切换。这些概要文件分别是“xml”和“javaconfig”。
Spring MVC的REST支持构建在现有的MVC控制器框架之上。因此,可以使用相同的web应用程序返回信息,既包括原始数据(如JSON),也可以使用表示格式(如HTML)。
这两种技术都可以很容易地在同一个控制器中同时使用,就像这样:
@Controller
class AccountController {
// RESTful method
@RequestMapping(value="/accounts", produces={"application/xml", "application/json"})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody List 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)的集成。第二个方法调用第一个方法,并在模型中设置响应,以供视图使用。这就避免了重复的逻辑。
为了确定要选择的两个@requestmapping方法中的哪一个,我们再次使用了我们的PPA内容协商策略。它允许生产选项工作。url以账户。xml或账户。json映射到第一个方法,任何在帐户中结束的url。任何东西都映射到第二个。
或者,如果我们使用视图来生成所有可能的内容类型,那么我们可以只使用一个方法来完成整个事情。这是ContentNegotiatingViewResolver的由来,那将是我的下一篇文章的主题。
文章地址:content-negotiation-using-spring-mvc