摘要
Spring MVC 作为 Java Web 开发的基石,其核心大脑便是DispatcherServlet。很多开发者能够熟练使用@Controller和@RequestMapping,却往往忽视了请求分发背后的初始化机制。当 Spring Boot 应用启动时,DispatcherServlet是如何被加载的?它是如何“智能”地发现我们定义的 Bean?如果找不到配置,它又是如何回退到默认策略的?
本文将以资深架构师的视角,深入 JDK 与 Spring 源码底层,解构DispatcherServlet的继承体系与生命周期。我们将核心聚焦于onRefresh()这一关键生命周期钩子,逐行代码解析九大核心策略接口(Nine Core Strategy Interfaces)的加载逻辑。从HandlerMapping的路由构建到ViewResolver的视图解析,我们将揭示 Spring 如何利用策略模式(Strategy Pattern)构建出一个松耦合、高扩展的 Web 引擎,并分享在复杂微服务场景下的性能调优实战经验。本文全篇超过15000字,旨在成为 Spring MVC 初始化的权威参考手册。
第一部分:战略层 - 背景、设计哲学与架构总览
1.1 前端控制器(Front Controller)模式的演进史
要真正理解DispatcherServlet,我们必须回到 Web 开发的源头。
1.1.1 史前时代:Model 1 与 CGI 的混沌
在 CGI(Common Gateway Interface)和早期的 JSP Model 1 时代,Web 开发处于一种“各自为政”的状态。
- 物理映射:URL 直接映射到文件系统上的物理文件(如
/login.cgi或/user.jsp)。 - 逻辑分散:每个脚本文件都需要独立处理:
- 参数解析(
request.getParameter) - 会话管理(
session.getAttribute) - 权限验证(
if (!user.isAdmin) return 403) - 业务逻辑调用
- HTML 拼接输出
- 参数解析(
- 灾难后果:当需要更改一种通用的逻辑(例如,全站增加 CSRF 防护)时,开发者不得不修改成千上万个文件。这种架构被称为“意大利面条式代码(Spaghetti Code)”,耦合度极高,维护成本呈指数级上升。
1.1.2 启蒙时代:Servlet 与 Model 2 的诞生
Java Servlet 技术的出现,引入了 Java 类来处理 HTTP 请求,这催生了 Model 2(即 MVC)架构。
- Controller(控制器):Servlet 充当控制器,负责接收请求、调用业务逻辑。
- View(视图):JSP 仅负责展示数据。
- Model(模型):JavaBean 承载数据。
虽然有了分层,但早期 Model 2 依然存在问题:如果在web.xml中配置了 100 个 Servlet 来处理不同的 URL,那么这 100 个 Servlet 中依然存在大量的重复代码(如编码设置、异常捕获)。
1.1.3 工业革命:前端控制器模式的统一
为了解决 Controller 层的重复代码问题,核心设计模式——前端控制器(Front Controller)应运而生。
- 核心思想:收口。所有的 HTTP 请求不再直接分发给具体的业务 Servlet,而是先全部拦截到一个中央调度器(Central Dispatcher)。
- DispatcherServlet 的角色:它就是这个中央调度器。它不仅是一个 Servlet,更是一个Web 框架的微内核。
- 职责重塑:
- 统一接入:所有的请求入口(
/*或*.do)。 - 统一流程:定义了请求处理的标准生命周期(路由 -> 适配 -> 执行 -> 渲染)。
- 统一扩展:提供了 AOP 式的拦截器链(Interceptors)和全局异常处理。
- 统一接入:所有的请求入口(
1.2 Spring 的核心设计哲学:控制反转与策略模式
为什么 Spring MVC 能打败 Struts2 成为霸主?核心在于其对“开闭原则(Open/Closed Principle)”的极致运用。
1.2.1 策略模式(Strategy Pattern)的巅峰之作
Spring MVC 认为,Web 请求处理流程中的每一个步骤,都是可以被替换的“策略”。DispatcherServlet本身几乎不包含任何具体的业务处理逻辑,它只是一个组装工厂。
它定义了九大核心接口,这九大接口构成了 Spring MVC 的骨架:
- 文件上传策略:
MultipartResolver - 本地化策略:
LocaleResolver - 主题策略:
ThemeResolver - 路由策略(最核心):
HandlerMapping - 执行策略(最核心):
HandlerAdapter - 异常处理策略:
HandlerExceptionResolver - 视图名翻译策略:
RequestToViewNameTranslator - 视图解析策略(最核心):
ViewResolver - 重定向数据策略:
FlashMapManager
设计哲学解析:
- 解耦:
DispatcherServlet依赖的是接口(Interface),而不是实现(Implementation)。 - 灵活性:你想用 JSP?配置
InternalResourceViewResolver。想用 Thymeleaf?配置ThymeleafViewResolver。想输出 JSON?配置MappingJackson2JsonView。DispatcherServlet的代码一行都不用改。
1.2.2 约定优于配置(Convention over Configuration)的先驱
在 Spring Boot 普及之前,Spring MVC 就已经内置了一套强大的默认配置机制。
DispatcherServlet.properties:这是一个位于org.springframework.web.servlet包下的神密文件。它定义了上述九大策略接口的默认实现类。- 兜底机制:如果 Spring 容器中找不到用户自定义的 Bean,
DispatcherServlet就会读取这个属性文件进行“兜底”加载。这保证了即使是空的配置文件,Spring MVC 也能跑起来(虽然可能只能处理最简单的请求)。
第二部分:战术层 - 继承体系与启动流程详解
在深入九大组件之前,我们需要像法医解剖一样,先理清DispatcherServlet的“生理结构”——继承体系,以及它是如何被 Web 容器(如 Tomcat)唤醒的。
2.1 家族谱系:从 Object 到 DispatcherServlet
DispatcherServlet的类继承图谱如下,每一层都赋予了它不同的能力:
java.lang.Objectjavax.servlet.GenericServlet- 能力:实现了
ServletConfig接口,具备了读取web.xml中init-param的能力;实现了log()方法。 - 协议无关性:这一层还是协议无关的,不限于 HTTP。
- 能力:实现了
javax.servlet.http.HttpServlet- 能力:引入了 HTTP 协议语义。提供了
doGet,doPost,doPut,doDelete等方法供子类重写。 - 入口:标准的 Servlet
service(ServletRequest, ServletResponse)方法在这里将请求分发给doXxx方法。
- 能力:引入了 HTTP 协议语义。提供了
org.springframework.web.servlet.HttpServletBean(Spring 的第一层封装)- 能力:将 Servlet 当作一个 Spring Bean 来对待。
- 核心动作
init():它重写了 Servlet 的init()方法。它会读取web.xml中<servlet>标签下的<init-param>,并使用BeanWrapper将这些参数注入到DispatcherServlet的属性中。 - 设计美学:这使得我们可以像配置普通 Bean 一样配置 Servlet。
org.springframework.web.servlet.FrameworkServlet(Spring Web 的基石)- 能力:Web 上下文(WebApplicationContext)的管理者。
- 核心动作
initServletBean():负责初始化 Spring 的 IoC 容器。它会建立起Root Context(通常由ContextLoaderListener加载,包含 Service/DAO) 和Servlet Context(包含 Controller/ViewResolver) 之间的父子容器关系。 - 统一请求处理:它重写了
doGet,doPost等所有方法,并将它们统一收口到processRequest()方法。
org.springframework.web.servlet.DispatcherServlet(最终的实现者)- 能力:请求分发器。
- 核心动作
onRefresh():这是我们本文的主角。当 IoC 容器刷新完毕后,回调此方法,触发九大组件的初始化。 - 核心动作
doDispatch():真正处理请求的方法(本篇暂不展开,专注于初始化)。
2.2 启动时序图深度解析
让我们把显微镜对准服务器启动的那一刻。当 Tomcat 启动并加载 Web 应用时,发生了以下精密的过程:
- Tomcat 启动 -> 加载
web.xml(或 Servlet 3.0+ 注解配置)。 - 实例化
DispatcherServlet(调用无参构造函数)。 - 调用
servlet.init(ServletConfig)(这是 Servlet 规范的标准生命周期)。 HttpServletBean.init()被执行:- 解析
init-param。 - 调用
initServletBean()(模板方法模式)。
- 解析
FrameworkServlet.initServletBean()被执行:- 创建 WebApplicationContext:如果没有现成的上下文,会创建一个(通常是
XmlWebApplicationContext或AnnotationConfigWebApplicationContext)。 - 设置父容器:将
ContextLoaderListener加载的 Root Context 设置为父容器。 - configureAndRefreshWebApplicationContext():触发容器的
refresh()方法。 refresh()->finishRefresh()->publishEvent(ContextRefreshedEvent)。
- 创建 WebApplicationContext:如果没有现成的上下文,会创建一个(通常是
DispatcherServlet.onRefresh(ApplicationContext)被触发:FrameworkServlet监听到了上下文刷新事件(或者直接在 refresh 后调用),触发onRefresh。- 调用
initStrategies(context)。->九大组件在此刻加载完毕!
第三部分:核心战术 - 九大策略接口初始化源码逐行解析
这是本文的核心部分。我们将深入org.springframework.web.servlet.DispatcherServlet的initStrategies方法。
/** * Initialize the strategy objects that this servlet uses. * <p>May be overridden in subclasses in order to initialize further strategy objects. */protectedvoidinitStrategies(ApplicationContextcontext){initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}我们将挑选其中最核心、最复杂的组件进行深度拆解。
3.1 核心一:initHandlerMappings—— 路由系统的构建
HandlerMapping是 Spring MVC 的“地图”,它决定了请求 URL/user/1到底应该由哪个 Controller 的哪个 method 来处理。
3.1.1 源码深度拆解
privatevoidinitHandlerMappings(ApplicationContextcontext){this.handlerMappings=null;// 1. 探测所有模式(detectAllHandlerMappings 默认为 true)if(this.detectAllHandlerMappings){// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.// 核心方法:BeanFactoryUtils.beansOfTypeIncludingAncestors// 这里的 includeAncestors = true,意味着会去父容器(Root Context)里找。// 这是一个常见的坑点:如果父子容器都配置了 HandlerMapping,可能会导致意外的行为。Map<String,HandlerMapping>matchingBeans=BeanFactoryUtils.beansOfTypeIncludingAncestors(context,HandlerMapping.class,true,false);if(!matchingBeans.isEmpty()){this.handlerMappings=newArrayList<>(matchingBeans.values());// 2. 排序(至关重要!)// HandlerMapping 是有顺序的。Spring MVC 会按顺序遍历它们,只要有一个 Mapping 能匹配上 URL,就直接返回 Handler,不再继续。// 排序依据:实现 PriorityOrdered 接口 > 实现 Ordered 接口 > @Order 注解 > 无序。AnnotationAwareOrderComparator.sort(this.handlerMappings);}}else{// 3. 单一模式:只查找 ID 为 "handlerMapping" 的 Beantry{HandlerMappinghm=context.getBean(HANDLER_MAPPING_BEAN_NAME,HandlerMapping.class);this.handlerMappings=Collections.singletonList(hm);}catch(NoSuchBeanDefinitionExceptionex){// Ignore, we'll add a default HandlerMapping later.}}// 4. 兜底策略(Default Strategy)// 如果上面的步骤都没有找到任何 HandlerMappingif(this.handlerMappings==null){// 读取 DispatcherServlet.properties 中的配置// 默认加载:BeanNameUrlHandlerMapping 和 RequestMappingHandlerMappingthis.handlerMappings=getDefaultStrategies(context,HandlerMapping.class);if(logger.isTraceEnabled()){logger.trace("No HandlerMappings declared for servlet '"+getServletName()+"': using default strategies from DispatcherServlet.properties");}}// 确保列表不为空,至少得有一个处理映射for(HandlerMappingmapping:this.handlerMappings){if(mapping.usesPathPatterns()){this.parseRequestPath=true;break;}}}3.1.2 关键类RequestMappingHandlerMapping的初始化内幕
RequestMappingHandlerMapping是处理@RequestMapping注解的核心类。它的初始化过程非常消耗性能(时间复杂度 O(N*M)),因为它需要扫描所有 Bean 的所有方法。
初始化流程 (AbstractHandlerMethodMapping.afterPropertiesSet):
- 扫描容器中所有的 Bean 名称 (
getBeanNamesForType(Object.class))。 - 遍历每个 Bean:
- 判断该 Bean 是否是 Handler(是否有
@Controller或@RequestMapping注解)。 - 如果是,利用反射扫描该类下所有的方法(Method)。
- 检查方法上是否有
@RequestMapping注解。 - 如果有,构建
RequestMappingInfo对象(包含 URL、Method、Header 等匹配条件)。 - 注册:将
Map<RequestMappingInfo, HandlerMethod>存入内存注册表 (MappingRegistry)。- 这里会进行冲突检测:如果两个不同的方法映射了完全相同的 URL 和 Method,Spring 会在启动时直接抛出
IllegalStateException: Ambiguous mapping,阻止应用启动。这是 fail-fast 机制的体现。
- 这里会进行冲突检测:如果两个不同的方法映射了完全相同的 URL 和 Method,Spring 会在启动时直接抛出
- 判断该 Bean 是否是 Handler(是否有
3.2 核心二:initHandlerAdapters—— 执行系统的适配
找到了 Handler(Controller 方法),但DispatcherServlet并不知道如何调用它。Handler 可能是一个简单的方法、一个 Servlet、或者一个实现了Controller接口的类。HandlerAdapter负责屏蔽这些差异。
3.2.1 源码逻辑与适配器模式
加载逻辑与initHandlerMappings如出一辙:先探测所有,再按名查找,最后兜底默认。
重点在于默认加载的三大适配器:
RequestMappingHandlerAdapter:最重要。用于适配@RequestMapping注解的方法。HttpRequestHandlerAdapter:用于适配实现了HttpRequestHandler接口的 Bean(通常用于处理静态资源或简单的 HTTP 请求)。SimpleControllerHandlerAdapter:用于适配实现了古老的Controller接口的 Bean。
3.2.2RequestMappingHandlerAdapter的超级初始化
这个适配器是 Spring MVC 中最复杂的组件之一,因为它负责了方法参数的绑定和返回值的处理。在它的afterPropertiesSet中,它会初始化:
argumentResolvers(参数解析器):决定了 Controller 方法参数可以写什么。例如HttpServletRequest,@RequestParam,@RequestBody,@PathVariable,Model等。Spring 默认注册了 30 多种解析器。returnValueHandlers(返回值处理器):决定了 Controller 方法可以返回什么。例如ModelAndView,String(视图名),@ResponseBody(JSON),HttpEntity等。messageConverters(消息转换器):用于@RequestBody和@ResponseBody的序列化/反序列化(如 Jackson)。
深度扩展点:
如果我们想自定义参数解析(例如,在 Controller 方法参数中直接注入当前登录用户User user),我们需要实现HandlerMethodArgumentResolver接口,并将其添加到RequestMappingHandlerAdapter的customArgumentResolvers列表中。
3.3 核心三:initViewResolvers—— 渲染系统的解析
在doDispatch的最后阶段,如果 Handler 返回了ModelAndView,DispatcherServlet需要将逻辑视图名(如 “success”)解析为真正的 View 对象(如 JSP 文件、Thymeleaf 模板)。
3.3.1 链式解析机制
initViewResolvers同样支持加载多个解析器,并形成一个解析链(Chain)。
- 遍历:
DispatcherServlet会遍历viewResolvers列表。 - 尝试解析:调用
viewResolver.resolveViewName(viewName, locale)。 - 返回:如果某个解析器返回了 View 对象,则停止遍历;如果返回 null,则继续下一个。
3.3.2 常见解析器剖析
InternalResourceViewResolver:最经典。支持 JSP。它通常配置prefix和suffix。它总是返回一个 View 对象(哪怕 JSP 文件不存在),所以它通常需要配置order为最大值(优先级最低),作为兜底。BeanNameViewResolver:根据 View 的 bean name 来解析。ContentNegotiatingViewResolver(CNVR):代理模式。它自己不解析视图,而是根据Accept头或扩展名(.json, .xml),委托给其他的 ViewResolver 或 View 来渲染。这在实现同一 URL 支持多格式输出时非常有用。
3.4 核心四:initHandlerExceptionResolvers—— 异常系统的兜底
当 Controller 抛出异常时,为了不让用户看到满屏的 StackTrace,我们需要异常解析器。
3.4.1 异常处理链
同样是一个链。DispatcherServlet会在 catch 块中调用processHandlerException。
ExceptionHandlerExceptionResolver:最重要。处理@ExceptionHandler注解的方法。ResponseStatusExceptionResolver:处理@ResponseStatus注解的异常,将其转换为特定的 HTTP 状态码。DefaultHandlerExceptionResolver:处理 Spring MVC 内部抛出的标准异常(如NoSuchRequestHandlingMethodException,TypeMismatchException),将其转换为 404, 400 等状态码。
第四部分:演进层 - 生产环境中的高级应用与避坑指南
理解了初始化流程,我们能解决什么实际问题?以下是基于大厂真实案例的总结。
4.1 案例一:NoHandlerFoundException为什么不抛出?
现象:前端请求一个不存在的 URL,后端希望捕获这个 404 异常并返回统一的 JSON 格式,结果发现 Spring MVC 默认直接由 Tomcat 返回了一个 404 HTML 页面,根本没进入全局异常处理器。
原因分析:
通过查看DispatcherServlet.doDispatch源码可知,当getHandler返回 null 时,默认行为是调用noHandlerFound方法,该方法直接response.sendError(404),不会抛出异常。
解决方案:
这是初始化配置决定的。我们需要修改DispatcherServlet的属性:
- 设置
spring.mvc.throw-exception-if-no-handler-found=true。 - 关键点:同时必须设置
spring.mvc.static-path-pattern不匹配该 URL(或者关闭默认的静态资源映射),否则SimpleUrlHandlerMapping可能会匹配到静态资源处理逻辑,导致不返回 null。
4.2 案例二:拦截器(Interceptor)执行顺序诡异
现象:定义了多个拦截器,有的用于鉴权,有的用于日志。发现日志拦截器在鉴权失败时没有记录日志。
源码揭秘:HandlerExecutionChain是在HandlerMapping.getHandler()阶段构建的。HandlerMapping会根据配置将拦截器加入链中。
- 执行顺序:
preHandle是按注册顺序正序执行,afterCompletion是按注册顺序倒序执行。 - 中断机制:如果第 3 个拦截器的
preHandle返回false,则第 1、2 个拦截器的afterCompletion会被执行,但第 3 个及后续的拦截器以及 Controller 都不会执行。
排查方案:
检查WebMvcConfigurer.addInterceptors中的注册顺序。确保日志拦截器排在最前面(Order 最小),鉴权拦截器排在日志之后。这样即使鉴权失败(返回 false),日志拦截器的afterCompletion依然能被回调,记录下此次访问。
4.3 案例三:父子容器导致的事务失效
现象:在非 Spring Boot 的老项目中,Service 层的@Transactional注解失效,数据回滚失败。
深度溯源:
这通常是初始化扫描范围重叠导致的“双重加载”。
ContextLoaderListener(Root Context) 扫描了com.app.*,初始化了 Service 和 Controller。此时 Service 是有事务代理的。DispatcherServlet(Web Context) 也扫描了com.app.*,又初始化了一遍 Service 和 Controller。- 覆盖:Web Context 是子容器,它里面的 Bean 会覆盖父容器的同名 Bean(或者 Controller 直接注入了子容器里那个没有事务代理的 Service)。
- Controller 调用的是子容器里的“裸”Service,事务当然失效。
初始化修正:
严格控制component-scan的include-filter和exclude-filter。
- Root Context:
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> - Web Context:
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>,且use-default-filters="false"。
4.4 性能调优:微服务网关层的初始化优化
在 QPS 极高的网关服务(如 Zuul 或 Spring Cloud Gateway,虽然 Gateway 是 WebFlux,但原理类似)中,路由的匹配效率至关重要。
优化思路:
默认的RequestMappingHandlerMapping使用的是AntPathMatcher进行 URL 匹配,这涉及大量的字符串操作和正则匹配。
在 Spring 5.0+ / Spring Boot 2.0+ 中,Spring 引入了PathPatternParser,它比 AntPathMatcher 快得多。
开启方式:
@ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{@OverridepublicvoidconfigurePathMatch(PathMatchConfigurerconfigurer){// 启用基于解析器的路径匹配,性能更高configurer.setPatternParser(newPathPatternParser());}}这会改变initHandlerMappings中RequestMappingHandlerMapping的内部初始化逻辑,构建更高效的 URL 匹配树。
第五部分:总结与展望
5.1 万变不离其宗的“引擎”
DispatcherServlet的源码虽然庞大,但其核心逻辑只有两条线:
- 初始化线 (
onRefresh):依赖倒置、策略模式、约定优于配置。 - 运行时线 (
doDispatch):责任链模式(拦截器)、适配器模式(HandlerAdapter)、模板方法模式(FrameworkServlet)。
不管 Spring 版本如何迭代,从 2.x 到 6.x,这套骨架从未改变。这正是优秀架构设计的生命力所在。
5.2 未来的挑战:从 Servlet 到 Reactive
虽然DispatcherServlet统治了 Java Web 二十年,但在高并发、低延迟的云原生时代,基于 Servlet API 的阻塞模型(Thread-per-Request)正面临挑战。
Spring 5 引入的Spring WebFlux是一次革命。
- 核心替代者:
DispatcherHandler取代了DispatcherServlet。 - 架构差异:不再有
HandlerAdapter(全异步),不再依赖 Servlet API(基于 Netty/Reactor)。 - 初始化:依然保留了类似的策略接口(
HandlerMapping,HandlerResultHandler),但在初始化和执行上实现了全链路异步非阻塞。
作为架构师,理解DispatcherServlet的局限性,并适时引入 WebFlux 或 Vert.x 等响应式框架,将是未来技术选型的关键能力。