Spring MVC 接口匹配性能优化:.do后缀引发的性能瓶颈分析
这个分析过程是我发在群里的,然后再让AI整理成文章的(有AI之后,写文章都变懒了😂),请勿介意
在一次线上系统性能排查中(其他项目组的),发现一个看似平常但影响深远的问题——**前端请求接口时携带了 .do
后缀(如 /fileType/findFileList.do
),而后端 Spring MVC 的 @RequestMapping
中并未显式包含 .do
,导致请求处理过程中触发了大量不必要的路径匹配逻辑,最终造成接口响应延迟高达 78 毫秒。
项目是一个老的技术栈,Tomcat7
和SpringMVC
项目,就是JSP
那一套,所以Spring
版本也不是很高
一、背景介绍
当前系统是一个基于 Spring MVC 的 Web 应用,使用 Tomcat 作为容器。前端通过 Ajax 请求后端接口,所有请求均以 .do
结尾,例如:
https://127.0.0.1/project/fileType/findFileList.do
而在后端控制器中,对应的映射路径为:
@RequestMapping("/fileType/findFileList")
public ResponseEntity<?> findFileList(...) { ... }
即:前端带 .do
,后端不带 .do
。
系统共有约 9500个 URL 映射,全部注册在 AbstractHandlerMethodMapping.MappingRegistry#urlLookup
中,这是一个 Map<String, List<HandlerMethod>>
,用于快速查找匹配的处理器方法。
二、问题复现与定位
1. 线程栈分析
我们在生产环境捕获到多个线程阻塞在以下调用链中:
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
的时候
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {addMatchingMappings(directPathMatches, matches, request);
}
- 若
lookupPath
在urlLookup
中存在,则直接返回对应 Handler,无需遍历。
2. 回退机制(全量扫描)
当lookupPath
为/fileType/findFileList.do
的时候getMappingsByUrl()
返回 null
时,执行:
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
- 遍历所有已注册的路径
3. 匹配逻辑细节
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)
具体体现在 RequestConditionHolder
和 PatternsRequestCondition
的实现中,当启用 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
(推荐)
@RequestMapping("/fileType/findFileList.do")
public ResponseEntity<?> findFileList(...) { ... }
✅ 优点:
- 直接命中
urlLookup
,避免全量扫描 - 改动小,不影响现有业务逻辑
- 不影响静态资源访问
❌ 缺点:
- 接口风格略显冗余
- 可能需要批量修改注解
👉 适合大多数场景,是最安全、最高效的方案。
方案二:配置 Tomcat 将非 .do
请求也交给 Spring MVC 处理
修改 web.xml
或使用 DispatcherServlet
的 url-pattern
配置:
<servlet-mapping><servlet-name>SpringMvc</servlet-name><url-pattern>/</url-pattern> <!-- 匹配所有请求 -->
</servlet-mapping>
⚠️ 注意事项:
- 静态资源(JS/CSS/IMG)也会被 Spring MVC 拦截,可能导致 404 或异常
- 需配合
ResourceHttpRequestHandler
或DefaultServlet
解决静态资源问题 - 可能引入额外的过滤器干扰
👉 风险较高,需谨慎测试,建议仅在可控环境下使用。
七、最佳实践建议
- 统一接口命名规范:明确是否使用
.do
后缀,并在整个项目中保持一致。 - 优先使用精确匹配:尽量让请求路径与
@RequestMapping
完全一致,避免依赖后缀匹配。 - 监控关键路径性能:利用 Arthas、SkyWalking 等工具监控
getHandler()
方法耗时。 - 避免大规模路径匹配:若系统有上万个接口,应避免频繁触发全量扫描。
八、总结
关键点 | 说明 |
---|---|
问题本质 | 请求路径含 .do ,但注册路径不含,导致无法命中 urlLookup |
性能影响 | 触发全量路径扫描,耗时从 1ms 升至 78ms |
根本原因 | Spring MVC 后缀匹配机制虽能工作,但效率低下 |
解决方案 | ✅ 显式添加 .do 到 @RequestMapping ;或 ⚠️ 修改 url-pattern |
最佳实践 | 统一接口风格,避免依赖模糊匹配 |