路径匹配

路径匹配概述

在 Spring 框架(这里聚焦于 Spring5.3 及后续版本涉及的请求路径匹配相关内容 )里,请求路径匹配是很关键的部分,是处理 Web 请求时确定 URL 路径与控制器方法映射关系的核心机制。

路径匹配是指框架如何将传入的 HTTP 请求 URL 映射到对应的控制器方法上。例如,当用户访问 /users/123 时,框架需要决定这个请求应该由哪个 @GetMapping@RequestMapping 注解的方法来处理。

在 Spring Framework 5.3 及 Spring Boot 2.4 之后,引入了一种新的路径匹配机制,这一变化在 Spring Boot 3 中得到了保留和进一步的应用。这个新机制主要是通过 PathPattern 代替了传统的 AntPathMatcher。

AntPathMatcher 是基于 Ant 风格的路径匹配,而 PathPattern 则是一个更高效、更精确的路径匹配方式,它是通过 PathPatternParser 解析得到的。

路径匹配两大策略

Spring5.3 之后加入了更多的请求路径匹配的实现策略:

以前只支持 AntPathMatcher 策略,现在提供了 PathPatternParser 策略。并且可以让我们指定到底使用那种策略。

Spring Boot 默认使用新版的路径匹配器,也就是 PathPatternParser ,性能高,但是 ** 不能写在中间

使用 spring.mvc.pathmath.macthing-strategy 改变匹配策略

具体策略说明

  • AntPathMatcher:是 Spring 中传统的路径匹配策略,基于 Ant 风格的路径模式语法,在之前的 Spring 版本中广泛使用,很多开发者对其规则比较熟悉,用于处理各种 Web 请求路径与配置的映射关系,像在 Spring MVC 里配置请求映射(@RequestMapping 等注解配合路径规则)时,常基于它工作 。
  • PathPatternParser:Spring5.3 之后新增的策略,它在一些场景下可能有更好的性能表现或者更灵活的特性,和 Ant 风格路径用法有一定关联但也有自身特点,也用于解析请求路径,确定是否匹配预设的路径模式,辅助完成请求的路由等操作 。开发者可以根据项目实际需求,比如对路径匹配性能、规则灵活度等方面的要求,来指定使用哪种策略。

Ant 风格路径

AntPathMatcher 是 Spring 框架中一个基于 Ant 风格模式的路径匹配器,它支持使用 ?*** 等通配符进行匹配。

Ant 风格的路径模式语法具有以下规则:

  • *(星号)
    • 作用是匹配任意数量的字符,但不包含目录分隔符(/ )。
    • 比如*.html ,就是说不管前面的文件名是什么(像a.htmlabc.html 等),只要是以.html 为后缀的文件路径,都能匹配上。常用于匹配同一层级下符合某种后缀特征的文件或路径片段。
  • ?(问号)
    • 代表匹配任意一个字符,同样不跨越目录分隔符。
    • 例如有路径a?b ,那么aabacb 等单字符差异的路径能匹配,而aabb (多了一个字符 )就匹配不上。可以用于精确控制路径中某一位字符的不确定性场景。
  • **(两个星号)
    • 强大之处在于能匹配任意数量的目录,包括多层嵌套的目录结构。
    • folder2/**/*.jsp ,不管folder2 下面有多少级子目录,只要最终文件是.jsp 后缀,就会被匹配到,比如folder2/a/b/c.jspfolder2/d.jsp 等情况都满足,在处理多层目录下的资源匹配时非常好用。
  • {}(花括号)
    • 用于定义命名的模式占位符,路径变量,这在 RESTful 风格的路径中很常用。
    • 比如/{type}/id}.html ,这里的{type}{id} 就是占位符,实际请求路径可能是/user/123.html ,此时type 就对应userid 对应123 ,在 Spring MVC 中,这些占位符的值可以被提取出来,作为方法参数使用(通过@PathVariable 注解 ),方便进行动态的路径参数处理。
  • [](方括号)
    • 用来表示字符集合,限定匹配的字符范围。
    • [a - z] 就表示只能匹配小写字母 a 到 z 中的一个字符,若路径规则是[a - z]bc ,那么abcbbc 等符合字符范围的能匹配,而1bc (数字不在集合内 )、Abc (大写字母不在集合内 )就匹配失败,可用于对路径中特定位置字符进行精确范围限制的场景,比如某些有特定字符规范的编号路径等 。

