news 2026/10/1 18:20:57

SpringMVC核心工作流程拆解:从DispatcherServlet到HandlerAdapter的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringMVC核心工作流程拆解:从DispatcherServlet到HandlerAdapter的完整链路

1. 拆解SpringMVC的核心工作流程

1.1 从一次HTTP请求说起

SpringMVC很多新手都学过,但真正能把一次请求从进来到出去完整讲清楚的人,真不多。我面试了不少候选人,问一句"输入URL回车之后SpringMVC做了什么",很多人能答出DispatcherServlet,但再往下追问HandlerMapping怎么找到一个能处理请求的方法,就卡住了。这个不是背题能解决的,是脑子里没有完整的链路。

一个典型的SpringMVC请求,从Tomcat接收到HTTP报文开始,会被交给DispatcherServlet这个前端控制器。DispatcherServlet拿到请求URL之后,先通过HandlerMapping找到对应的Handler(也就是Controller里的方法,SpringMVC里叫HandlerMethod),然后通过HandlerAdapter把这个方法调用起来。方法执行完会返回ModelAndView,或者直接通过@ResponseBody输出数据。如果是ModelAndView,还需要经过ViewResolver解析出具体视图,再把模型数据填充进去渲染成HTML返回给浏览器。

整个过程可以用一个生活化类比来理解:DispatcherServlet就像一个前台接待员,你到了公司说"我要找市场部的张三",接待员不会自己去找,而是先查通讯录(HandlerMapping),找到张三在哪个工位,然后告诉你在哪儿能找到他(HandlerAdapter调用方法)。最终张三给你一个答复(返回数据),你带着答复离开。所有的"找人""领路""记录"工作,都是前台统一做的,这就是前端控制器模式的核心价值。

1.2 为什么非要有一个"总入口"

在SpringMVC之前,我们写Java Web用的都是传统的Servlet。一个系统里可能有一百个Servlet,每个负责一个功能点。这么做最难受的地方在于公共逻辑没法收口——权限校验、日志记录、编码设置、异常兜底,所有Servlet都得自己写一遍。代码重复不说,还特别容易漏,今天加一个页面忘了配编码过滤器,明天就有一堆乱码bug找上门。

DispatcherServlet作为总入口解决的正是不一样的问题:所有请求先到它手里,它可以统一处理编码、统一做拦截器链调用、统一做异常转发。Controller本身不需要关心请求怎么路由过来的,只需要关注业务逻辑本身。这种"集中式控制+组件化执行"的设计思想,和公司的考勤制度一个道理:你每天上下班打卡,考勤系统统一记录,而不是每个部门各自拿个本子登记,这样管理成本低、规则统一、出了问题也好追溯。

1.3 核心组件各自扮演什么角色

SpringMVC的关键组件其实就五六个,把它们的职责搞清楚,整个框架就褪去了神秘感。

DispatcherServlet是入口,负责统筹调度,但它本身不做具体事。HandlerMapping负责根据URL找到对应的Handler。HandlerMapping最常用的实现是RequestMappingHandlerMapping,它会把@Controller类上和方法上的@RequestMapping注解解析成一个个映射条目,保存URL和方法的对应关系。HandlerAdapter负责真正去调用Controller方法,它处理参数解析、方法参数注入、返回值处理。这个组件特别关键,因为同样的一个方法,有@RequestBody这样的注解时怎么解析参数,没有注解时又从哪拿参数,都是HandlerAdapter在做。ViewResolver负责把逻辑视图名解析成真正的视图。比如Controller返回"index",ViewResolver会找到resolver配置好的前缀后缀,解析成/WEB-INF/views/index.jsp。还有ModelAndView,它不是组件,是Controller和视图之间的数据载体,既携带视图信息,也携带模型数据。

这几个组件之间的调用关系,Spring在DispatcherServlet中早就编排好了,我们不需要自己实现,只需要理解它们怎么配合。理解清楚之后,你在排查问题时会非常快。比如"控制器方法明明写了,但请求404",大概率是HandlerMapping没有注册到这个映射;"返回了JSON但是浏览器拿到的是乱码",多半是HandlerAdapter输出时没有配置好编码处理器。这些都不是玄学,链路搞清楚了,排查方向立刻就清晰了。

2. 核心细节解析与实操要点

2.1 从XML配置到注解驱动的演进

SpringMVC的配置方式,这十年来经历了三个阶段的演进。最早是SSH时代遗留下来的全XML配置,web.xml里要配置DispatcherServlet的映射和Spring的监听器,Spring配置文件里要配置组件扫描、注解驱动、视图解析器,前后可能上百行配置。后来Spring 3.1推出了基于注解+WebApplicationInitializer的配置方式,再后来Spring Boot出现,连配置类都省了,约定大于配置,直接用。

