后端面试里“Spring MVC请求流程”算是被问得最频繁的一道题,但多数人只能背出 DispatcherServlet → HandlerMapping → HandlerAdapter 这种三行答案。说实话,只背到这一步,面试官一追问细节就露馅,线上遇到问题也不知道该从哪里断点。我自己的习惯是:遇到请求相关的问题,直接打开源码,从doDispatch()方法一行一行往下看,把一次请求从头到尾走完。这篇文章就从源码视角把整条链路拆开,再把这几年实战里踩过的坑一并整理出来。
先说一下这篇文章适合谁:刚看完Spring基础、准备啃Spring MVC源码的初学者,能顺着链路把核心类串起来;有两年左右经验的后端,遇到参数解析、视图解析、异常处理这类问题能少走弯路。读完你会清楚一个HTTP请求从进入Servlet容器开始,到返回响应给客户端,中间到底经过哪些核心组件、每个组件在做什么、为什么有的接口返回JSON,有的接口却返回了视图名。
1. 一次请求的完整链路:先看清全局地图
1.1 请求到达DispatcherServlet前,容器做了什么
很多人一说Spring MVC就只盯着DispatcherServlet,但请求真正到达它之前,Servlet容器(Tomcat、Jetty等)已经做了一轮工作。以Tomcat为例,客户端发来一个HTTP请求,Tomcat的连接器线程会解析请求行、请求头、请求体,然后根据url-pattern或@WebServlet的映射规则,找到对应的Servlet来接管。这个阶段发生的事跟Spring MVC还没有任何关系,Spring MVC只是在这个环节被选中后才开始介入。
DispatcherServlet本身就是一个Servlet,它继承了HttpServlet,你在配置里看到的DispatcherServlet或者Spring Boot自动配置里注册的那个dispatcherServlet,最终都会被映射到一个路径上。常见的配置方式有两种:一种是在web.xml里写<servlet-mapping>,把/交给DispatcherServlet;另一种是Spring Boot中通过ServletRegistrationBean注册。这里有个容易忽略的细节:映射路径是/还是/*,行为差异非常大。/只匹配不含后缀的路径,静态资源、JSP等不会被拦截;/*则匹配所有路径,包括JSP和静态资源,这个在后面的避坑章节再展开。
请求进入DispatcherServlet后,很多人的源码阅读之旅就是从这里开始的。FrameworkServlet里有一个service()方法,它会调用doGet()、doPost()等,这些方法最终都汇聚到doService(),然后调用核心的doDispatch()。换句话说,整个Spring MVC的请求处理中枢就是DispatcherServlet.doService()和doDispatch(),一个是环境准备,一个是核心分发。
1.2 DispatcherServlet.doDispatch():整个流程的总纲
打开DispatcherServlet的源码,doDispatch()方法并不长,三四分钟就能逐行读完。它的逻辑可以抽取成一句话:把请求交给合适的处理器去处理,然后把处理结果包装成响应返回给客户端。这句话背后藏着组件协作的完整链条。
DispatcherServlet在初始化时(initStrategies)会从Spring容器里查找一组策略组件,关键的有这几类:
HandlerMapping:负责根据请求找到处理器(Handler)和拦截器(Interceptor),结果封装在HandlerExecutionChain里;HandlerAdapter:负责真正调用处理器方法,适配不同的调用方式;HandlerExceptionResolver:处理请求过程中抛出的异常,转换成响应;ViewResolver:当处理器返回逻辑视图名时,负责解析成真正的视图;LocaleResolver、ThemeResolver、MultipartResolver等:分别处理国际化、主题和文件上传解析。
在doDispatch()里,顺序大致是这样的:先处理Multipart请求,把请求包装成MultipartHttpServletRequest;然后遍历HandlerMapping,拿到HandlerExecutionChain;接着遍历HandlerAdapter,找到能支持该Handler的适配器;执行拦截器的preHandle;调用处理器方法,得到ModelAndView;执行拦截器的postHandle;最后交给processDispatchResult处理视图渲染或异常。
这里我建议初学者别急着背代码,先在IDEA里给doDispatch()打上断点,然后用浏览器或Postman发一个最简单的请求。你会看到流程非常清晰,每个变量的变化都能在调试窗口里看到。这种方式比任何资料都直观。真正把doDispatch()吃透了,后面的源码量再大也都是在这个框架里填肉。
2. HandlerMapping:怎么找到能处理请求的那个“处理器”
2.1 三种HandlerMapping的定位逻辑
先看HandlerMapping接口,核心方法只有一个:getHandler(HttpServletRequest request)。它返回HandlerExecutionChain,里面装着一个Handler和若干拦截器。Spring MVC默认注册了多个HandlerMapping,它们按顺序匹配,谁先命中谁负责。
默认配置里比较重要的几个:RequestMappingHandlerMapping,这是最核心的一个,它通过@RequestMapping注解建立请求条件和处理器方法之间的映射关系,负责我们平时写的Controller方法;SimpleUrlHandlerMapping,它通过配置URL与Handler的映射来匹配,适合传统的XML配置方式;BeanNameUrlHandlerMapping,它把Bean名称当作URL模式来匹配。在现代项目中,绝大多数请求都由RequestMappingHandlerMapping处理,另外两个更多用于特殊场景,比如静态资源的ResourceHttpRequestHandler处理。
RequestMappingHandlerMapping的底层是个MappingRegistry,它保存了PathPattern(或AntPath)到HandlerMethod的映射。当一个请求进来,它提取请求的路径、HTTP方法、请求参数、请求头等条件,逐一比对注册表里的RequestMappingInfo。比如一个@GetMapping("/user/{id}"),它记录的请求条件就是路径/user/{id}、方法限定GET、参数无要求、Header无要求。Spring Boot 2.6以上默认启用了新的PathPatternParser,它与旧版AntPathMatcher在路径匹配规则上有些差异,比如/**只匹配多层路径,不再匹配单层路径。
由于存在多个HandlerMapping按Order排序,查找时会按顺序尝试,一旦某个HandlerMapping返回了非空的HandlerExecutionChain就立即停止。有时候你注册了自定义的HandlerMapping,没有注意它的Order值,导致它抢在RequestMappingHandlerMapping前面处理了本该由注解方式匹配的请求,就会出现“请求没走进Controller”的诡异现象。
2.2 查找过程中容易踩的坑
第一个常见坑是静态资源404。把请求路径映射到/后,DispatcherServlet其实并不认识css/js/png这类静态资源。它的HandlerMapping根本无法在MappingRegistry里找到对应处理器,于是返回404。解决办法通常是两步:在Spring Boot里默认已经配置了ResourceHttpRequestHandler来处理静态资源,路径默认是/**对应classpath下的static目录;如果手写配置,可以把ResourceHandlerRegistry的相关配置补上。另一个办法是配置<mvc:default-servlet-handler/>,把处理不了的请求交回给Servlet容器默认的Servlet去处理。这两种思路本质上是“给Spring MVC提供一个兜底处理器”。
第二个坑是两个Controller的方法有重复的请求映射路径。比如@GetMapping("/user/{id}")写了两遍,Spring容器在启动时会检测到路径冲突并抛异常。但如果一个是/user/{id},另一个是/user/{name},在路径变量占据相同位置的情况下,运行时才会出现Ambiguous mapping的异常。这个报错信息会明确告诉你哪两个方法冲突,定位起来很快。
第三个关于拦截器。HandlerExecutionChain是Handler和拦截器的组合,Spring MVC的拦截器并不是Servlet规范的Filter,而是Spring自己的HandlerInterceptor。它的执行顺序是preHandle→ 处理器方法 →postHandle→afterCompletion,前一个拦截器preHandle返回false,后续拦截器和处理器方法都不会执行。很多人在拦截器里做一些权限校验,但忘记对静态资源放行,最后页面白屏,卡在资源加载上。遇到这种情况,第一反应应该去看拦截器的excludePathPatterns是否配置完整。
3. HandlerAdapter与InvokableHandlerMethod:真正调用我们写的代码
3.1 适配器模式选型与核心结构
HandlerAdapter是整个调用链里最关键的一环。它存在的意义在于:DispatcherServlet不关心具体的Handler长什么样,它只要求每个处理器都能被某个适配器调用。适配器模式让Spring MVC既支持注解方式的HandlerMethod,也支持Controller接口实现和其他类型的Handler。
RequestMappingHandlerAdapter是最常用的一类适配器,它专门支持@RequestMapping注解标注的方法。它内部把Handler方法包装成ServletInvocableHandlerMethod,这个类负责在调用目标方法前处理参数解析,在调用后处理返回值。深入看,ServletInvocableHandlerMethod继承了InvocableHandlerMethod,什么东西放在父类、什么东西放在子类,是理解这套设计的关键:InvocableHandlerMethod只负责“参数解析+反射调用”,而ServletInvocableHandlerMethod额外多了“返回值处理”和响应状态的设置。
我建议读者把InvocableHandlerMethod当成核心研究对象,尤其是它的getMethodArgumentValues()方法和doInvoke()方法。前者决定了你写在Controller方法里的参数是怎么从HTTP请求里拿到值的,后者决定了方法到底怎么被反射调用。其中参数解析是整个流程里最容易出问题、也最能体现源码功底的部分。
3.2 参数解析器逐个过关
Spring MVC把“怎么从请求里取参数”这个职责交给了HandlerMethodArgumentResolver接口,它只有两个方法:supportsParameter和resolveArgument。调用时,InvocableHandlerMethod会遍历所有已注册的参数解析器,找到第一个supportsParameter返回true的解析器,然后用它去解析参数。
默认注册的解析器非常多,从名字就能看出用途:
RequestParamMethodArgumentResolver:处理@RequestParam注解,把请求参数绑定到方法参数;PathVariableMethodArgumentResolver:处理@PathVariable注解,从URL模板变量取值;RequestHeaderMethodArgumentResolver:处理@RequestHeader注解,从请求头取值;RequestBodyMethodArgumentResolver:处理@RequestBody注解,通过HttpMessageConverter把请求体反序列化成Java对象;ModelAttributeMethodProcessor:处理@ModelAttribute注解,也处理没有注解的复杂对象参数,支持表单绑定的情况。
这里最容易出错的场景有两个。第一个是@RequestBody和@RequestParam混用导致415。当你用@RequestBody接收JSON时,客户端请求的Content-Type必须是application/json,否则Spring MVC会直接返回415 Unsupported Media Type。很多新手前端没设这个请求头,后端口口声声说接口有问题。第二个是参数类型转换失败,比如前端传了"123"而接口定义Integer id,Spring MVC会用ConversionService做类型转换,一旦转换失败,默认抛出MethodArgumentTypeMismatchException,最后返回400。这类问题排查起来非常快,因为异常信息里会带“Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'”这样的明确描述。
还要注意@RequestParam(required = false)和没有注解的小差别。如果你给一个参数加了@RequestParam但没设置required=false,那这个参数默认必传,缺了直接400。而如果不加任何注解,Spring MVC先把参数当成简单类型处理,如果类型不匹配再尝试把整个请求体绑定到复杂对象上。很多老项目里出现“请求字段丢失但接口没报错”的情况,就是因为参数解析器选错了,比如本应取@RequestBody里的字段,结果参数解析器从query string里取,取不到就给了null。
3.3 返回值处理器与@ResponseBody的JSON输出
处理器方法执行完后,ServletInvocableHandlerMethod要决定怎么处理返回值。这一步交给HandlerMethodReturnValueHandler,接口方法也是两个:supportsReturnType和handleReturnValue。返回值处理器同样有一条责任链,常见的处理器有以下几类:
ModelAndViewMethodReturnValueHandler:处理方法返回ModelAndView的情况;RequestResponseBodyMethodProcessor:处理标注了@ResponseBody的方法,直接序列化对象写入响应体;HttpEntityMethodProcessor:处理返回ResponseEntity/HttpEntity的情况,也会直接写响应体;ViewNameMethodReturnValueHandler:处理返回void、且没有写入响应体的情况,把方法里通过Model添加的数据封装成ModelAndView,视图名默认为请求路径;StreamingResponseBodyReturnValueHandler:处理流式输出的情况,比如下载大文件。
重点说一下RequestResponseBodyMethodProcessor。它先判断当前Handler方法的返回值类型上是否有@ResponseBody注解(类级别或方法级别),有就交给它处理。它内部会遍历HttpMessageConverter,找到一个能写目标返回类型的转换器,比如在Spring Boot Web场景下MappingJackson2HttpMessageConverter可以把一个User对象序列化成JSON。写响应体之前,它还会做几件事:设置响应状态码(如果Controller方法用了@ResponseStatus)、设置响应头(如果有Content-Type需求)、把返回对象写入HttpServletResponse的输出流。这个类内部的writeWithMessageConverters方法就是JSON输出最底层的实现,打断点跟一遍,比背十篇博客都理解得深。
反过来说,如果你在Controller方法上忘了加@ResponseBody,返回值是一个普通的User对象,Spring MVC会认为这是一个需要找视图来渲染的对象,于是交给其他返回值处理器,最后得到一个“逻辑视图名”,然后由ViewResolver去解析。这也是为什么很多新手有“返回JSON却跳了个页面”的困惑:本质是返回值处理选错了处理器。
4. 视图解析与响应返回:到底什么时候用ViewResolver
4.1 ModelAndViewContainer的状态判断
经过参数解析和方法调用之后,返回结果最终被封装到ModelAndViewContainer里。这个容器对象有两个标志位特别关键:一个是requestHandled,它表示“请求是否已经被完全处理,不再需要视图渲染”;另一个是view字段,它保存要渲染的视图名或视图对象。
当返回值处理器执行完,ModelAndViewContainer的状态决定了后续流程走向。比如RequestResponseBodyMethodProcessor在写完JSON后会让requestHandled = true,因为响应体已经写出,不需要视图解析了。而此时ModelAndViewContainer里的view字段往往还是null。反过来,一个返回逻辑视图名的方法(比如返回字符串"user/list"),它的requestHandled仍为false,后续processDispatchResult就会尝试用ViewResolver解析视图来渲染。
processDispatchResult是DispatcherServlet里负责“收尾”的方法:如果mv不为null且view还没被渲染,它就从ViewResolver列表里找能解析的Resolver,解析成View对象,再调用view.render()把模型数据渲染进最终页面。这就是整个MVC里“View”的部分。
4.2 返回JSON与返回页面混合场景的处理
同一个Controller里通常既有返回JSON的接口,也有返回页面的方法。在前后端分离不太彻底的传统项目里,这个混合场景很常见。以一个典型的Controller为例:
@Controller @RequestMapping("/user") public class UserController { @GetMapping("/detail") @ResponseBody public User detail(@RequestParam Long id) { return userService.getById(id); } @GetMapping("/list") public String list(Model model) { model.addAttribute("users", userService.listAll()); return "user/list"; } }第一个方法返回JSON,它通过@ResponseBody走完了请求体写入;第二个方法返回视图名,Spring MVC把它交给InternalResourceViewResolver,拼接前缀/WEB-INF/views/和后缀.jsp,得到/WEB-INF/views/user/list.jsp,然后转发渲染。
从结果看,一个是纯数据,一个是HTML页面,两者互不干扰,但底层走的流程完全不同。这提醒我们一个关键点:返回类型的语义是由返回值处理器决定的,不是由方法签名决定的。同样是返回String,加了@ResponseBody就是JSON字符串,不加就是逻辑视图名。想明白这一点,很多让人挠头的现象就都能解释了。
4.3 RestController与Controller对视图解析的差别
@RestController的源码其实就是@Controller和@ResponseBody的组合注解。它等价于类上标注了@Controller,并且该类的每个处理方法默认带上了@ResponseBody。从视图解析的角度看,使用@RestController的类,所有方法返回值都会被当成直接写响应体来处理,不存在视图解析环节。
这个设计在后端接口开发里非常实用。假设一个项目里接口全部返回JSON,那么用@RestController是最干净的写法。但如果某个类是个页面Controller,返回视图名,用@Controller是合适的。一旦混用,比如用@RestController写了一个返回页面的方法,最后前端收到的会是“user/list”这个字符串本身,而不是HTML页面。这个问题我在接手旧项目时见过不止一次,通常是前人从某个Controller复制代码,顺手改了注解但没注意到方法的用途。
还要提一下ResponseEntity。它是HttpEntity的子类,可以在返回结果的同时设置状态码和响应头。当处理器返回ResponseEntity时,HttpEntityMethodProcessor会把它当作请求已处理完,写入响应体。所以哪怕@RestController类下有个方法返回ResponseEntity<String>,也不会走视图解析。用ResponseEntity的好处是灵活控制HTTP状态码,适合RESTful API的设计。
5. 异常处理与兜底逻辑
5.1 HandlerExceptionResolver责任链
请求处理过程中抛出的异常并不会直接变成500错误,Spring MVC会把它送到HandlerExceptionResolver责任链里处理。DispatcherServlet默认注册了三个重要的异常处理器:
ExceptionHandlerExceptionResolver:扫描@ExceptionHandler注解方法,支持在@ControllerAdvice里集中定义异常处理逻辑;ResponseStatusExceptionResolver:处理@ResponseStatus注解标注的异常,直接设置响应状态码;DefaultHandlerExceptionResolver:处理Spring MVC自身抛出的标准异常,比如NoHandlerFoundException、MethodArgumentNotValidException等,转为对应的HTTP状态码。
当处理器方法抛出异常时,doDispatch()会捕获异常,调用processHandlerException,把这个异常交给这几个处理器依次尝试。这里的关键设计是:异常处理器返回的ModelAndView可以是空视图(表示异常已处理,直接渲染),也可以是包含视图名的ModelAndView(表示要渲染一个错误页面),还可以返回null,表示“我处理不了”,让容器继续抛出异常。
ExceptionHandlerExceptionResolver内部的工作方式和RequestMappingHandlerAdapter非常像:它也需要做参数解析和返回值处理,只不过目标方法是@ExceptionHandler标注的异常处理方法。这解释了一个坑:你在@ExceptionHandler方法里返回了一个String,如果没有加@ResponseBody,它会被当成逻辑视图名,页面上显示的是HTTP 500加一堆异常栈,而不是你想要的JSON。所以全局异常处理器返回JSON时,要么在方法上加@ResponseBody,要么把异常类所在类标注为@RestControllerAdvice。
5.2 异常处理常见坑
第一个坑是异常被“半路拦截”。如果在Spring MVC的HandlerInterceptor的preHandle里抛了异常,HandlerExceptionResolver不一定能处理它。因为它可能在真正进入doDispatch()的处理器调用阶段之前就抛出了,如果主流程没有进入processHandlerException分支,异常就顺着容器栈继续往外冒。在实际项目里,权限拦截器抛异常时如果没经过@ExceptionHandler,最后看到的往往是一个Tomcat默认错误页。这时候如果你确实想让全局异常机制兜底,可以在拦截器里 try-catch 或者把异常包装后在afterCompletion阶段处理。
第二个坑是404没有日志。大部分时候404意味着没有对应的HandlerMapping命中,这时处理器调用根本不会发生,当然也没有自定义异常处理器介入的余地。线上排查404时,第一件事是确认请求URL有没有被某个HandlerMapping命中,而不是去翻用户的异常日志。Spring Boot里可以通过server.error.include-message=always来让404响应带上具体信息,辅助定位。
第三个坑是@ControllerAdvice与局部@ExceptionHandler的优先级问题。Spring的规则是:局部异常处理方法(在当前Controller内)优先于全局的@ControllerAdvice。如果你在某个Controller里写了一个范围很大的异常处理方法(比如捕获Exception),它会把全局的异常处理给盖掉。这本身是设计好的行为,但很容易让人困惑:明明全局异常处理器写了,为什么某些接口的异常没有按全局方式返回?顺着优先级检查一遍就能定位。
6. 高频问题速查与线上排查思路
6.1 高频问题速查表
把过去的经验和群里问得最多的问题汇总一下,直接整理了张速查表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 请求没进Controller,返回404 | HandlerMapping没有匹配;静态资源被拦;NoHandlerFoundException未启用 | 看URL是否是静态资源路径,看日志是否有“No mapping for POST” |
POST请求用@RequestParam取不到值 | 请求体格式不是application/x-www-form-urlencoded;前端传的是JSON却没用@RequestBody | 打开浏览器F12看Content-Type和请求体格式 |
@RequestBody返回415 | 请求头Content-Type不是application/json;缺少JSON转换依赖 | 检查请求头、检查pom中是否有jackson-databind |
| 返回JSON却出现了视图名或跳转错误页面 | 方法上没加@ResponseBody;类上用了@Controller而不是@RestController | 检查返回值处理器选的哪个,依据是方法有没有@ResponseBody |
| 拦截器没生效 | 拦截器注册的路径范围不对;preHandle返回了false | 检查WebMvcConfigurer.addInterceptors的路径配置 |
| 异常没有被全局处理 | 异常发生在拦截器阶段;局部@ExceptionHandler覆盖了全局;异常处理器返回了null | 打断点看processHandlerException是否被调用 |
| 前端提交表单后跳回同一页面且是GET | 表单POST的方法没写@PostMapping,Spring MVC按GET处理并返回视图 | 检查URL是否匹配GET方法,反馈的状态码是405 |
这张表覆盖了我在群里答疑时超过八成的问题。大多数问题的根源,都是对“参数解析器选型”和“返回值处理器选型”理解不到位。这两条责任链是Spring MVC请求流程里的灵魂,建议花时间跟源码。
6.2 线上排查的经验
说两个亲测有效的排查思路。
第一个思路是直接在DispatcherServlet.doDispatch()方法入口打断点,然后把HandlerMappings、HandlerAdapters的局部变量列表展开看。比如怀疑某个请求被错误的HandlerMapping处理了,展开mappedHandler就能看到HandlerExecutionChain里到底包着哪个HandlerMethod。如果发现HandlerMethod是自己那个Controller方法,说明HandlerMapping没问题;如果HandlerMethod是个ResourceHttpRequestHandler,说明请求被当成静态资源处理了,这时候根本不是Controller问题,而是路径映射问题。
第二个思路是看异常处理链路的走向。在processHandlerException方法里打断点,观察exception变量和处理结果ModelAndView。如果exception里是NoHandlerFoundException,说明URL从根上就没匹配上任何路由;如果异常处理器最终返回的mav为null,异常会继续抛出到容器,这种情况下自定义的全局异常处理器没有生效,需要往拦截器阶段排查。
第三个建议是开启日志观察“视图解析”阶段。在application.properties中设置logging.level.org.springframework.web.servlet=DEBUG,请求结束后日志会打印Completed 200 OK这样的记录。如果某个请求经历过视图解析,日志里会显示View name 'xxx', model {....},而REST接口通常不会出现这方面的日志。这个差别在排查混合项目问题时特别有用。
还有个小技巧:在Spring Boot中想看当前项目到底注册了哪些HandlerMapping,可以在启动后注入ApplicationContext,用getBeansOfType(HandlerMapping.class)打印出所有HandlerMapping的类和Order。这样可以快速确认自定义HandlerMapping是否和默认组件冲突,也能看出ResourceHandlerMapping是否被正确注册。
6.3 从源码角度给出的避坑清单
写任何Controller方法前先想清楚它属于“返回数据”还是“返回视图”,据此决定用
@RestController还是@Controller,决定要不要加@ResponseBody。前后端分离项目里,建议全局接口层统一使用
@RestController,不要在一个类里混用页面和接口。如果确实要混用,方法上的注解一定要写清楚,避免从@RestController复制过来的代码忘记删掉@ResponseBody。配置
WebMvcConfigurer时,静态资源的放行要放在拦截器addInterceptors之前理清思路,先想明白哪些路径需要被Spring MVC接管,哪些需要交给ResourceHttpRequestHandler。接口参数接收JSON时,优先用DTO对象 +
@RequestBody,不要用Map<String, Object>,这样参数校验、类型转换、字段命名都能更可控。启动时开启spring.jackson相关配置,确认JSON序列化字段命名规则不会和前端不一致。全局异常处理器建议单独新建一个类,用
@RestControllerAdvice,方法返回统一的结构体(比如Result<T>),彻底避免异常处理方法又把String当视图名返回的坑。不要随便在Controller里直接用
HttpServletResponse写数据。一旦你手动写了响应体,requestHandled会被标记为true,后续再返回一个对象,Spring MVC就不会再做视图解析了,这个行为依赖性强,容易因返回值解析器的变化出现意外。如果某个请求迟迟不返回,优先看是不是视图解析没有完成(比如视图名拼错导致跳到一个不存在的页面),而不是先怀疑数据库慢查询。Spring MVC的视图解析阶段确实可能吞掉一部分时间。
尾声
聊到最后,倒不是说要把所有源码都背得滚瓜烂熟才叫“吃透”。那个范围太大了,也没有必要。真正有收获的做法是带着问题去读:遇到一个异常,先看一眼doDispatch()走到哪一步出的问题;再顺着参数解析器和返回值处理器的责任链去定位是选型错误,还是数据格式不对。这套流程就像一张地图,地图上的每个节点是干嘛的、什么时候会被走到,看多了自然就熟了。我自己刚学的时候是用断点一遍一遍断出来的,有时候一个问题要断四五遍,但每断一遍,对Spring MVC的理解就扎实一分。别怕慢,这类基础框架的源码,慢就是快。