ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringMVC升级排坑:Maven依赖、Tomcat与拦截器配置指南

SpringMVC升级排坑:Maven依赖、Tomcat与拦截器配置指南 新版本SpringMVC升级完只能对着报错日志干瞪眼这种体验大概每个搞Java的都经历过。前阵子把一个基于Maven构建的模块化SpringMVC项目从5.x往新版本上迁顺带着换IDEA里的Tomcat容器配置中间踩了一串坑从项目起不来、接口404到拦截器不生效每个问题都得翻官方文档加源码才能定位。这篇文章把整个排查链路和修复方案完整写出来给正准备升级或者已经在升级路上卡住的同学一个参考。这个项目本身是多模块结构父POM管理公共依赖子模块按功能拆分最终通过IDEA配置Tomcat容器启动。升级的核心动因是新版本带来的性能提升和长周期维护支持但新版本也意味着Java版本基线、Servlet命名空间、路径匹配策略都在变。文章内容的适用范围很明确SpringMVC旧项目升级到新版本使用Maven构建依赖IDEA配置Tomcat启动并且项目里使用了拦截器做统一鉴权或日志处理的场景。1. 升级后的第一道坎Maven模块化依赖的版本冲突这种多模块项目升级最先爆雷的通常不是代码而是Maven依赖。SpringFramework新版本把依赖拆得比旧版更细且各模块版本必须严格一致。很多模块化项目为了让子模块灵活一点会在各自POM里直接声明Spring相关依赖没有统一放到父POM的dependencyManagement里。结果就是spring-webmvc升到新版本了spring-context还是旧的某些传递依赖甚至拉到了完全不着调的版本启动时候直接给你抛NoSuchMethodError或者ClassNotFoundException。排这个问题第一步就是把依赖树拉出来看mvn dependency:tree -Dincludesorg.springframework:* -Dverbose看看所有spring-*模块的版本到底统一在哪个版本上。如果发现某个子模块单独引了旧版本马上改到父POM统一管理。正确的做法是在父POM里用dependencyManagement把所有Spring核心模块的版本锁死子模块只声明groupId和artifactId不写version。这样能省掉大量莫名其妙的问题。依赖树干净之后还要检查是否引入了重复的servlet-api。SpringMVC新版本对Servlet容器的版本基线有明确要求如果你的项目里还显式引了旧版的javax.servlet-api且Tomcat版本也偏老启动阶段就会出现各种类加载冲突。建议把servlet相关的依赖全部交由Tomcat容器提供Maven侧只保留provided作用域或直接去掉。这是模块化项目里最常见的坑因为子模块各自引依赖谁也没注意到重复。另外一个隐藏很深的坑是spring-web和spring-webmvc版本不一致这种情况在只用dependencyManagement管版本但子模块间传递依赖时还是可能发生。最终极的检查方法是看mvn dependency:tree里所有org.springframework组的模块是否完全一致不一致就回到父POM重新锁版本。2. javax到jakarta新版本带来的包名迁移SpringFramework新版本一个最大变化是把Servlet相关API从javax.servlet迁到了jakarta.servlet。很多老代码里写的import javax.servlet.http.HttpServletRequest全部要改成import jakarta.servlet.http.HttpServletRequest。这个改动量看着不大但当你项目里一百多个类都有这种import的时候光替换就能折腾一阵子。我当时采取的办法是先在IDEA里做全局替换把javax.servlet整体替换为jakarta.servlet然后重新编译靠编译器报错逐个找漏网之鱼。这里有个细节要注意老项目里可能会自定义一些Filter比如登录校验Filter、字符编码Filter这些类的包名必须同步替换。如果你的项目里还有自定义的HandlerInterceptor其preHandle方法的参数签名同样涉及HttpServletRequest和HttpServletResponse不换包名的话过不了编译。这里要特别提一下Tomcat版本的问题。javax.servlet对应的是Tomcat 9及以下新版本SpringMVC如果要跑在Tomcat上建议直接上Tomcat 10.1.x因为这个版本内置的是Jakarta Servlet 6.0和Spring新版本配合最稳。如果你用的是Tomcat 10.0.x注意它对应Servlet 5.0功能上也能跑但某些API细节略有差别最省心的还是10.1.x。在IDEA里配置Tomcat的时候要注意老项目经常会遇到一个情况项目编译没问题、war包也打出来了但Tomcat启动日志里出现ClassNotFoundException: jakarta.servlet.*之类的问题。这多半是Tomcat版本太老跑不了Jakarta API或者Tomcat版本是10但lib目录下还有旧版的servlet-api.jar在捣乱。确认Tomcat版本、确认lib目录干净、确认项目引用的API命名空间一致这三点做到位这一关就能过。还有一个容易忽略的扫描场景是web.xml里的listener、filter等配置。如果web.xml里还在用老的监听器类或者配置里写了旧的类名Tomcat容器启动时同样会报类加载错误。升级之后最好把web.xml原有的Servlet声明全部review一遍确认引用的类都换到了新包名。而且新版本SpringMVC本身就不推荐在web.xml里写大量配置往后逐步注解化才是正路。3. IDEA配置Tomcat启动war包和war exploded的取舍IDEA配置Tomcat启动SpringMVC项目常规操作是Run/Debug Configurations里新建一个Tomcat Server - Local然后在Deployment页签把项目的war或war exploded加进去。这一步看似简单但踩坑的点非常细。对于模块化项目最容易出现的问题是部署的artifact选错。如果选的是war包模式IDEA每次启动前都会重新打war包再部署整个过程偏慢而且如果编译输出目录有问题打出来的war包内容就会残缺启动后部分模块的类找不到。而war exploded模式是把编译后的classes、资源和依赖库直接按目录结构展开部署启动速度快排查问题也方便适合开发调试。建议在IDEA里日常开发用war exploded发布时再用Maven打包生成正式war。另一个坑和模块化结构强相关多模块项目部署时只有被添加到部署包里的模块才会被Tomcat加载。比如你的项目分了common、dao、service、web四个模块但Deployment里只选择了web模块的war包IDEA会按依赖关系自动带上被依赖的模块通常没问题。但如果你用了一些特殊的依赖配置比如某个模块通过system scope引用本地jar或者模块间依赖没在POM里声明全IDEA的部署包就不一定会带上对应模块的class启动后就是各种ClassNotFoundException。解决方式是确认Deployment的archive里实际包含哪些模块输出。在Artifacts配置页面点开Web Application Exploded的目录结构能看到每个模块是否被拉进来了如果缺模块就手动添加。还有一个相关坑是IDEA的Tomcat启动时classpath顺序问题有时候两个jar里有同名类Tomcat加载哪个完全看classpath顺序出现奇怪的AbstractMethodError时可以先想想是不是某个依赖包被重复打包了。我实际开发中用war exploded的部署模式配好之后启动非常顺改代码热部署也快。如果遇到Hot Swap失败或者资源加载不到的情况优先检查Artifacts里是否有重复的classes目录或多余的jar包清理一下再启动。4. 拦截器不生效新版本路径匹配策略变了项目正常运行后紧接着遇到的问题就是登录拦截器不生效。代码逻辑没改、spring-mvc.xml里的配置也没动但请求就是不进拦截器或者只有部分路径进了。这个坑整整排查了半天最后定位到是Spring新版本把默认路径匹配解析器从AntPathMatcher换成了PathPatternParser两者在路径匹配规则上有细微差异。Spring 5.3开始引入PathPatternParser到了新版本spring-webmvc里的默认匹配策略就成了PathPattern。如果你的拦截器配置里用了比较特殊的路径表达式比如带**的多级通配、或路径末尾有斜杠的写法两种解析器的处理结果可能不一样。最典型的表现是mvc:exclude-mapping path/resources/**/之类排除路径失效导致静态资源也被拦截器拦截或者反过来本应被拦截的路径没进拦截器。排查方式可以靠日志。给拦截器的preHandle方法里加一行日志输出然后在浏览器里请求目标路径看日志有没有打印能快速确认拦截器是否被匹配到。如果没打印八成就是路径匹配规则的问题。解决方式有两种。一种是把路径匹配器切回AntPathMatcher保持和旧版本行为一致mvc:annotation-driven mvc:path-matching suffix-patternfalse trailing-slash-matchtrue/ /mvc:annotation-driventrailing-slash-matchtrue是旧版默认行为新版默认不再自动匹配带尾斜杠的路径。另一种是适应新版规则把配置里的路径表达式调整成PathPattern的写法。PathPattern解析器要求的表达式更严格例如/user/**这类写法和AntPathMatcher不太一样/user/*只匹配一级路径/user/**匹配多级。如果原来表达式的写法在旧版下模糊匹配能过但新规则下就是匹配不到精确路径。还有一块是HandlerInterceptor的注册方式。新版本更推荐通过Java配置类来实现WebMvcConfigurer接口重写addInterceptors方法完成注册比在XML里配mvc:interceptors要直观得多而且类型安全、不容易写错。比如这样一个配置类Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /static/**, /error); } }这种配置方式的优势是排除路径和拦截路径都在同一个方法里一眼能看全后期改动也方便。但要注意配置类必须能被Spring扫描到如果你的配置类放在了子模块里而子模块没被web模块依赖就不会生效。另一个容易踩的坑是多个拦截器时的执行顺序。新版本中拦截器注册顺序就是执行顺序preHandle按注册顺序执行postHandle和afterCompletion按注册顺序的逆序执行。如果多个拦截器之间存在依赖关系比如一个拦截器往request里存了用户信息后一个拦截器再取出来用顺序搞反了就会拿到null。5. 静态资源访问404ResourceHandler配置需要注意的细节SpringMVC新版本中静态资源处理相关的配置也发生过变化。很多老项目里会配置类似mvc:default-servlet-handler/或者自定义的ResourceHandler来映射静态资源。升级后发现CSS、JS、图片全部404页面样式完全丢失。新版本中如果开启了PathPatternParserResourceHandler的路径匹配也受新规则影响。我遇到的问题是原来配置的/static/**映射到classpath下的/static/目录但新版中某些路径写法导致映射没被正确识别。解决办法是在WebMvcConfigurer里重写addResourceHandlers方法Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/static/**) .addResourceLocations(classpath:/static/); }这样等于用代码明确指定了静态资源的URL路径和物理路径之间的映射关系绕开了XML配置和路径匹配解析器之间的兼容歧义。还有件事需要注意如果项目之前用mvc:default-servlet-handler/托管那些没有自定义映射的静态资源新版本里这种配置依然能用但要确保对应的DefaultServlet在你的Tomcat里确实是可用的且不会被其他Filter拦截掉。某些环境下静态资源400或404多半是请求在到DefaultServlet前就被过滤器拦了比如前面提过的拦截器排除路径没写对。6. 请求参数和JSON序列化新版本里的兼容性改动SpringMVC新版本底层用的是Spring新一代的HTTP消息转换体系对JSON序列化的支持细节也有变化。老项目如果用的是MappingJackson2HttpMessageConverter加自定义ObjectMapper的套路升级后需要重新确认一下配置是否依然生效。我遇到的场景是接口返回的JSON字段名和客户端对不上。排查发现是项目里原来手动配置的ObjectMapper没有在Spring初始化时自动注入到消息转换器里新版本对Bean的装配顺序做了调整导致自定义ObjectMapper生效的位置变了。解决办法是在WebMvcConfigurer里显式配置消息转换器Override public void configureMessageConverters(ListHttpMessageConverter? converters) { MappingJackson2HttpMessageConverter converter new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(new CustomObjectMapper()); converters.add(converter); }另外如果你依赖fastjson或者其他JSON库作为消息转换器实现升级新版本后建议尽快迁到Jackson。Spring新版本对Jackson的整合最深入其他第三方库能不能跟上新版本的Servlet和Spring API变化存在不确定性。日期格式处理、空值字段是否输出这类老问题在新版本中一样需要重新确认。特别是LocalDateTime类型的字段如果没加JsonFormat注解或没配置JavaTimeModule序列化结果会是一串数组数字看起来跟踩了坑一样。7. 排错思路总结升级SpringMVC新版本时要按这个顺序查回头总结整个升级过程我把遇到的坑整理成了一套排查链路给后面再升级的人一个参考。第一步确认Java版本符合要求。新版本SpringFramework要求Java 17及以上如果本地还是Java 8或11先升级JDK。第二步用Maven dependency插件拉一遍完整依赖树确认所有spring-*模块版本一致确认没有重复的servlet-api、javax.servlet相关包。第三步检查代码里的import包名把所有javax.servlet全部换成jakarta.servlet编译通过后再开始配置容器。第四步确认Tomcat版本首选Tomcat 10.1.x里面内置的Jakarta Servlet API和Spring新版本匹配最完美。第五步在IDEA里配置Tomcat部署时选war exploded模式打开Artifacts结构检查部署包内容是否完整把缺失模块补上。第六步启动项目后先用最简单的接口测通链路确认Spring容器能正常初始化再开启拦截器等AOP相关功能。第七步如果拦截器不生效、路径匹配行为怪异先把路径匹配策略切回AntPathMatcher验证假设确认问题出在路径解析器上之后再决定是调整表达式还是移植到新规则。第八步遇到静态资源404、JSON序列化结果不对这类问题检查ResourceHandler和MessageConverter的配置优先用JavaConfig的方式重写一遍。这套顺序建立在从外到内的排查思想上先解决依赖和容器层面的问题再解决应用代码层面的问题。每个阶段都用最小可复现的方式验证能显著缩小定位范围不会陷入改一行测一次还是不对的死循环。我在实际升级完成后最大的体会是新版本SpringMVC对配置的规范性要求更高了。老版本很多写法都能凑合跑新版本则倾向于把歧义和隐式行为都收敛掉路径匹配更严格、命名空间更统一、配置方式更推荐JavaConfig。适应这种变化的过程就是重新梳理项目配置的过程虽然折腾但改完之后项目的结构约束更清晰后续维护成本反而降了不少。如果你的项目也准备升级建议先在一个分支上把依赖、包名、容器版本都调整好跑通最小链路再逐步合入其他业务模块别一次迁移大动干戈。
返回列表