配置方式本身没有绝对的好坏,但理解和掌握"底层发生了什么"始终重要。我现在带团队就要求新人必须用传统XML项目跑一遍SpringMVC,不是为了复古,是为了让新人理解注解只是省去了写配置的步骤,并没有省略配置本身。比如你在Spring Boot里只需要在spring-mvc.properties里写一个prefix和suffix,本质上就是把原来XML里的InternalResourceViewResolver配置搬进去,逻辑没变。

实践中最常见的配置要点有这么几个:第一,DispatcherServlet的url-pattern配置成"/",这样才会进入SpringMVC的映射机制。很多老项目里配的是"*.do",能跑但语义不同,会绕开静态资源的自然访问。第二,必须开启注解扫描,否则@Controller写了白写。第三,配置文件要引入spring-mvc命名空间,开启<mvc:annotation-driven />,没有这个,HandlerAdapter不会启用,到时候方法上的注解一个都不好使。

2.2 请求映射的各种玩法

@RequestMapping是整个SpringMVC开发中出现频率最高的注解之一。类级别加方法级别的组合构成了完整的URL映射。类上写@RequestMapping("/user"),方法上再写@RequestMapping("/list"),合并出来的访问路径就是/user/list。

细化到HTTP方法,Spring 4.3开始提供了简化注解:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping。它们解决的一个重要问题是语义清晰。一个普通方法用@RequestMapping写了method属性限制为GET,代码上看起来还是宽松的,而@GetMapping一眼就知道这方法只处理GET。在实际开发里,RESTful接口推荐用这些派生注解,避免接口被误用。

URL中带参数有两种常见方式。一种叫路径变量,@GetMapping("/user/{id}")配合@PathVariable("id") Integer id,适用于资源定位。一种叫查询参数,@GetMapping("/user")配合@RequestParam("name") String name,适用于过滤场景。路径变量和查询参数在实际项目中都是高频使用,我见过不少新手把两种方式混在一起用,结果要么URL长得没法看,要么传参传不过去。一个简单的取舍原则是:标识资源身份用路径变量,筛选条件用查询参数。

2.3 参数绑定与数据校验

Controller方法入参的绑定,是SpringMVC让我觉得"写起来最爽"的一个部分。一个POJO对象,比如User类有id、name、age三个字段,你接收GET表单请求时,只要方法的入参写成User user,Spring会自动按字段名把请求参数绑定进去。原理其实不复杂,HandlerAdapter通过WebDataBinder,把request的参数一个个匹配到POJO的属性上,再通过反射设置进去。理解了这个底层动作,就能解释为什么POJO字段名和请求参数名不一致时绑不上——因为根本没有可以对齐的桥梁。

日期格式的绑定是新手踩坑的重灾区。HTTP请求里传来的日期通常是个字符串,比如2024-05-20,而POJO字段类型是java.util.Date。SpringMVC默认的日期转换器只能处理特定格式,其他格式直接报400。解决办法是给字段加@DateTimeFormat(pattern = "yyyy-MM-dd"),或者在全局配置一个转换器统一处理。我更推荐全局处理,因为一个项目里日期格式通常是统一的,分散到每个字段上太啰嗦。

参数校验方面,JSR-303规范的@Valid注解配合Hibernate Validator是主流方案。在POJO字段上加@NotNull、@Size、@Email这类约束注解,方法参数前加@Valid,SpringMVC就会在参数绑定时自动执行校验。校验失败抛出的BindException或MethodArgumentNotValidException,再配合全局异常处理,能统一返回错误信息。这个组合是标准做法,我强烈建议任何非内部接口都必须做参数校验,不要信任任何外部输入。

2.4 返回ModelAndView还是直接写JSON

Controller返回数据的方式,大致分两个流派。传统服务端渲染项目,方法返回String逻辑视图名,然后用Model或ModelAndView携带数据,最终由ViewResolver解析成JSP渲染HTML。这种方式的优势是页面在服务端组装好,对浏览器友好,SEO也好处理。前后端分离项目则完全不同,方法上加@ResponseBody,直接返回对象或Map,SpringMVC通过HttpMessageConverter把对象序列化成JSON输出。Spring 4.0之后,可以直接在类上使用@RestController组合注解,等价于@Controller + @ResponseBody。

