1. 概述

本文介绍如何在 Spring MVC 项目中实现内容协商。

通常,可以通过三个选项来确定请求的媒体类型:

  • (Deprecated) 在请求中使用 URL 后缀(扩展名)(例如 .xml/.json
  • 在请求中使用 URL 参数(例如 ?format=json
  • 在请求中使用 Accept 标头

默认情况下,这是 Spring 内容协商管理器尝试使用这三种策略的顺序。如果这些都没有启用,我们可以指定回退到默认内容类型。

2. 内容谈判策略

让我们从必要的依赖项开始 - 我们正在使用 JSON 和 XML 表示形式,因此在本文中,我们将使用 Jackson 来表示 JSON:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.10.2</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.2</version>
</dependency>

对于 XML 支持,我们可以使用 JAXB、XStream 或较新的 Jackson-XML 支持。

由于我们已经在之前有关 HttpMessageConverters 的文章中解释了 Accept 标头的使用 因此让我们深入讨论前两种策略。

3. URL后缀策略

从 Spring Boot 2.6.x 版本开始,针对已注册的 Spring MVC 处理程序映射匹配请求路径的默认策略已从 AntPathMatcher 更改为 PathPatternParser。

由于 PathPatternParser 不支持后缀模式匹配,因此在使用此策略之前,我们首先需要使用旧式路径匹配器。

我们可以在 application.properties 文件中添加 spring.mvc.pathmatch.matching-strategy 以将默认策略切换回 AntPathMatcher。

默认情况下,此策略是禁用的,我们需要通过在 application.properties 中将 spring.mvc.pathmatch.use-suffix-pattern 设置为 true 来启用它:

一旦启用,框架可以直接从 URL 检查路径扩展以确定输出内容类型。

spring.mvc.pathmatch.use-suffix-pattern=true
spring.mvc.pathmatch.matching-strategy=ant-path-matcher

在进入配置之前,让我们快速看一下一个示例。我们在典型的 Spring 控制器中实现了以下简单的 API 方法:

@RequestMapping(
  value = "/employee/{id}", 
  produces = { "application/json", "application/xml" }, 
  method = RequestMethod.GET)
public @ResponseBody Employee getEmployeeById(@PathVariable long id) {
    return employeeMap.get(id);
}

让我们使用 JSON 扩展来调用它来指定资源的媒体类型:

curl http://localhost:8080/spring-mvc-basics/employee/10.json

如果我们使用 JSON 扩展,我们可能会得到以下结果:

{
    "id": 10,
    "name": "Test Employee",
    "contactNumber": "999-999-9999"
}

以下是 XML 的请求-响应:

curl http://localhost:8080/spring-mvc-basics/employee/10.xml

响应正文:

<employee>
    <contactNumber>999-999-9999</contactNumber>
    <id>10</id>
    <name>Test Employee</name>
</employee>

现在, 如果我们不使用任何扩展 或使用未配置的扩展,则将返回默认内容类型:

curl http://localhost:8080/spring-mvc-basics/employee/10

现在让我们看看如何使用 Java 和 XML 配置来设置此策略。

3.1. Java配置

public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.favorPathExtension(true).
    favorParameter(false).
    ignoreAcceptHeader(true).
    useJaf(false).
    defaultContentType(MediaType.APPLICATION_JSON); 
}

让我们看一下详细信息。

首先,我们启用路径扩展策略。 还值得一提的是,从Spring Framework 5.2.4开始, favorPathExtension(boolean) 方法已被弃用,以阻止在内容协商中使用路径扩展。

然后,我们禁用 URL 参数策略和 Accept header 策略——因为我们只想依靠路径扩展方式来确定内容的类型。

然后我们关闭 Java Activation Framework;如果传入请求与我们配置的任何策略都不匹配,JAF 可以用作后备机制来选择输出格式。我们禁用它是因为我们要将 JSON 配置为默认内容类型。请注意, 从 Spring Framework 5 开始, useJaf() 方法已被弃用

最后 – 我们将 JSON 设置为默认值。这意味着如果两种策略均不匹配,则所有传入请求都将映射到提供 JSON 服务的控制器方法。

3.2. XML配置

我们还可以快速浏览一下仅使用 XML 的相同配置:

<bean id="contentNegotiationManager" 
  class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="favorPathExtension" value="true" />
    <property name="favorParameter" value="false"/>
    <property name="ignoreAcceptHeader" value="true" />
    <property name="defaultContentType" value="application/json" />
    <property name="useJaf" value="false" />
</bean>

4. URL参数策略

我们在上一节中使用了路径扩展 - 现在让我们设置 Spring MVC 以使用路径参数。

我们可以通过将 favorParameter 属性的值设置为true来启用此策略。

让我们快速看一下它如何与我们之前的示例一起工作:

curl http://localhost:8080/spring-mvc-basics/employee/10?mediaType=json

JSON 响应正文如下:

{
    "id": 10,
    "name": "Test Employee",
    "contactNumber": "999-999-9999"
}

如果我们使用 XML 参数,输出将为 XML 形式:

curl http://localhost:8080/spring-mvc-basics/employee/10?mediaType=xml

响应正文:

<employee>
    <contactNumber>999-999-9999</contactNumber>
    <id>10</id>
    <name>Test Employee</name>
</employee>

现在让我们再次进行配置,首先使用 Java,然后使用 XML。

4.1. Java配置

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); 
}

