请求映射原理
本章是"控制器与请求映射"板块的内核章节。前一章你已经学会了如何用
@RequestMapping、@GetMapping等注解声明 URL 映射,但声明只是表面,匹配才是本质。如果不理解 HandlerMapping 如何在运行时从成千上万个候选方法中精准定位唯一目标,遇到 404 时你只能盲目猜测。本章将揭开映射表的建立过程、匹配算法的优先级规则,以及冲突解决的底层逻辑,为后续学习拦截器、参数绑定和异常处理奠定路由层面的认知基础。
定义与作用
请求映射(Request Mapping) 的本质是 Spring MVC 在应用启动阶段建立的一张"URL → 处理器方法"的查找表,以及在请求到达阶段执行的匹配算法。
你可以把它理解为公司前台的智能导引系统:
- 启动时:行政人员把所有部门的门牌号、接待规则录入系统(扫描
@Controller→ 解析注解 → 注册映射) - 运行时:访客报出姓名和事由,系统按规则匹配最合适的部门(精确匹配 → 通配匹配 → 正则匹配)
- 冲突时:如果两个部门都声称能接待,系统按优先级裁定(最长路径优先、显式映射优先)
HandlerMapping 接口的多个实现类(如 RequestMappingHandlerMapping)共同承担了这一职责。Spring Boot 2.x 默认使用的正是 RequestMappingHandlerMapping。
核心原理
映射表的建立过程
应用启动时,Spring 容器会执行一次全量扫描,将散落在各个 @Controller 类中的映射信息收集到一张全局表中。这张表不是简单的 HashMap<String, HandlerMethod>,而是一个支持多维度匹配的复杂结构。
关键数据结构:
// org.springframework.web.servlet.mvc.method.RequestMappingInfo
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
private final PatternsRequestCondition patterns; // URL 路径
private final RequestMethodsRequestCondition methods; // HTTP 方法
private final ConsumesRequestCondition consumes; // Content-Type
private final ProducesRequestCondition produces; // Accept
private final ParamsRequestCondition params; // 参数条件
private final HeadersRequestCondition headers; // 头条件
}
映射表的核心是 MappingRegistry,内部维护了两个关键集合:
mappingLookup:Map<RequestMappingInfo, HandlerMethod>,用于按映射信息查找方法urlLookup:Map<String, List<RequestMappingInfo>>,用于按 URL 快速定位候选集
URL 匹配优先级流程
当一个 HTTP 请求到达 DispatcherServlet 时,匹配算法按以下优先级逐层筛选:
匹配维度的执行顺序(Spring 5.x 源码逻辑):
- 路径匹配(Patterns):先按 URL 路径筛选候选集。支持精确路径、
?和*通配、以及**多级通配 - 方法匹配(Methods):检查
@RequestMapping(method = ...)是否与请求的 HTTP 方法一致 - Consumes 匹配:检查请求的
Content-Type是否满足@RequestMapping(consumes = ...) - Produces 匹配:检查请求的
Accept头是否满足@RequestMapping(produces = ...) - Params / Headers 匹配:检查请求参数和请求头是否满足条件注解
冲突解决:当多个方法都匹配时
如果经过上述筛选后仍有多个方法符合条件,Spring 会启动比较器链进行排序,规则如下:
| 比较规则 | 说明 | 示例 |
|---|---|---|
| 最长路径优先 | 路径段数越多、通配符越少,优先级越高 | /users/{id} 优于 /users/* |
| 显式映射优先于通配 | 不含通配符的路径优于含通配符的 | /users/123 优于 /users/{id} |
| 更多条件优先 | 指定了更多匹配条件的方法优先 | @GetMapping("/users", produces = "application/json") 优于 @GetMapping("/users") |
| 方法参数数量 | 参数更多的方法优先(较少使用) | 带 @PathVariable 的方法优先 |
源码层面的比较器链:
// RequestMappingInfo 的 compareTo 方法逻辑
public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
int result = patterns.compareTo(other.patterns, request); // 路径比较
if (result != 0) return result;
result = params.compareTo(other.params, request); // 参数比较
if (result != 0) return result;
result = headers.compareTo(other.headers, request); // 头比较
if (result != 0) return result;
result = consumes.compareTo(other.consumes, request); // Consumes 比较
if (result != 0) return result;
result = produces.compareTo(other.produces, request); // Produces 比较
if (result != 0) return result;
result = methods.compareTo(other.methods, request); // 方法比较
if (result != 0) return result;
return 0; // 完全等价,会抛异常
}
完整示例
场景
飞翔科技正在开发一套智能仓储管理系统。白歌作为架构师,要求所有 RESTful 接口的 URL 设计必须遵循统一规范。小崔在实现"库存查询"模块时,写了如下 Controller:
@RestController
@RequestMapping("/api/v1/inventory")
public class InventoryController {
// 1. 精确路径 + 显式方法 + Produces 限定
@GetMapping(value = "/items/{itemId}", produces = "application/json")
public ItemDetail getItemById(@PathVariable String itemId) {
return inventoryService.findById(itemId);
}
// 2. 通配路径(用于批量查询)
@GetMapping("/items/*")
public List<ItemSummary> getItemsByPattern(@PathVariable String pattern) {
return inventoryService.findByPattern(pattern);
}
// 3. 根路径(兜底查询)
@GetMapping("/items")
public Page<ItemSummary> listItems(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return inventoryService.list(page, size);
}
// 4. 条件映射:仅当请求头 X-API-VERSION=2 时生效
@GetMapping(value = "/items", headers = "X-API-VERSION=2")
public List<ItemDetail> listItemsV2() {
return inventoryService.listAllDetails();
}
}
映射表建立后的内部视图
应用启动完成后,RequestMappingHandlerMapping 内部的映射表大致如下:
| 注册顺序 | URL 模式 | HTTP 方法 | Produces | Headers | 目标方法 |
|---|---|---|---|---|---|
| 1 | /api/v1/inventory/items/{itemId} | GET | application/json | - | getItemById |
| 2 | /api/v1/inventory/items/* | GET | - | - | getItemsByPattern |
| 3 | /api/v1/inventory/items | GET | - | - | listItems |
| 4 | /api/v1/inventory/items | GET | - | X-API-VERSION=2 | listItemsV2 |
请求匹配实战
场景 A:黄俪前端调用 GET /api/v1/inventory/items/ABC-123
- URL 精确匹配:路径
/api/v1/inventory/items/{itemId}匹配成功(ABC-123绑定到{itemId}) - HTTP 方法匹配:GET 符合
- Produces 匹配:前端请求默认
Accept: */*,满足application/json - 最终选中:
getItemById方法
场景 B:黄俪调用 GET /api/v1/inventory/items/ABC-123,但请求头带了 Accept: text/html
- 路径和方法都匹配
getItemById - Produces 检查:
text/html不满足application/json - 继续检查其他候选:
/items/*也匹配路径ABC-123 getItemsByPattern没有 Produces 限制,匹配成功- 最终选中:
getItemsByPattern方法
场景 C:黄俪调用 GET /api/v1/inventory/items?page=0&size=10,请求头 X-API-VERSION=2
- 路径
/items同时匹配注册项 3 和 4 - 注册项 4 多了
Headers条件,按"更多条件优先"规则胜出 - 最终选中:
listItemsV2方法
场景 D:黄俪调用 GET /api/v1/inventory/items/ABC-123,此时小崔又加了一个方法:
@GetMapping("/items/{itemId}")
public ItemDetail getItemByIdV2(@PathVariable String itemId) { ... }
启动时直接抛出异常:
java.lang.IllegalStateException: Ambiguous mapping.
Cannot map 'inventoryController' method
...getItemByIdV2 to {GET /api/v1/inventory/items/{itemId}}:
There is already 'inventoryController' bean method
...getItemById mapped.
白歌 review 时指出:两个方法的路径、方法、Consumes、Produces、Params、Headers 完全相同,Spring 无法裁决,必须在启动期就拒绝这种模糊映射。
易错场景与面试考点
误区一:404 就是"URL 写错了"
错误认知:看到 404 就检查 @RequestMapping 的路径字符串是否拼写错误。
纠正:404 只是"找不到 Handler"的统称,实际原因可能发生在匹配的任何一个维度。李眉在运维时总结了一张排查决策树:
真实案例:小崔曾遇到前端报告 POST /api/v1/inventory/items 返回 404。排查发现 Controller 上写的是 @PostMapping(value = "/items", consumes = "application/json"),而前端测试时用了 Content-Type: text/plain。表面是 404,实际是 Consumes 不匹配导致候选被过滤,最终没有可用 Handler。
误区二:通配符可以随便用
错误认知:/** 能匹配所有请求,放在 Controller 里做兜底很方便。
纠正:/** 通配会吞噬所有本应匹配到更精确路径的请求。白歌在架构评审中明确禁止在业务 Controller 中使用 /**,只允许在静态资源处理或专门的 fallback Controller 中使用。
// ❌ 错误:这个方法的优先级问题会导致意外行为
@GetMapping("/**")
public String catchAll() { ... }
// ✅ 正确:静态资源或专门的错误处理才使用宽泛匹配
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/");
}
}
面试高频:Spring MVC 的 URL 匹配优先级是什么?
标准回答:
- 路径维度:精确路径 > 带
{变量}的路径 > 带*的通配路径 >**多级通配 - 条件维度:指定了更多匹配条件(produces/consumes/headers/params)的方法优先
- 比较器链:Spring 内部通过
RequestMappingInfo.compareTo按 patterns → params → headers → consumes → produces → methods 的顺序逐层比较 - 启动期校验:如果两个映射在所有维度上完全等价,Spring 会在启动时抛出
IllegalStateException: Ambiguous mapping,而不是在运行时随机选择
小结
请求映射不是简单的"字符串比对",而是一个多维度、分阶段、有优先级的精密匹配系统。理解映射表的建立过程(启动期扫描 → 元数据提取 → 注册到 MappingRegistry),理解匹配算法的执行顺序(路径 → 方法 → Consumes → Produces → Params/Headers),以及冲突解决的比较器链规则,是排查一切路由问题的根本能力。
本章与全局的关系:本章揭示了"请求如何找到 Controller 方法"的底层机制。下一章"GetMapping / PostMapping 等注解"将聚焦单个注解的语法细节和组合技巧,而"请求参数获取与转换"板块将讲解匹配成功后的参数绑定流程——即 HandlerMethod 被确定后,Spring 如何把 HTTP 请求中的零散数据组装成方法参数。