使用 Spring MVC 进行内容协商

工程 | Paul Chapman | 2013 年 5 月 11 日 | ...

有两种方法可以使用 Spring MVC 生成输出

  • 您可以使用 RESTful 的 @ResponseBody 方法和 HTTP 消息转换器,通常用于返回 JSON 或 XML 等数据格式。程序化客户端、移动应用程序和启用 AJAX 的浏览器是常见的客户端。
  • 或者,您可以使用 *视图解析*。尽管视图可以根据需要完美地生成 JSON 和 XML(我的下一篇文章将详细介绍),但视图通常用于生成 HTML 等表示格式,用于传统的 Web 应用程序。
  • 实际上,还存在第三种可能性 - 一些应用程序同时需要两者,而 Spring MVC 可以轻松地支持此类组合。我们将在最后再回到这一点。

无论哪种情况,您都需要处理控制器返回的相同数据的多种表示形式(或视图)。确定要返回哪种数据格式称为 *内容协商*。

有三种情况我们需要知道在 HTTP 响应中发送哪种数据格式

  • **HttpMessageConverters:** 确定要使用的正确转换器。
  • **请求映射:** 将传入的 HTTP 请求映射到返回不同格式的不同方法。
  • **视图解析:** 选择要使用的正确视图。

确定用户请求的格式依赖于 ContentNegotationStrategy。开箱即用地提供了一些默认实现,但您也可以根据需要实现自己的实现。

在这篇文章中,我想讨论如何使用 Spring 配置和使用内容协商,主要是在使用 HTTP 消息转换器的 RESTful 控制器方面。在以后的 文章 中,我将展示如何专门为使用 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 MVC 中启用内容协商

Spring 支持两种用于选择所需格式的约定:URL 后缀和/或 URL 参数。这些与 Accept 标头一起使用。因此,可以通过三种方式请求内容类型。默认情况下,它们按以下顺序检查

  • 在 URL 中添加路径扩展名(后缀)。因此,如果传入的 URL 类似于 http://myserver/myapp/accounts/list.html,则需要 HTML。对于电子表格,URL 应为 http://myserver/myapp/accounts/list.xls。后缀到媒体类型的映射是通过 *JavaBeans Activation Framework* 或 JAF 自动定义的(因此 activation.jar 必须位于类路径上)。
  • 类似这样的 URL 参数: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" />

由任一设置创建的 ContentNegotiationManagerContentNegotationStrategy 的实现,它实现了上面描述的 *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>

我们在两种情况下都做了什么

  • 禁用路径扩展名。请注意,favor 并不意味着优先使用一种方法而不是另一种方法,它只是启用或禁用它。检查顺序始终是路径扩展名、参数、Accept 标头。
  • 启用 URL 参数的使用,但我们不使用默认参数 format,而是使用 mediaType
  • 完全忽略 Accept 标头。如果大多数客户端实际上是 Web 浏览器(通常通过 AJAX 进行 REST 调用),这通常是最佳方法。
  • 不要使用 JAF,而是手动指定媒体类型映射 - 我们只希望支持 JSON 和 XML。

列出用户帐户示例

为了演示,我将一个简单的帐户列表应用程序作为我们的工作示例组合在一起 - 屏幕截图显示了 HTML 中典型的帐户列表。完整的代码可以在 Github 上找到:https://github.com/paulc4/mvc-content-neg

要以 JSON 或 XML 格式返回帐户列表,我需要一个像这样的控制器。我们现在将忽略 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 中设置活动配置文件来在 MVC 的 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<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 中设置响应以供视图使用。这样避免了重复的逻辑。

为了确定要选择哪两个 @RequestMapping 方法,我们再次使用我们的 PPA 内容协商策略。它允许 produces 选项工作。以 accounts.xmlaccounts.json 结尾的 URL 映射到第一个方法,任何其他以 accounts.anything 结尾的 URL 映射到第二个方法。

另一种方法

或者,如果我们使用视图生成所有可能的内容类型,我们可以只用一个方法完成所有操作。这就是 ContentNegotiatingViewResolver 发挥作用的地方,这将是我下一篇文章的主题 文章

致谢

我要感谢 Rossen Stoyanchev 在撰写本文方面提供的帮助。任何错误都是我自己的。

附录:带注释的 Account 类

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 ...

}

获取 Spring 电子邮件简报

通过 Spring 电子邮件简报保持联系

订阅

领先一步

VMware 提供培训和认证,助您快速提升。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部