三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

深入解析SpringMVC请求处理流程:从DispatcherServlet到视图渲染

深入解析SpringMVC请求处理流程:从DispatcherServlet到视图渲染

1. 从一次诡异的404说起:为什么必须搞懂SpringMVC流程

那天下午,我正对着屏幕发呆,一个刚入行的同事跑过来,脸上写满了困惑:“哥,我明明在Controller里写了@RequestMapping(“/user”),浏览器访问也显示路径没错,可为啥返回的就是个404?后端日志连个请求的影子都没看到。” 我让他把配置和代码发过来,扫了一眼,问题其实很简单:他的DispatcherServletweb.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模式下数据传递问题。

注意:这“九大组件”中,HandlerMappingHandlerAdapterViewResolver是我们最常打交道的。它们的初始化顺序和默认配置,都在DispatcherServlet的初始化策略(initStrategies方法)里。理解这一点,你就知道为什么自定义一个ViewResolver需要以@Bean的形式注入Spring容器。

2.2 请求入口:doService与doDispatch

当一个HTTP请求到达,容器会调用DispatcherServletservice方法,最终会流转到核心的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,询问它们:“这个请求你能处理吗?”第一个返回非空HandlerExecutionChainHandlerMapping胜出。

HandlerExecutionChain非常重要,它不仅仅包含目标处理器(Handler),还包含了应用于该处理器的所有拦截器(HandlerInterceptor。这就是为什么拦截器的配置通常和路径映射绑定在一起的原因。

为什么有时候明明路径匹配,却返回noHandlerFound

  1. 最可能的原因:开头提到的,DispatcherServleturl-pattern没匹配上。
  2. 配置问题@Controller类没有被Spring扫描到(<context:component-scan>配置错误),或者RequestMappingHandlerMapping没有正确注册。
  3. 请求方法不匹配:你的控制器方法标注了@PostMapping,但用GET请求访问。
  4. 路径匹配策略: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,流程直接终止,返回响应 }

找到处理器后,需要找到能驾驭它的“适配器”HandlerAdaptergetHandlerAdapter方法会遍历所有适配器,找到第一个支持该处理器的。

紧接着,拦截器(Interceptor)的preHandle方法被调用。这是拦截器三大方法中的第一个,也是最常用的一个。它的返回值是布尔型:

  • true:继续执行后续拦截器和处理器本身。
  • false:中断流程,DispatcherServlet认为该请求已被处理完毕,直接返回。注意,此时postHandleafterCompletion方法也不会被调用。

拦截器使用心得

  • preHandle非常适合做权限校验、日志记录、性能监控。比如,在这里检查Session中是否有用户登录信息,没有则返回false并重定向到登录页。
  • 执行顺序:拦截器的执行顺序与配置顺序一致。在preHandle阶段,是按配置顺序正序执行的。
  • 资源释放:如果在preHandle里打开了数据库连接或文件流,务必在对应的afterCompletion里关闭,否则会造成资源泄漏,因为preHandle返回false时afterCompletion不会执行。

3.4 第四步:真正的业务处理(HandlerAdapter)

// 5. 由适配器实际调用处理器(我们的Controller方法),并返回ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

这是核心业务逻辑执行的地方。HandlerAdapterhandle方法做了大量我们看不见但至关重要的工作:

  1. 参数绑定(Data Binding):将HTTP请求参数(Query String、Form Data)、路径变量(@PathVariable)、请求头(@RequestHeader)、Cookie(@CookieValue)、Session属性等,绑定到控制器方法的入参上。这是通过HandlerMethodArgumentResolver(参数解析器)家族完成的。常见的坑比如日期格式转换、自定义对象绑定,都需要在这里配置相应的ConverterFormatter
  2. 消息转换(Http Message Conversion):对于@RequestBody@ResponseBody,会使用HttpMessageConverter(如MappingJackson2HttpMessageConverter)将请求体中的JSON/XML转换为Java对象,或将返回对象转换为JSON/XML写入响应体。这里常遇到序列化/反序列化失败,比如JSON字段名与Java属性名不对应、未知属性等,需要检查Jackson注解或配置。
  3. 调用控制器方法:上述准备工作就绪后,最终通过反射调用我们编写的控制器方法。
  4. 返回值处理:控制器方法可以返回多种类型:String(视图名)、ModelAndView@ResponseBody注解的对象、甚至voidHandlerAdapter需要处理这些返回值。对于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方法):这是ViewResolverView组件登场的时候。render方法会遍历所有ViewResolver,尝试将逻辑视图名解析为具体的View对象。然后调用Viewrender方法,将模型数据合并到视图模板中,生成最终的响应内容输出给客户端。视图解析的常见问题

  • 视图找不到:检查ViewResolver的配置(前缀、后缀)、视图文件的位置和名称。
  • 模型数据在视图中取不到:检查往Model里添加数据的key,以及在视图(如JSP的EL表达式、Thymeleaf的变量)中引用的key是否一致。
  • 乱码:确保View(如JSP)的页面编码、HttpServletResponse的字符编码设置正确。对于JSON响应,检查HttpMessageConverter的编码配置。