注意:Ant 风格的路径模式语法中的特殊字符需要转义

转义说明

在 Ant 风格路径模式语法里,特殊字符(像*? 等本身具有特殊匹配含义的字符 )如果是作为普通字符出现在路径中(比如路径里真的有一个* 作为文件名的一部分 ),就需要进行转义,一般是通过在特殊字符前加反斜杠(\ )等方式,让框架把它们当作普通字符对待,而不是执行匹配规则,否则框架会按照匹配规则去解析,导致路径匹配结果不符合预期 。

1
2
3
4
@GetMapping("/files/\\*.txt")  // 匹配字面量 /files/*.txt
public String handleFile() {
return "file";
}

PathPatternParser 风格路径

PathPatternParser: 一个新的路径解析器,用于解析路径模式字符串,创建 PathPattern 对象。它引入了更严格的语法规则,并且设计了更高效的匹配算法。

PathPattern: 由 PathPatternParser 解析路径模式字符串得到的对象,代表了一种更加精确和高效的路径匹配方式。

特点:

  • 性能: 相比 AntPathMatcher,PathPattern 提供了更高的性能。这是因为 PathPattern 在匹配过程中采用了更加高效的算法,在 jmh 基准测试下,有 6~8 倍吞吐量提升,降低 30%~40%空间分配率。
  • 精确性: PathPattern 的语法规则更严格,能够提供更精确的匹配结果。
    • 路径变量语法更严格,必须明确指定变量名:/{name}
  • 使用场景: 在 Spring Framework 5.3 及之后的版本中,默认使用 PathPattern 进行路径匹配。如果你的应用是基于这些版本的 Spring Boot 构建的,那么在处理路径匹配时,你将会默认使用 PathPattern。
  • ** 通配符的使用有限制(不能出现在路径中间)
    • 有效:/resources/**
    • 无效:/resources/**/file

路径匹配示例

控制器方法映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/api")
public class MyController {

// 匹配 /api/users/123
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// ...
}

// 匹配 /api/files/2023/任何内容
@GetMapping("/files/2023/**")
public List<File> get2023Files() {
// ...
}

// 匹配 /api/products/abc123
@GetMapping("/products/{code:[a-z]+\\d+}")
public Product getProduct(@PathVariable String code) {
// ...
}
}

解释一下/products/{code:[a-z]+\\d+}这段正则是如何匹配/api/products/abc123

  • {code:[a-z]+\\d+} 这种形式定义了一个名为 code 的路径变量,并且对它的值设定了正则表达式约束。
  • 其中,其中 [a-z]+ 表示这个部分必须由至少一个小写字母组成,\\d+ 表示这部分必须由至少一个数字组成
  • 所以说,整个正则表达式 [a-z]+\\d+ 的意思是,code 的值要先有至少一个小写字母,接着有至少一个数字,而且字母和数字之间不能有其他字符,所以,/api/products/a1 也能匹配,而 /api/products/123abc 就无法匹配

静态资源匹配

