这个分析过程是我发在群里的,然后再让AI整理成文章的(有AI之后,写文章都变懒了😂),请勿介意
在一次线上系统性能排查中(其他项目组的),发现一个看似平常但影响深远的问题------**前端请求接口时携带了 .do 后缀(如 /fileType/findFileList.do),而后端 Spring MVC 的 @RequestMapping 中并未显式包含 .do,导致请求处理过程中触发了大量不必要的路径匹配逻辑,最终造成接口响应延迟高达 78 毫秒。
项目是一个老的技术栈,Tomcat7和SpringMVC项目,就是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.domappingRegistry.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);
}
- 若
lookupPath在urlLookup中存在,则直接返回对应 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)
具体体现在 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(推荐)
java
@RequestMapping("/fileType/findFileList.do")
public ResponseEntity<?> findFileList(...) { ... }
✅ 优点:
- 直接命中
urlLookup,避免全量扫描 - 改动小,不影响现有业务逻辑
- 不影响静态资源访问
❌ 缺点:
- 接口风格略显冗余
- 可能需要批量修改注解
👉 适合大多数场景,是最安全、最高效的方案。
方案二:配置 Tomcat 将非 .do 请求也交给 Spring MVC 处理
修改 web.xml 或使用 DispatcherServlet 的 url-pattern 配置:
xml
<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 |
| 最佳实践 | 统一接口风格,避免依赖模糊匹配 |