SpringMVC模块化项目实战:DispatcherServlet与拦截器机制解析
2026/9/9 9:59:20 网站建设 项目流程

1. 项目概述与SpringMVC核心机制

1.1 为什么到现在还要学SpringMVC

我接触SpringMVC的时间不算短,从早期SSH时代转过来的人都知道,SpringMVC上手的那一瞬间,那种"原来Web层可以这么清爽"的感觉有多强烈。现在Spring Boot大行其道,很多新项目直接用spring-boot-starter-web,连web.xml都不见了,但底层跑的还是SpringMVC那套组件。面试问框架原理,问DispatcherServlet的工作流程,问HandlerInterceptor的执行时机,问HandlerMapping怎么找到Controller的,这些照样是高频问题。

这篇内容我会从一个完整的、基于Maven构建的SpringMVC模块化项目出发,带着大家把环境搭建、核心配置、拦截器实现、常见坑位全部过一遍。无论你是刚学Java Web的新手,还是用了Spring Boot但没摸过SpringMVC底层的人,这一篇的内容都能对得上。

先看一个最核心的问题:SpringMVC到底解决了什么问题?在没有SpringMVC之前,我们用Servlet写Web接口,一个类继承HttpServlet,重写doGet、doPost,手动从request里拿参数,手动setContentType,手动把JSON字符串写回response。一个业务方法就要一个Servlet类,然后去web.xml里配Servlet映射。接口多了之后,web.xml越来越长,类的数量也越来越多,关键是每个Servlet里都有大量重复的"取参数-转类型-写响应"样板代码。

SpringMVC的核心思路,就是把"请求如何找到处理方法"这件事从程序员手里接过来。你只需要写一个普通的Java类,在方法上标注@RequestMapping之类的注解,方法参数直接写你需要的数据类型,返回值直接写你要渲染的数据,剩下的参数绑定、类型转换、视图解析、响应输出全部由框架完成。程序员从"处理HTTP协议细节"中解放出来,只关注业务逻辑本身。

1.2 一次完整请求在SpringMVC中的整个流程

理解SpringMVC,第一件事就是理解DispatcherServlet。这个东西是整个SpringMVC的入口,所有的请求先经过它,再由它分发给各个组件去处理。

一次完整的请求流程大概是这样的:

  1. 浏览器发送请求,比如GET /user/list,Tomcat收到后根据web.xml或Servlet 3.0的配置,把请求交给DispatcherServlet。
  2. DispatcherServlet拿着请求的URL去找HandlerMapping。HandlerMapping的作用就是维护"URL和Handler方法之间的映射关系"。它内部会扫描所有标注了@RequestMapping的方法,把注解里的路径值和Controller类实例、Method对象关联起来,存到一个Map结构里。
  3. HandlerMapping返回一个HandlerExecutionChain,里面包含了找到的Handler方法,还有一串拦截器(HandlerInterceptor)。
  4. DispatcherServlet接着把请求交给HandlerAdapter去执行。为什么需要Adapter?因为HandlerMethod的类型有很多种,如果你用的是@Controller,方法是普通的方法调用;如果你用的是Controller接口(老写法),那要调用handleRequest方法;还有HttpRequestHandler这种写法。Adapter把这个差异屏蔽掉了,让DispatcherServlet不用关心Handler具体长什么样。
  5. HandlerAdapter执行拦截器的preHandle方法,然后反射调用Controller的方法,拿到返回值。
  6. 返回值的处理分两种情况:如果方法标注了@ResponseBody,返回值会被HttpMessageConverter直接转成JSON字符串写回浏览器,不经过视图解析;如果没有@ResponseBody,返回值会被当成逻辑视图名,交给ViewResolver解析成物理视图,比如JSP页面,然后渲染后返回。
  7. 响应返回给DispatcherServlet,DispatcherServlet再执行拦截器的afterCompletion方法,如果有异常还会走HandlerExceptionResolver,最后响应回到浏览器。