1
2
# 匹配所有静态html文件
spring.mvc.static-path-pattern=/*.html

拦截器路径配置

1
2
3
4
5
6
7
8
9
10
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/admin/**") // 拦截/admin下的所有路径
.excludePathPatterns("/admin/login"); // 排除登录页
}
}

内容协商

注意这里多源码解读

什么是内容协商

在 Spring Boot 3 中,内容协商(Content Negotiation)是一个非常重要的概念,特别是在构建 RESTful API 时。

内容协商机制允许客户端和服务器就如何交换资源的数据格式达成协议,允许同一资源URL根据客户端的偏好提供不同格式的表示。这一过程通常由服务器和客户端共同完成:客户端告知服务器它期望的内容类型,服务器根据自身能力选择最合适的表现形式返回。

简单来说,它允许客户端通过请求头指定它们希望接收响应的格式(如 JSON,XML 等),服务器基于这些信息来决定以什么格式返回数据。

内容协商主要依靠媒体类型(Media Type),也称为MIME类型,如application/jsonapplication/xmltext/html等。

也就是,一套系统适配多端数据返回。

image-20250607143353192

Spring Boot 中的内容协商架构

SpringBoot基于Spring MVC的内容协商机制,通过以下组件实现:

  1. ContentNegotiationManager: 负责协调整个内容协商过程
  2. ContentNegotiationStrategy: 定义如何确定客户端请求的媒体类型
  3. HttpMessageConverter: 负责在Java对象和HTTP请求/响应体之间进行转换

SpringBoot默认支持多种内容协商策略,可以根据需求进行配置和组合。

多端内容适配

基于请求头实现内容协商

基于请求头的内容协商是最符合HTTP规范的一种方式,它通过检查HTTP请求中的Accept头来确定客户端期望的响应格式。例如,当客户端发送Accept: application/json头时,服务器会优先返回JSON格式的数据。

这种策略由HeaderContentNegotiationStrategy实现,是SpringBoot的默认内容协商策略。

  • 优先检查请求的Accept
  • 其次检查URL路径扩展名(如.json)
  • 默认支持JSON格式(通过Jackson),因为默认的 web 场景导入了 jackson 的相关包

这种行为有很多好处

  • 符合HTTP规范,是RESTful API的推荐实践,一般在面向程序化客户端的API接口,当多种客户端需要相同数据的不同表现形式时使用
  • 无需修改URL,保持URL的简洁性
  • 适用于所有HTTP客户端
  • 对缓存友好

在SpringBoot中,默认已启用基于请求头的内容协商,无需额外配置。如果需要显式配置,可以在application.propertiesapplication.yml中添加:

1
2
3
4
5
6
7
8
9
10
11
12
# 启用/禁用基于后缀的内容协商
spring.mvc.contentnegotiation.favor-path-extension=true

# 启用/禁用请求参数内容协商
spring.mvc.contentnegotiation.favor-parameter=true

# 设置请求参数名称(默认为format)
spring.mvc.contentnegotiation.parameter-name=format

# 注册的媒体类型映射
spring.mvc.contentnegotiation.media-types.json=application/json
spring.mvc.contentnegotiation.media-types.xml=application/xml

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class WebConfig implements WebMvcConfigurer {

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

基于请求参数的内容协商

基于请求参数的内容协商通过URL查询参数来确定客户端期望的响应格式。例如,/api/products?format=json请求JSON格式,而/api/products?format=xml请求XML格式。

  • 发送请求 GET/projects/spring-boot?format=json
  • 匹配到 @GetMapping("/projects/spring-boot")
  • 根据参数协商,优先返回 JSON 类型的数据,需要开启参数匹配设置

这种策略由ParameterContentNegotiationStrategy实现,需要显式启用。

配置方式如下

1
2
3
4
5
6
7
8
9
10
11
12
spring:
mvc:
contentnegotiation:
# 启用基于请求参数的内容协商
favor-parameter: true
# 设置请求参数名称,默认为 "format"
parameter-name: format
# 注册扩展名和媒体类型的映射
media-types:
json: application/json
xml: application/xml
html: text/html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.favorParameter(true)
.parameterName("format")
.ignoreAcceptHeader(false)
.defaultContentType(MediaType.APPLICATION_JSON)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.mediaType("html", MediaType.TEXT_HTML);
}
}

注意相关实体类添加对应的注解

基于URL路径扩展名实现内容协商

基于URL路径扩展名的内容协商通过URL末尾的文件扩展名来确定客户端期望的响应格式。例如,/api/products.json请求JSON格式,而/api/products.xml请求XML格式。

这种策略由PathExtensionContentNegotiationStrategy实现,需要特别注意的是,从Spring 5.3开始,出于安全考虑,默认已禁用此策略。

由于路径扩展策略可能导致路径遍历攻击,Spring 5.3后默认禁用。如果必须使用,建议做好URL的安全配置

配置方式如下

1
2
3
4
5
6
7
8
9
10
spring:
mvc:
contentnegotiation:
# 启用基于 URL 路径扩展名的内容协商
favor-path-extension: true
# 明确指定路径扩展名与媒体类型的映射关系
media-types:
json: application/json
xml: application/xml
html: text/html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.favorPathExtension(true)
.ignoreAcceptHeader(false)
.defaultContentType(MediaType.APPLICATION_JSON)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.mediaType("html", MediaType.TEXT_HTML);
}
}

组合策略实现高级内容协商

如何进行策略组合的配置

在实际应用中,通常会组合多种策略实现提供最大的灵活性。可以通过 ContentNegotiationConfigurer 来配置组合策略。

例如如下配置组合了基于请求参数、Accept 请求头和 URL 路径扩展名的内容协商策略,提供了更灵活的内容协商方式。

没错,这个配置类来实现更复杂的配置,是通过实现 WebMvcConfigurer 接口方法实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 启用通过请求参数指定响应格式
configurer.favorParameter(true)
// 设置请求参数名称,默认为 "format"
.parameterName("format")
// 不忽略 Accept 请求头
.ignoreAcceptHeader(false)
// 启用基于 URL 路径扩展名的内容协商
.favorPathExtension(true)
// 设置默认的内容类型为 JSON
.defaultContentType(MediaType.APPLICATION_JSON)
// 注册扩展名和媒体类型的映射
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.mediaType("html", MediaType.TEXT_HTML);
}
}

自定义内容协商策略

简介

自定义内容协商格式主要涉及到两个方面:一是自定义支持的媒体类型(Media Types),二是自定义对这些媒体类型的处理。

在 Spring Boot 3 中,自定义内容协商格式通常需要以下几个步骤:

  • 注册自定义媒体类型:你可以通过配置类来注册自定义的媒体类型,让 Spring MVC 知道你打算支持哪些额外的格式。
  • 实现 HttpMessageConverter 接口:对于每种你想支持的媒体类型,你需要提供一个相应的HttpMessageConverter实现,用于序列化和反序列化数据。
  • 配置 Spring MVC 以使用你的自定义 HttpMessageConverter:最后,你需要在 Spring MVC 配置中注册你的 HttpMessageConverter 实现,以确保Spring MVC 会使用它们进行请求和响应的处理。

ContentNegotiationConfigurer接口

ContentNegotiationConfigurer 是 Spring 框架中的一个接口,用于自定义内容协商策略,主要通过以下几种方式来实现:

  1. URL参数: 通过 URL 参数来指定响应格式,例如,?format=json

  2. Accept头:通过 Accept 请求头来指定希望接收的响应类型,这是HTTP规范推荐的方式。

  3. 扩展名: 通过 URL 的扩展名来指定响应的格式。例如,.json 表示希望响应为 JSON 格式,.xml 表示希望响应为 XML 格式。

实现自定义内容协商-以实现 yaml 内容协商为例子

首先是注册自定义媒体类型,假设你想添加对 application/yaml 这种媒体类型的支持,首先需要在配置类中注册这种媒体类型,注意别忘了引入可能需要的依赖,然后接下来,需要创建一个 WebMvcConfigurer 实例,重写其中的configureContentNegotiation内容,把消息转换器和注册器注册进去,用于处理 YAML 格式的数据。

引入需要的依赖

添加 Jackson 的 YAML 处理模块:

1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
创建 YAML 消息转换器

实现HttpMessageConverter接口,用于处理 YAML 格式的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

import java.io.IOException;

/**
* YAML格式消息转换器,用于处理application/x-yaml媒体类型
*/
public class YamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

