乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

内容协商

本章聚焦 Spring MVC 的内容协商机制。在已经掌握请求映射、参数绑定、返回值处理之后,必须回答一个 RESTful API 设计的核心问题:同一个接口如何根据客户端的偏好,自动返回 JSON、XML 或 HTML 等不同格式的响应? 理解 ContentNegotiationManager 的决策流程和三种协商策略的优先级,是设计规范 RESTful 接口的基础。


定义与作用

内容协商(Content Negotiation)是 HTTP 协议的核心机制之一,指客户端和服务器就响应资源的表示格式达成一致的过程。在 Spring MVC 中,内容协商体现为:同一个 Controller 方法返回同一个 Java 对象,Spring MVC 根据客户端的偏好,自动选择将其序列化为 JSON、XML 或 HTML。

没有内容协商时,开发者需要为每种格式写独立的接口或手动判断:

// 丑陋的手动判断方式
@GetMapping("/user")
public Object getUser(@RequestParam String format) {
    User user = userService.findById(1);
    if ("json".equals(format)) {
        return jsonConverter.write(user);  // 手动转 JSON
    } else if ("xml".equals(format)) {
        return xmlConverter.write(user);   // 手动转 XML
    }
    return user;  // 默认
}

Spring MVC 的内容协商机制消除了这种手动判断。Controller 只需返回业务对象,"用什么格式"由协商策略决定,"怎么序列化"由 HttpMessageConverter 执行。

生活类比:多语言餐厅的点餐系统

想象一家国际化餐厅,菜单有中文、英文、日文三个版本:

  • 没有内容协商:客人必须明确说"我要中文菜单",服务员才能给对。如果客人没说,服务员随机给一份,客人可能看不懂。
  • 有内容协商:客人进门时,服务员观察客人的衣着、语言、护照(Accept 头),自动推断最可能需要的语言版本。如果推断不出来,再礼貌询问(fallback 策略)。

Spring MVC 的内容协商就是这位"自动推断的服务员"——它根据客户端发送的信号,选择最合适的响应格式,让 Controller 无需关心格式细节。


核心原理

内容协商决策流程

上图展示了 Spring MVC 内容协商的完整决策流程。注意三个策略的检查顺序:路径扩展 → 请求参数 → Accept 头。这个顺序不是随意的,而是由 ContentNegotiationManager 中注册的 ContentNegotiationStrategy 顺序决定。Spring Boot 默认配置下,路径扩展策略优先,但 RESTful 最佳实践建议关闭它。

三种协商策略对比

策略触发方式优先级(默认)RESTful 推荐度示例
路径扩展URL 后缀最高(默认开启)❌ 不推荐/users/1.json
请求参数URL 查询参数中(默认关闭)⚠️ 谨慎使用/users/1?format=json
Accept 头HTTP 请求头最低(默认)✅ 强烈推荐Accept: application/json

路径扩展策略(默认开启,但应关闭)

// 请求:GET /users/1.json
// Spring MVC 从 .json 推断需要 application/json
// 问题:与 RESTful 资源定位冲突,且暴露实现细节

为什么不推荐:

  • /users/1.json 暗示"1.json"是一个资源,但实际上资源是 /users/1,JSON 只是表示格式
  • 路径扩展与内容类型耦合,更换序列化方式需要改 URL
  • 存在安全风险(路径遍历与扩展名解析的边界情况)

请求参数策略(可选开启)

// 请求:GET /users/1?format=json
// 适合需要让非技术用户(如测试人员)快速切换格式的场景
// 但不应作为 API 的主要协商方式

Accept 头策略(RESTful 标准)

// 请求:GET /users/1
// Accept: application/json
// 或 Accept: application/xml
// 或 Accept: text/html

为什么推荐:

  • 符合 HTTP 协议设计初衷:资源标识(URL)与表示格式(Accept)分离
  • 同一 URL 可以有多种表示,不破坏资源定位的语义
  • 前端框架、浏览器、API 测试工具都原生支持

Accept 头解析过程

