Spring Boot 从“会用”到“精通”:请求映射原理

请求映射原理

一、是什么 ------ 请求映射到底在做什么?

1. 一句话定义

请求映射(Request Mapping)就是当 HTTP 请求到达时,Spring MVC 根据 URL 路径、请求方式等条件,找到对应的 Controller 方法来处理请求的过程。

2. 用大白话理解

想象一个大型快递分拣中心:

  • 每个包裹(HTTP 请求)上都有地址标签(URL + Method)
  • 分拣系统(HandlerMapping)根据地址标签,把包裹精准投递到对应的快递员(Controller 方法)
  • 如果地址写错了或不存在,包裹被退回(404)

3. 核心角色:DispatcherServlet

在 Spring MVC 中,所有请求的"总调度"是 DispatcherServlet(前端控制器)。它不亲自处理业务,而是像个"交警"一样,把请求引导到正确的地方。


二、为什么 ------ 为什么需要这套映射机制?

1. 解耦 URL 和 Controller

如果没有映射机制,每个 URL 都要写一个 Servlet 来处理------早期 Java Web 开发就是这样,一个 URL 对应一个 Servlet 类,项目一大,Servlet 类爆炸。

Spring MVC 的 @RequestMapping 让我们可以在方法级别声明 URL 映射,一个 Controller 类可以处理多个相关 URL。

2. 灵活的匹配策略

