深入解析SpringMVC请求处理流程:从DispatcherServlet到视图渲染
1. 从一次诡异的404说起:为什么必须搞懂SpringMVC流程
那天下午,我正对着屏幕发呆,一个刚入行的同事跑过来,脸上写满了困惑:“哥,我明明在Controller里写了@RequestMapping(“/user”),浏览器访问也显示路径没错,可为啥返回的就是个404?后端日志连个请求的影子都没看到。” 我让他把配置和代码发过来,扫了一眼,问题其实很简单:他的DispatcherServlet在web.xml里的<url-pattern>配的是*.do,而他访问的路径是/user/list。请求压根就没进SpringMVC的大门,直接被容器(比如Tomcat)当静态资源或默认Servlet处理了,自然404。
这个看似低级的问题,恰恰是理解SpringMVC工作流程最好的切入点。很多人学了SpringMVC,会写@Controller,会用@RequestMapping,但一旦遇到请求没进来、参数绑定不上、拦截器不生效、视图解析出错这些问题,就抓瞎了。其根本原因,是对“一个HTTP请求从浏览器发出,到最终渲染出HTML页面,中间到底经历了什么”这个过程缺乏全景式的认知。
SpringMVC的工作流程,不是死记硬背的“九大组件”或“八步流程图”,而是一个高度可定制、责任链清晰的请求处理流水线。理解它,就像是拿到了整个Web层的地图和开关总闸。你知道请求从哪个门进来(DispatcherServlet),经过哪些检查站(HandlerMapping,Interceptor),由谁负责接待和处理(HandlerAdapter,Controller),处理完的“伴手礼”(ModelAndView)又交给谁去包装和递送(ViewResolver,View)。掌握了这个流程,上面那个404问题,你一眼就能定位到是“入口映射”环节出了错;遇到参数绑定异常,你会知道是“处理器适配”环节的DataBinder在作祟;视图渲染乱码,那肯定是“视图解析”或“视图渲染”环节的编码没设对。
所以,今天我们不聊空洞的概念,就结合我这些年踩过的坑、调过的错,把SpringMVC这条流水线,从电源接通(请求到达)到产品出厂(响应返回),每一个环节的运作机制、可插拔的扩展点、以及最容易栽跟头的细节,给你彻彻底底地捋清楚。无论你是刚接触Web开发的新手,还是想深化底层理解的老兵,这篇详解都能让你对SpringMVC的掌控力,提升一个维度。
2. 核心枢纽:DispatcherServlet 如何成为总指挥
要理解流程,首先得认清谁是老大。在SpringMVC的世界里,DispatcherServlet就是这个无可争议的总指挥,它本质上是一个Servlet。但它的职责不是处理具体的业务逻辑,而是像一个调度中心,负责协调所有其他组件共同完成一次请求-响应周期。
2.1 初始化:九大核心组件的加载
当Servlet容器(如Tomcat)启动时,会初始化DispatcherServlet。这个初始化过程至关重要,它完成了SpringMVC“九大组件”的初始化。这些组件大多是接口,SpringMVC为我们提供了默认实现,但每一个都可以被定制替换。我习惯把它们分为三组:
第一组:请求路由组
- HandlerMapping(处理器映射器):它的任务是根据当前请求的URL,找到对应的处理器(Handler)。这个“处理器”通常就是我们写的
@Controller中的某个方法。常见的实现有RequestMappingHandlerMapping(处理@RequestMapping注解)、BeanNameUrlHandlerMapping(根据Bean名字映射)等。 - HandlerAdapter(处理器适配器):光找到处理器还不够,处理器有各种形态(比如基于
@Controller注解的、实现Controller接口的、或者HttpRequestHandler等)。HandlerAdapter的作用就是使用统一的接口去调用这些形态各异的处理器。RequestMappingHandlerAdapter就是用来适配@RequestMapping注解方法的。
第二组:请求处理组
- HandlerExceptionResolver(异常处理器):当处理器执行过程中抛出异常时,由它来捕获并决定如何处理,可以返回一个特定的错误视图或JSON数据。这是实现全局异常处理的核心。
- ViewResolver(视图解析器):处理器执行后返回一个逻辑视图名(比如
"user/list"),ViewResolver负责将这个字符串解析成一个具体的View对象(比如JstlView、ThymeleafView等)。 - View(视图):负责将模型数据渲染成最终的响应内容,比如生成HTML、JSON、XML等。
第三组:请求增强组
- MultipartResolver(文件上传解析器):如果请求是
multipart/form-data类型(文件上传),它负责将请求解析,方便我们获取上传的文件。 - LocaleResolver(区域信息解析器):决定当前请求使用的语言、区域等国际化信息。
- ThemeResolver(主题解析器):用于解析当前主题。
- FlashMapManager(Flash属性管理器):管理
FlashMap,用于在重定向时传递参数,解决POST-REDIRECT-GET模式下数据传递问题。
注意:这“九大组件”中,
HandlerMapping、HandlerAdapter、ViewResolver是我们最常打交道的。它们的初始化顺序和默认配置,都在DispatcherServlet的初始化策略(initStrategies方法)里。理解这一点,你就知道为什么自定义一个ViewResolver需要以@Bean的形式注入Spring容器。
2.2 请求入口:doService与doDispatch
当一个HTTP请求到达,容器会调用DispatcherServlet的service方法,最终会流转到核心的doDispatch(HttpServletRequest request, HttpServletResponse response)方法。这个方法,就是整个SpringMVC工作流程的完整代码级体现。我们后续的所有环节分析,都是对doDispatch方法执行过程的拆解。
在深入doDispatch之前,有个关键点常被忽略:DispatcherServlet有自己的WebApplicationContext。它通常是从根WebApplicationContext(ContextLoaderListener加载的)继承而来的子上下文,专门用于配置Web相关的Bean,如控制器、视图解析器等。这种父子容器的设计,实现了关注点分离。
3. 请求处理流水线:doDispatch 方法逐行拆解
现在,我们进入最核心的部分,跟着一个请求,走完它在doDispatch方法中的一生。我会结合关键代码逻辑和实际场景来讲解。
3.1 第一步:检查与预处理(文件上传)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; // 1. 检查是否为文件上传请求 processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); }流程的第一步,DispatcherServlet会检查当前请求是否是multipart(文件上传)类型。这是通过MultipartResolver组件完成的。如果是,它会将标准的HttpServletRequest包装成一个MultipartHttpServletRequest,方便后续通过getFile等方法获取上传的文件。这里一个常见的坑是:如果你配置了MultipartResolver(比如CommonsMultipartResolver),但没有正确设置最大文件大小、编码等参数,可能会导致大文件上传失败或请求解析异常,错误可能发生得非常早,甚至还没进入你的控制器。
3.2 第二步:寻找合适的处理器(HandlerMapping)
// 2. 为当前请求确定一个处理器执行链 mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; }getHandler方法会遍历所有已注册的HandlerMapping,询问它们:“这个请求你能处理吗?”第一个返回非空HandlerExecutionChain的HandlerMapping胜出。
HandlerExecutionChain非常重要,它不仅仅包含目标处理器(Handler),还包含了应用于该处理器的所有拦截器(HandlerInterceptor)。这就是为什么拦截器的配置通常和路径映射绑定在一起的原因。
为什么有时候明明路径匹配,却返回noHandlerFound?
- 最可能的原因:开头提到的,
DispatcherServlet的url-pattern没匹配上。 - 配置问题:
@Controller类没有被Spring扫描到(<context:component-scan>配置错误),或者RequestMappingHandlerMapping没有正确注册。 - 请求方法不匹配:你的控制器方法标注了
@PostMapping,但用GET请求访问。 - 路径匹配策略:Spring MVC默认使用Ant风格路径匹配。注意
/**和/*的区别。/**匹配所有路径(包括子路径),而/*只匹配一级路径。在Spring Boot中,如果配置了spring.mvc.servlet.path,也需要考虑在内。
3.3 第三步:获取适配器并执行拦截器前置处理
// 3. 获取能执行该处理器的适配器 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 4. 执行处理器执行链中所有拦截器的preHandle方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; // 如果某个拦截器返回false,流程直接终止,返回响应 }找到处理器后,需要找到能驾驭它的“适配器”HandlerAdapter。getHandlerAdapter方法会遍历所有适配器,找到第一个支持该处理器的。
紧接着,拦截器(Interceptor)的preHandle方法被调用。这是拦截器三大方法中的第一个,也是最常用的一个。它的返回值是布尔型:
true:继续执行后续拦截器和处理器本身。false:中断流程,DispatcherServlet认为该请求已被处理完毕,直接返回。注意,此时postHandle和afterCompletion方法也不会被调用。
拦截器使用心得:
preHandle非常适合做权限校验、日志记录、性能监控。比如,在这里检查Session中是否有用户登录信息,没有则返回false并重定向到登录页。- 执行顺序:拦截器的执行顺序与配置顺序一致。在
preHandle阶段,是按配置顺序正序执行的。 - 资源释放:如果在
preHandle里打开了数据库连接或文件流,务必在对应的afterCompletion里关闭,否则会造成资源泄漏,因为preHandle返回false时afterCompletion不会执行。
3.4 第四步:真正的业务处理(HandlerAdapter)
// 5. 由适配器实际调用处理器(我们的Controller方法),并返回ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());这是核心业务逻辑执行的地方。HandlerAdapter的handle方法做了大量我们看不见但至关重要的工作:
- 参数绑定(Data Binding):将HTTP请求参数(Query String、Form Data)、路径变量(
@PathVariable)、请求头(@RequestHeader)、Cookie(@CookieValue)、Session属性等,绑定到控制器方法的入参上。这是通过HandlerMethodArgumentResolver(参数解析器)家族完成的。常见的坑比如日期格式转换、自定义对象绑定,都需要在这里配置相应的Converter或Formatter。 - 消息转换(Http Message Conversion):对于
@RequestBody和@ResponseBody,会使用HttpMessageConverter(如MappingJackson2HttpMessageConverter)将请求体中的JSON/XML转换为Java对象,或将返回对象转换为JSON/XML写入响应体。这里常遇到序列化/反序列化失败,比如JSON字段名与Java属性名不对应、未知属性等,需要检查Jackson注解或配置。 - 调用控制器方法:上述准备工作就绪后,最终通过反射调用我们编写的控制器方法。
- 返回值处理:控制器方法可以返回多种类型:
String(视图名)、ModelAndView、@ResponseBody注解的对象、甚至void。HandlerAdapter需要处理这些返回值。对于String,它会包装成一个ModelAndView对象;对于@ResponseBody,它会通过消息转换器直接写回响应,后续的视图解析流程就会跳过。
3.5 第五步:拦截器后置处理与视图解析
// 6. 当默认视图名需要应用时,解析视图名(如果返回的ModelAndView不为空且视图不为空) applyDefaultViewName(processedRequest, mv); // 7. 执行所有拦截器的postHandle方法 mappedHandler.applyPostHandle(processedRequest, response, mv);applyDefaultViewName是一个细节:如果控制器方法返回的ModelAndView中的视图名称为空,但请求不是异步或重定向,则会尝试使用默认视图名(通常是请求路径)。
然后,执行拦截器的postHandle方法。此时处理器已经执行完毕,ModelAndView对象已经生成(但尚未渲染)。你可以在这里对模型数据(Model)或视图(View)进行最后的修改。注意:postHandle在异步处理场景下可能不会被调用!
3.6 第六步:处理返回结果——渲染视图或处理异常
// 8. 处理分发结果,这包括渲染视图、处理异常等 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); }doDispatch方法最后将收尾工作委托给processDispatchResult。这个方法逻辑同样关键:
private void processDispatchResult(...) throws Exception { boolean errorView = false; // 如果有异常发生(dispatchException不为空) if (exception != null) { // 使用HandlerExceptionResolver解析异常,得到一个新的ModelAndView (mv) mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } // 如果返回的ModelAndView不为空,且没有被标记为“已清理”(通常指重定向) if (mv != null && !mv.wasCleared()) { // 渲染视图 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { // 如果没有视图需要渲染(比如@ResponseBody已写回),只是确保响应流被刷新 if (logger.isDebugEnabled()) { logger.debug("Null ModelAndView returned, assuming response handled."); } } // 9. 无论成功与否,最终触发拦截器的afterCompletion方法 if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(request, response, null); } }视图渲染(render方法):这是ViewResolver和View组件登场的时候。render方法会遍历所有ViewResolver,尝试将逻辑视图名解析为具体的View对象。然后调用View的render方法,将模型数据合并到视图模板中,生成最终的响应内容输出给客户端。视图解析的常见问题:
- 视图找不到:检查
ViewResolver的配置(前缀、后缀)、视图文件的位置和名称。 - 模型数据在视图中取不到:检查往Model里添加数据的key,以及在视图(如JSP的EL表达式、Thymeleaf的变量)中引用的key是否一致。
- 乱码:确保
View(如JSP)的页面编码、HttpServletResponse的字符编码设置正确。对于JSON响应,检查HttpMessageConverter的编码配置。
异常处理(processHandlerException):如果前面任何步骤(包括拦截器的preHandle)抛出了异常,都会被捕获,并交给HandlerExceptionResolver处理。你可以通过@ControllerAdvice+@ExceptionHandler注解的方式,或者实现HandlerExceptionResolver接口,来定义全局或特定控制器的异常处理逻辑。这里的关键是理解异常处理的优先级和粒度控制。
拦截器收尾(triggerAfterCompletion):无论请求处理成功还是中途出现异常,只要preHandle返回了true,afterCompletion方法都会被调用。它像是finally块,适合进行资源清理、记录最终状态等操作。注意,它的执行顺序与preHandle相反,是倒序执行的。
4. 流程中的关键扩展点与实战避坑指南
理解了主干流程,我们来看看这条流水线上有哪些重要的“扩展点”和“故障点”。
4.1 拦截器(Interceptor)与过滤器(Filter)的抉择与协作
这是最容易混淆的概念。它们都能对请求进行预处理和后处理,但处于不同的层级和生命周期。
| 特性 | 过滤器 (Filter) | 拦截器 (Interceptor) |
|---|---|---|
| 规范 | Servlet 规范定义 | Spring MVC 框架定义 |
| 依赖 | 不依赖Spring,任何Java Web应用可用 | 依赖Spring MVC框架 |
| 作用范围 | 作用于所有进入容器的请求(静态资源、Servlet等) | 只作用于进入DispatcherServlet并由Spring MVC处理的请求 |
| 获取Spring Bean | 较难,需通过WebApplicationContextUtils | 很容易,本身由Spring管理 |
| 执行时机 | 在DispatcherServlet之前和之后执行 | 在DispatcherServlet内部,处理器执行之前、之后和完成之后执行 |
| 可获取信息 | 原始的ServletRequest/ServletResponse | 可以获取处理器(Handler)、ModelAndView等信息 |
实战建议:
- 用Filter的场景:解决跨域(CORS)、全局编码设置、日志记录(记录所有请求)、防止XSS攻击的请求参数过滤。因为这些工作需要最早介入,且可能不涉及Spring的业务逻辑。
- 用Interceptor的场景:权限验证(需要访问Spring的Service)、业务日志(需要记录操作人,从Session取用户信息)、性能监控(需要关联Spring管理的组件)。因为它们需要Spring容器的支持,并且能精细地关联到具体的控制器方法。
- 执行顺序:一个请求的完整处理链是:
Filter#doFilter->DispatcherServlet#doService->Interceptor#preHandle->Controller->Interceptor#postHandle->视图渲染->Interceptor#afterCompletion->Filter#doFilter(后半部分)。
4.2 异步请求处理(DeferredResult, Callable)对流程的影响
从Servlet 3.0开始支持异步处理,Spring MVC也提供了DeferredResult和Callable等机制。这彻底改变了上述同步流程。
当控制器方法返回DeferredResult或Callable时:
HandlerAdapter会立即返回,ModelAndView为null。- Spring MVC会使用一个独立的线程(来自
TaskExecutor)来执行异步任务(对于Callable)或等待外部事件设置结果(对于DeferredResult)。 - 与此同时,HTTP工作线程被释放回容器线程池,可以处理其他请求。这是异步提升吞吐量的关键。
- 当异步任务完成或
DeferredResult被设置值后,Spring MVC会重新发起一次内部调度,几乎重走一遍doDispatch流程(但会跳过已执行的拦截器preHandle,因为它们在第一次请求时已经执行并返回true),最终完成视图渲染。
重要影响:
- 拦截器
postHandle和afterCompletion:它们在第一次请求(启动异步时)不会被执行,而是在异步结果返回后的第二次内部调度中才执行。如果你的拦截器逻辑依赖于这两个方法(比如记录最终响应时间),在异步场景下需要特别注意,可能需要使用AsyncHandlerInterceptor这个子接口。 - 线程上下文:异步任务中无法直接获取原始的
HttpServletRequest和HttpServletResponse,因为它们所属的线程已经释放。需要通过RequestContextHolder或DeferredResult的构造参数来传递必要信息。 - 超时处理:务必为
DeferredResult设置超时时间(setTimeout),并提供一个超时回调(onTimeout),否则客户端连接可能一直挂起。
4.3 全局异常处理(@ControllerAdvice)的生效时机与优先级
通过@ControllerAdvice标注的类,可以包含@ExceptionHandler、@InitBinder、@ModelAttribute方法,作用于所有@Controller。
对于异常处理,其核心是ExceptionHandlerExceptionResolver。当控制器方法抛出异常时:
- 首先查找当前控制器内部是否有匹配的
@ExceptionHandler方法。 - 如果没有,则查找**
@ControllerAdvice**类中是否有匹配的@ExceptionHandler方法。 - 如果还没有,则交给
HandlerExceptionResolver链中下一个解析器(如DefaultHandlerExceptionResolver处理Spring MVC标准异常,ResponseStatusExceptionResolver处理@ResponseStatus注解等)。 - 最后,如果所有解析器都无法处理,异常会抛给Servlet容器,通常就变成500错误页面。
避坑指南:
- 优先级:控制器内部的
@ExceptionHandler优先级高于@ControllerAdvice中的。这允许你对特定控制器进行特殊的异常处理。 - 方法签名:
@ExceptionHandler方法可以灵活地声明异常参数、请求/响应对象、Model等,但不能随意声明不相关的参数。 - 处理
@ResponseBody的异常:如果你的控制器是@RestController或方法有@ResponseBody,确保@ExceptionHandler方法也返回一个对象(或ResponseEntity),Spring会通过消息转换器将其序列化。如果你想返回一个错误视图,则需要相应调整。 - 无法处理的异常:
@ExceptionHandler只能处理请求处理过程中(即doDispatch内)抛出的异常。Filter中抛出的异常,它无法捕获。
4.4 静态资源处理与流程短路
默认情况下,DispatcherServlet的url-pattern配置为/,它会拦截所有请求。那么CSS、JS、图片这些静态资源怎么办?如果也让DispatcherServlet处理,会走到noHandlerFound导致404。
Spring提供了几种解决方案,其本质都是让请求在到达DispatcherServlet核心流程前被“短路”处理:
<mvc:default-servlet-handler/>:这个配置会注册一个DefaultServletHttpRequestHandler。它会尝试将无法匹配到Controller的请求,转发给Servlet容器默认的Servlet(如Tomcat的DefaultServlet)来处理,而容器默认Servlet通常负责提供静态资源。它的优先级较低。<mvc:resources mapping=”…” location=”…”/>:更推荐的方式。它注册一个ResourceHttpRequestHandler,专门用于处理指定路径下的静态资源。它的匹配优先级高于DefaultServletHttpRequestHandler,性能也更好。- Spring Boot自动配置:在Spring Boot中,如果你在
classpath:/static/,/public/等目录下放置资源,且没有自定义MVC配置,它会自动使用ResourceHttpRequestHandler提供静态资源服务。
理解短路机制:无论是ResourceHttpRequestHandler还是DefaultServletHttpRequestHandler,它们本身也是一种特殊的Handler(实现了HttpRequestHandler接口)。当请求映射到它们时,HandlerAdapter(这里是HttpRequestHandlerAdapter)会直接调用它们的handleRequest方法,该方法会读取文件并写入响应,然后流程结束,不会走视图渲染那套逻辑。这就是“短路”。
5. 从流程理解常见问题排查思路
掌握了完整流程,很多问题就有了清晰的排查路径。这里分享几个典型案例的排查思路:
问题一:@RequestBody接收到的对象属性全部为null。
- 排查点:第四步,
HandlerAdapter的参数绑定与消息转换。 - 可能原因:
- 请求头
Content-Type不是application/json。前端发送的可能是text/plain或application/x-www-form-urlencoded。 - JSON字段名与Java对象属性名不匹配。默认使用Jackson,属性名需一致或使用
@JsonProperty注解。 - 缺少无参构造函数或Setter方法。Jackson反序列化需要。
- HttpMessageConverter未正确配置。检查是否引入了Jackson依赖,在非Spring Boot项目中是否配置了
<mvc:annotation-driven/>。
- 请求头
问题二:使用了@ResponseBody,但返回的中文乱码。
- 排查点:第四步的消息转换,以及第六步的
View渲染(虽然跳过了视图解析,但消息转换器写响应也算一种渲染)。 - 可能原因:
StringHttpMessageConverter默认编码是ISO-8859-1。你需要配置MappingJackson2HttpMessageConverter或StringHttpMessageConverter的默认编码为UTF-8。- 在Spring Boot中,可以通过
spring.http.encoding.charset=UTF-8和spring.http.encoding.force=true配置。 - 手动设置
HttpServletResponse的编码和Content-Type:在控制器方法中response.setCharacterEncoding(“UTF-8”); response.setContentType(“application/json;charset=UTF-8”);(但更推荐全局配置)。
问题三:拦截器的postHandle方法里修改了Model数据,但页面没变。
- 排查点:第五步的拦截器后置处理。
- 可能原因:
- 请求已被转发或重定向:如果在控制器中使用了
forward:或redirect:前缀,视图名会被特殊处理,Model中的数据在重定向时默认不会传递(需用RedirectAttributes),postHandle中修改的Model可能不生效。 - 异步请求:如前所述,异步请求的
postHandle在第一次请求时不执行。 - 视图技术限制:某些视图技术(如Thymeleaf、FreeMarker)可能在
postHandle之前就已经确定了渲染模板和变量,此时修改Model可能无效。
- 请求已被转发或重定向:如果在控制器中使用了
问题四:自定义的HandlerExceptionResolver不生效。
- 排查点:第六步的异常处理。
- 排查步骤:
- 确认你的
HandlerExceptionResolver是否被Spring容器管理(@Component或@Bean)。 - 确认异常是否在
doDispatch流程中抛出。Filter中的异常它无法处理。 - 检查优先级。如果有
@ControllerAdvice,它的优先级通常高于自定义的HandlerExceptionResolver(取决于Order顺序)。 - 在
resolveException方法中,确保返回了一个有效的ModelAndView。返回null表示无法处理,会交给下一个解析器。
- 确认你的
理解SpringMVC的工作流程,绝不是为了背诵步骤,而是为了在问题出现时,能像拥有X光透视一样,一眼看穿请求在哪个环节“卡壳”或“走偏”。从DispatcherServlet的初始化,到doDispatch的每一步流转,再到各种扩展点的介入时机,每一个环节都蕴含着设计者的巧思,也对应着实际开发中可能遇到的坑。下次当你再遇到一个诡异的SpringMVC问题时,不妨在脑子里过一遍这条流水线,从请求入口开始,一步步推导,相信你很快就能锁定问题的根源。