这整个流程里面最值得琢磨的一件事是:HandlerMapping为什么能那么快找到对应的Handler?这里面SpringMVC用了一个比较巧的设计,在应用启动的时候就已经把所有RequestMapping信息扫描并缓存好了。所以每次请求来的时候不是去重新扫描注解,而是直接查缓存,这也是SpringMVC请求性能不错的原因之一。

2. 基于Maven构建SpringMVC模块化项目的完整实操

2.1 为什么选择Maven多模块结构而不是单模块

很多初学者起步的时候习惯创建一个普通的Maven Web项目,pom.xml里堆一堆依赖,src/main/java、src/main/resources、src/main/webapp全部放在一个module里。这种单模块结构在demo阶段完全没问题,但到了真实项目,问题就开始暴露了。

比如同一个项目里既有Controller又有Service又有Mapper,如果某一天你想把Service层单独抽出来给另一个项目复用,你会发现这几乎做不到,因为Service和Controller在同一个模块里,必然会把整个Web层的东西都带过去。再比如多个人协同开发时,所有人都在同一个模块里提交代码,冲突率高,构建时间也长。

模块化项目的思路,是把项目拆成多个有层次的子模块,模块之间通过Maven依赖管理串联起来。常见的拆分方式是:

  • 父POM:负责统一管理依赖版本、插件配置、公共属性,不写业务代码。
  • 公共模块(common):放通用的工具类、常量、统一返回结果封装、异常处理类。
  • 数据访问模块(dao/mapper):放MyBatis或者MyBatis-Plus的Mapper接口,以及对应的XML文件。
  • 业务模块(service):放Service接口和实现类,依赖dao模块。
  • Web模块(web/controller):放Controller、拦截器、配置文件,依赖service模块。

这样做的好处有三个。第一,依赖方向是单向的,Web依赖Service,Service依赖Dao,Dao依赖Common,模块之间不会出现循环引用,代码边界清晰。第二,每个模块可以独立测试,比如Service模块可以单独写单元测试,不需要启动Tomcat。第三,模块复用性高,以后如果要做微服务拆分,Service模块基本可以原封不动地搬走。

2.2 父POM与子模块pom.xml的层级配置

这里我用具体代码演示一下到底怎么配。假设你的项目名叫demo-springmvc,第一步是创建父工程。

在IDEA里新建一个空Maven项目作为父工程,只需要保留pom.xml,src目录直接删掉。父pom.xml的关键配置是这样的:

<groupId>com.demo</groupId> <artifactId>demo-springmvc-parent</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>5.3.25</spring.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.5</version> </dependency> </dependencies> </dependencyManagement>

注意packaging必须是pom,这样它才是一个纯粹的聚合工程。dependencyManagement的作用是声明版本号,子模块引用依赖时不需要再写version,由父工程统一控制。这一点在多模块项目里特别重要,否则每个子模块各写各的版本,升级时容易漏改。

然后创建子模块,通常是四个:demo-common、demo-dao、demo-service、demo-web。IDEA里右键父工程,New -> Module,选择Maven,输入模块名即可。子模块pom.xml会自动多出parent节点:

<parent> <groupId>com.demo</groupId> <artifactId>demo-springmvc-parent</artifactId> <version>1.0.0</version> </parent> <artifactId>demo-common</artifactId>

模块之间的依赖关系,在各自的pom里配置。比如demo-dao依赖demo-common,这样配置:

<dependencies> <dependency> <groupId>com.demo</groupId> <artifactId>demo-common</artifactId> <version>1.0.0</version> </dependency> </dependencies>

demo-service依赖demo-dao,demo-web依赖demo-service。

2.3 SpringMVC核心依赖的引入与版本选型

在demo-web模块的pom.xml里,需要引入SpringMVC相关的核心依赖。这里给出一个最小可用集合:

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies>

关于版本选型,说两个实际经验。

第一,spring-webmvc的版本和JDK版本必须匹配。Spring 5.x要求JDK 8+,如果你的服务器还是JDK 7,只能用Spring 4.x。编译的时候如果出现UnsupportedClassVersionError,先查这个。另外Spring 6.x要求JDK 17+,如果你的项目还在JDK 8,别硬上Spring 6。

