Spring MVC 源码解析:AbstractHandlerMethodMapping.MappingRegistry 映射注册表的设计与实现

发布时间:2026/9/20 10:12:20
Spring MVC 源码解析:AbstractHandlerMethodMapping.MappingRegistry 映射注册表的设计与实现 Spring MVC 源码解析AbstractHandlerMethodMapping.MappingRegistry 映射注册表的设计与实现【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunterMappingRegistry 是 Spring MVC 请求路由的核心枢纽它负责把RequestMapping注解解析出的映射信息URL、HTTP 方法、参数条件等与具体的 Controller 处理方法进行绑定并提供高性能的查询与安全的并发控制。本文以 Spring MVC 源码为线索从数据结构、注册链路、查询命中、跨域装配到注销清理完整拆解org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry内部类的设计与实现读完你将掌握请求是如何按图索骥找到对应 HandlerMethod 的。MappingRegistry 在 Spring MVC 中的定位在 Spring MVC 中HandlerMapping接口负责将HttpServletRequest转换为处理器执行链HandlerExecutionChain其核心方法只有一个public interface HandlerMapping { HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; }而基于注解驱动的RequestMapping映射主要由org.springframework.web.servlet.handler.AbstractHandlerMethodMapping这一抽象类实现。它内部嵌套了一个名为MappingRegistry的内部类——即本文的主角。类全路径为org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry可以这样理解整条链路DispatcherServlet调用HandlerMapping#getHandler→AbstractHandlerMethodMapping#getHandlerInternal→ 通过MappingRegistry中的urlLookup/mappingLookup查找到匹配的HandlerMethod。也就是说MappingRegistry 是所有 HandlerMethod 映射信息的注册表与索引是整个请求分发的数据底座。关于 HandlerMapping 的整体流程可结合阅读仓库文档 Spring-MVC-HandlerMapping.md。核心数据结构五张映射表 一把读写锁先看 MappingRegistry 的基本属性class MappingRegistry { /** * key:mapping * value: mapping registration */ private final MapT, MappingRegistrationT registry new HashMap(); /** * key: mapping * value: handlerMethod */ private final MapT, HandlerMethod mappingLookup new LinkedHashMap(); /** * key: url * value: list mapping */ private final MultiValueMapString, T urlLookup new LinkedMultiValueMap(); /** * key: name * value: handler method */ private final MapString, ListHandlerMethod nameLookup new ConcurrentHashMap(); /** * key:handler method * value: 跨域配置 */ private final MapHandlerMethod, CorsConfiguration corsLookup new ConcurrentHashMap(); /** * 读写锁 */ private final ReentrantReadWriteLock readWriteLock new ReentrantReadWriteLock(); }五张表各司其职分别服务于不同的查询维度字段类型key → value用途registryHashMapT, MappingRegistrationTmapping → 注册快照保存一次注册的完整信息mapping、handlerMethod、直接 URL 列表、name供注销时反查mappingLookupLinkedHashMapT, HandlerMethodmapping → handlerMethod以映射对象为键快速定位处理器方法urlLookupMultiValueMapString, TLinkedMultiValueMapurl → mapping 列表URL 直查索引一个 URL 可能对应多个 mapping不同条件nameLookupConcurrentHashMapString, ListHandlerMethod映射名 → handlerMethod 列表支持按映射名称反查名称形如DC#gocorsLookupConcurrentHashMapHandlerMethod, CorsConfigurationhandlerMethod → 跨域配置保存注解解析出的跨域配置请求阶段直接取用其中urlLookup使用MultiValueMap而非普通Map是因为同一个 URL 可以被多个 mapping 命中例如不同 HTTP 方法、不同参数条件此时 value 是 Listregistry中的MappingRegistration则像一个聚合根把一次注册涉及的所有信息打包存档。一次映射注册的完整旅程Spring 容器启动时RequestMappingHandlerMapping会扫描所有Controller类把每个带有RequestMapping系列注解的方法注册进 MappingRegistry。以如下 Controller 为例RestController RequestMapping(/demo) public class DemoController { GetMapping(/do) public Object go() { return fff; } }最终该 Controller 会注册一条GET /demo/do的映射。下面追踪这条注册链路的完整调用路径。第一步RequestMappingHandlerMapping#registerHandlerMethodorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping重写了注册入口在调用父类方法后额外做了一次Consumes条件的更新protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) { super.registerHandlerMethod(handler, method, mapping); this.updateConsumesCondition(mapping, method); }第二步AbstractHandlerMethodMapping#registerHandlerMethod父类org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#registerHandlerMethod的实现非常简洁直接把注册动作委托给内部类protected void registerHandlerMethod(Object handler, Method method, T mapping) { this.mappingRegistry.register(mapping, handler, method); }handler在这里通常是 Controller 的 beanName字符串method是处理方法如go()mapping是解析后的RequestMappingInfo携带 URL 路径、HTTP 方法、参数条件等元数据。第三步MappingRegistry#register —— 核心注册逻辑register方法是本文的重点。先看完整实现/** * 注册方法 将controller 相关信息存储 * * param mapping 请求地址 * param handler 处理类 * param method 函数 */ public void register(T mapping, Object handler, Method method) { // 上锁 this.readWriteLock.writeLock().lock(); try { // 创建 HandlerMethod , 通过 handler 创建处理的对象(controller) HandlerMethod handlerMethod createHandlerMethod(handler, method); validateMethodMapping(handlerMethod, mapping); // 设置值 this.mappingLookup.put(mapping, handlerMethod); // 获取url ListString directUrls getDirectUrls(mapping); for (String url : directUrls) { // 设置 this.urlLookup.add(url, mapping); } String name null; if (getNamingStrategy() ! null) { name getNamingStrategy().getName(handlerMethod, mapping); addMappingName(name, handlerMethod); } CorsConfiguration corsConfig initCorsConfiguration(handler, method, mapping); if (corsConfig ! null) { this.corsLookup.put(handlerMethod, corsConfig); } this.registry.put(mapping, new MappingRegistration(mapping, handlerMethod, directUrls, name)); } finally { // 开锁 this.readWriteLock.writeLock().unlock(); } }整个方法用ReentrantReadWriteLock的写锁包裹保证并发注册/注销时各张映射表的一致性。其内部执行顺序可以概括为六步加写锁readWriteLock.writeLock().lock()独占期间其他线程不可读不可写创建 HandlerMethod调用createHandlerMethod(handler, method)把 handler 与 method 封装为HandlerMethod唯一性校验validateMethodMapping检查是否出现映射歧义Ambiguous mapping写入映射表mappingLookup.put(mapping, handlerMethod)建立 mapping → HandlerMethod 关系建立 URL 索引通过getDirectUrls提取不含通配符的直接 URL逐个写入urlLookup可选信息补充若配置了命名策略则生成映射名并写入nameLookup若存在CrossOrigin注解则解析跨域配置写入corsLookup最后把所有信息封装进MappingRegistration存入registry存档。下图是register方法执行时的调试截图可以看到mapping RequestMappingInfo GET /demo/do、handler demoController、method DemoController.go()以及当前写锁计数为 1 的实时状态需要说明的是在更新版本的 Spring 中register方法开头还会增加一段 Kotlin 检测逻辑——如果方法声明类为 Kotlin 类型且方法为suspend挂起函数会抛出IllegalStateException(Unsupported suspending handler method detected: ...)拒绝注册避免挂起函数无法被 Servlet 调用栈正确处理。createHandlerMethod构建 HandlerMethodcreateHandlerMethod位于org.springframework.web.servlet.handler.AbstractHandlerMethodMapping中负责把处理器 方法组装为HandlerMethod。它需要兼容两种 handler 形态beanName 字符串与对象实例。protected HandlerMethod createHandlerMethod(Object handler, Method method) { // 是否是字符串 if (handler instanceof String) { // 创建对象 return new HandlerMethod((String) handler, obtainApplicationContext().getAutowireCapableBeanFactory(), method); } return new HandlerMethod(handler, method); }对应两个构造函数public HandlerMethod(String beanName, BeanFactory beanFactory, Method method){} public HandlerMethod(Object bean, Method method) {}从源码可以看出当handler是字符串beanName时需要借助ApplicationContext的AutowireCapableBeanFactory在运行时解析真实 Bean当handler已是对象实例时则直接包装。这种设计保证了注册阶段无需提前实例化 Controller将实例化延迟到真正调用时。HandlerMethod处理器方法的封装HandlerMethod是对哪个 Bean 的哪个方法处理请求这一事实的封装其成员变量如下public class HandlerMethod { /** Logger that is available to subclasses. */ protected final Log logger LogFactory.getLog(getClass()); /** * beanName 或者 bean 实例 */ private final Object bean; /** * 上下文 */ Nullable private final BeanFactory beanFactory; /** * bean 类型 */ private final Class? beanType; /** * 处理方法 */ private final Method method; private final Method bridgedMethod; /** * 方法参数 */ private final MethodParameter[] parameters; }各字段含义beanbeanName 或 bean 实例是后续反射调用的目标beanFactory当 bean 以 name 形式存在时持有 BeanFactory用于延迟解析与依赖注入beanTypebean 的类型用于解析CrossOrigin等类级注解与命名策略取类名缩写method实际要调用的方法bridgedMethod桥接方法处理泛型擦除后编译器生成的桥接方法parameters方法参数元数据数组供参数解析器HandlerMethodArgumentResolver在调用前解析入参。validateMethodMapping重复映射校验同一路径若被多个处理方法撞车Spring 会在注册阶段直接抛出异常提示开发者而不是等到请求到来时才暴露。这正是validateMethodMapping的职责private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) { // Assert that the supplied mapping is unique. // 从缓存中获取 HandlerMethod existingHandlerMethod this.mappingLookup.get(mapping); // 是否为空 , 是否相同 if (existingHandlerMethod ! null !existingHandlerMethod.equals(handlerMethod)) { throw new IllegalStateException( Ambiguous mapping. Cannot map handlerMethod.getBean() method \n handlerMethod \nto mapping : There is already existingHandlerMethod.getBean() bean method\n existingHandlerMethod mapped.); } }逻辑很直接先从mappingLookup中以 mapping 为键取出已注册的 HandlerMethod若已存在且与当前待注册对象不相等则抛出IllegalStateException异常信息中会完整列出两个冲突的 Bean 与方法。日常开发中常见的 Ambiguous mapping 启动报错即源于此处。getDirectUrls提取可直接匹配的 URLurlLookup建立的是URL → mapping的直查索引但一个 mapping 的路径条件中可能既有字面路径如/demo/do也有带通配符的模板如/demo/**。只有不含通配符的路径才能作为直查键因此需要getDirectUrls做过滤private ListString getDirectUrls(T mapping) { ListString urls new ArrayList(1); // mapping.getPatternsCondition().getPatterns() for (String path : getMappingPathPatterns(mapping)) { // 是否匹配 if (!getPathMatcher().isPattern(path)) { urls.add(path); } } return urls; }getMappingPathPatterns返回 mapping 声明的全部路径模式getPathMatcher().isPattern(path)判断路径是否包含*、?、{...}等模式字符。仅当路径不含模式字符时才会加入直接 URL 列表进而写入urlLookup。这样一来请求到来时若 URL 恰好命中urlLookup的 key就能 O(1) 定位候选 mapping无需全表扫描只有直接匹配失败例如带路径变量的 URL才需要遍历全部 mapping 做模式匹配。这就是 Spring MVC 请求分发性能的关键设计。命名策略类名缩写#方法名除了按 URL 查询Spring MVC 还支持按映射名称反查 HandlerMethod。注册时若getNamingStrategy()不为空就会为当前映射生成名称String name null; if (getNamingStrategy() ! null) { // 获取名字 // 类名#方法名 name getNamingStrategy().getName(handlerMethod, mapping); // 设置 handlerMethod name 的关系 addMappingName(name, handlerMethod); }默认命名策略由org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMethodMappingNamingStrategy提供Override public String getName(HandlerMethod handlerMethod, RequestMappingInfo mapping) { if (mapping.getName() ! null) { return mapping.getName(); } StringBuilder sb new StringBuilder(); // 短类名 String simpleTypeName handlerMethod.getBeanType().getSimpleName(); for (int i 0; i simpleTypeName.length(); i) { if (Character.isUpperCase(simpleTypeName.charAt(i))) { sb.append(simpleTypeName.charAt(i)); } } // 组装名称 // 类名#方法名称 sb.append(SEPARATOR).append(handlerMethod.getMethod().getName()); return sb.toString(); }命名规则一目了然类名大写字母缩写 # 方法名。例如DemoController#go会生成名称DC#go如果RequestMapping显式指定了name属性则优先使用自定义名称。生成的名称通过addMappingName写入nameLookupMapString, ListHandlerMethod供后续按名称定位 HandlerMethod 的场景使用例如根据映射名构建链接等需求即可从该索引反查。注意nameLookup与corsLookup均使用ConcurrentHashMap因为这两个索引在运行期可能被并发读写而注册期其他表的写操作已有写锁保护。initCorsConfiguration跨域配置的装配MappingRegistry 同时承担了跨域配置的注册期预解析。注册时调用org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#initCorsConfiguration将类级与方法级的CrossOrigin注解提前解析为CorsConfiguration并存入corsLookup这样请求阶段无需再重复反射解析注解。该方法在仓库文档 SpringMVC-CROS.md 中有完整展开Override protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { // 创建 handlerMethod HandlerMethod handlerMethod createHandlerMethod(handler, method); // 获取 beanType Class? beanType handlerMethod.getBeanType(); // 获取跨域注解 CrossOrigin CrossOrigin typeAnnotation AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class); CrossOrigin methodAnnotation AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class); if (typeAnnotation null methodAnnotation null) { return null; } // 跨域信息配置 CorsConfiguration config new CorsConfiguration(); // 更新跨域配置 updateCorsConfig(config, typeAnnotation); updateCorsConfig(config, methodAnnotation); if (CollectionUtils.isEmpty(config.getAllowedMethods())) { // 跨域配置赋给方法 for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) { config.addAllowedMethod(allowedMethod.name()); } } // 应用跨域 return config.applyPermitDefaultValues(); }要点解读通过AnnotatedElementUtils.findMergedAnnotation分别查找类级与方法级的CrossOrigin支持注解继承与合并类级 方法级配置会叠加若两者都不存在直接返回nullcorsLookup不会存储任何内容updateCorsConfig逐个填充origins、methods、allowedHeaders、exposedHeaders、allowCredentials、maxAge若未显式配置允许的 HTTP 方法则回退使用 mapping 上RequestMapping声明的methods条件最后调用config.applyPermitDefaultValues()补齐默认值如默认允许源*、默认允许方法等。最终解析出的CorsConfiguration以 HandlerMethod 为键存入corsLookupif (corsConfig ! null) { this.corsLookup.put(handlerMethod, corsConfig); }当请求真正到达时AbstractHandlerMethodMapping#getHandler会通过getCorsConfiguration从corsLookup取出配置并结合全局配置生成跨域处理链。CorsConfiguration的核心属性包括/** * 允许请求源 */ Nullable private ListString allowedOrigins; /** * 允许的http方法 */ Nullable private ListString allowedMethods; /** * 允许的请求头 */ Nullable private ListString allowedHeaders; /** * 返回的响应头 */ Nullable private ListString exposedHeaders; /** * 是否允许携带 cookies */ Nullable private Boolean allowCredentials; /** * 存货有效期 */ Nullable private Long maxAge;跨域相关完整链路CorsUtils.isCorsRequest判断 →getCorsHandlerExecutionChain装配CorsInterceptor/PreFlightHandler→DefaultCorsProcessor处理可继续阅读 SpringMVC-CROS.md。unregister优雅地移除映射有注册就有注销。当应用运行期动态增删映射例如热部署 Controller时unregister负责把一条映射从所有索引中彻底移除并且同样受写锁保护public void unregister(T mapping) { this.readWriteLock.writeLock().lock(); try { MappingRegistrationT definition this.registry.remove(mapping); if (definition null) { return; } this.mappingLookup.remove(definition.getMapping()); for (String url : definition.getDirectUrls()) { ListT list this.urlLookup.get(url); if (list ! null) { list.remove(definition.getMapping()); if (list.isEmpty()) { this.urlLookup.remove(url); } } } removeMappingName(definition); this.corsLookup.remove(definition.getHandlerMethod()); } finally { this.readWriteLock.writeLock().unlock(); } }注销过程与注册严格对称且充分利用了registry中存档的MappingRegistration从registry中取出该 mapping 的注册快照若不存在则直接返回从mappingLookup移除 mapping → HandlerMethod 关系遍历definition.getDirectUrls()从urlLookup中逐个移除该 mapping若某 URL 的映射列表因此清空则连 URL 键一起删除通过removeMappingName从nameLookup移除对应名称从corsLookup移除 HandlerMethod 对应的跨域配置。这也解释了为什么registry要额外保存一份MappingRegistration没有它注销时无法知道该 mapping 当初关联了哪些直接 URL 和名称也就无法精确清理所有索引。请求阶段的查询链路与并发控制MappingRegistry 的读取发生在每次请求分发时。AbstractHandlerMethodMapping#getHandlerInternal展示了读路径的典型写法——读锁保护 查询Override protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { // 获取当前请求路径 String lookupPath getUrlPathHelper().getLookupPathForRequest(request); // 设置属性 request.setAttribute(LOOKUP_PATH, lookupPath); // 上锁 this.mappingRegistry.acquireReadLock(); try { // 寻找 handler method HandlerMethod handlerMethod lookupHandlerMethod(lookupPath, request); return (handlerMethod ! null ? handlerMethod.createWithResolvedBean() : null); } finally { // 释放锁 this.mappingRegistry.releaseReadLock(); } }acquireReadLock/releaseReadLock内部封装的是ReentrantReadWriteLock的读锁操作。由于请求分发是典型的读多写少场景读写锁让并发请求可以同时读取映射表仅在注册/注销时短暂阻塞兼顾了吞吐与一致性。lookupHandlerMethod的查询策略体现了索引优先、全量兜底的思想Nullable protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { ListMatch matches new ArrayList(); // 从 MultiValueMap 获取 ListT directPathMatches this.mappingRegistry.getMappingsByUrl(lookupPath); // 如果不为空 if (directPathMatches ! null) { // 添加匹配映射 addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { // No choice but to go through all mappings... // 添加匹配映射 addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } // ... }其中按 URL 直查正是命中urlLookup索引Nullable public ListT getMappingsByUrl(String urlPath) { return this.urlLookup.get(urlPath); }如果直接命中则只需在少量候选中做精确条件匹配addMatchingMappings内部调用getMatchingMapping判断 HTTP 方法、参数、Produces 等条件只有直接匹配为空例如带路径变量的 URL才退化为遍历mappingLookup.keySet()全量匹配。匹配结果会经过MatchComparator排序取最优匹配若前两名得分相同且非预检请求则抛出 Ambiguous handler methods mapped 异常。匹配成功后会设置BEST_MATCHING_HANDLER_ATTRIBUTE等请求属性并返回bestMatch.handlerMethod。完整的lookupHandlerMethod排序、歧义判定与handleMatch/handleNoMatch分支可参阅 Spring-MVC-HandlerMapping.md。小结一张注册表如何撑起整个 MVC 路由组件作用生命周期register建立 mapping、URL、name、CORS 四类索引并归档MappingRegistration容器启动 / 动态注册写锁保护urlLookupURL 直查索引O(1) 定位候选 mapping请求分发读注册期写mappingLookupmapping → HandlerMethod校验与兜底遍历的数据源请求分发读注册期写nameLookup映射名 → HandlerMethod 列表支持按名反查并发读写ConcurrentHashMapcorsLookupHandlerMethod → CorsConfiguration请求期免反射直接取用并发读写ConcurrentHashMapreadWriteLock读多写少场景下的并发控制全程从数据结构设计到读写锁策略MappingRegistry 处处体现着空间换时间、索引优先、读写分离的工程思想注册阶段用写锁一次性建立多维索引请求阶段用读锁支持高并发直查直接 URL 走urlLookup索引通配路径才退化全量匹配跨域配置在注册期预解析缓存运行时零反射开销。理解了这张注册表也就理解了 Spring MVC 请求分发从 URL 到方法的最后一公里。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询