资讯动态

SpringMVC请求处理全解析:从DispatcherServlet到参数绑定

发布时间:2026/10/16 0:54:46 来源:尧图企业网站定制
终于走到 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 groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.30/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/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)的花括号名字和方法参数名要对应如果方法参数名跟变量名一致也可以省略括号里的名字。第二路径变量默认是字符串但如果参数类型声明成LongSpringMVC 会尝试自动转换。转不了就报 400 错误。第三编号{id}可以用正则约束比如GetMapping(/user/{id:\\d})限定 id 只能是数字不匹配的 URL 就不进这个方法留给别的匹配规则处理。我还见过有人把路径变量和查询参数混着用/user/{id}?namexxx。这种情况我强烈建议能放进路径的语义化主键就放路径像排序、分页、筛选这类可选条件一律放查询参数别混成一团。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?keywordspringpage2keyword和page会自动赋值。SpringMVC 是根据参数名匹配的而不是按位置所以参数顺序无所谓。但这里有坑如果请求里缺了keywordSpring 会传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?namezhangage27pageNo1pageSize10SpringMVC 会自动调用 setter 把同名参数塞进去。注意JavaBean 必须有无参构造方法并且要有 setter这两点缺了都不行。我之前在这上面栽过跟头看着参数传过来了结果对象里全是 null一查才发现是 IDE 自动生成的 setter 被误删了。这种对象绑定方式在处理表单请求时特别好用比一个个RequestParam干净得多。但前提是字段不能太多否则类本身就会变得很臃肿建议一个查询对象只对应一个业务场景。3.3 RequestBody 绑定 JSON 数据现在前后端分离的项目POST 请求多数直接传 JSON 字符串而不是表单格式。这时候要用RequestBodyPostMapping(/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、ResponseBodyController 方法的返回值类型决定了 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本身就是ControllerResponseBody的组合注解。我在正式项目里几乎只用RestController返回 JSON服务端渲染已经很少写了。但学习阶段反而建议你把三种方式都试一遍因为面试题特别喜欢问“方法返回 String 有什么区别”本质就是在考你对视图解析器和消息转换器这两个机制的区分。4.2 返回 JSON 背后的消息转换原理ResponseBody之所以能把一个User对象变成 JSON 字符串靠的是HttpMessageConverter机制。SpringMVC 会遍历注册好的转换器根据返回类型和请求端的Accept头决定用哪个转换器。Jackson 的MappingJackson2HttpMessageConverter负责把对象序列化成 JSON 字符串。这里有个容易被忽视的点如果你返回的类型是String并且也用了ResponseBodySpring 会优先用StringHttpMessageConverter不会经过 Jackson。这就导致返回字符串时中文可能会是 ISO-8859-1 编码浏览器看到乱码。解决办法是显式给StringHttpMessageConverter设置 UTF-8或者在方法上加produces application/json;charsetUTF-8。我在多年前的老项目里被这个乱码折腾过一下午印象太深刻了。4.3 重定向与转发redirect、forward 的区别Controller 里返回redirect:/user/listSpringMVC 会响应一个 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传了pageabc一定会报错。这种问题别去 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 返回比光看我写体会深刻得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