不是简单的字符串 equals,Spring MVC 支持:

  • 精确匹配/user/list
  • 路径变量匹配/user/{id}
  • 通配符匹配/user/**
  • 正则匹配/user/{id:\\d+}

三、怎么做 ------ 请求映射的源码执行流程

1. DispatcherServlet 的继承链

java 复制代码
// HttpServlet ------ Java Servlet API 的根
public abstract class HttpServlet extends GenericServlet { }

// HttpServletBean ------ Spring 对 Servlet 的第一层增强
public abstract class HttpServletBean extends HttpServlet 
    implements EnvironmentCapable, EnvironmentAware { }

// FrameworkServlet ------ 重写 doGet/doPost,统一收口到 processRequest()
public abstract class FrameworkServlet extends HttpServletBean 
    implements ApplicationContextAware { }
    
// DispatcherServlet ------ Spring MVC 的核心调度器
public class DispatcherServlet extends FrameworkServlet { }

2. 从 doGet/doPost 到 doDispatch 的路由

FrameworkServlet 重写了 doGet()doPost()不管什么请求方式,全部收口到同一个方法

java 复制代码
// FrameworkServlet.java
protected final void doGet(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException {
    this.processRequest(request, response);
}

protected final void doPost(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException {
    this.processRequest(request, response);
}

processRequest() 做一些预处理后,调用 doService()doService()DispatcherServlet 中实现,最终调用核心方法 doDispatch()

java 复制代码
// DispatcherServlet.java
protected void doService(HttpServletRequest request, HttpServletResponse response) 
    throws Exception {
    // ... 设置一些 Request 属性 ...
    this.doDispatch(request, response);
}

调用链总结

复制代码
Tomcat 接收请求
  → HttpServlet.doGet/doPost()
    → FrameworkServlet.processRequest()
      → DispatcherServlet.doService()
        → DispatcherServlet.doDispatch()  ← ★ 所有请求的统一入口

3. doDispatch() ------ 请求分发的总指挥

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) 
    throws Exception {
    
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    
    // 第一步:检查是否为文件上传请求
    processedRequest = checkMultipart(request);
    
    // 第二步:★ 获取处理器执行链(核心!)
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);  // 404
        return;
    }
    
    // 第三步:★ 获取处理器适配器
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    
    // 第四步:执行拦截器的 preHandle
    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
        return;
    }
    
    // 第五步:★ 真正执行处理器(适配器调用 Controller 方法)
    ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    // 第六步:执行拦截器的 postHandle
    mappedHandler.applyPostHandle(processedRequest, response, mv);
    
    // 第七步:处理派发结果(视图渲染)
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

4. getHandler() ------ ★ 逐个遍历 HandlerMapping

这是请求映射的核心!Spring 不只有一个 HandlerMapping,而是有多个,每个负责一类请求:

java 复制代码
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;  // 找到就返回!
            }
        }
    }
    return null;
}

遍历顺序(Spring Boot 默认注册的 HandlerMapping):

HandlerMapping 负责处理
RequestMappingHandlerMapping @RequestMapping / @GetMapping 等注解
WelcomePageHandlerMapping 欢迎页(/index.html
BeanNameUrlHandlerMapping Bean 名称以 / 开头的处理器
RouterFunctionMapping 函数式路由(WebFlux 风格)
SimpleUrlHandlerMapping 静态资源 / 其他 URL 直接映射

关键设计 :这是一个责任链模式。只要有一个 HandlerMapping 返回了非 null,后面的就不会再尝试。

5. lookupHandlerMethod() ------ ★ 请求路径匹配的核心算法

RequestMappingHandlerMapping 拿到请求后,调用 lookupHandlerMethod() 进行匹配。这是整个请求映射最精妙的地方:

java 复制代码
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) 
    throws Exception {
    
    List<Match> matches = new ArrayList<>();
    
    // ★ 第一步:精确匹配(O(1),极快)
    List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
    if (directPathMatches != null) {
        this.addMatchingMappings(directPathMatches, matches, request);
    }
    
    // ★ 第二步:如果精确匹配没结果,遍历所有映射(如 /user/{id} 这样的模式)
    if (matches.isEmpty()) {
        this.addMatchingMappings(
            this.mappingRegistry.getRegistrations().keySet(), matches, request
        );
    }
    
    // ★ 第三步:没找到 → 404
    if (matches.isEmpty()) {
        return handleNoMatch(...);
    }
    
    // ★ 第四步:排序,选出最佳匹配
    Match bestMatch = matches.get(0);
    if (matches.size() > 1) {
        Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
        matches.sort(comparator);
        bestMatch = matches.get(0);
        
        // ★ 第五步:歧义检查
        Match secondBestMatch = matches.get(1);
        if (comparator.compare(bestMatch, secondBestMatch) == 0) {
            // 两个匹配完全无法分出高低 → 抛异常
            throw new IllegalStateException(
                "Ambiguous handler methods mapped for '" + request.getRequestURI() + "'"
            );
        }
    }
    
    return bestMatch.getHandlerMethod();
}

逐段分析

第一步:精确匹配(Direct Path Match)
java 复制代码
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);

mappingRegistry 内部维护了一个 Map 结构 (实质是 MultiValueMap),直接用 URL 做 key 去 get,时间复杂度 O(1)

为什么这样做? 实际开发中 80% 以上的请求都是精确路径(如 /api/user/list),优先用 Map 直接查,极大提升性能。

第二步:模式匹配(Pattern Match)

如果精确匹配没结果(比如请求是 /user/123,映射是 /user/{id}),就需要遍历所有已注册的映射,逐个进行模式匹配。

版本补充 :Spring MVC 5.3+ 默认使用 PathPatternParser 替代了旧的 AntPathMatcherPathPattern 采用预编译路径树,哪怕遍历匹配性能也大幅提升。

第三步:匹配排序与歧义判定

当有多个匹配时(例如 /users/*/users/{id} 都能匹配 /users/1),MatchComparator 按以下规则排序:

  1. 精确匹配优先/user/list > /user/{id}
  2. 通配符少优先/user/* > /user/**
  3. HTTP 方法具体优先 :指定了 GET 的映射 > 通配所有方法的映射

如果前两名打分完全一致

java 复制代码
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
    throw new IllegalStateException(
        "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}"
    );
}

这就是**"不能写相同请求路径和相同请求方式的处理器方法"**的底层原因。

注意区分 :大多数"路径冲突"在项目启动时 就会被检测到(mappingRegistry 初始化时就会报 Ambiguous mapping)。运行时这里的歧义检查,更多是处理那些模式不同但在当前请求下打分一致的极端情况(如使用了复杂的 Headers/Produces 匹配条件)。

6. 返回的不是方法,是 HandlerExecutionChain(处理器执行链)

getHandler() 返回的 HandlerExecutionChain 不仅包含目标方法(HandlerMethod),还包含:

复制代码
HandlerExecutionChain
├── handler: HandlerMethod(目标 Controller 方法)
└── interceptorList: List<HandlerInterceptor>(所有匹配的拦截器)

拦截器的 preHandle 会在这里执行,执行失败直接返回,不会进入 Controller。


四、总结流程图

复制代码
HTTP 请求 → Tomcat → Filter 链
    ↓
DispatcherServlet.doDispatch()
    ↓
getHandler() → 遍历 HandlerMapping
    ├── RequestMappingHandlerMapping
    │       ├── 精确匹配(O(1) Map 查找)
    │       ├── 没命中 → 模式匹配(遍历 /user/{id} 等)
    │       ├── 排序(精确 > 通配符少 > 方法具体)
    │       ├── 歧义检查(防止相同路径+方法)
    │       └── 返回 HandlerExecutionChain
    ├── WelcomePageHandlerMapping  →  / → index.html
    └── SimpleUrlHandlerMapping    →  静态资源
    ↓
getHandlerAdapter() → 找到合适适配器
    ↓
执行拦截器 preHandle
    ↓
适配器执行 Controller 方法
    ↓
执行拦截器 postHandle
    ↓
processDispatchResult(视图渲染 / 响应写回)

五、核心要点总结

核心点 说明
总入口 DispatcherServlet.doDispatch()
映射查找 遍历 handlerMappings 列表,责任链模式
匹配策略 先精确匹配(O(1)),再模式匹配(遍历)
路径变量 /user/{id} 匹配 /user/123,id = 123
歧义保护 两个匹配打分为 0 时抛异常
返回结构 HandlerExecutionChain = HandlerMethod + Interceptors
相关推荐
杨运交1 小时前
[028][缓存模块]命名缓存:多级个性化缓存配置的设计与实现
spring boot
MariaH1 小时前
Node-fs模块
后端
李白的天不白1 小时前
vim /etc/nginx/conf.d/default.conf
spring
峰子20121 小时前
PG 管控系统技术方案
数据库·后端·pg
阿文的代码库1 小时前
干货分享|C++运算符重载知识点
java·c++·算法
码不停蹄的玄黓1 小时前
Java 实现阻塞队列
java·开发语言
SunnyDays10111 小时前
Java 实现 PDF 转 PDF/A 和 PDF/A 转 PDF(超详细教程)
java·开发语言·pdf
muddjsv1 小时前
Java语言学习路线全解析:从入门到精通的核心模块与进阶路径
java
未若君雅裁1 小时前
线程池核心参数与执行流程
java·开发语言