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

    • 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章 扩展与异步机制

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

请求映射原理

本章是"控制器与请求映射"板块的内核章节。前一章你已经学会了如何用 @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 源码逻辑):

  1. 路径匹配(Patterns):先按 URL 路径筛选候选集。支持精确路径、? 和 * 通配、以及 ** 多级通配
  2. 方法匹配(Methods):检查 @RequestMapping(method = ...) 是否与请求的 HTTP 方法一致
  3. Consumes 匹配:检查请求的 Content-Type 是否满足 @RequestMapping(consumes = ...)
  4. Produces 匹配:检查请求的 Accept 头是否满足 @RequestMapping(produces = ...)
  5. 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 方法ProducesHeaders目标方法
1/api/v1/inventory/items/{itemId}GETapplication/json-getItemById
2/api/v1/inventory/items/*GET--getItemsByPattern
3/api/v1/inventory/itemsGET--listItems
4/api/v1/inventory/itemsGET-X-API-VERSION=2listItemsV2

请求匹配实战

场景 A:黄俪前端调用 GET /api/v1/inventory/items/ABC-123

  1. URL 精确匹配:路径 /api/v1/inventory/items/{itemId} 匹配成功(ABC-123 绑定到 {itemId})
  2. HTTP 方法匹配:GET 符合
  3. Produces 匹配:前端请求默认 Accept: */*,满足 application/json
  4. 最终选中:getItemById 方法

场景 B:黄俪调用 GET /api/v1/inventory/items/ABC-123,但请求头带了 Accept: text/html

  1. 路径和方法都匹配 getItemById
  2. Produces 检查:text/html 不满足 application/json
  3. 继续检查其他候选:/items/* 也匹配路径 ABC-123
  4. getItemsByPattern 没有 Produces 限制,匹配成功
  5. 最终选中:getItemsByPattern 方法

场景 C:黄俪调用 GET /api/v1/inventory/items?page=0&size=10,请求头 X-API-VERSION=2

  1. 路径 /items 同时匹配注册项 3 和 4
  2. 注册项 4 多了 Headers 条件,按"更多条件优先"规则胜出
  3. 最终选中: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 匹配优先级是什么?

标准回答:

  1. 路径维度:精确路径 > 带 {变量} 的路径 > 带 * 的通配路径 > ** 多级通配
  2. 条件维度:指定了更多匹配条件(produces/consumes/headers/params)的方法优先
  3. 比较器链:Spring 内部通过 RequestMappingInfo.compareTo 按 patterns → params → headers → consumes → produces → methods 的顺序逐层比较
  4. 启动期校验:如果两个映射在所有维度上完全等价,Spring 会在启动时抛出 IllegalStateException: Ambiguous mapping,而不是在运行时随机选择

小结

请求映射不是简单的"字符串比对",而是一个多维度、分阶段、有优先级的精密匹配系统。理解映射表的建立过程(启动期扫描 → 元数据提取 → 注册到 MappingRegistry),理解匹配算法的执行顺序(路径 → 方法 → Consumes → Produces → Params/Headers),以及冲突解决的比较器链规则,是排查一切路由问题的根本能力。

本章与全局的关系:本章揭示了"请求如何找到 Controller 方法"的底层机制。下一章"GetMapping / PostMapping 等注解"将聚焦单个注解的语法细节和组合技巧,而"请求参数获取与转换"板块将讲解匹配成功后的参数绑定流程——即 HandlerMethod 被确定后,Spring 如何把 HTTP 请求中的零散数据组装成方法参数。

上一页
RESTful