private final ObjectMapper objectMapper;

public YamlHttpMessageConverter() {
// 支持的媒体类型
super(MediaType.valueOf("application/x-yaml"),
MediaType.valueOf("text/yaml"),
MediaType.valueOf("text/x-yaml"));

// 创建YAML格式的ObjectMapper
this.objectMapper = new ObjectMapper(new YAMLFactory());
}

@Override
protected boolean supports(Class<?> clazz) {
// 支持所有类型,由ObjectMapper处理具体序列化/反序列化
return true;
}

@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
try {
// 从输入流读取并反序列化为对象
return objectMapper.readValue(inputMessage.getBody(), clazz);
} catch (Exception e) {
throw new HttpMessageNotReadableException("Failed to read YAML: " + e.getMessage(), e, inputMessage);
}
}

@Override
protected void writeInternal(Object obj, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
try {
// 将对象序列化为YAML并写入输出流
objectMapper.writeValue(outputMessage.getBody(), obj);
} catch (Exception e) {
throw new HttpMessageNotWritableException("Failed to write YAML: " + e.getMessage(), e);
}
}
}
配置内容协商策略

注册自定义媒体类型并配置消息转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.HashMap;
import java.util.Map;

/**
* Web MVC配置,用于自定义内容协商策略
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 配置支持的媒体类型映射
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json", MediaType.APPLICATION_JSON);
mediaTypes.put("yaml", MediaType.valueOf("application/x-yaml"));

// 配置内容协商策略
configurer
.favorParameter(true) // 支持通过URL参数指定格式
.parameterName("format") // 参数名:format=yaml
.ignoreAcceptHeader(false) // 不忽略Accept请求头
.useRegisteredExtensionsOnly(false) // 允许未注册的扩展名
.defaultContentType(MediaType.APPLICATION_JSON) // 默认返回JSON
.mediaType("json", MediaType.APPLICATION_JSON) // JSON格式
.mediaType("yaml", MediaType.valueOf("application/x-yaml")); // YAML格式
}

// 注册自定义消息转换器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 将YAML转换器添加到列表开头,优先使用
converters.add(0, new YamlHttpMessageConverter());
}
}
创建示例实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

// 用户实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private Long id; // 用户ID
private String name; // 用户名
private Integer age; // 年龄
private String email; // 邮箱
}
创建 REST 控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;

/**
* 用户控制器,演示内容协商的使用
*/
@RestController
@RequestMapping("/api/users")
public class UserController {

/**
* 获取所有用户
* 支持根据请求协商返回JSON或YAML格式
*/
@GetMapping
public List<User> getAllUsers() {
return Arrays.asList(
new User(1L, "Alice", 30, "alice@example.com"),
new User(2L, "Bob", 25, "bob@example.com"),
new User(3L, "Charlie", 35, "charlie@example.com")
);
}

/**
* 根据ID获取用户
*/
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
// 模拟根据ID查找用户
return new User(id, "User" + id, id.intValue() + 20, "user" + id + "@example.com");
}