异常处理(processHandlerException):如果前面任何步骤(包括拦截器的preHandle)抛出了异常,都会被捕获,并交给HandlerExceptionResolver处理。你可以通过@ControllerAdvice+@ExceptionHandler注解的方式,或者实现HandlerExceptionResolver接口,来定义全局或特定控制器的异常处理逻辑。这里的关键是理解异常处理的优先级和粒度控制。

拦截器收尾(triggerAfterCompletion):无论请求处理成功还是中途出现异常,只要preHandle返回了trueafterCompletion方法都会被调用。它像是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也提供了DeferredResultCallable等机制。这彻底改变了上述同步流程。

当控制器方法返回DeferredResultCallable时:

  1. HandlerAdapter会立即返回,ModelAndView为null。
  2. Spring MVC会使用一个独立的线程(来自TaskExecutor)来执行异步任务(对于Callable)或等待外部事件设置结果(对于DeferredResult)。
  3. 与此同时,HTTP工作线程被释放回容器线程池,可以处理其他请求。这是异步提升吞吐量的关键。
  4. 当异步任务完成或DeferredResult被设置值后,Spring MVC会重新发起一次内部调度,几乎重走一遍doDispatch流程(但会跳过已执行的拦截器preHandle,因为它们在第一次请求时已经执行并返回true),最终完成视图渲染。

重要影响

  • 拦截器postHandleafterCompletion:它们在第一次请求(启动异步时)不会被执行,而是在异步结果返回后的第二次内部调度中才执行。如果你的拦截器逻辑依赖于这两个方法(比如记录最终响应时间),在异步场景下需要特别注意,可能需要使用AsyncHandlerInterceptor这个子接口。
  • 线程上下文:异步任务中无法直接获取原始的HttpServletRequestHttpServletResponse,因为它们所属的线程已经释放。需要通过RequestContextHolderDeferredResult的构造参数来传递必要信息。
  • 超时处理:务必为DeferredResult设置超时时间(setTimeout),并提供一个超时回调(onTimeout),否则客户端连接可能一直挂起。

4.3 全局异常处理(@ControllerAdvice)的生效时机与优先级

通过@ControllerAdvice标注的类,可以包含@ExceptionHandler@InitBinder@ModelAttribute方法,作用于所有@Controller

对于异常处理,其核心是ExceptionHandlerExceptionResolver。当控制器方法抛出异常时:

  1. 首先查找当前控制器内部是否有匹配的@ExceptionHandler方法。
  2. 如果没有,则查找**@ControllerAdvice**类中是否有匹配的@ExceptionHandler方法。
  3. 如果还没有,则交给HandlerExceptionResolver链中下一个解析器(如DefaultHandlerExceptionResolver处理Spring MVC标准异常,ResponseStatusExceptionResolver处理@ResponseStatus注解等)。
  4. 最后,如果所有解析器都无法处理,异常会抛给Servlet容器,通常就变成500错误页面。

避坑指南

  • 优先级:控制器内部的@ExceptionHandler优先级高于@ControllerAdvice中的。这允许你对特定控制器进行特殊的异常处理。
  • 方法签名@ExceptionHandler方法可以灵活地声明异常参数、请求/响应对象、Model等,但不能随意声明不相关的参数
  • 处理@ResponseBody的异常:如果你的控制器是@RestController或方法有@ResponseBody,确保@ExceptionHandler方法也返回一个对象(或ResponseEntity),Spring会通过消息转换器将其序列化。如果你想返回一个错误视图,则需要相应调整。
  • 无法处理的异常@ExceptionHandler只能处理请求处理过程中(即doDispatch内)抛出的异常。Filter中抛出的异常,它无法捕获。

4.4 静态资源处理与流程短路

默认情况下,DispatcherServleturl-pattern配置为/,它会拦截所有请求。那么CSS、JS、图片这些静态资源怎么办?如果也让DispatcherServlet处理,会走到noHandlerFound导致404。

