☰
SpringMVC请求处理全解析:从DispatcherServlet到参数绑定
2026/10/14 7:59:41 网站建设 项目流程

终于走到 SpringMVC 这一站了。前面十几篇我一直在折腾 Spring 的 IoC 容器、Bean 生命周期、AOP 这些东西,虽然都在讲“框架”,但说白了都是在后台干活,真正跟用户打交道、接收 HTTP 请求、把数据从浏览器弄进来再写回去的,还得看 SpringMVC。这篇就把 SpringMVC 的快速入门和请求处理整个过程过一遍,你跟着敲一遍,基本就能明白 Controller 是怎么接收请求、怎么解析参数、怎么往客户端返回数据的了。

1. SpringMVC 快速入门:搞懂它到底替我们干了什么活

1.1 为什么非要有 SpringMVC?先看 Servlet 时代的痛点

要理解 SpringMVC 的价值,最好先回到原生 Servlet 那个年代。以前写 Web 接口,你得写一堆重复动作:在doGet/doPost里手动判断请求方式,手动调用request.getParameter()取值,手动把字符串转成数字,手动把查询结果塞进HttpServletRequest的属性里,然后forward到 JSP 页面去渲染。业务逻辑稍微多一点,Servlet 类就膨胀得惨不忍睹,而且每个接口都要重写一遍“取参—调服务—转页面”的套路,极其枯燥。

SpringMVC 解决的就是这一层的重复劳动。它把“接收请求、解析参数、调用处理方法、渲染结果”这个标准流程收纳进框架内部,程序员只需要写一个普通的 Java 类,在上面标@Controller,再给具体方法标上@GetMapping这类注解,框架就会自动把 URL 映射到方法上。你不用碰HttpServletRequest的底层 API,也能拿到经过类型转换的参数。这套机制在 MVC 模式里处于 Controller 层,是 Web 应用的入口,也是我整个 Spring 学习曲线中最能直观感受到“框架威力”的一环。

1.2 最小工程搭建:纯 Java 配置也能跑

现在大多数项目直接基于 Spring Boot 起步,spring-boot-starter-web一加上,SpringMVC 就被自动装配好了,根本不需要写web.xml。但作为学习,我还是建议你亲手搭一次“非 Boot”的 SpringMVC 工程,这样才能看清底层配置到底配了什么。

以 Maven 工程为例,核心依赖就这几个:

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.30</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>

接着写一个初始化类,替代web.xml里的配置:

public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { RootConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] { WebConfig.class }; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } }

WebConfig里这样写:

@Configuration @EnableWebMvc @ComponentScan("com.example.controller") public class WebConfig implements WebMvcConfigurer { @Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); return resolver; } }

配完这个,一个最小可用的 SpringMVC 环境就起来了。DispatcherServlet会被自动注册到/路径下,所有请求先进它手里,再由它分发。

注意:纯 Java 配置里@EnableWebMvc一定要加。不加的话,SpringMVC 的一些默认策略(比如参数解析器、消息转换器)不会注册,你后面写@RequestBody的时候会碰见各种奇怪报错。这算是我第一次搭环境时踩过的坑。

1.3 DispatcherServlet 是请求处理的总入口

SpringMVC 的核心叫DispatcherServlet,它本质上还是一个 Servlet,只不过把所有活儿都集中到了一起。一次请求的生命周期大致是这个样子:

浏览器发来请求,DispatcherServlet先通过HandlerMapping找到能处理这个 URL 的HandlerExecutionChain,也就是你写的那个 Controller 方法,外加可能拦截它的拦截器。找到之后,DispatcherServlet会调用HandlerAdapter去真正执行这个处理方法。执行之前,SpringMVC 会做参数解析,把请求里的数据变成方法参数;执行完成后,返回值会被HandlerAdapter处理成ModelAndView,最后交给ViewResolver解析成真正的视图渲染给用户。

我刚开始学的时候一直绕不清楚“HandlerMapping、HandlerAdapter”这两个词。后来我想了个笨办法理解:HandlerMapping是负责找人的——根据 URL 找到该找哪个方法;HandlerAdapter是负责带人干活的——因为不同方法的参数、注解、返回值五花八门,得有一个适配器统一指挥。这两个角色配合好了,Controller 才能跑得顺。

2. 请求映射:如何把 URL 准确送到对应方法手里

2.1 @RequestMapping 与快捷注解怎么选