需要特别提醒的是,很多新手在使用@ResponseBody时遇到JSON格式错误,原因往往是实体类里有循环引用、Date日期序列化格式不对、或者某个getter方法写得不规范导致序列化异常。处理策略上,我建议前后端分离项目统一用@RestController,实体对象里的日期字段统一加@JsonFormat注解,循环引用用@JsonIgnore或@JsonManagedReference处理。JSON序列化这些事情,越早定规范后面越省事。

3. 拦截器、异常处理与文件上传实战

3.1 拦截器:认证、日志、请求计时的一站式解决方案

SpringMVC的拦截器(Interceptor),很多人知道它存在,但用得并不充分。这个机制对应的是HandlerInterceptor接口,包含三个方法:preHandle在Controller方法执行之前调用,postHandle在方法执行完成后视图渲染之前调用,afterCompletion在整个请求处理完成之后调用。三个方法执行的时机不同,决定了你能在哪个环节做哪些事。

preHandle里做的事情最多,也是拦截器最常用的位置。登录校验、权限判断、请求参数预处理、记录请求开始时间,这些都在preHandle里做。关键是这个方法有返回值,返回true放行,返回false就拦住。这就是为什么临时性的登录拦截可以直接在preHandle里写,而不需要改动业务方法。postHandle里不太适合做重量级操作,因为视图还没有渲染,但你仍可以对ModelAndView做最后的调整。afterCompletion不管业务是否抛异常都会执行,适合做资源清理、记录请求耗时,类似Servlet中Filter的after方法。我曾经用它统计接口响应时间,把超过1秒的请求单独打日志,优化接口性能时特别有用。

拦截器要生效,必须注册进SpringMVC的拦截器链。在Spring Boot里写一个WebMvcConfigurer的配置类,在addInterceptors方法里registry.addInterceptor(loginInterceptor).addPathPatterns("/").excludePathPatterns("/login", "/register", "/static/")。拦截器链的执行顺序就是注册顺序,先注册的先执行。这个点我吃了不少亏,一开始以为和Filter的注解顺序一样,结果发现多个拦截器时顺序完全由注册顺序决定,这个细节一定要记住。

这里还必须区分拦截器和Filter,两者不是一回事,很多人混为一谈。Filter是Servlet规范里的东西,在请求进入DispatcherServlet之前执行,属于Web容器层。拦截器是SpringMVC框架的东西,在DispatcherServlet已经收到请求、HandlerMapping已定位到具体Handler之后的环节执行。Filter更擅长处理编码、CORS这类与应用逻辑无关的事,拦截器更适合做与业务绑定紧密的处理,比如登录态校验。

3.2 统一异常处理怎么做才优雅

Controller里每个方法都try-catch是代码坏味道的一种,极其影响阅读。SpringMVC给了我们统一处理的杀手锏:@ControllerAdvice + @ExceptionHandler。@ControllerAdvice可以理解为一个全局的控制器增强,它对所有Controller的方法生效,配合@ExceptionHandler可以指定处理哪种类型的异常,并返回统一的错误响应。

举个例子,定义一个GlobalExceptionHandler类,标注@RestControllerAdvice,然后在方法上写@ExceptionHandler(BusinessException.class),方法返回一个Result对象。这样整个项目里Controller方法可以完全不处理业务异常,只管抛出,异常信息在全局处理类里统一包装。这个模式在团队开发里特别有价值,不同的人写接口时返回错误码格式不统一是家常便饭的事,统一异常处理强制约束了格式,出问题了也知道从哪里查。

还要注意,@ExceptionHandler可以捕获的参数范围很有讲究。比如Exception这个大类不要轻易捕,否则会把系统内部错误也包装成业务错误返回给前端,掩盖真实的异常信息。宁可加一个兜底的Exception处理类,单独返回"系统繁忙"之类的提示,同时把异常堆栈记录到日志,也不要把底层异常直接抛给调用方。我在项目里通常做法是:业务异常精确匹配、参数校验异常单独处理、最后留一个Exception兜底并打完整日志,方便排查线上问题。

3.3 文件上传的完整配置

文件上传也是SpringMVC里用得很多的功能,但配置不好时会出现各种莫名其妙的问题。首先要明确,SpringMVC处理multipart/form-data格式的请求需要MultipartResolver,最常用的是StandardServletMultipartResolver,它基于Servlet 3.0的Part API实现。在Spring Boot里配置相对简单,spring.servlet.multipart.max-file-size和spring.servlet.multipart.max-request-size两个参数就可以控制大小限制。