让我们通读一下这个配置。

首先,当然,路径扩展和 Accept header 策略被禁用(以及 JAF)。

其余配置相同。

4.2. XML配置

<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="defaultContentType" value="application/json" />
    <property name="useJaf" value="false" />

    <property name="mediaTypes">
        <map>
            <entry key="json" value="application/json" />
            <entry key="xml" value="application/xml" />
        </map>
    </property>
</bean>

此外,我们可以同时 启用两种策略(扩展和参数)

public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.favorPathExtension(true).
    favorParameter(true).
    parameterName("mediaType").
    ignoreAcceptHeader(true).
    useJaf(false).
    defaultContentType(MediaType.APPLICATION_JSON).
    mediaType("xml", MediaType.APPLICATION_XML). 
    mediaType("json", MediaType.APPLICATION_JSON); 
}

在这种情况下,Spring 将首先查找路径扩展,如果不存在,则将查找路径参数。如果这两者在输入请求中都不可用,则将返回默认内容类型。

5. Accept header 策略

如果启用了 Accept 标头,Spring MVC 将在传入请求中查找其值以确定表示类型。

我们必须将 ignoreAcceptHeader 的值设置为 false 才能启用此方法,并且禁用其他两种策略,以便我们知道我们只依赖于 Accept 标头。

5.1. Java配置

public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.favorPathExtension(true).
    favorParameter(false).
    parameterName("mediaType").
    ignoreAcceptHeader(false).
    useJaf(false).
    defaultContentType(MediaType.APPLICATION_JSON).
    mediaType("xml", MediaType.APPLICATION_XML). 
    mediaType("json", MediaType.APPLICATION_JSON); 
}

5.2. XML配置

<bean id="contentNegotiationManager" 
  class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="favorPathExtension" value="true" />
    <property name="favorParameter" value="false"/>
    <property name="parameterName" value="mediaType"/>
    <property name="ignoreAcceptHeader" value="false" />
    <property name="defaultContentType" value="application/json" />
    <property name="useJaf" value="false" />

    <property name="mediaTypes">
        <map>
            <entry key="json" value="application/json" />
            <entry key="xml" value="application/xml" />
        </map>
    </property>
</bean>

最后,我们需要将内容协商管理器插入到整体配置中来打开它:

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />

六,结论

我们就完成了。我们研究了内容协商在 Spring MVC 中的工作原理,并重点介绍了一些设置它以使用各种策略来确定内容类型的示例。

本文的完整实现可以在 GitHub 上找到。