资讯详情

DispatcherServlet源码解析:一个Servlet如何撑起Spring MVC九大组件

📅 2026/10/12 9:40:45 | 华诺云谱 👁 阅读
DispatcherServlet源码解析:一个Servlet如何撑起Spring MVC九大组件
1. 从请求入口到分发中心DispatcherServlet在MVC里的真实位置1.1 一次请求的完整生命周期如果你维护过老式的Servlet项目可能对那种每个URL映射一个Servlet的做法印象深刻文件上传写一个Servlet登录校验写一个Servlet下载导出再写一个Servlet。功能一多web.xml动辄上百行维护成本直线上升。Spring MVC的思路完全不同——它只暴露一个前端控制器也就是标题里这个DispatcherServlet让所有进入Web应用的请求先汇到这里再由它根据URL、请求方法、参数类型把活儿分派给对应的Controller方法。一次典型的请求是这样走的浏览器发来GET /user/detail?id123请求先被容器交给DispatcherServletDispatcherServlet拿到请求后问HandlerMapping“这个请求谁能处理”HandlerMapping返回一个HandlerExecutionChain里面包含匹配的HandlerMethod以及拦截器接着DispatcherServlet让HandlerAdapter去真正调用这个方法——在这个过程中MultipartResolver处理文件上传、LocaleResolver决定用哪个语言环境、HandlerExceptionResolver兜底异常方法执行完返回逻辑视图名后ViewResolver负责把名字解析成实际页面或JSON输出在重定向场景下FlashMapManager还会帮你把参数带到下一个请求。所以你看Servlet本身只是薄薄一层壳真正的处理能力全部来自那些被它初始化、持有并调用的组件。这也正是标题里“一个Servlet撑起整个MVC框架”的含义DispatcherServlet不负责具体业务而是负责把所有处理和周边能力拧成一股绳。1.2 为什么只有它一个Servlet也能撑起全家桶这里有一个容易误解的地方在Spring Boot时代很多人以为自己并没有配置DispatcherServlet直到看到spring-boot-starter-web里自动注册了一个DispatcherServlet映射路径是/才恍然明白整个MVC的入口一直是它。不管你是写RestController、Controller、RequestMapping还是配置了Filter、Interceptor最终请求都会落到这个统一的servlet入口上。它能够撑起全家桶关键原因在于它设计成了一套“策略模式的聚合器”它自己不实现具体策略只负责查找和调用。默认情况下Spring会从IoC容器里挑出那些类型匹配的Bean放进对应的组件集合里如果你没有显式提供某些组件它还会使用getDefaultStrategies从spring-webmvc的配置文件中加载默认实现。正因为这种“有就用自定义没有就用默认”的双轨加载逻辑你才能只写一个Controller、加一个EnableWebMvc注解整个框架就能跑得有模有样。我们日常排查MVC问题追到最底层时几乎所有路径都会汇聚到Dispat​​cherServlet的几个核心方法上。理解了组件初始化的机制等于掌握了排查一切诡异请求分发问题的总钥匙。2. initStrategies()九大组件初始化的启动顺序与触发时机2.1 初始化在哪里发生Servlet的init与FrameworkServlet的onRefresh处理请求之前Servlet容器必然先调用Servlet的init()方法这是Java EE的规范。Spring这里做了一层继承DispatcherServlet继承自FrameworkServletFrameworkServlet又继承自HttpServletBean。HttpServletBean的init()负责把Servlet配置参数绑定到当前实例上然后调用initServletBean()FrameworkServlet在这里完成WebApplicationContext的初始化并在最后触发onRefresh()而DispatcherServlet重写的onRefresh()正是标题里九大组件初始化的真正入口。// FrameworkServlet中的关键调用链 Override protected final void initServletBean() throws ServletException { // ... 创建WebApplicationContext this.webApplicationContext initWebApplicationContext(); // 容器刷新完成后回调 onRefresh(this.webApplicationContext); } // DispatcherServlet中重写的方法 Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } // 九大组件初始化方法一字排开 protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); }从这段源码可以直观看到九大组件的加载顺序是固定的先解析器类组件再路由与执行组件然后是异常处理和视图组件。这些方法在执行时只干一件事——从容器里捞指定类型的Bean捞不到就回退到默认配置。注意在Spring Boot中这一步依然存在只是被ServletRegistrationBean包装后由内嵌容器触发了。你可以在日志里看到Initializing Spring DispatcherServlet和Completed initialization in X ms这两行日志分别对应初始化开始和最后完成。2.2 九大组件的初始化先后顺序为什么不能乱为什么MultipartResolver排第一个因为文件上传解析是偏底层的输入解析能力后面的HandlerMapping、HandlerAdapter在匹配处理器、绑定参数时需要拿到已经解析好的MultipartFile对象。如果顺序颠倒后加载的组件就可能在请求处理中取不到完整参数。HandlerMapping和HandlerAdapter的顺序也讲究前者负责“找得到谁”后者负责“怎么调用”。标题里九大组件的顺序基本遵循了请求处理的自然流向先解析输入再匹配路由然后适配调用接着处理异常最后解析视图和传递重定向参数。这里出现过一类比较坑的情况有人自定义了一个HandlerInterceptor希望它在所有请求前打印日志结果发现部分请求根本没有经过它。排查到最后是因为这个拦截器被注册到了某个自定义HandlerMapping上而默认的RequestMappingHandlerMapping不包含它。组件的加载顺序决定了多个HandlerMapping之间的查找顺序也决定了拦截器挂在哪个路由链条上这类问题没有对初始化机制的理解根本无从下手。2.3 默认与缺省的取舍哪些组件可以不提供再强调一次双轨加载机制这是DispatcherServlet设计的核心。以initViewResolvers为例private void initViewResolvers(ApplicationContext context) { this.viewResolvers null; if (this.detectAllViewResolvers) { MapString, ViewResolver matchingBeans BeanFactoryUtils .beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); if (!matchingBeans.isEmpty()) { this.viewResolvers new ArrayList(matchingBeans.values()); AnnotationAwareOrderComparator.sort(this.viewResolvers); } } // 容器中没有任何ViewResolver时加载默认策略 if (this.viewResolvers null) { this.viewResolvers getDefaultStrategies(context, ViewResolver.class); } }注意detectAllViewResolvers这个开关默认是true。它会从容器和所有父容器里找出全部同类型Bean如果设为false则只会找名字叫viewResolver的那个。绝大多数项目并不需要把加载逻辑调成false但如果你在多个子容器里各配置了一个ViewResolver并且希望精确控制谁生效这个开关就有用了。在九个组件里MultipartResolver、LocaleResolver、ThemeResolver、RequestToViewNameTranslator、FlashMapManager都有默认实现兜底。真正影响巨大的两个组件是HandlerMapping和HandlerAdapter容器里没有它们的Bean时Spring会加载默认的BeanNameUrlHandlerMapping、RequestMappingHandlerMapping和对应的Adapter可一旦我们自己向容器注册了一个HandlerMapping默认那套就不会再出现除非额外把它也注册进去。这一点是“为什么我加了一个自定义Controller后所有接口都404”的常见根源。3. 逐组件拆解从MultipartResolver到FlashMapManager各自管什么3.1 MultipartResolver文件上传的守门员MultipartResolver负责判断当前请求是不是multipart/form-data如果是就把请求包装成MultipartHttpServletRequest以便后续用RequestParam(file) MultipartFile file直接拿到文件。Spring Boot默认注册了StandardServletMultipartResolver它是基于Servlet 3.0的PartAPI做的老式SSM项目常配CommonsMultipartResolver内部通过Apache Commons FileUpload解析。这个组件的配置细节直接影响文件大小上限和临时目录。使用StandardServletMultipartResolver时限制写在MultipartConfigElement里Bean public MultipartConfigElement multipartConfigElement() { MultipartConfigFactory factory new MultipartConfigFactory(); factory.setMaxFileSize(DataSize.ofMegabytes(10)); factory.setMaxRequestSize(DataSize.ofMegabytes(20)); return factory.createMultipartConfig(); }我踩过一个坑文件明明只有几MB配置也写对了但上传时一直报MaxUploadSizeExceededException。排查后发现那个项目同时存在两个MultipartResolverBean而initMultipartResolver的逻辑是如果找到一个就算成功并不做顺序排序结果某个配置了较小阈值的Resolver抢占了位置。从那以后我看到项目里出现第二个MultipartResolver就会特别警惕。3.2 LocaleResolver与ThemeResolver本地化与皮肤LocaleResolver负责解析请求对应的Locale语言环境Spring MVC用它来决定消息源MessageSource取哪套文案以及时间、数字格式化成什么样。默认实现是AcceptHeaderLocaleResolver直接读HTTP头里的Accept-Language然而很多后台管理系统并不可靠于是大家会改成SessionLocaleResolver或CookieLocaleResolver让用户自己从页面上切换语言。CookieLocaleResolver是我用得最多的实现因为它把语言选择存在浏览器Cookie里关闭浏览器再打开依然有效也不需要服务端存储会话状态。配置起来也很简单Bean public LocaleResolver localeResolver() { CookieLocaleResolver resolver new CookieLocaleResolver(); resolver.setCookieName(LOCALE); resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return resolver; }ThemeResolver和它思路类似解析当前主题源然后ThemeSource提供CSS、图片等资源定位。说句实话在现代前后端分离架构下主题切换基本由前端框架接管后端ThemeResolver用得越来越少但它依然是九大组件之一你至少得知道它在整个请求链里处于什么位置否则看别人的老项目配置时容易一头雾水。3.3 HandlerMapping与HandlerAdapter路由与反射执行HandlerMapping维护“请求条件→处理器”的映射。RequestMappingHandlerMapping会扫描所有标注了RequestMapping的方法把URL、HTTP方法、参数、Header等条件组织成RequestMappingInfo请求进来时它按精确匹配、路径模式匹配、参数匹配的优先级命中目标。多个HandlerMapping可以按照Order排序先询问前面的能否处理不能处理再往后走。HandlerAdapter则解决“调用什么样的处理器”的问题。RequestMappingHandlerAdapter专门负责调用带有RequestMapping的方法把请求里的路径变量、查询参数、请求体按注解绑定到方法参数上调用方法然后将返回值适配成ModelAndView或直接写到Response里。// 一个Controller方法要被正确调用依赖HandlerAdapter完成参数绑定 GetMapping(/user/{id}) public UserInfo getUser(PathVariable Long id, RequestParam(required false) String detail) { // ... }处理PathVariable、RequestParam这种参数绑定的核心是HandlerMethodArgumentResolver列表它虽然不属于九大组件但由RequestMappingHandlerAdapter内部持有。当你自定义了某个注解希望直接注入到Controller参数里实际就是在扩展一个HandlerAdapter内部的参数解析器列表。3.4 HandlerExceptionResolver异常的统一收容所Controller方法抛出的异常默认不会直接抛给Servlet容器而是先经过HandlerExceptionResolver链。ExceptionHandlerExceptionResolver负责处理ExceptionHandler标注的方法DefaultHandlerExceptionResolver处理Spring MVC内置异常比如NoHandlerFoundException返回404、HttpRequestMethodNotSupportedException返回405。ExceptionHandler能生效底层就是这个Resolver在起作用。它的查找规则是先找当前Controller里ExceptionHandler标注的方法找不到再往ControllerAdvice里找。我在项目里见过一个很有意思的问题子类是Controller父类写了个ExceptionHandler子类自己又写了一个同名异常处理方法结果却不生效。最后定位到规则细节——Spring会先从当前类找找不到才去父类但如果用ControllerAdvice则遵循另一个优先级逻辑。排查这种问题直接看ExceptionHandlerExceptionResolver的getExceptionHandlerMethod方法最合适。3.5 RequestToViewNameTranslator与ViewResolver视图名的最后一公里RequestToViewNameTranslator是一个容易被忽略的组件。当Controller方法返回void或者没有显式指定视图名时它会从请求URL推导出一个默认视图名。比如访问/user/detail且方法没返回视图名默认翻译结果就是user/detail再交给ViewResolver解析。大多数Restful接口用不上它但老式服务端渲染页面项目经常依赖这种默认行为。ViewResolver则是把逻辑视图名变成View对象的关键角色。常见的InternalResourceViewResolver会拼前缀后缀把user/detail翻译成/WEB-INF/jsp/user/detail.jsp。Spring Boot整合模板引擎后ThymeleafAutoConfiguration会注册ThymeleafViewResolver模板路径拼装逻辑类似。这里最容易出的错是逻辑视图名写错层级、前缀后缀配置不对导致Could not resolve view with name xxx其实要查的就是ViewResolver列表里谁有资格解析这个视图名。3.6 FlashMapManager重定向场景下的跨请求缓存重定向Redirect是浏览器发起的新HTTP请求之前request里的参数不会带过去。常规做法是在URL后面拼参数但有时参数太复杂或不想暴露在地址栏这时就需要FlashMap。FlashMapManager在重定向前保存参数在重定向请求到达时读取一次后立即清除实现“只存活一次请求”的临时数据传递。Spring MVC中的RedirectAttributes接口就是配合FlashMapManager使用的PostMapping(/save) public String save(RedirectAttributes ra) { // 保存到FlashMap重定向后还能取到 ra.addFlashAttribute(msg, 保存成功); return redirect:/list; } GetMapping(/list) public String list(Model model) { // model中已经自动包含上一次FlashMap带过来的msg return list; }默认的SessionFlashMapManager把FlashMap存在HttpSession里重定向后从Session提取再删除。如果Session不可用或者接口返回纯JSON这个组件基本没有存在感但一旦你手写重定向跳转页面FlashMapManager就是那个“看不见的参数搬运工”。4. 启动排障从空白页到No mapping found的诊断思路4.1 现象一所有请求都404HandlerMapping常量为空有次接手一个老项目启动日志没有任何异常但每个接口都404。疯狂检查WebMvc配置和注解扫描包后发现Controller确实被扫描到了IoC容器里也有Bean可HandlerMapping里就是找不到这些路径。后来看了启动过程中加载的HandlerMapping发现容器里出现了一个自定义的HandlerMappingBean它覆盖了默认的RequestMappingHandlerMapping。因为initHandlerMappings的逻辑是把容器里所有HandlerMapping都收集起来如果自定义的HandlerMapping自己没有任何路由映射同时默认那个又没有被注册进去结果就是空转。这个问题的本质是Spring MV​​C并没有硬性保证默认HandlerMapping一定会被保留它只保证“有的话全收没有的话才用默认”。解决办法很直接要么去掉那个空转的自定义HandlerMapping要么手动把RequestMappingHandlerMapping注册进去并且给不同的HandlerMapping排好Order。诊断时最有效的动作是先看运行时容器里究竟有哪几个HandlerMapping实例再对着DispatcherServlet的handlerMappings字段看一眼就能定位是哪一环断了。4.2 现象二文件上传接口注入MultipartFile为空另一个高频问题接口上明明写了RequestParam(file) MultipartFile file结果运行时参数是null再仔细看MultipartResolver根本没有执行解析。最常见的原因是项目中用的是老式web.xml部署没有配置multipart-config或者CommonsMultipartResolver缺失。Servlet 3.0之后容器解析multipart请求的前提是Servlet注册时设置了MultipartConfig。如果DispatcherServlet在web.xml里没有加multipart-configStandardServletMultipartResolver拿到请求时调用request.getParts()会抛出异常或直接跳过解析。Spring Boot内嵌容器默认给DispatcherServlet配置了MultipartConfig所以Boot项目很少遇到这个问题。排查路径建议先确认DispatcherServlet实例里multipartResolver字段是否为空如果不是空再看请求类型是否是MultipartHttpServletRequest如果是再检查参数名是否匹配、是否真的用对了RequestParam。逐层往下走通常比盲目改配置更快。4.3 现象三视图解析失败报错却没有剩余堆栈Could not resolve view with name xxx这类日志往往只是完整异常的一部分。如果不看上下文很容易以为是ViewResolver配置错了但实际原因可能在Controller根本没有执行到返回视图那一步就抛了异常异常又被某个HandlerExceptionResolver吞掉后转成了别的结果。有一次我把一个RestController改成了Controller返回对象本来需要ResponseBody才不会被当作逻辑视图名。启动后访问接口日志打出“Could not resolve view”第一反应是InternalResourceViewResolver前缀后缀写错了。后来一查真正原因是Controller方法返回了一个自定义DTOSpring把它当逻辑视图名然后找不到对应视图。这类误判的根源在于对组件职责边界不够清晰ViewResolver只负责把字符串变成View它不会去猜返回值本来该不该序列化成JSON。排障时我习惯把日志级别调到DEBUG重点看DispatcherServlet里processDispatchResult方法前面的处理链日志。从请求进来到ExceptionResolver介入每一步都有迹可循比盯着最后一行报错要有效得多。4.4 排查组件初始化状态的两个实用手段第一在DispatcherServlet初始化完成后通过实现ApplicationListenerContextRefreshedEvent或者在WebMvcConfigurer配置类里临时打印DispatcherServlet持有的组件实例列表。这个方法能直观看到九个组件分别是什么实现类。EventListener public void onStartup(ApplicationReadyEvent event) { DispatcherServlet ds event.getApplicationContext() .getBean(DispatcherServlet.class); System.out.println(ds.getHandlerMappings()); System.out.println(ds.getViewResolvers()); }第二利用WebMvcConfigurer的接口方法来观察容器中的组件状态。如果你重新注册一个组件原有的默认实现会不会被覆盖可以直接从RequestMappingHandlerAdapter的argumentResolvers列表里看到。很多时候项目里的“奇怪现象”其实不是框架bug而是某个看起来不起眼的组件被替换成了你并不认识的实现类。5. 定制组件之前必须想清楚的事替换、排序与作用域5.1 通过WebMvcConfigurer扩展而不是重写DispatcherServlet很多人想“既然DispatcherServlet里组件都是从容器拿那我直接往容器里丢一个Bean不就行了”理论可行但容易踩覆盖顺序的坑。Spring官方推荐的姿势是实现WebMvcConfigurer接口重写里面的addInterceptors、configureMessageConverters、addViewControllers等方法。这些回调最终会由WebMvcConfigurationSupport里的默认组件构建器调用保证你能在既有的容器组件集合上做“加法”而不是替换成全新的一套。举个例子你想给某个Controller方法加自定义参数绑定最稳妥的做法是Configuration public class WebConfig implements WebMvcConfigurer { Override public void addArgumentResolvers(ListHandlerMethodArgumentResolver resolvers) { resolvers.add(new UserInfoArgumentResolver()); } }如果直接Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter()重新定义就等于把Spring MVC内部很多默认的配置全部绕开了——包括默认的HandlerMethodArgumentResolver、ReturnValueHandler、消息转换器列表。短时间看似正常等到处理复杂请求参数时各种缺少解析器的问题会接踵而至。5.2 多HandlerMapping排序与Order的实际表现当应用里同时存在RequestMappingHandlerMapping和自定义的HandlerMapping时它们的执行顺序按Order值决定数值小的先执行。常见的扩展场景是给某些特殊URL提供独立的处理器比如老系统的Action类适配层。这种构架下建议把基于注解的RequestMappingHandlerMapping留在最后兜底保证所有常规接口不被自定义规则抢走。调试的方法也很直接给DispatcherServlet打个断点看handlerMappings数组的排列顺序或者从日志中搜索Mapped to查看某个请求到底匹配到了哪个HandlerMapping。实际项目中把自定义HandlerMapping的Order设成1、默认的设成2基本能满足绝大多数“先走特殊路由、再走常规路由”的需求反过来设特殊路由等于不存在。5.3 我在项目里替换LocaleResolver和配FlashMap的体会替换组件这件事最怕的是“改完A影响B”。比如我曾在老系统里把AcceptHeaderLocaleResolver换成了CookieLocaleResolver本意是让用户语言选择不再受浏览器请求头左右。结果发现一部分接口返回的日期格式突然变了因为Locale变了导致DateTimeFormat的默认解析方式跟着变。后来我在Resolver里锁定了Locale.CHINA作为兜底值才让格式稳定下来。FlashMapManager的定制机会相对少但也有值得注意的点默认SessionFlashMapManager要求会话可用如果接口是无状态、跨域重定向模式FlashMap数据可能存不进去。我在一个单点登录对接项目里用过RequestDataValueProcessor配合FlashMap做表单参数透传绕开了Session依赖效果不错。但这种做法对请求生命周期理解要求高不建议新手直接用能走RedirectAttributes的常规路线就尽量走常规路线。最后再分享一个经验每次改动九大组件中的任意一个别只看当前接口通不通要再用真正生产形态的请求验证一遍——带上Cookie、跨域头、文件上传、错误参数这些边角路径最能暴露组件替换后的副作用。MVC框架的魅力在于它把所有可变点都设计成可插拔的策略但插拔得越多你对整体初始化的理解就越必须跟上。组件之间环环相扣牵一发而动全身这既是风险的来源也是DispatcherServlet作为一个Servlet能撑起整套MVC框架的根本底气。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