Spring MVC 接口匹配性能优化:.do后缀引发的性能瓶颈分析

这个分析过程是我发在群里的,然后再让AI整理成文章的(有AI之后,写文章都变懒了😂),请勿介意

在一次线上系统性能排查中(其他项目组的),发现一个看似平常但影响深远的问题------**前端请求接口时携带了 .do 后缀(如 /fileType/findFileList.do),而后端 Spring MVC 的 @RequestMapping 中并未显式包含 .do,导致请求处理过程中触发了大量不必要的路径匹配逻辑,最终造成接口响应延迟高达 78 毫秒

项目是一个老的技术栈,Tomcat7SpringMVC项目,就是JSP那一套,所以Spring版本也不是很高


一、背景介绍

当前系统是一个基于 Spring MVC 的 Web 应用,使用 Tomcat 作为容器。前端通过 Ajax 请求后端接口,所有请求均以 .do 结尾,例如:

text 复制代码
https://127.0.0.1/project/fileType/findFileList.do

而在后端控制器中,对应的映射路径为:

java 复制代码
@RequestMapping("/fileType/findFileList")
public ResponseEntity<?> findFileList(...) { ... }

即:前端带 .do,后端不带 .do

系统共有约 9500个 URL 映射 ,全部注册在 AbstractHandlerMethodMapping.MappingRegistry#urlLookup 中,这是一个 Map<String, List<HandlerMethod>>,用于快速查找匹配的处理器方法。


二、问题复现与定位

1. 线程栈分析

我们在生产环境捕获到多个线程阻塞在以下调用链中:

java 复制代码
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod
→ addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request)
→ for (T mapping : mappings) { getMatchingMapping(mapping, request); }

关键点在于:

  • lookupPath/fileType/findFileList.do
  • mappingRegistry.getMappingsByUrl(lookupPath) 返回 null(因为注册的是不带 .do 的路径)
  • 进入 addMatchingMappings() 方法,遍历整个 keySet()(好几千个,不是urlLookup中的数量),逐个进行模式匹配

2. 性能对比实测

请求方式 响应时间 是否命中 urlLookup
/fileType/findFileList.do 78ms ❌ 否
/fileType/findFileList 1ms ✅ 是

💡 结论 :当请求路径带有 .do 且未在 @RequestMapping 中声明时,Spring MVC 无法直接命中 urlLookup,只能通过全量遍历 + 模式匹配的方式寻找匹配项,导致性能急剧下降。


三、Spring MVC 路径匹配机制详解

1. 正常流程(直接命中)

lookupPath/fileType/findFileList的时候

java 复制代码
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
    addMatchingMappings(directPathMatches, matches, request);
}
  • lookupPathurlLookup 中存在,则直接返回对应 Handler,无需遍历。

2. 回退机制(全量扫描)

lookupPath/fileType/findFileList.do的时候getMappingsByUrl() 返回 null 时,执行:

java 复制代码
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
  • 遍历所有已注册的路径

3. 匹配逻辑细节

java 复制代码
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
    for (T mapping : mappings) {
        if (match = getMatchingMapping(mapping, request)) {
            matches.add(new Match(match, this.mappingRegistry.getMapping(mapping)));
        }
    }
}

其中 getMatchingMapping() 内部会调用 AntPathMatcher.match(),涉及正则表达式匹配、通配符解析等操作,每次调用都消耗一定 CPU 资源。


四、为何 .do 能"自动"匹配?

尽管后端没有写 .do,但前端请求仍能成功访问,是因为:

Spring MVC 默认开启后缀匹配(suffix pattern matching)

具体体现在 RequestConditionHolderPatternsRequestCondition 的实现中,当启用 useSuffixPatternMatch=true(默认开启)时,Spring 会对请求路径进行后缀剥离,尝试匹配无后缀版本。

例如:

  • 请求:/admin/sysNotice/getMyNotice.do
  • 实际匹配:/admin/sysNotice/getMyNotice