/**
* 创建新用户
* 支持处理JSON或YAML格式的请求体
*/
@PostMapping
public User createUser(@RequestBody User user) {
// 模拟创建用户,实际应用中会保存到数据库
user.setId(4L);
return user;
}
}
测试

下面通过几种方式测试 YAML 格式的内容协商:

1
2
3
4
5
# 请求JSON格式
curl -X GET http://localhost:8080/api/users

# 请求YAML格式
curl -X GET -H "Accept: application/x-yaml" http://localhost:8080/api/users

发送 YAML 格式的请求体

1
2
3
4
5
6
curl -X POST -H "Content-Type: application/x-yaml" -d '
id: 5
name: David
age: 40
email: david@example.com
' http://localhost:8080/api/users

内容协商原理 - HttpMessageConverter

基于上述我们的自定义内容协商的内容,我们可以知道,实现自定义内容协商的配置关键在于,编写WebMvcConfigurer提供的configureMessageConverters底层,修改底层的MessageConverters

这里就讲一下是如何实现的和其中的实现原理

内容协商的完整流程

当客户端发起请求时,内容协商的执行流程如下:

  1. 请求到达 DispatcherServlet:Spring MVC 的前端控制器接收请求
  2. HandlerMapping 确定处理器:找到处理该请求的 Controller 方法
  3. 内容协商启动
    • 检查请求的 Accept 头(如 Accept: application/json
    • 检查 URL 参数(如 ?format=json
    • 检查 URL 扩展名(如 .json
  4. 选择合适的 Converter
    • 根据协商结果(如 JSON、XML),从 HttpMessageConverter 列表中选择支持该媒体类型的转换器
    • 排序规则:优先使用用户自定义的 Converter(通过 extendMessageConverters 添加),再使用默认的
  5. 执行转换
    • 请求处理:读取请求体并转换为 Controller 方法的参数
    • 响应处理:将 Controller 返回值序列化为响应格式

核心接口:HttpMessageConverter

在 Spring MVC 中,内容协商是将 HTTP 请求和响应与特定格式(如 JSON、XML、YAML 等)进行匹配的过程。这一过程的核心在于 HttpMessageConverter 接口,它负责处理 HTTP 请求和响应的序列化与反序列化。

HttpMessageConverter 是一个策略接口,负责:

  • 读取 HTTP 请求体并将其转换为 Java 对象(反序列化)
  • 写入 Java 对象到 HTTP 响应体(序列化)

其核心方法包括:

  • canRead(Class clazz, MediaType mediaType):判断是否能将请求转换为指定类型
  • canWrite(Class clazz, MediaType mediaType):判断是否能将对象序列化为指定媒体类型
  • read(Class clazz, HttpInputMessage inputMessage):从请求中读取并转换为对象
  • write(T t, MediaType contentType, HttpOutputMessage outputMessage):将对象写入响应

内容协商的具体实现

内容协商是确定响应格式的过程,主要由 ContentNegotiationManager 负责:

  1. 确定客户端期望的媒体类型
    • 检查 Accept 请求头
    • 检查 URL 参数(如 ?format=json
    • 检查 URL 扩展名(如 .json
  2. 选择合适的 HttpMessageConverter
    • 遍历所有注册的 HttpMessageConverter
    • 调用 canWrite() 方法判断转换器是否支持该类型和媒体类型
    • 选择第一个匹配的转换器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
Iterator var2 = this.strategies.iterator();
// 遍历所有注册的协商策略
List mediaTypes;
do {
if (!var2.hasNext()) {
return MEDIA_TYPE_ALL_LIST;
}

ContentNegotiationStrategy strategy = (ContentNegotiationStrategy)var2.next();
mediaTypes = strategy.resolveMediaTypes(request);
} while(mediaTypes.equals(MEDIA_TYPE_ALL_LIST));

return mediaTypes;
}

@ResponseBodyHttpMessgaeConverter处理

@ResponseBody 注解的作用是:将 Controller 方法的返回值直接写入 HTTP 响应体,而不是转发到视图页面,当方法或类标注了 @ResponseBody@RestController 等价于类上标注 @Controller + @ResponseBody)时,Spring MVC 会启用特殊的返回值处理逻辑。

@ResponseBody 注解是内容协商机制的关键触发点:

  • 当 Controller 方法被 @ResponseBody 注解时(或类被 @RestController 注解),Spring MVC 会:

    • 步骤层次

      • 通过内容协商确定响应的媒体类型
      • 找到支持该媒体类型的 HttpMessageConverter
      • 使用该 Converter 将返回值序列化为响应体
    • 源码层次

      • 请求进来先来到DispatcherServlet类中的doDispatch()方法处理

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        // DispatcherServlet.java
        // 这里是简化的源码流程
        protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 1. 找到处理请求的 Handler
        // 其实一上来这里是空的,一点点补充的
        HandlerExecutionChain mappedHandler = getHandler(request);

        // 2. 找到执行 Handler 的 Adapter
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

        // 3. 执行 Handler
        ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

        // 4. 处理返回值
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
      • 找到了一个HandlerAdapter适配器,利用适配器执行目标方法

        image-20250607154029850
      • RequestMappingHandlerAdapter来执行,最终会调用invokeHandlerMethod()来执行目标方法

        image-20250607160624189
      • 目标方法执行之前,准备好两个东西

        • HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
        • HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值改怎么处理
      • RequestMappingHandlerAdapter里面的invokeAndHandle()真正执行目标方法

      • 目标方法执行完成,会返回返回值对象

      • 找到一个合适的返回值处理器HandlerMethodReturnValueHandler

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        // HandlerMethodReturnValueHandlerComposite.java
        @Override
        public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

        // 查找能处理该返回值的处理器
        HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
        if (handler == null) {
        throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        }

        // 使用处理器处理返回值
        handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }

        private HandlerMethodReturnValueHandler selectHandler(@Nullable Object returnValue, MethodParameter returnType) {
        // 是否是异步返回值
        boolean isAsyncValue = isAsyncReturnValue(returnValue, returnType);
        for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
        if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
        continue;
        }
        // 判断处理器是否支持该返回值类型
        if (handler.supportsReturnType(returnType)) {
        return handler;
        }
        }
        return null;
        }
      • 最终找到RequestResponseBodyMethodProcessor能处理标注了@ResponseBody注解的方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        mavContainer.setRequestHandled(true);
        ServletServerHttpRequest inputMessage = this.createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = this.createOutputMessage(webRequest);
        if (returnValue instanceof ProblemDetail detail) {
        outputMessage.setStatusCode(HttpStatusCode.valueOf(detail.getStatus()));
        if (detail.getInstance() == null) {
        URI path = URI.create(inputMessage.getServletRequest().getRequestURI());
        detail.setInstance(path);
        }

        this.invokeErrorResponseInterceptors(detail, (ErrorResponse)null);
        }

        this.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
        }
      • RequestResponseBodyMethodProcessor调用writeWithMessageConverters,利用MessageConverter把返回值进行序列化然后写出去

      上述的源码内容都在说一个事:@ResponseBodyHttpMessgaeConverter处理的

    • HttpMessageConverter会进行内容协商

      • 遍历所有的MessageConverter,看谁支持这种内容类型的数据
      • 默认的MessageConverter有以下:
      • 最终因为要 json,所以MappingJackson2HttpMessageConverter支持写出 JSON
      • Jackson 用ObjectMapper把对象写出去
  • @ResponseBodyHttpMessageConverter 的协作流程可以概括为:

    1. 请求处理:DispatcherServlet 接收请求并找到处理方法
    2. 方法执行:HandlerAdapter 执行目标方法
    3. 返回值处理:通过 HandlerMethodReturnValueHandler 找到 RequestResponseBodyMethodProcessor
    4. 内容协商:确定响应的媒体类型
    5. 转换器选择:遍历 HttpMessageConverter 列表,找到第一个支持该类型和媒体类型的转换器
    6. 序列化输出:使用选定的转换器将返回值序列化为响应体