请求映射最基础的注解是@RequestMapping,比如这样:

@Controller public class UserController { @RequestMapping("/user/detail") public String detail() { return "user_detail"; } }

只要访问/user/detail,不管 GET 还是 POST,都会进到detail()。但如果我限定只接收 GET 请求,就得写成@RequestMapping(value = "/user/detail", method = RequestMethod.GET)。这么写没问题,但代码有点啰嗦。所以 SpringMVC 后来提供了@GetMapping、@PostMapping、@PutMapping、@DeleteMapping这些快捷注解,它们的本质就是@RequestMapping加上了 method 限定。

我在实际项目里基本只用快捷注解。一方面代码简洁,一眼能看出接口只接受什么请求;另一方面也避免因为忘了写 method 而把 POST 接口意外暴露成 GET。给新手的建议是:能用快捷注解就别用@RequestMapping写长参数,除非你要同时接收多种请求方式。

2.2 路径变量与 REST 风格映射

接口设计走向 REST 之后,URL 里普遍会带资源 ID,比如/user/123。SpringMVC 支持用花括号声明路径变量:

@GetMapping("/user/{id}") @ResponseBody public String getUser(@PathVariable("id") Long id) { return "user id = " + id; }

这里有几个细节。第一,@PathVariable("id")的花括号名字和方法参数名要对应,如果方法参数名跟变量名一致,也可以省略括号里的名字。第二,路径变量默认是字符串,但如果参数类型声明成Long,SpringMVC 会尝试自动转换。转不了就报 400 错误。第三,编号{id}可以用正则约束,比如@GetMapping("/user/{id:\\d+}"),限定 id 只能是数字,不匹配的 URL 就不进这个方法,留给别的匹配规则处理。

我还见过有人把路径变量和查询参数混着用:/user/{id}?name=xxx。这种情况我强烈建议,能放进路径的语义化主键就放路径,像排序、分页、筛选这类可选条件一律放查询参数,别混成一团。

2.3 映射冲突与请求无法匹配的排查

请求映射最让人头疼的问题是冲突。同一个 URL 被多个方法映射时会直接启动报错:“Ambiguous mapping found”。最典型的情况有两种:一是同事各写各的,两个 Controller 里都映射了/user/detail;二是 REST 风格下/user/{id}和/user/query这种静态路径频繁交叉。Spring 匹配的时候,静态路径的优先级高于路径变量,所以/user/query会优先匹配静态的,但两个方法如果都定义成/user/{id}形态,就冲突了。

如果请求发出去没有得到预期响应,第一件事不是看业务代码,而是去看看有没有 404。404 有两种含义:要么项目没起来,要么根本没有匹配的映射。我以前排查的时候有个习惯:先看控制台启动日志有没有报“Mapped”相关记录,再决定是查路由还是查容器。

3. Controller 参数绑定:从请求里把数据剥出来

3.1 简单参数与 @RequestParam 的区别

Controller 方法里直接写参数名就能接收到请求参数,这在 SpringMVC 里叫参数绑定。比如:

@GetMapping("/search") @ResponseBody public String search(String keyword, Integer page) { return "keyword=" + keyword + ", page=" + page; }

访问/search?keyword=spring&page=2,keyword和page会自动赋值。SpringMVC 是根据参数名匹配的,而不是按位置,所以参数顺序无所谓。但这里有坑:如果请求里缺了keyword,Spring 会传null进来;如果你声明的是基本类型int page,缺参时直接报 500 错误,因为没法把 null 赋给 int。所以能用包装类型Integer就用包装类型,或者配合默认值。

@RequestParam则更主动。它可以指定参数名、设置是否必填、提供默认值:

@GetMapping("/search") @ResponseBody public String search(@RequestParam("keyword") String kw, @RequestParam(value = "page", defaultValue = "1") Integer page) { return "kw=" + kw + ", page=" + page; }

我个人的原则是:参数少且跟方法名意图一致时直接裸写;参数多、需要改名或给默认值时,统一加@RequestParam,让接口契约更明确。

3.2 表单参数封装成对象

请求参数一多,逐个写在方法签名里很难看。SpringMVC 支持直接把参数封装到 JavaBean 里:

public class UserQuery { private String name; private Integer age; private Integer pageNo; private Integer pageSize; // getter/setter 省略 } @GetMapping("/user/list") @ResponseBody public String list(UserQuery query) { return "name=" + query.getName() + ", page=" + query.getPageNo(); }

访问/user/list?name=zhang&age=27&pageNo=1&pageSize=10,SpringMVC 会自动调用 setter 把同名参数塞进去。注意:JavaBean 必须有无参构造方法,并且要有 setter,这两点缺了都不行。我之前在这上面栽过跟头,看着参数传过来了,结果对象里全是 null,一查才发现是 IDE 自动生成的 setter 被误删了。

这种对象绑定方式在处理表单请求时特别好用,比一个个@RequestParam干净得多。但前提是字段不能太多,否则类本身就会变得很臃肿,建议一个查询对象只对应一个业务场景。

3.3 @RequestBody 绑定 JSON 数据

现在前后端分离的项目,POST 请求多数直接传 JSON 字符串,而不是表单格式。这时候要用@RequestBody:

@PostMapping("/user/save") @ResponseBody public String save(@RequestBody User user) { return "saved: " + user.getName(); }

前端传{"name":"zhang","age":27},SpringMVC 会通过HttpMessageConverter把 JSON 反序列化成User对象。默认情况下,SpringMVC 需要你有 Jackson 依赖,配置好MappingJackson2HttpMessageConverter,或者直接引入spring-boot-starter-web(它默认带了 Jackson)。没有 Jackson 的话,@RequestBody会报“HttpMessageNotReadableException”。

我见过不少新手在接口层用@RequestBody,却把 Controller 方法标注成@Controller而不是@RestController,结果返回值被当成视图名去找 JSP,前端拿到一整页 HTML 而不是 JSON。这个坑下面一节要仔细展开。

4. 响应处理与视图解析:请求完了数据怎么出去

4.1 返回值决定行为:String、ModelAndView、@ResponseBody

Controller 方法的返回值类型决定了 SpringMVC 下一步怎么走。最常见的有三种:

第一种,返回String且类上没有额外注解,SpringMVC 会把它当成视图名,交给ViewResolver解析。比如返回"user_detail",配合前面配置的prefix="/WEB-INF/views/"和suffix=".jsp",最终定位到/WEB-INF/views/user_detail.jsp。

第二种,返回ModelAndView,手动指定视图名和数据模型:

@GetMapping("/user/info") public ModelAndView info() { ModelAndView mv = new ModelAndView("user_info"); mv.addObject("name", "zhang"); return mv; }

第三种,用@ResponseBody注解告诉 SpringMVC:“别找视图了,直接把返回值写进 HTTP 响应体。”配合@RestController可以省略这个方法上的@ResponseBody,因为@RestController本身就是@Controller+@ResponseBody的组合注解。

我在正式项目里几乎只用@RestController返回 JSON,服务端渲染已经很少写了。但学习阶段反而建议你把三种方式都试一遍,因为面试题特别喜欢问“方法返回 String 有什么区别”,本质就是在考你对视图解析器和消息转换器这两个机制的区分。

4.2 返回 JSON 背后的消息转换原理

@ResponseBody之所以能把一个User对象变成 JSON 字符串,靠的是HttpMessageConverter机制。SpringMVC 会遍历注册好的转换器,根据返回类型和请求端的Accept头决定用哪个转换器。Jackson 的MappingJackson2HttpMessageConverter负责把对象序列化成 JSON 字符串。

这里有个容易被忽视的点:如果你返回的类型是String,并且也用了@ResponseBody,Spring 会优先用StringHttpMessageConverter,不会经过 Jackson。这就导致返回字符串时,中文可能会是 ISO-8859-1 编码,浏览器看到乱码。解决办法是显式给StringHttpMessageConverter设置 UTF-8,或者在方法上加produces = "application/json;charset=UTF-8"。我在多年前的老项目里被这个乱码折腾过一下午,印象太深刻了。

4.3 重定向与转发:redirect、forward 的区别

Controller 里返回"redirect:/user/list",SpringMVC 会响应一个 302 让浏览器重新发起新请求;返回"forward:/user/list"则是服务器内部直接转发,浏览器 URL 地址栏不变。区别说白了就是:重定向是浏览器换页面,转发是服务器内部跳转。

我给新手的建议是:处理完 POST 请求后跳转页面,一定用redirect,这样浏览器刷新时不会把上次的 POST 请求再提交一遍,避免重复提交的坑。接口内部做权限校验不通过时,可以用forward到统一的错误页面,不过现在前后端分离之后,这类场景更多是返回状态码让前端自己处理。

5. 实战中的高频问题与排查技巧

5.1 请求到了但 404?先分清容器层级问题

遇到 404 时,先确认是“整个应用没启动”还是“路径没匹配上”。如果 Tomcat 起来了但还是 404,八成是DispatcherServlet的映射路径问题。getServletMappings()返回/时,Controller 里的路径都是相对于应用根路径的;返回/api时,所有请求都要带/api前缀。这个前缀不匹配,SpringMVC 根本不会收到请求。

还有一个文件夹层面的坑:视图 JSP 放在/WEB-INF/views/下面,浏览器直接访问是访问不到的,这是 Servlet 规范的限制,反而成了保护。所以 JSP 找不着时,要检查ViewResolver的prefix和suffix有没有拼错,特别是最后那个斜杠,/WEB-INF/views和/WEB-INF/views/解析出来的路径是完全不同的。

5.2 参数接收为 null 或类型转换报错,从日志找答案

参数绑定失败时,SpringMVC 抛出的异常通常是MethodArgumentTypeMismatchException或ServletRequestBindingException。看到这类异常,先检查请求参数类型是不是能转成你声明的类型。比如声明Integer page,传了page=abc,一定会报错。这种问题别去 Controller 里加 try-catch,数据进方法之前就已经转化失败了,加 catch 也救不回来。正确做法是定义@ExceptionHandler统一处理,或者用@InitBinder自定义类型转换器。

我自己的习惯是:Controller 里尽量用 DTO 或包装类型接参,别直接接收基本类型,同时每个列表查询接口都给分页参数设默认值。这样即便前端漏传,也不至于因为 null 导致后续空指针。

5.3 拦截器不生效,先查配置和注册路径

拦截器是 SpringMVC 请求处理里经常用到的一个环节,但它不生效的时候也很迷惑。常见的原因有两个:一是拦截器没有注册到WebMvcConfigurer的addInterceptors里,只是单纯写了一个实现了HandlerInterceptor的类,那它永远不会被执行;二是注册了,但addPathPatterns写的是/api/**,而实际业务请求路径是/user/list,自然拦不到。

让我给你一个排查拦截器的顺序:先确认实现类有没有被 Spring 管理,再确认注册代码在哪个配置类里生效,最后通过临时在preHandle里加日志确认有没有进入。三步走完,基本能定位到 90% 的问题。

提示:拦截器适合做登录校验、日志记录、接口耗时统计,但不适合做复杂的业务逻辑。业务逻辑放拦截器里会让流程变得隐蔽,出问题的时候排查成本很高。

6. 个人经验分享:如何把 SpringMVC 学扎实

上面把 SpringMVC 的快速入门和请求处理链路讲完了,最后分享几个我这十几篇 Spring 学习过程中的体会。

第一,SpringMVC 的请求处理链路跟 Servlet 规范紧密相关,你真正理解了DispatcherServlet、HandlerMapping、HandlerAdapter这三个角色,后面看 Spring Boot 的自动配置也会轻松很多。千万别跳过框架原理直接只写 Controller,那块地基不扎实,后面排查问题全靠猜。

第二,建议你亲手把 Spring Boot 环境下的 Web 请求跟纯配置环境下的 Web 请求各调一次。你会发现 Boot 里很多东西是自动配好的,比如 Jackson、Tomcat、路径匹配策略。用纯配置环境调试时,你才能看到@EnableWebMvc和ViewResolver到底做了什么,这个“祛魅”过程很重要。

第三,请求处理的最后一步不是 Controller 返回,而是数据正确格式化并写回响应。@ResponseBody、HttpMessageConverter、UTF-8 编码,这一串东西是前后端联调最容易出乱子的地方。建议你每个接口写完都顺手用 Postman 验证一下请求方法、URL、JSON 结构和响应编码,形成肌肉记忆。

SpringMVC 这一站学完,Web 层的基本功就算立住了。下一篇我打算把拦截器和异常处理这两个点单拎出来细讲,毕竟在实际项目里,安全校验、日志埋点、统一异常返回都靠它们。现在你可以先把这篇里的最小工程搭出来,写两个 Controller,用 Postman 反复发几次请求试试参数绑定和 JSON 返回,比光看我写体会深刻得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询