因此虽然不能直接命中 urlLookup,但仍可通过 addMatchingMappings 找到目标 Controller。

但这带来了严重的性能代价!


五、性能瓶颈可视化证据

1. 线程栈截图分析


红框部分显示大量调用 getMatchingCondition()getMatchingPattern()doMatch() 等方法,正是 addMatchingMappings 中的循环匹配过程。

2. Arthas 性能监控结果


  • .do 的请求耗时接近 80ms
  • 不带 .do 的请求仅需 0.7ms

说明:是否直接命中 urlLookup 决定了性能差异


六、解决方案建议

方案一:在 @RequestMapping 中显式添加 .do(推荐)

java 复制代码
@RequestMapping("/fileType/findFileList.do")
public ResponseEntity<?> findFileList(...) { ... }

✅ 优点:

  • 直接命中 urlLookup,避免全量扫描
  • 改动小,不影响现有业务逻辑
  • 不影响静态资源访问

❌ 缺点:

  • 接口风格略显冗余
  • 可能需要批量修改注解

👉 适合大多数场景,是最安全、最高效的方案。


方案二:配置 Tomcat 将非 .do 请求也交给 Spring MVC 处理

修改 web.xml 或使用 DispatcherServleturl-pattern 配置:

xml 复制代码
<servlet-mapping>
    <servlet-name>SpringMvc</servlet-name>
    <url-pattern>/</url-pattern> <!-- 匹配所有请求 -->
</servlet-mapping>

⚠️ 注意事项:

  • 静态资源(JS/CSS/IMG)也会被 Spring MVC 拦截,可能导致 404 或异常
  • 需配合 ResourceHttpRequestHandlerDefaultServlet 解决静态资源问题
  • 可能引入额外的过滤器干扰

👉 风险较高,需谨慎测试,建议仅在可控环境下使用。


七、最佳实践建议

  1. 统一接口命名规范 :明确是否使用 .do 后缀,并在整个项目中保持一致。
  2. 优先使用精确匹配 :尽量让请求路径与 @RequestMapping 完全一致,避免依赖后缀匹配。
  3. 监控关键路径性能 :利用 Arthas、SkyWalking 等工具监控 getHandler() 方法耗时。
  4. 避免大规模路径匹配:若系统有上万个接口,应避免频繁触发全量扫描。

八、总结

关键点 说明
问题本质 请求路径含 .do,但注册路径不含,导致无法命中 urlLookup
性能影响 触发全量路径扫描,耗时从 1ms 升至 78ms
根本原因 Spring MVC 后缀匹配机制虽能工作,但效率低下
解决方案 ✅ 显式添加 .do@RequestMapping;或 ⚠️ 修改 url-pattern
最佳实践 统一接口风格,避免依赖模糊匹配
相关推荐
没有bug.的程序员6 小时前
AOP 原理深剖:动态代理与 CGLIB 字节码增强
java·spring·aop·动态代理·cglib
2401_837088506 小时前
ResponseEntity - Spring框架的“标准回复模板“
java·前端·spring
IT·陈寒10 小时前
当 JVM 开始“内卷”:一次性能优化引发的 GC 战争
java·jvm·性能优化
不会吃萝卜的兔子10 小时前
spring微服务宏观概念
java·spring·微服务
侑虎科技10 小时前
对Android游戏画面抖动现象的研究
android·性能优化
Chan1610 小时前
流量安全优化:基于 Nacos 和 BloomFilter 实现动态IP黑名单过滤
java·spring boot·后端·spring·nacos·idea·bloomfilter
yuanbenshidiaos10 小时前
【性能优化】--perfetto分析思路
性能优化·perfetto
摸着石头过河的石头11 小时前
深入理解JavaScript事件流:从DOM0到DOM3的演进之路
前端·javascript·性能优化
顾漂亮14 小时前
Redis深度探索
java·redis·后端·spring·缓存