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
最佳实践 统一接口风格,避免依赖模糊匹配
相关推荐
不光头强25 分钟前
Spring框架的事务管理
数据库·spring·oracle
小王不爱笑1323 小时前
Spring AOP(AOP+JDBC 模板 + 转账案例)
java·后端·spring
百***37489 小时前
【mybatis】基本操作:详解Spring通过注解和XML的方式来操作mybatis
xml·spring·mybatis
百***581410 小时前
Windows操作系统部署Tomcat详细讲解
java·windows·tomcat
一个帅气昵称啊11 小时前
在.NET中使用RAG检索增强AI基于Qdrant的矢量化数据库
ai·性能优化·c#·.net·rag·qdrant
七夜zippoe12 小时前
Java并发编程基石:深入理解JMM(Java内存模型)与Happens-Before规则
java·开发语言·spring·jmm·happens-before
YDS82912 小时前
苍穹外卖 —— Spring Task和WebSocket的运用以及订单统一处理、订单的提醒和催单功能的实现
java·spring boot·后端·websocket·spring
小小前端_我自坚强12 小时前
前端性能优化实战:打造极致用户体验
前端·性能优化
峰哥的Android进阶之路13 小时前
Android常见的内存性能优化场景解决方案
android·性能优化
羊锦磊13 小时前
[ 项目开发 1.0 ] 新闻网站的开发流程和注意事项
java·数据库·spring boot·redis·spring·oracle·json