第二,servlet-api的scope一定要用provided。因为这个依赖由Tomcat容器提供,如果用了默认的compile,部署到Tomcat时会和Tomcat自带的servlet-api冲突,启动时经常报NoSuchMethodError或者LinkageError,看起来非常莫名其妙。

jackson-databind是用来做JSON序列化和反序列化的。Controller方法返回对象,标注@ResponseBody后,HandlerAdapter会调用StringHttpMessageConverter或者MappingJackson2HttpMessageConverter把对象转成JSON。如果pom里没引jackson,你会看到请求能到Controller,但返回时报415或者406错误,那种错特别容易让人误以为是配置问题。

3. IDEA配置Tomcat容器启动SpringMVC项目的全流程

3.1 Tomcat的下载与IDEA集成

这一步很多人卡住不是因为不会,而是因为IDEA版本和Tomcat版本的兼容性问题。我建议用Tomcat 8.5或者9.0做SpringMVC 5.x项目的容器,Tomcat 10及以上版本的javax.servlet包名改成了jakarta.servlet,Spring 5.x不兼容。当然如果你用的是Spring 6,那就必须Tomcat 10+。

下载Tomcat很简单,去官网拿tar.gz或zip包,解压到本地目录,不需要安装。解压后目录结构要认识:

  • bin:启动和关闭脚本,startup.bat/shutdown.bat、catalina.bat等。
  • conf:server.xml、web.xml等核心配置文件。
  • lib:Tomcat运行需要的jar包,比如servlet-api.jar。
  • webapps:部署Web应用的默认目录。
  • logs:日志目录,排错时最常看的就是这个目录下的catalina.out或localhost.log。

打开IDEA,进入Run -> Edit Configurations,左上角加号,找到Tomcat Server -> Local。然后在Application server那里点击Configure,选择Tomcat解压目录,IDEA会自动识别版本。

这里有一个容易踩坑的地方:关联Tomcat的时候,IDEA会提示"Libraries"还是"Sources",一般选Libraries就行。如果你误选了Sources,IDEA会把Tomcat源码也拉进来编译,项目启动速度会明显变慢,有时候还会因为源码中某些特殊文件导致编译异常。

3.2 Artifact配置:war与war exploded的区别

IDEA里部署Web应用,必须配置Artifact。这个配置其实就是告诉IDEA"你的项目编译完之后,哪些东西要打包交给Tomcat去运行"。

打开Project Structure(Ctrl+Alt+Shift+S),找到Artifacts,点加号,Web Application: Exploded -> From Modules。这里会有两种选择:war和war exploded。很多人不管三七二一直接选war,结果每次改代码都要手动重新打包或者等IDEA重新构建,效率极低。

war是压缩包形式,部署时Tomcat需要先解压;war exploded是解压后的目录形式,IDEA可以直接把编译后的class和资源文件同步到Tomcat的Web应用目录里。开发阶段一定要选war exploded,这样修改代码后,只要Rebuild一下,IDEA自动把变更的class推到Tomcat里,配合Jrebel之类的热部署插件几乎可以做到秒级生效。

配置完Artifact后,还要回到Run Configuration的Deployment选项卡,把刚才创建的Artifact加进去,Application context设置为根路径/,这样访问的时候就不需要带项目名前缀。如果你设置了/springmvc-demo这种context,所有的请求路径都需要加上这个前缀,开发时很容易搞混。

3.3 启动参数与端口配置避坑

Tomcat在IDEA里启动时,实际上执行的是Tomcat的catalina脚本,IDEA通过JMX协议远程管理Tomcat的生命周期。所以你会在Run Configuration里看到两个端口:HTTP port和JMX port。

HTTP port就是服务端口,默认8080,如果你本机其他程序占用了8080,可以改成8081或者9090。JMX port用于IDEA连接Tomcat,如果同一个机器上起了多个Tomcat实例,JMX端口必须不同,否则启动时会报Address already in use: JVM_Bind。