关键理解:

  1. q 值(质量因子):客户端用 q=0.x 表达偏好强度,越大越优先。application/json;q=0.9, application/xml;q=0.8 表示优先 JSON
  2. 通配符:Accept: */* 表示接受任何格式,通常返回默认格式(JSON)
  3. 无 Accept 头:浏览器通常发送 Accept: text/html,application/xhtml+xml...,API 客户端可能不发送。此时使用默认格式

ContentNegotiationConfigurer 配置

Spring Boot 2.x / Spring 5.x 中,通过 WebMvcConfigurer 关闭路径扩展、启用 Accept 头策略:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            // 关闭路径扩展策略(不推荐在 RESTful API 中使用)
            .favorPathExtension(false)
            // 关闭 URL 参数策略(可选,根据团队规范决定)
            .favorParameter(false)
            // 设置默认内容类型,当无法协商时返回 JSON
            .defaultContentType(MediaType.APPLICATION_JSON)
            // 明确注册支持的媒体类型
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML);
    }
}

与 HttpMessageConverter 的关系

内容协商和消息转换是分工协作的两个层次:

层次职责代表组件
内容协商决定"用什么格式"ContentNegotiationManager、ContentNegotiationStrategy
消息转换决定"怎么序列化"MappingJackson2HttpMessageConverter、Jaxb2RootElementHttpMessageConverter

关键边界:内容协商只负责"选格式",如果选中的格式没有对应的 HttpMessageConverter 支持,Spring MVC 会抛出 HttpMediaTypeNotAcceptableException(406 Not Acceptable)。


完整示例

场景

飞翔科技对外提供开发者开放平台 API。CTO 大翔要求 API 严格遵循 RESTful 规范,架构师白歌负责设计内容协商策略。前端黄俪的 Web 管理后台需要 JSON 数据,部分企业客户的老系统只支持 XML 解析,运维李眉需要偶尔用浏览器直接访问接口查看原始数据。

白歌决定:统一使用 Accept 头进行内容协商,关闭路径扩展策略,避免 URL 混乱。

配置内容协商

@Configuration
public class OpenApiConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorPathExtension(false)   // 禁用 /user.json 这种写法
            .favorParameter(false)       // 禁用 ?format=json
            .ignoreAcceptHeader(false)   // 必须解析 Accept 头
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML);
    }

    // 注册 XML 转换器(Spring Boot 默认只有 JSON)
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // Jackson 处理 JSON(Spring Boot 已自动配置)
        // 手动添加 JAXB XML 转换器
        converters.add(new Jaxb2RootElementHttpMessageConverter());
    }
}

支持 XML 的实体类

@XmlRootElement(name = "developer")
@XmlAccessorType(XmlAccessType.FIELD)
public class Developer {
    
    private Long id;
    private String name;
    private String email;
    
    // JAXB 需要无参构造器
    public Developer() {}
    
    public Developer(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    // getters/setters...
}

Controller 代码(与格式完全解耦)

@RestController
@RequestMapping("/api/v1/developers")
public class DeveloperController {

    @Autowired
    private DeveloperService developerService;

    @GetMapping("/{id}")
    public Developer getDeveloper(@PathVariable Long id) {
        // 完全不需要关心返回 JSON 还是 XML
        // 由客户端的 Accept 头决定
        return developerService.findById(id);
    }

    @GetMapping
    public List<Developer> listDevelopers() {
        return developerService.findAll();
    }
}

不同客户端的请求与响应

黄俪的前端(JSON):

curl -H "Accept: application/json" http://localhost:8080/api/v1/developers/1
{
  "id": 1,
  "name": "白歌",
  "email": "baige@feixiang.tech"
}

企业客户的老系统(XML):

curl -H "Accept: application/xml" http://localhost:8080/api/v1/developers/1
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<developer>
  <id>1</id>
  <name>白歌</name>
  <email>baige@feixiang.tech</email>
</developer>

李眉用浏览器直接访问:

浏览器默认发送 Accept: text/html,...,如果服务器不支持 HTML 表示,会返回 406。白歌为此添加了 HTML 回退:

@Controller
@RequestMapping("/api/v1/developers")
public class DeveloperHtmlController {

    @Autowired
    private DeveloperService developerService;

    // 当 Accept 包含 text/html 时命中此方法
    @GetMapping(value = "/{id}", produces = MediaType.TEXT_HTML_VALUE)
    public String getDeveloperHtml(@PathVariable Long id, Model model) {
        model.addAttribute("developer", developerService.findById(id));
        return "developer/detail";  // Thymeleaf 模板
    }
}

变化分析:

  • 小崔新增接口时,无需考虑格式问题,专注业务逻辑
  • 黄俪前端调用时显式指定 Accept: application/json,获得紧凑的 JSON
  • 企业客户的老系统用 Accept: application/xml 获得兼容格式
  • 李眉用浏览器访问时自动渲染 HTML 页面,无需安装 Postman
  • 白歌 review 时发现所有接口 URL 统一,没有 .json 或 .xml 后缀,符合 RESTful 规范

易错场景与面试考点

误区一:路径扩展策略是 RESTful 的推荐做法

错误认知:"很多教程用 /user.json,所以这是标准做法。"

纠正:/user.json 是早期 Rails 等框架的习惯,不是 RESTful 规范。Roy Fielding 的 REST 论文明确指出:资源标识符(URL)应与表示格式分离。正确的 RESTful 设计是:

  • 资源:/users/1
  • 格式偏好:Accept: application/json

路径扩展的问题:

  • 暴露技术实现细节到 URL
  • 一个资源有 N 种格式就有 N 个 URL,违背"统一接口"原则
  • 与缓存策略冲突(/users/1.json 和 /users/1.xml 被视为不同资源)

误区二:Content-Type 请求头决定响应格式

错误认知:"客户端发送 Content-Type: application/json,服务器就应该返回 JSON。"

纠正:Content-Type 描述的是请求体的格式,不是期望的响应格式。决定响应格式的是 Accept 请求头。混淆两者的典型错误:

# 错误:用 Content-Type 要求响应格式
POST /users
Content-Type: application/json  ← 这表示"我发送的是 JSON"
Accept: application/xml         ← 这才表示"我希望收到 XML"

GET 请求没有请求体,此时 Content-Type 毫无意义,不应发送。

误区三:协商失败时 Spring MVC 会自动 fallback

错误认知:"如果客户端要求 XML 但服务器不支持,会自动返回 JSON。"

纠正:默认行为是抛出异常,返回 406 Not Acceptable:

// 客户端:Accept: application/xml
// 服务器:没有配置 XML 转换器
// 结果:406 Not Acceptable

如果需要 fallback 到默认格式,需要显式配置:

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer
        .defaultContentType(MediaType.APPLICATION_JSON)  // 协商失败时返回 JSON
        .ignoreAcceptHeader(false);
}

面试高频:内容协商的完整流程

标准回答:

  1. 请求到达,DispatcherServlet 调用 ContentNegotiationManager
  2. 按注册策略顺序尝试解析请求的媒体类型偏好:路径扩展 → 请求参数 → Accept 头
  3. 获取 Controller 返回值和候选 HttpMessageConverter 列表
  4. 遍历协商出的媒体类型,找到第一个支持该类型的 Converter
  5. Converter 将 Java 对象序列化为对应格式,写入响应体
  6. 设置 Content-Type 响应头

面试高频:为什么 RESTful API 应该用 Accept 头

标准回答:

  1. 语义分离:URL 标识资源,Accept 标识格式,职责清晰
  2. 协议原生:Accept 头是 HTTP/1.1 标准的一部分,所有 HTTP 客户端都支持
  3. 缓存友好:同一 URL 配合 Vary: Accept 头,缓存系统可以正确区分不同表示
  4. 扩展性:新增格式无需改 URL,只需客户端改 Accept 头,服务端加 Converter

小结

Spring MVC 的内容协商机制通过 ContentNegotiationManager 协调多种策略,根据客户端偏好自动选择响应格式。RESTful API 的最佳实践是关闭路径扩展和参数策略,依赖 Accept 请求头,让 URL 专注资源定位,格式偏好通过标准 HTTP 机制表达。

核心分工记忆:

  • 内容协商决定 "用什么格式"
  • HttpMessageConverter 决定 "怎么序列化"

本章与全局的关系:本章讲解了 Spring MVC 如何根据客户端偏好选择响应格式。下一章"自定义参数解析器"将深入讲解 Spring MVC 的扩展机制——如何通过 HandlerMethodArgumentResolver 将自定义对象(如 JWT 解析后的 CurrentUser)直接注入 Controller 方法参数。

上一页
自定义参数解析器