news 2026/10/16 3:45:34

Spring MVC HandlerMapping 源码深度解析:路由机制与自定义实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring MVC HandlerMapping 源码深度解析:路由机制与自定义实现

1. 从一次请求分发异常说起:为什么我要啃 HandlerMapping 源码

前段时间帮一个朋友排查线上问题,现象很典型:他自定义了一个注解,想通过注解把某些请求路由到特定的处理方法上,结果发现请求进来之后,要么 404,要么被路由到了一个完全不相干的 Controller 方法上。他一开始怀疑是注解没生效,后来又怀疑是包扫描路径不对,折腾了大半天,最后定位到问题出在请求映射的注册和查找环节——也就是HandlerMapping这条链路上。

这件事让我意识到,很多人对 Spring MVC 的理解停留在“写个@RequestMapping就能用”的层面,一旦路由行为不符合预期,就完全不知道从哪下手。而HandlerMapping恰恰是整个请求分发流程的“第一道关卡”:它决定了进来的这个 HTTP 请求,到底该交给哪个处理器方法去执行。你写的每一个@GetMapping、@PostMapping,最终都要经过它的手。

所以这篇内容,我想把HandlerMapping接口的源码从头到尾拆一遍。不是那种贴一段源码翻译一段注释的流水账,而是按照“它要解决什么问题、接口怎么设计的、核心实现类怎么干的、实际用的时候哪里容易踩坑”这个思路来讲。适合已经会用 Spring MVC、但对底层路由机制一知半解的开发者,也适合想自己扩展路由规则、写自定义 HandlerMapping 的同学。看完之后,你至少能做到两件事:第一,遇到路由相关的问题知道往哪个方向查;第二,有能力自己写一个能跑起来的自定义 HandlerMapping。

2. HandlerMapping 接口到底在抽象什么

2.1 接口定义里藏着的设计意图

先看接口本身。HandlerMapping的定义非常简洁,核心就一个方法:

public interface HandlerMapping { HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; }

就这么一个方法。但恰恰是这种极简的接口,背后藏着 Spring 一贯的设计哲学:把“请求到处理器的映射”这件事抽象成一个单一职责,至于你怎么映射、映射的规则是什么、映射的数据从哪来,接口一概不管。

这里有个关键点很多人会忽略:返回的不是HandlerMethod,而是HandlerExecutionChain。这个区别很重要。HandlerExecutionChain里面包了两样东西——一个是真正的处理器(handler),另一个是拦截器列表(interceptorList)。也就是说,HandlerMapping的职责不只是“找到谁来处理”,还包括“在处理前后要经过哪些拦截器”。这就解释了为什么拦截器的顺序、拦截器的匹配路径,本质上也是由 HandlerMapping 决定的。

我个人的理解是,这个设计把“路由”和“拦截”这两件本来耦合的事情,通过一个返回值统一了起来。你在配置拦截器的时候写的addPathPatterns,最终就是在这个环节被解析并组装进执行链的。

2.2 抽象类 AbstractHandlerMapping 补了哪些能力

光有接口没法直接用,Spring 提供了一个抽象基类AbstractHandlerMapping,把通用的逻辑都实现好了。它主要补了这么几块能力:

第一块是拦截器的管理。它内部维护了一个List<HandlerInterceptor>,并且提供了setInterceptors方法。在getHandler方法里,它会先调用子类实现的getHandlerInternal拿到处理器,然后如果处理器不为空,就把所有匹配的拦截器组装成HandlerExecutionChain返回。

第二块是默认的处理器兜底。它有一个defaultHandler属性,当子类找不到任何匹配的处理器时,可以返回这个默认处理器。这个机制在静态资源处理、欢迎页处理里用得比较多。

第三块是 CORS 配置的集成。跨域相关的CorsConfiguration也是在这一层被织入执行链的,这也是为什么你配置了跨域之后,不需要在每个 Controller 里单独处理。

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); if (handler == null) { handler = getDefaultHandler(); } if (handler == null) { return null; } // 如果是字符串,说明是 bean 名称,需要从容器里取 if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } // 组装执行链,织入拦截器 HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); // CORS 相关处理 // ... return executionChain; }

注意这里getHandler被声明成了final,意味着子类不能覆盖整个流程,只能通过实现getHandlerInternal来定制“怎么找处理器”这一步。这种模板方法模式在 Spring 里到处都是,好处是流程稳定、扩展点清晰。

2.3 getHandlerInternal 才是子类的真正战场

所有具体的 HandlerMapping 实现,核心工作都集中在getHandlerInternal这个方法上。它的签名是:

protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;

返回类型是Object,不是HandlerMethod。为什么这么设计?因为处理器的类型可以很多样:可以是一个HandlerMethod(最常见的 Controller 方法),可以是一个HttpRequestHandler对象,可以是一个Servlet,甚至可以是任意一个被包装过的对象。这种宽松的返回类型,让 HandlerMapping 的适用范围远远超出了注解式 Controller。

理解了这一点,你就能明白为什么 Spring MVC 里会有那么多种 HandlerMapping 实现,它们本质上都是在用不同的策略回答同一个问题:这个请求该由谁来处理。

3. 三大核心实现类的分工与源码细节

3.1 RequestMappingHandlerMapping:注解式路由的主力

这是日常开发中最重要的一个实现类,负责处理@RequestMapping及其衍生注解(@GetMapping、@PostMapping等)。它的核心逻辑分两个阶段:启动时的注册阶段,和运行时的查找阶段。

注册阶段发生在容器启动时。它会扫描所有 Bean,找出带有@Controller或@RequestMapping的类,然后解析类级别和方法级别的注解,把每一个“请求条件 + 处理方法”的组合注册到一个MappingRegistry里。这个注册过程的核心方法是registerHandlerMethod,它会为每个方法创建一个RequestMappingInfo,然后存进一个多值映射结构。

这里有个细节值得说:MappingRegistry内部用了多个 Map 来存储映射关系,包括按路径直接匹配的、按类型匹配的等等。这种多级索引的设计,是为了在查找时能快速缩小范围,而不是每次都遍历所有注册的方法。

查找阶段就是getHandlerInternal被调用的时候。它会根据请求的 URL、方法、参数、头信息等,构造一个RequestInfo,然后去注册表里找匹配的RequestMappingInfo。匹配的过程涉及路径匹配、条件匹配等多个环节,最终返回一个HandlerMethod。

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { String lookupPath = initLookupPath(request); this.mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } finally { this.mappingRegistry.releaseReadLock(); } }

注意这里的读写锁。因为注册表在启动后基本是只读的,但理论上支持动态注册,所以用了读写锁来保证并发安全。这个细节在你自己写 HandlerMapping 时值得借鉴。

3.2 SimpleUrlHandlerMapping:最直白的 URL 映射

如果说RequestMappingHandlerMapping是“智能路由”,那SimpleUrlHandlerMapping就是“手动挡”。它不解析任何注解,而是直接维护一个 URL 到处理器的映射表,你配置什么它就映射什么。

它的getHandlerInternal实现非常直接:拿请求路径去 Map 里查,查到了就返回,查不到就返回 null。路径匹配支持 Ant 风格的通配符,也支持精确匹配。