所以请求处理的整体流程就很清晰了,当客户端发送 HTTP 请求到 Spring MVC 应用时,整个处理流程可以概括为:

  1. 请求进入 DispatcherServlet:Spring MVC 的前端控制器接收所有请求
  2. HandlerMapping 确定处理器:找到处理该请求的 Controller 方法
  3. HandlerAdapter 执行方法:通过适配器执行目标方法
  4. 返回值处理:将方法返回值转换为 HTTP 响应
  5. 响应输出:将处理结果返回给客户端

例如

1
2
3
4
5
@GetMapping("/users")
@ResponseBody // 可省略,@RestController 已隐含此注解
public List<User> getUsers() {
return Arrays.asList(new User(1L, "Alice", 30));
}

Spring MVC 会根据协商结果选择:

  • MappingJackson2HttpMessageConverter 处理 JSON
  • Jaxb2RootElementHttpMessageConverter 处理 XML
  • 自定义的 YamlHttpMessageConverter 处理 YAML

WebMvcAutoConfiguration提供几种默认的HttpMessageConverters

Spring MVC 默认注册了多种 HttpMessageConverter,按顺序排列如下:

  1. ByteArrayHttpMessageConverter:处理二进制数据,支持 application/octet-stream
  2. StringHttpMessageConverter:处理字符串,支持 text/plain
  3. ResourceHttpMessageConverter:处理资源文件,支持 application/octet-stream
  4. ResourceRegionHttpMessageConverter:处理部分资源,支持媒体类型范围请求
  5. SourceHttpMessageConverter:处理 javax.xml.transform.Source,支持 XML
  6. AllEncompassingFormHttpMessageConverter:处理表单数据,支持 application/x-www-form-urlencoded
  7. Jaxb2RootElementHttpMessageConverter:处理 JAXB 注解的对象,支持 XML
  8. MappingJackson2HttpMessageConverter:处理 JSON,支持 application/json
  9. MappingJackson2XmlHttpMessageConverter:处理 XML,支持 application/xml
  10. AtomFeedHttpMessageConverter:处理 Atom 格式,支持 application/atom+xml
  11. RssChannelHttpMessageConverter:处理 RSS 格式,支持 application/rss+xml