还有一个经常被忽略的选项:在Startup/Connection标签页里,可以设置运行前执行的Maven任务。我建议把clean和compile两个任务加上,确保每次启动时用的都是最新编译产物。如果不加,偶尔会出现改了代码但运行时还是旧逻辑的诡异情况,排查半天才发现是IDEA没重新编译。

启动时如果控制台乱码,回到Help -> Edit Custom VM Options,在idea64.exe.vmoptions里加上-Dfile.encoding=UTF-8,重启IDEA后基本能解决。Tomcat本身的日志编码问题,可以在conf/logging.properties里把java.util.logging.ConsoleHandler.encoding改成UTF-8。

3.4 启动成功后如何验证环境是否正常

Tomcat启动后,控制台会输出类似"Server startup in [1234] milliseconds"的日志。此时先访问一下http://localhost:8080/,如果能显示Tomcat默认首页,说明容器正常。

但注意,这只能说明Tomcat起来了,不代表SpringMVC容器初始化成功了。要验证SpringMVC环境,必须访问一个实际的Controller接口。我习惯在项目里先写一个最简单的测试Controller:

@Controller public class PingController { @RequestMapping("/ping") @ResponseBody public String ping() { return "pong"; } }

然后浏览器访问http://localhost:8080/ping,如果返回pong,说明DispatcherServlet已经成功初始化,Spring容器也正常加载了。如果这一步出错,重点检查配置文件路径是否写对、web.xml中的spring配置加载路径是否正确、pom依赖是否缺失。

4. SpringMVC拦截器:从配置到源码级理解

4.1 拦截器与Filter的区别

SpringMVC的拦截器,也就是HandlerInterceptor,是很多人在项目里绕不开的一个功能点。它能拦截Controller方法调用,在请求进入Controller之前做校验,在处理完成后做日志或者清理工作。

使用拦截器之前,首先要搞清楚它和Servlet Filter的区别。Filter是Servlet规范里的东西,拦截的是Servlet的请求,所以它的执行顺序是在DispatcherServlet之前。也就是说,一个请求经过的链路是:Tomcat -> Filter链 -> DispatcherServlet -> preHandle -> Controller方法 -> postHandle -> afterCompletion -> Filter链的后续环节。Filter拿不到HandlerMethod的信息,只能拿到HttpServletRequest和HttpServletResponse。而HandlerInterceptor可以拿到HandlerMethod对象,所以它能够知道当前请求将要调用哪个Controller的哪个方法,这是Filter做不到的。

在SpringMVC项目里,鉴权、登录校验、接口幂等、日志记录这些需求,优先用HandlerInterceptor做,因为可以拿到方法级别的信息,配合自定义注解还能做更精细的控制。

4.2 HandlerInterceptor三个方法的执行时机与顺序

HandlerInterceptor接口定义三个方法:preHandle、postHandle、afterCompletion。很多人背过这三兄弟,但真正理解它们的执行时机的并不多。

preHandle在Controller方法执行之前执行,返回true表示放行,返回false表示拦截,此时整个请求链路直接中断,后续的拦截器和Controller都不会执行。这里有一个细节:如果preHandle返回false,SpringMVC不会再调用postHandle和afterCompletion,但Filter链的后续环节会恢复执行,因为请求还是回到了Servlet容器。

postHandle在Controller方法执行之后执行,但这个时候的"之后"有一个重要细节:如果Controller方法执行过程中抛出了异常,那么postHandle不会被调用。因为SpringMVC的实现里,postHandle是在HandlerAdapter的handle方法正常返回后,由DispatcherServlet的doDispatch方法调用;如果handle方法抛出异常,直接跳到了processDispatchResult,postHandle就错过了。

afterCompletion在请求结束之后执行,不管Controller方法是否抛异常,只要preHandle放行了,最终都会走到afterCompletion。这个方法最适合做资源清理、请求耗时统计。但注意,如果preHandle就返回false了,afterCompletion也不会执行。

多个拦截器的执行顺序遵循"第一层preHandle顺序执行,postHandle和afterCompletion倒序执行"的规则。比如拦截器A和B,先执行A.preHandle,再执行B.preHandle,然后执行Controller方法,之后先执行B.postHandle再执行A.postHandle,afterCompletion同理,先执行B的再执行A的。这有点像栈的弹出顺序,也是验证"拦截器是分层签入、对称签出"的一个体现。