protected Object getHandlerInternal(HttpServletRequest request) throws Exception { String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); Object handler = lookupHandler(lookupPath, request); if (handler == null) { // 尝试用默认处理器 Object rawHandler = null; if ("/".equals(lookupPath)) { rawHandler = getRootHandler(); } if (rawHandler == null) { rawHandler = getDefaultHandler(); } // ... } return handler; }

这个类在实际项目里用得不多,但在处理静态资源、做简单的 URL 转发时非常方便。比如你想把/old-api/**全部转发到某个统一的处理器上,用它就比写一堆 Controller 方法清爽得多。

3.3 BeanNameUrlHandlerMapping:按 Bean 名称映射

这个实现类的逻辑是:如果一个 Bean 的名字是以斜杠开头的,就把它当作一个 URL 路径,注册为处理器。比如你定义了一个 Bean 叫/myHandler,那访问/myHandler就会路由到它。

它的查找逻辑就是拿请求路径去容器里找同名的 Bean。这个机制在早期的 Spring MVC 里用得比较多,现在基本被注解式取代了。但理解它有助于你明白一件事:HandlerMapping 的“映射依据”可以非常灵活,可以是注解、可以是配置、可以是 Bean 名称,甚至可以是数据库里的数据。

3.4 三者的优先级与协作关系

在默认配置下,Spring MVC 会注册多个 HandlerMapping,它们是有顺序的。RequestMappingHandlerMapping的 order 是 0,SimpleUrlHandlerMapping和BeanNameUrlHandlerMapping的 order 通常是更大的值。查找时按 order 从小到大依次尝试,谁先返回非 null 的处理器就用谁。

这个顺序很重要。如果你自定义了一个 HandlerMapping 但没设置 order,它可能被排在很后面,导致永远轮不到它。我踩过这个坑:写了个自定义映射想拦截特定前缀的请求,结果因为 order 太大,请求先被RequestMappingHandlerMapping匹配走了。

实现类映射依据默认 order典型场景
RequestMappingHandlerMapping注解0常规 Controller
SimpleUrlHandlerMapping显式配置较大静态资源、URL 转发
BeanNameUrlHandlerMappingBean 名称较大早期风格、简单映射
自定义 HandlerMapping自定义需手动设置特殊路由需求

4. 请求查找的完整链路:一次 getHandler 调用经历了什么

4.1 从 DispatcherServlet 到 HandlerMapping

请求进入DispatcherServlet的doDispatch方法后,第一件事就是调用getHandler(request)。这个方法会遍历容器里所有的 HandlerMapping,按 order 顺序逐个调用它们的getHandler。

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; }

这段代码看起来简单,但有个关键点:只要有一个 HandlerMapping 返回了非 null 的执行链,循环就结束了。这意味着 HandlerMapping 之间的匹配是“短路”的,前面的匹配上了,后面的就没机会了。这也是为什么 order 设置错误会导致自定义映射失效。

4.2 路径匹配的细节:Ant 风格与 PathPattern

在RequestMappingHandlerMapping内部,路径匹配经历了从 AntPathMatcher 到 PathPattern 的演进。早期版本用的是AntPathMatcher,它支持**、*、?等通配符,但性能一般,而且有些边界情况处理得不够好。

后来引入了PathPattern,它是预解析的,性能更好,语义也更清晰。比如/**和/*的区别,在 PathPattern 里定义得更明确。默认情况下,Spring Boot 2.6 之后已经切换到 PathPattern 了。

这个变化对实际开发的影响是:某些以前能匹配上的路径,现在可能匹配不上了。比如/**/foo这种模式,在 Ant 风格下能匹配多层路径,在 PathPattern 下行为可能不同。如果你从老版本升级上来发现路由 404,很可能就是这里的问题。

4.3 条件匹配:不只是看路径

RequestMappingInfo里封装的条件远不止路径。它还包括:

  • 请求方法(GET、POST 等)
  • 请求参数(params 条件)
  • 请求头(headers 条件)
  • 消费的媒体类型(consumes)
  • 生产的媒体类型(produces)

查找时,这些条件会逐一比对。任何一个不满足,这个映射就不会被选中。这就解释了为什么同一个路径可以对应多个方法:只要它们的请求方法或参数条件不同就行。

// 这两个方法路径相同,但通过请求方法区分 @GetMapping("/user") public User getUser() { ... } @PostMapping("/user") public User createUser() { ... }

如果两个方法的条件完全一样,启动时就会报错,提示存在歧义的映射。这个检查是在注册阶段做的,不是运行时才发现的。

4.4 匹配到多个时的排序规则

有时候一个请求可能同时满足多个映射的条件,比如一个精确路径的映射和一个通配符路径的映射。这时候 Spring 会按照一定的规则选出“最优”的那个。

排序的依据包括:路径的精确程度、通配符的数量、条件的多少等。精确路径优先于通配符路径,条件多的优先于条件少的。这个排序逻辑在RequestMappingInfo的compareTo方法里实现。

我遇到过一个案例:两个 Controller 分别映射了/api/user和/api/**,访问/api/user时,理论上应该匹配第一个,但实际却匹配到了第二个。排查后发现是其中一个映射多了一个params条件,导致排序结果和预期不一致。这种问题不看源码很难想明白。

5. 自己写一个 HandlerMapping:从需求到落地

5.1 什么场景下需要自定义

大部分情况下你不需要自定义 HandlerMapping,注解式路由已经够用了。但有几类场景,自己写一个会更优雅:

第一类是路由规则来自外部配置或数据库,而不是代码里的注解。比如多租户系统里,不同租户的路由规则可能不同,这时候用注解就不合适了。

第二类是需要对路由做特殊处理,比如根据请求头里的某个字段动态决定处理器,或者实现灰度发布的路由逻辑。

第三类是要接管某些特定前缀的请求,做统一的预处理或转发。

5.2 继承 AbstractHandlerMapping 的最小实现

自定义 HandlerMapping 最省事的方式是继承AbstractHandlerMapping,只需要实现getHandlerInternal方法。

public class CustomHandlerMapping extends AbstractHandlerMapping { private final Map<String, Object> handlerMap = new ConcurrentHashMap<>(); public void registerHandler(String path, Object handler) { handlerMap.put(path, handler); } @Override protected Object getHandlerInternal(HttpServletRequest request) throws Exception { String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); return handlerMap.get(lookupPath); } }

就这么几行,一个能用的自定义 HandlerMapping 就出来了。AbstractHandlerMapping会帮你处理拦截器组装、默认处理器兜底、CORS 等逻辑。

5.3 注册到容器并设置正确的 order

写完之后要注册到 Spring 容器里。如果你用 Spring Boot,可以通过配置类:

@Configuration public class CustomMappingConfig { @Bean public CustomHandlerMapping customHandlerMapping() { CustomHandlerMapping mapping = new CustomHandlerMapping(); mapping.setOrder(-1); // 关键:设置比默认更小的 order mapping.registerHandler("/custom/hello", new CustomHelloHandler()); return mapping; } }

这里的setOrder(-1)是关键。如果不设置,默认 order 是Ordered.LOWEST_PRECEDENCE,会排在所有内置映射后面,基本轮不到它。设置成 -1 让它排在RequestMappingHandlerMapping前面,这样特定路径的请求会先被它拦截。

注意:order 设置得太小会拦截所有请求,包括那些本该由注解式 Controller 处理的。所以自定义映射的路径要尽量精确,避免用/**这种宽泛的模式。

5.4 处理器的返回值与执行链

getHandlerInternal返回的对象,最终会被包装进HandlerExecutionChain。如果返回的是一个普通的 Java 对象,Spring 需要知道怎么执行它。这时候通常需要配合一个HandlerAdapter来使用。

如果你返回的是HandlerMethod,那RequestMappingHandlerAdapter就能处理。如果返回的是自定义对象,你得自己写一个对应的HandlerAdapter,告诉 Spring 怎么调用它、怎么处理返回值。

public class CustomHelloHandler { public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException { response.getWriter().write("hello from custom handler"); } }

对应的 HandlerAdapter 需要实现supports和handle方法。这部分内容展开又是一大篇,这里先点到为止,核心是让你知道:HandlerMapping 和 HandlerAdapter 是配套的,前者负责“找”,后者负责“执行”。

6. 那些年我在路由上踩过的坑

6.1 拦截器不生效:可能是 HandlerMapping 没匹配上

有一次配置了一个拦截器,想拦截/api/**的所有请求,结果发现部分请求没被拦截。排查后发现,那些没被拦截的请求走的是SimpleUrlHandlerMapping(静态资源处理),而拦截器是配在RequestMappingHandlerMapping上的。

这个坑的本质是:拦截器是绑定在 HandlerMapping 上的,不同的 HandlerMapping 有各自的拦截器列表。你在 WebMvcConfigurer 里加的拦截器,默认只加到RequestMappingHandlerMapping上。如果请求被其他 HandlerMapping 处理了,拦截器自然不生效。

解决办法是要么把静态资源也纳入注解式路由的管理,要么给对应的 HandlerMapping 单独配置拦截器。

6.2 路径匹配的优先级陷阱

前面提到过,/api/user和/api/**同时存在时,匹配结果可能和直觉不符。更隐蔽的一种情况是:两个不同 Controller 里的方法,路径模式有重叠,但因为你没意识到排序规则,导致请求被路由到了错误的方法。

排查这类问题的技巧是:打开 Spring MVC 的 debug 日志,它会打印出所有注册的映射以及匹配过程。看到实际的匹配顺序,问题就一目了然了。

logging.level.org.springframework.web.servlet.handler=DEBUG logging.level.org.springframework.web.servlet.mvc.method=DEBUG

6.3 自定义 HandlerMapping 返回 null 的连锁反应

自定义 HandlerMapping 如果返回 null,Spring 会继续尝试下一个 HandlerMapping。这本身没问题,但如果你在getHandlerInternal里抛了异常,整个请求就会直接失败,后面的 HandlerMapping 根本没机会尝试。

所以自定义实现里要做好异常处理,拿不准的情况下宁可返回 null,让后续的映射有机会处理,也不要让异常冒泡出去。

6.4 动态注册映射的并发问题

有些场景需要在运行时动态添加或删除映射,比如多租户系统里租户配置变化时。这时候要注意MappingRegistry的并发控制。前面看到它用了读写锁,但如果你直接操作内部的 Map,绕过了锁,就可能出现并发问题。

正确的做法是通过registerMapping和unregisterMapping这些官方提供的方法来操作,它们内部会处理好锁和索引更新。

坑点现象根因解决思路
拦截器不生效部分请求绕过拦截器拦截器绑定在特定 HandlerMapping 上确认请求由哪个映射处理,补配拦截器
路径优先级异常请求路由到非预期方法映射排序规则与直觉不符开 debug 日志看匹配顺序
自定义映射失效自定义逻辑从不执行order 太大,被内置映射抢先设置更小的 order
动态注册并发异常偶发的映射丢失或错乱绕过锁直接操作内部结构使用官方注册方法

7. 从源码里能学到的设计思路

拆完HandlerMapping这条链路,有几个设计思路我觉得值得单独拎出来说。

第一个是“单一职责 + 模板方法”的组合。接口只定义一件事,抽象类固定流程,子类只填核心逻辑。这种结构让扩展变得非常可控,你不用担心改坏框架的流程,只需要关注自己的那部分。

第二个是“多级索引”的思想。MappingRegistry不是简单地把所有映射塞进一个 List 里遍历,而是建了多个索引来加速查找。这种思路在你写任何需要频繁查找的数据结构时都用得上。

第三个是“顺序即优先级”的显式化。通过 order 属性把优先级这件事摆到明面上,而不是藏在代码逻辑里。这样调试的时候,你只要看 order 值就能推断出匹配顺序,大大降低了排查成本。

第四个是“返回值承载上下文”的做法。HandlerExecutionChain不只是返回一个处理器,还把拦截器、CORS 配置都打包进去了。这种设计减少了方法调用次数,也让调用方拿到的是一个完整的执行上下文,而不是零散的信息。

这些思路不限于 Spring MVC,在你设计自己的框架或中间件时,都可以借鉴。尤其是当你需要处理“多种策略、按优先级选择”这类问题时,HandlerMapping 的模式几乎可以直接套用。

最后分享一个我自己的习惯:每次遇到路由相关的问题,第一反应不是去改代码,而是先把 debug 日志打开,看清楚请求到底经过了哪些 HandlerMapping、最终被哪个映射处理了。大部分时候,日志看完问题就清楚了一半。源码不用背,但这条链路的走向心里要有数,排查起来才不会像无头苍蝇。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 3:45:23

Python文件读取:read()、readline()、readlines()怎么选才不爆内存?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 3:45:12

基于Omron NX与EtherCAT的电池焊接检测机控制系统设计

做电池焊接检测机之前&#xff0c;我一直用的是传统中小型PLC加一堆独立模块的方案。直到上一个项目交期被压到极限&#xff0c;甲方还提了一堆“要数据追溯”“要快速换型”“要和MES对接”的需求&#xff0c;我才认真把Omron NX这套控制器从里到外捋了一遍。做完这个项目之后…

作者头像 李华
网站建设 2026/10/16 3:44:44

ICMP协议从原理到实战:报文结构、核心机制与排障技巧

开篇聊几句&#xff1a;做网络这行&#xff0c;ICMP 可能是你最早接触、却最容易低估的一个协议。说接触早&#xff0c;是因为刚学会用电脑时&#xff0c;就有人告诉你"ping 一下试试"&#xff0c;第一次看到 "Reply from 192.168.1.1: bytes32 time1ms TTL64&q…

作者头像 李华
网站建设 2026/10/16 3:44:31

一条HTTP请求的旅程:从封装到解封装的底层原理与抓包实战

开写之前先说个感受&#xff1a;很多后端、客户端开发&#xff0c;常年工作在应用层&#xff0c;天天调接口、发请求、接回调&#xff0c;可真要问他“一条HTTP请求从你按下回车到对面服务器返回数据&#xff0c;这一路上数据到底经历了哪些处理”&#xff0c;能一口气讲清楚的…

作者头像 李华
网站建设 2026/10/16 3:43:14

小程序 TextEncoder 缺失怎么办?手写轻量 polyfill 与避坑指南

如果你在微信小程序里跑过 WebSocket 消息处理、文件上传下载或者二维码生成这类逻辑&#xff0c;十有八九遇到过这样的红字&#xff1a;ReferenceError: TextEncoder is not defined。再或者&#xff0c;你在工具里写得好好的 UTF-8 编解码代码&#xff0c;一上真机就白屏。这…

作者头像 李华
网站建设 2026/10/16 3:43:12

数据包如何从你的电脑抵达服务器?网络通信全链路解析

你有没有想过&#xff0c;当你打开某个网站&#xff0c;按下回车键之后&#xff0c;那一个字节一个字节的数据&#xff0c;究竟是怎么从你的电脑跑到几百公里外的服务器上的&#xff1f;这中间经过了哪些设备、哪些处理环节&#xff0c;每一步又是在干什么&#xff1f;说实话&a…

作者头像 李华