但如果你维护的是一个传统SpringMVC项目,步骤会多一些:web.xml里DispatcherServlet的multipart-config要配置,或者用JavaConfig方式通过MultipartConfigElement来配置,否则MultipartResolver根本拿不到上传的Part。我当年在一个老项目里踩过这个坑,Spring容器里配置了CommonsMultipartResolver,但Tomcat一直报"Required Part 'file' not present"错误,排查了半天才意识到是web.xml里漏配了multipart-config。控制器方法里接收MultipartFile参数,方法签名写成(@RequestParam("file") MultipartFile file)就行,数据会被封装成MultipartFile对象,可以直接获得原始文件名、文件大小、输入流。

文件上传到服务器,务必要做这几件事:文件类型白名单校验、文件大小限制、文件名重命名防止路径穿越和重名覆盖、存储目录权限控制。不要直接使用用户上传的文件名拼接路径,否则碰到../这种特殊字符时很容易造成安全问题。存文件的目录建议放在项目的应用外部目录,避免打包部署时文件丢失,同时维护起来也方便。

4. 常见问题与排查技巧实录

4.1 常见问题速查表

这些年在群里答疑,SpringMVC的新手问题翻来覆去就那么几个。我把高频问题和排查思路整理成一个速查表,希望你能收藏备用:

现象可能原因排查方法
请求404,页面提示无映射@Controller类没有加注解或没被扫描检查类上注解,检查component-scan配置路径
请求404,访问静态资源失效DispatcherServlet拦截了静态资源配置<mvc:resources mapping="/static/**" location="/static/"/>或WebMvcConfigurer的addResourceHandlers
中文乱码请求/响应编码不一致检查CharacterEncodingFilter配置,检查数据库连接URL的编码参数,检查JSP页面编码
JSON返回报错,页面显示500对象序列化异常检查循环引用、日期格式化、getter方法是否规范
参数绑定失败请求参数名和POJO字段名不一致核对字段名,或使用@RequestParam显式指定参数名
@RequestParam必传参数缺失前端没传该参数设置required=false或前端补传
拦截器不生效没有注册或路径匹配规则错误检查addInterceptors注册配置,以及拦截器的path匹配写法
日期字段绑定失败格式不匹配无转换器加@DateTimeFormat或自定义全局日期转换器
上传文件超过大小限制max-file-size配置过小或未生效检查配置文件,检查是否正确使用multipart配置
方法执行两次拦截器链顺序或Forward跳转重复检查日志确认是哪一层重复调用

4.2 踩过的几个经典坑

第一个坑是404的"模棱两可"。一个Controller方法写的没问题,启动的时候也加载了,但访问永远404。最后发现是类名和方法名都没问题,但类的包路径不在组件扫描的范围内。这种情况Spring不会给任何警告,你只能通过检查启动日志里有没有注册这个映射来确认。后来我的习惯是,新写任何一个Controller,先看启动日志是否刷出Mapping路径,刷出来了再测,能省大量时间。

第二个坑是日期格式问题。有个项目里前端统一传“yyyy/MM/dd HH:mm”的字符串,但POJO里用@DateTimeFormat只配了“yyyy-MM-dd”,结果所有带时间的接口全部400。小范围看是少了一个格式,本质上是全项目没有统一约定日期格式。后来我明确规定前后端交互的所有时间字段统一用时间戳或ISO格式的字符串,JVM层面配置统一时区,之后这类问题基本绝迹。

第三个坑是拦截器里的异常处理。有一版我在preHandle里做登录校验,如果用户没登录,直接response.getWriter().write("未登录")并返回false。看起来没毛病,但被全局异常处理器捕获不到,因为响应已经直接写回去了,格式和其他接口不一致。前端拿到的数据结构和登录失效的处理逻辑对不上,排查时愣是没往拦截器方向想。现在的做法是:拦截器里发现未登录时,直接抛出一个自定义的AuthException,交给全局异常处理器统一输出。

4.3 排查思路的核心方法论

排查SpringMVC问题,我有一个自己的固定套路。第一步,确认请求确实到达了DispatcherServlet,在拦截器preHandle里打一条日志就能确认。第二步,确认Controller方法有没有被调用,在方法入口打日志。第三步,确认返回的数据和视图解析有没有问题。三步日志打下来,链路中哪个环节断了一目了然。这个方法效率极高,比瞎猜强太多。

另外一个方法论是"先确认配置,再确认代码"。SpringMVC的很多问题,其实都是配置层面的问题,比如扫描路径、静态资源配置、拦截器注册。我见过有人对着一个配置错误的项目调试了几个小时Controller代码,最后发现只是配置类少写了一个@EnableWebMvc。配置文件不是无关紧要的"工程脚手架",它就是程序的一部分,排查问题要把它放在第一优先级。

5. 一点更进阶的实战建议

