
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把通用的逻辑都实现好了。它主要补了这么几块能力第一块是拦截器的管理。它内部维护了一个ListHandlerInterceptor并且提供了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 是 0SimpleUrlHandlerMapping和BeanNameUrlHandlerMapping的 order 通常是更大的值。查找时按 order 从小到大依次尝试谁先返回非 null 的处理器就用谁。这个顺序很重要。如果你自定义了一个 HandlerMapping 但没设置 order它可能被排在很后面导致永远轮不到它。我踩过这个坑写了个自定义映射想拦截特定前缀的请求结果因为 order 太大请求先被RequestMappingHandlerMapping匹配走了。实现类映射依据默认 order典型场景RequestMappingHandlerMapping注解0常规 ControllerSimpleUrlHandlerMapping显式配置较大静态资源、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 MapString, 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.handlerDEBUG logging.level.org.springframework.web.servlet.mvc.methodDEBUG6.3 自定义 HandlerMapping 返回 null 的连锁反应自定义 HandlerMapping 如果返回 nullSpring 会继续尝试下一个 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、最终被哪个映射处理了。大部分时候日志看完问题就清楚了一半。源码不用背但这条链路的走向心里要有数排查起来才不会像无头苍蝇。