
SpringMVC这套东西讲真很多人面试前都能把工作流程背得滚瓜烂熟DispatcherServlet、HandlerMapping、HandlerAdapter、ViewResolver……一溜名词往外蹦。但真到了项目里接口报404了不知道去哪查拦截器不生效了不知道看哪配个前后端分离项目又发现压根走不到视图解析那一步。你能背出流程不代表你理解那条请求到底经过了谁的手、在哪一步可能被拦下来、每一步的输入输出是什么。这篇不打算给你画一张漂亮的标准流程图然后让你背我会把SpringMVC的完整工作链路从源码执行的视角拆开配合Maven构建、IDEA部署Tomcat、拦截器这些实际项目里绕不开的东西讲清楚每一步为什么要存在以及出了问题应该怀疑哪个环节。适合刚学完SSH/Servlet想系统理解SpringMVC的初学者也适合写了几年CRUD但没认真抠过请求链路的同学。1. 一条HTTP请求的完整旅程从浏览器到响应页面的八个环节先花一分钟把整体的链路走一遍。你发一个GET /user/list请求到浏览器最终渲染出页面或者前端收到JSON中间其实穿越了Servlet容器Tomcat、Spring容器、DispatcherServlet、处理器、视图渲染这几个大区域。整体环节可以拆成八步浏览器发起HTTP请求Tomcat根据web.xml或注解配置找到对应的Servlet。请求命中DispatcherServlet它作为前端控制器接收所有请求。DispatcherServlet通过HandlerMapping查找能处理该请求的Handler也就是Controller里的方法。找到Handler后通过HandlerAdapter适配并调用这个Handler。Controller执行业务逻辑返回ModelAndView或直接返回数据。经过HandlerAdapter把结果封装回ModelAndView交给DispatcherServlet。DispatcherServlet把ModelAndView传给ViewResolver进行视图解析。ViewResolver解析出真实视图JSP、Thymeleaf模板等视图渲染后返回HTTP响应。这就是最经典的SpringMVC八步流程。但这八步只是骨架面试背到这里只能算及格真正理解一条请求需要把第3步、第4步内部发生了什么搞清楚以及把第2步之前容器做了什么、第7步之后视图是怎么拼出来的这些边角料也补全。1.1 为什么先看流程而不是先学注解很多人一上来就学Controller、RequestMapping会用了就觉得自己会SpringMVC了。但一旦遇到问题就抓瞎因为注解只是声明真正干活的还是那套流程框架。比如你配了一个RequestMapping(/user/list)SpringMVC怎么知道这个URL对应这个方法这背后是HandlerMapping在起作用。你写一个return user/listSpringMVC怎么知道要跳去哪个JSP这背后是ViewResolver在起作用。流程就是整个框架的地基注解是在地基上盖的房子。先把流程吃透后面遇到任何参数绑定失败、路径匹配不上、返回结果不对这类问题时至少知道该去哪个环节找原因。1.2 核心环节逐段拆解第1步到第2步之间Tomcat和Spring之间还有一次握手Spring容器在Web应用启动时通过ContextLoaderListener初始化这个容器管理Service、DAO等bean而DispatcherServlet自身也会创建一个子容器SpringMVC容器管理Controller、HandlerMapping等web层bean。子容器可以访问父容器的bean反过来不行。这是很多配置问题的根源后面第6章会细讲。第3步和第4步是整个流程的发动机HandlerMapping从org.springframework.web.servlet.handler.AbstractHandlerMapping这条继承链出发经过RequestMappingHandlerMapping把RequestMapping的路径映射关系缓存起来然后匹配获取执行链。值得留意的是它返回的不一定是Controller对象而是一个HandlerExecutionChain这个链里除了Handler还有拦截器。这意味着流程中拦截器是在第3步就挂上钩的。第5步到第8步就是典型的小马拉大车Controller方法返回值可能是String、ModelAndView、ResponseEntity最终都会被适配成ModelAndView然后视图解析器根据viewName找模板渲染输出。前后端分离后这一步被弱化了但理解它仍然有助于理解SpringMVC的设计哲学。1.3 流程图之外的三个隐含环节标准流程图不会告诉你这些但实际运行中它们参与度极高异常处理环节Controller抛出的异常在哪被捕获流程永远不会把异常直接抛给Servlet容器而是交给DispatcherServlet的processDispatchResult方法去问HandlerExceptionResolver。所以你有ControllerAdviceExceptionHandler时全局异常处理是在这一步介入的。数据绑定与参数解析HandlerAdapter在调用Controller方法之前要做参数解析、数据绑定、校验。流程图的第4步和第5步之间其实藏了HandlerMethodArgumentResolver这一整组组件。异步处理SpringMVC 3.2开始支持Callable和DeferredResult请求会被挂起等异步任务完成后由容器回调继续执行第7步之后的流程。流程图上没这条分支但你做长连接推送时一定会遇到。2. DispatcherServlet凭什么当总指挥初始化与请求分发的核心职责整个流程的心脏就是DispatcherServlet它是一个标准的HttpServlet但它做的不是传统Servlet那种doGet/doPost处理业务而是把所有请求收拢进来再派发给Spring容器里的各种组件。这个设计叫前端控制器模式Front Controller Pattern好处是所有请求都经过一个门面公共逻辑编码处理、权限拦截、日志记录都可以集中在这里处理。2.1 初始化时偷偷装配了九大组件DispatcherServlet的继承关系是DispatcherServlet extends FrameworkServlet extends HttpServletBean extends HttpServlet。在HttpServletBean的init()方法里先把Servlet的init-param参数绑定到当前bean的属性上然后FrameworkServlet.initServletBean()会创建SpringMVC的WebApplicationContext最后DispatcherServlet.onRefresh()调用initStrategies()方法这个方法一口气初始化了九个策略组件MultipartResolver处理文件上传解析LocaleResolver国际化区域解析ThemeResolver主题样式解析HandlerMappings处理器映射器HandlerAdapters处理器适配器HandlerExceptionResolvers异常解析器RequestToViewNameTranslator请求到视图名的翻译器ViewResolvers视图解析器FlashMapManager重定向Flash数据管理器这九个组件全部是从当前WebApplicationContext里按类型查找的查不到就使用默认配置。也就是说你可以替换其中任意一个组件的实现这给了框架巨大的扩展空间。理解这一点你再看配置文件里写的mvc:annotation-driven/——它干的事之一就是把RequestMappingHandlerMapping和RequestMappingHandlerAdapter这些注解驱动的组件注册进容器。2.2 从请求进来讲起doDispatch到底做了什么请求进来后DispatcherServlet的doService()方法先给request设置一些属性如org.springframework.web.servlet.DispatcherServlet.CONTEXT然后快速转发给doDispatch()方法。这个方法就是整个SpringMVC工作流程的源代码级实现值得花时间把它的执行顺序捋清楚// 简化后的doDispatch核心流程 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest request; HandlerExecutionChain mappedHandler null; boolean multipartRequestParsed false; WebAsyncManager asyncManager WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv null; Exception dispatchException null; try { processedRequest checkMultipart(request); multipartRequestParsed (processedRequest ! request); // 步骤1通过HandlerMapping找到HandlerExecutionChain mappedHandler getHandler(processedRequest); if (mappedHandler null || mappedHandler.getHandler() null) { noHandlerFound(processedRequest, response); return; } // 步骤2通过HandlerAdapter找到能操作这个Handler的适配器 HandlerAdapter ha getHandlerAdapter(mappedHandler.getHandler()); // 步骤3执行拦截器的preHandle方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 步骤4真正调用Controller方法返回ModelAndView mv ha.handle(processedRequest, response, mappedHandler.getHandler()); // 步骤5默认视图名处理 if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); // 步骤6执行拦截器的postHandle方法 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException ex; } catch (Throwable err) { dispatchException new NestedServletException(Handler dispatch failed, err); } // 步骤7处理结果包括异常解析、视图渲染 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } }看到没真正的流程其实就是getHandler→getHandlerAdapter→applyPreHandle→handle→applyPostHandle→processDispatchResult外加穿插在其中的triggerAfterCompletion。你背的那些步骤全部浓缩在这里。2.3 为什么DispatcherServlet必须映射到/配置过web.xml的同学都知道servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping/和/*的区别是被问烂的面试题也是实践里特别容易踩的坑。/会匹配除JSP之外的请求路径Tomcat默认的JspServlet处理*.jsp所以SpringMVC不碰JSP。而/*会匹配所有请求包括JSP如果用它映射DispatcherServlet连JSP都会被DispatcherServlet拦下来然后进入HandlerMapping找不到对应的Handler返回404。更隐蔽的问题是静态资源CSS、JS、图片如果也走DispatcherServletHandlerMapping同样找不到处理器你会看到页面样式全丢。解决办法是额外配置静态资源映射mvc:resources mapping/static/** location/static//或者直接用WebMvcConfigurer.addResourceHandlers方法配置。这类问题一旦出现第一反应不是查代码而是先确认DispatcherServlet的url-pattern是不是配错了。3. HandlerMapping与HandlerAdapter怎么找到处理器又怎么让处理器跑起来流程的第3、4步是最容易被轻描淡写带过的但恰恰是整个框架最精妙的部分。HandlerMapping解决请求对应哪个Handler的问题HandlerAdapter解决Handler怎么被调用的问题。为什么需要两个组件而不是一个因为SpringMVC要兼容多种风格的处理器既有基于注解的RequestMapping方法也有传统的Controller接口实现实现handleRequest方法还有HttpRequestHandler。它们各自的调用方式完全不同HandlerAdapter的存在就是把这些差异包装起来让DispatcherServlet不关心处理器长什么样。3.1 HandlerMapping不是一个映射而是一条查找链在容器里HandlerMapping实际上是支持配置多个的它们按顺序组成一条查找链。DispatcherServlet在初始化时会收集所有HandlerMapping bean排序后存进handlerMappings列表。每次请求来了遍历列表逐个调用getHandler(request)方法返回非空就停止。默认启用的是两个RequestMappingHandlerMapping处理RequestMapping注解优先级最高。SimpleUrlHandlerMapping处理URL模式到Controller的显式映射通常用于静态资源、页面跳转这类场景。RequestMappingHandlerMapping在初始化的时候会扫描容器里的所有bean把标注了Controller或RequestMapping的方法注册成HandlerMethod并建立路径模板到HandlerMethod的映射表。它内部用了PathPatternSpring 5.3或AntPathMatcher做路径匹配所以你的请求路径是/user/*、/user/**、/user/{id}都能精确匹配还能从路径里提取模板变量。这一步的执行结果返回的不是Handler本身而是HandlerExecutionChain继续看它的判断逻辑如果查到了匹配的Handler就把注册好的拦截器逐个包成链。拦截器是流程里独立的一层后面第5章专门展开。3.2 HandlerAdapter适配器模式在SpringMVC里的教科书级应用某个Handler匹配上之后DispatcherServlet要做的事情很纯粹调用它。但调用细节因Handler类型而异对于HandlerMethod注解式Controller方法需要解析方法参数、处理返回值、处理注解。对于Controller接口实现只需要调用handleRequest参数解析和返回值处理都省了。对于HttpRequestHandler由它自己处理请求和响应。DispatcherServlet不可能内置所有调用逻辑所以引进了适配器。DefaultAnnotationHandlerMapping对应AnnotationMethodHandlerAdapterRequestMappingHandlerMapping对应RequestMappingHandlerAdapter。初始化时同样会收集所有HandlerAdapter调用supports(handler)方法逐一判断哪个适配器能处理该Handler。RequestMappingHandlerAdapter的核心逻辑在invokeHandlerMethod方法里它会创建ServletInvocableHandlerMethod并装配一组参数解析器HandlerMethodArgumentResolver和返回值处理器HandlerMethodReturnValueHandler。Controller方法的参数都是在这里被一个个解析出来的。比如你写RequestParam(name) String name实际就是某个ArgumentResolver从request里getParameter(name)取值再绑定。你写RequestBody User user就是HttpMessageConverter把JSON反序列化成User对象。3.3 少了适配器会怎样从报错反推设计动机我遇到过不少同学在配置SpringMVC时把mvc:annotation-driven/删了项目立刻大面积报错核心日志是HttpMediaTypeNotSupportedException: Content type application/json;charsetUTF-8 not supported或者java.lang.IllegalArgumentException: No adapter for handler ...原因就是缺少RequestMappingHandlerAdapter的自动注册导致参数解析器、消息转换器、返回值处理器都没有初始化。这恰好反过来说明适配器不是可有可无的装饰品它是把请求数据转成Java方法参数、再把Java返回值转成HTTP响应的关键装配层。调试技巧在DispatcherServlet.doDispatch()的ha.handle()那行打断点进入RequestMappingHandlerAdapter.invokeHandlerMethod一路看参数解析器是怎么把request数据变进方法参数里的。这会让你对参数绑定的理解比看十篇博客都深。4. 视图解析与渲染ModelAndView如何变成浏览器里的最终HTML如果项目是前后端分离的这一步你接触得少但它依然是SpringMVC工作流程的标准收尾动作。在经典服务端渲染项目中Controller方法返回一个字符串逻辑视图名这个字符串会被ViewResolver解析成真实视图再渲染成HTML。4.1 视图解析器的选择逻辑DispatcherServlet持有的ViewResolver也是一个列表遍历列表逐个尝试解析。解析结果是View对象每个View都有render(MapString, ? model, HttpServletRequest request, HttpServletResponse response)方法。常见的解析器组合视图解析器特点InternalResourceViewResolver专为JSP设计把逻辑视图名拼成JSP物理路径ThymeleafViewResolver解析Thymeleaf模板常用于Spring Boot项目FreeMarkerViewResolver解析FreeMarker模板ContentNegotiatingViewResolver根据请求头Accept、URL后缀等协商选择视图类型以最常见的InternalResourceViewResolver为例配置通常是bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean你返回user/list它拼成/WEB-INF/jsp/user/list.jsp再交给JspServlet渲染。这也是为什么JSP要放在WEB-INF目录下——外面的请求直接访问不到必须经过Controller转发既安全又符合流程。4.2 转发与重定向在流程中的位置差异这里有个细节值得展开return user/list是转发return redirect:/user/list是重定向。两者在流程中的行为完全不同。转发时DispatcherServlet拿到的是同一个request和response视图渲染就是RequestDispatcher.forward()浏览器地址栏不变一次请求搞定。重定向时ViewResolver解析出来的不是渲染视图的View而是一个RedirectView它的render方法执行response.sendRedirect()浏览器收到302后重新发一次全新请求。所以重定向后第一次请求的request属性天然丢失只能通过URL参数或FlashMap传数据。这也是为什么FlashMapManager这个组件会出现在九大组件列表里——它是专为重定向场景准备的数据传递通道。4.3 前后端分离后视图解析环节还剩多少如果你是在做纯API项目Controller直接返回ResponseBody或者ResponseEntity这时流程会走到RequestResponseBodyMethodProcessor它本质上是一个返回值处理器通过HttpMessageConverter把Java对象直接序列化成JSON写进response。整个过程不经过ViewResolver或者说ViewResolver这层被跳过了。但这不代表视图解析不存在了它只是让位。SpringMVC的返回值处理机制里HandlerMethodReturnValueHandler接口的职责就是怎么处理返回值。返回String但方法上有ResponseBody注解时走的是消息转换返回String但没加注解时走的是视图名解析。同一个返回值不同路径这种设计才是SpringMVC弹性的来源。5. 拦截器在这条流程链上的插队位置执行顺序、生效时机与常见陷阱SpringMVC的拦截器是工作流程中必然会经过的一环前提是你配置了也是项目中做登录校验、日志记录、权限控制最常用的手段。热搜词里的springmvc拦截器说明它确实是大家关注的高频话题。这里把拦截器的执行时机、执行顺序和常见坑一次说清楚。5.1 拦在哪一步preHandle/postHandle/afterCompletion的时间线HandlerInterceptor接口定义了三个方法三者在流程链上的位置完全不同preHandle在Controller方法执行前调用。返回true继续返回false中止后续流程。此时还没进入Controller适合做权限校验、参数预处理。postHandle在Controller方法执行后、视图渲染前调用。此时ModelAndView已经生成但还没渲染成HTML可以往Model里追加公共数据或者做响应数据的统一包装。afterCompletion在视图渲染完成后调用无论是否抛出异常都会执行除非preHandle返回false被短路。适合做资源清理、日志收尾。一张时间线图可以这样理解用文字版表示请求进入 - 拦截器1.preHandle - 拦截器2.preHandle - Controller方法 - 拦截器2.postHandle - 拦截器1.postHandle - 视图渲染 - 拦截器2.afterCompletion - 拦截器1.afterCompletion注意postHandle和afterCompletion的执行顺序是逆序的也就是说拦截器定义的顺序只对preHandle生效后两个方法像栈一样弹栈执行。原因很简单后注册的拦截器更接近Controller它的postHandle应该先于外围拦截器执行。5.2 HandlerInterceptor与Filter的区别以及为什么经常被搞混这个点我几乎每次带人都会强调。Filter是Servlet规范的东西只依赖Servlet容器不依赖Spring。HandlerInterceptor是SpringMVC框架的一部分它依赖DispatcherServlet的流程。从生命周期上看Filter在请求进入Servlet容器时触发早于DispatcherServlet用的还是Servlet的doFilter机制HandlerInterceptor则位于DispatcherServlet内部出现在HandlerMapping返回执行链之后。两者都能做请求拦截但Filter能拦的是到达DispatcherServlet之前的一切包括静态资源、JSPHandlerInterceptor只能拦经过DispatcherServlet分配的请求静态资源如果被单独映射了比如mvc:resources默认不会触发拦截器。注意如果你想在拦截器里读取请求体比如统一鉴权某些字段要小心getInputStream()只能读一次的问题。RequestBody一旦被拦截器消费Controller里的RequestBody就会拿到空对象。解决办法是用ContentCachingRequestWrapper包装请求或者在Controller里改成RequestParam。这种坑在前后端分离项目里特别常见。5.3 拦截器不生效的三个典型原因拦截器踩坑概率极高我遇到过的问题集中在三处没有在配置类或XML中注册拦截器。只写一个实现HandlerInterceptor的类放容器里是没用的必须通过WebMvcConfigurer.addInterceptors()注册或者XML配置mvc:interceptors并指定拦截路径。拦截路径写错。注册时如果不指定path默认匹配所有路径。指定了/user/**就只能拦以/user/开头的请求斜杠写法要和HandlerMapping的路径匹配规则一致。静态资源把拦截器绕过了。如果你用的是REST接口 Vue/React前端前端页面资源放在静态目录通常希望拦截器只作用在API上不拦静态资源。这时要显式排除静态路径否则就是反过来的问题——拦得太多页面加载被卡住。排查路径先确认Interceptor类是否被Spring容器扫描到再确认注册配置类是否被扫描到最后检查addPathPatterns和excludePathPatterns的路径写法。万无一失的做法是在preHandle里加一行日志看看到底有没有进来。6. 从流程图到可运行项目基于Maven构建、IDEA配置Tomcat的落地链路流程讲得再好项目跑不起来等于白讲。热词里有人搜基于maven构建的springmvc模块化项目 通过idea配置tomcat容器启动说明不少人卡在了从理论到落地的这一步。这里给出一套完整的、可以直接照着做的搭建链路。6.1 pom.xml里到底需要哪些依赖一个干净的SpringMVC项目即使在Spring Boot大行其道的今天也还有它的舞台存量项目、课设、以及对Spring Boot启动黑盒不放心的人Maven依赖这样写properties spring.version5.3.39/spring.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- Servlet APITomcat提供实现 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- Jackson做JSON序列化 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency /dependenciesservlet-api一定要用provided作用域让Tomcat启动时用自己的Servlet实现否则可能出冲突。有同学问过JSTL要不要加如果你用JSP做视图需要追加javax.servlet:jstl:1.2不用JSP就不用加。如果要做模块化项目可以把工程拆成parent、common、web等多个moduleweb模块最终打成war包依赖关系通过Maven的module依赖维护。核心点web模块的pom要声明打包方式为war父pom里统一管理依赖版本避免各模块版本漂移。6.2 配置DispatcherServlet的两种姿势传统方式是把DispatcherServlet配在web.xml里web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener /web-appload-on-startup设为1让容器启动时就初始化DispatcherServlet否则第一个请求会特别慢。Servlet 3.0也支持纯Java配置用AbstractAnnotationConfigDispatcherServletInitializerpublic 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[]{/}; } }纯Java配置更现代也更容易在Spring Boot之间切换。getRootConfigClasses返回根容器配置Service、DAOgetServletConfigClasses返回MVC容器配置Controller、视图解析器、拦截器。6.3 IDEA里配置Tomcat的三个细节在IDEA里跑SpringMVC项目配置Tomcat时我见过太多次翻车一次说清关键点部署选war包还是war exploded日常开发选war exploded模式秒级热部署不用每次改代码都重新打war包。点开Run/Debug Configurations → Deployment → Add → Artifact选择项目名:war exploded。Application context的命名Application context就是你项目的访问根路径。如果填/ssm_demo启动后访问地址是http://localhost:8080/ssm_demo/user/listController的RequestMapping(/user/list)前面会自动带上这个context path。如果填/则直接http://localhost:8080/user/list。Tomcat端口和JMX端口别冲突8080端口被占用的概率极大改的时候顺手把9005这类JMX端口也一起改掉。IDEA里Server标签页的JMX port默认9005具体随版本而定不改成别的值很容易起冲突。还有一个隐形坑如果使用Tomcat 10Servlet API包名从javax.servlet变成了jakarta.servlet依赖坐标也要跟着换。老项目迁移到Tomcat 10经常卡在IDE编译通过但启动报ClassNotFoundException: javax.servlet.Filter。要么降回Tomcat 9要么把项目的servlet依赖全部迁移到jakarta.servlet:jakarta.servlet-api。7. 跑不起来时对照这张表流程各环节报错与排查思路遇到问题不要慌按工作流程的环节反推定位速度会快很多。这里把SpringMVC项目最常见的几类故障按环节归类并给出排查路径。异常表现故障环节排查思路404Tomcat欢迎页能打开但接口404路径映射环节确认Controller上有没有RequestMapping确认类是否被扫描确认请求URL与映射是否完全一致确认web.xml或初始化类的Servlet映射是否为/405 Method Not AllowedHandlerMapping匹配成功但方法不允许确认请求方式GET/POST与RequestMapping的method属性是否匹配注意重定向后的第二次请求往往是GET500 No adapter for handlerHandlerAdapter环节确认是否配置了mvc:annotation-driven/或者Java配置是否加了EnableWebMvc500 HttpMessageNotReadableException参数解析/消息转换环节检查JSON格式、Content-Type请求头缺jackson依赖会导致反序列化失败500 视图解析失败ViewResolver环节确认视图路径是否存在前缀后缀拼接是否正确确认JSP是否放在WEB-INF下且被正确访问拦截器不执行拦截器注册环节检查拦截器是否被Spring扫描、是否在注册类里添加确认路径匹配是否把请求包含在内静态资源404DispatcherServlet映射/静态资源配置确认mvc:resources或addResourceHandlers是否配置删除与context path冲突的目录命名JSON数据返回中文乱码消息转换的编码配置在MappingJackson2HttpMessageConverter里设置UTF-8或在RequestMappingHandlerAdapter里统一配置消息转换器排查工具方面调试SpringMVC流程最有效的方法是打断点看DispatcherServlet.doDispatch()的执行路径。它走到哪个分支报错就说明故障在哪个环节。比e.printStackTrace()盲猜高效得多。还有一个经验分享很多404其实发生在Tomcat层面而不是SpringMVC层面。用浏览器的请求体看返回体如果返回的404页面是Tomcat默认的错误页比较朴素的那种说明请求压根没进到DispatcherServlet如果返回的是一个带Spring标志或者JSON格式的404说明请求已经穿过DispatcherServlet但没找到Handler。这个细节能帮你快速定位问题到底在容器还是框架。另一个常见问题是开发环境正常、部署到服务器404。这类问题十有八九是context path不同导致的。本地IDEA配的Application context是/ssm_demo部署到Tomcat的war包名可能是ROOT.war或demo.war路径前缀不同API签名就不同。建议把所有访问路径都加上项目名前缀或者统一使用相对路径别写死本地环境。最后再补一句实践层面的心得。我见过太多人把SpringMVC工作流程当成纯面试题背得滚瓜烂熟但代码里连最基本的异常处理、参数校验都没做。真实项目里工作流程的意义不在于让你背出八步而在于帮你建立请求责任链的意识每个请求要经过哪些关卡每道关卡负责什么事哪个关卡出了问题应该去找谁。按照这个链路去排查问题99%的SpringMVC故障都能在两杯咖啡的时间内定位。如果你现在正好被某个404或者拦截器问题折磨别急着改代码打开调试器把断点拖到DispatcherServlet.doDispatch里走一遍流程你会发现自己以前对SpringMVC的理解还是太浅了。