当你能熟练使用SpringMVC处理日常开发之后,建议从这几个角度继续深挖。其一是拦截器与异步请求的关系,SpringMVC从3.2开始支持Servlet 3异步处理,异步请求时拦截器会先执行完preHandle再触发实际业务线程,此时postHandle和afterCompletion的行为会变得不同。如果你的接口里用了DeferredResult或WebAsyncTask,一定要把拦截器的执行时机重新想一遍。

其二是对HandlerInterceptor和Filter的使用边界要有清晰的判断。全局的请求日志、防止重复提交这类需求,用Filter更合适,因为它在最外层;而基于登录态的页面跳转、角色权限校验,用拦截器更顺手,因为它能拿到HandlerMethod反射上的注解信息。我在项目里经常同时使用两者,各管一摊,相得益彰。

其三是数据校验、参数绑定和异常处理的组合使用。当项目规模变大,接口数量过百时,统一的参数校验规范能极大的降低沟通成本。我会把所有的自定义校验逻辑封装成注解,和JSR-303内置约束一起使用,配合全局异常处理器统一输出错误码和错误消息。这个体系一旦搭好,新增一个接口只需要写好POJO约束和业务逻辑,再也不用关心错误响应格式不一致的问题。

最后分享一个自己用了很久的小技巧。开发阶段把SpringMVC的日志级别调到DEBUG,能看到HandlerAdapter对每个方法的调用细节和处理耗时。生产环境再调回INFO,不影响性能。这个做法帮我解决过很多"说不清道不明"的奇怪问题,说白了就是让框架把自己做的事尽量展示出来。SpringMVC的学习没有终点,它作为Spring全家桶的基石,你今天花在理解DispatcherServlet上的每一分钟,未来在理解Spring Boot自动配置和微服务内的Web层时,都会加倍还给你。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 18:20:39

一文搞懂Shell特殊符号:通配符、引号、重定向与管道

天天和 Linux 打交道&#xff0c;谁还没被 shell 命令行里的特殊符号坑过&#xff1f;反正我是实打实被坑过很多次&#xff0c;尤其是刚把 shell 脚本当回事的那段时间&#xff0c;一个没加引号的变量、一条写错的重定向&#xff0c;就能让备份任务在半夜静悄悄失败。后来才慢慢…

作者头像 李华
网站建设 2026/10/1 18:20:12

CrewAI实战:从零搭建多Agent协作流水线

1. 框架选型&#xff1a;为什么是CrewAI而不是LangChain或AutoGen 先说结论&#xff1a;这个5.9万Star的项目&#xff0c;大概率是CrewAI。它是目前多智能体编排领域最火的开源框架之一&#xff0c;GitHub上五位数Star&#xff0c;社区活跃度非常高&#xff0c;文档友好&#x…

作者头像 李华
网站建设 2026/10/1 18:18:19

BP神经网络信贷信用评估实战:从预处理到违约概率预测

简介&#xff1a;基于BP神经网络的个人信贷信用评估&#xff0c;是一份面向金融风控入门者与机器学习初学者的MATLAB实现方案。资源围绕信用评估场景&#xff0c;利用BP神经网络对个人信贷数据进行分类识别&#xff0c;包含完整可运行的main.m主脚本&#xff0c;以及配套的germ…

作者头像 李华
网站建设 2026/10/1 18:16:30

ANSYS许可合规检查:授权文件、日志台账与并发审计实战

1. 许可合规检查真正查的是什么&#xff1a;从"能不能跑起来"到"跑得合不合规"大部分人第一次接触ANSYS 许可合规性检查&#xff0c;都是被动的——要么是采购部门要续费&#xff0c;需要一份"到底有多少人真在用"的说明&#xff1b;要么是外部合…

作者头像 李华
网站建设 2026/10/1 18:14:18

基于Spring Boot+Vue的校园生活服务平台设计与实现

1. 项目概述与选题思路1.1 为什么选“校园生活服务平台”这个题目每年到毕设季&#xff0c;后台咨询最多的题目类型之一就是“Spring Boot Vue”。原因很简单&#xff1a;这个组合是目前国内中小型系统开发最主流的技术栈&#xff0c;企业里用得多&#xff0c;网上教程也多&am…

作者头像 李华
网站建设 2026/10/1 18:13:46

EVO-X5 Pro:桌面级AI超算的协同架构解析

1. 这不是“升级”&#xff0c;而是桌面计算范式的迁移&#xff1a;EVO-X5 Pro 的真实定位解析“极摩客发布搭载 AMD AI Max 的桌面超算 EVO-X5 Pro”——这个标题里藏着三个被绝大多数媒体和用户忽略的关键定语&#xff1a;“AI Max”、“桌面”、“超算”。它们不是营销话术的…

作者头像 李华