
写了半年Spring MVC的Controller你大概率能熟练搞定RestController、GetMapping项目里的接口一个接一个地冒出来。但突然有一天面试官问了一句DispatcherServlet在请求处理流程里到底承担了什么那一瞬间我发现自己居然答不上来。代码写了不少可脑子里对Spring MVC这张大网始终缺一个“总纲”。后来我把精力花在啃DispatcherServlet这个核心上才发现它不只是某个配置类而是整个Spring MVC之所以叫MVC的关键。这篇文章就从一个使用者亲历过的困惑出发把这个最核心的思想掰开揉碎。无论你是刚学框架的Java新手还是已经在工作中写了一年Controller的开发这篇文章都能让你真正做到“用起来放心面试讲得清”。1. 从一次被问住开始DispatcherServlet在Spring MVC里到底是个什么角色很多人第一次学Spring MVC接触到的第一句话是“DispatcherServlet是前端控制器”。听完就过去了接着就去写Controller了。真正的问题在于前端控制器这个“前端”到底指什么它和Controller又是什么关系1.1 前端控制器所有请求必须先过它这一关先想清楚一个场景你有一个浏览器地址栏里输入http://localhost:8080/order/detail?orderId1001按下回车。请求到Tomcat之后Tomcat会根据web.xml或者注解扫描找到匹配的Servlet。如果按照传统Servlet的写法一个Servlet通常对应一个明确的业务动作比如OrderServlet专门处理订单、UserServlet专门处理用户。你访问/order/detail就得先有一个映射到/order/detail的Servlet存在。但Spring MVC不是这么做的。它只注册一个DispatcherServlet用url-pattern把它映射到应用的根路径也就是/。意思很直白只要请求进入这个Web应用不管路径是什么统统先交给DispatcherServlet再说。它在收到请求之后再去问一堆“组件”该找哪个类、哪个方法处理参数怎么解析返回类型怎么适配视图怎么渲染所以说“前端控制器”这个名字很精准——它站在所有请求的最前面控制了后面所有事情的走向。它不负责具体的业务逻辑而是负责“分配”、“调度”、“统筹”。我们平时写的Controller里的那些方法反而是真正的“后端执行者”。1.2 它和普通Servlet的本质区别调度与执行的分工这里可以用一个生活化的类比加深印象把DispatcherServlet想象成快递分拣中心Tomcat把“包裹”请求运进来DispatcherServlet根据包裹上的地址分配给不同的“快递员”Controller里的处理方法。快递员只负责把这一票活干好送到收件人手里至于整个快件系统怎么运转、快递员从哪来、路线怎么规划快递员不用操心。传统Servlet时代就相当于你既是分拣员、又是快递员、还是客服。一个OrderServlet里既要接收参数、又要连接数据库、又要拼HTML、又要决定跳转到哪个页面。当时的开发也没错但一旦业务复杂每个入口都这样写代码必然爆炸。DispatcherServlet的核心思想就是把“公共流程”和“具体实现”彻底剥离。公共流程包括接收请求、找到处理者、参数解析、方法调用、异常处理、视图渲染、响应返回。具体实现就是你写的业务方法。我印象最深的一刻是在源码里看到doDispatch这个方法的时候。几百行代码把整个请求生命周期串得明明白白。那一刻我突然意识到学了那么久的Spring MVC其实真正在跑的始终是这一个方法。这个思路理解透了后面所有的配置、拦截器、异常处理、视图解析都只是在这条主链路上的一个个扩展点。2. 一张请求的完整旅程DispatcherServlet在doDispatch里都干了什么要理解DispatcherServlet不能停在概念层面必须跟着一个真实请求走一遍。下面我以最经典的“返回一个JSP页面”场景为例拆解整个流程。这个场景对新手最友好因为中途能遇到的组件最多。2.1 doDispatch的主流程从拿到HttpServletRequest开始DispatcherServlet本身继承自FrameworkServletFrameworkServlet又继承自HttpServletBean最终是标准Servlet。所以当请求进入时Servlet容器会调用它的service()方法FrameworkServlet已经重写了service()最终把请求送到doGet或doPost。这几个方法内部都统一调用了processRequest流程会汇入我们真正关心的doDispatch(HttpServletRequest request, HttpServletResponse response)。用一段简化过的源码骨架可以很清楚地看到DispatcherServlet的核心决策路径protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest request; HandlerExecutionChain mappedHandler null; boolean multipartRequestParsed false; // 1. 检查是不是文件上传请求 WebAsyncManager asyncManager WebAsyncUtils.getAsyncManager(request); // ... try { ModelAndView mv null; Exception dispatchException null; try { processedRequest checkMultipart(request); multipartRequestParsed (processedRequest ! request); // 2. 根据请求找到对应的Handler执行链 mappedHandler getHandler(processedRequest); if (mappedHandler null) { noHandlerFound(processedRequest, response); return; } // 3. 找到能执行这个Handler的适配器 HandlerAdapter ha getHandlerAdapter(mappedHandler.getHandler()); // 4. 执行拦截器的preHandle if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 5. 真正调用Controller方法拿到ModelAndView mv ha.handle(processedRequest, response, mappedHandler.getHandler()); // 6. 执行拦截器的postHandle mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException ex; } // 7. 处理返回结果视图解析 渲染 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } finally { // 8. 执行拦截器的afterCompletion等收尾 } }源码里的细节很多但记住五件事就够了检查multipart → 找Handler → 找Adapter → 执行拦截器和业务方法 → 渲染视图。整个请求在DispatcherServlet那里就是一条干净的流水线任何一步出问题都有对应的异常处理兜底。2.2 HandlerMapping怎么从URL定位到能干活的那个方法getHandler(processedRequest)这行代码本质上是遍历handlerMappings这个List逐个问“这个请求你能处理吗”只要有一个HandlerMapping返回了非空的结果遍历就停止。我调试过一个实际项目用户请求/order/listDispatcherServlet会问一圈HandlerMapping最终由RequestMappingHandlerMapping认出这是OrderController里GetMapping(/list)这个方法。RequestMappingHandlerMapping是注解驱动时代最核心的一个HandlerMapping。Spring容器启动时它会扫描所有声明了Controller的类把类上和方法上的RequestMapping合并形成一个完整的“请求路径 → 处理方法”映射表。等请求真正进来它就拿着请求URL加上请求方法GET/POST、请求头等信息去匹配这张表。这里特别容易忽略的是HandlerExecutionChain。getHandler()返回的并不只是一个Handler方法而是一个“执行链”——里面既有目标方法也有当前配置的所有拦截器Interceptor。拦截器之所以能有机会在业务方法前后执行逻辑就是因为DispatcherServlet拿到的不是孤零零的Handler而是带了一串拦截器的链。2.3 HandlerAdapter把Spring MVC的方法“翻译”给Servlet容器听找到Handler之后下一步是getHandlerAdapter(mappedHandler.getHandler())。这一步经常被新手忽略但它恰恰是整个框架最具匠心的地方之一。你得先想一件事HandlerMapping找到的“Handler”到底是什么类型在老一点的项目里Controller可能实现Controller接口再早可能是一个HttpRequestHandler而注解时代是一个HandlerMethod说白了就是“类的反射信息加上方法”。这些东西Servlet容器可完全不懂。DispatcherServlet要调用它们但又有各种不同的调用方式总不能写一堆if else吧于是就有了HandlerAdapter。它的任务是在“DispatcherServlet”和“各种形态的Handler”之间做适配。所有HandlerAdapter都遵循统一约定handle(request, response, handler)DispatcherServlet根本不用关心你要调用的是普通老Controller还是注解方法。它只要找到支持当前Handler的那一个适配器把固定的方法给它传进去就行了。所以现在你在Controller方法里写RequestParam、PathVariable、RequestBody参数能自动被解析成对象这些其实都不是Servlet能力而是RequestMappingHandlerAdapter在执行时调用了二十多种参数解析器HandlerMethodArgumentResolver所致。同理ResponseBody能直接把对象序列化成JSON也是因为它调用了返回值处理器HandlerMethodReturnValueHandler。换个不恰当的比喻DispatcherServlet负责制定“统一的军规”HandlerAdapter负责“翻译给士兵听”。2.4 视图渲染和价值闭环ModelAndView去哪了HandlerAdapter执行完业务方法后会返回一个ModelAndView或者null比如方法上加了ResponseBody。在传统不分离项目里这个ModelAndView里包含两个关键信息一个是视图名比如字符串order/list一个是需要填进页面的模型数据。processDispatchResult这步会把视图名交给ViewResolver。大多数项目用的是InternalResourceViewResolver它做的是一个很简单的拼接动作前缀 视图名 后缀。比如bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean那么order/list就会被解析成/WEB-INF/views/order/list.jsp。注意WEB-INF目录下的资源对客户端不可直接访问这也是Spring MVC一个巧妙的安全设计——视图页面只能通过服务器内部转发来访问用户没法绕过控制器直接打开JSP。解析出视图之后DispatcherServlet把Model里的数据塞进View里调用view.render()完成渲染。JSP会通过request.getRequestDispatcher(path).forward()把输出内容交还给Servlet容器最终写回浏览器。到这里一个完整的“请求进入 → 找到处理者 → 执行业务 → 渲染页面 → 返回响应”的闭环就合上了。所有环节都围绕DispatcherServlet它才是这条闭环上真正的唯一支柱。3. 调度思想拆解为什么Spring MVC要把活拆给一堆“角色”很多初学者会问Spring直接写死一个“请求路径到方法的映射器”、“参数解析器”、“视图解析器”不就完了为什么非要把它们拆成HandlerMapping、HandlerAdapter、ViewResolver还允许你自己扩展这就是框架设计的核心思想策略模式。3.1 策略模式在三大组件上的现形DispatcherServlet最大的特点就是“流程固定策略可变”。流程本身就在doDispatch里写死了找目标、找适配器、调方法、解析视图。但每个步骤的具体实现全部通过接口注入。这就带来一个很实际的好处当业务场景发生变化时你不需要去改DispatcherServlet只需要新增或替换策略。比如项目的接口不再返回JSP而是希望接口层的请求全部返回JSON你可以在方法上加ResponseBody而整个框架的处理流程不会有任何变化。再比如你想给某个老系统的Controller加权限控制不必重写框架只要在HandlerInterceptor里插一段逻辑即可。preHandle返回false请求会当场中断后面的Handler根本不会被调到。我当年在项目里做过一个自定义拦截器用来做大屏接口的鉴权。当时配置一下mvc:interceptors所有匹配路径的请求就全部走到拦截器里了。那时我还没认真读源码只觉得Spring MVC“好神奇”。后来看了applyPreHandle的实现才明白拦截器实际上是挂在HandlerExecutionChain上和业务Handler一起被DispatcherServlet统一调用的。这就是策略模式最典型的应用DispatcherServlet只管“流程正确”不关心具体策略。3.2 九大组件一张图看懂Spring MVC的整体装配DispatcherServlet初始化时有一段非常经典的代码initStrategies(context)这段代码在onRefresh里被调用。它会初始化九种策略组件。我把它们在老版本源码里的名字列出来你就能感受到Spring为这套调度体系留了多少口子。组件默认实现以注解驱动为例作用HandlerMappingRequestMappingHandlerMapping等决定请求由哪个Handler处理HandlerAdapterRequestMappingHandlerAdapter等适配调用各种形态的HandlerHandlerExceptionResolverExceptionHandlerExceptionResolver等处理Controller抛出的异常ViewResolverInternalResourceViewResolver等把视图名解析成真正的ViewRequestToViewNameTranslatorDefaultRequestToViewNameTranslator当没有指定视图名时根据请求推导默认视图名LocaleResolverAcceptHeaderLocaleResolver等处理国际化、区域信息ThemeResolverFixedThemeResolver等切换主题样式MultipartResolverStandardServletMultipartResolver处理文件上传FlashMapManagerSessionFlashMapManager等管理FlashMap用于重定向时传递数据这九个组件每一个都对应一个面向句口。比如HandlerExceptionResolver如果你在项目里用过ControllerAdvice加ExceptionHandler那你已经用过它了。Spring MVC默认会把异常的处理也放回“策略”轨道里让DispatcherServlet在主流程中出错时不至于直接崩溃而是进入异常解析策略链。3.3 可扩展点在哪拉一个自定义ViewResolver会碰到什么既然流程固定、策略可变那扩展一套自己的逻辑有多难我实操过一件事改造一个老项目让部分接口直接从数据库取到的内容生成PDF视图。当时我新建了一个PdfViewResolver实现ViewResolver接口重写resolveViewName()方法。配置进去之后DispatcherServlet拿到视图名会按顺序问每个ViewResolver。如果我的解析器说“我喜欢这个视图名这是我的View”框架就会优先用它。如果我的解析器返回null框架就继续问下一个。这个体验正是“策略模式”最好的注脚。DispatcherServlet的代码不需要改一行我的业务就融进了框架。很多初学者担心这东西太黑盒其实一旦理解它是策略模式你就找到了阅读Spring MVC源码的钥匙不要试图一口气读完所有逻辑只关心每个策略接口的扩展方式和装配方式。4. 配置与生命周期DispatcherServlet从启动到接管请求理解了思想再看配置很多当年觉得死记硬背的东西就彻底通了。无论你是用Spring Boot还是传统SSM项目DispatcherServlet都要经历从“初始化”到“接管请求”的过程。传统Servlet项目里这些配置更“暴露”反而更容易看明白。4.1 容器启动时它做了什么onRefresh与initStrategies先看一段经典的web.xml配置。这段配置在SSM时代的项目里几乎天天见servlet servlet-namespringMvc/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namespringMvc/servlet-name url-pattern//url-pattern /servlet-mappingload-on-startup为1意思是Tomcat启动时就要创建并初始化这个Servlet而不是等第一个请求来了再初始化。初始化过程里init()会被调用最终走到initWebApplicationContext()和onRefresh()。onRefresh()里那句initStrategies(context)会把前面说的九大组件全部初始并放入容器。之后每一次请求进来DispatcherServlet就已经是“全副武装”的状态了。这段配置里contextConfigLocation是另一个容易被忽视的关键点。如果不配置DispatcherServlet默认会去WEB-INF目录下找springMvc-servlet.xml这个文件名是“[servlet-name]-servlet.xml”。第一次配置如果漏了init-param经常报FileNotFoundException或BeanDefinitionStoreException原因就在这里。4.2 父子容器为什么Controller和Service常常分开扫描SSM时代经常有两种配置applicationContext.xml一般由ContextLoaderListener加载管理Service、Dao这类业务层组件spring-mvc.xml则交给DispatcherServlet加载管理Controller和MVC组件。很多人不知道为什么这么拆只背了一个结论“拆分容器”。搞懂父子容器后这段历史就一目了然。ContextLoaderListener创建的容器是“父容器”DispatcherServlet创建的容器是“子容器”。子容器可以看到父容器里的Bean父容器看不到子容器里的Bean。所以Controller放在子容器spring-mvc.xml能调用父容器中的ServiceService放在父容器applicationContext.xml它不该也不依赖Controller如果作用反了比如让父容器扫描了Controller子容器也扫描了Controller可能会产生“一个请求能被多个Handler匹配”的歧义甚至日志出现重复Bean定义。Spring Boot时代虽然用自动配置把这些细节藏起来了但理解父子容器后你再去看Primary、循环依赖、“为什么某些Bean找不到”这类问题会清晰很多。框架并不是魔法它只是把选择权交给了你然后给了默认值。4.3 静态资源为什么老被它“劫持”路径映射的博弈把DispatcherServlet映射到/之后会有一个经典问题静态资源图片、CSS、JS的请求也会进DispatcherServlet。因为这个“兜底”会把所有未匹配的路径全吸进来。DispatcherServlet拿到/static/css/app.css这种请求HandlerMapping根本匹配不到任何Controller方法默认返回404。解决办法有两类。一类是用mvc:resources mapping/static/** location/static/让Spring自己提供静态资源处理。另一类是用mvc:default-servlet-handler /把这部分请求再交还给Servlet容器的默认Servlet处理。两者的区别在于前者是Spring在内部用ResourceHttpRequestHandler处理后者是直接甩给Tomcat的DefaultServlet处理。这个问题的本质就是DispatcherServlet作为“唯一入口”带来的副作用。你享受了“入口统一”的优势就必须接受“入口统一”的摩擦。好在Spring MVC早就替你想好了方案。5. 实战避坑我踩过的几个DispatcherServlet周边的坑理解了原理不代表不会踩坑。以下四个坑都是我在真实项目里遇到过的每一个排查起来都很有代表性。我把现象、原因、排查过程一并写出来希望能帮你省掉几个晚上的排查时间。5.1 坑一url-pattern写成/*页面出不来还无限循环有个老项目web.xml里把DispatcherServlet映射成了/*。结果接口请求正常但任何一个JSP页面都会反复嵌套渲染页面最终直接报错。原因是/*会匹配所有路径包括JSP文件本身。流程变成请求JSP → DispatcherServlet接管 → 找到Controller → 返回JSP视图名 → ViewResolver拼成JSP路径 → 服务器转发JSP → JSP又当成新请求再次进入DispatcherServlet。死循环由此而来。正确做法是映射/只让它接管“非JSP”的路径。为什么/能避开JSP因为JSP文件在Servlet容器里有自己的处理ServletJspServlet通常被映射到*.jsp精确匹配优先级高于/兜底匹配。所以JSP请求会先被JspServlet接管不会进DispatcherServlet。这个坑是学习阶段最常踩的而且一旦遇到光看日志很难立刻反应过来。5.2 坑二No mapping found for HTTP request with URI有一次我配置了一个新页面Controller里的GetMapping(/user/detail)写得清清楚楚但访问时控制台打出No mapping found for HTTP request with URI [/user/detail]浏览器404。第一反应是“映射没拼对”但检查了好几遍都没问题。后来才发现Controller所在的包根本没被spring-mvc.xml的component-scan扫描到。排查过程很有代表性先确认HandlerMapping的映射表里到底有没有这个URL如果没有不是URL写错就是Controller压根没被Spring容器发现。这时候可以临时加一个启动期日志遍历RequestMappingHandlerMapping里的HandlerMethod把注册的所有路径打出来一眼就能看到路径是否真的存在。后来我在很多团队里推广了这个办法比反复猜快得多。5.3 坑三Filter和Interceptor的执行顺序搞反有同事在统一权限校验时用了HandlerInterceptor自测通过。上线后某些请求却出现了“静态资源也被拦截”的问题。排查后发现他以为Interceptor是在Servlet之前工作的其实不是。生命周期顺序是请求先进Servlet容器 → Filter链先执行 → 然后才到DispatcherServlet → 进入DispatcherServlet后再执行Interceptor的preHandle。所以如果你的映射路径包含了静态资源而Interceptor的匹配路径又写得比较宽静态资源也会被拦截器影响。Filter看不到Controller和HandlerInterceptor能拿到HandlerMethod和ModelAndView。两者定位不同选择上也该先想清楚。遇到过日志里preHandle执行了两次的情况最后发现是同一路径既被/*的Filter匹配又在Interceptor里重复校验。这种重复性问题多踩几次就长记性了过滤器管粗粒度、跨请求的通用处理拦截器管细粒度的Controller增强。5.4 坑四重复注册DispatcherServlet启动时出现ambiguous mapping警告另外一个项目启动时日志出现“There are multiple servlets mapped to the same path”或者“ambiguous mapping”导致接口访问时路由混乱。最后定位出来是web.xml里手动配了一个DispatcherServlet同时项目里的某个框架配置又通过编程方式注册了同一个Spring MVC两边都映射到了/。这个坑在Spring Boot项目里也偶尔能看到自定义WebMvcConfigurer时无意中又配了一个DispatcherServlet实例或者在Bean里重复创建了它。排查方法也简单检查项目里搜索DispatcherServlet的注册点确认Tomcat的启动日志里到底初始化了几个DispatcherServlet。正常情况只该有一个。如果出现两个优先确认是不是配置重复而不是试图改url-pattern来“避开冲突”。5.5 常见坑汇总对照表现象可能原因排查思路解决方向接口返回404但Controller存在包扫描漏配HandlerMapping映射表里没有该URL打印所有已注册路径修正component-scan页面无限渲染或JSP模式异常url-pattern误配为/*查看Servlet映射改为/静态资源404未配置静态资源处理器检查HandlerMapping命中情况配置resources或default-servlet-handler拦截器影响静态资源拦截路径过宽分不清Filter和Interceptor职责日志打点确认执行链收紧interceptor匹配路径启动重复Servlet警告web.xml和编程式注册重复查看Tomcat下Servlet实例数去掉重复注册RequestBody参数为nullHandlerAdapter没有匹配的MessageConverter看请求Content-Type配置MappingJackson2HttpMessageConverter这些坑看起来彼此没有关联其实根子都在DispatcherServlet的“统一入口”机制上。你越理解它内部的流程越能在踩坑时迅速判断问题出在“哪个环节”而不是漫无目的地试。回到最初那个面试问题DispatcherServlet的核心思想到底是什么说白了就是一句话——通过一个统一的入口把“请求调度”和“业务实现”隔离开再用一组可替换的策略组件完成从定位、执行到渲染的完整闭环。从用户角度看它是入口从开发角度看它是枢纽从架构角度看它是策略模式与前端控制器模式的经典结合。如果你也想验证自己的理解程度我最推荐的做法是读一遍doDispatch源码再在自己项目里打印一次“请求从进入DispatcherServlet到返回响应”的日志。这两件事做完Spring MVC对你来说就不再是一个黑盒。