请求映射原理
一、是什么 ------ 请求映射到底在做什么?
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 替代了旧的 AntPathMatcher。PathPattern 采用预编译路径树,哪怕遍历匹配性能也大幅提升。
第三步:匹配排序与歧义判定
当有多个匹配时(例如 /users/* 和 /users/{id} 都能匹配 /users/1),MatchComparator 按以下规则排序:
- 精确匹配优先 :
/user/list>/user/{id} - 通配符少优先 :
/user/*>/user/** - 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 |