Spring提供了几种解决方案,其本质都是让请求在到达DispatcherServlet核心流程前被“短路”处理:

  1. <mvc:default-servlet-handler/>:这个配置会注册一个DefaultServletHttpRequestHandler。它会尝试将无法匹配到Controller的请求,转发给Servlet容器默认的Servlet(如Tomcat的DefaultServlet)来处理,而容器默认Servlet通常负责提供静态资源。它的优先级较低
  2. <mvc:resources mapping=”…” location=”…”/>:更推荐的方式。它注册一个ResourceHttpRequestHandler,专门用于处理指定路径下的静态资源。它的匹配优先级高于DefaultServletHttpRequestHandler,性能也更好。
  3. Spring Boot自动配置:在Spring Boot中,如果你在classpath:/static/,/public/等目录下放置资源,且没有自定义MVC配置,它会自动使用ResourceHttpRequestHandler提供静态资源服务。

理解短路机制:无论是ResourceHttpRequestHandler还是DefaultServletHttpRequestHandler,它们本身也是一种特殊的Handler(实现了HttpRequestHandler接口)。当请求映射到它们时,HandlerAdapter(这里是HttpRequestHandlerAdapter)会直接调用它们的handleRequest方法,该方法会读取文件并写入响应,然后流程结束,不会走视图渲染那套逻辑。这就是“短路”。

5. 从流程理解常见问题排查思路

掌握了完整流程,很多问题就有了清晰的排查路径。这里分享几个典型案例的排查思路:

问题一:@RequestBody接收到的对象属性全部为null。

  • 排查点:第四步,HandlerAdapter的参数绑定与消息转换。
  • 可能原因
    1. 请求头Content-Type不是application/json。前端发送的可能是text/plainapplication/x-www-form-urlencoded
    2. JSON字段名与Java对象属性名不匹配。默认使用Jackson,属性名需一致或使用@JsonProperty注解。
    3. 缺少无参构造函数或Setter方法。Jackson反序列化需要。
    4. HttpMessageConverter未正确配置。检查是否引入了Jackson依赖,在非Spring Boot项目中是否配置了<mvc:annotation-driven/>

问题二:使用了@ResponseBody,但返回的中文乱码。

  • 排查点:第四步的消息转换,以及第六步的View渲染(虽然跳过了视图解析,但消息转换器写响应也算一种渲染)。
  • 可能原因
    1. StringHttpMessageConverter默认编码是ISO-8859-1。你需要配置MappingJackson2HttpMessageConverterStringHttpMessageConverter的默认编码为UTF-8。
    2. 在Spring Boot中,可以通过spring.http.encoding.charset=UTF-8spring.http.encoding.force=true配置。
    3. 手动设置HttpServletResponse的编码和Content-Type:在控制器方法中response.setCharacterEncoding(“UTF-8”); response.setContentType(“application/json;charset=UTF-8”);(但更推荐全局配置)。

问题三:拦截器的postHandle方法里修改了Model数据,但页面没变。

  • 排查点:第五步的拦截器后置处理。
  • 可能原因
    1. 请求已被转发或重定向:如果在控制器中使用了forward:redirect:前缀,视图名会被特殊处理,Model中的数据在重定向时默认不会传递(需用RedirectAttributes),postHandle中修改的Model可能不生效。
    2. 异步请求:如前所述,异步请求的postHandle在第一次请求时不执行。
    3. 视图技术限制:某些视图技术(如Thymeleaf、FreeMarker)可能在postHandle之前就已经确定了渲染模板和变量,此时修改Model可能无效。

问题四:自定义的HandlerExceptionResolver不生效。

  • 排查点:第六步的异常处理。
  • 排查步骤
    1. 确认你的HandlerExceptionResolver是否被Spring容器管理(@Component@Bean)。
    2. 确认异常是否在doDispatch流程中抛出。Filter中的异常它无法处理。
    3. 检查优先级。如果有@ControllerAdvice,它的优先级通常高于自定义的HandlerExceptionResolver(取决于Order顺序)。
    4. resolveException方法中,确保返回了一个有效的ModelAndView。返回null表示无法处理,会交给下一个解析器。

理解SpringMVC的工作流程,绝不是为了背诵步骤,而是为了在问题出现时,能像拥有X光透视一样,一眼看穿请求在哪个环节“卡壳”或“走偏”。从DispatcherServlet的初始化,到doDispatch的每一步流转,再到各种扩展点的介入时机,每一个环节都蕴含着设计者的巧思,也对应着实际开发中可能遇到的坑。下次当你再遇到一个诡异的SpringMVC问题时,不妨在脑子里过一遍这条流水线,从请求入口开始,一步步推导,相信你很快就能锁定问题的根源。

← 返回列表