注意这里需要添加可能需要的注解才可能在适配器遍历的时候被选中为有效的

而其中,我们可以知道:

  • 系统提供的默认的 MessageConverter的功能很有限,仅适用于 json 或者普通的数据返回
  • 需要增加额外的内容协商功能,必须添加新的HttpMessageConverters

自定义 HttpMessageConverter 的优先级

当添加自定义 HttpMessageConverter 时,注册顺序非常重要:

  • 替换默认转换器

    1
    2
    3
    4
    5
    6
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.clear(); // 移除所有默认转换器
    converters.add(new YamlHttpMessageConverter()); // 添加自定义转换器
    // 添加其他必要的转换器
    }

    注意:这种方式会完全替换默认转换器,需谨慎使用

  • 扩展默认转换器(推荐方式):

    1
    2
    3
    4
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(0, new YamlHttpMessageConverter()); // 添加到列表开头,优先使用
    }

    原理:Spring MVC 按顺序遍历转换器列表,选择第一个支持的转换器

常见的内置 HttpMessageConverter

Spring MVC 默认提供了多种转换器:

转换器 支持的媒体类型 用途
MappingJackson2HttpMessageConverter application/json, application/*+json JSON 处理
MappingJackson2XmlHttpMessageConverter application/xml, text/xml XML 处理
StringHttpMessageConverter text/plain 字符串处理
FormHttpMessageConverter application/x-www-form-urlencoded 表单数据处理
ByteArrayHttpMessageConverter application/octet-stream 二进制数据处理

总结

内容协商是 Spring MVC 处理不同格式数据的核心机制,其关键点在于:

  1. HttpMessageConverter 是实现格式转换的核心接口
  2. 内容协商策略决定了如何选择合适的转换器
  3. 注册顺序影响转换器的优先级
  4. @ResponseBody 注解触发自动内容协商

通过自定义 HttpMessageConverter 和内容协商策略,可以轻松支持 JSON、XML、YAML 甚至自定义格式,实现灵活的 API 设计。