4.3 编码实现一个登录校验拦截器

我用一个最常见的登录校验场景来演示。在真实项目中,登录校验几乎是每个RestController都要做的事情,但总不能每个接口都写一遍判断session的代码吧。用拦截器集中处理就是最优解。

定义一个拦截器类:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user == null) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } return true; } }

这里有两个容易被忽略的点。

第一个,handler instanceof HandlerMethod判断。为什么要有这一步?因为SpringMVC里,有一部分请求并不到Controller方法上来,比如静态资源映射的请求,它对应的handler类型是ResourceHttpRequestHandler。如果不对handler做类型判断,直接强转HandlerMethod会抛ClassCastException。

第二个,未登录时返回JSON而不是重定向到登录页。因为在前后端分离的场景下,前端拿到的是响应状态码和JSON数据,重定向反而会让前端困惑。返回401状态码比返回200然后前端自己去判断更符合RESTful规范。

然后在SpringMVC环境中配置这个拦截器。这里分两种情况:使用XML配置的,在springmvc.xml里加mvc:interceptors标签;使用Java Config的,重写WebMvcConfigurer的addInterceptors方法。我用Java Config演示,这是目前推荐的做法,也是Spring Boot诞生以来更主流的姿势:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/register", "/css/**", "/js/**", "/images/**"); } }

这里要理解addPathPatterns和excludePathPatterns的匹配规则。addPathPatterns("/**")表示拦截所有请求,excludePathPatterns表示排除指定的路径。路径匹配的规则和Spring的AntPathMatcher一致,/**匹配多级路径,/*只匹配一级。如果把/register/abc这种二级路径排在排除规则里,得写/register/**才能生效。

配置完之后,还要确认一件事:你的Java Config类有没有被Spring扫描到。在web.xml里配置DispatcherServlet时,会指定spring配置文件位置,比如contextConfigLocation指向classpath:springmvc.xml。如果用Java Config,需要在初始化参数里指定配置类。这里有个很经典的坑:开发者在某个配置类上标了@Configuration,但这个问题类在Spring扫描的包路径之外,导致配置根本没生效。如果拦截器不生效,第一步先检查这个。

4.4 拦截器源码执行顺序验证

为了验证拦截器的执行顺序真的符合预期,我在实际项目中特意做过一次实验,写了一个极简的拦截器,在每个方法里打印当前方法名和一个计数器:

public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion"); } }

配了三个这种拦截器A、B、C,然后请求一个Controller方法,控制台输出的顺序是:

A.preHandle B.preHandle C.preHandle Controller method invoked C.postHandle B.postHandle A.postHandle C.afterCompletion B.afterCompletion A.afterCompletion

这个结果验证了两点。第一,preHandle是链式顺序执行,任何一个返回false都会中断后续。第二,postHandle和afterCompletion是逆序执行,类似栈的弹栈过程。这个顺序在真实项目中处理跨拦截器的数据传递时很重要,比如A拦截器设置了ThreadLocal变量,B拦截器想取用,那么A.preHandle必须先执行才能保证B能看到A设置的值。

另外注意一个细节:postHandle和afterCompletion的执行时机,一个在视图渲染前,一个在视图渲染后。如果Controller返回的是@ResponseBody JSON数据,postHandle在SpringMVC写完JSON之后才会执行,而afterCompletion在请求完成之后执行。对于接口耗时统计,得在afterCompletion里计算,因为postHandle的时候JSON可能还没写完。

5. SpringMVC模块化项目的高频问题与排查实录

5.1 启动报错问题速查表

我从实际带项目经验中整理了一份问题清单,很多问题刚上手SpringMVC的人都会遇到:

现象可能原因排查思路
Tomcat能启动但访问404DispatcherServlet映射路径不对检查web.xml里servlet-mapping的url-pattern是否为/
启动时报ClassNotFoundException: DispatcherServletspring-webmvc依赖缺失或版本冲突检查pom.xml是否有spring-webmvc依赖
访问接口返回406缺少jackson依赖检查是否有jackson-databind
访问接口返回415Content-Type不匹配检查请求头是否有application/json
静态资源被拦截拦截器配置了/**且未放行静态资源在excludePathPatterns里排除
中文乱码编码过滤器未配置或配置顺序不对在web.xml配置CharacterEncodingFilter
拦截器不执行拦截器类未被Spring扫描到检查@Component注解和扫描包路径
接口返回JSON格式不对没有配置MappingJackson2HttpMessageConverter检查 mvc:annotation-driven 或@EnableWebMvc

5.2 过滤器导致的中文乱码问题

中文乱码这个问题在国产Web开发里几乎必现。POST请求中文乱码,80%的原因是CharacterEncodingFilter没有配置或者配置在DispatcherServlet之后。编码过滤器的执行时序必须在DispatcherServlet之前,所以web.xml里CharacterEncodingFilter的servlet-mapping顺序有讲究。

配置方法:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

forceEncoding设为true是关键,这样request和response都会强制使用UTF-8,不受请求头本身Content-Type的影响。如果只设了encoding,没有设forceEncoding,那么只有当请求头里没有指定charset时才会用UTF-8,一旦前端传了application/x-www-form-urlencoded这种头,编码又被带偏了。

另外,如果使用了Tomcat的Connector,别忘了检查server.xml里的URIEncoding。Tomcat 8.5及以上默认URIEncoding就是UTF-8,但如果你的Tomcat是改过的老版本配置,可能在Connector上设置了URIEncoding="ISO-8859-1",那GET请求的参数中文必然乱码。这也是一个特别隐蔽的坑。

5.3 静态资源被拦截的经典问题

在做SpringMVC项目的时候,很多人会遇到一个现象:JSP页面能通过InternalResourceViewResolver正常渲染,但页面上引入的css、js、images全部404或者返回Controller里的JSON数据。

这个问题的根源在于DispatcherServlet的url-pattern。如果配置的是/,那么除了JSP之外的所有请求都会被DispatcherServlet处理,包括css/js这类静态资源。DispatcherServlet发现没有对应的RequestMapping就会抛404。

解决方法有两种。第一种是在springmvc.xml里加:

<mvc:default-servlet-handler/>

这个配置的意思是:当DispatcherServlet找不到对应的Handler时,把请求转回给Servlet容器默认的DefaultServlet去处理。DefaultServlet是Tomcat自带的,专门用来处理静态资源的。

第二种方法是在Java Config里重写addResourceHandlers:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("/static/"); }

这种方法的好处是控制粒度更细,可以指定哪些路径是静态资源、对应哪个物理目录。但注意,一旦用了addResourceHandlers,默认的静态资源处理会被覆盖,如果前面还有mvc:default-servlet-handler也要同步调整。

5.4 拦截器不生效的排查流程

拦截器不生效时,我建议按照下面的顺序排查,每步都有明确结论,很快能定位:

第一步,确认"拦截器类"确实被Spring管理。在拦截器类上加上@Component注解,然后检查Spring扫描器有没有扫描到它所在的包。如果ComponentScan注解没覆盖到,拦截器类根本不会注册成Bean,自然也不会执行。

第二步,确认配置类被加载。如果你用的Java Config,看WebMvcConfigurer的实现类有没有被Spring扫描到。可以用一个简单的办法,在addInterceptors方法里打印一行日志,启动时看控制台有没有输出,没有的话就是配置类都没加载。

第三步,确认路径匹配规则。如果你配置了addPathPatterns("/"),然后被测的URL是http://localhost:8080/user/list,那这个路径是否被匹配,要看从项目根路径算起的完整路径。如果项目context-path设置了,比如/server,那么实际路径是/server/user/list,而SpringMVC的path匹配是基于context-path之后的路径,所以/user/list在addPathPatterns里依然匹配/

第四步,确认是否有其他Filter提前中断了请求。比如你写了一个Filter,在拦截器之前就把请求拦截了,那HandlerInterceptor可能永远不会执行。

这四步走完,几乎能解决所有"拦截器没生效"的问题。在我的经验里,遇到最多的就是第二种情况——配置类在包路径之外,或者配置类被重复加载了。

5.5 模块化项目打包部署的细节

模块化项目在本地IDEA里启动没问题,但到了打war包部署生产环境的时候,容易出现一些奇奇怪怪的问题。

第一个坑是web.xml的缺失。如果你的demo-web模块比较新潮,用了Servlet 3.0的注解式配置,那么打出来的war包可能没有web.xml。这在Tomcat 7及以上是支持的,但有些运维同事或者云平台的脚本还是默认Tomcat 6的规格,会直接报"web.xml is missing"。稳妥起见,还是保留一个web.xml,即使里面只写着metadata-complete="false"。

第二个坑是打war包的打包方式。在多模块项目中,父pom的packaging必须是pom,demo-web的packaging必须是war,其他模块是jar。如果demo-web的packaging忘了改成war,IDEA的Artifact配置里就找不到Web Application的选项。

第三个坑是配置文件的外置。生产环境的数据库密码、日志级别等肯定不能和本地一样。我习惯在demo-web模块里用Spring的PropertyPlaceholderConfigurer加载外部配置文件,通过-Dspring.profiles.active=prod参数来区分环境。模块化项目里,每个模块的resources目录下的同名文件会有优先级问题,这个很隐蔽,建议把公共配置放到demo-common里的一个独立分包下,和自动扫描的包路径错开。

6. 实操经验总结与后续扩展方向

这里聊一些我在实际项目中沉淀下来的经验。

第一件事,学习SpringMVC的时候,强烈建议自己动手把DispatcherServlet的doDispatch方法源码过一遍。这个方法的完整代码不到两百行,但它把SpringMVC的核心组件全部串联起来了。我之前带新人的时候,让他们做的第一件事就是在doDispatch方法里加断点,然后请求一个简单的接口,一步步看请求是怎么走的。这个方法虽然简单粗暴,但比读十篇博客都有效。HandlerMapping找Handler、HandlerAdapter调方法、ViewResolver解析视图、HandlerExceptionResolver处理异常,全部在这一个方法里能看到。

第二件事,SpringMVC的拦截器要和HandlerMethod的综合使用。拦截器里拿到的handler参数,可以强转成HandlerMethod,然后通过getMethod()、getBean()拿到Controller的方法对象和实例。这样就可以配合自定义注解做很多有意思的事情,比如权限控制的时候,在方法上标注@RequiresPermission("user:list"),拦截器里通过HandlerMethod读取这个注解,判断当前用户是否拥有对应权限,比写死路径匹配灵活得多。这个模式在很多企业级项目里是标配,建议大家都掌握。

第三件事,就是模块化项目的持续演进。SpringMVC只是Web层框架,真正的企业级项目还需要整合Spring、MyBatis等。当你手里的模块化项目跑通之后,可以继续做的扩展方向有三个:集成MyBatis完成完整的SSM整合;集成Shiro或者Spring Security做安全控制;再进一步,理解Spring Boot是如何在SpringMVC基础上做自动配置的。每一步踩坑的收获,都远大于单纯看API文档。

第四件事,多模块项目的开发规范一定要尽早定好。模块之间的依赖方向、包的命名规则、实体类放哪个模块、DTO放哪个模块,这些如果不提前约定,等到项目写到一半就会纠结得不行。我经历过一个项目,一个User实体类在common和dao里各有一个,两个类字段还不一致,Controller层和Service层来回转换的时候谁也不敢删,最后花了一整个迭代才清理干净。

SpringMVC这个框架虽然看起来是"老技术"了,但它的设计思想在Spring Boot里处处有体现。把SpringMVC的整个链路从请求进来到响应返回都理清楚,你后面理解Spring Boot的自动配置、理解各种Filter和Interceptor的执行链,都会觉得特别轻松。这也是我一直建议初学者不要上来就直奔Spring Boot的原因——地基不打牢,楼盖得再高也是虚的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询