
先交代一下背景。我手头一个老项目Spring Boot 2.7.9 Spring Framework 5.3.x跑了三年多一直稳如老狗。因为团队整体要切 JDK 17 和新的基础设施我被迫把 SpringMVC 一路升到 6.1对应 Spring Boot 3.2。当时想得很天真不就是换个版本号吗实际动手之后javax 改 jakarta、路由匹配规则变化、springmvc拦截器不生效、静态资源 404——这些坑排着队等我。这篇不聊虚的只讲我实际踩过并且已经解决的问题。我把每个问题的现象、原因、解决办法都整理了配置和代码直接给全。适合两类人看一类是从 Spring Boot 2.x / SpringMVC 5 往 SpringMVC 6 升级的开发者一类是想把 springmvc工作流程、springmvc拦截器这块彻底搞明白的同学后者可以把第 5 节当作一份精简的复习资料。先说结论SpringMVC 6 整体不难难的是旧习惯。很多报错都是因为“你以为它还是老样子”下面逐条拆。1. 升级先捋清版本基线SpringMVC 6 到底多了什么升级前先别急着改代码把版本基线理清楚后面所有坑都能对上号。我当时的版本对照是这样的项目升级前升级后Spring Boot2.7.93.2.xSpring Framework5.3.x6.1.xJDK817Servlet 命名空间javax.servletjakarta.servlet默认路径匹配方式AntPathMatcherPathPatternParser拦截器基类HandlerInterceptorAdapter已移除直接实现 HandlerInterceptorSpring Framework 6 的底座是 Jakarta EE 9/10JDK 要求最低 17。这不是可选项而是硬门槛——JDK 不到 17 连框架都加载不起来。所以升级之前先把 JDK 版本统一掉别想着“先让代码编过再处理环境”那样只会把问题搅在一起。建议直接在项目根目录的 README 里写清楚目标版本矩阵团队所有人照着统一环境操作能省掉一半“在我机器上是好的”这种破事。1.1 从 javax 到 jakarta先解决编译都过不去的问题升级后第一波报错毫无悬念package javax.servlet does not exist。SpringMVC 6 把整个 Servlet 规范迁移到了jakarta.servlet命名空间所有直接或者间接引用javax.servlet的代码都要跟着改。// 升级前 import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; // 升级后 import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;除 Servlet 之外Bean Validation 的包名同样从javax.validation变成了jakarta.validation。这地方非常容易漏很多人只盯着javax.servlet替换结果NotNull、Valid还引用着旧的javax.validation.constraints.NotNull。如果项目里恰好还有旧的 validation-api 依赖编译能过但校验完全不生效——这是最隐蔽的坑我排查了一整个下午才定位到。替换的方式不用一个个手敲。如果你的代码量不大直接用 IDE 的全局替换即可代码量大的话推荐 OpenRewrite 的 Spring Boot 3 迁移配方它能把常见的包名替换、API 变更一次性处理掉。不过无论用哪种方式替换完都要全局搜一遍javax.servlet、javax.validation、javax.annotation确认没有漏网的。javax.annotation这个也要注意Spring 6 里Resource、PostConstruct这些注解同样迁移到了jakarta.annotation漏了的话启动时可能会有诡异的 Bean 初始化失败。还有个容易被忽略的点HandlerInterceptorAdapter在 Spring 6 中被移除了。以前写拦截器习惯extends HandlerInterceptorAdapter升级后直接编译报错。正确做法是implements HandlerInterceptor后面第 3 节我会贴完整代码。遇到这种“类找不到”的报错别慌先查这个类是新版本删掉的还是依赖没拉进来多数是前者。1.2 第三方依赖里残留的 javax 才是大坑如果说包名替换是明坑那第三方依赖里的旧 Servlet API 就是暗坑。我遇到的情况是项目里有一个内部公共组件还是两年前编译的内部引用了javax.servlet.http.HttpServletResponse。项目本身升级后编译、启动都正常但只要走到那个组件的方法立刻抛NoClassDefFoundError: javax/servlet/ServletException。这就是典型的“依赖残留”。Spring Boot 3 自带的容器Tomcat 10.1 之后只认 jakarta不再提供 javax.servlet 类谁还引着旧 API谁就会在运行时炸。而且这种错误往往出现在运行一段时间之后不是启动时立刻爆出来排查难度比编译报错高得多。mvn dependency:tree -Dincludesjavax.servlet用上面这条命令可以把依赖树里所有 javax.servlet 相关依赖揪出来。找到之后分两种情况处理能升级的把第三方组件升到适配 Jakarta 的版本不能升级的在 Maven 里排除掉冲突的旧依赖。dependency groupIdcom.example/groupId artifactIdlegacy-common/artifactId exclusions exclusion groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId /exclusion /exclusions /dependency注意排除依赖只是“眼不见为净”前提是你确认代码里没有再直接调用 javax 的类。最稳妥的办法还是让第三方组件方提供基于 Jakarta 的新版本排掉旧依赖只是临时方案。2. 路由匹配规则变了404 是最忠诚的报警器版本升级完、编译也过了进入联调阶段才是真正的好戏开场。我最先遇到的是一批莫名其妙的 404。接口定义明明没动前端也没改发出去的请求就是找不到 handler。2.1 PathPatternParser 取代 AntPathMatcher斜杠这种细节最要命SpringMVC 6 默认使用PathPatternParser做路径匹配取代了老版本的AntPathMatcher。框架这么做的原因是性能更好匹配规则更清晰不再有正则回溯之类的不确定性。但随之而来的规则变化会让老代码原地崩溃。其中最典型的尾部斜杠不再自动匹配。老版本中/user这个映射默认也能接住/user/的请求很多前端代码因此习惯性地在 URL 后面加一个斜杠。新规则下/user只匹配/user/user/是另一个路径找不到 handler直接 404。对比项老版本AntPathMatcherSpringMVC 6PathPatternParserURL/user能否匹配请求/user/默认可以不能/user/*能否匹配/user/a/b不能* 只匹配单段不能/files/{*path}多段路径参数写法复杂原生支持匹配性能尚可有回溯风险更快、更稳定新写法里比较有用的特性是{*path}捕获剩余路径。比如GetMapping(/files/{*path}) public String file(PathVariable String path) { // path report/2025/01/data.xlsx return handle(path); }这在老版本里得写正则或者用/**加手动截取现在一个路径变量就搞定。2.2 我的 404 排查实录与两种解法我当时遇到的报错是这样的系统首页正常但所有以斜杠结尾的业务接口全部 404日志里刷着一行No mapping for GET /api/order/detail/。接口定义我确认过是对的当时第一反应是“springmvc拦截器把请求吞了”于是把拦截器全部注释掉404 依旧。然后才想到路径匹配策略。排查过程其实就三步先看日志里有没有No mapping for ...有就是根本没找到 handler跟业务代码无关。用 curl 直接把尾部斜杠去掉再请求一次如果通了十有八九是尾斜杠匹配问题。去 Spring Framework 6 的迁移说明里确认匹配策略的变更。解法有两种按团队情况选。解法 A统一前端 URL去掉尾部斜杠。这是官方推荐的方向。HTTP 语义里/api/order/detail和/api/order/detail/本来就是两个不同的 path后端没必要把两者强行等价。解法 B切回 AntPathMatcher 做兼容。项目实在改不动 URL 时可以临时恢复老规则spring: mvc: pathmatch: matching-strategy: ant_path_matcher如果是纯 Spring MVC 项目不走 Boot在WebMvcConfigurer里把匹配器设置回去Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setPathMatcher(new AntPathMatcher()); }坦白讲解法 B 只能救急。Spring 官方在新版本里已经明确不鼓励尾斜杠匹配后续版本会不会彻底移除都是未知数。我最后是让前端做了全局替换把 URL 全部规范化一劳永逸。另外推荐一个配套设置在配置文件里打开spring.mvc.throw-exception-if-no-handler-foundtrue同时配合全局异常处理把“找不到 handler”的 404 转成统一的 JSON 结构。不然很多 404 会直接打到默认错误页前端再做一份 404 文案体验很割裂。这个配置我在升级前完全没在意踩过一次坑之后才知道它多有用。3. springmvc拦截器的坑不生效、拦不到、拦错路径路径匹配搞定之后又轮到 springmvc拦截器。这节内容在网上一搜一大把但我还是要说一句新版本里拦截器的问题十有八九不是 API 变了而是“你以为配了其实没配到位”。3.1 拦截器不触发的三个常见原因原因一配置类根本不在扫描路径里。WebMvcConfigurer的实现类必须被 Spring 容器扫描到拦截器才会注册。我见过有人把配置类放在com.example.legacy这种老包主类在com.example.app扫描不到代码看着没什么问题拦截器就是死活不跑。解决办法很简单把配置类放到主类所在包的子包下或者用ComponentScan显式指定。原因二Spring Boot 项目里顺手加了EnableWebMvc。这可以说是升级头号杀手。EnableWebMvc会让 Boot 的WebMvcAutoConfiguration直接失效MVC 的全部默认配置回到框架最基础的状态。表现不只是拦截器不生效静态资源映射、JSON 消息转换、默认异常处理全部会丢。如果你是在 Spring Boot 里写 Web 配置不要加EnableWebMvc只需要写一个Configuration类去实现WebMvcConfigurer就够了。原因三拦截路径写错。addPathPatterns(/api/*)和addPathPatterns(/api/**)差别巨大。*只能匹配一个路径段/api/*能拦到/api/login拦不到/api/user/info。很多人的拦截器“只拦了一半”不是代码 bug是对通配符理解偏差。这类问题在新版本里尤其常见因为 PathPatternParser 对通配符的解释更严格老版本里一些“半模糊”写法还能匹配上新版本就直接不认了。3.2 新版本下拦截器注册的正确姿势含代码先看拦截器本体。Spring 6 之后没有HandlerInterceptorAdapter了直接实现HandlerInterceptor接口public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(X-Token); if (token null || token.isBlank()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或token缺失\}); return false; } // 这里写 token 解析和用户信息填充 return true; } Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // handler 执行完后、视图渲染前 } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求结束无论是否异常都会调用 } }注册方式Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/api/**, /user/**) .excludePathPatterns(/api/login, /api/register, /static/**, /error); } }几个细节值得记一下excludePathPatterns一定要包含登录、注册、静态资源、健康检查这些公开接口不然系统一上线还没登录就把自己锁死了。多个拦截器时preHandle按addInterceptor的调用顺序执行postHandle和afterCompletion按相反顺序执行。如果多个拦截器之间有依赖关系这个先后顺序要想清楚。如果拦截器里需要访问 Spring 容器中的 Bean不要用new创建把拦截器也定义成Component再通过构造注入拿进来。直接在addInterceptors里new出来的对象是不受容器管理的里面注入的任何依赖都是 null这个坑我见过太多次。3.3 OPTIONS 预检请求被拦截跨域失败的常见元凶跨域问题在新版本里也容易和拦截器撞在一起。浏览器在发起跨域 AJAX 前会先发一个 OPTIONS 预检请求如果你的拦截器对所有请求都要 tokenOPTIONS 直接 401浏览器的正式请求根本没机会发出去前端看到的就是“跨域失败”。我在上面拦截器代码里已经放了放行逻辑if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }更规范一点可以用 Spring 自带的判断import org.springframework.web.cors.CorsUtils; if (CorsUtils.isPreFlightRequest(request)) { return true; }同时保证 CORS 配置是对的。全局 CORS 可以在WebMvcConfigurer里加Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(https://front.example.com) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); }注意allowCredentials(true)时allowedOrigins不能写*必须写明确域名否则浏览器直接拒绝。这是我当时在 Safari 下怎么调都不通、换 Chrome 才看到控制台完整报错的原因。跨域问题的排查建议直接用 Chrome 的 Network 面板看 OPTIONS 请求的响应头比猜快得多。4. 静态资源、视图解析与请求响应的连带坑拦截器消停之后静态资源又开始闹。CSS、JS 全部 404接口倒是全好了。4.1 静态资源 404 的排查思路先别急着加资源映射先想一个问题你的项目里是不是也加了EnableWebMvc如果是恭喜你找到了问题根源。原因在第 3 节说过EnableWebMvc把 Boot 的默认静态资源映射干掉了classpath:/static/下的文件全部失去访问路径。去掉EnableWebMvcSpring Boot 3 默认会自动把以下位置映射为静态资源classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/。如果你的文件一直放在src/main/resources/static/css/app.css访问路径应该是/css/app.css不需要自己配。特殊情况才需要手工加资源映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file:/data/upload/) .setCachePeriod(3600); }这种一般用于把服务器磁盘上的目录暴露出来。要注意的是自定义/files/**这类路径时别和 Controller 的GetMapping(/files/{*path})撞车否则 Spring 会因为路径冲突报 ambiguous mapping两个都失效。我升级时就因为本地测试环境保留了一个旧的/files/**映射结果跟新版 Controller 撞了报错信息还特别不明显排查了很久。4.2 视图解析与转发/重定向的细节如果项目还在用 JSP新版本里要格外留个心眼。Spring Boot 3 以 jar 方式打包时内置容器对 JSP 的支持非常有限InternalResourceViewResolver经常出现“能找到视图但渲染 500”的尴尬。强烈建议趁升级机会把视图层迁到 Thymeleaf或者坚持 JSP 就改打成 war 包部署到外部容器。Thymeleaf 在 Spring Boot 3 下基本是零配置加依赖后把页面放到src/main/resources/templates/Controller 返回字符串即可GetMapping(/page) public String page() { return index; }再提醒一个容易忽略的细节forward:和redirect:的行为差异。forward:是服务端内部转发浏览器 URL 不变请求会再走一遍 DispatcherServlet 的流程但不会重新触发已经执行过的拦截器的preHandleredirect:是让浏览器重新发起请求URL 会变会完整走一遍新的请求链路。写权限控制逻辑时想清楚用的是哪种不然会出现“明明要跳转结果被拦截器拦下来”的乌龙。另外顺带提一句消息转换器。如果你在WebMvcConfigurer里重写了configureMessageConverters它会清空默认的消息转换器列表Jackson 就不会自动生效表现为接口返回的数据变成纯字符串或者直接 406。如果没有特别明确的需求优先用extendMessageConverters做追加而不是覆盖。这是升级后一个比较典型的隐藏坑尤其是老项目里如果有人为了“定制 JSON”重写过这个方法。5. 顺带把 springmvc工作流程彻底捋一遍聊到这里其实已经把 SpringMVC 工作流程里最容易出问题的几个环节全部踩了一遍。借着这次升级我把整套流程重新梳理一遍这部分对你理解新版本和面试都有用。5.1 一次请求从头到尾经历了什么环节核心组件一句话职责1. 入口DispatcherServlet前端控制器统一接收 HTTP 请求2. 找 handlerHandlerMapping根据 URL 找到对应的 Controller 方法3. 适配调用HandlerAdapter执行定位到的方法处理参数绑定、校验4. 前置/后置HandlerInterceptor请求前、handler 后、完成后三段拦截5. 结果封装ModelAndView / HttpMessageConverter返回视图或直接输出 JSON6. 视图渲染ViewResolver View把逻辑视图名解析成真实视图并渲染一次请求进来后完整链路是这样的请求先落在DispatcherServlet。它是 SpringMVC 的“中央调度器”所有 handler 的查找、适配、结果处理都从这里开始但它自己不做业务。HandlerMapping根据请求 URL 和 Controller 上的RequestMapping生成匹配结果。默认实现是RequestMappingHandlerMapping新版本里它用的路径匹配器就是 PathPatternParser。匹配结果里除了 handler 方法本身还有一组需要执行的拦截器。HandlerAdapter开始调用 handler。这一步会做参数解析PathVariable、RequestParam、RequestBody全部在这里绑定。参数绑定出错时比如 JSON 格式不对会抛HttpMessageNotReadableException这就要靠ControllerAdvice接住。拦截器链路在这个阶段起作用。顺序是preHandle进入 handler 前→ 业务方法 →postHandlehandler 返回后、视图渲染前→afterCompletion请求结束。Controller 方法执行完结果的归途分两种方法上有ResponseBody或者类上有RestController时RequestMappingHandlerAdapter会用HttpMessageConverter默认是 Jackson 的MappingJackson2HttpMessageConverter把对象序列化成 JSON 写回否则返回的字符串会当作逻辑视图名交给 ViewResolver。ViewResolver拿到逻辑视图名后解析成真实视图。Thymeleaf 有ThymeleafViewResolverJSP 有InternalResourceViewResolver解析成功后由View.render输出 HTML。5.2 新版本里工作流程里哪些环节悄悄变了对照上面流程升级后的变化集中在几个地方第 2 步的匹配器换了。AntPathMatcher变成PathPatternParser规则和性能都变了。权限系统里如果用了/**之类的表达式要按新规则重新验证一遍。第 4 步的拦截器简化了。HandlerInterceptorAdapter删除接口方法本身就是默认空实现直接implements HandlerInterceptor就行。第 5 步的错误处理更规范了。Spring 6 中ResponseEntityExceptionHandler默认生成ProblemDetailRFC 9457 格式响应体异常结构变成{ type: ..., title: Bad Request, status: 400, detail: ... }做全站统一报错时比之前的自定义 Map 更规范。第 5 步的异步支持更完善。DeferredResult、CompletableFuture这类异步返回在 Spring 6 里是原生支持的适合对接慢接口、消息队列回调不会因为线程阻塞占满容器线程池。内容协商策略收敛了。老版本可以通过 URL 后缀.json、.xml来协商响应格式新版本默认不再支持后缀匹配必须通过Accept请求头来定。如果你有/user.json这种老接口升级后大概率 404要改用Accept: application/json。这个问题很多 migrate 项目都会遇到前端和后端要同步改造。6. 高频问题速查表与升级实操清单最后把这次升级过程中遇到的所有问题汇总成一张速查表再给你一份升级前自查清单。6.1 常见报错与解决方案对照表现象原因解决方案编译报错package javax.servlet does not existServlet 命名空间迁移全部替换为jakarta.servlet运行时报NoClassDefFoundError: javax/servlet/...第三方依赖残留旧 Servlet APImvn dependency:tree排查后升级或排除接口 404URL 最后带斜杠PathPatternParser 不做尾斜杠匹配前端去掉斜杠或临时切 ant_path_matcher静态资源 404误用EnableWebMvc导致默认映射丢失去掉该注解或手动补 resource handler拦截器完全不执行配置类没被扫描 / 路径写错检查包扫描、addPathPatterns写成/**拦截器只拦住部分接口*与**通配符理解错误用/**匹配多段路径跨域请求失败、控制台报 OPTIONS 401拦截器拦截了预检请求preHandle放行 OPTIONS /CorsUtils.isPreFlightRequestHandlerInterceptorAdapter找不到Spring 6 已移除该类改为implements HandlerInterceptorJSP 渲染 500Boot 3 jar 打包对 JSP 支持弱迁移 Thymeleaf 或改 war 部署JSON 输出异常或 406configureMessageConverters覆盖默认转换器改用extendMessageConverters/user.json老接口 404新版本不支持后缀内容协商改成Accept请求头协商6.2 升级前建议你按这个清单自查先统一 JDK 17再动框架版本避免环境变量和编译目标互相干扰。全局搜一遍javax.servlet、javax.validation、javax.annotation替换成 jakarta 同名包。检查所有extends HandlerInterceptorAdapter的地方改成implements HandlerInterceptor。用mvn dependency:tree排查旧 Servlet API对每个第三方依赖确认它是否支持 Jakarta。对着接口清单测试一遍 URL重点看尾斜杠、大小写、URL 编码前端如有拼斜杠的习惯必须改。检查拦截器、过滤器、Spring Security 的路径规则如果 Security 还在用antMatchersSpring Security 6 已经把它标记为弃用并建议移除统一换requestMatchers。静态资源和 JSP/模板确认EnableWebMvc没被误加、资源路径没冲突、模板方案在新版本可用。最后再分享一个我自己的习惯升级这种事别指望一次到位也别怕当场报错。报错日志其实是最诚实的老师它告诉你哪里变了、哪里漏了。我每次升级完都会先跑一遍全链路接口测试再用浏览器把页面全部点一遍把 404、406、500 这些问题在前置环境里全部炸出来而不是等上线后让用户帮你测。我在这个项目里踩过的最大教训是改动之前先把“旧行为”和“新行为”的差异表列出来逐项核对而不是看到一个报错改一个。如果你正在做 SpringMVC 升级强烈建议把我这份清单拿着一边升级一边打勾能少走一半弯路。